深浅拷贝
- 问题引入
- 浅拷贝
- 深拷贝
- 总结
问题引入
对于一个普通的string类:
class String {
public:
String(const char* str = "")
{
//构造函数
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String()
{
//析构函数
if (_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
浅拷贝
对于上述的 String 类来说,浅拷贝也就是将一个对象原封不动的复制粘贴到新的对象当中,当类中没有给定拷贝构造函数时系统会默认生成一个拷贝构造函数,但默认生成的拷贝构造函数就属于浅拷贝:
当前系统默认生成的拷贝构造函数类似于:
那么,浅拷贝是否合理存在?
由上述运行截图我们可以观察到使用系统默认生成的拷贝构造函数或是代码中展现的拷贝构造方法构造出来的对象与原对象内容地址是完全一致的,也就类似于复制粘贴了一个相同的对象,那么它会产生什么问题?
在前边类与对象章节中,我们知道了在创建的类对象使用结束之后系统会自动调用相应的析构函数来释放资源空间(先构造的后释放,后构造的先释放),因此在该类中,首先要进行释放的是 s2,后释放 s1,我们来看看现象:
不知道,读者是否发现了问题所在?
解析:
在使用 s2(s1),s1拷贝构造 s2 对象时,我们发现创建出来的 s2 对象完全是复制粘贴 s1 对象而形成的,不仅对象内容完全相同,对象的地址空间也完全相同,这就导致了两个对象是共享同一份地址空间了,故在进行析构时,产生了同一份地址空间二次释放而发生异常提示
同理,在 String 类中倘若用户没有显式定义赋值运算符重载函数,则系统会默认生成一个赋值运算符重载函数,我们来看看会有什么问题?
显然,同样产生了浅拷贝(复制粘贴),则在进行空间销毁时候定会产生空间的二次释放异常,那么我们应该如何来解决这种问题?
介绍两种技术:深拷贝(重点)、写时拷贝
深拷贝
产生浅拷贝的主要是因为在进行拷贝构造或是赋值运算符重载时,直接将就对象的内容原封不动的粘贴给新对象,因而在空间释放时形成了多次释放问题,因此深拷贝主要就是解决不同对象分配不同地址空间的问题:
String(const String& s):_str(nullptr)
{
//s2(s1)
//首先需要给s2对象开辟一份独立的新空间
_str = new char[strlen(s._str) + 1];
//其次将s1对象中的内容拷贝到s2空间当中
strcpy(_str, s._str);
}
同理,赋值运算符也是一样的:
String& operator=(const String& s)
{
if (this != &s) //判断不是自己给自己赋值
{
char* tmp = new char[strlen(s._str) + 1];
strcpy(tmp, s._str);
delete[] _str; //释放旧空间
_str = tmp;
}
return *this;
}
显然,两个对象的地址空间是不相同的,则释放空间时不会发生同一份空间多次释放的问题。
现代新写法(注意理解):
String(const String& s):_str(nullptr)
{
//拷贝构造函数
String tmp(s._str); //采用当前对象的内容构造出一个新对象tmp
swap(_str,tmp._str); //进行资源交换,将临时空间 tmp 的内容与 当前对象的内容进行交换------------临时对象在函数周期结束自动析构释放
}
String& operator=(String s) { //以值的方式传参,需要调用一次拷贝构造函数,构造出一个新的对象 s
//赋值运算符重载
if (this != &s) {
swap(_str, s._str); //直接进行交换
}
return *this;
}
总结
在类中,倘若用户没有显式定义拷贝构造函数以及赋值运算符重载函数,则编译器会自动生成一份,但在使用时属于浅拷贝会造成同一份空间多个对象共享,在对象使用结束释放空间时会发生多次释放问题产生,其次在赋值运算符重载函数中,不仅会使得多个对象共享空间,还会导致空间丢失问题:
String s1("hello"); //调用构造
String s2 = "world";
//使用系统默认生成的赋值运算符重载函数会导致原有的 s2 对象空间泄漏
s2 = s1;
因此, 我们应该在使用时进行显式的定义,防止发生浅拷贝问题的产生
写时拷贝技术
写时拷贝技术同样是针对浅拷贝问题的解决方式
允许多个对象共享同一份地址空间以及空间中的内容,并定义一个计数器来统计当前空间所共享的对象个数,并在对象使用结束之后在析构函数内部进行计数器 - 1,当析构一个对象之后,计数器变为 0,说明当前对象是共享空间的最后一个使用者,则需要对这块空间进行释放--------------------------------但同样存在一个问题:倘若某一个对象对这块共享空间的内容进行了修改,则其他对象指向的这块共享空间内容也发生了变化
因此,写时拷贝技术是在对象需要对共享的内存空间进行修改时拷贝构造出独立的空间来进行修改,这么做就不会影响到其他对象对共享空间的访问。
ps:
博文内容为原创,欢迎各位共同学习~~