實習目標 | 瞭解拷貝建構元 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"; }在我的機器上 (Visual Studio 2010, XP sp3) 執行結果如下: 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 0 1001 1002 1003 1004 1005 1006 1007這個結果和你預期的相同嗎? 怎麼會這樣? (實際結果和你執行的平台, 編譯器相關) 請再看一下 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 2010 有編譯時候的錯誤? (VC 2008 以前更慘, 這會是執行時的錯誤) 該不會是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) { // 內容當然就是拷貝 src.m_size, // 配置足夠大的記憶體, // 寫一個迴圈拷貝 src.m_data[0] ... src.m_data[m_size-1] }請記得使用初始化串列 |
步驟五 | 第二個程式的錯誤還是和拷貝建構元與解構元有關, 只是這次是 fstream
函式庫中的 ofstream 類別, 你能夠嘗試解釋編譯的錯誤訊息嗎?
你仔細看錯誤的訊息會發現是一個合成拷貝建構元的錯誤, VC2010 的 ofstream 類別, 不允許你合成拷貝建構元, 也就是說 ofstream 的串流物件是不允許你在程式裡拷貝好幾份的, 你也許在第一份 ofstream 串流物件裡由第 10 個位元組開始寫進去 10 個位元組, 但是在第二份 ofstream 串流物件裡由第 3 個位元組開始寫進去 7 個位元組, 一個檔案串流的寫入不能有兩個讀寫位置,另外在解構拷貝的 os 物件時會將相對應的檔案關閉, 也會造成透過另外一個 ofstream 串流物件讀寫時的錯誤, 所以在合成拷貝建構元的時候會失敗, 合成出來的程式碼沒有辦法編譯!! 解決的辦法和上一個程式的第一種方法相同, 第二種方法則不行, 主要的原因有二, 第一是 ofstream 是 fstream 函式庫中的類別, 你沒有辦法替它定義一個新的拷貝建構元 (請參考 cpluscplus:The copy constructor (3) is explicitly deleted (as well as the copy assignment overload of operator=).), 那麼為什麼製作 fstream 函式庫的人不替它定義一個安全一點的 copy ctor 呢? 因為就算你能夠替它定義一個拷貝建構元, 幫這個串流物件重新開啟一個檔案, 拷貝原來檔案的內容過來似乎不是一個經濟的作法。 在 VC2010 中編譯的錯誤訊息如下:
請修改程式, 更正它的錯誤, 另外你能夠用指標 ofstream * 來完成上面的修改嗎? |
步驟六 | 請助教檢查後, 將所完成的 專案 (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .suo, .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\ 資料匣下的所有內容) 壓縮起來, 選擇 Lab8-1 上傳, 後面的實習課程可能需要使用這裡所完成的程式 |
如果你的類別裡自行配置記憶體來存放資料, 或是你運用到其它自行配置記憶體的類別物件當作成員,
那麼你應該要自己寫 copy ctor, 否則使用這個類別的程式只要用到 call-by-value 時就會發生意外, 例如 ofstream
類別就是這樣, 如果你所設計的類別不希望客戶程式在無意間使用 call-by-value 的話, 也可以把一個空的 copy ctor 宣告成
private 的, 例如:
class Vector { ... private: Vector(Vector &src) {} };如此客戶程式碼要用到 call-by-value 時, compiler 會告訴它 "無法使用 copy ctor", 強迫它用 call-by-reference, 不過用這種方法時, 如果是類別裡自己使用 call-by-value 的話, compiler 就不會幫你偵測出來了。 你可以告訴我為什麼 copy ctor X(X&) 的參數需要用 "參考 X&" 嗎? 為什麼不用 call-by-value? |
|
請下載下列程式專案, 編譯, 在 VC2010,
VC2008 裡是有編譯錯誤的, 想一想原因, 試看看你能不能找到正確的解釋, 修改看看
專案 1 (提示: X(X&) 和 X(const X&) 是兩個不同的拷貝建構元) |
回
C++ 物件導向程式設計課程 首頁 製作日期: 03/13/2013 by 丁培毅 (Pei-yih Ting) E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615 海洋大學 電機資訊學院 資訊工程學系 Lagoon |