C++中的拷贝构造函数跟Java中的对象克隆(clone)是一样的,它们的目的都是通过一个类的实例来获取它的一个副本或者叫拷贝,这个副本或拷贝跟原来的对象拥有相同的数据成员。
在普通的变量赋值中,比如int i=5; int j=i;我们可以用变量i的值去初始化j的值,此时,变量j就叫做变量i的一个拷贝,修改j的值不会影响i的值。实例的初始化也可以通过其他实例进行初始化,即用一个实例去构造另一个实例。在构造的时候,将已存在的实例中的数据成员值传递给新的实例,将其初始化为与已存在的实例具有相同数据的实例。
用一个实例构造另一个实例的方法有两种。一种是先建立实例,然后将已存在的实例值一一赋给新实例,但是这样做非常繁琐;另一种就是利用类的拷贝构造函数实现。类的拷贝构造函数和普通的构造函数差不多,但是参数是固定的,就是相同的类对象的一个引用。拷贝构造函数的作用是将一个已经存在的实例去初始化另一个新的同类实例。它的原型如下:
类名(类名& 实例名)
拷贝构造函数的实现可以在类外部实现也可以在是内联函数。
下面给出一个简单的拷贝构造函数的例子:
#include <iostream>
using namespace std;
class A
{
public:
A(int i); // 普通的构造函数
A(A& a); // 拷贝构造函数
int get() { return n; }
private:
int n;
};
A::A(int i) :n(i) {}
A::A(A& a)
{
cout << "copy constructor is called!" << endl;
n = a.n;
}
int main()
{
A a1(5);
A a2(a1);
A a3 = a2;
cout << "a2 n=" << a2.get() << endl;
cout << "a3 n=" << a3.get() << endl;
}
打印结果:
copy constructor is called!
copy constructor is called!
5
5
上面的例子说明了拷贝构造函数被调用的一种情况,就是当用一个类的实例去初始化该类的另一个实例时,或者是给该类的另一个实例赋值时都会调用拷贝构造函数。拷贝构造函数是由系统调用的。在以下三种情况下,系统会调用拷贝构造函数:
1.当用一个类的实例去初始化该类的另一个实例时,或者是给该类的另一个实例赋值时系统会调用拷贝构造函数。
2.当函数的形参是类的实例,在调用这个函数进行形参和实参结合时,系统会调用拷贝构造函数。
3.当函数的返回值为类的实例,函数调用结束返回时,系统会调用拷贝构造函数。
继续上面的例子:
void getValue(A a)
{
cout << a.get();
}
int main()
{
A a(5);
getValue(a); // 此时,拷贝函数被调用
return 0;
}
分析:这种情况会调用拷贝构造函数是因为在参数传递过程中,函数需要对参数建立副本。建立副本的过程相当于建立了一个新的实例,并且用参数中的实例来对其进行初始化。
A createA()
{
A a(5);
return a; // 此时,拷贝构造函数被调用
}
int main()
{
A a = createA(); // 此时,拷贝构造函数被调用
return 0;
}
分析:在createA函数中创建了一个局部变量,当函数调用结束后,这个局部变量会消亡(会被系统回收),函数的局部变量无法在主调函数中继续生存。对于实例,也是如此。为了将函数中的返回值带回主调函数,编译系统会建立临时的无名实例,以便在主调函数中给其他实例赋值。在建立无名实例时,就是建立了一个新的实例,并且用局部函数中的返回对象对其进行初始化,所以调用了拷贝构造函数。在main函数中,用a去接收这样一个实例,相当于第一种情况,即用一个实例去初始化另一个实例,此时,也要调用拷贝构造函数,也就是说,一共要调用两次拷贝构造函数。(按道理来讲应该是这样的,但是我在VS2008中测试结果却不是这样的,只有在函数返回的时候调用了一次拷贝构造函数,在main函数里初始化a的时候没有调用拷贝构造函数)
默认拷贝构造函数
在类的定义中,如果开发者未定义拷贝构造函数,则C++会自动提供一个默认的拷贝构造函数,这点和类的构造函数类似。默认的拷贝构造函数的作用是拷贝实例中的每一个非静态数据成员给新的同类实例。如果系统调用了默认的拷贝构造函数,其会将实例中所有的非静态数据成员一一赋给新的实例,这样就完成了实例的拷贝,也就是说,默认的构造函数完成的工作是所有非静态数据成员的对拷。
按照上例,类A中只有一个非静态数据成员n,所以,即使我们不去手动编写一个拷贝构造函数,系统也会为我们添加一个默认的拷贝构造函数,并且能够完成和我们的自己定义的拷贝构造函数相同的功能。需要注意的是,在拷贝函数进行工作时,对静态数据成员是不进行拷贝的,因为静态数据成员只有一份拷贝,所有实例共享这份拷贝。
浅拷贝和深拷贝
既然系统已经提供了默认的拷贝构造函数,为什么还需要自己定义拷贝构造函数呢?主要有以下两个原因:
1.默认的拷贝构造函数无法满足开发者对视力复制细节控制的要求。默认的拷贝构造函数是将实例中的所有非静态成员完全拷贝,但是,如果我现在不需要将所有的非静态成员完全拷贝,而只是希望拷贝一部分呢?这就要求拷贝构造函数能够更灵活,更细致,这是默认的拷贝构造函数无法做到的,所以需要为每个类定义自己的拷贝构造函数。
2.默认的拷贝构造函数无法对实例的资源进行拷贝(如动态内存等)。如果在实例中的数据成员拥有资源,拷贝构造函数只会建立该数据成员的一个拷贝,而不会自动为其分配资源,这样两个实例中就会拥有同一个资源。这样的局面显然是不合理的,不仅不符合对实例的要求,而且在析构函数中会被释放两次资源,导致程序错误。下面就是一个利用默认的拷贝构造函数导致程序出错的例子。
#include <iostream>
using namespace std;
class A
{
public:
A(int i, int j);
~A();
private:
int n;
int *p;
};
A::A(int i, int j) : n(i), p(&j) {}
A::~A()
{
cout << "destructor is called!" << endl;
delete p; // 在析构函数中要析构对象持有的资源,否则会造成内存泄露
p = NULL;
}
int main()
{
A a1(3, 4);
A a2 = a1; // 因为没有定义拷贝构造函数,此时会调用默认的拷贝构造函数
return 0;
}
分析:如果在VS2008下面测试的话,会发现只能打印出一个destructor is called!,然后程序就终止了,原因是对同一个资源释放了两次。在A的构造函数中,n被赋值为一个整形变量3,p被赋值为指向变量4的指针(假设p的值为0x1001)。当A a2 = a1时,会调用A的拷贝构造函数,因为我们并没有定义拷贝构造函数,因此,系统为我们添加了一个默认的拷贝构造函数,这个拷贝构造函数会将对象的所有非静态成员完全拷贝,也就是说这个时候a2中的n也是3,p也是0x1001,即a2中的p和a1中的p指向同一个变量。在main方法结束前,要将所有的栈对象全部析构,因此,会首先析构a1,在析构a1的时候同时释放了a1中的p所指向的那块资源,也就是整形变量4所占的4个字节的空间也同时被系统回收了。当析构a2的时候,同样会调用a2的析构函数,同时去析构a2中的p所指向的那块资源,因为a1中的p和a2中的p指向的是同一块资源并且a1中的p所指向的那块资源已经被析构掉了,因此,在析构a2时就会重复析构一个已经析构掉了的资源,这时系统就会报错。
修正这个bug很简单,就是给类A添加一个自定义的拷贝构造函数。代码如下:
A::A(A& a)
{
n = a.n;
p = new int;
*p = *a.p;
}
这样,a1和a2的p就分别持有自己的资源了,即使析构了a1,也不会析构掉a2中的p。
小结:当对实例进行拷贝时,未对实例的资源进行拷贝的过程称为浅拷贝。对于浅拷贝所带来的弊端,可以通过自定义拷贝构造函数来解决。当对实例进行拷贝时,对实例的资源也进行拷贝的过程称为深拷贝。对于需要拷贝构造函数进行深拷贝的并不止堆内存,对文件的操作、系统设备的占有(如计算机端口、打印)等都需要进行深拷贝,一般来讲,需要在析构函数中手动析构的所有成员(比如指针、引用等)都需要在拷贝构造函数中对其重新分配资源。