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裡 Complex 類別的設計, 替它加上幾個 overloaded functions

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

步驟二

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

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

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

    Complex y;

    y.setValue(x); // 由 x 中拷貝資料到 y 中 (把 y 物件的內容設定為 x 物件的內容)

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

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

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

請將函式參數型態改為

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

注意: print 函式的參數型態是 ostream, (需要 #include <ostream>, 也需要 using namespace std;) 雖然希望接收的參數希望是類似下面定義的 outfile 檔案串流物件, 型態是 ofstream:

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

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

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

    cout << x;

對 C++ 編譯器來說 cout << x; 相當於是 cout.operator<<(x); 的函式呼叫, 你可以用 debugger 來確定 
(把中斷點設在 cout << x; 然後逐步執行到函式裡, 你就可以看到執行到哪一個函式)
ostream 這個類別不是我們自己寫的, 我們不可能去增加它的成員函式, 但是我們可以藉由覆蓋 operator<<() 這個全域函式來讓我們感覺好像是擴充了 ostream 這個類別的功能, 我們希望可以這樣子使用:
    Complex x, y;

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

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

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

        ...

        return os;

    }

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

  1. 把 Complex::m_real 和 Complex::m_imaginary 兩個資料成員改成 public 的。
    當然不建議用這種方法, 這樣子做不就沒有封裝了嗎? 物件內部的資料變成可以被任意程式修改...

  2. 替 Complex 類別增加兩個 accessor 函式, Complex::getReal() 和 Complex::getImaginary() 函式裡就直接回傳 m_real 和 m_imaginary 的數值
    使用這個方法以後, 基本上別的模組只能夠看到 m_real 和 m_imaginary 的資料,但是不能直接修改, 甚至你也還保留了一些修改實作方法的彈性, 如果有一天你希望 m_real 不是單純一個 double 型態變數, 而是由一個函式由極座標運算出來, 你還是可以自由的修改而不會影響到客戶端程式; 如果有其他選擇的話, 還是不太建議這種用法, 因為我們說出現 accessor 和 mutator 通常代表界面設計並不完整

  3. 將 ostream & ::operator<<(ostream &os, Complex &rhs) 設為 Complex 類別的夥伴函式。也就是在 Complex 的類別宣告中加上下面這一列
    friend ostream &operator<<(ostream &os, Complex &rhs);
    表示 Complex 類別特別允許這個函式存取它的私有的內容, 如此就可以完成上面的功能了 (friend 的語法只在 C++ 中有, Java 中就沒有, friend 語法從某種角度來看是破壞封裝的, 如果有其它選擇, 不建議用此種方法)

  4. 呼叫上一個步驟所作的 Complex::print(ostream &) 函式來完成這個功能, 並且在 main() 函式中測試一下, 注意這個函式應該要放在 Complex.cpp 中, 函式的原型應該要放在 Complex.h 中

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

    Complex x;

    x.setValue(3, 4);

    ofstream file("outputfile.txt");

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

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

請注意:

    cout << "Hello"; 和

    cout.operator<<("Hello");

所呼叫的函式是不一樣的, 所以它們的表現是不一樣的, 你可以用 debugger 來驗證這件事, 事實上 cout << "Hello"; 是呼叫 operator<<(cout, "Hello") 函式來完成的, 而且根本就不存在 ostream::operator<<(char *) 這樣的成員函式, 同樣地 cout << 'h'; 是呼叫 operator<<(cout, 'h');, 根本不存在 ostream::operator<<(char) 這樣的成員函式, 如果你嘗試用 cout.operator<<('h'); 呼叫, 程式會執行, 但是你會發現其實呼叫到的成員函式是 ostream::operator<<(int)。

接下來你也可以嘗試擴充 iostream 中的 extraction operator 函式

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

步驟六 在這個步驟中希望大家更進一步了解和運算子 << 等效的函式呼叫敘述, 首先大家知道
    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&, Complex &) 的全域函式, 所以可以用下面的敘述:

    Complex x, y;

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

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

提示: endl 應該用 cout.operator<<(...), 字元和字串應該用 operator<<(cout, ...), 而我們自己定義的複數類別也應該用 operator<<(cout, ...), 請思考為什麼有的時候用 cout.operator<<(...) 有的時候用 operator<<(cout, ...)?

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

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

        return os;

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

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

    cout << x << endl;
等效的敘述來測試, 程式請保留起來, demo 給助教看, 然後再寫和下列等效的敘述
    cout << x << y << endl;
以及和
    operator<<(operator<<(cout, x), y).operator<<(endl);

等效的敘述來測試, 程式請保留起來, demo 給助教看, 做了這麼些練習後, 應該發覺 參考變數 (reference) 有很多好處了吧!!

Hint: operator<<(operator<<(&cout, x), y)->operator<<endl);

步驟八 請助教檢查後, 將所完成的 專案 (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .suo, .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\ 資料匣下的所有內容) 壓縮起來, 選擇 Lab4-2 上傳, 後面的實習課程需要使用這裡所完成的程式

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

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