对象的复制
对于普通类型的对象来说,它们之间的复制是很简单的,例如:
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类型,用来约束作为参数的对象在构造新对象中是不能被改变的。
略一归纳:类X的复制构造函数的形式为X(X& x)。
当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,复制构造函数就会被自动调用。也就是说,当类的对象需要复制时,复制构造函数将会被调用。以下情况都会调用复制构造函数:
- 一个对象以值传递的方式传入函数体
- 一个对象以值传递的方式从函数返回
- 一个对象需要通过另外一个对象进行初始化。
如果在类中没有显式地声明一个复制构造函数,那么,编译器将会自动生成一个默认的复制构造函数,该构造函数完成对象之间的浅复制,后面将进行说明。
自定义复制构造函数是一种良好的编程风格,它可以阻止编译器形成默认的复制构造函数,提高源码效率。
浅复制和深复制
所谓浅复制,如同上面出现过的构造函数中处理的一样,直接为数据成员赋值即可。在很多情况下,这是可以的。创建新的对象,要为对象的数据成员分配存储空间,直接赋值就将值保存在相应的空间中。
然而,这种浅复制,却并不能通行天下,下面的程序中,浅复制带来了问题。
#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);时,同样的事情还要发生。
str这样的野指针是多么的霸道!在有的系统里,这样的行为是被接受的,可以想到其有多危险。有些系统中,不允许这样的事情发生的。于是,上面的程序在codeblock中调试会出错。这个错来的好。
解决这样的问题的方法,就是在构造函数中,要为指针类型的成员,分配专门的空间。以这条规则构建的复制,称作为深复制!
上面的程序,改写为:
#include <iostream> #include <cstring> using namespace std; class Test { private: int a; char *str; public: Test(int b, char *s) { a=b; str=new char[strlen(s)+1]; //分配str指向的空间,其长度根据s指向的字符串定。为何加1?字符串结束要用\0 strcpy(str,s); //前一程序的肇事地点,祸端已经由上一句摘除 } Test(const Test& C) { a=C.a; str=new char[strlen(C.str)+1]; //同上,这样str就不是野指针了 strcpy(str,C.str); } ~Test() { delete []str; } void show () { cout<<a<<","<<str<<endl; } }; int main() { Test a(100,"hello"); Test b(a); a.show(); b.show(); return 0; }好了,a和b对象的str成员,明确地给分配了空间,他们再不是野指针了。因为明确地分配了空间, 析构函数中要释放对应的空间。我们不能用野指针,当然,也不能对象要撤销了,还占着空间不放,做事不能这样不厚道。
深复制就体现在第13和第19行分配指针指向的空间,这段空间的地址,也将是指针的值(分清指针的值和指针指向的值)。
再一个深复制的例子
下面再给一个例子,类A的数据成员可以保存len个整型数据。类中的数据成员arrayAddr是指向整型的指针,可以作为一个一元数组的起始地址。这个类有指针数据成员,构造函数的定义中,必须要采用深复制的方法,第16行体现了这一点。另外,析构函数中完成了对分配的空间的释放
#include<iostream> using namespace std; class A { private: int *arrayAddr;//保存一个有len个整型元素的数组的首地址 int len; //记录动态数组的长度 public: A(int *a, int n); ~A(); int sum(); }; A::A(int *a, int n) { len=n; arrayAddr=new int[n]; //为指针数据成员分配空间,注意,没有上面例子中加1那回事 for(int i=0; i<n; i++) //逐个地将a指向的值逐个地复制过来 { arrayAddr[i]=a[i]; } } //析构函数的类外定义,释放指针型数据a所指向的空间 A::~A() { delete [] arrayAddr; } int A::sum() //获得a指向的数组中下标为i的元素的值 { int s=0; for(int i=0; i<len; i++) //逐个地将a指向的值逐个地复制过来 { s+=arrayAddr[i]; } return s; } int main(){ int b[10]= {75, 99, 90, 93, 38, 15, 5, 7, 52, 4}; A r1(b,10); cout<<"和:"<<r1.sum()<<endl; int c[15] = {18,68,10,52,3,19,12,100,56,96,95,97,1,4,93}; A r2(c,15); cout<<"和:"<<r2.sum()<<endl; return 0; }
==================== 迂者 贺利坚 CSDN博客专栏================= |== IT学子成长指导专栏 专栏文章的分类目录(不定期更新) ==| |== C++ 课堂在线专栏 贺利坚课程教学链接(分课程年级) ==| |== 我写的书——《逆袭大学——传给IT学子的正能量》 ==| ===== 为IT菜鸟起飞铺跑道,和学生一起享受快乐和激情的大学 ===== |