雖然對象複製看上去很簡單,然而如果你沒有對其正確理解,可能會出現一些嚴重問題。預設情況下,複製對象會導致相應的所有成員的複製。如果你只有執行個體成員,這看上去是相當不錯的。但是如果你的類中含有指向在堆中分配的對象時,情況會怎樣呢?考慮下面的代碼片斷:
#include <stdio.h> #include <string.h> class Person { private: char* _name; public: Person() { _name = new char[256]; } void SetName(const char* name) { if(strlen(name) + 1 < 256) strcpy(_name,name); } void PrintName() { printf("%s\n",_name); } }; int main() { // 建立對象的第一個執行個體並賦於名字為John Person p1; p1.SetName("John"); p1.PrintName(); //通過複製p1引用的對象建立另一個對象 Person p2(p1); p2.SetName("Alice"); p2.PrintName(); //現在再輸出p1的名字 p1.PrintName(); scanf("q"); return 0; } |
這裡的類Person有一個指向在堆上分配的字元數組的指標。當構造Person對象時,它建立該字元數組並把它的位置存放到變數_name中。
但是當你建立Person 對象 p2 時,p2的成員用p1的成員初始化。因而,p1的 _name與p2的 _name指向相同的堆對象。如在上例中看到的,調用p2.SetName將改變由這兩個類共用的值。所以,當第二次調用p1.PrintName,列印結果是"Alice"。
所以,這不是我們複製對象所期望的結果,而且還會導致堆崩潰的問題。請再考慮某個函數刪除了該數組而p1又要調用該函數的情況?下面,當p2調用PrintName時,它將盡量存取實際上不是在堆上的對象。這種情況下產生的結果往往是難以預料的。
C++允許我們通過定義拷貝建構函式來克服這類問題。在我們每次通過複製另一個對象來初始化一個對象時,拷貝建構函式都被執行。你可以在拷貝建構函式中覆蓋掉預設的成員函數的複製行為。
所以,我們的類Person應該修改如下:
class Person { private: char* _name; public: Person() { _name = new char[256]; } // 這是拷貝建構函式。在此我們初始化一個新的數組,為Person的執行個體所用 Person(Person&) { _name = new char[256]; } void SetName(const char* name) { if(strlen(name) + 1 < 256) strcpy(_name,name); } void PrintName() { printf("%s\n",_name); } }; |
這裡類Person中的拷貝建構函式保證了它初始化一個新的數組,為在複製時產生的每一個對象執行個體所用。這就避免了前面我們提到的問題。