C++ 类 复制构造函数 The Copy Constructor
一、复制构造函数的定义
复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性。复制构造函数创建一个新的对象,作为另一个对象的拷贝。复制构造函数只含有一个形参,而且其形参为本类对象的引用。复制构造函数形如 X::X( X& ), 只有一个参数即对同类对象的引用,如果没有定义,那么编译器生成缺省复制构造函数。
复制构造函数的两种原型(prototypes),以类Date为例,Date的复制构造函数可以定义为如下形式:
Date(Date & );
或者
Date( const Date & );
不允许有形如 X::X( X )的构造函数,下面的形式是错误的:
Date(Date); // error C2652: “Date”: 非法的复制构造函数: 第一个参数不应是“Date”
作用:复制构造函数由编译器调用来完成一些基于同一类的其他对象的构件及初始化。
当我们设计一个类时,若缺省的复制构造函数和赋值操作行为不能满足我们的预期的话,我们就不得不声明和定义我们需要的这两个函数。
例如:
Example class Namelist { public: Namelist( ) { size = 0; p = 0; } Namelist( const string[ ], int ); //…… private: int size; string* p; }; void Namelist :: set( const string& s, int i ) { p[i] = s; } int main( ) { //…… Namelist d1( list, 3 ); Namelist d2( d1 ); d2.set( "Great Dane", 1 ); //…… }
对象d1和d2共享了一个字符串数组,但这不是我们所希望的。 我们希望d1和d2拥有独立的字符串数组。
这里涉及到了一个深拷贝和浅拷贝的问题,可以参照这篇文章https://www.cnblogs.com/always-chang/p/6107437.html
编译系统在我们没有自己定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝!即对指针name拷贝后会出现两个指针指向同一个内存空间。
在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
该为深拷贝后:
class Namelist { public: Namelist( const Namelist& d ) { p = 0; copyIntoP( d ); } //…… private: int size; string* p; void copyIntoP( const Namelist& ); }; void Namelist :: copyIntoP( const Namelist& d ) { delete[ ] p; if( d.p != 0 ) { p = new string[size = d.size]; for( int i = 0; i < size; i++ ) p[i] = d.p[i]; } else { p = 0; size = 0; } }
如果程序员不提供一个复制构造函数,则编译器会提供一个。编译器版本的构造函数会将源对象中的每个数据成员原样拷贝给目标对像的相应数据成员。
#include <iostream> using namespace std; class Complex { public: Complex(double r, double i) { real = r; imag = i; } void show() { cout<<"real = "<<real<<" imag = "<<imag<<endl; } private : double real, imag; }; int main( ) { Complex c1(5,10); //调用构造函数Complex(double r, double i) Complex c2(c1); //调用缺省的复制构造函数,将 c2 初始化成和c1一样 c2.show(); return 0; }
二、复制构造函数的调用
复制构造函数在以下三种情况被调用:
(1)一个对象需要通过另外一个对象进行初始化,例如:
#include <iostream> using namespace std; class Complex { public: Complex(double r, double i) { real = r; imag = i; } Complex( Complex & c) { real = c.real; imag = c.imag; cout<<"copy constructor!"<<endl; } private : double real, imag; }; int main( ) { Complex c1(1,2); //调用构造函数Complex(double r, double i) Complex c2(c1); // 调用复制构造函数Complex( Complex & c) Complex c3 = c1; // 调用复制构造函数Complex( Complex & c) return 0; }
程序执行结果为:
copy constructor!
copy constructor!
(2)一个对象以值传递的方式传入函数体
函数的形参是类的对象,调用函数时,进行形参和实参的结合。
如果某函数有一个参数是类Complex的对象,那么该函数被调用时,类Complex的复制构造函数将被调用。
void func(Complex c) { }; int main( ) { Complex c1(1,2); func(c1); // Complex的复制构造函数被调用,生成形参传入函数 return 0; }
程序执行结果为:
copy constructor!
(3)一个对象以值传递的方式从函数返回
除了当对象传入函数的时候被隐式调用以外,复制构造函数在对象被函数返回的时候也同样的被调用。换句话说,你从函数返回得到的只是对象的一份拷贝。
Complex func() { Complex c1(1,2); return c1; // Complex的复制构造函数被调用,函数返回时生成临时对象 }; int main( ) { func(); return 0; }
程序执行结果为:
copy constructor!
注意:对象间用等号赋值并不导致复制构造函数被调用!C++中,当一个新对象创建时,会有初始化的操作,而赋值是用来修改一个已经存在的对象的值,此时没有任何新对象被创建。初始化出现在构造函数中,而赋值出现在operator=运算符函数中。编译器会区别这两种情况,赋值的时候调用重载的赋值运算符,初始化的时候调用复制构造函数。
#include <iostream> using namespace std; class CMyclass { public: int n; CMyclass( ) {}; CMyclass( CMyclass & c) { n = 2 * c.n ; } }; void main( ) { CMyclass c1,c2; c1.n = 5; c2 = c1; // 对象间赋值 CMyclass c3(c1); // 调用复制构造函数 cout <<"c2.n=" << c2.n << endl; cout <<"c3.n=" << c3.n << endl; }
程序执行结果为:
c2.n=5
c3.n=10
上例中,执行c2 = c1时并没有调用复制构造函数,只是进行了下内存拷贝,因此c2.n的值为5。赋值操作是在两个已经存在的对象间进行的(c2和c1都是已经存在的对象)。而初始化是要创建一个新的对象,并且其初值来源于另一个已存在的对象。执行CMyclass c3(c1)时会调用复制构造函数,因此c3.n的值为10。
#include <iostream> using namespace std; class CMyclass { public: CMyclass( ) {}; CMyclass( CMyclass & c) { cout << "copy constructor" << endl; } ~CMyclass( ) { cout << "destructor" << endl; } }; void fun(CMyclass obj_ ) { cout << "fun" << endl; } CMyclass c; CMyclass Test( ) { cout << "test" << endl; return c; } void main() { CMyclass c1; fun(c1); Test(); }
程序执行结果为:
copy constructor//传参
fun
destructor //参数消亡
test
copy constructor//return
destructor // 返回值临时对象消亡
destructor // 局部变量消亡
destructor // 全局变量消亡
三、私有的复制构造函数
在有些应用中,不允许对象间的复制操作,这可以通过将其复制构造函数声明为private,同时不为之提供定义来做到。
class A { public: A ( ) {}; private: A (A & ) {}; }; int main( ) { A a1; A a2(a1); // error C2248: “A::A”: 无法访问 private 成员(在“A”类中声明) A a3 = a1; // error C2248: “A::A”: 无法访问 private 成员(在“A”类中声明) return 0; }
如果一个类的复制构造函数是private,顶层函数(top-level functions)或是其它类中的成员 函数不能按值传递或是返回此类的对象,因为这需要调用复制构造函数。(If the copy constructor is private, top-level functions and methods in other classes cannot pass or return class objects by value precisely because this requires a call to the copy constructor.)在你不想对这个类的对象进行随意复制时,最好将复制构造函数声明为私有,同时最好也将operator=()也声明为私有(详见运算符重载)。
复制构造函数通常是在函数参数出现值传递时发生,而这种传值方式是不推荐的(推荐的是传递引用,尤其是对于类对象来说),所以可以声明一个空的私有的复制构造函数,这样当编译器试图使用复制构造函数时,就会报错,从而防止了值传递造成不可预知的后果。
例:
class A { public: A ( ) {}; private: A (A & ) {}; }; A a; void fun1(A & obj ) { } void fun2(A obj ) { } A & fun3( ) { return a; } A fun4() { return a; // error C2248: “A::A”: 无法访问 private 成员(在“A”类中声明) } int main( ) { A a1, a2; fun1(a1); fun2(a1); // error C2248: “A::A”: 无法访问 private 成员(在“A”类中声明) a2 = fun3( ); a2 = fun4( ); return 0; }