深浅拷贝

先来说一下什么叫拷贝?
所谓拷贝,是把一个事物变成两个或多个的过程,我们了解到的最多的拷贝方式应该就是CTRL+C/V吧,但是在C/C++中的拷贝有它独特的地方,独特在哪呢?

独特在于C/C++中拷贝的事物不同:只拷贝指针而不管指针指向的内容,这种称为浅拷贝;拷贝了指针并且拷贝指针指向的内容叫做深拷贝

先来看一下浅拷贝:
class Func {
public:
	Func(const char* str)
		:_str(new char[strlen(str) + 1])
	{
		strcpy(_str, str);
	}

	Func(const Func& fun)
		:_str(fun._str)
	{}

	Func& operator=(const Func& fun)
	{
		if (this != &fun)
		{
			_str = fun._str;
		}
		return *this;
	}
	~Func()
	{
		if (_str)
		{
			delete _str;
			_str = nullptr;
		}
	}
private:
	char *_str;
};

int main()
{
	Func s1("hello");
	Func s2(s1);
	return 0;
}

通过调试并查看监视窗口发现:
在这里插入图片描述
说明构造函数创建的对象s1和拷贝构造函数创建的对象s2指向的是同一块空间,如图示:
在这里插入图片描述
这个就是简单的拷贝了指针,而没有拷贝指针所指向的内容,浅拷贝会出现什么问题呢?

  • 浅拷贝只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用析构函数时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃;
  • 浅拷贝使得s1和s2指向同一块内存,任何一方的变动都会影响到另一方;
  • 释放内存时,会造成s1原有的内存没有被释放(如果没有走自定义的拷贝构造函数,申请内存空间,Func s2(s1);也不走默认构造函数,走的是默认的拷贝构造函数,何来分配空间之说,更不会造成s1原有的内存没有被释放),造成内存泄露。
    事实是这样的,当delete s2, s2内存被释放后,由于之前s2和s1指向的是同一个内存空间,s2所指的空间不能在被利用了,delete s1时不会成功,无法操作该空间,所以会导致内存泄露。
为了解决这个问题,我们引入了深拷贝:
  1. 传统写法:
    用s1对象拷贝构造或赋值给s2(s2(s1)或 s2 = s1),当涉及到深浅拷贝的问题时:
    对于拷贝构造函数来说,s2先开一块和s1一样大的空间,再将s1里的数据拷贝给s2;
    对于赋值运算符重载函数来说s2已经存在,则必须先释放s2的空间,再让s2开辟一块与s1一样大的空间(否则就会导致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就是我们想要的对象,然后将新tmp的指针_ptr与自己的指针进行交换;
    对于构造函数来说,因为String有一个带参数的构造函数,则用现代写法写拷贝构造时可以调用构造函数,而对于没有无参的构造函数的类只能采用传统写法(开空间然后拷数据)

//拷贝构造的现代写法      
String(const String& s)
	:_str(NULL)
{
	String tmp(s._str);   //调用构造函数,则tmp是需要的

	swap(_str, tmp._str);   //将_str与tmp的_str指向的空间进行交换,
							//tmp._str就会指向_str的空间,出了这个作用域,tmp就会调用析构函数,
							//但是tmp里面的_str值可能不确定,所以在初始化列表中将_str置空,这样tmp._str=NULL
}

//赋值的现代写法
String& operator=(const String& s)
{
	if (this != &s)
	{
		String tmp(s._str);    //调用构造函数
		swap(_str, tmp._str);   //tmp是局部对象,出了这个作用域就会调用析构函数,
								//就会将tmp里面的指针指向的空间释放掉,
	}
	return *this;
}

其他:

  1. 有时候为了防止默认拷贝发生,可以声明一个私有的拷贝构造函数(不用写代码),这样的话,如果试图调用 A b(a); 就调用了私有的拷贝构造函数,编译器会报错,这也是一种偷懒的做法。
  2. 一个类中可以存在多个拷贝构造函数,例如:
  Calss A
  {
  Public:
  X(const X&);		 //const拷贝构造
  X(X &);           //非const拷贝构造
  X(X& , int  iData);
  }
写时拷贝:
  1. 常用场景:

有时会多次调用拷贝构造函数,但是拷贝构造的对象并不会修改这块空间的值;如果采用深拷贝,每次都会重复的开空间,然后拷数据,最后再释放这块空间,这会花费很大的精力; 浅拷贝不用重复的开空间,但是会有问题。
为了解决释放多次的问题可以采用引用计数,当有新的指针指向这块空间的时候,我们可以增加引用计数,当这个指针需要销毁时,就将引用计数的值减1,当引用计数的值为1时才去释放这块空间。
当有一个指针指需要修改其指向空间的值时,才去开一块新的空间(也就是写时拷贝),这相当于一个延缓政策,如果不需要修改,则不用开新的空间,毕竟开空间需要很大的消耗。 引用计数解决了空间被释放多次的问题,写时拷贝解决了多个指针指向同一块空间会修改的问题。

  1. String写时拷贝的的三种方案的选择:
  • 将引用计数定义为int类型
class Func {
private:
	char* _str;
	int _count;
};

在这里插入图片描述
缺陷:
每个对象的引用计数之间是独立的,如果增加指向这块空间的指针,也只会修改新增这个指针所在对象的引用计数,就会使得每块空间对应引用计数不相同

  • 将引用计数定义为static int
class Func {
private:
	char* _str;
	static int _count;
};

在这里插入图片描述
缺陷 :
静态成员为该类的所有对象所共享,理论上就会使得利用String类创建的所有对象哪怕他们指针指向不同的空间,但是这些对象的引用计数都相等,实际上根本不能编译通过

  • 将引用计数定义为int*的指针

该指针指向一块空间,这块空间里面存放的是引用计数,当用s1拷贝构造s2时,s1与s2里面的_str指向同一块空间,s1与s2的引用计数也存放在同一块空间里。创建3个Func对象,s1与s2指向同一块空间,则s1与s2的引用计数都为2,s3指向另一块空间,s3的引用计数为1。

class Func {
private:
	char* _str;
	int* _count;
};

在这里插入图片描述
3. 写时拷贝的改进

如果将引用计数单独的定义为一个int的指针,它占4个字节,每次创建一个String对象,都会为其向操作系统申请4个字节的内存,这样就会经常申请许多小块内存,会造成内存碎片,也会对效率造成影响。这时可以考虑将_str与引用计数放在一起,就在_str的头上4个字节存放引用计数,当我们取引用计数时,只用将((int*)(_str-4))。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值