Lab 4-2: New/delete, function overloading, and reference test

 
實習目標 1. 練習使用 new / delete 配置記憶體
2. 練習撰寫 overloaded global function
3. 練習撰寫 overloaded member function
4. 練習撰寫 overloaded operators, ex. operator<<, operator>>
5. 練習使用參考 (reference) 變數
 
步驟一 在上課時我們談到在 C++ 中同一個函式名稱可以定義多個不同的函式, 只要函式所接受的參數有一些不一樣就可以了, 例如:
    void add(int, int);

    void add(int);

    void add(float, float);
這些都是可以同時定義的函式, 你在呼叫這些函式的時候 compiler 不會產生任何混淆, 例如:
    int x, y;

    float r, s;

    add(x);

    add(x, y);

    add(10, y);

    add(r, s);

    add(r, 10);
compiler 都可以根據參數的型態和參數的個數去找到正確的函式來呼叫

在這個練習的第一部份裡我們繼續實習 3.1裡 CComplex 類別的設計, 替它加上幾個 overloaded functions

請下載你上一次實習時的程式檔案, 產生一個新的 project, 把 complex.h 和 complex.cpp 拷貝進來, 並且加入 project 中

步驟二

我們先寫一小段應用程式, 請使用 new 配置一個有 10 個元素的 CComplex 物件陣列, 寫一個迴圈將其中資料設為 (1+i), (2+2i), (3+3i), ..., (10+10i), 再寫一個迴圈將它們都加起來, 將加總結果用 print() 成員函式列印出來, 最後用 delete[] 刪除所配置的記憶體

步驟三 請參考實習 3.1中第五步驟, 在那裡我們定義了一個 setValue(double, double) 的函式, 現在我們要定義一個 setValue(CComplex &src) 的函式,這個函式最主要的目的是為了拷貝一個複數物件 src 到自己這個物件裡, 用法如下:
    CComplex x;

    .... // 計算或是設定 x 的內容

    CComplex y;

    y.setValue(x); // 由 x 中拷貝資料到 y 中

    assert(x.equal(y, 1e-20));
請完成 setValue(CComplex &src) 成員函式的定義 (在函式中應該複製 src.m_real 和 src.m_imaginary 的欄位), 並且在 main() 或是 unitTest() 函式中測試此成員函式的功能

請注意在這裡我們已經開始使用方便的參考變數來取代指標變數了

步驟四 在我們進行下一步驟之前我們需要先修改 實習 3.1 中第九步驟所作的 void CComplex::print() 成員函式, 這個函式本來會把類別裡的資料固定列印到螢幕上去, 現在我們稍微修改一下讓它可以更一般化, 可以列印到不同的資料串流裡

請將函式型態改為

    void CComplex::printFile(ostream &out);
在函式內請將資料寫到傳入的串流變數 out 內, 而不要固定寫到 cout 去。 如果你覺得用同樣函式名稱概念很清楚的話也可以直接定義同名的函式, 就是 overload print() 這個函式

請將函式型態改為

    void CComplex::print(ostream &out);

注意: 不管是 printFile 或是 print 我的參數型態用的是 ostream, 雖然希望接收的參數希望是類似下面定義的 outfile 檔案串流物件, 型態是 ofstream:

    ofstream outfile("outputfile.txt");
    CComplex x;
    ...
    x.print(outfile);

但是有的時候也希望可以直接列印到螢幕去, 到字串裡, 甚至到網路遠端的裝置去, 例如

    x.print(cout)
所以我們就不用 ofstream 而用它的父類別 ostream,這個概念目前還沒有講到,但是你可以想像說 "類別" 是很多同類型物件共同的特徵所抽象化出來的, 我們定義了某一個類別以後就用這個類別來描述所有同類型的物件, "父類別" 則是從很多同類型的 "類別" 抽象化出來的共通特性和介面; ostream 是 ofstream 和 ostrstream 共同的父類別, 所以 ostream 具有 ofstream 和 ostrstream 共同的特性, 所以我們只需要說參數是 ostream 型態的, 實際上傳遞 cout 或是 outfile 給這個函式時參數型態都是對的, 沒有型態不合的問題。進一步更完整的描述要等到我們上到繼承時才會講。
步驟五 接下來我們要練習覆蓋 (overload) 一個全域 (global) 的函式 operator<<(), 在 ostream 類別裡本來就有定義一個成員函式
    ostream &ostream::operator<<(int);
這個函式最主要是給下面的敘述使用的:
    int x;

    cout << x;

    // 對 C++ 編譯器來說相當於是 cout.operator<<(x); 的函式呼叫, 你可以用 debugger 
來確定
ostream 這個類別不是我們自己寫的, 我們不可能去增加它的成員函式, 但是我們可以藉由覆蓋 operator<<() 這個全域函式來讓我們感覺好像是擴充了 ostream 這個類別的功能, 我們希望可以這樣子使用:
    CComplex x, y;

    x.setValue(3, 4); y.setValue(5, 6);

    cout << '(' << x << ") * (" << y << ')';     

    // 在螢幕上列印出 (3 + 4 i) * (5 + 6 i)
函式的定義應該如下:
    ostream &operator<<(ostream &os, CComplex &rhs)
    {

        ...

        return os;

    }

新增上述程式之後, 你可以編譯一下, 然後立刻就發現了一個大問題, 編譯器不允許你在 operator<<() 函式中直接存取 CComplex 類別內的私有資料成員, 這是封裝的特性: 所有的資料成員只有這個類別的成員函式才能夠直接存取, 要克服這個困難, 一般編譯器允許你用四種方法:

  1. 把 CComplex::m_real 和 CComplex::m_imaginary 兩個資料成員改成 public 的。
    當然不建議用這種方法, 這樣子做不就沒有封裝了嗎? 物件內部的資料變成可以被任意程式修改...
  2. 替 CComplex 類別增加兩個 accessor 函式, CComplex::getReal() 和 CComplex::getImaginary() 函式裡就直接回傳 m_real 和 m_imaginary 的數值
    使用這個方法以後, 基本上別的模組只能夠看到 m_real 和 m_imaginary 的資料,但是不能直接修改, 甚至你也還保留了一些修改實作方法的彈性, 如果有一天你希望 m_real 不是單純一個 double 型態變數, 而是由一個函式由極座標運算出來, 你還是可以自由的修改而不會影響到客戶端程式; 如果有其他選擇的話, 還是不太建議這種用法, 因為我們說出現 accessor 和 mutator 通常代表界面設計並不完整
  3. 將 ostream & ::operator<<(ostream &os, CComplex &rhs) 設為 CComplex 類別的夥伴函式。也就是在 CComplex 的類別宣告中加上下面這一列
    friend ostream &operator<<(ostream &os, CComplex &rhs);
    表示 CComplex 類別特別允許這個函式存取他的內容, 如此就可以完成上面的功能了 (friend 的語法只在 C++ 中有, Java 中就沒有, friend 語法從某種角度來看是破壞封裝的, 如果有其它選擇, 不建議用此種方法)
  4. 呼叫上一個步驟所作的 CComplex::print(ostream &) 函式來完成這個功能, 並且在 main() 函式中測試一下, 注意這個函式應該要放在 Complex.cpp 中, 函式的原型應該要放在 Complex.h 中

完成上面的函式之後, 你也可以將一個複數類別的物件以格式化的方式寫到檔案裡面,例如:

    CComplex x;

    x.setValue(3, 4);

    ofstream file("outputfile.txt");

    file << x; // 在檔案裡寫入 3 + 4 i
請測試一下, 程式碼要保留 demo 給助教看

請注意上面的應用完全不需要另外增加別的設計, 這是 C++ 中繼承性質所帶來的好處, 請注意我們前面把 CComplex::print() 改為 CComplex::print(ostream &) 也是為了讓 print 能夠支援任意型態的資料串流, 我們在課程後面一點會詳細地說明。

請注意:

    cout << "Hello"; 和

    cout.operator<<("Hello");

所呼叫的函式是不一樣的, 所以它們的表現是不一樣的, 你可以用 debugger 來驗證這件事, 事實上 cout << "Hello"; 是呼叫 operator<<(cout, "Hello") 函式來完成的。

當然你也可以嘗試擴充 iostream 中的 extraction operator 函式

istream &operator>>(istream &is, CComplex &rhs) { ... return is; }

步驟六 在這個步驟中希望大家了解和 operator 等效的函式呼叫敘述, 首先大家知道
    int x, y;

    cout << x;

    cout << x << y;
可以寫成等效的
    cout.operator<<(x);

    cout.operator<<(x).operator<<(y);
或是
    (cout.operator<<(x)).operator<<(y);
請測試一下

在上面的敘述裡 cout.operator<<(x) 是呼叫一個名稱是 operator<< 的成員函式, 把 x 用參考的型態傳進函式裡, 函式會傳回一個 ostream& 型態的參考, 如上一步驟所示, 事實上是傳回 cout 物件的參考, 所以可以繼續作下一次的呼叫 (...).operator<<(y);

在上一個步驟裡我們定義了 ostream& operator<<(ostream&, CComplex &) 的全域函式, 所以可以用下面的敘述:

    CComplex x, y;

    x.setValue(3, 4); y.setValue(5, 6);

    cout << '(' << x << ")*(" << y << ')';
請用 operator<<() 的語法改寫上面這一列程式

提示: 基本型態應該用 cout.operator<<(...) 而我們自己定義的複數類別和基本型態稍微有一點不同, 應該用 operator<<(cout, ...) 而不是 cout.operator<<(...) 為什麼?

步驟七 在這個步驟中, 希望大家能夠更清楚地知道為什麼要有參考型態的參數傳遞, 請用指標變數來取代參考變數, 定義下面的全域函式
    ostream *operator<<(ostream *os, CComplex src)
    {

        ... // 請列印 [[a + b i]] 以便和另外一個函式有所區別

        return os;

    }
請完成上面的函式的定義, 並且寫一小段和
    CComplex x;

    cout << x;
等效但是運用上面的函式的敘述來測試, (Hint: 應該是 &cout << x;), 然後再寫一個和
    CComplex x;

    cout << x << endl;
等效的敘述來測試, 程式請保留起來, demo 給助教看, 然後再寫和下列等效的敘述
    cout << x << y << endl;
以及和
    ((cout.operator<<(x)).operator<<(y)).operator<<(endl);
等效的敘述來測試, 程式請保留起來, demo 給助教看, 做了這麼些練習後, 應該發覺 參考變數 (reference) 有很多好處了吧!!
步驟八 請助教檢查後, 將所完成的 project (去掉 debug/ 資料匣下的所有內容) 壓縮起來, 選擇 Lab4-2 上傳, 後面的實習課程可能需要使用這裡所完成的程式

C++ 物件導向程式設計課程 首頁

製作日期: 03/02/2004 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程系 Lagoon