- 浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进行操作时,就会发生了访问违规。
下面来看一段代码:
String(const char* pstr = "")
:_pstr(new char[strlen(pstr)+1])
{
strcpy(_pstr,pstr);
}
void StringTest()
{
String s1("hello world!");
String s2(s1);//s2需要调用String类拷贝构造函数来创建,如果该类没有显示定义,则使用系统合成的默认构造函数
}
大致结构如图:
当StringTest函数结束时,需要将s1和s2销毁掉,调用系统默认的析构函数。先销毁s2,s2将_pstr所指的空间释放掉,s2成功销毁,但是s1中_pstr就成为野指针了,当销毁s1时发生错误。导致问题的原因是:s1、s2公用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃。这种拷贝方式就称为浅拷贝。
- 深拷贝
如果一个类中涉及资源的清理的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显示给出,一般情况都是按照深拷贝方式提供。
上面的代码可以改成:
String(const String& s)//s是s1的别名
:_pstr(new char[strlen(s._pstr)+1])
{
strcpy(_pstr,s._pstr);
}
每个String类对象都要用空间来存放字符串,而s2要用s1拷贝构造出来。因此深拷贝:给每个对象独立分配资源,保证多个对象之间不会因为共享资源而造成多次释放造成程序崩溃问题。
一般日期类是浅拷贝,而string类是深拷贝。
- 传统写法和现代写法的String类
- 传统写法的构造函数
String(const char* str = "")
{
if(nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str)+1];//这里加1存的是'\0'
strcpy(_str,str);
}
- 现代写法的构造函数
String(const char* str = "")
{
if(nullptr == str)
{
str = "";
}
_str = new char[strlen(str)+1];
strcpy(_str,str);
}
- 传统写法的拷贝构造函数
String(const String& s)
:_str(new char[strlen(s._str)+1]);
{
strcpy(_str,s._str);
}
- 现代写法的拷贝构造函数
String(const String& s)
:_str(nullptr)//初始化_str为nullptr
{
String strTmp(s._str);//拷贝构造一个strtmp
swap(_str,strTmp);//通过交换完成拷贝构造
}
- 传统写法的赋值重载函数
String& operator=(const String& s)
{
if(this != &s)//防止赋值在同一个地址上,即自己给自己赋值
{
char* pstr = new char[strlen(s._str)+1];
strcpy(pstr,s._str);
delete[] _str;
_str = pstr;
}
return *this;
}
- 现代写法的赋值重载函数
String& operator=(String s)
{
swap(_str,s._str);
return *this;
}
- 传统和现代写法的析构函数相同
~String()
{
if(_str != nullptr)
{
delete[]_str;
_str = nullptr;
}
}
写时拷贝
写时拷贝就是一种拖延症,它是在浅拷贝的基础上增加了引用计数的方式。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当每个对象被销毁时,先给计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象是资源的最后一个使用者,将该资源释放,否则就不能释放,因为还有其他对象正在使用该资源。