C++类对象的赋值操作分为深拷贝和浅拷贝两种,我们所说的的浅拷贝就是赋值操作,这种拷贝比较常见,比如:
class A { public: A(); ~A(); public: int i; }; A::A() { i = 5; } A::~A() { } void main() { A a1;//执行默认构造函数 初始化a1.i = 7 A a2 = a1;//发生赋值操作 此刻a2.i = 5 a2.i = 7;//只修改a2中的成员,对a1中的不造成影响 return; }
这就是一个简单的浅拷贝,当我们拷贝之后,发现变量 i 的值确实被拷贝过来了,此时a1和a2中的变量值都一样。
而我们对a2中的 i 进行操作,修改其中的内容的时候,并不影响a1中 i 的值。
但是如果我们的类中涉及到动态内存的申请与释放,那么简单的浅拷贝就会出现问题:
class A { public: A(); ~A(); public: char* ptr1; }; A::A() { ptr1 = new char[5]; memset(ptr1, 'h', 5); } A::~A() { if (ptr1 != nullptr) { delete[]ptr1; ptr1 = nullptr; } } void main() { A a1;//执行默认构造函数 A a2 = a1;//发生赋值操作 return; }
涉及到动态内存的分配与释放的时侯就比较危险,可能会造成对内存的二次释放,出现内存堆被破坏,导致内存奔溃的情况发生!这是为什么呢?我们上述代码中,对于类对象的赋值是简单的拷贝操作,也就是直接把a1中申请的那块内存的值复制给了a2中的ptr1对象,它相当于做了这样一个操作:
a2.ptr1 = a1.ptr1;
我们也可以通过调试信息来验证我们的这个说法:
我们可以从图中看到两个指针指向的地址是同一个地址,所以这只是指针的简单赋值,并没有开辟新的内存空间,那么我们都知道,当类的对象销毁的时候,类的析构函数会被执行,这样我们就会执行析构函数中的内存释放操作,我们目前在main函数中有两个对象a1和a2,那么肯定会分别执行各自的析构函数。那么就会对同一块内存区域释放两次,肯定会导致堆的奔溃,自然就导致内存奔溃!我们可以从动图中看到,第一次释放内存没问题!第二次释放内存导致内存奔溃!
但是我们可以用深拷贝来解决这个问题!
class A { public: A();//默认构造函数 A(const A& A1);//拷贝构造函数 ~A(); public: char* ptr1; }; A::A() { ptr1 = new char[5]; memset(ptr1, 'h', 5); } A::A(const A &A1) { ptr1 = new char[5]; strcpy(ptr1, A1.ptr1); } A::~A() { if (ptr1 != nullptr) { delete[]ptr1; ptr1 = nullptr; } } void main() { A a1;//执行默认构造函数 A a2 = a1;//发生深拷贝赋值操作,另外开辟了内存空间 A a3(a1);//发生深拷贝赋值操作,另外开辟了内存空间 return; }
在类中定义一个拷贝构造函数,传参传入当前类的引用,然后对内存进行重新的申请,再将指针指向内存中的内容进行复制操作,这样我们就完成了深拷贝,在内存析构时候自然就不会出现二次释放同一片内存的问题,我们可以从如下的调试截图中看到三个ptr1指针指向了不同的三个地址空间!
最后来看一下最终析构时候的调试动图,没有发生任何的内存奔溃。