今天我们来谈谈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++浅拷贝和深拷贝的有关问题,如果聪明的你浏览到这篇文章并觉得文章内容对你有帮助,请不吝动动手指,给博主一个小小的赞和收藏 🌻🌻🌻