語法書 / AA 競程語法書 下冊 / 第十三單元 / 運算子多載:讓結構體學會比大小

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 沒拿到第三個參數時,靠元素型態自己的「小於」排序——intstring、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 &b7.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 剛好裝得下。