一、復制構造函數的定義
復制構造函數是一種特殊的構造函數,具有一般構造函數的所有特性。復制構造函數創建一個新的對象,作為另一個對象的拷貝。復制構造函數只含有一個形參,而且其形參為本類對象的引用。復制構造函數形如 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; }