目录
[三、成员函数 vs 全局函数:如何选择?](#三、成员函数 vs 全局函数:如何选择?)
[危险示例:重载 && 失去短路求值](#危险示例:重载 && 失去短路求值)
[1. 不能发明新运算符](#1. 不能发明新运算符)
[2. 不能改变优先级和结合性](#2. 不能改变优先级和结合性)
[3. 不能改变操作数个数](#3. 不能改变操作数个数)
[4. 不能改变内置类型的行为](#4. 不能改变内置类型的行为)
[5. 至少一个操作数是用户自定义类型](#5. 至少一个操作数是用户自定义类型)
[1. 忘记 const 正确性](#1. 忘记 const 正确性)
[2. 返回类型错误](#2. 返回类型错误)
[3. 没有处理自赋值(+= 等)](#3. 没有处理自赋值(+= 等))
[4. 重载 && 或 || 以为有短路求值](#4. 重载 && 或 || 以为有短路求值)
一、为什么需要运算符重载?
写一个复数类 Complex,没有运算符重载时:
cpp
Complex a(1, 2), b(3, 4);
Complex c = add(a, b); // 不能写 a + b
Complex d = multiply(a, b); // 不能写 a * b
这很不直观。数学上复数就应该用 +、-、* 运算。运算符重载让代码更自然:
cpp
Complex a(1, 2), b(3, 4);
Complex c = a + b; // 清晰!
Complex d = a * b; // 直观!
本质 :运算符重载就是定义特殊的函数------函数名是 operator 加上运算符符号。
二、基本语法
两种实现方式
| 方式 | 函数名 | 参数个数 | 示例 |
|---|---|---|---|
| 成员函数 | operator@ |
少一个(左侧对象是this) | a + b → a.operator+(b) |
| 全局函数 | operator@ |
全部作为参数 | a + b → operator+(a, b) |
示例:加法运算符
成员函数版本:
cpp
class Complex {
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
};
Complex a(1, 2), b(3, 4);
Complex c = a + b; // 等价于 a.operator+(b)
全局函数版本(通常需要友元):
cpp
class Complex {
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
friend Complex operator+(const Complex& a, const Complex& b);
};
Complex operator+(const Complex& a, const Complex& b) {
return Complex(a.real + b.real, a.imag + b.imag);
}
三、成员函数 vs 全局函数:如何选择?
| 运算符 | 推荐方式 | 原因 |
|---|---|---|
= () [] -> |
必须是成员函数 | C++语法规定 |
+ - * / |
成员或全局均可 | 通常用成员函数 |
<< >> |
必须是全局函数 | 左操作数不是本类对象 |
| 双目运算符 | 两种都行 | 看是否需要访问私有成员 |
| 单目运算符 | 成员函数更自然 | ++a 对应 a.operator++() |
指导原则
-
左操作数不是你控制的类型 → 必须用全局函数
cpp
// cout << obj,左操作数是ostream,你改不了ostream friend ostream& operator<<(ostream& os, const MyClass& obj); -
需要对称性 → 用全局函数(比如
int + Complex和Complex + int都支持) -
自然属于对象的行为 → 用成员函数(比如
obj += other)
四、可以重载的运算符
C++大部分运算符都可以重载(共约40个):
算术运算符
text
+ - * / %
+= -= *= /= %=
关系/逻辑运算符
text
== != < > <= >=
&& || !
位运算符
text
& | ^ ~ << >>
&= |= ^= <<= >>=
自增自减
text
++ -- (前置和后置)
下标/调用/成员访问
text
[] () -> ->
赋值/内存管理
text
= new delete new[] delete[]
逗号/指针解引用
text
, *
五、不能重载的运算符
有5个运算符不能重载(C++语法禁止):
| 运算符 | 名称 | 不能重载的原因 |
|---|---|---|
:: |
作用域解析 | 操作的是类型和命名空间,不是值 |
. |
成员访问 | 保持内置语义,保证成员访问的安全性 |
.* |
成员指针解引用 | 语义复杂,重载会混乱 |
?: |
三元条件 | 第二个参数可以是表达式,重载无法模拟短路求值 |
sizeof |
获取大小 | 编译时运算,不能变成运行时函数 |
还有几个特殊情况
| 运算符 | 情况 | 说明 |
|---|---|---|
= |
可以重载 | 但必须是成员函数,且编译器默认生成 |
() |
可以重载 | 必须是成员函数(仿函数) |
[] |
可以重载 | 必须是成员函数 |
-> |
可以重载 | 必须是成员函数,且返回值必须是指针或可重载->的对象 |
&& ` |
` |
危险示例:重载 && 失去短路求值
cpp
bool operator&&(const MyClass& a, const MyClass& b) {
// 编译器会求值两个操作数后才调用这个函数
// 短路求值失效!
}
规则 :除非有特殊需求,否则不要重载 &&、|| 和 ,。
六、运算符重载的规则和限制
1. 不能发明新运算符
cpp
// ❌ 不能定义 @ 或 # 这种新符号
MyClass operator@(MyClass a, MyClass b); // 错误
2. 不能改变优先级和结合性
cpp
// + 的优先级永远高于 ==,无法改变
3. 不能改变操作数个数
cpp
// ++ 永远是单目,不能变成三目
MyClass operator++(MyClass a, MyClass b, MyClass c); // 错误
4. 不能改变内置类型的行为
cpp
// ❌ 不能重载 int 的 +
int operator+(int a, int b); // 错误
5. 至少一个操作数是用户自定义类型
cpp
// ✅ 正确:有一个是 Complex
Complex operator+(const Complex& a, const Complex& b);
// ❌ 错误:两个都是内置类型
int operator+(int a, int b); // 错误
七、完整例子:有理数类的运算符重载
cpp
#include <iostream>
#include <numeric> // for gcd
using namespace std;
class Rational {
private:
int num; // 分子
int den; // 分母
void reduce() {
int g = gcd(num, den);
num /= g;
den /= g;
if (den < 0) { num = -num; den = -den; }
}
public:
Rational(int n = 0, int d = 1) : num(n), den(d) {
if (den == 0) throw invalid_argument("分母不能为0");
reduce();
}
// 算术运算符(成员函数版本)
Rational operator+(const Rational& other) const {
return Rational(num * other.den + other.num * den,
den * other.den);
}
Rational operator-(const Rational& other) const {
return Rational(num * other.den - other.num * den,
den * other.den);
}
Rational operator*(const Rational& other) const {
return Rational(num * other.num, den * other.den);
}
Rational operator/(const Rational& other) const {
return Rational(num * other.den, den * other.num);
}
// 复合赋值运算符(成员函数)
Rational& operator+=(const Rational& other) {
*this = *this + other;
return *this;
}
// 比较运算符
bool operator==(const Rational& other) const {
return num == other.num && den == other.den;
}
bool operator<(const Rational& other) const {
return num * other.den < other.num * den;
}
// 输入输出运算符(必须是全局函数,后面详解)
friend ostream& operator<<(ostream& os, const Rational& r);
friend istream& operator>>(istream& is, Rational& r);
};
// 全局函数:输出
ostream& operator<<(ostream& os, const Rational& r) {
if (r.den == 1) os << r.num;
else os << r.num << "/" << r.den;
return os;
}
// 全局函数:输入
istream& operator>>(istream& is, Rational& r) {
is >> r.num;
char slash;
is >> slash;
if (slash == '/') is >> r.den;
else r.den = 1;
r.reduce();
return is;
}
int main() {
Rational a(1, 2); // 1/2
Rational b(2, 3); // 2/3
cout << a << " + " << b << " = " << a + b << endl;
cout << a << " - " << b << " = " << a - b << endl;
cout << a << " * " << b << " = " << a * b << endl;
cout << a << " / " << b << " = " << a / b << endl;
Rational c(3, 4);
c += Rational(1, 4);
cout << "c += 1/4 -> " << c << endl;
cout << "1/2 == 2/4 ? " << (Rational(1,2) == Rational(2,4)) << endl;
cout << "1/2 < 2/3 ? " << (Rational(1,2) < Rational(2,3)) << endl;
return 0;
}
输出:
text
1/2 + 2/3 = 7/6
1/2 - 2/3 = -1/6
1/2 * 2/3 = 1/3
1/2 / 2/3 = 3/4
c += 1/4 -> 1
1/2 == 2/4 ? 1
1/2 < 2/3 ? 1
八、常见错误
1. 忘记 const 正确性
cpp
Complex operator+(Complex& other) { ... } // 无法加const对象
应该用 const Complex& 参数,函数本身也要是 const(成员函数时)。
2. 返回类型错误
cpp
Complex operator+(const Complex& a, const Complex& b) {
Complex result;
// ...
return &result; // ❌ 不要返回局部变量的指针/引用
}
3. 没有处理自赋值(+= 等)
cpp
Complex& operator+=(const Complex& other) {
real += other.real;
imag += other.imag;
return *this; // ✅ 返回引用,支持链式调用
}
4. 重载 && 或 || 以为有短路求值
重载后两个操作数都会被求值,行为与内置 && 不同,容易迷惑使用者。
九、这一篇的收获
你现在应该理解:
-
语法 :
operator@定义运算符重载函数 -
实现方式:成员函数(左操作数是 this)vs 全局函数(两侧对称)
-
不可重载的5个 :
::..*?:sizeof -
规则:不能发明新运算符、不能改优先级/结合性、至少一个操作数是自定义类型
-
选择原则 :
=()[]->必须是成员;<<>>必须是全局;其余看设计
💡 小作业:实现一个
Vector3D类(x, y, z),重载+、-、*(点积,返回 double)、==、<<。测试v1 + v2、v1 * v2等操作。
下一篇预告 :第22篇《输入输出运算符重载:<< 与 >> 的友元实现》------为什么 cout << obj 必须用全局函数?为什么要用友元?如何正确实现 << 和 >> 支持链式输出/输入?下篇详解。