[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 ...
随机推荐
- solr特点三: QueryElevation(编辑结果排序)
在理想的情况下,搜索引擎只返回与用户查询相关的文档.而在现实的查询中,编辑(没发现更合适的表达)通常需要指定特定文档在搜索结果中的特定位置.这样做有很多原因.或许 “置顶” 的文档就是最好的查询结果. ...
- 随手记录: MVC自定义提交form
function mySubmit() { var frm = $('#frm'); var result = frm.valid(); if (ret) { frm.submit(); } else ...
- 手动安装httpd服务器
首先安装apr(Apache Portable Runtime) apr-util apr-iconv 安装之前需要 前置知识: 自己手动编译安装的软件的安装位置: /usr/local bin, s ...
- ADO.NET批量添加数据到SQL Server—BulkCopy使用指南
BulkCopy位于System.Data.SqlClient命名空间,允许你使用其他源的数据有效地批量加载 SQL Server 表. 属性: BatchSize :每个批处理中的行数. 在每个批处 ...
- 纸壳CMS3.0中的规则引擎,表达式计算
纸壳CMS3.0中的规则引擎,用于计算通用表达试结果.通常业务逻辑总是复杂多变的,使用这个规则引擎可以灵活的修改计算表达式. IRuleManager IRuleManager,是使用规则引擎的主要接 ...
- CSS文字大小单位PX、EM的区别
◆px像素(Pixel)是相对长度单位,像素px是相对于显示器屏幕分辨率而言的.(引自CSS2.0手册)◆em是相对长度单位,相对于当前对象内文本的字体尺寸.如当前对行内文本的字体尺寸未被人为设置,则 ...
- 467. Unique Substrings in Wraparound String
Consider the string s to be the infinite wraparound string of "abcdefghijklmnopqrstuvwxyz" ...
- c++ 多态总结
C++的多态性用一句话概括就是: 在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数. 如果对象类型是派生类,就调用派生类的函数:如果对象类型是 ...
- 3分钟解决MySQL 1032 主从错误(转)
转自 https://blog.51cto.com/suifu/1845457 3分钟解决MySQL 1032主从错误 Part1:写在最前 1032错误----现在生产库中好多数据,在从库误删了, ...
- 处理序列的几个小技巧:保持原序去重,命名切片以及Counter类
一. 去重并保持原来元素的顺序 def dedupe(items): h = [] for item in items: if item not in h: h.append(item) return ...