面向对象(四):运算符重载
原理
运算符重载的本质
a + b 等价于 a.operator+(b)(成员形式)或 operator+(a, b)(非成员形式)。编译器遇到表达式时,先进行名称查找找到候选的 operator+ 函数,然后通过重载决议选择最佳匹配。
成员 vs 友元的底层区别
成员形式和友元形式在汇编层产生完全相同的代码——区别仅在于参数的语义标签:
; 成员形式 a+b → call X::operator+ , rdi=&a, rsi=&b
; 友元形式 a+b → call operator+(X,X), rdi=&a, rsi=&b
; 函数内部的指令完全相同决策表:
| 场景 | 选成员 | 选友元 | 原因 |
|---|---|---|---|
左操作数确定是 *this | 可 | 可 | 都可以,成员更自然 |
左操作数不是本类类型(如 5+obj, cout<<obj) | 不可 | 必须 | 成员函数要求左操作数有对应成员 |
= [] () -> | 必须 | 不可 | C++ 标准强制规定 |
三个不变原则
- 优先级不变:优先级是语法解析阶段就确定的,重载决议发生在语义分析阶段——远在 AST 构建之后
- 结合性不变:
a + b + c永远等价于(a + b) + c(左结合运算符) - 操作数个数不变:二元运算符永远接收两个操作数
返回类型的决策依据
| 返回类型 | 典型运算符 | 汇编特征 |
|---|---|---|
T(值返回) | + - * / | call new; call 构造; ret,创建新对象 |
T&(引用返回) | = += -= << | mov rax, rdi; ret,直接返回 this,零开销 |
bool | == != < > | xor eax, eax; cmp; sete al; ret |
前置 vs 后置自增的汇编
; 前置 ++c — operator++() → Counter&
inc dword [rdi] ; 直接自增(1 条指令)
mov rax, rdi ; 返回 this(1 条指令)
; 后置 c++ — operator++(int) → Counter
mov eax, [rdi] ; 保存旧值
inc dword [rdi] ; 自增
; 可能还有拷贝构造 → 至少多一次拷贝操作后置版本的 int 参数是语法标记,由编译器自动传入 0,仅用于重载决议中区分前后置。前置版本返回 T&(修改后自身),后置版本返回 T(旧值副本)。
转换构造函数与运算符的交互
转换构造函数(非 explicit 的单参数构造函数)允许隐式类型转换参与运算符匹配:
Complex a(1,2);
Complex b = a + 3.5; // 3.5 → Complex(3.5, 0.0),然后 operator+ 匹配
Complex c = 3.5 + a; // 编译错误!3.5 是 double,没有 operator+(Complex)explicit 关键字在重载决议中阻断隐式转换路径,编译器跳过该构造函数,只保留显式构造的路径。
ADL 让运算符跨命名空间工作
a + b 中如果 a 和 b 的类型在命名空间 N 中,ADL 自动把 N 加入 operator+ 的搜索范围。不用 using namespace N; 就能使用运算符——这是运算符重载自然语法的关键设计。
语法
算术与赋值运算符
class Vec {
double x, y;
public:
Vec operator+(const Vec& v) const { return {x+v.x, y+v.y}; }
Vec& operator+=(const Vec& v) { x+=v.x; y+=v.y; return *this; }
// 通常用 += 实现 +
friend Vec operator+(const Vec& a, const Vec& b) { Vec r(a); r+=b; return r; }
};比较运算符
bool operator==(const Vec& v) const { return x==v.x && y==v.y; }
bool operator!=(const Vec& v) const { return !(*this == v); }
bool operator<(const Vec& v) const { return x < v.x || (x==v.x && y<v.y); }
// >, <=, >= 通过 < 和 == 实现自增自减
Vec& operator++() { ++x; return *this; } // 前置:返回引用
Vec operator++(int) { Vec t(*this); ++x; return t; } // 后置:返回值下标与函数调用
T& operator[](size_t i) { return data[i]; } // 必须成员
void operator()(int x) { /* 函数对象 */ } // 必须成员流运算符(必须友元)
friend std::ostream& operator<<(std::ostream& os, const Vec& v) {
return os << "(" << v.x << ", " << v.y << ")";
}类型转换运算符
operator double() const { return length; } // 隐式转换(不推荐)
explicit operator int() const { return (int)x; } // 显式转换(C++11,推荐)
explicit operator bool() const { return valid; } // 条件判断可用字面量运算符(C++11)
Distance operator"" _km(long double val) { return Distance(val * 1000.0); }
auto d = 5.0_km; // Distance(5000.0)C++20 太空船运算符
auto operator<=>(const T&) const = default;
// 编译器自动生成 ==, !=, <, >, <=, >= 全部六个运算符实践
力扣题目:力扣日期处理题(日期类与比较运算符),力扣排序题(结构体排序与运算符重载)。
AI 自检:给 AI 以下代码,要求指出运算符重载中的错误并解释原因:
Vec operator+(const Vec& v) { x += v.x; y += v.y; return *this; }
// 这个 operator+ 和预期的语义有什么不同?建议先阅读:05_面向对象(一)类与对象基础 — 转换构造函数和 explicit 的深入讲解;03_指针与引用 — 引用返回 vs 值返回的底层区别和性能影响。