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
三個重點:
- 呼叫走「.」:
f.reduce()的意思是「f,約分你自己」。成員函式屬於每一個結構體變數——g.reduce()約的就是 g。 - 函式內直接寫成員名:
reduce裡的p、q不用寫成f.p——在成員函式裡,「自己的成員」直接叫名字就好;是誰呼叫的,動的就是誰的成員。 - 跟普通函式一樣能收參數、能回傳:
value()回傳double,要看小數值的時候再轉浮點——平常整數、需要才浮點,9.10 的精神。
reduce 裡有兩個小心機:gcd 吃絕對值(分子分母可能是負的);約完把負號統一搬到分子、分母保持為正。這讓同一個值只有一種長相——\frac{2}{-3} 和 \frac{-2}{3} 存起來一模一樣,之後想判斷兩個分數相等,比對成員就好。
什麼時候把函式收進結構體? 經驗法則:看這個操作是不是「這包資料自己的事」。讀入、約分、印出,主詞都是「這個分數」——收進去,主程式讀起來就像在講故事(f.read()、f.reduce()、f.print());反之,跟特定一包資料無關的通用函式(例如 7.4 的 isOdd 判斷函式),留在外面就好。
動手試試看:回頭幫 13.2 的通訊錄 student 加上 read() 與 print() 成員函式,主程式的讀入迴圈改成 a[i].read();——跟逐成員 cin 的版本比一比,哪個好讀?