浅拷贝:又称值拷贝,将源对象 的值拷贝到目标拷贝对象中去,本质上来说源对象和目标拷贝对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。举个简单的例子:你的小名叫西西,大名叫沫沫,当别人叫你西西或者沫沫的时候你都会答应,这两个名字虽然不相同,但是都指的是你。
假设有一个String类,String s1;String s2(s1);在进行拷贝构造的时候将对象s1里的值全部拷贝到对象s2里。
我现在来简单的实现一下这个类:
#include
#include
#pragma warning(disable:4996)
using namespace std;
class STRING{
public:
STRING(char* s="")
:_str(new char[strlen(s)+1])
{
strcpy(_str,s);
}
STRING(const STRING& s)
{
_str=s._str;//两个指针指向了同一块内存区域
}
STRING& operator=(const STRING& s)
{
if(this!=&s)
{
this->_str=s._str;
}
return *this;
}
~STRING()
{
if(_str)
{
delete[] _str;
_str=NULL;
}
}
void show()
{
cout<<_str<
其实这个程序是存在问题的,什么问题呢?我们想一下,创建s2的时候程序必然会去调用拷贝构造函数,这时候拷贝构造仅仅只是完成了值拷贝,导致两个指针指向了同一块内存区域。随着程序的运行结束,又去调用析构函数,先是s2去调用析构函数,释放了它所指向的内存区域,接着s1又去调析构函数,这时候析构函数企图释放一块已经被释放的内存区域,程序将会崩溃。s1和s2的关系是这样的:
为了验证s1和s2确实指向了同一块内存区域,我进行了调试,如图所示:
所以程序会崩溃是应该的。那这个问题应该怎么去解决呢?这就引出了深拷贝。
深拷贝,拷贝时先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标拷贝对象中去,这样两个指针就指向了不同的内存位置,并且里面的内容还是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。
深拷贝实际上是这样的:
下面为深拷贝的拷贝构造函数和赋值运算符的重载传统实现:
STRING(const STRING& s)
{
_str=new char[strlen(s._str)+1];
strcpy(_str,s._str);
}
STRING& operator=(const STRING& s)//赋值运算符的重载
{
if(this!=&s)//不允许自己给自己赋值
{
delete[] _str;
this->_str=new char[strlen(s._str)+1];
strcpy(this->_str,s._str);
}
return *this;
}
这里的拷贝构造函数我们很容易理解,先开辟出和源对象一样大的内存区域,然后将需要拷贝的数据复制给目标拷贝对象。
那这里的赋值运算符的重载是怎样做的呢?
这种方法解决了我们的指针悬挂问题,通过不断的开空间让不同的指针指向不同的内存,以防止同一块内存被释放两次的问题 ,还有一种深拷贝的现代写法:
STRING(const STRING& s)
:_str(NULL)//初始化为NULL,否则释放空指针会出错
{
STRING tmp(s._str);//调用了构造函数,完成了空间的开辟以及值的拷贝
swap(this->_str,tmp._str);//交换tmp和目标拷贝对象所指向的内容
}
STRING& operator=(const STRING& s)
{
if(this!=&s)//不让自己给自己赋值
{
STRING tmp(s._str);//调用构造函数完成空间的开辟以及赋值工作
swap(this->_str,tmp._str);//交换tmp和目标拷贝对象所指向的内容
}
return *this;
}
先来分析下拷贝构造是怎么实现的:
拷贝构造调用完成之后,会接着去调用析构函数来销毁局部对象tmp.按照这种思路,不难可以想到s2的值一定和拷贝构造里的tmp的值一样,指向同一块内存区域。通过调试来证明一下:
在拷贝构造函数里的tmp:
调用完拷贝构造后的s2:(此时tmp被析构)
赋值运算符的重载实现过程与上述拷贝构造完全相同。
关于赋值运算符的重载还可以这样来写:
STRING& operator=(STRING s)
{
swap(_str,s._str);
return *this;
}
当实现如下调用时:
void test()
{
char* str="hello linux!";
STRING s1(str);
STRING s2;
s2=s1;//先调用拷贝构造,再调用了赋值运算符的重载
s1.show();
s2.show();
}
先创建s2对象(调用构造函数),s1=s2;(调用拷贝构造,再调用赋值运算符的重载)。为什么又去调用了拷贝构造呢?为什么???很简单,因为在这个赋值运算符的重载的参数里创建了一个临时对象,为什么说它是一个临时对象,因为出了赋值运算符重载的作用域它就被析构掉了。这种方法的过程就是通过传参创建一个临时对象,通过调用拷贝构造来完成空间的开辟并且拷贝源对象里的数据。然后再交换目标对象和临时对象的值,赋值运算符重载函数被调用完之后,临时对象被析构。