Lab 8-1: Copy ctor (拷貝建構元)

 
實習目標 瞭解拷貝建構元 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.print() 也會輸出正確的結果

不幸的事情發生在離開 doSomething() 函式的那一刻, 由於 data 物件是 doSomething() 函式內區域性的變數, 所以在離開時會解構掉, 根據 Vector 類別的解構元

    Vector::~Vector()
    {
        delete[] m_data;
    }
CPU 會將 data.m_data 所指到的記憶體刪除, 也就產生如下圖的 dangling pointer (dataHolder.m_data)
雖然在 dataHolder.m_data 指標變數裡還記著一個記憶體位址, 可是其實所配置的記憶體已經還給系統, 系統並沒有保留給 dataHolder 使用

在 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(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