运算符重载
钱用「分」存成整数,就不会有小数的误差。写一个 Money 类,两笔钱加起来,最自然的写法是 a + b。可编译器不知道两个 Money 怎么加:
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
| Moneyno match for 'operator+':找不到能用在两个 Money 上的 +。int、double、std::string 的 + 是写好的;自己的类型,要自己告诉编译器 + 是什么意思。这叫运算符重载。
重载 +
运算符其实就是一个名字特别的函数:+ 叫 operator+。把它写成成员函数:
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,按习惯返回自己的引用:
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 分子分母都不一样,可它们是同一个数。== 由你来定:交叉相乘相等,就是相等。!= 照着 == 写,不用再想一遍:
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<,就能直接排序,不用再写比较函数:
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 声明它是「朋友」,让它能用私有成员:
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}
a + b 返回一个新的 Point(11, 22),它再 + a 得到(12, 24)。+ 比 << 先算,所以最后显示的是加完的结果。
什么时候值得重载
重载运算符是为了让代码读起来自然。原则只有一条:让它做大家一看就猜得到的事。
| 值得 | 别这样 |
|---|---|
钱、分数、坐标的 + - | 用 + 往歌单里加歌(写成 add 更清楚) |
能比相等的东西写 ==(!= 一起写) | == 只比一部分成员,又不说明 |
有自然先后的东西写 <,好排序 | 先后不明显时硬写 <(换成第 14 课的比较函数) |
想显示的类型写 << | << 除了显示还偷偷改对象 |
拿不准的时候,就写一个名字清楚的成员函数。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
时间排排队
✓给 Time 写 operator<:小时早的在前,小时一样就比分钟。写好以后 std::sort 不用第三个参数就能排。
7:45 9:15 9:30
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}
显示一个分数
✓给 Fraction 写 <<,显示成 分子/分母。程序要显示 一份是 3/4。
一份是 3/4
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}
修好它:找零
✓程序编译不过,因为 Money 不会做减法。给它加上 operator-,让程序显示 找零:750 分。
找零:750 分
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}
这一课学会了
- 运算符是名字特别的函数:
a + b就是a.operator+(b)。自己的类型想用运算符,就自己写;没写时报no match for 'operator+'。 +不改两边:参数用const引用、函数加const、返回一个新对象。+=改自己:不加const,返回*this的引用。private按类算:成员函数能用同一个类的另一个对象的私有成员。==和!=要分别写,!=照着==写:!(*this == other)。- 写好
operator<,std::sort不用第三个参数,set和map也能用它当键。 <<写成类外面的函数,在类里用friend声明;它返回std::ostream&,才能连着<<。- 只在意思一看就明白时重载运算符,拿不准就写一个名字清楚的成员函数。