[C++]拷贝构造函数
一.拷贝构造函数
拷贝构造函数也是类的一个重载版本的构造函数,它能够用一个已知的对象初始化一个被创建的同类新对象。该函数的形式参数是本类对象的常引用,因此与普通构造函数在形式参数上有非常明显的区别。跟构造函数一样,C++为每一个类定义了一个默认的拷贝构造函数,可以实现将实参对象的数据成员值复制到新创建的当前对象对应的数据成员中。用户可以根据需要定义自己的拷贝构造函数,从而实现同类对象之间数据成员的值传递。
拷贝构造函数的定义格式如下:
class 类名
{
public:
类名(const 类名&对象名);
...
};
拷贝构造函数是一种特殊的构造函数,在创建类的对象时如果实际参数是本类的对象,则调用拷贝构造函数。
以下3种情况系统自动调用拷贝构造函数:
- 明确表示由一个对象初始化另一个对象时;
- 当对象作为函数的实际参数传递给函数的值形式参数时。注意,如果形式参数时引用参数或指针参数,都不会调用拷贝构造函数,因为此时不会产生新对象;
- 当对象作为函数返回值时。
给一个程序帮助理解:
1 #include"stdafx.h" 2 #include<iostream> 3 using namespace std; 4 5 class TDate 6 { 7 private: 8 int year,month,day; 9 public: 10 TDate(int y=2013,int m=9,int d=28); 11 TDate(const TDate &date); 12 ~TDate() 13 { 14 cout<<"Deconstructor called."<<endl; 15 } 16 void Print(); 17 }; 18 19 TDate::TDate(int y,int m,int d) 20 { 21 year=y; 22 month=m; 23 day=d; 24 cout<<"Constructor called."<<endl; 25 } 26 27 TDate::TDate(const TDate &date) 28 { 29 year=date.year; 30 month=date.month; 31 day=date.day; 32 cout<<"Copy Constructor called."<<endl; 33 } 34 35 void TDate::Print() 36 { 37 cout<<year<<"."<<month<<"."<<day<<endl; 38 } 39 40 TDate f(TDate Q) 41 { 42 TDate R(Q); 43 return Q; 44 } 45 46 void main() 47 { 48 TDate day1(2013,1,1);//调用普通带参构造函数 49 TDate day3;//调用普通构造函数,使用默认参数值 50 TDate day2(day1);//调用拷贝构造函数 51 TDate day4=day2;//调用拷贝构造函数 52 day3=day2;//复制语句,不调用任何构造函数 53 day3=f(day2);//实参传值给形参Q调用拷贝构造函数 54 //f内部定义对象R(Q)时调用拷贝构造函数 55 //返回Q调用拷贝构造函数 56 day3.Print();
前3个析构函数是f函数调用结束时引起的,后4个析构函数是day1-day4生命期结束调用的,他们的调用顺序与构造函数完全相反。
二.深拷贝与浅拷贝
系统为每一个类提供的默认拷贝构造函数,可以实现将源对象所有数据成员的值逐一赋值给目标对象相应的数据成员。如果讲上面程序中拷贝构造函数的原型声明及定义去掉,并不影响程序的正确执行,结果如下:
可以看到,析构函数调用了7次,说明有5次调用的是析构函数。那么什么时候必须为类定义拷贝构造函数呢?
通常,如果一个类包含指向动态存储空间指针类型的数据成员,并且通过该指针在构造函数中动态申请了空间,则必须为该类定义一个拷贝构造函数,否则在析构时容易出现意外错误。
1 #include"stdafx.h" 2 #include<iostream> 3 using namespace std; 4 5 class String 6 { 7 private: 8 char *S; 9 public: 10 String(char *p=0); 11 // String(const String &s1); 12 ~String(); 13 void Show(); 14 }; 15 16 String::String(char *p) 17 { 18 if(p) 19 { 20 S=new char[strlen(p)+1]; 21 strcpy(S,p); 22 } 23 else S=0; 24 } 25 /* 26 String::String(const String &s1) 27 { 28 if(s1.S) 29 { 30 S=new char[strlen(s1.S)+1]; 31 strcpy(S,s1.S); 32 } 33 else S=0; 34 } 35 */ 36 String::~String() 37 { 38 if(S) delete[]S; 39 } 40 41 void String::Show() 42 { 43 cout<<"S="<<S<<endl; 44 } 45 46 void main() 47 { 48 String s1("teacher"); 49 String s2(s1); 50 s1.Show(); 51 s2.Show(); 52 }
该程序在编译无error也无warning,但在执行后会报错,中断执行。因为在执行String s1("teacher");语句时,构造函数动态地分配存储空间,并将返回的地址赋给对象s1的成员S,然后把teacher的内容拷贝到这块空间。
由于String没有定义拷贝构造函数,因此当语句Sttring s2(s1);定义对象s2时,系统将调用默认的拷贝构造函数,负责将对象s1的数据成员S中存放的地址值赋值给对象s2的数据成员S。
上图中,对象s1复制给对象s2的仅是其数据成员S的值,并没有把S所指向的动态存储空间进行复制,这种复制称为浅拷贝。
浅拷贝的副作用是在调用s1.Show();与s2.show();时看不出有什么问题,因为两个对象的成员S所指向的存储区域是相同的,都能正确访问。但是,当遇到对象的生命期结束需要撤销对象时,首先由s2对象调用析构函数,将S成员所指向的字符串teacher所在的动态空间释放,其数据成员S成为悬挂指针。那么在s1自动调用析构函数的时候,无法正确执行析构函数代码delete[]S,从而导致出错。
我们通过定义拷贝函数实现深拷贝可以解决浅拷贝所带来的指针悬挂问题。深拷贝指不复制指针值本身,而是复制指针所指向的动态空间中的内容。这样,两个对象的指针成员就拥有不同的地址值,指向不同的动态存储空间首地址,而两个动态空间的内容完全一样。
在上面的程序中添加被注释的语句,在执行String s2(s1);时,使用对象s1去创建对象s2,调用自定义的拷贝构造函数,当前新对象通过数据成员S另外申请了一快内存,然后将已知对象s1的数据成员S复制到当前对象s2的S所指向的内存空间。
此时运行程序在析构时不存在指针悬挂的问题,程序可以正确运行。