深浅拷贝的原理与区别

本文探讨了浅拷贝和深拷贝的概念,指出浅拷贝可能导致的指针悬挂问题,并介绍了深拷贝如何解决这个问题。此外,还提到了写时拷贝作为另一种拷贝策略,用于优化性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

#浅拷贝和深拷贝和写时拷贝的区别及原理**

1、浅拷贝
当出现类的等号赋值时,会调用拷贝函数
这里写图片描述
什么是浅拷贝?

在未定义显示拷贝构造函数时,系统默认调用拷贝构造函数(浅拷贝),够完成成员的复制。当数据成员中没有指针时,浅拷贝是可行的。

如果有指针,浅拷贝(只是拷贝了指针指向的地址),则类中的两个指针会指向同一块空间,当对象快结束时,会调用两次析构函数释放同一块空间,而导致指针悬挂现象。
所有,这时,必须采用深拷贝。

2、深拷贝
这里写图片描述

深拷贝和浅拷贝的区别

深拷贝会在堆内存中另外申请空间来存储数据,从而解决了指针悬挂的问题。

浅拷贝

浅拷贝,是指原对象与拷贝对象公用一份实体,仅仅是对象名字不同而已,其中任何一个对象改变都会导致其他的对象也跟着它变。如下面这段代码:

#include<iostream>
#include<cstring>
using namespace std;

class String
{
public:
	String(const char* pstr = "")
		:_pstr(new char[strlen(pstr)+1])//为字符串开辟空间
	{
		if (0 == *pstr)  //如果字符串为空
		{
			*_pstr = '\0'; 
		}
		else   //字符串不为空
		{
			strcpy(_pstr, pstr);
		}
	}
	//s2(s1)
	String(const String& s) //拷贝构造函数
	{
		_pstr = s._pstr;
	}
	// s3=s1
	String& operator=(String& s)  //运算符重载
	{
		if (_pstr != s._pstr)
		{
			_pstr = s._pstr;
		}
		return *this;
	}
	~String()
	{
		if (NULL == _pstr)
		{
			return;
		}
		else
		{
			delete[] _pstr;
			_pstr = NULL;
		}
	}
private:

	char* _pstr;
};
void Fun1()
{
	String s1("abcd");
	String s2(s1); 
	String s3 = s2;  
	String s4;
	s4 = s3;

}

int main()
{
	Fun1();
	system("pause");
	return 0;
}

其中s1,s2,s3,s4包含的指针对象同时指向一块内存,析构时delete了这个空间四次
可是代码并没有判断内存是否有效,会导致指针悬挂。

改进1:用一个计数器来控制析构函数

   结果: 还是无法避免浅拷贝造成的内存泄漏问题
   因为:1 四个对象本来指向同一块空间,计数器应该为4,现在结果却是计数器
   只能控制与它相邻对象的计数器,对象创建完成后计数器并不统一
	    2 调用4次析构函数后,本来应该四个对象同时被释放,结果却没有一个
	对象的计数器为0,也就是这块空间没有被释放

改进2 :采用 static计数器

#include<iostream>
#include<cstring>

using namespace std;

class String
{
public:
	String(const char* pstr = "")
		:_pstr(new char[strlen(pstr)+1])//为字符串开辟空间
	{
		if (0 == *pstr)  //如果字符串为空
		{
			*_pstr = '\0'; 
		}
		else   //字符串不为空
		{
			strcpy(_pstr, pstr);
		}
		_count++;
	}
	//s2(s1)
	String(const String& s) //拷贝构造函数
	{
		_pstr = s._pstr;
		s._count++;
		_count = s._count;
	}

	~String()
	{
		if (NULL == _pstr)
		{
			return;
		}
		else
		{
			if (--_count == 0)
			{
				delete[] _pstr;
				_pstr = NULL;
			}
		}
	}
	String& operator=(String& s)  //运算符重载
	{
		if (_pstr != s._pstr)
		{
			_pstr = s._pstr;
			s._count = _count;
			_count++;
		}
		return *this;
	}

private:

	char* _pstr;
	static int _count;
};

int String::_count = 0;

void Fun1()
{
	String s1("abcd");
	String s2(s1); 
	String s3 = s2;  
	String s4;
	s4 = s3;

}

int main()
{
	Fun1();
	system("pause");
	return 0;
}
	结果:还是出现bug
	
	1 我们创建了4个对象可是计数器却是5,因为静态成员变量为对象共享
	任何对象都可用对它进行修改,每创建一个对象,我们对计数器加1,却忽略
	创建的新对象是否与已经存在的对象占同一块空间
	
	 2 调用4次析构函数,计数器值为1,导致空间又没有被释放

改进3:采用 指针 计数

class String
{
public:
	String(const char* pstr = "")//构造函数
		:count(new int(0))
		, _pstr(new char[strlen(pstr)+1])
	{
		if (NULL == pstr)
		{
			*_pstr = '\0';
		}
		else
		{
			strcpy(_pstr, pstr);
		}
		*count = 1;
	}
	String(const String& s)//拷贝构造
		:count(s.count)
	{
		_pstr = (char*)s._pstr;
		count = s.count;
		(*count)++;
	}
	~String()//析构函数
	{
		if (NULL == _pstr)
		{
			return;
		}
		else
		{
			if (--(*count) == 0)
			{
				delete[]count;
				delete[]_pstr;
				_pstr = NULL;
				count = NULL;
			}
		}
	}
	String& operator=(String& s)//赋值运算符重载
	{
		if (_pstr != s._pstr)
		{
			_pstr = s._pstr;
			count = s.count;
			(*count)++;
		}
		return *this;
	}
private:
	int* count;
	char* _pstr;
};


void Funtest()
{
	String s1("abcd");
	String s2(s1);
	String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
	String s4;//s4对象已经存在了
	s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
	Funtest();
	system("pause");
	return 0;
}

空间被准确释放,指针计数看起来是很完美的操作 ,然而还是有瑕疵
比如:每个对象为指针计数多创建一个指针,浪费空间,还有释放麻烦,有可能我们只记得释放_pstr,却忘记释放计数器指针的空间,造成内存泄漏。

深拷贝

所谓深拷贝,就是为新对象开辟一块新的空间,并将原对象的内容拷贝给新开的空间,释放时就不会牵扯到多次析构的问题

class String
{
public:
	String(const char* pstr = "")
		:_pstr(new char[strlen(pstr)+1]) //开辟空间
	{
		if (0 == *_pstr)      //如果是空内容
		{
			*_pstr = '\0';
		}
		else
		{
			strcpy(_pstr, pstr); //拷贝字符串
		}
	}
	String(String& s)  //拷贝构造函数
		:_pstr(NULL) //防止交换后temp指向随机空间,本函数调用结束导致内存泄漏致崩溃
	{
		String temp(s._pstr);//如果不给出临时变量,交换后s的值将为NULL
		std::swap(_pstr, s._pstr);
	}

	String& operator=(const String &s)//赋值运算符重载
	{
		if (_pstr != s._pstr) //防止自己给自己赋值
		{
			String temp(s._pstr); //如果不给出临时变量交换后的值为NULL
			std::swap(_pstr, temp._pstr);
		}
		return *this;
	}
	~String()
	{
		if (NULL == _pstr)
		{
			return;
		}
		else
		{
			delete[]_pstr;
			_pstr = NULL;
		}
	}
private:
	char* _pstr;
};

void Funtest()
{
	String s1("abcd");
	String s2(s1);
	String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
	String s4;//s4对象已经存在了
	s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
	Funtest();
	system("pause");
	return 0;
}

写时拷贝

class String
{
public:
	String(const char* pstr = "")
		:_pstr(new char[strlen(pstr) + 4+1])//每次多开辟4个空间存放当前地址有几个对象
	{
		if (NULL == pstr)    //如果当前字符为空
		{
			(*(int*)_pstr) = 1; //将前4个字节用来计数
			_pstr += 4;
			*_pstr = '\0';
		}
		else   //字符串不为空
		{
			(*(int*)_pstr) = 1;//将前4个字节用来计数
			_pstr += 4;//指针向后偏移4个字节
			strcpy(_pstr, pstr); //将pstr内容拷贝到当前对象_pstr中
			cout << "String" << endl;
		}
	}
	//s2(s1)
	String(const String& s) //拷贝构造函数
		:_pstr(s._pstr)
	{
		++(*(int*)(_pstr - 4)); //向前偏移4个字节将计数加1
		cout << "Stirng kaobei" << endl;
	}

	~String()
	{
		if (NULL == _pstr)
		{
			return;
		}
		else
		{
			cout << "~String" << endl;
			if (--(*(int*)(_pstr-4)) == 0) //向前偏移4个字节判断计数是否为0,是0则是否
			{
				delete (_pstr-4);
				_pstr = NULL;
			}
		}
	}
	String& operator=(String& s)  //运算符重载
	{
		if (_pstr != s._pstr)
		{
			if (--(*(int*)(_pstr - 4)) == 0) //释放旧空间
			{
				delete (_pstr - 4);
				_pstr = NULL;
			}
			_pstr = s._pstr;  //指向新空间
			++(*(int*)(_pstr - 4)); //计数加1
		}
		return *this;
	}

private:

	char* _pstr;
};


void Fun1()
{
	String s1("abcd");
	String s2(s1);
	String s3 = s2;
	String s4;
	s4 = s3;

}

int main()
{
	Fun1();
	system("pause");
	return 0;
}

这里写图片描述
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值