實習目標 |
瞭解拷貝建構元 copy ctor 的用途
瞭解什麼時候該定義 copy ctor, 曉得該定義時沒有定義的後果 練習定義拷貝建構元 |
---|---|
步驟一 |
並不是每一個你製作的類別都需要定義 copy ctor,
例如下面這個類別你就不需要定義 copy ctor:
class MyClass { public: MyClass(double x, int size, char cdata, float fdata); void print(); private: double m_data1; int m_data2; char m_data3[100]; vector<float> m_data4; }; MyClass::MyClass(double x, int size, char cdata, float fdata) : m_data1(x), m_data2(size) { int i; for (i=0; i<m_data2; i++) { m_data3[i] = cdata; m_data4.push_back(fdata); } } void MyClass::print() { int i; cout << endl; cout << "m_data1=" << m_data1 << endl; cout << "m_data2=" << m_data2 << endl; for (i=0; i<m_data2; i++) cout << "m_data3["<<i<< "]=" << m_data3[i] << endl; for (i=0; i<m_data2; i++) cout << "m_data4["<<i<< "]=" << m_data4[i] << endl; }如果你定義一個 copy ctor 如下, MyClass::MyClass(MyClass &src) : m_data1(src.m_data1), m_data2(src.m_data2), m_data4(src.m_data4) { int i; cout << "entering copy ctor\n"; // 確定一下真的有呼叫 for (i=0; i<100; i++) m_data3[i] = src.m_data3[i]; }你所做的其實和 compiler 自動幫你做的一模一樣, 可以不需要做, 不相信的話你可以寫一小段程式測試看看, 例如: void main() { MyClass x1(1.234, 3, 'a', 12.3); MyClass x2(x1); x1.print(); x2.print(); }竟然在作白工, compiler 到底還有幫忙做了些什麼事呢? 還是有什麼特殊情況要求一定要自己定義 copy ctor 呢? |
步驟二 |
請下載 testCtor1.cpp 編譯並且執行
這個程式裡主要定義一個 Vector 類別封裝一個整數陣列 class Vector { public: Vector(int size); ~Vector(); int &operator[](int index); void print(); private: int *m_data; int m_size; };main() 函式裡主要內容如下: // 產生一個 dataHolder 物件可以放 20 個整數 // 設定 20 個數值, 並且列印 Vector dataHolder(20); for (i=0; i<20; i++) dataHolder[i] = 2*i; dataHolder.print(); // 將 dataHolder 傳入函式 doSomething() 中 doSomething(dataHolder); // 在此函式中作一些和 dataHolder 物件沒有關係的事 doIrrelevantThings(); // 再把 dataHolder 物件內的資料印一遍 dataHolder.print();doSomething() 和 doIrrelevantThings() 兩個函式內容如下: void doSomething(Vector data) { cout << "entering function doSomething()...\n"; data.print(); cout << "leaving function doSomething()\n\n"; } void doIrrelevantThings() { int i; cout << "entering function doIrrelevantThings()...\n"; int *ptr=new int[20]; for (i=0; i<20; i++) ptr[i] = 1000 + i; delete[] ptr; cout << "leaving function doIrrelevantThings()\n\n"; }在我的機器上執行結果如下: C:\>testctor1 ============================ m_size=20 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 entering function doSomething()... ============================ m_size=20 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 leaving function doSomething() entering function doIrrelevantThings()... leaving function doIrrelevantThings() ============================ m_size=20 34083363408336 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019這個結果和你預期的相同嗎? 請再看一下 doSomething() 和 doIrrelevantThings() 兩個函式, 看得出來為什麼執行完這兩個函式後 dataHolder 物件的內容會變更嗎??? 這是一個很嚴重的 bug, 到底發生了什麼事? 該怎麼去除這個 bug? 請花一點時間看一下原始程式碼吧? 這個程式裡有一點點 C++ exception 的用法 try-throw-catch, 你可以先忽略它 |
步驟三 |
請下載 testCtor2.cpp 編譯並且執行,
看到那個很討厭的視窗了嗎?
這個程式很短, 內容如下: #include <fstream> #include <iostream> using namespace std; void printMessage(ofstream os) { cout << "entering function printMessage()..." << endl; os << "entering function printMessage()..." << endl; cout << "leaving function printMessage()" << endl; os << "leaving function printMessage()" << endl; } void main() { ofstream outfile("out.txt"); cout << "entering function main()..." << endl; outfile << "entering function main()..." << endl; printMessage(outfile); cout << "leaving function main()" << endl; outfile << "leaving function main()" << endl; }主要是利用 ofstream 開啟一個檔案, 把一些資料寫入檔案中, 並且呼叫一個 printMessage() 函式, 將檔案串流傳入函式內, 函式內也是將一些資料寫入檔案中而已, 這麼簡單的幾列程式, 為什麼竟然有錯? 該不會是VC 壞了吧! 還是系統壞了? 重灌吧? 不! 不! 不! 資訊系的不要隨便下這樣的結論, Visual Studio 或是 Windows 也不過是一支一支程式組合起來的而已, 要有信心!?! |
步驟四 |
上面這兩個程式的錯誤都和 copy ctor 有關,
在第一個程式中,
void doSomething(Vector data) { ... } ... doSomething(dataHolder);dataHolder 物件是以 call-by-value 的方式傳入 doSomething 函式中, 此時 compiler 會呼叫 copy ctor 來把 dataHolder 物件複製一遍, 產生一個新的 data 物件, 由於 Vector 物件沒有定義 copy ctor, compiler 就用預設的 bitwise-copy ctor 來完成這個拷貝的動作, 因此 dataHolder 內的 m_data 指標會被複製一遍, 但是並沒有重新配置一個可以放 20 個整數的陣列, 記憶體內產生下圖的結構: 不幸的事情發生在離開 doSomething() 函式的那一刻, 由於 data 物件是 doSomething() 函式內區域性的變數, 所以在離開時會解構掉, 根據 Vector 類別的解構元 Vector::~Vector() { delete[] m_data; }CPU 會將 data.m_data 所指到的記憶體刪除, 也就產生如下圖的 dangling pointer (dataHolder.m_data) 在 doIrrelevantThings() 函式裡, 我們雖然沒有再去使用到 dataHolder 這個物件, 但是我們又向系統要了一些記憶體來存放資料, 很不幸的是系統就把原本配置給 dataHolder 的那一塊記憶體拿給你使用, 所以你會以為系統很神奇地改變了 dataHolder 內的資料??!!?? 修改的方式有兩種, 第一種方式比較簡單, 就是在這種狀況下不要用 call-by-value, 把 doSomething() 函式的參數改為 void doSomething(Vector &data) { ... }如此在開始執行時不會呼叫拷貝建構元, 離開時也不會呼叫解構元, 就不會發生不幸的事情了, (請測試一下), 不過如果你不希望 doSomething() 函式有 side effects, 也就是你希望就算在函式內修改 data 物件的內容也不要動到 main() 函式內的 dataHolder 物件的話, 這種方法就不行了 第二種方法是替 Vector 類別定義一個 copy ctor, 理想的 Vector 類別物件的拷貝應該如下圖所示: Vector::Vector(Vector &src) |
步驟五 |
第二個程式的錯誤還是和拷貝建構元與解構元有關,
只是這次是 fstream 函式庫中的 ofstream 類別,
你能夠嘗試解釋錯誤的原因嗎?
在函式 printMessage() 內其實沒有出現錯誤, 但是在離開時, 解構 os 物件時會將相對應的檔案關閉, 所以在下一次寫檔時就會發生錯誤 解決的辦法和上一個程式的第一種方法相同, 第二種方法則不行, 主要的原因有二, 第一是 ofstream 是 fstream 函式庫中的類別, 你沒有辦法替它定義一個新的拷貝建構元, 那麼為什麼製作 fstream 函式庫的人不替它定義一個安全一點的 copy ctor 呢? 因為就算你能夠替它定義一個拷貝建構元, 幫這個串流物件重新開啟一個檔案, 拷貝原來檔案的內容過來似乎不是一個經濟的作法。 請修改程式, 更正它的錯誤, 另外你能夠用指標 ofstream * 來完成上面的修改嗎? |
步驟六 | 請將所完成的 project (去掉 debug/ 資料匣下的所有內容) 壓縮起來儲存在 cyber 上你的帳號內, 後面的實習課程可能需要使用這裡所完成的程式 |
如果你的類別裡自行配置記憶體來存放資料,
或是你運用到其它自行配製記憶體的類別物件當作成員,
那麼你應該要自己寫 copy ctor,
否則使用這個類別的程式只要用到 call-by-value 時就會發生意外,
例如 ofstream 類別就是這樣,
如果你所設計的類別不希望客戶程式在無意間使用 call-by-value 的話,
也可以把一個空的 copy ctor 宣告成 private 的, 例如:
class Vector { ... private: Vector(Vcetor &src) {} };如此客戶程式碼要用到 call-by-value 時, compiler 會告訴它 "無法使用 copy ctor", 強迫它用 call-by-reference, 不過用這種方法時, 如果是類別裡自己使用 call-by-value 的話, compiler 就不會幫你偵測出來了。 你可以告訴我為什麼 copy ctor X(X&) 的參數需要用 "參考 X&" 嗎? 為什麼不用 call-by-value? |
回
C++ 物件導向程式設計課程
首頁
製作日期: 04/06/2004
by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw
TEL: 02 24622192x6615
海洋大學
電機資訊學院
資訊工程系
Lagoon