學習C++ -> 復制構造函數
一、復制構造函數的介紹
在一般的數據類型中, 我們經常會用一個變量來初始化另一個變量, 例如:
int a = 10; int b = a;
使用a變量來初始化b變量, 同樣, 對於類創建的對象也可以用這種方式使用一個對象去初始化另一個對象。例如還在上篇中介紹的 Point 類中, 使用一個對象去初始化另一個對象:

1 //Point.h 2 #include <iostream> 3 4 class Point 5 { 6 public: 7 Point(int x = 0, int y = 0):xPos(x), yPos(y) {} 8 void printPoint() 9 { 10 std::cout<<"xPos = "<< xPos <<std::endl; 11 std::cout<<"yPOs = "<< yPos <<std::endl; 12 } 13 14 private: 15 int xPos; 16 int yPos; 17 };
1 #include "Point.h" 2 3 int main() 4 { 5 Point M(10, 20); 6 Point N = M; //使用對象 M 初始化對象 N 7 N.printPoint(); 8 9 return 0; 10 }
編譯運行的結果:
xPos = 10 yPOs = 20 Process returned 0 (0x0) execution time : 0.462 s Press any key to continue.
上面代碼使用Point類創建了一個對象 M,初始化xPos, yPos為 10和20, 在 Point N = M; 這行中創建了一個對象 N 並且已經初始化好的 M 來初始化它, 所以當對象 N 在調用 printPoint 時輸出的結果與 M 的xPos, yPos值相同。
語句 Point N = M; 也可以寫成 Point N(M); 的形式。在執行該句時就相當於將 M 中每個數據成員的值賦值到對象 N 中相對應的成員數據中。當然, 這只是表面現象, 實際上系統調用了一個復制構造函數來完成這部分的動作, 當類中沒有顯式定義該復制構造函數時, 編譯器會默認為其生成一個默認復制構造函數, 也稱拷貝構造函數, 該函數的原型如下:
Point::Point( const Point & );
也可以將該復制構造函數看做是一個普通的構造函數, 只不過是函數的形參不同罷了, 其復制構造函數的形參為本類對象的引用類型。
二、默認復制構造函數的不足
盡管有默認的復制構造函數來解決一般對象與對象之間的初始化問題, 但是在有些情況下我們必須手動顯式的去定義復制構造函數, 例如:
1 #include <iostream> 2 #include <cstring> 3 4 using namespace std; 5 6 class Book 7 { 8 public: 9 Book( const char *name ) 10 { 11 bookName = new char [strlen(name)+1]; //使用 new 申請 strlen(name)+1 大小的空間 12 strcpy(bookName, name); 13 } 14 ~Book() { delete []bookName; } //釋放申請的空間 15 void showName(){ cout<<"Book name: " << bookName << endl; } 16 17 private: 18 char *bookName; 19 }; 20 21 int main() 22 { 23 Book CPP("C++ Primer"); 24 Book T(CPP); //使用 CPP 初始化對象 T 25 CPP.showName(); 26 CPP.~Book(); //手動釋放對象CPP所申請的空間 27 28 T.showName(); 29 30 return 0; 31 }
編譯運行的結果:
Book name: C++ Primer Book name: Process returned 0 (0x0) execution time : 0.281 s Press any key to continue.
按照前面的思路, 使用 CPP 對象對 T 對象進行初始化后, 那么 T 對象的 bookName 屬性理論上來說也是 "C++ Primer", 但是從輸出結果來看在輸出 CPP 對象的 bookName 屬性時是正常的, 而 T 對象的 bookName 輸出有問題, 正確的情況下應該也是 "C++ Primer", 不過此時輸出的卻是空白。
這正是構造函數的一點不足之處, 造成這種現象的原因在於 CPP.~Book(); 這行, 還原下該默認復制構造函數的實現:
Book( const Book &obj ) { bookName = obj.bookName; }
可以看到, 實際上當用 CPP 對象來初始化 T 對象時, 默認復制構造函數只是簡單的將 CPP 對象的 bookName 賦值給 T 對象的 bookName, 換句話說, 也就是只是將 CPP 對象的 bookName 指向的空間地址賦值給 T 的 bookName, 這樣一來, T 對象的 bookName 和 CPP 對象的 bookName 就是指向同一處內存單元, 當 CPP 對象調用析構函數后, CPP 的 bookName 所指向的內存單元就會被釋放, 由於 T 對象 bookName 與 CPP 對象的 bookName 指向的是同一處內存, 所以此時 T 對象的 bookName 指向的內存就變成了一處不可用的非法內容(因為已經釋放), 所以在指向的內存被釋放的情況下進行輸出勢必會造成了輸出的錯誤。
一般來說, 當類中含有指針型的數據成員、需要使用動態內存的, 最好手動顯式定義復制構造函數來避免該問題。
三、顯式定義復制構造函數
顯式定義復制構造函數的步驟非常簡單, 只要記得函數的參數是 本類成員的引用 就行, 雖然也可以通過指針來實現, 但是不推薦這樣做, 指針在某種程度上來說要比引用危險。顯式定義復制構造函數解決上例中的問題:
1 #include <iostream> 2 #include <cstring> 3 4 using namespace std; 5 6 class Book 7 { 8 public: 9 Book( const char *name ) 10 { 11 bookName = new char [strlen(name)+1]; 12 strcpy(bookName, name); 13 } 14 Book( const Book &obj ) //顯式定義復制構造函數 15 { 16 bookName = new char [strlen(obj.bookName)+1]; //調用復制構造函數時再次申請一處新的空間 17 strcpy(bookName, obj.bookName); 18 } 19 ~Book() { delete []bookName; } //釋放申請的空間 20 void showName(){ cout<<"Book name: " << bookName << endl; } 21 22 private: 23 char *bookName; 24 }; 25 26 int main() 27 { 28 Book CPP("C++ Primer"); 29 Book T = CPP; //使用 CPP 初始化對象 T 30 CPP.showName(); 31 CPP.~Book(); //手動釋放對象CPP所申請的空間 32 33 T.showName(); 34 35 return 0; 36 }
編譯運行的結果:
Book name: C++ Primer Book name: C++ Primer Process returned 0 (0x0) execution time : 0.281 s Press any key to continue.
在該示例中我們顯式定義了復制構造函數來代替默認復制構造函數, 在該復制構造函數的函數體內, 不是再直接將源對象所申請空間的地址賦值給被初始化的對象, 而是自己獨立申請一處內存后再將源對象的屬性復制過來, 此時 CPP 對象的 bookName 與 T 對象的 bookName 就是指向兩處不同的內存單元, 這樣即便是源對象 CPP 被銷毀后被初始化的對象 T 也不會再受到影響。
--------------------
wid, 2013.02.20
上一篇: 學習C++ -> 構造函數與析構函數