Kodewick
第 17 课,共 30 课目录

运算符重载

第 17 课,共 30 课 · 第 4 部分 · 类 · 约 15 分钟

钱用「分」存成整数,就不会有小数的误差。写一个 Money 类,两笔钱加起来,最自然的写法是 a + b。可编译器不知道两个 Money 怎么加:

main.cpp
1#include <iostream>2 3class Money {4public:5    Money(int cents) : cents_(cents) {}6 7    int cents() const {8        return cents_;9    }10 11private:12    int cents_;13};14 15int main() {16    Money a{250};17    Money b{199};18    Money total = a + b;19    std::cout << total.cents() << '\n';20    return 0;21}
输出
main.cpp: In function 'int main()':
main.cpp:18:21: error: no match for 'operator+' (operand types are 'Money' and 'Money')
   18 |     Money total = a + b;
      |                   ~ ^ ~
      |                   |   |
      |                   |   Money
      |                   Money

no match for 'operator+':找不到能用在两个 Money 上的 +。int、double、std::string 的 + 是写好的;自己的类型,要自己告诉编译器 + 是什么意思。这叫运算符重载。

重载 +

运算符其实就是一个名字特别的函数:+ 叫 operator+。把它写成成员函数:

main.cpp
1#include <iostream>2 3class Money {4public:5    Money(int cents) : cents_(cents) {}6 7    Money operator+(const Money& other) const {8        return Money{cents_ + other.cents_};9    }10 11    int cents() const {12        return cents_;13    }14 15private:16    int cents_;17};18 19int main() {20    Money a{250};21    Money b{199};22    Money total = a + b;23    std::cout << total.cents() << '\n';24    return 0;25}
输出
449

a + b 就是 a.operator+(b):左边的 a 是调用它的对象,右边的 b 是参数 other。+ 不改 a 也不改 b,所以参数是 const 引用,函数本身也加 const,返回一个新的 Money。

other.cents_ 是 b 的私有成员,为什么能用?因为 private 是按类算的,不是按对象:Money 的成员函数能用任何一个 Money 的私有成员。点单步,看进入 operator+ 时 other 指向的是 b。

+= 改自己

+= 和 + 不一样:它改左边的对象。所以它不加 const,按习惯返回自己的引用:

main.cpp
1#include <iostream>2 3class Money {4public:5    Money(int cents) : cents_(cents) {}6 7    Money& operator+=(const Money& other) {8        cents_ += other.cents_;9        return *this;10    }11 12    int cents() const {13        return cents_;14    }15 16private:17    int cents_;18};19 20int main() {21    Money wallet{1000};22    wallet += Money{250};23    wallet += Money{75};24    std::cout << wallet.cents() << '\n';25    return 0;26}
输出
1325

*this 就是「这个对象自己」(this 是指向它的指针,第 19 课讲指针时再说)。Money{250} 在这里临时做出一笔钱,加完就没了。

== 和 !=

两个分数什么时候相等?1/2 和 2/4 分子分母都不一样,可它们是同一个数。== 由你来定:交叉相乘相等,就是相等。!= 照着 == 写,不用再想一遍:

main.cpp
1#include <iostream>2 3class Fraction {4public:5    Fraction(int num, int den) : num_(num), den_(den) {}6 7    bool operator==(const Fraction& other) const {8        return num_ * other.den_ == other.num_ * den_;9    }10 11    bool operator!=(const Fraction& other) const {12        return !(*this == other);13    }14 15private:16    int num_;17    int den_;18};19 20int main() {21    Fraction half{1, 2};22    Fraction two_quarters{2, 4};23    Fraction third{1, 3};24    std::cout << (half == two_quarters) << '\n';25    std::cout << (half != third) << '\n';26    return 0;27}
输出
1
1

注意 (half == two_quarters) 外面的括号:<< 比 == 先算,不加括号就成了 (std::cout << half) == ...,编译不过。

注意: 写了 ==,编译器不会顺手给你 !=(C++20 起才会);< 和 > 也要各写各的:写了 <,> 不会自己有。要哪个写哪个。

< 和排序

std::sort 不给第三个参数时,用的是 <。给自己的类型写好 operator<,就能直接排序,不用再写比较函数:

main.cpp
1#include <algorithm>2#include <iostream>3#include <string>4#include <vector>5 6class Song {7public:8    Song(const std::string& title, int seconds) : title_(title), seconds_(seconds) {}9 10    bool operator<(const Song& other) const {11        return seconds_ < other.seconds_;12    }13 14    void show() const {15        std::cout << title_ << " " << seconds_ << " 秒\n";16    }17 18private:19    std::string title_;20    int seconds_;21};22 23int main() {24    std::vector<Song> list{{"清晨", 269}, {"回家的路", 265}, {"夏夜", 226}};25    std::sort(list.begin(), list.end());26    for (const auto& s : list) {27        s.show();28    }29    return 0;30}
输出
夏夜 226 秒
回家的路 265 秒
清晨 269 秒

按时长从短到长排好了。std::set<Song> 和以 Song 为键的 std::map 也靠这个 < 排顺序,没有它就放不进去。

<< 让 cout 显示对象

一直写 cents() 再自己拼小数点太麻烦,最好能直接 std::cout << price。可 << 的左边是 std::cout,不是 Money,没法写成 Money 的成员函数。办法是写成普通函数,两个参数是左边和右边;再在类里用 friend 声明它是「朋友」,让它能用私有成员:

main.cpp
1#include <iostream>2 3class Money {4public:5    Money(int cents) : cents_(cents) {}6 7    friend std::ostream& operator<<(std::ostream& out, const Money& m);8 9private:10    int cents_;11};12 13std::ostream& operator<<(std::ostream& out, const Money& m) {14    out << m.cents_ / 100 << ".";15    if (m.cents_ % 100 < 10) {16        out << "0";17    }18    out << m.cents_ % 100 << " 元";19    return out;20}21 22int main() {23    Money price{1205};24    Money fee{90};25    std::cout << "价格:" << price << '\n';26    std::cout << "运费:" << fee << '\n';27    return 0;28}
输出
价格:12.05 元
运费:0.90 元

std::ostream 是 std::cout 这类输出流的类型。函数最后返回 out,std::cout << "价格:" << price << '\n' 才能一个接一个地连下去:std::cout << price 的结果还是 std::cout。

先猜一猜

这段程序显示什么?

1#include <iostream>2 3class Point {4public:5    Point(int x, int y) : x_(x), y_(y) {}6 7    Point operator+(const Point& other) const {8        return Point{x_ + other.x_, y_ + other.y_};9    }10 11    friend std::ostream& operator<<(std::ostream& out, const Point& p);12 13private:14    int x_;15    int y_;16};17 18std::ostream& operator<<(std::ostream& out, const Point& p) {19    out << "(" << p.x_ << ", " << p.y_ << ")";20    return out;21}22 23int main() {24    Point a{1, 2};25    Point b{10, 20};26    std::cout << a + b + a << '\n';27    return 0;28}

什么时候值得重载

重载运算符是为了让代码读起来自然。原则只有一条:让它做大家一看就猜得到的事。

值得别这样
钱、分数、坐标的 + -用 + 往歌单里加歌(写成 add 更清楚)
能比相等的东西写 ==(!= 一起写)== 只比一部分成员,又不说明
有自然先后的东西写 <,好排序先后不明显时硬写 <(换成第 14 课的比较函数)
想显示的类型写 <<<< 除了显示还偷偷改对象

拿不准的时候,就写一个名字清楚的成员函数。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

时间排排队

给 Time 写 operator<:小时早的在前,小时一样就比分钟。写好以后 std::sort 不用第三个参数就能排。

要显示
7:45
9:15
9:30
exercise1.cpp
1#include <algorithm>2#include <iostream>3#include <vector>4 5class Time {6public:7    Time(int hours, int minutes) : hours_(hours), minutes_(minutes) {}8 9    void show() const {10        std::cout << hours_ << ":" << minutes_ << '\n';11    }12 13private:14    int hours_;15    int minutes_;16};17 18int main() {19    std::vector<Time> alarms{{9, 30}, {7, 45}, {9, 15}};20    std::sort(alarms.begin(), alarms.end());21    for (const auto& t : alarms) {22        t.show();23    }24    return 0;25}
输出
点「运行」或按 Ctrl+Enter
练习 2

显示一个分数

给 Fraction 写 <<,显示成 分子/分母。程序要显示 一份是 3/4。

要显示
一份是 3/4
exercise2.cpp
1#include <iostream>2 3class Fraction {4public:5    Fraction(int num, int den) : num_(num), den_(den) {}6 7private:8    int num_;9    int den_;10};11 12int main() {13    Fraction share{3, 4};14    std::cout << "一份是 " << share << '\n';15    return 0;16}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:找零

程序编译不过,因为 Money 不会做减法。给它加上 operator-,让程序显示 找零:750 分。

要显示
找零:750 分
exercise3.cpp
1#include <iostream>2 3class Money {4public:5    Money(int cents) : cents_(cents) {}6 7    int cents() const {8        return cents_;9    }10 11private:12    int cents_;13};14 15int main() {16    Money paid{2000};17    Money price{1250};18    Money change = paid - price;19    std::cout << "找零:" << change.cents() << " 分\n";20    return 0;21}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 运算符是名字特别的函数:a + b 就是 a.operator+(b)。自己的类型想用运算符,就自己写;没写时报 no match for 'operator+'。
  • + 不改两边:参数用 const 引用、函数加 const、返回一个新对象。+= 改自己:不加 const,返回 *this 的引用。
  • private 按类算:成员函数能用同一个类的另一个对象的私有成员。
  • == 和 != 要分别写,!= 照着 == 写:!(*this == other)。
  • 写好 operator<,std::sort 不用第三个参数,set 和 map 也能用它当键。
  • << 写成类外面的函数,在类里用 friend 声明;它返回 std::ostream&,才能连着 <<。
  • 只在意思一看就明白时重载运算符,拿不准就写一个名字清楚的成员函数。