●浅拷贝:
顾名思义,浅拷贝只是将对象的值拷贝过来。
如果没有显示实现构造函数或是拷贝构造函数,那么编译器会默认以浅拷贝的方式自动生成。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<Windows.h>
#include<string.h>
using namespace std;
class String
{
public:
String()
:_str(new char[1])
{
_str[0] = '\0';
}
String(const char* str)
:_str(new char[strlen(str)+1])
{
strcpy(_str, str);
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
void test1()
{
String s1;
String s2("hello");
String S3(s2);
}
int main()
{
test1();
system("pause");
return 0;
}
我们自己模拟一个string类显示实现其构造函数和析构函数,运行程序,test函数中,编译器会调用s1和s2我们自己模拟实现的构造函数,默认生成S3的拷贝构造函数,而编译器默认实现的是浅拷贝,也就是说,S3和S2拥有相同的一块空间,这有点像我们学过的引用,可以把它理解为S3是s2的别名,但是这会引起严重的后果——一块空间由于多次释放而程序崩溃。在test函数调用完成时,编译器要依次析构S3,s2和s1,当S3析构完成,s2以为这块空间还在,去释放的时候就会导致系统崩溃。
调试程序,可以看到s2和S3地址相同,这就验证了前面说的编译器默认是浅拷贝去调用它的拷贝构造函数,其实调用构造函数也是如此。
如果不写析构函数的话,浅拷贝是没有什么问题的,但是一旦自己实现了析构函数,系统就会崩溃。
●深拷贝
1>为了解决上述问题,引入深拷贝。
2>有的类中涉及到资源管理,就要用深拷贝的方式实现,并且要显式给出其构造函数,拷贝构造函数,析构函数和赋值运算符的重载。
3>不难想到,深拷贝就是为每个对象分配自己独有的资源,这样就不会由于多个对象共有一块空间而引起违规访问的问题了,不管析构多少次,都是没有问题的。
●下面是深拷贝的传统写法:
传统写法符合我们的正常思路,先创建相同大小的空间,再把内容拷过去。
class String
{
public:
//构造
String(const char* 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);
}
//重载等号
//s1(this)=s2(s)
String& operator=(const String& s)
{
if (this != &s)
{
//释放旧空间
delete[] _str;
//开辟新空间并拷贝s内容
char* newstr = new char[strlen(s._str) + 1];
strcpy(newstr, s._str);
//this指向新空间
_str = newstr;
}
return *this;
}
~String()
{
if(_str)
delete[] _str;
}
private:
char* _str;
};
void test2()
{
String s1;
String s2("hello");
String s3(s2);
String s4 = s2;
}
int main()
{
//test1();
test2();
system("pause");
return 0;
}
1>>s3由s2拷贝构造得到——>为s3开辟与s2同样大小的空间,再将s2中内容赋到这块空间中。
2>>s4由s3赋值得到——>将s4空间释放后,新开辟一块与s2相同大小的空间并将s2中内容赋给它,然后让s4指向这块空间。
通过监视看到s2,s3,s4地址都不相同,说明它们各自拥有不同的空间。
●深拷贝的现代写法:
class String
{
public:
//构造
String(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
//拷贝构造
String(const String& s)
:_str(nullptr)
{
//用s构造新的String
String tmp(s._str);
//交换this和tmp
swap(_str, tmp._str);
}
//重载=
//s1=s2
String& operator=(const String& s)
{
if (this != &s)
{
String tmp(s._str);
swap(_str, tmp._str);
}
return *this;
}
/*
String& operator=(String& s)
{
swap(_str, s._str);
return *this;
}
*/
~String()
{
if (_str)
delete[] _str;
}
private:
char* _str;
};
void test3()
{
String s1;
String s2("hello");
String s3(s2);
String s4 = s2;
}
int main()
{
//test1();
//test2();
test3();
system("pause");
return 0;
}
1>>s3由s2拷贝构造得到——>由s2构造一个新tmp,再将这个tmp和s3交换,实际上是交换了它指针的指向。
一开始s3并不知道指向哪里,但是通过交换我们可以拿到我们想要的内存,至于tmp最终指向哪里我们并不关心,最后编译器会释放它的。
2>>s4由s2赋值得到——>由s2构造一个新tmp,再将这个tmp和s4交换,返回s4。
s4一开始已经有自己的内存,s2和s4交换指针指向,出了作用域tmp会自动调用它的析构函数,也就把原来s4指向的那块内存释放了。
现代写法是在传统写法的方式进行改进,虽然逻辑变得复杂,但是更加高效。