目录
1 浅拷贝
简单来说,浅拷贝就是原封不动的将源对象所占用的一片内存中的所有数据全部拷贝到目的对象所在的内存中,不管这片内存中存放的是什么,即使源对象中的数据成员包含指针(包括虚表指针),那么也会将该指针变量(实际上就是一个地址)拷贝到目的对象中,如果没有显式定义拷贝构造函数,那么在需要的时候程序会自动调用一个默认的拷贝构造函数,而这个默认的拷贝构造函数就是浅拷贝。如下所示:
class A
{
public:
A() :x(1),y(2),z(3){};
char x;
short y;
short z;
};
int _tmain(int argc, _TCHAR* argv[])
{
A a;
A b = a; //默认拷贝构造b
system("pause");
return 0;
}
通过反汇编来观察拷贝过程:
根据反汇编代码可知,拷贝过程就是从a的首地址开始,根据数据成员的对齐情况一块一块的将源对象的数据拷贝到目的对象所在内存中。拷贝之后对象a和对象b各自内存中所存放的数据是完全相同的。
根据整个拷贝过程,不难想到:如果对象中存在指针变量,那么拷贝之后两个对象的指针变量都是相同的,也就是说,两个对象中的指针都指向同一位置,一旦这一位置的数据发生了改变,那么对这两个指针而言,其所指向的目标位置都已经发生了改变。对于一般情况而言,这似乎没有太大影响,但是如果类中存在需要在堆上动态分配内存的变量,那么就会引发严重的问题:
class A
{
public :
A() //构造函数
{
cout << "A() called !" << endl;
x = 1;
p = new int; //堆上申请内存
cout << "Constructed !" << endl;
}
~A() //析构函数
{
cout << "~A() called !" << endl;
delete p; //销毁p
p = NULL;
cout << "Destructed !" << endl;
}
int x;
int* p;
};
int _tmain(int argc, _TCHAR* argv[])
{
A a; //构造对象a
A b = a; //拷贝构造对象b
return 0;
}
构造时申请内存,析构时释放内存,但是如果仍然是默认拷贝构造的话,那么main函数执行结束时就会引发问题:
可以看到,如果正常的话,这里在“~A() called!”后面还应当有一个“Destructed”,因此引发问题的时期是对象b析构调用delete p;这一时刻,为什么会有问题呢?还是默认浅拷贝的问题。
构造对象a时,动态为a.p申请了内存,a.p也就存放了一个有效的地址,而当b浅拷贝a时,b.p又被赋值为了a.p,也就是说a.p和b.p是指向的同一个地方,当main函数结束时,先析构对象a,此时就把p销毁掉了,a.p所指的地址也被释放了,然后再析构对象b,也要去释放b.p所指的内存,而此时这片内存早已在a.p销毁时被释放了,这就引发了二次析构,自然就报错了。
浅拷贝的问题很明显,就是完全无脑拷贝一片内存,而不管这片内存中的数据的意义,如果这片内存中存放的数据是地址,特别是指向堆上动态申请的内存时,那么浅拷贝就很容易引发问题。
2 深拷贝
为了避免浅拷贝引发的问题,我们就需要自己定义一个拷贝构造函数,并且在自定义拷贝构造函数中,对于相应的指针变量,重新开辟空间,以上面引发二次析构的程序为例,自定义一个拷贝构造函数如下:
class A
{
public :
A()
{
cout << "A() called !" << endl;
x = 1;
p = new int;
(*p) = 2;
cout << "Constructed !" << endl;
}
~A()
{
cout << "~A() called !" << endl;
delete p;
p = NULL;
cout << "Destructed !" << endl;
}
A(const A& a) //自定义拷贝构造函数(深拷贝)
{
cout << "A() copy called !" << endl;
x = a.x; //拷贝a.x
p = new int; //重新开辟空间
*(p) = *(a.p); //拷贝*(a.p)
cout << "Copy constructed !" << endl;
}
int x;
int* p;
};
int _tmain(int argc, _TCHAR* argv[])
{
A a; //构造对象a
A b = a; //拷贝构造对象b
cout << a.x << " " << a.p << " " << *(a.p) << endl; //输出a的数据成员以及a.p指向的值
cout << b.x << " " << b.p << " " << *(b.p) << endl; //输出b的数据成员以及b.p指向的值
system("pause");
return 0;
}
可见,这样一来a.p和b.p的值就不一样了,也就是说二者之一改变也不会影响另一个,析构时也不会引发二次析构问题,这种在自定义拷贝构造函数中重新分配内存再拷贝的方式就是深拷贝。
3 赋值
除了初始化时的拷贝构造问题,还有对象与对象之间的赋值问题。和默认拷贝构造函数相似,如果没有自定义重载赋值运算符,当出现对象赋值给对象时(如A a; A b; b=a;),也会默认执行浅拷贝,即是将a所占的那一片内存中的数据全部拷贝到b所占的内存中去,这样就会出现前面所说的浅拷贝构造问题。
为了解决赋值中默认浅拷贝的问题,与前面类似,重载赋值运算符来实现深拷贝,不过这是从运行时角度来考虑。当然还有另一种方法,从编译时来考虑,就是直接将重载赋值函数声明为private或者protected(只需声明即可),这样一来类外部对象就无法调用重载的赋值函数,也就无法实现对象之间的赋值,编译时期就会报错了,这种方法自然也适用于拷贝构造函数。
补充说明
还有以下两种情况也会调用拷贝构造函数,值得注意:
①函数形参为对象并采用值传递,函数被调用时,会生成临时对象,而这个临时对象会对实参先进行拷贝;
②函数返回类型为对象的值类型,函数返回时,实际上也是先对待返回的对象进行拷贝后,返回拷贝后的对象。