面向对象(四):运算符重载

原理

运算符重载的本质

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++ 标准强制规定

三个不变原则

  1. 优先级不变:优先级是语法解析阶段就确定的,重载决议发生在语义分析阶段——远在 AST 构建之后
  2. 结合性不变a + b + c 永远等价于 (a + b) + c(左结合运算符)
  3. 操作数个数不变:二元运算符永远接收两个操作数

返回类型的决策依据

返回类型典型运算符汇编特征
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 值返回的底层区别和性能影响。