首先来看一段代码:
#include <iostream>
#include <string.h>
using namespace std;
class String
{
public:
string(const char* str = "wsc")
{
_str = (const char*)malloc(strlen(strlen) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
system("pause");
return 0;
}
这段代码本意是拷贝构造s1给s2,但是通过测试,我们发现程序会崩溃,那为什么会造成崩溃呢?这里涉及到一个深浅拷贝问题。下面来说说,什么是浅拷贝,什么又是深拷贝?
浅拷贝:按内存存储字节序完成的拷贝就叫做浅拷贝,又称值拷贝。在c++中,如果我们不写拷贝构造函数,系统自动生成的拷贝构造就是我们所说的值拷贝。
需要说明的是,我们在写日期类时,可以不写拷贝构造函数,使用编译器自动生成的函数,不会有什么问题,但是像String这种比较特殊的类,如果我们还是不写拷贝构造函数的话,程序就会出现上面代码中的问题,具体原因是:
这个程序会释放两次,但是正常情况下只能释放一次,开辟空间和释放空间必须是一一对应的,因为当释放掉一次后,此时这块空间已经不属于你,而属于操作系统,如果恰好这块空间被操作系统分配给别人,如果再释放一次的话,相当于把别人的空间给释放了。这是不允许的,下面画图解释一下:
深拷贝:如果一个类中涉及到资源的管理,那么其拷贝构造函数、赋值运算重载、析构函数都要显式给出,也就是我们要自己实现,且都是按照深拷贝方式来完成。
深拷贝的原理:自己开辟一块空间,把s2的数据拷贝过去,析构时,自己析构自己的,也不会出现多次释放的问题。
传统的(深)拷贝构造:
String(const String& s)//传统的深拷贝
{
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
传统的赋值重载:先释放掉s3的空间,再去开辟一块和s2一样大小的空间
String& operator=(const String& s)//传统的赋值
{
if (*this != &s)//防止自己给自己赋值
{
delete[] _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;
}
现代的(深)拷贝构造:
String(const String& s)
//s3(s2)
:_str(nullptr)//s3一开始是随机值,才准备构造,所以把要构造的s3初始化为nullptr
{
String tmp(s._str);//构造一个和s2一样的tmp
swap(_str, tmp._str);//把tmp和已经初始化为nullptr的s3一交换,拷贝构造完成
}
现代的赋值重载:
String& operator=(const String& s)
{
if (this != &s)
{
String tmp(s._str);
swap(_str, tmp._str);
}
return *this;
}
还有一种更简单的:s3(s2)
String& operator=(String s)
{
swap(_str, s._str);
return *this;
}
传统的拷贝构造:比较直观
现代写法:简洁+复用
其实,解决浅拷贝还有另外一种方式,就是写时拷贝,写时拷贝实质上就是拖延症,它采用引用计数,当count减到0时,意味着没有一个对象指向这块空间,当count>1时,不释放,让count--,直到count为1时,表明只有一个对象指向这块空间,此时再去释放这块空间,虽然这种方式效率高,但是会存在线程安全问题,所以我们还是不推荐这种方式。