【C++】——C++深浅拷贝以及写时拷贝的理解(调试+代码)

深浅拷贝和写时拷贝

(最近好烦哦,觉得自己状态不是很好,复习的效率也不是很高,刷个LeetCode,人家写个递归,我觉得自己得想好久,调试好久,还不一定完全理解,事情也太多了)今天上课的时候,老师说深浅拷贝这个是高频考点,行吧,看不懂递归,我来看点自己可以理解的。

1、浅拷贝

比如说,现在有一个对象A,需求是将 A 拷贝给对象B,当B拷贝了A的数据后,当B的改变会导致A的改变,此时叫做B浅拷贝了A。如以下代码所示:

class String
{
public:
	String(const char* str):_str(new char[strlen(str)+1])
	{
		strcpy(_str, str);
	}
	String(const String& s)
	{
		_str = s._str;
	}
	String& operator=(const String& s)
	{
		if (this != &s) //防止自己给自己赋值
		{
			_str = s._str;
		}
		return *this;
	}
	~String()
	{
		if (_str)
		{
			delete[] _str;
		}
		_str = NULL;
	}
private:
	char* _str;
};

那我们为什么会说一个Sting对象的改变会导致另一个String对象的改变?是因为我们将一个指针的值赋值给另一个指针,那就使得两个指针指向同一块地址空间,这就产生了浅拷贝。通过调试窗口,发现结果确实如此:
在这里插入图片描述

浅拷贝存在的问题?

  • 两个或者两个以上指针指向了同一块空间,这个内存就会被释放很多次,例如以上的代码,我们定义了一个String对象s1,以浅拷贝的方式构造s2,s1和s2的_str就指向了同一块地址空间,当出了作用域,s2肯定会先析构,之后s1析构,这时候问题就出现了,这块地址空间已经被s2释放掉了,当s1去析构的时候就会出现问题
  • 当两个指针指向同一块空间的时候,一旦一个指针修改了这块空间的值,另一块空间的值也会改变。

所以解决浅拷贝的方法就是:

1、深拷贝

2、引用计数的写时拷贝

2、深拷贝

为了解决浅拷贝带来的问题,深拷贝所做的事情就是对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制了其内的成员变量深拷贝有两种写法,一种是传统写法,另一种是现代写法

1)传统写法
例如:将s1赋值给s2,s2(s1)或者s2=s1

  • 对于拷贝构造来说,s2会先开辟和s1同样大小的空间; 但是对于赋值来说,s2在开辟一块和s1同样大小的空间之前,要先释放掉s2原来的空间,因为s2原来就已经存在(防止s2里面的指针没有被释放)。
  • 让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;//先释放掉原来的空间
			_str = NULL;
			_str = new char[strlen(s._str) + 1];  //开空间拷数据
			strcpy(_str, s._str);
		}
		return *this;
	}
  1. 现代写法
  • 本质:让别人去开空间拷数据,我只需要和你进行交换
  • 实现:例如用s1拷贝构造一个对象s2:s2(s1),可以通过构造函数将s1的指针_str构造一个临时对象tmp,构造函数会开空间然后拷贝数据到tmp中,此时tmp就是我想要的对象,我们将自己的_str和tmp的_str进行交换
    代码如下:
//拷贝构造的现代写法
String(const String& s):_str(NULL)
{
	String tmp(s._str);  //调用构造函数
	swap(_str, tmp_str);  //交换
}
String& operator=(const String& s)
{
	if (this != &s)
	{
		String tmp(s._str);  //调用构造函数
		swap(_str, tmp_str);  //交换
	}
	return *this;
}

注意,以上代码使用了String原本就有的构造函数,利用现代写法写拷贝构造的时候,可以直接调用构造函数,而对于无参的构造函数的类,只能采用传统写法。

3、写时拷贝

其实大家也会经常这么叫,“利用计数的写时拷贝”,为什么这么说?先看下他的使用场景:

  • 想要多次调用拷贝构造函数,但是拷贝构造的对象不会修改这块空间的值。
  • 如果采用深拷贝,每次都会开空间,然后释放空间,消耗会很大
  • 为了解决释放多次的问题,我们采用计数,当有新的指针指向这块空间的时候,我们可以增加引用计数,当这个指针需要销毁的时候,将计数减1,判断计数是否为0,为0就释放
  • 当有一个指针需要修改其指向空间的值时候,才会去开辟一个新的空间
  • 引用计数解决了空间被释放多次的问题,写时拷贝解决了多个指针指向同一空间会被修改的问题。

写时拷贝的写法1:

//写时拷贝的写法1
class String
{
public:
	String(const char* str) //构造函数 
		:_str(new char[strlen(str)+1]),_refCount(1)
	{
		strcpy(_str, str);
	}
	String(const String&s)  //拷贝构造
		:_str(s._str)
	{
		_count = s._refCount;
		++_refCount;

	}
private:
	char* _str;
	int _refCount;  //计数
};

但是这有一个缺点,每个对象的计数是独立的,就好像这块空间可能有多个对象与之对应,每个对象都有一个count,那count进行加减的时候,只是所在对象的count的变化,使得这块空间对应的引用计数不相同。例如下图所示:
在这里插入图片描述
那有的同学可能会说,直接将_refCount改为static修饰的不就行了吗?

class String
{
	private:
		char* str;
		static int _refCount;	
};

这是不可以的,静态成员为该类的所有对象所共享,就会使得利用String类创建的所有对象,哪怕他们的指针指向不同的空间,导致_refcount都是相同的,这是不合理的。

经过分析,这里有另外一种高效的写法

写时拷贝的写法3:

class String
{
public:
	String(const char* str) //构造函数 
		:_str(new char[strlen(str)+1]), _refCount(new int(1))  //每个对象对应一个整形空间存放
	{
		strcpy(_str, str);
	}
	String(const String& s)  //拷贝构造
		:_str(s._str),_refCount(s._refCount)
	{
		++_refCount[0];
	}
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			if (--_refCount[0] == 0) //旧的计数减1,如果是最后一个引用对象则释放对象
			{
				delete[] _str;
				delete[] _refCount;
			}
			//如果不是最后一个引用对象
			_str = s._str;
			_refCount = s._refCount;
			++_refCount[0];
		}
		return *this;

	}
private:
	char* _str;
	int* _refCount;
};
int main()
{
	String s1("hello");
	String s2(s1);
	String s3 ("world");
	system("pause");
	return 0;
}

该指针指向一块空间,存放的是引用计数,当s1拷贝构造s2的时候,s1和s2的_str都指向了同一块空间,当然引用计数也存放在同一空间。调试如下:
在这里插入图片描述
s1和s2的因为都指向了同一块空间,因此引用计数也是相同,而s3因为指向了另一块空间因此是计数不同的

原创文章 78 获赞 21 访问量 3535

猜你喜欢

转载自blog.csdn.net/Vicky_Cr/article/details/105618974