浅析C++中的深浅拷贝

浅拷贝:又称值拷贝,将源对象 的值拷贝到目标拷贝对象中去,本质上来说源对象和目标拷贝对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。举个简单的例子:你的小名叫西西,大名叫沫沫,当别人叫你西西或者沫沫的时候你都会答应,这两个名字虽然不相同,但是都指的是你。

假设有一个String类,String s1;String s2(s1);在进行拷贝构造的时候将对象s1里的值全部拷贝到对象s2里。

我现在来简单的实现一下这个类:

#include<iostream>
#include<cstring>
#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<<endl;
	}
private:
	char* _str;
};
void test()
{
	STRING  s1("hello linux!");
	STRING s2(s1);
	s2.show();
}
int main()
{
	test();
	system("pause");
	return 0;
}

其实这个程序是存在问题的,什么问题呢?我们想一下,创建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;(调用拷贝构造,再调用赋值运算符的重载)。为什么又去调用了拷贝构造呢?为什么???很简单,因为在这个赋值运算符的重载的参数里创建了一个临时对象,为什么说它是一个临时对象,因为出了赋值运算符重载的作用域它就被析构掉了。这种方法的过程就是通过传参创建一个临时对象,通过调用拷贝构造来完成空间的开辟并且拷贝源对象里的数据。然后再交换目标对象和临时对象的值,赋值运算符重载函数被调用完之后,临时对象被析构。
总结一下:这种现代方式本质上来说就是我自己不想开空间,让临时对象去开空间并完成拷贝,最后只要让我指向临时对象所指向的内容,再让临时对象指向我原先的所在的区域。最后临时对象被析构掉。












猜你喜欢

转载自blog.csdn.net/qq_39344902/article/details/79798297