前言

上一篇攻略中,我们已经充分理解了不带指针的类的设计原则,并且还从标准库设计大师的作品里收获了不少功力。而这一篇攻略,将继续完成基于对象的类的关卡,解决这一关的最后一个问题,那就是带指针的类。在这途中,我们将会遇到知名的 BIG THREE ,剖析 BIG THREE 的设计原则!

带指针的类

一个带指针的类,我们可以想到很多,但是 String 类一定是一个最经典的例子。在这里,我们自己设计一个 String 类,看看带指针的类在设计过程中到底需要注意什么情况。source code

class String
{
public:
String(const char *cstr = 0);
String(const String &str);
String& operator = (const String &str);
~String();
char *get_c_str() const { return m_data; }
private:
char *m_data;
};

一些人可能会问,这个类的实现也可以不使用指针,直接声明一个数组来存储字符不就行了么?使用数组当然可以,不过这样的设计很low,比较明显的一个理由就是我们在一开始并不知道字符串的长度多长,太少则浪费空间,太长则导致溢出。在这种情况下,使用指针是非常好而且非常普遍的,在32位的操作系统中,一个指针的大小是4个字节,因此一个 String 对象的大小十分小,对于效率的提升非常大。

BIG THREE

我们常说的 BIG THREE 就是如下三个:

  String(const String &str);
String& operator = (const String &str);
~String();

分别是拷贝构造函数、拷贝赋值函数和析构函数。那么问题来了,在上一篇攻略中,我们的 complex 类为什么没有 BIG THREE 呢?其实也有的,只是我们没有显式地写出来,而是使用的编译器提供的 BIG THREE。以下我们逐一分析为什么要使用它们,为什么不能使用默认的 BIG THREE。

接下来我们考虑使用者会怎么使用我们的 String 类。首先能够想到的就是构造函数,这部分同上一篇攻略类似,考虑下参数、默认参数以及初始化列表的问题:

String::String(const char *cstr)
{
if(cstr) {
m_data = new char[strlen(cstr) + 1];
strcpy(m_data, cstr);
} else {
m_data = new char[1];
*m_data = '\0';
}
}

对于一个带指针的类,最为麻烦,而且也是最需要关注的点就是如何进行赋值,赋值的方法有如下几种:

String s1("hello");
String s2(s1);
String s3 = s1;
s3 = s1;

对于

String s2(s1);

s2是第一次出现,因此会调用构造函数,而参数是 String 对象,因此会调用 BIG THREE 的第一个函数,拷贝构造函数。

String::String(const String &str)
{
m_data = new char[strlen(str) + 1];
strcpy(m_data, str.m_data);
}

拷贝构造函数也是构造函数,它的特征非常明显,就是参数const String &str 为同类对象,这个函数比较简单,我们就不多嘴了。一些朋友可能会对下面这个等式产生疑问,

String s3 = s1;

其实这个用法和String s3(s1);完全相同,同样也是调用拷贝构造函数。那么我们为什么需要手写,而不能使用默认的拷贝构造函数呢?这是由指针导致的。一个默认的拷贝构造函数大概长这样:

String::String(const String &str)
{
m_data = str.m_data;
}

这样的构造函数会造成大麻烦,这涉及到深浅拷贝的问题。默认的拷贝函数是浅拷贝:



s1和s2都指向同一份字符串,当这份字符串被释放了,就会导致野指针问题,但是这并不是我们想要的,我们需要的是深拷贝:

接下来是

s3 = s1;

这种情况的赋值是操作在两个已存在的对象上,这就不关构造函数任何事了。我们同样需要s3和s1指向两份不同的字符串,而默认的拷贝赋值函数会是这样的:

String& String::operator=(const String& str)
{
this.m_data = str.m_data;
return *this;
}

这样造成的后果就是,两个对象指向的还是同一份字符串,这是很不好的,因此我们必须手写拷贝赋值函数,也就是操作符重载函数:

String& String::operator=(const String& str)
{
if(this == &str) return *this;
delete[] m_data;
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
return *this;
}

我们的函数首先判断是不是自我赋值s1 = s1;,如果是自我赋值,那么直接返回自己。一般情况下,我们必须首先杀死自己,然后再重新分配一片内存,再将字符填进去。这样就完成了我们的赋值函数。

最后是我们的析构函数函数,同样地,我们不使用默认的析构函数,而是自己写一个析构函数,那是因为默认的析构函数只会回首指针占用的4个字节的空间,而指针指向的空间却没有回收,导致内存泄漏:



因此我们必须要手动回收字符串占有的空间,写出我们的析构函数:

String::~String()
{
delete[] m_data;
}

总结

这一篇攻略我们讨论了拷贝构造函数、拷贝赋值函数和析构函数,以及默认的 BIG THREE 。在带指针的类中,我们必须要手写 BIG THREE ,这样做的原因在于编译器默认给我们的拷贝赋值的方法是浅拷贝的方法,而我们需要深拷贝。在对象结束生命时,我们需要手动回收分配的空间,以免造成内存泄漏。

这一篇攻略就到此为止,接下来我们将深度分析分配空间的种种问题,包括 new delete 操作以及内存空间管理的浅层问题。

Reference

C++面向对象高级编程, 侯捷.

[CPP] Big Three的更多相关文章

  1. 使用“Cocos引擎”创建的cpp工程如何在VS中调试Cocos2d-x源码

    前段时间Cocos2d-x更新了一个Cocos引擎,这是一个集合源码,IDE,Studio这一家老小的整合包,我们可以使用这个Cocos引擎来创建我们的项目. 在Cocos2d-x被整合到Cocos引 ...

  2. Json CPP 中文支持与入门示例

    在每一个Json Cpp自带*.cpp文件头加上: #include "stdafx.h" 将Json Cpp对自带的头文件的引用修改为单引号方式,例如json_reader.cp ...

  3. cpp 调用python

    在用cpp调用python时, 出现致命错误: no module named site  ,  原因解释器在搜索路径下没有找到python库.可以在调用Py_Initialize前,调用 Py_Se ...

  4. nginx+fastcgi+c/cpp

    参考:http://github.tiankonguse.com/blog/2015/01/19/cgi-nginx-three/ 跟着做了一遍,然后根据记忆写的,不清楚有没错漏步骤,希望多多评论多多 ...

  5. APM程序分析-ArduCopter.cpp

    该文件是APM的主文件. #define SCHED_TASK(func, rate_hz, max_time_micros) SCHED_TASK_CLASS(Copter, &copter ...

  6. APM程序分析-AC_WPNav.cpp

    APM程序分析 主程序在ArduCopter.cpp的loop()函数. /// advance_wp_target_along_track - move target location along ...

  7. Dev Cpp 输出中文字符问题

    最近 c++ 上机作业,vc++6.0 挂了没法用,只好用 Dev Cpp 先顶替一下,然而在遇到输出中文字符的时候出现了乱码的情况,但这种情况又非常诡异.于是简单了解了一下写成此博客. [写在前面] ...

  8. 【安卓】aidl.exe E 10744 10584 io_delegate.cpp:102] Error while creating directories: Invalid argument

    这几天在使用.aidl文件的时候eclipse的控制台总是爆出如下提示: aidl.exe E 10744 10584 io_delegate.cpp:102] Error while creatin ...

  9. Identify Memory Leaks in Visual CPP Applications —— VLD内存泄漏检测工具

    原文地址:http://www.codeproject.com/Articles/1045847/Identify-Memory-Leaks-in-Visual-CPP-Applications 基于 ...

  10. 估计PI——OpenCV&Cpp

    来源:<Learning Image Processing With OpenCV> 算法原理:蒙特卡洛 PI的计算公式: Cpp代码: #include <opencv2/open ...

随机推荐

  1. pssh执行本地文件(脚本)

    场景:目标命令中含有特殊符号,导致pssh批量执行可能出问题. 用法: pssh -h RemoteHosts.ip -P -I < ~/LocalScript.sh

  2. yum初识

    yum仓库中的元数据文件: primary.xml.gz 所有RPM包的列表: 依赖关系: 每个RPM安装生成的文件列表: filelists.xml.gz 当前仓库中所有RPM包的所有文件列表: o ...

  3. PostSharp 结合 log4net 自动记录日志

    环境: VS 2012 PostSharp-4.1.28  (下载地址)https://visualstudiogallery.msdn.microsoft.com/a058d5d3-e654-43f ...

  4. Devexpress GridControl无限高度惹得祸

    异常提示: issue, place the grid into a container that will give a finite height to the grid, or manually ...

  5. 当我们在谈论multidex65535时,我们在谈论什么

    本文来自网易云社区 作者:郑文 首先我们并不在讨论车牌号.本文尽量避免谈论重复的技术点,只探讨一下multidex提供给我们的技术启示. 原理 multidex技术原理可以分成两个部分: 在app启动 ...

  6. Kubernetes 集群安装部署

    etcd集群配置 master节点配置 1.安装kubernetes etcd [root@k8s ~]# yum -y install kubernetes-master etcd 2.配置 etc ...

  7. WPF 内部Template 动画板 无法冻结此 Storyboard 时间线树供跨线程使用

    解决此问题,需要一定的想象力. 换个思路即可 大体是 使用Tag或者别无用的可以输入数值的属性,或者附加属性也可以的.来绑定到你要动画的属性 当然这个过程中要使用转换器了 我给出一个关于Button ...

  8. Remoteland HDU - 4196

    题意: 给出一个n,在[1, n] 中挑选几个不同的数相乘,求能的到的最大完全平方数 解析: 最大的肯定是n!, 然后n!不一定是完全平方数 (我们知道一个完全平方数,质因子分解后,所有质因子的质数均 ...

  9. 【Oracle 12c】CUUG OCP认证071考试原题解析(35)

    35.choose the best answer View the Exhibit and examine the description of the EMPLOYEES table. Evalu ...

  10. CentOS运行C++语言的Hello World

    1,编写代码,hello.cpp #include <iostream> using namespace std; int main(){ cout<<"hello ...