运算符重载概述¶
运算符重载是C++最具特色的语言特性之一,它让自定义类型能够像内置类型一样自然地使用运算符,极大地提升了代码的可读性和表达能力。
从问题出发:为什么需要运算符重载¶
内置类型的运算符支持¶
在C++中,内置类型(int、double、char 等)天然支持丰富的运算符。这些运算符的使用直观自然,是程序员习以为常的表达方式。
内置类型的运算符使用
自定义类型的困境¶
当我们定义了自己的类(如复数、向量、字符串等),情况就不同了:
自定义类型无法直接使用运算符
思考
复数在数学上天然支持加法、减法等运算。如果每次都要调用 add()、subtract() 这样的成员函数,代码会变得冗长且不自然。能否让复数类也像 int 一样使用 +、- 运算符?
运算符重载的意义¶
运算符重载(Operator Overloading) 正是为解决这一问题而设计的。它允许程序员为自定义类型定义运算符的行为,使自定义类型获得与内置类型一致的使用体验。
运算符重载的核心价值
- 自然直观:
c3 = c1 + c2比c3 = c1.add(c2)更符合直觉。 - 代码简洁:运算符语法比函数调用更简洁,减少代码量。
- 类型统一:自定义类型可以和内置类型一样自然地参与表达式。
- 领域表达:让代码更贴近问题领域的表达习惯(如线性代数、物理计算等)。
运算符重载的基本概念¶
什么是运算符重载¶
运算符重载是对已有运算符赋予新的含义,使同一个运算符作用于不同类型的数据时表现出不同的行为。
重载的含义
运算符重载本质上是一种特殊的函数重载——通过定义特定的函数(operator+、operator- 等)来实现对运算符行为的定制。
两种重载方式¶
C++ 支持两种重载运算符的方式:
运算符重载的两种方式
| 方式 | 实现位置 | 调用方式 | 典型场景 |
|---|---|---|---|
| 成员函数 | 类的成员函数 | oprd1.operator+(oprd2) |
左操作数是本类对象 |
| 非成员函数 | 普通函数(可友元) | operator+(oprd1, oprd2) |
左操作数不是本类对象 |
运算符重载的基本规则¶
可以重载与不能重载的运算符¶
可以重载的运算符
C++ 几乎可以重载全部已有的运算符(不能发明新运算符):
| 类别 | 运算符 |
|---|---|
| 算术运算符 | + - * / % |
| 关系运算符 | == != < > <= >= |
| 逻辑运算符 | ! && || |
| 位运算符 | & | ~ ^ << >> |
| 赋值运算符 | = += -= *= /= %= &= |= ^= <<= >>= |
| 自增/自减 | ++ -- |
| 下标/函数调用 | [] () |
| 指针/成员访问 | * -> ->* |
| 内存管理 | new delete new[] delete[] |
| 逗号/类型转换 | , 类型转换运算符 |
不能重载的运算符
| 运算符 | 原因 |
|---|---|
.(成员访问) |
保证成员访问的语义不变 |
.*(成员指针访问) |
保证成员指针访问的语义不变 |
::(作用域限定) |
保证名称查找的语义不变 |
?:(条件运算符) |
保证短路求值的语义不变 |
sizeof |
编译时求值,无法重载 |
typeid |
运行时类型信息,无法重载 |
static_cast 等 |
编译时类型转换,无法重载 |
不可改变的特性¶
运算符重载不会改变以下特性
- 优先级不变:
*的优先级仍然高于+。 - 结合性不变:
a - b - c仍然等价于(a - b) - c。 - 操作数个数不变:一元运算符仍需要一个操作数,二元需要两个。
- 语法结构不变:
a + b仍需要两个操作数,不能写成a +或+ b。
至少一个操作数是自定义类型¶
重要约束
运算符重载要求至少有一个操作数是自定义类型(类或枚举)。不能改变内置类型的运算符行为。
不能发明新运算符¶
注意
只能重载 C++ 已有的运算符,不能创建新的运算符符号:
运算符重载的语义约定¶
保持自然语义¶
最重要的设计原则
运算符重载后的行为应该与运算符的原始语义保持一致,让使用者能够凭直觉理解代码。
重载运算符是为了让代码更自然,而不是为了"炫技"或制造困惑。
好与坏的对比
保持一致性¶
一致的运算符应该表现一致
| 一致性要求 | 说明 |
|---|---|
| 对称性 | a + b 和 b + a 应产生相同结果(如果运算满足交换律) |
| 传递性 | 如果有 a < b 和 b < c,则应有 a < c(严格弱序) |
| 与复合运算符一致 | a + b 的行为应与 a += b 后 a 的值一致 |
| 与反向运算符一致 | 重载了 <,通常也应重载 >、<=、>= |
| 与内置类型一致 | 重载后的行为应模仿对应内置类型的语义 |
操作数的类型¶
操作数可以是不同类型
运算符重载时,操作数不必是同一类型,可以支持混合运算:
运算符重载的应用场景¶
适合重载运算符的典型场景
| 类型 | 适合重载的运算符 | 说明 |
|---|---|---|
| 数值类型(复数、向量、矩阵) | + - * / == != |
使数值运算自然直观 |
| 容器类(数组、字符串、列表) | [] = == != < |
像内置数组一样访问元素 |
| 智能指针 | * -> bool |
像普通指针一样使用 |
| 迭代器 | ++ -- * == != |
像指针一样遍历集合 |
| 函数对象 | () |
像函数一样被调用 |
| I/O 流 | << >> |
支持流式输入输出 |
小结¶
运算符重载是C++强大表现力的重要来源。正确使用运算符重载,可以让自定义类型获得与内置类型一致的使用体验,使代码更加简洁、自然和可读。
-
运算符重载的意义:让自定义类型像内置类型一样自然地使用运算符,提高代码可读性和表达力。
-
基本规则:
- 只能重载已有的运算符,不能发明新运算符。
- 不能改变运算符的优先级、结合性和操作数个数。
- 至少一个操作数是自定义类型。
- 不能重载
.、.*、::、?:、sizeof等运算符。
-
两种重载方式:
- 成员函数:左操作数是本类对象。
- 非成员函数:左操作数不是本类对象(或需要混合运算支持)。
-
设计准则:
- 保持运算符的自然语义。
- 保持一致性(对称性、与复合运算符一致等)。
- 让代码符合直觉,而不是制造困惑。