13.6 運算子多載:讓結構體學會比大小
第十一單元結尾的預告,現在兌現:結構體的資料怎麼排序?
先看直接開排會發生什麼事。把通訊錄換成成績單——每位選手有名字和分數兩個成員,定義一個新的 student:
struct student {
string name; // 名字
int score; // 分數
};
student a[4] = {{"alice", 90}, {"bob", 95}, {"cathy", 90}, {"dora", 88}};
sort(a, a + 4); // 編譯錯誤!
這行會噴出上百行錯誤訊息——別慌,滿屏紅字裡只有一句是重點:
error: no match for 'operator<' (operand types are 'student' and 'student')
翻譯:「我找不到兩個 student 之間的小於」。11.4 說過,sort 沒拿到第三個參數時,靠元素型態自己的「小於」排序——int、string、pair 都內建小於,但 student 是你昨天才發明的型態,C++ 不可能知道兩位選手誰該排前面。
補救的路線有兩條。路線一你已經會了——11.6 的比較函式,參數換成結構體照樣能用:
bool cmp(const student &x, const student &y) { // 11.6 的比較函式,參數換成 student
return x.score > y.score; // 分數高的排前面
}
然後 sort(a, a + n, cmp);,完全可行。(參數寫 const student &——上冊 7.5 與上一單元 12.3 的傳參考加 const:結構體一包可能很大,複製划不來,順便立下唯讀保證。)
路線二是本節的主角:與其外聘裁判,不如教會型態自己比大小。C++ 允許你定義「兩個 student 之間的 < 是什麼意思」,這個機制叫運算子多載(operator overloading)——寫一個名字固定叫 operator < 的成員函式:
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
struct student {
string name;
int score;
bool operator < (const student &b) const {
if (score != b.score) {
return score > b.score; // 分數高的排前面
}
return name < b.name; // 平手:名字字典序小的排前面
}
};
student a[4] = {{"alice", 90}, {"bob", 95}, {"cathy", 90}, {"dora", 88}};
int main() {
sort(a, a + 4); // 不用第三個參數了!
for (int i = 0; i < 4; i++) {
cout << a[i].name << ' ' << a[i].score << '\n';
}
return 0;
}
執行結果:
bob 95
alice 90
cathy 90
dora 88
拆開來看這個函式:
- 名稱固定叫
operator <:多載「小於」。從此x < y這個運算式合法,效果是呼叫 x 的這個函式、把 y 當參數 b 傳進去——左邊的當事人是「自己」,右邊的變成參數。 - 語意跟 11.6 的比較函式一字不差:回答「自己是否必須排在 b 前面」。11.5 的
cmp(x, y)有兩個參數,這裡只剩一個,因為左邊那位就是自己——同一件事,換了個住處。 sort(a, a + n)不用第三個參數了:sort要找「小於」,找到的就是你多載的這個。- 附贈:
max(x, y)、min(x, y)這些靠「小於」吃飯的內建函式,也跟著能用了。
¶兩個 const,各管一件事
範例裡出現了兩個 const。參數的 const student &b 是 7.5 的老朋友:傳參考免複製、承諾不改動對方。參數列後面那個 const 是新面孔:寫在成員函式小括號之後,承諾「這個函式也不會改動自己的成員」。比大小本來就不該動到任何一邊,兩個承諾都立下是慣例——而且有些用法會強制檢查(例如剛提到的 max,少了結尾的 const 就編譯失敗)。先照抄:寫比較用的運算子,兩個 const 都帶上。
¶貫穿範例:分數也學會比大小
9.10 的補充知識框教過一招純整數比較:分母都為正時,\frac{a}{b} < \frac{c}{d} 等價於 a \times d < c \times b——交叉相乘,不用除法、零誤差。我們的 Fraction 約分後分母保持為正,條件剛好成立:
#include <iostream>
#include <algorithm>
using namespace std;
struct Fraction {
long long p; // 分子
long long q; // 分母(保持為正)
bool operator < (const Fraction &b) const {
return p * b.q < b.p * q; // 分母都是正的:交叉相乘,比大小不用除法
}
void print() {
cout << p << '/' << q << '\n';
}
};
Fraction a[3] = {{1, 2}, {-2, 3}, {5, 4}}; // 1/2、-2/3、5/4(都已是最簡、分母為正)
int main() {
sort(a, a + 3); // 由小到大
for (int i = 0; i < 3; i++) {
a[i].print();
}
return 0;
}
執行結果:
-2/3
1/2
5/4
交叉相乘的乘積可能到 10^{18} 等級——成員用 long long 的又一個理由(9.10 補充知識框的老提醒:乘之前先確認乘積不會爆範圍)。
動手試試看:解掉分數運算——用本單元的 Fraction 把三種操作一網打盡:指派交給建構子;四則運算自己寫成函式(加法要通分:\frac{a}{b} + \frac{c}{d} = \frac{a \times d + c \times b}{b \times d},減法同理,乘除更簡單;每步算完記得 reduce());比大小用剛多載的 <(「相等」呢?都約成最簡分數了,比對分子分母即可)。題目保證約分後分子分母不超過 10^9,交叉相乘和通分的乘積用 long long 剛好裝得下。