拷贝构造函数是一种特殊的构造函数,其形参是本类对象的一个引用。使用拷贝构造函数的目的是用一个已经存在的对象来初始化同类的一个新对象。这种做法类似于现实世界中的复印机。当需要获得某些文件的副本时,可以使用复印机复制出与原件相同的复制品。所以某些时候,程序员也可能希望获得某个对象的副本。当然读者可能会想到的一个做法是,初始化一个新的对象,然后将已有对象的各个数据成员逐个赋给新对象的成员变量。但是这个方法明显有些麻烦,C++中可以使用拷贝构造函数来处理这个问题。拷贝构造函数的声明语法规则如下:
类名(类名 & 对象名);
来看一个使用拷贝构造函数的简单例子。
#include <iostream>
using namespace std;
class Time
{
int hour, minute, second;
public:
Time():hour(0),minute(0),second(0){};
Time(Time & t);
void getTime();
void setTime(int h, int m, int s);
};
Time::Time(Time & t)
{
hour = t.hour;
minute = t.minute;
second = t.second;
cout<<"Notice!!! Copy constructor is being called."<<endl;
}
void Time::getTime()
{
cout <<"Now the time is "<<hour<<":"<<minute<<":"<<second<<endl;
}
void Time::setTime(int h, int m , int s)
{
if(m<0||m>59||s<0||s>59||h<0||h>23)
{
cout<<"Error! Please Check!"<<endl;
return;
}
hour = h;
minute = m;
second = s;
}
void fun0(Time t){}
Time fun1()
{
Time t;
return t;
}
void main()
{
Time time0;
time0.setTime(12, 5, 7);
Time time1(time0); //调用拷贝构造函数
time1.getTime();
fun0(time0); //调用拷贝构造函数
Time time2;
time2=fun1(); //调用拷贝构造函数
}
通常构造函数只在对象创建时被调用,而拷贝构造函数则在以下三种情况下被调用:
l 当使用类的一个对象去初始化该类的一个新对象时;
l 如果函数的形参是类的对象,那么当调用该函数时,拷贝构造函数也会被调用;
l 如果函数的返回值是类的对象,那么函数执行完成返回调用者时。
读者可以编译并运行上述程序,观察运行结果,易知在上面例子中注释说明的三个地方程序都调用了拷贝构造函数
。
这里对于第二种情况稍作解释。如果函数中的参数传递的是一个值,而非指针或是引用,那么程序就会自动复制该值的一个副本。函数体中也只是对该副本进行操作。因此如果函数的形参是类的一个对象,那么同理程序需要复制该对象的副本,所以这个时候必然会用到拷贝构造函数。但是读者是否想过这个问题,那就是如果类中没有拷贝构造函数,是不是函数就不能使用一个类的对象作为参数了呢?当然不是,事实上即使类中没有显式地给出拷贝构造函数,函数依然可以使用一个类的对象作为参数。同样,程序也会为这个类对象建造一个副本,并在函数体中对副本进行操作。这表示C++为每个没有自定义拷贝构造函数的类都提供了一个默认的拷贝构造函数,这跟前面讲过的默认构造函数是一样的。
如果仅是简单的将原对象的所有数据成员都赋给新对象,那么特别编写一个拷贝构造函数就没意义了,这时可以使用默认拷贝构造函数。但是如果在程序中进行对象的复制有更多的要求,比如需要有变化或者有选择地复制对象,这时就必须自己写拷贝构造函数。
此外当类的数据成员中包含有指针类型时,我们也不推荐使用默认拷贝构造函数。原因在于默认拷贝构造函数实现的只能是浅拷贝,而浅拷贝会带来数据安全方面的隐患。要避免这些问题的出现就要求自己编写拷贝构造函数。来看下面这个例子。
#include <iostream>
using namespace std;
class Person
{
char *name; int age;
public:
Person(){name = NULL;age = 20;}
void Init(int size, int age)
{
name = new char[size];
this->age = age;
}
int getAge(){return age;}
char * getName(){return name;}
void setAge(int age){this->age = age;};
void setName(char * name){strcpy(this->name,name);};
};
void main()
{
Person p1;
p1.Init(20, 45);
Person p2 = p1;
p1.setName("Peter");
cout<<p2.getName()<<endl;
}
猜想一下上述程序的运行结果是什么呢?主函数里语句的意思大约是首先创建一个Person类的对象p1,并初始化对象p1的成员变量age=45,name=NULL。然后使用默认的拷贝构造函数由对象p1创建了新的对象p2,即此时p2的成员变量age=45,name=NULL。然后程序调用对象p1的方法setName,将p1的成员变量name的值改为Peter。到此刻,程序似乎没有对p2进行操作,那么p2的成员变量name的值本来应该仍未NULL。可是编译并运行程序,结果发现程序输出了Peter。考虑一下产生这个结果的原因。读者应当注意到类的对象中包含指针成员,该成员指向动态分配的内存资源。这时如果使用p1来初始化p2,即必须把p1内的数据成员值赋给p2的数据成员。由于直接使得p2.name = p1.name,所以这时p2.name和p1.name同时指一个堆内存区域,从而产生了对象内存不独立的情况,这就是所谓的浅拷贝(Bitcopy)问题。
现在,我们可以从一个简单的角度来定义浅拷贝和深拷贝。如果一个类拥有资源,当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之如果对象存在资源但复制过程并未复制资源的情况则被称为浅拷贝。浅拷贝资源后在释放资源的时候会产生资源归属不清的情况,从而导致程序运行出错。可见浅拷贝带来了程序安全性上的很大隐患,实际编程时必须杜绝浅拷贝的情况。解决该问题的途径就是使用自定义的拷贝构造函数,而非默认拷贝构造函数。
在本小节最后,我们要介绍一些关于拷贝构造函数的细节问题。尽管这些东西很琐碎,但它们的确都很有用,而且是很多非初学者仍然有可能搞不清楚的问题。
首先,类中是可以存在超过一个拷贝构造函数的。但是,一旦一个类中只存在一个参数为X&的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化。
既然一个类中可以存在多个拷贝构造函数,那么拷贝构造函数的不同形式可能有哪些呢?通常,对于一个类X,如果它的一个构造函数的第一个参数是如下这四者中之一:X&,const X&,volatile X&或者const volatile X&,且没有其它参数或其它参数都有默认值,那么这个函数就是该类的拷贝构造函数。
--------------------------------------------------