[CPP] Big Three
前言
上一篇攻略中,我们已经充分理解了不带指针的类的设计原则,并且还从标准库设计大师的作品里收获了不少功力。而这一篇攻略,将继续完成基于对象的类的关卡,解决这一关的最后一个问题,那就是带指针的类。在这途中,我们将会遇到知名的 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的更多相关文章
- 使用“Cocos引擎”创建的cpp工程如何在VS中调试Cocos2d-x源码
前段时间Cocos2d-x更新了一个Cocos引擎,这是一个集合源码,IDE,Studio这一家老小的整合包,我们可以使用这个Cocos引擎来创建我们的项目. 在Cocos2d-x被整合到Cocos引 ...
- Json CPP 中文支持与入门示例
在每一个Json Cpp自带*.cpp文件头加上: #include "stdafx.h" 将Json Cpp对自带的头文件的引用修改为单引号方式,例如json_reader.cp ...
- cpp 调用python
在用cpp调用python时, 出现致命错误: no module named site , 原因解释器在搜索路径下没有找到python库.可以在调用Py_Initialize前,调用 Py_Se ...
- nginx+fastcgi+c/cpp
参考:http://github.tiankonguse.com/blog/2015/01/19/cgi-nginx-three/ 跟着做了一遍,然后根据记忆写的,不清楚有没错漏步骤,希望多多评论多多 ...
- APM程序分析-ArduCopter.cpp
该文件是APM的主文件. #define SCHED_TASK(func, rate_hz, max_time_micros) SCHED_TASK_CLASS(Copter, &copter ...
- APM程序分析-AC_WPNav.cpp
APM程序分析 主程序在ArduCopter.cpp的loop()函数. /// advance_wp_target_along_track - move target location along ...
- Dev Cpp 输出中文字符问题
最近 c++ 上机作业,vc++6.0 挂了没法用,只好用 Dev Cpp 先顶替一下,然而在遇到输出中文字符的时候出现了乱码的情况,但这种情况又非常诡异.于是简单了解了一下写成此博客. [写在前面] ...
- 【安卓】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 ...
- Identify Memory Leaks in Visual CPP Applications —— VLD内存泄漏检测工具
原文地址:http://www.codeproject.com/Articles/1045847/Identify-Memory-Leaks-in-Visual-CPP-Applications 基于 ...
- 估计PI——OpenCV&Cpp
来源:<Learning Image Processing With OpenCV> 算法原理:蒙特卡洛 PI的计算公式: Cpp代码: #include <opencv2/open ...
随机推荐
- 经典的兔子生兔子问题(C#递归解法)
古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 思路:先求出每个月新增的兔子,再用循环求和即可算出这个月 ...
- Javascript 535种方式!!!实现页面重载
原文地址: http://www.phpied.com/files/location-location/location-location.html 完全出于好玩,竟然有人整理了500多种方法来实现刷 ...
- 「HNOI 2016」 序列
\(Description\) 给你一个序列,每次询问一个区间,求其所有子区间的最小值之和 \(Solution\) 这里要用莫队算法 首先令\(val\)数组为原序列 我们考虑怎么由一个区间\([l ...
- iOS 模拟器网络设置
在 iOS Simulator 下,点上面菜单栏最左面的苹果标志,第三项 网络设置,然后先最后面一项,网络设置. 然后在网络设置界面下可以开启或关闭网络.
- Javascript实例 -- 计时器, 搜索框,selected联动
计时器: <body> <input type="text" id="i1"> <input type="button& ...
- c++实验6 递归
1 利用递归设计此函数. int p(int a,int b) { if(a>=b) ; else ; } //粘贴测试数据及运行结果: //测试数据 int main() { cout < ...
- 微信小程序二维码是无法识别二维码跳转到小程序
今天测试了一下,微信小程序圆形二维码是不能直接识别跳转到小程序: 但h5页面的那种微信公众号二维码是可以直接识别
- “全栈2019”Java异常第二十一章:finally不被执行的情况
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java异 ...
- 【timeisprecious】【JavaScript 】JavaScript对象
JavaScript 对象 var a=[];console.log(new Boolean(a)); VM1319: Boolean {true} undefined var a=[];consol ...
- robot framework学习笔记之十-模板
测试模板可以让关键字驱动测试用例转换为数据驱动测试用例.鉴于普通测试用例是由关键字和可能的参 数组成,使用了模板的测试用例只需要定义模板关键字的参数即可