
| 實習目標 |
瞭解拷貝建構元 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