深拷贝与浅拷贝的理解

我们直接通过现象看问题!(下面的拷贝构造函数和赋值运算符重载是有问题的!!!)

#include <string>
class String
{
public:
	String(const char* str = "")
	{
		if (nullptr == str)
			str = "";

		_str = new char[strlen(str) + 1];
		strcpy(_str,str);
	}

	String(const String& s)
		:_str(s._str)
	{}
	//问题:1.内存泄漏  2.与拷贝构造函数类似
	String& operator=(const String& s)
	{
		_str = s._str;
		return *this;
	}

	~String()
	{
		delete[] _str;
		_str = nullptr;
	}
private:
	char* _str;
};

int main()
{
	String s1("hello");
	String s2(s1);

	String s3("world");
	String s4 = s3;
	return 0;
}

深拷贝与浅拷贝的理解
我们可以从监视窗口清楚的看到,s1和s2共用一块内存空间。s3和s4共用一块内存空间。
深拷贝与浅拷贝的理解
上面的代码是浅拷贝的写法
怎么样来理解这个问题(我们只看拷贝构造函数的例子,赋值运算符重载的原理基本一样):首先给s1分配内存空间,地址为0x008f5308,然后放入该地址管理的资源"hello",当用s1拷贝构造s2时,调用拷贝构造函数,*str是一个指针,直接用其进行赋值,所以等于对象s1、s2共用同一块空间,那么当对象生命周期结束时,需要调用析构函数对其资源进行释放,相对于s1,优先释放s2,那么0x008f5308这段空间已被释放,当再释放s1时,那么系统就会崩溃。
正确的写法是下面的这种深拷贝

template<class T>
void Swap(T& a, T& b)
{
	T c(a);
	a = b;
	b = c;
}

class String
{
public:

	String(const char* str = "")
	{
		if (nullptr == str)
			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);
	//}

	//现代写法
	String(const String& s)
	{
		String strTemp(s._str);
		Swap(_str, strTemp._str);
	}

	//传统写法
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			//写法一:
			delete[] _str;//释放旧空间
			_str = new char[strlen(s._str) + 1];//申请新空间
			strcpy(_str, s._str);//完成拷贝

			//写法二:优点:如果开辟空间失败,原来的内容还存在
			//char* pStr = new char[strlen(s._str) + 1]; 
			//strcpy(pStr, _str);
			//delete[] _str;
			//_str = pStr;
			return *this;
		}
	}


	//现代写法1
	/*String& operator=(const String& s)
	{
		if (this != &s)
		{
			String strTemp(s);
			Swap(_str, strTemp._str);
		}
	}*/

	//现代写法2
	String& operator=(String s)
	{
		Swap(_str, s._str);
		return *this;
	}

	~String()
	{
		delete[] _str;
		_str = nullptr;
	}
private:
	char* _str;
};


int main()
{
	String s1;
	String s2(s1);
	String s3;
	String s4 = s3;
	return 0;
}

深拷贝与浅拷贝的理解
我们可以清楚的看到深拷贝的对象的内存空间各不相同,当对象生命周期结束时,先调用析构函数对s4的资源进行清理,再调用析构函数清理s3,因为它们的内存空间各不相同,所以就不会发生冲突。
浅拷贝+引用计数来看看下面的代码。
下面的代码仅仅在单线程下时安全的

class String
{
public:
	String(const char* str = "")
		:_pCount(new int(1))
	{
		if (nullptr == str)
			str = "";

		_str = new char[strlen(str) + 1];
		strcpy(_str, str); 
	} 

	String(String& s)
		:_str(s._str)
		, _pCount(s._pCount)
	{
		++(*_pCount);
	}

	String& operator=(const String& s)
	{
		if (this != &s)
		{
			//先处理自己的资源
			if (0 == --(*_pCount) && _str)
			{
				delete[] _str;
				_str = nullptr;

				delete _pCount;
				_pCount = nullptr;
			}
			//共享资源
			_str = s._str;
			_pCount = s._pCount;

			//计数+1
			++(*_pCount);
		}
	}

	char& operator[](size_t index)
	{
		if (*_pCount > 1)
		{
			String str(_str);
			this->Swap(str);
		}
		return _str[index];
	}

	~String()
	{
		if (0 == --(*_pCount) && _str)
		{
			delete[] _str;
			_str = nullptr;

			delete[] _pCount;
			_pCount = nullptr;
		}
	}

	void Swap(String& s)
	{
		swap(_str, s._str);
		swap(_pCount, s._pCount);
	}
private:
	char* _str;
	int* _pCount;
};


void test()
{
	String s1("hello");
	String s2(s1);
	s1[0] = 'H';//写时拷贝
	String s3;
}

深拷贝与浅拷贝的理解
从上面我们可以看出,这样也可以解决问题,就是加一个pCount计数器,当有对象使用这块空间时,计数+1,当该对象生命周期结束时,计数-1,当该计数为0时,就释放这块空间,这里这个计数不能通过静态成员变量来实现,因为静态成员变量时全局作用域的,当不发生拷贝构造时,指向不同空间的不同对象也使用同一个计数count,就会出现问题