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

 
實習目標 1. 練習如何宣告類別 (如何運用 Visual Studio 界面)
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 界面新增一個 C++ 檔案 請在檔案內寫一個空的 main() 函式, Build, 新增一個資料匣為 testComplex, 存檔為 testComplex.cpp 此時 project 將會自動命名為 testComplex
步驟三 請使用右鍵在 Workspace/ClassView 的名稱 testComplex classes 上點一下, 選擇 New Class..., 在 Class information 的 Name 欄位填入 CComplex 作為新的類別名稱, 此時你可以檢查 ClassView 中看到新增了一個 CComplex 類別, 在 FileView 中你會看到 Complex.cpp 和 Complex.h, 請在 ClassView 中雙擊 CComplex 來查看 Complex.h 檔案內類別的宣告 (declaration),
    class CComplex  
    {
    public:
        CComplex();
        virtual ~CComplex();
    };
    
這是 Visual Studio 界面自動幫你作出來的類別定義, 這兩個函式有特殊的用途, 將來我們會仔細談它們的用法, 目前對你沒有影響, 你也不需要去修改它們。 至於檔案中其它的 #if !defined(...), #define, #endif 是為了避免重複引入 .h 定義檔 而設計的, 這是所有多檔案 C/C++ 程式都需要有的定義, 如果 Visual Studio 界面不幫你做好, 你也一樣要自己做。

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

步驟四 現在我們應該來定義 CComplex 類別內儲存資料的欄位了, 我們知道複數平面上需要有兩個實數座標, 請務必替它們取適當的名字, 我們把它們取為 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 的 ClassView 界面來做: 在 ClassView 裡找到 CComplex 類別, 以右鍵選擇 Add Member Variable, 出現一個對話視窗, 在 Variable Type 中填入可以存放實數座標的型態, 在 Variable Name 欄位中填入剛才取好的變數名稱, 在 Access 中選擇 Private。 注意在 ClassView 中你應該立刻可以看到這個變數, 你也應該可以看到一個鎖的圖示出現在變數前面, 知道它代表什麼意思嗎?
步驟五

為了要能夠有設定 CComplex 物件內容的界面函式, 我們替這個類別增加一個 setValue(double, double); 的 public 成員函式, 如此就可以初始化物件的內容了

增加一個成員函式也和增加資料成員一樣有兩種作法, 可以自己在 complex.h 以及 complex.cpp 檔案中加入成員函式的宣告和定義, 也可以在 ClassView 中用右鍵選擇 Add Memeber Function, 在 Function Type欄位填入函式的回傳值型態, 例如 void, 在 Function Declaration 欄位中填入函式的名稱和參數的型態定義, 例如 setValue(double real, double imaginary), Access 選擇 Public, 代表是公開的界面函式, Static 和 Virtual 目前都不要選。

函式的內容基本上就是

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

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

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

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

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

  1. 直接編輯 Complex.h 檔案在 public 區段加入這四個函式的宣告
  2. 透過 Visual Studio 的 ClassView 界面來做: 在 ClassView 裡找到 CComplex 類別, 以右鍵選擇 Add Member Function, 出現一個對話視窗, 在 Function Type: 欄位中填入函式傳回值的型態, 在 Function Declaration: 欄位中填入函式的名稱, 選擇 Public; Static 和 Virtual 都不要勾選 (將來會解釋它們的用途)。
有了 +, -, *, / 這幾個函式的空殼後, 我們應該在 main() 函式裡運用 assert() 函式先增加 "單元測試" 的程式碼, 例如對於除法所要執行的測試如下:
    CComplex x1, x2, x3;
    x1.setValue(7, 3);
    x2.setValue(1, 1);
    x3.setValue(5, -2);
    assert(x1.divide(x2));
    assert(x1.equal(x3, 1e-10)); // 1e-10 代表 1x1010
    x2.setValue(0, 0);
    assert(!x1.divide(x2));
    
當然, 現在你應該在你的函式裡將前面 +, -, *, / 的函式內容寫完, 並且對所寫的運算函式都設計好單元測試的資料, 如此萬一以後修改程式時所有的測試都可以自動化進行

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

步驟七 現在你應該要自己做 equal (相等) 的成員函式了, 函式應該要傳回 bool 型態的結果, 你在這裡會遭遇問題, 主要是浮點數並沒有辦法精確地比對, 兩個浮點數之間比對每一個 bit 都相等是沒有太大意義的, 例如下面程式:
    double x = 3.13;
    ...
    if (x == 3.13)
        cout << "x == 3.13\n";
    ...
或是
    double x = 3.09;
    x = x/2.0 + 1.51;
    if (x == 3.055)
        cout << "x == 3.055\n";

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

所以在我們的程式中, 你應該用 subtract() 和 magnitude() 來測試看看你的答案是不是 和我列在上面的答案小於一個很小的數字, 例如 0.0000001

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

步驟八

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

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

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

此時我們可以把前面放在 main() 中的所有的類別功能測試的程式碼, 集合起來到一個 unitTest() 的成員函式內, 這樣子的函式是物件導向程式作架構調整 (refactoring) 時很重要的憑據, 也是你寫的程式模組的品質管理的基本方法。

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

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

CComplex dummy;
dummy.unitTest();

才能夠呼叫 unitTest() 這個成員函式, 如果你使用 static 保留字來宣告 unitTest(), 也就是用下列的語法來定義:

class CComplex 
{
...
public:
    static void unitTest();
...
}; 
如此定義出來的成員函式稱為 class method,不屬於任何一個物件, 所以在呼叫的時候不需要透過物件來呼叫它: 直接寫 CComplex::unitTest(); 就可以了。
步驟十一

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

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

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

        CComplex x;
        x.setValue(10, 20);
        CComplex y = x;   // copy constructor 拷貝建構元
        CComplex z;
        z = x;            // assignment operator 設定運算子
    
我們在以後的課程裡會介紹這兩種寫法的不同, 也會解釋為什麼像 CComplex 這樣子單純的類別可以使用 C++ compiler 預設的運算, 什麼樣的類別不能用上面的兩種方法來拷貝資料
步驟十二 請將所完成的 project (去掉 debug/ 資料匣下的所有內容) 壓縮起來儲存在 cyber 上你的帳號內, 後面的實習課程可能需要使用這裡所完成的程式

做完這個實習以後, 你應該會發現 Visual Studio 左方那個 ClassView 的視窗真的很有用, 你可以隨時修改某一個類別, 隨時找到類別裡的資料或是成員函式; 但是有時你用 Visual Studio 介面會發現某一個類別的定義突然就不見了,這應該是 Visual Studio 介面的一個小問題吧, 怎麼解決呢? 請到 FileView 視窗中找到那個突然不見了的類別的 .h 檔案, 點兩下打開, 存檔一次, 這樣子應該就可以在 ClassView 視窗中再看到這個類別的定義了。

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

製作日期: 02/24/2004 by 丁培毅 (Pei-yih Ting)
E-mail: [email protected] TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程系 Lagoon