Lab 3-1: Complex number: class declaration
practice (data member / member function)

 
實習目標 1. 練習如何運用 Visual Studio 2010 界面宣告類別 (2005/2008, VC6)
2. 練習如何使用 private 與 public access specifier (了解 C++ 如何控制存取權限)
3. 練習如何定義成員函式 (member function), 在成員函式中如何存取類別資料
4. 了解浮點數的比對相關問題
5. 如何拷貝一個簡單類別的物件
 
步驟一 我們曉得 C/C++ 的基本資料型態裡並沒有複數 (complex number) 這樣的東西, 所以我們在這個單元裡要透過 C++ 類別的語法定義一個新的資料型態來存放複數:

直覺的想法應該會覺得這件事情有點簡單, 複數包含實數部份 (real part) 和虛數部份 (imaginary part), 似乎只要用 C 裡頭的 struct 語法就可以定義一個新的資料型態了。

不過我們的要求還稍微多一點, 每一個抽象的資料型態 (abstract data type) 除了包含所存放的資料以外, 還必須要定義它們的運算, 這樣才能精確地描述這個資料型態, 在這個實習中我們考慮下列的運算:

  • 加法 (a + b i) + (c + d i) = (a+c) + (b+d) i
  • 減法 (a + b i) - (c + d i) = (a-c) + (b-d) i
  • 乘法 (a + b i) * (c + d i) = (ac-bd) + (ad+bc) i
  • 除法 (a + b i) / (c + d i) = (ac+bd)/(c*c+d*d) + (-ad+bc)/(c*c+d*d) i
  • 大小 (複數平面上向量的長度) | a + b i | = (a*a+b*b)0.5
  • 相等
步驟二 請利用 Visual Studio 2010 界面 檔案/新增/專案, 選擇一般 , 空專案, 不要勾選 "為方案建立目錄", 選取位置資料夾, 輸入專案名稱 Lab31, 按下確定

方案總管 中, 使用滑鼠右鍵選剛才建立的專案名稱 Lab31, 加入, 新增項目, 選 程式碼, 選 C++ 檔案, 填寫檔案名稱 testComplex.cpp, 請在檔案內寫一個空的 main() 函式:

#include <cstdlib>

void main()
{
    system("pause");
}
建置/建置方案
步驟三
  1. 請使用滑鼠右鍵在 類別檢視 窗格中點選剛才建立的專案名稱 Lab31,
  2. 選擇加入, 類別, 已安裝的範本:C++, 點選 新增 按鈕,
  3. 在 類別名稱 欄位填入 Complex 作為新的類別名稱, 勾選 虛擬解構函式 點選 完成 按鈕

此時你可以檢查 類別檢視, 看到新增了一個 Complex 類別, 在 方案總管 中你會看到 Complex.cpp 和 Complex.h, 請在 類別檢視 中以滑鼠左鍵雙擊 Complex 來查看 Complex.h 檔案內類別的宣告 (declaration),

    #pragma once
    class Complex  
    {
    public:
        Complex(void);
        virtual ~Complex(void);
    };
    
這是 Visual Studio 2010 界面自動幫你作出來的類別定義, 這兩個函式一個叫做 "建構元 (constructor)", 一個叫做 "解構元 (destructor)", 在定義一個類別時有特殊的用途, 將來我們會仔細談它們的用法, 目前對你沒有影響, 你也不需要去修改它們。 至於檔案中 #pragma once 敘述, 和我們先前介紹的 #if !defined(...), #define, #endif 一樣, 是 為了避免重複引入 .h 定義檔 而設計的, 這是所有多檔案 C/C++ 程式都需要有的定義, 如果 Visual Studio 界面不幫你做好, 你也一樣要自己做, #ifndef #if #define #endif 是所有編譯器都確定可以用的, #pragma once 不是標準的前處理器指令, 但是目前常用的編譯器都有支援, Visual Studio 是可以使用的, 其他的編譯器請參考 wiki

請在 類別檢視 中點 Complex 類別, 在下面窗格中你可以看到 Visual Studio 界面幫你定義的兩個成員函式, 所有的函式定義 (definition) 都應該在 .cpp 檔案中, 請點選任何一個函式打開 Complex.cpp 來檢視這兩個函式的定義。

注意: 先前你運用 Visual Studio 寫 C 程式的時候, 不太需要用到 類別檢視 窗格, 但是你在寫 C++ 程式時, 如果不使用類別檢視的話, 你會花費很多很多的時間在許多檔案裡尋找你自己寫的程式碼

步驟四 現在我們應該來定義 Complex 類別內儲存資料的欄位 (資料成員)了, 我們知道複數平面上每個點需要有兩個實數座標, 請務必替它們取適當的名字, 我們把它們取為 m_real 和 m_imaginary, 再來必須要決定它們是 private 還是 public, 目前這個決定很簡單, 就是 private, 除了很小很小部份的資料需要是 public 之外, 幾乎所有的資料成員都需要是 private, 如果你要讓它是 public 的話, 你最好要有很充分的理由。

現在你有兩種選擇來增加這兩個資料成員 (data member),

  1. 直接編輯 Complex.h 檔案: 在 class 定義裡加入 private: 以及兩個資料成員變數的宣告
    private:
        double m_real;
        double m_imaginary;
  2. 透過 Visual Studio 的介面來做: 在 類別檢視 裡找到 Complex 類別, 以右鍵選擇 加入/加入變數, 出現一個對話視窗, 在 存取 欄位中選擇 private, 在 變數型別 欄位中填入可以存放實數座標的型態, 在 變數名稱 欄位中填入剛才取好的變數名稱。 注意在 類別檢視 中你應該立刻可以看到這個資料成員變數, 你也應該可以看到一個鎖頭的圖示出現在變數前面, 知道它代表什麼意思嗎? (如果沒有就錯了)
步驟五

因為類別外面的程式沒有辦法直接存取剛才製作的兩個資料成員, 為了要能夠有設定 Complex 物件內容(狀態)的界面函式, 我們替這個類別增加一個 setValue(double, double); 的 public 成員函式, 如此就可以初始化物件的內容了

增加一個成員函式也和增加資料成員一樣有兩種作法, 可以自己在 complex.h 以及 complex.cpp 檔案中加入成員函式的宣告和定義, 也可以在 類別檢視 中用右鍵選擇 加入/加入函式, 在 傳回型別 欄位填入函式的回傳值型態, 例如 void, 在 函式名稱 欄位中填入函式的名稱 (例如: setValue), 在 參數型別 欄位中填入參數的型態定義 (例如: double), 在 參數名稱 欄位中填入參數的名稱 (例如: real), 點選加入按鈕, 重複以上兩個步驟再增加第二個參數, 在 存取 欄位中選擇 public, 代表是公開的界面函式, Static, Virtual, Pure 和 Inline 目前都不要選, 最後按下 完成 按鈕。

函式的內容基本上就是

m_real = real;
m_imaginary = imaginary;
呼叫的方法類似於
Complex x;
x.setValue(13,25); // 13 + 25 i

步驟六 現在我們應該來定義 Complex 類別的 +, -, *, / 運算了:

首先應該先決定這些運算的名稱, 決定這些運算所需要傳入的參數, 所需要傳回的數值型態, 然後決定這些運算是 public 還是 private。

  • 我們用 add, subtract, multiply, divide 作為運算的名稱
  • 由於這四個運算都需要兩個運算元參與(例如: a + b), 每一個成員函式被呼叫時基本上是表示有第二個複數要和自己這個複數來運算, 例如:
    Complex x, y;
    ...
    x.add(y); // 代表希望把  y 加在 x 這個物件上
  • 因此每一個運算應該都需要傳入另外一個 Complex 類別的物件(或是物件指標或是參考)作為參數, 這樣才能計算自己這個複數和傳入的複數的和, 差, 乘積, 以及商, 並把自己這個物件的資料設定為運算的結果
  • 這四個運算中除了除法外只會更改自己這個物件的資料, 不需要傳回任何數值, 除法則可能因為除數是 0 而失敗, 所以我們選擇傳回一 bool 型態的結果來表示成功或是失敗
  • 這四個函式不完全是獨立的, 例如 subtract 應該可以運用 add 完成, divide 應該可以運用 multiply 以及額外的 double 除法完成 (如果你把它看成是獨立的功能, 其實你的程式裡面就有一些重複的片段)
  • 最後, 這幾個運算都是在 Complex 模組以外的程式裡用來操作 Complex 類別物件的方法, 所以應該是 public 的

做好上面的選擇後, 你還是有兩種方法來增加這些成員函式 (member function),

  1. 直接編輯 Complex.h 檔案在 public 區段加入這四個函式的宣告
  2. 類別檢視 中用右鍵選擇 加入/加入函式, 在 傳回型別 欄位填入函式的回傳值型態, 例如 void, 在 函式名稱 欄位中填入函式的名稱 (例如: setValue), 在 參數型別 欄位中填入參數的型態定義 (例如: double), 在 參數名稱 欄位中填入參數的名稱 (例如: real), 點選加入按鈕, 在 存取 欄位中選擇 public, 代表是公開的界面函式, Static, Virtual, Pure 和 Inline 目前都不要選, 最後按下 完成 按鈕 (將來會解釋它們的用途)。
有了 add, subtract, multiply, divide 這幾個函式的空殼後, 我們應該在 main() 函式裡運用 assert() 函式先增加 "單元測試" 的程式碼, 例如對於除法所要執行的測試如下:
    Complex x1, x2, x3;
    x1.setValue(7, 3);
    x2.setValue(1, 1);
    x3.setValue(5, -2); // (7+3i)/(1+i) = (5-2i)
    assert(x1.divide(x2));
    assert(x1.equal(x3, 1e-10)); // C/C++ 常數 1e-10 代表 1x10-10
    x2.setValue(0, 0);
    assert(!x1.divide(x2));
當然, 現在你應該在你的函式裡將前面 add, subtract, multiply, divide 的函式內容寫完, 並且對每一個函式所設計的功能都設計好單元測試的資料, 如此萬一以後修改程式時 (更換演算法或是更換資料表達的方法), 所有原先設計的功能 (很多也許你在修改時都已經忘掉了的功能) 都自動化地測試一遍 (所謂 TestDriven 的程式發展方法, 很多工程領域或是製造業裡都有這種品管的機制, 軟體製作時也有這種需求, 尤其是物件導向的程式常常需要進行 "新增功能" 或是 "架構調整 (refactoring)"。)

在撰寫上面的函式內容時請注意 C++ 的存取權限控制是針對類別來做的, 不是針對物件來做的, 也就是說別的類別的物件不能存取 Complex 類別物件的 private 資料成員, 但是一個 Complex 物件可以存取另外一個 Complex 物件內的 private 資料成員

學長姊們痛苦的經驗 1, 2

步驟七 現在你應該要自己做 equal (相等) 的成員函式了, 函式應該要傳回 bool 型態的結果, 你在這裡會遭遇問題, 主要是浮點數不適合用預設的運算子 == 精確地比對, 兩個浮點數之間比對每一個 bit 都相等是沒有太大意義的, 例如下面程式:
    double x = 3.13;
    ...
    if (x == 3.13)
        cout << "x == 3.13\n";
    ...
你有可能在某些機器上發現不會列印出 "x==3.13" 或是
    double x = 3.09;
    x = x/2.0 + 1.51;
    if (x == 3.055)
        cout << "x == 3.055\n";

你覺得螢幕上會列印任何東西嗎? 如果不會的話, 原因是什麼? 你可以列印一下 x 的內容, 又會發現看起來 x 好像是對的, 真的不是機器不好, 這和浮點數的表示方法有密切關係 :)

接下來寫一個簡單的測試程式讓你看一下兩個 double 變數 (各 8 個位元) 的實際內容: 範例程式專案, 你可以注意到 a 和 b 兩個變數的內容其實只有差最後一個位元 (70 vs. 71), 另外也請你注意一個位元組一個位元組比較的結果和直接 a == b 的結果是一樣的

在我們的程式中, 你應該用 subtract() 和 magnitude() 來檢查 "你的答案" 和 "你預期的答案" 的差異是不是小於一個很小的數字, 例如 0.0000001

所以你的 equal() 函式應該要接受兩個參數, 第一個參數是要比較的數字, 第二個參數則是精確度

步驟八

現在你應該要完成計算複數大小的成員函式了, 叫它 magnitude 吧, 函式應該要傳回一 double 的結果

在製作這個函式時你可能需要 C 標準函式庫中的 sqrt() 函式, 記得 #include <math.h>

步驟九

接下來我們為了能夠在螢幕上顯示一個 Complex 物件的內容, 需要替這個類別加上一個 print() 的 public 成員函式, 任何時候只要呼叫 print() 就可以在螢幕 (cout) 上印出 a + b i 格式的資料

請注意我們想要列印一個複數, 可是選擇不要替 Complex 類別加上 getReal() 和 getImaginary() 的 accessor, 而是加上 print() 介面, 理由是在設計物件化的程式時我們需要 "盡力去維護類別的封裝", 通常 accessor 是偷懶的設計, 給了accessor 之後, 顯然不只可以完成列印的功能, 還可以完成很多很多其他的功能, 雖然比起直接把資料成員設為 public 好, 但是這樣的封裝還是有名無實的, 比較起來算是失敗的

如果你考量到也許有些程式裡需要把 Complex 物件輸出到檔案串流裡, 你也可以把介面設計成 print(ostream& out), 測試時就把螢幕串流 cout 傳遞進去

步驟十

此時我們可以把前面放在 main() 中的所有的類別功能測試的程式碼, 集合起來到一個 unitTest() 的成員函式內, 這樣子的函式是物件導向程式作 "架構調整 (refactoring)" 時很重要的憑據, 有了 unitTest() 之後你才可以放心的修改你自己的程式, 不用擔心把原先已經測試過的程式改成錯的了, 這也是你寫的程式模組的品質管理的最基本方法。

然後你可以在 main() 中呼叫這個 unitTest() 的函式來執行單元測試

可是這個時後如果我們把 unitTest() 定義為一個成員函式, 你會發現在 main() 函式中你需要寫

Complex dummy;
dummy.unitTest();

才能夠呼叫 unitTest() 這個成員函式, 可是呼叫 unitTest 時並不是想要傳遞什麼訊息給 dummy 物件, 上面這樣子用起來不太合理; 如果你使用 static 保留字來宣告 unitTest(), 也就是用下列的語法來宣告:

class Complex 
{
...
public:
    static void unitTest();
...
}; 
如此宣告出來的成員函式稱為類別成員函式 class method, 不屬於任何一個物件, 所以在呼叫的時候不需要透過物件來呼叫它: 直接寫 Complex::unitTest(); 就可以呼叫它了, 這也是你剛才在運用加入/加入函式 功能時勾選那個 Static 的選項的意義

請注意一個 static 的成員函式裡面並沒有一般成員函式裡面那個隱藏的 this 指標, 也沒有辦法像一般的成員函式一樣, 寫 m_real/m_imaginary 就代表那個接受訊息的物件的兩個資料成員

步驟十一

現在我們應該要在 main() 函式裡寫一個比較實際的應用程式, 請由鍵盤輸入一個二元一次方程式 a x2 + b x + c = 0 的實數係數 a, b, c, 請利用公式:
x1=(-b+sqrt(b2-4ac))/(2a), x2=(-b-sqrt(b2-4ac))/(2a), 求出它的兩個根, 並且列印出來, 接著請計算

    ((x1)5 + (x2)5) / ((x2)10 - (x1)7)
並且將它的大小計算出來, 輸出在螢幕上。 請下載並執行範例測試程式

請注意: 對於我們今天寫的 Complex 類別來說, 你可以用下面兩種不同的敘述來拷貝一個 Complex 物件 x 到另外一個 Complex 物件 y 或是 z 去

        Complex x;
        x.setValue(10, 20);
        Complex y = x;   // copy constructor 拷貝建構元
        Complex z;
        z = x;            // assignment operator 設定運算子
我們在以後的課程裡會介紹這兩種寫法的不同, 也會解釋為什麼像 Complex 這樣子單純的類別可以使用 C++ compiler 預設的運算, 很多我們自己設計的類別是不能用compiler 提供的預設方法來拷貝資料的
步驟十二 請助教檢查後, 將所完成的 專案 (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .suo, .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\ 資料匣下的所有內容) 壓縮起來, 選擇 Lab3-1 上傳, 後面的實習課程需要使用這裡所完成的程式

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

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