对于普通类型的对象来说,它们之间的复制是很简单的,例如:
int a=88;
int b=a;
double f=3.12;
double d(f);
而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种数据成员。
#include <iostream>
using namespace std;
class Test
{
private:
int a,b;
public:
Test(int x, int y) //提供的形式参数,是为了给数据成员直接初始化的
{
a=x;
b=y;
}
Test(const Test& C) //复制构造函数,提供一个同类型对象作为参数
{
a=C.a;
b=C.b;
}
void show ()
{
cout<<a<<" "<<b<<endl;
}
};
int main()
{
Test a(100,10); //执行构造函数Test::Test(int x, int y)
Test b(a); //执行构造函数Test::Test(const Test& C)
Test c=a; //也执行构造函数Test::Test(const Test& C)
b.show();
c.show();
return 0;
}
运行程序,屏幕输出两行100 10。
从以上代码的运行结果可以看出,系统在声明对象b和c时,完成了由对象a的复制。
复制构造函数,就类对象而言,相同类型的类对象是通过复制构造函数来完成整个复制过程的。
上例中的Test::Test(const Test& C),就是我们自定义的复制构造函数。可见,复制构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,用来约束作为参数的对象在构造新对象中是不能被改变的。
当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,复制构造函数就会被自动调用。也就是说,当类的对象需要复制时,复制构造函数将会被调用。以下情况都会调用复制构造函数:
一个对象以值传递的方式传入函数体
一个对象以值传递的方式从函数返回
一个对象需要通过另外一个对象进行初始化。
如果在类中没有显式地声明一个复制构造函数,那么,编译器将会自动生成一个默认的复制构造函数,该构造函数完成对象之间的浅复制,后面将进行说明。
自定义复制构造函数是一种良好的编程风格,它可以阻止编译器形成默认的复制构造函数,提高源码效率。
浅复制和深复制
所谓浅复制,如同上面出现过的构造函数中处理的一样,直接为数据成员赋值即可。在很多情况下,这是可以的。创建新的对象,要为对象的数据成员分配存储空间,直接赋值就将值保存在相应的空间中。然而,这种浅复制,却并不能通行天下,下面的程序中,浅复制带来了问题。
#include <iostream>
#include <cstring>
using namespace std;
class Test
{
private:
int a;
char *str;
public:
Test(int b, char *s)
{
a=b;
strcpy(str,s); //肇事地点,但不是祸端
}
Test(const Test& C)
{
a=C.a;
strcpy(str,C.str);
}
void show ()
{
cout<<a<<","<<str<<endl;
}
};
int main()
{
Test a(100,"hello");
Test b(a);
a.show();
b.show();
return 0;
}
程序运行中,会弹出一个窗口:程序的执行意外停止了。面对这个窗口,我们应该有感觉,这和使用指针不当有关系。
我们从main函数看起。
当程序执行到第28行Test a(100,"hello");时,对象a的数据成员a获得实际参数100的值,而数据成员str,即指针,是个随机地址值(指针的值,非指针指向的值)!在程序中,试图通过strcpy(str,s);将形式参数s指向的字符串"hello",复制到str所指向的那个位置,而那个位置,其地址并不是经过系统分配来的,这是个危险的操作。在这里,str这样未经过分配的地址(指针),被称为“野指针”。
在执行第29行Test b(a);时,同样的事情还要发生。