自学内容网 自学内容网

浅谈C++的浅拷贝和深拷贝问题

今天我们来谈谈C++的浅拷贝和深拷贝问题,这里先上定义,可以直接浏览下面的表格,比较直观😊😊😊 。在C++中,浅拷贝和深拷贝是两种对象复制的方式,其中🐱浅拷贝(Shallow Copy)是指将一个对象的指赋值到另一个对象中,但只赋值对象的成员变量的值,并不对复制对象的动态分配内存(如堆内存)等外部资源,这也就意味着当原对象修改自己指向的外部资源时,可能会影响到另一个对象;🐶深拷贝(Deep Copy)是指在复制对象时,不仅复制对象中的值,还复制指向动态分配内存的指针所指向的内存。这样每个对象都有自己的独立内存,它们之间不会相互干扰。自定义的复制构造函数和赋值运算符通常会进行深拷贝。

对象复制的方式定义
🐱浅拷贝(Shallow Copy)将一个对象的指赋值到另一个对象中,但只赋值对象的成员变量的值,并不对复制对象的动态分配内存(如堆内存)等外部资源
🐶深拷贝(Deep Copy)将一个对象的值复制给另一个对象,包括了对象成员变量的值以及对象的动态分配内存(如堆内存)外部资源

从上述定义中我们可以想到,要是通过浅拷贝的方式拷贝对象,当发生原对象修改自己指向的外部资源时,由于另一个对象并不会复制原对象的外部资源,而是与原对象指向同一块外部资源,如图所示:
在这里插入图片描述
因此在程序结束调用析构函数时,很可能会对指向的外部资源进行二次析构而导致系统崩溃,下面给出一段错误代码🫤:

#include <iostream>
using namespace std;

class SeqStack
{
public:
SeqStack(int size = 5)
{
cout << this << " SeqStack() " << endl;
_pstack = new int[size];
_top = -1;
_size = size;
}
~SeqStack() 
{
cout << this << " ~SeqStack() " << endl;
delete[] _pstack;
_pstack = nullptr;
}

private:
int* _pstack;
int _top;
int _size;
};

int main()
{
SeqStack s;  //没有提供任何构造函数时,编译器会提供默认构造和析构函数
SeqStack s1(5);  
SeqStack s2 = s1; // #1  拷贝构造函数,做内存拷贝
//SeqStack s3(s1); #2 = #1

return 0;
}

根据上述描述,我们可以推导出,在程序准备进行析构时,会先调用 s2 的析构函数(释放*_pstack),此时*_pstack变成了空指针,再进行s1的析构,由于它们指向同一块外部资源,s1的析构会导致系统崩溃💦💦💦,结果如下:
在这里插入图片描述
因此在对象成员有开辟了外部资源的前提下,我们进行拷贝构造时需要自定义构造函数来避免出现此类问题,下面为自定义拷贝构造函数的代码:

//自定义拷贝构造函数
SeqStack(const SeqStack& src)
{
_pstack = new int[src._size];
for (int i = 0; i <= src._top; i++)  
_pstack[i] = src._pstack[i];

//memcpy() realloc()
_top = src._top;
_size = src._size;
}

加上这段代码,此时就会在复制对象时新开辟一块独立于原对象的外部资源,做了一次深拷贝,防止出现浅拷贝问题(二次析构),猜猜现在的程序是否能够正常运行呢?

🤔🤔🤔思考一下:为什么我们要自定义拷贝构造函数使用for循环而不直接使用 memcpy 或者 realloc 函数来进行拷贝呢?

同理

  • memcpy函数只是简单的将一块内存中的内容复制到另一块内存中,不会处理动态内存的分配和释放,也不会对对象中的成员进行初始化和析构, 会导致指针成员指向同一块内存。
  • realloc函数用于重新分配已经分配的动态内存,它会尝试扩大或缩小内存块的大小,同时保持原有内存块中的数据不变。虽然在需要调整动态内存大小的时候非常有用,但是它并不能正确地处理C++对象的构造和析构。

我们再来看看一个简单的赋值操作,可以在main函数中在 s2 做完深拷贝之后,加一条s2 = s1语句,如下:

int main()
{
SeqStack s;  //没有提供任何构造函数时,编译期会提供默认构造和析构函数
SeqStack s1(5);   // #1  拷贝构造函数,做内存拷贝
SeqStack s2 = s1; // #2

s2 = s1;  //这里!!!!
return 0;
}

看看运行结果,令人发指 😧 😧 😧

在这里插入图片描述

这块的话其实也是浅拷贝问题的一种,因为我们没有在类中自定义赋值函数,C++编译器会调用它的默认赋值函数,做的也是数据的内存拷贝(即浅拷贝),直接把对象s2*_pstack指向了对象s1*_pstack,而原先对象s2*_pstack所指向的另一块外部资源被丢弃了,使之无法释放,如下同所示:
在这里插入图片描述
于是乎我们想到🤔,还要得重载一下赋值函数,同时释放那块即将被丢弃的外部资源:

//重载赋值函数
void operator=(const SeqStack& src) //大致跟拷贝构造函数类似
{
cout << "operatot=" << endl;
//if (this == &src) return; //防止自赋值 s1 = s1 而引起非法访问

delete[]_pstack; //需要把原来指向的外部资源释放

_pstack = new int[src._size];
for (int i = 0; i <= src._top; i++)  _pstack[i] = src._pstack[i];
_top = src._top;
_size = src._size;
}

我们常说理论指导实践,在看完上面的论述后,将给出一个应用实例作为练习,需要实例代码的话可以私信我😊
在这里插入图片描述
哦对,这里

最后来做个总结吧: 概念总结

🐱 浅拷贝:

  • 只复制对象的成员变量的值,不复制动态分配的资源。
  • 原对象和副本对象共享同一块堆内存。
  • 修改其中一个对象会影响到另一个对象。

🐶 深拷贝:

  • 复制对象的成员变量的值以及动态分配的资源。
  • 原对象和副本对象拥有各自独立的资源。
  • 修改其中一个对象不会影响到另一个对象。

🌻🌻🌻以上就是有C++浅拷贝和深拷贝的有关问题,如果聪明的你浏览到这篇文章并觉得文章内容对你有帮助,请不吝动动手指,给博主一个小小的赞和收藏 🌻🌻🌻


原文地址:https://blog.csdn.net/icc_hhy/article/details/135882802

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!