13.4 成員函式

通訊錄把語法基本盤練完了。接下來換一位會陪我們走完整個單元的主角——分數

為什麼是分數?第九單元的教訓還熱著:浮點數存不準也比不準(9.6),能用整數就用整數(9.10)。但 \frac{1}{3} 這種數,單一個整數存不下——兩個整數卻剛剛好:分子、分母各一個,加減乘除全程零誤差。兩個強相關的值要綁成一包,正是結構體的工作:

struct Fraction {
    long long p;    // 分子
    long long q;    // 分母
};

(分子分母一相乘,數字動不動就很大——直接用 long long 開好開滿。)

有些操作永遠跟著分數走:讀入、約分、印出。以約分為例,你可以照 7.5 寫一個普通函式 void reduce(Fraction &f)——完全可行;但 C++ 提供了更貼身的選擇:把函式直接寫進結構體裡,成為它的成員函式(member functions)

#include <iostream>
#include <numeric>    // gcd
#include <cstdlib>    // abs
using namespace std;

struct Fraction {
    long long p;    // 分子
    long long q;    // 分母

    void read() {                        // 讀入自己:兩個整數代表分子、分母
        cin >> p >> q;
    }
    void reduce() {                      // 約分:把自己化成最簡分數
        long long g = gcd(abs(p), abs(q));
        p /= g;
        q /= g;
        if (q < 0) {                     // 負號統一放在分子、分母保持為正
            p = -p;
            q = -q;
        }
    }
    void print() {                       // 用「分子/分母」的樣子印出自己
        cout << p << '/' << q << '\n';
    }
    double value() {                     // 成員函式也能有回傳值:這個分數的小數值
        return (double)p / q;            // 9.4 的老規矩:先轉浮點數再除
    }
};

int main() {
    Fraction f;
    f.read();       // 成員函式一樣用「.」呼叫:讀入 f 的分子分母
    f.reduce();     // reduce 動的是 f 自己的 p、q
    f.print();
    cout << f.value() << '\n';
    return 0;
}

執行結果(輸入 14 -21):

-2/3
-0.666667

三個重點:

  1. 呼叫走「.」f.reduce() 的意思是「f,約分你自己」。成員函式屬於每一個結構體變數——g.reduce() 約的就是 g。
  2. 函式內直接寫成員名reduce 裡的 pq 不用寫成 f.p——在成員函式裡,「自己的成員」直接叫名字就好;是誰呼叫的,動的就是誰的成員。
  3. 跟普通函式一樣能收參數、能回傳value() 回傳 double,要看小數值的時候再轉浮點——平常整數、需要才浮點,9.10 的精神。

reduce 裡有兩個小心機:gcd 吃絕對值(分子分母可能是負的);約完把負號統一搬到分子、分母保持為正。這讓同一個值只有一種長相——\frac{2}{-3}\frac{-2}{3} 存起來一模一樣,之後想判斷兩個分數相等,比對成員就好。

什麼時候把函式收進結構體? 經驗法則:看這個操作是不是「這包資料自己的事」。讀入、約分、印出,主詞都是「這個分數」——收進去,主程式讀起來就像在講故事(f.read()f.reduce()f.print());反之,跟特定一包資料無關的通用函式(例如 7.4isOdd 判斷函式),留在外面就好。

動手試試看:回頭幫 13.2 的通訊錄 student 加上 read()print() 成員函式,主程式的讀入迴圈改成 a[i].read();——跟逐成員 cin 的版本比一比,哪個好讀?