第13章 运算符重载
第13章 运算符重载
想象一下,如果+只能做数字加法,生活该多无聊啊!"Hello" + "World"?不行不行,得调函数!vec1 + vec2?做梦吧您!幸好C++给了我们运算符重载这把魔法棒,让我们可以重新定义运算符的行为。今天就让我们一起来玩转这把魔法棒,看看如何让+做加法以外的事情——当然,是做有意义的事情!
13.1 运算符重载基础
运算符重载(Operator Overloading)是C++的一项超级power功能,它允许我们为自定义类型重新定义运算符的含义。简单来说,就是让+、-、<<这些运算符不仅能处理内置类型(int、double等),还能处理我们自己的类。
把运算符重载理解成给运算符发一张"新工作证"——它本来只会做一件事(比如int相加),但现在我们可以训练它做新事情(比如复数相加、向量相加)。但别忘了,重载不等于乱来,保持语义一致是关键!
可重载运算符列表
C++允许重载的运算符基本上涵盖了所有常用的二元和一元运算符,但有一些"铁饭碗"运算符是坚决不让重载的——它们的重要性太高了,不能随便改写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| #include <iostream>
// C++允许重载的运算符:
// 算术:+ - * / % ++ --
// 位运算:& | ^ ~ << >>
// 赋值:= += -= *= /= %= &= |= ^= <<= >>=
// 比较:== != < > <= >=
// 逻辑:&& || !
// 其他:() [] -> ->* new delete new[] delete[]
// 不能重载的::: . .* ?: sizeof typeid
class Complex {
private:
double real_, imag_; // 实部和虚部,这才是复数的灵魂!
public:
// 构造函数,用初始化列表更高效哦
Complex(double r = 0, double i = 0) : real_(r), imag_(i) {}
// 重载+运算符,让复数可以相加
// 规则:(a+bi) + (c+di) = (a+c) + (b+d)i
Complex operator+(const Complex& other) const {
return Complex(real_ + other.real_, imag_ + other.imag_);
}
// 打印复数,带点格式化更美观
void print() const {
std::cout << real_ << (imag_ >= 0 ? "+" : "") << imag_ << "i" << std::endl;
}
};
int main() {
Complex a(3.0, 4.0); // 3 + 4i,复数界的经典款
Complex b(1.0, 2.0); // 1 + 2i
Complex c = a + b; // 使用重载的+运算符,就像魔法一样!
c.print(); // 输出: 4+6i
return 0;
}
|
这个例子展示了最基本的运算符重载:我们定义了一个Complex类(复数类),然后重载了+运算符,使得两个复数可以直接用+相加,而不用调用一个叫add()的函数。优雅,太优雅了!
运算符重载原则
重载运算符看起来很酷,但这里有两个黄金法则要牢记:
- 保持原有语义:如果
+表示加法,重载后也要做加法,别搞成减法! - 不要创造性地"发明":运算符重载是为了让代码更自然,不是为了炫技
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| #include <iostream>
#include <string>
class Number {
private:
int value_; // 存一个整数,很简单对吧?
public:
Number(int v = 0) : value_(v) {}
// 重载原则1:保持原有语义
// + 就应该是加法,不是减法!不是乘法!
Number operator+(const Number& other) const {
return Number(value_ + other.value_); // 正确示范!
}
// 重载原则2:不要太创造性的"发明"
// 如果你把operator+实现成减法,你的同事会拿着40米大刀来找你
// operator+应该做加法,不是减法!
int getValue() const { return value_; }
};
int main() {
Number a(10), b(20);
Number c = a + b; // 自然语义:10 + 20 = 30
std::cout << "10 + 20 = " << c.getValue() << std::endl; // 输出: 10 + 20 = 30
return 0;
}
|
什么时候运算符重载是合理的?当你想要类型的行为"自然而然"的时候。比如,复数应该能相加,日期应该能相减(算天数),矩阵应该能相乘。但如果你试图让+表示字符串拼接,虽然技术上可行,但很可能会让未来的维护者一脸问号。
13.2 算术运算符重载
算术运算符是最常被重载的一类运算符。想象一下,如果你定义了一个二维向量类Vec2,你肯定希望可以直接用v1 + v2来计算向量加法,而不是调用v1.add(v2)。这不仅仅是懒,这是追求优雅!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| #include <iostream>
class Vec2 {
private:
double x_, y_; // 二维向量的x和y坐标
public:
Vec2(double x = 0, double y = 0) : x_(x), y_(y) {}
// +运算符:向量加法
// (x1, y1) + (x2, y2) = (x1+x2, y1+y2)
Vec2 operator+(const Vec2& other) const {
return Vec2(x_ + other.x_, y_ + other.y_);
}
// -运算符:向量减法
Vec2 operator-(const Vec2& other) const {
return Vec2(x_ - other.x_, y_ - other.y_);
}
// 一元负运算符:取反
// (x, y) -> (-x, -y)
Vec2 operator-() const {
return Vec2(-x_, -y_);
}
// +=运算符(成员):修改自身并返回引用
Vec2& operator+=(const Vec2& other) {
x_ += other.x_;
y_ += other.y_;
return *this; // 返回引用支持链式调用,如 a += b += c
}
// *=运算符:标量乘法
Vec2& operator*=(double scalar) {
x_ *= scalar;
y_ *= scalar;
return *this;
}
// 友元声明:让非成员运算符也能访问private成员
friend Vec2 operator*(const Vec2& v, double scalar);
friend Vec2 operator*(double scalar, const Vec2& v);
void print() const {
std::cout << "(" << x_ << ", " << y_ << ")" << std::endl;
}
};
// 非成员运算符实现交换律:v * 2 = 2 * v
// 如果只定义成员形式的 v * 2(成员 operator*),那 2 * v 就无法编译了!
Vec2 operator*(const Vec2& v, double scalar) {
return Vec2(v.x_ * scalar, v.y_ * scalar);
}
// 复用上面的实现,保持DRY原则(Don't Repeat Yourself)
// 注意:这里 v * scalar 调用的是非成员 operator*(const Vec2&, double)(由友元声明),而非自身,避免无限递归
Vec2 operator*(double scalar, const Vec2& v) {
return v * scalar; // 调用非成员 operator*(const Vec2&, double)
}
int main() {
Vec2 v1(1.0, 2.0);
Vec2 v2(3.0, 4.0);
Vec2 v3 = v1 + v2;
std::cout << "v1 + v2 = "; v3.print(); // 输出: (4, 6)
Vec2 v4 = -v1; // 一元负运算符
std::cout << "-v1 = "; v4.print(); // 输出: (-1, -2)
v1 += v2; // 记住,+=修改了v1自身!
std::cout << "v1 += v2: "; v1.print(); // 输出: (4, 6)
Vec2 v5 = v1 * 2.0; // Vec2 * double
Vec2 v6 = 3.0 * v2; // double * Vec2,都能编译!
std::cout << "v1 * 2 = "; v5.print(); // 输出: (8, 12)
std::cout << "3 * v2 = "; v6.print(); // 输出: (9, 12)
return 0;
}
|
小贴士:为什么operator*要用非成员函数?因为数学上乘法是交换的——v * 2应该等于2 * v。如果只有成员函数Vec2::operator*(double)(即只支持v * 2),那2 * v编译器就会报错:“没有匹配的操作符”。必须同时提供非成员版本才能让两种写法都成立!
13.3 关系运算符重载
关系运算符(==、!=、<、<=、>、>=)的重载在C++中超级重要,因为它们是STL容器(如std::set、std::map)和算法(如std::sort)的基石。没有比较运算符,你的自定义类型就别想放进std::set里了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| #include <iostream>
#include <string>
class Person {
private:
std::string name_; // 姓名
int age_; // 年龄
public:
Person(const std::string& name, int age) : name_(name), age_(age) {}
// ==运算符:判断两个人是否"完全相同"
bool operator==(const Person& other) const {
return name_ == other.name_ && age_ == other.age_;
}
// !=运算符:直接取反就好了,偷懒是美德
bool operator!=(const Person& other) const {
return !(*this == other);
}
// <运算符:用于std::set、std::map等有序容器
// 字典序比较:先比姓名,姓名相同再比年龄
bool operator<(const Person& other) const {
if (name_ != other.name_) return name_ < other.name_;
return age_ < other.age_;
}
// <= > >= 通常基于<和==
bool operator<=(const Person& other) const {
return !(other < *this); // a <= b 等价于 !(b < a)
}
bool operator>(const Person& other) const {
return other < *this; // a > b 等价于 b < a
}
bool operator>=(const Person& other) const {
return !(*this < other); // a >= b 等价于 !(a < b)
}
void print() const {
std::cout << name_ << " (" << age_ << ")" << std::endl;
}
};
int main() {
Person alice1("Alice", 25);
Person alice2("Alice", 25);
Person bob("Bob", 30);
std::cout << "alice1 == alice2: " << (alice1 == alice2) << std::endl; // 输出: 1 (true)
std::cout << "alice1 != bob: " << (alice1 != bob) << std::endl; // 输出: 1 (true)
std::cout << "alice1 < bob: " << (alice1 < bob) << std::endl; // 输出: 1 (Alice < Bob字典序)
return 0;
}
|
重要规则:如果重载了==,通常也应该重载!=,因为它们逻辑上是配对的。同样,如果你的类型要放入std::set或std::map,就必须重载operator<。C++20引入了operator<=>(宇宙飞船运算符),可以一键生成所有比较运算符,后面的章节会详细介绍!
13.4 输入输出运算符重载
<<和>>是我们老朋友std::ostream和std::istream的成员函数(或者说,是它们的插入/提取运算符)。重载这两个运算符可以让你直接用cout << obj来打印对象,用cin >> obj来读取输入,简直是调试神器!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| #include <iostream>
#include <string>
class Point {
private:
double x_, y_; // 平面上的一个点
public:
Point(double x = 0, double y = 0) : x_(x), y_(y) {}
// 友元声明:允许operator<<访问private成员
// 必须是非成员函数,因为左操作数是ostream而不是Point
friend std::ostream& operator<<(std::ostream& os, const Point& p);
friend std::istream& operator>>(std::istream& is, Point& p);
};
// operator<<必须是友元或非成员函数
// 返回ostream引用是为了支持链式调用:cout << p1 << p2 << endl;
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x_ << ", " << p.y_ << ")";
return os; // 返回ostream引用以支持链式调用
}
// operator>>:从输入流读取点,格式是 (x, y)
std::istream& operator>>(std::istream& is, Point& p) {
char ch; // 用于读取括号和逗号
is >> ch >> p.x_ >> ch >> p.y_ >> ch; // 读取格式 (x, y)
return is; // 返回istream引用以支持链式调用
}
int main() {
Point p1(3.14, 2.71);
Point p2;
// 使用重载的<<输出,再也不用写print()了!
std::cout << "p1 = " << p1 << std::endl; // 输出: p1 = (3.14, 2.71)
// 使用重载的>>输入
std::cout << "Enter point (x, y): ";
std::cin >> p2;
std::cout << "You entered: " << p2 << std::endl;
return 0;
}
|
为什么<<和>>必须是友元或非成员函数?因为它们左边的操作数是std::ostream或std::istream,不是我们的Point类型。如果写成成员函数,那调用方式就变成了p1 << cout,语义完全反了!
13.5 下标运算符重载
operator[]是数组的"灵魂伴侣",重载它可以让你用arr[i]的方式访问自定义容器。而且,它可以返回引用,这意味着既可以读也可以写!太棒了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| #include <iostream>
class SafeArray {
private:
static const int MAX_SIZE = 100; // 数组最大容量
int data_[MAX_SIZE]; // 存储数据的数组
int size_; // 实际大小
public:
SafeArray(int size) : size_(size) {
// 初始化,每个元素的值是索引*10
for (int i = 0; i < size_; ++i) {
data_[i] = i * 10;
}
}
// 下标运算符(返回引用,可读可写)
// 关键点:返回int&而不是int,这样 arr[2] = 999 才能工作!
int& operator[](int index) {
if (index < 0 || index >= size_) {
throw std::out_of_range("Index out of bounds! 索引越界啦!");
}
return data_[index];
}
// const版本:只读访问,用于const对象或const引用
// 如果不提供const版本,const对象就只能读取普通数组了
const int& operator[](int index) const {
if (index < 0 || index >= size_) {
throw std::out_of_range("Index out of bounds! 索引越界啦!");
}
return data_[index];
}
int size() const { return size_; }
};
int main() {
SafeArray arr(5);
// 读取元素
std::cout << "arr[2] = " << arr[2] << std::endl; // 输出: arr[2] = 20
// 修改元素
arr[2] = 999; // 返回的是引用,所以可以赋值!
std::cout << "After modify, arr[2] = " << arr[2] << std::endl;
// 输出: After modify, arr[2] = 999
// const对象只能调用const版本
const SafeArray& constArr = arr;
std::cout << "constArr[0] = " << constArr[0] << std::endl; // 输出: constArr[0] = 0
// arr[10] = 0; // 注释掉这句!运行时会抛出异常:Index out of bounds!
return 0;
}
|
划重点:如果你希望对象既可读又可写,一定要同时提供两个版本的下标运算符:普通版本返回int&,const版本返回const int&。否则const对象就只能读不能写了,或者反过来。
多维下标运算符
虽然operator[]只能接受一个参数,但我们可以使用operator()(函数调用运算符)来实现多维下标访问。这种写法比m[row * 3 + col]直观多了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| #include <iostream>
class Matrix2D {
private:
int data_[9]; // 3x3 matrix,用一维数组存储
public:
Matrix2D() {
// 初始化 0 1 2
// 3 4 5
// 6 7 8
for (int i = 0; i < 9; ++i) data_[i] = i;
}
// 使用 operator() 实现多维下标访问
// 注意是圆括号而不是方括号,这样可以用 m(1, 1) 来访问二维矩阵
int& operator()(int row, int col) {
if (row < 0 || row >= 3 || col < 0 || col >= 3) {
throw std::out_of_range("Matrix index out of range 矩阵索引越界!");
}
return data_[row * 3 + col]; // 将二维索引转为一维存储
}
const int& operator()(int row, int col) const {
return data_[row * 3 + col];
}
// 打印整个矩阵
void print() const {
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << (*this)(i, j) << " ";
}
std::cout << std::endl;
}
}
};
int main() {
Matrix2D m;
std::cout << "m(1, 1) = " << m(1, 1) << std::endl; // 输出: m(1, 1) = 4
m(1, 1) = 100; // 修改元素
std::cout << "After modify, m(1, 1) = " << m(1, 1) << std::endl;
// 输出: After modify, m(1, 1) = 100
return 0;
}
|
小贴士:使用圆括号()而不是方括号[]来实现多维下标访问。[]只能接受一个参数,而()可以接受任意多个参数。用m(row, col)访问二维矩阵,比m[row * n + col]直观多了!
静态下标运算符
静态下标运算符意味着你可以不创建对象就直接调用ClassName[i],就像访问静态数组一样。这在设计"全局注册表"或"配置类"时很有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| #include <iostream>
class Registry {
public:
// 静态operator[]
// 可以在不创建对象的情况下调用
// 就像访问一个全局的注册表一样
static int data[5]; // 静态数据成员
static int& operator[](int index) {
return data[index];
}
};
// 静态成员定义
int Registry::data[5] = {10, 20, 30, 40, 50};
int main() {
// 不需要创建Registry对象就能使用
// 语法:ClassName[index]
std::cout << "Registry[0] = " << Registry[0] << std::endl; // 输出: Registry[0] = 10
Registry[0] = 999; // 也可以修改
std::cout << "Registry[0] = " << Registry[0] << std::endl; // 输出: Registry[0] = 999
return 0;
}
|
静态下标运算符看起来很酷,但使用时要小心语义清晰。Registry[0]到底是访问什么东西?如果你的类有多个静态数组,用命名更明确的静态方法可能更清晰。
13.6 箭头运算符重载
operator->是C++中最独特的运算符之一,它的特别之处在于:即使返回的是一个指针,你仍然可以继续使用->访问成员。这种"递归"调用机制叫作"指针代理"(Pointer Proxy),是实现智能指针和迭代器的基石。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| #include <iostream>
class Ptr {
private:
Widget* data_; // 代理一个Widget对象
public:
Ptr(Widget* p) : data_(p) {}
// operator-> 必须返回指针(或另一个重载了->的对象)
// 关键点:返回Widget*后,编译器会继续解引用,所以可以访问Widget的成员
Widget* operator->() const {
return data_;
}
~Ptr() { delete data_; }
};
class Widget {
public:
void display() const {
std::cout << "Widget::display() called!" << std::endl;
}
int value = 42;
};
int main() {
Widget w;
Ptr p(&w);
// p-> 返回 Widget*,但 -> 会继续解引用,所以可以访问 Widget 的成员!
p->display(); // 输出: Widget::display() called!
std::cout << "p->value = " << p->value << std::endl; // 输出: p->value = 42
return 0;
}
|
operator->的返回类型非常灵活:可以是T*(最常见),也可以是另一个重载了operator->的对象(形成调用链)。这就是智能指针如std::unique_ptr和迭代器如std::map::iterator的工作原理!
链式箭头运算符
operator->的一个重要特性是它可以被链式调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| #include <iostream>
class Inner {
public:
int x = 100;
};
class Middle {
public:
Inner inner;
Inner* operator->() { return &inner; }
};
class Outer {
public:
Middle middle;
Middle* operator->() { return &middle; }
};
int main() {
Outer obj;
// 链式调用:obj->x 会递归调用 operator->()
// obj.operator->() 返回 Middle*(指向 middle)
// 编译器发现返回值还是类类型指针,继续调用 Middle::operator->(),得到 Inner*
// 最终通过 Inner* 访问 x 成员
std::cout << "obj->x = " << obj->x << std::endl; // 输出: obj->x = 100
return 0;
}
|
关键点:operator->具有递归特性!当你写obj->x时,编译器会递归调用operator->(),直到获得一个真正的指针(Inner*),然后才执行->x访问。这就是智能指针和迭代器的实现原理——每一层代理都可以继续代理下去!
13.7 函数调用运算符重载
operator()被称为函数调用运算符(Functor Operator)。重载它可以让类的对象"假装"是一个函数——可以像函数一样用()调用,但同时还能保存状态!这在STL算法中超级有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| #include <iostream>
#include <vector>
#include <algorithm>
class Add {
private:
int delta_; // 要加的数值
public:
Add(int d) : delta_(d) {}
// 重载函数调用运算符
int operator()(int x) const {
return x + delta_; // 返回 x + delta
}
};
class MeanValue {
private:
std::vector<int> data_; // 存储数据
public:
MeanValue(const std::vector<int>& d) : data_(d) {}
// 无参函数调用运算符
double operator()() const {
if (data_.empty()) return 0.0;
long sum = 0;
for (int v : data_) sum += v;
return static_cast<double>(sum) / data_.size();
}
};
int main() {
// 函数对象(Functor):可以保存状态的"函数"
Add add5(5); // 创建了一个"加5"的计算器
std::cout << "add5(10) = " << add5(10) << std::endl; // 输出: add5(10) = 15
std::cout << "add5(100) = " << add5(100) << std::endl; // 输出: add5(100) = 105
// 用于STL算法:std::transform
std::vector<int> nums = {1, 2, 3, 4, 5};
// 将每个元素加10
std::transform(nums.begin(), nums.end(), nums.begin(), Add(10));
std::cout << "After adding 10: ";
for (int n : nums) std::cout << n << " "; // 输出: 11 12 13 14 15
std::cout << std::endl;
// 计算平均值
MeanValue mean(nums);
std::cout << "Mean value: " << mean() << std::endl; // 输出: Mean value: 13
return 0;
}
|
为什么用函数对象而不是普通函数?因为函数对象可以保存状态!Add(10)创建了一个"加10"的计算器,这个对象里存着数字10。每次调用operator()时,它都知道要加10。而普通函数addTen(x)就只是一个函数,无法保存额外信息。
静态函数调用运算符
函数调用运算符也可以是静态的!这意味着你可以不创建对象就调用ClassName(args),就像调用一个命名空间函数一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| #include <iostream>
class Adder {
public:
// 静态operator()
// 可以不用创建对象直接调用
// Adder(1, 2) 就像调用一个命名空间函数
static int operator()(int a, int b) {
return a + b;
}
static std::string operator()(const std::string& a, const std::string& b) {
return a + b; // 字符串拼接
}
};
int main() {
// 不需要创建Adder对象
std::cout << "Adder(1, 2) = " << Adder(1, 2) << std::endl; // 输出: Adder(1, 2) = 3
std::cout << "Adder(\"Hello\", \" World\") = " << Adder("Hello", " World") << std::endl;
// 输出: Adder("Hello", " World") = Hello World
return 0;
}
|
静态operator()的好处是可以把相关的静态方法集中在一个类里。如果你有一组功能相关的纯函数,可以用这种方式"命名空间化"它们,同时语法上还是函数调用。
13.8 递增递减运算符重载
++和--看起来简单,但它们有前置和后置两个版本。前置返回引用(++后马上用),后置返回值(旧值的拷贝)。这个区别在重载时尤为重要!
前置与后置的区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| #include <iostream>
class Counter {
private:
int value_; // 计数器当前值
public:
Counter(int v = 0) : value_(v) {}
// 前置++:先加1,返回引用
// 语义:++c 表示"先增加,再使用"
Counter& operator++() {
++value_; // 先增加
return *this; // 返回引用,性能更好
}
// 后置++:先返回旧值,再加1
// 关键:用int参数区分前置版本!编译器会传0作为这个参数
Counter operator++(int) {
Counter temp(*this); // 拷贝当前值的副本
++value_; // 增加
return temp; // 返回旧值的副本(不是引用!)
}
// 前置--:先减1,返回引用
Counter& operator--() {
--value_;
return *this;
}
// 后置--:先返回旧值,再减1
Counter operator--(int) {
Counter temp(*this);
--value_;
return temp;
}
int get() const { return value_; }
};
int main() {
Counter c(5);
std::cout << "c = " << c.get() << std::endl; // 输出: c = 5
Counter c1 = ++c; // 前置:先加到6,然后赋值给c1
std::cout << "++c = " << c1.get() << ", c = " << c.get() << std::endl;
// 输出: ++c = 6, c = 6 (c自己也变成6了)
Counter c2 = c++; // 后置:先赋值c2=6,然后c再加1变成7
std::cout << "c++ = " << c2.get() << ", c = " << c.get() << std::endl;
// 输出: c++ = 6, c = 7 (c2是旧值,c是新值)
Counter c3 = --c; // 前置递减
std::cout << "--c = " << c3.get() << ", c = " << c.get() << std::endl;
// 输出: --c = 6, c = 6
Counter c4 = c--; // 后置递减
std::cout << "c-- = " << c4.get() << ", c = " << c.get() << std::endl;
// 输出: c-- = 6, c = 5
return 0;
}
|
小技巧:如何记住前置和后置的区别?
- 前置:
++i → 先变后用 → 返回引用 - 后置:
i++ → 先用后变 → 返回值(副本)
小孩子才做选择,成年人全都要——但要分清楚场景!如果你不需要返回旧值,用前置更高效(省去了拷贝构造)。
13.9 类型转换运算符
类型转换运算符(operator Type())允许类的对象在特定情况下自动或显式转换为其他类型。这在设计API时非常有用,但也要小心隐式转换的陷阱!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| #include <iostream>
#include <string>
class Fraction {
private:
int numerator_; // 分子
int denominator_; // 分母
public:
Fraction(int n, int d) : numerator_(n), denominator_(d) {}
// 转换为double(显式转换,防止意外)
// explicit关键字:必须用static_cast才能转换
explicit operator double() const {
return static_cast<double>(numerator_) / denominator_;
}
// 转换为int(隐式转换,会截断小数部分)
// 注意:这里故意不用explicit,是为了演示隐式转换的陷阱
// 实际项目中,建议也加上explicit
operator int() const {
return numerator_ / denominator_; // 整数除法,直接截断
}
void print() const {
std::cout << numerator_ << "/" << denominator_ << std::endl;
}
};
int main() {
Fraction f(7, 3); // 7/3 = 2.333...
f.print(); // 输出: 7/3
// 显式转换为double:需要static_cast
double d = static_cast<double>(f);
std::cout << "As double: " << d << std::endl; // 输出: As double: 2.33333
// 隐式转换为int:直接赋值就自动转换了
int i = f; // 7/3 = 2(截断)
std::cout << "As int: " << i << std::endl; // 输出: As int: 2
return 0;
}
|
什么时候用explicit?当你不想让转换悄悄发生时。explicit operator double()意味着你必须写static_cast<double>(f)才能转换,不能写double d = f;。这可以防止一些意外的类型转换。
避免隐式转换陷阱
隐式类型转换是双刃剑——用得好是神器,用不好是坑货。来看看如何避免隐式转换的陷阱:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| #include <iostream>
#include <string>
class String {
private:
std::string data_; // 实际存储的字符串
public:
String(const std::string& s) : data_(s) {}
// explicit构造函数:不能隐式地从const char*构造String
explicit String(const char* s) : data_(s) {}
// 非explicit的转换运算符:可以隐式转换为const char*
operator const char*() const {
return data_.c_str();
}
};
void processString(const std::string& s) {
std::cout << "Processing: " << s << std::endl;
}
int main() {
// String s1 = "Hello"; // 编译错误!因为String的构造函数是explicit
// 不能隐式转换:const char* -> String
String s2("Hello"); // OK:显式构造
String s3(std::string("World")); // OK:显式构造
// processString(s2); // 危险!需要隐式转换:String -> const char* -> std::string
// 可能产生意外行为,不推荐!
processString(static_cast<std::string>(s2)); // OK:显式转换,明确告诉编译器要什么
return 0;
}
|
黄金法则:如果构造函数只接受一个参数,加上explicit关键字可以防止意外的类型转换。除非你真的希望这种隐式转换发生。
13.10 三向比较运算符 <=>(C++20)
C++20引入了一个革命性的运算符——宇宙飞船运算符<=>(Spaceship Operator)!它的名字来源于星际迷航中的宇宙飞船。这个运算符一次比较就能返回三种结果:小于、等于、大于。更棒的是,只要你定义了<=>,编译器就能自动生成所有比较运算符!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| #include <iostream>
#include <compare>
class Version {
private:
int major_, minor_, patch_; // 版本号:主版本.次版本.补丁版本
public:
Version(int m, int n, int p) : major_(m), minor_(n), patch_(p) {}
// C++20: 三向比较运算符(宇宙飞船运算符)
// 返回一个可以比较的对象,支持 <, ==, > 等
auto operator<=>(const Version& other) const {
// 链式比较:先比major,不相等就返回;相等再比minor...
if (auto cmp = major_ <=> other.major_; cmp != 0) return cmp;
if (auto cmp = minor_ <=> other.minor_; cmp != 0) return cmp;
return patch_ <=> other.patch_;
}
// == 运算符自动生成(基于<=>)
bool operator==(const Version& other) const = default;
void print() const {
std::cout << major_ << "." << minor_ << "." << patch_ << std::endl;
}
};
int main() {
Version v1(1, 2, 3);
Version v2(1, 2, 3);
Version v3(1, 3, 0);
std::cout << "v1 == v2: " << (v1 == v2) << std::endl; // 输出: 1 (true)
std::cout << "v1 < v3: " << (v1 < v3) << std::endl; // 输出: 1 (true,minor更小)
std::cout << "v1 > v3: " << (v1 > v3) << std::endl; // 输出: 0 (false)
std::cout << "v1 <=> v3 结果: " << ((v1 <=> v3) == 0 ? "等于" : ((v1 <=> v3) < 0 ? "小于" : "大于")) << std::endl;
// 输出: v1 <=> v3 结果: 小于
return 0;
}
|
<=>运算符的返回值是一个比较类别(comparison category)类型:
std::strong_ordering:用于没有等价概念的比较(如整数)std::weak_ordering:用于有等价概念但不等同的比较(如字符串忽略大小写)std::partial_ordering:用于支持"无序"比较的类型(如浮点数)
定义一次<=>,六种比较运算符全搞定!这就是C++20的威力!
13.11 默认比较(C++20)
如果你觉得<=>还是太复杂,C++20还提供了更简单的语法——= default!只要一行代码,编译器就能自动生成所有比较运算符。这简直是懒人福音!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| #include <iostream>
struct Point {
int x, y; // 坐标
// C++20: = default 自动生成所有比较运算符
// 编译器会自动生成 operator<=>、operator== 以及所有六个比较运算符
auto operator<=>(const Point&) const = default;
};
int main() {
Point p1{1, 2};
Point p2{1, 2};
Point p3{3, 4};
std::cout << "p1 == p2: " << (p1 == p2) << std::endl; // 输出: 1 (true)
std::cout << "p1 != p3: " << (p1 != p3) << std::endl; // 输出: 1 (true)
std::cout << "p1 < p3: " << (p1 < p3) << std::endl; // 输出: 1 (true,按字典序比较)
std::cout << "p1 <= p2: " << (p1 <= p2) << std::endl; // 输出: 1 (true)
// spaceship运算符自动推导所有比较
std::cout << "Default spaceship comparison works!" << std::endl;
// 输出: Default spaceship comparison works!
return 0;
}
|
= default的比较运算符使用成员逐个比较的字典序(lexicographical ordering)。也就是说,先比较x,如果相等再比较y。简单粗暴但有效!
13.12 运算符重载陷阱
运算符重载虽然强大,但也有雷区。以下是几个经典的"不要这样做":
不要重载&&、||、,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| #include <iostream>
class Widget {
public:
// 危险!不要重载 && || ,
bool operator&&(const Widget&) const {
std::cout << "Custom && called" << std::endl;
return true;
}
bool operator||(const Widget&) const {
std::cout << "Custom || called" << std::endl;
return true;
}
};
// 原因:短路求值规则会被破坏!
// 原生的&& || 短路求值:第一个条件为false/true时,不计算第二个
// 重载后:两个操作数都会被求值,且求值顺序不确定!
int main() {
Widget w1, w2;
// if (w1 && w2) { ... }
// 危险:编译器可能先算w2再算w1(与预期相反!)
// 而且由于是函数调用,参数求值顺序完全不确定
std::cout << "Danger: Don't overload && and ||" << std::endl;
std::cout << "Reason: Short-circuit evaluation is broken!" << std::endl;
return 0;
}
|
重要的事情说三遍:不要重载&&和||!不要重载&&和||!不要重载&&和||!
原生的&&和||有短路求值特性:如果是a && b,当a为false时,b根本不会被执行。如果是重载的operator&&,由于它是普通函数,所有参数在调用前都必须被求值,这就破坏了短路语义,可能导致未定义行为。
对称性问题
对称运算符(如+、-、*、/)需要是非成员函数,以确保交换律成立。也就是说,a + b应该等于b + a。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| #include <iostream>
class Number {
private:
int value_; // 存储的值
public:
Number(int v = 0) : value_(v) {}
// 如果+是对称的,a+b应该等于b+a
Number operator+(int rhs) const {
return Number(value_ + rhs);
}
// 问题:这只支持 Number + int,不支持 int + Number!
// 交换律问题:需要非成员函数来实现
// friend Number operator+(int lhs, const Number& rhs); // 友元声明
int getValue() const { return value_; }
};
// 实现交换律:int + Number 和 Number + int 都支持!
Number operator+(int lhs, const Number& rhs) {
return Number(lhs + rhs.getValue());
}
int main() {
Number n(10);
Number r1 = n + 5; // OK: n.operator+(5)
Number r2 = 5 + n; // OK: operator+(5, n)
std::cout << "n + 5 = " << r1.getValue() << std::endl; // 输出: 15
std::cout << "5 + n = " << r2.getValue() << std::endl; // 输出: 15
// 如果没有operator+(int, Number),5+n会编译失败!
// 编译器会报错:"没有匹配的operator+"
return 0;
}
|
对称性法则:对于数学上对称的运算符(如加法、乘法),一定要同时提供成员函数版本和非成员函数版本,或者只提供非成员函数版本。这样才能保证a + b和b + a都能正常工作。
本章小结
本章我们探索了C++运算符重载的精彩世界,现在让我们来回顾一下学到的要点:
| 运算符类型 | 重载方式 | 注意事项 |
|---|
算术运算符+, -, *, / | 成员或非成员 | 对称运算符用非成员实现交换律 |
复合赋值+=, -=, *= | 成员,返回引用 | 修改自身,支持链式调用 |
比较运算符==, <, >等 | 成员或非成员 | 推荐用C++20的<=>一键生成 |
<<, >> | 非成员,友元 | 返回ostream/istream引用 |
[] | 成员,返回引用 | 同时提供const版本 |
() 函数调用 | 成员 | 可保存状态的"函数",支持静态版本 |
-> 箭头运算符 | 成员 | 必须返回指针或另一个重载了->的对象 |
++, -- | 成员 | 用int参数区分后置版本 |
| 类型转换 | 成员 | 谨慎使用,考虑explicit |
核心原则:
- 保持语义:重载运算符应该做它"应该做"的事情
- 对称性:数学运算符要考虑交换律
- 避免陷阱:不要重载
&&、||、, - 善用C++20:
<=>可以一键生成所有比较运算符
运算符重载是C++最强大的特性之一,用得好可以让代码优雅得像诗,用不好则会变成天书。记住:能力越大,责任越大!
graph TD
A[运算符重载] --> B[算术运算符]
A --> C[比较运算符]
A --> D[下标运算符]
A --> E[函数调用运算符]
A --> F[输入输出运算符]
A --> G[箭头运算符]
B --> B1[保持对称性<br/>实现交换律]
C --> C1[推荐使用C++20<br/>spaceship运算符]
D --> D1[返回引用<br/>提供const版本]
E --> E1[函数对象<br/>可保存状态]
F --> F1[非成员函数<br/>友元声明]
G --> G1[返回指针或<br/>代理对象]最后的最后,记住一句箴言:运算符重载不是为了炫技,而是为了让代码更自然、更易读。 如果你发现自己重载的运算符语义很奇怪,那多半是你错了,不是语言错了。
恭喜你完成了第13章的学习!继续加油,向C++大师之路迈进! 🚀