#浅拷贝问题的出现
我们常常使用类的拷贝构造函数来初始化类对象。而类对象在创建和销毁时会分别调用构造函数和析构函数。
如下代码:
#include "iostream"
#include "vector"
using namespace std;
class person{//creat a class named person
public:
int id;
int *m_Age;
person(int _id,int _m_Age){
id = _id;
m_Age = new int(_m_Age);//use the heap to allocate memory
}
~person(){
if(m_Age!=NULL){
delete m_Age;
m_Age = NULL;
}
}
};
void test1 (){
person p1(5, 15);
person p2 = p1;//call the copy constructor
}
int main(){
test1();
return 0;
}
当类成员中有指针变量需要在堆区开辟内存,且我们要调用编译器(MSCV)自动创建的拷贝构造函数时,逐步调试,出现浅拷贝问题,即。
在这个浅拷贝过程中,p2 的 id 和 m_Age 会被新的值所覆盖。对于 id 来说,这并不是问题,因为它是一个基本类型。但是对于 m_Age 来说,这就是问题了。m_Age 是一个指向 int 的指针,它指向的内存是在构造函数中通过 new 分配的。拷贝时系统默认将p1的内容复制给p2,因此就会使p1与p2的m_Age指针成员所指向同一块堆区内存。在p2发生析构之后p2.m_Age所指向的内存空间被释放并置空,此时p1.m_Age仍指向已被释放的之前的地址空间。因此,p1析构时,发现p1指针不空,就会再次使用delete释放非法的地址空间。
要解决上述问题,可以手动添加拷贝构造函数
person::person(const person& p) {
id = p.id;
m_Age = new int(*(p.m_Age));
}
应当注意的是,在使用拷贝构造函数时,传入的相同类型的类应加上const和&:
#赋值运算符的重载
继续,若执行如下代码:
#include "iostream"
#include "vector"
using namespace std;
class person
{
public:
person(int _id, int _m_Age) {
cout << "地址为" << this << "的构造函数调用" << endl;
id = _id;
m_Age = new int(_m_Age);
}
person(const person& p);
int id;
int* m_Age;
~person() {
cout << "地址为" << this << "的析构函数调用" << endl;
if (m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
}
};
person::person(const person& p) {
cout << "地址为" << this << "的构造函数调用" << endl;
id = p.id;
m_Age = new int(*(p.m_Age));
}
void test2() {
person p1(5, 15), p2(3, 13);
cout << "p1地址" << &p1 << endl;
cout << "p2地址" << &p2 << endl;
p2 = person(p1);//or use p2 = p1;
}
int main() {
test2();
return 0;
}
逐步调试,发现仍会出现浅拷贝的问题。p2 = person(p1); 这一句其实涉及到了两个操作:
-
person(p1):这是一个拷贝构造函数的调用,会创建一个新的 person 对象,该对象的 id 和 m_Age 的值是从 p1 中拷贝过来的。这个新对象在堆栈上创建,是一个临时对象。
-
p2 =:这是一个赋值操作,它会把等号右边的对象(即刚刚创建的临时对象)赋值给 p2。但是,person 类没有定义赋值运算符的重载,所以这里会使用默认的赋值运算符,它会进行浅拷贝。这就是问题的根源。
在赋值操作中,p2.m_Age 的旧值(也就是它原来指向的内存地址)会被新的值(也就是临时对象的 m_Age 所指向的内存地址)所覆盖,而 p2.m_Age 原来指向的内存并没有被 delete,这就造成了内存泄漏。
同时,因为 p2.m_Age 和临时对象的 m_Age 现在指向同一块内存,当临时对象在语句结束后被析构时,这块内存会被 delete,而** p2.m_Age 还在指向这块已经被释放的内存**,这就造成了 p2.m_Age 是一个悬挂指针,这是非常危险的。
解决这个问题的方法就是为 person 类定义一个赋值运算符的重载,进行深拷贝:
person& person::operator=(const person& p) {
if (this != &p) {
delete m_Age;
id = p.id;
m_Age = new int(*(p.m_Age));
}
return *this;
}
这样,在赋值操作中,p2.m_Age 原来指向的内存就会被正确地释放,而且 p2.m_Age 会指向一块新的、和临时对象的 m_Age 所指向的内存不同的内存。