16、运算符重载:重载规则、常见运算符重载(+ - * / == << >>)、函数调用运算符(仿函数)、类型转换运算符

运算符重载,说白了就是让 C++ 的运算符能“认识”你自定义的类型。我刚开始学的时候觉得这玩意儿挺花哨,后来在项目里写一个矩阵类,发现如果没有重载 +*,代码会变成一坨屎——到处都是 matrix.add(matrix2) 这种调用,可读性极差。嗯,从那以后我就明白了,运算符重载不是为了炫技,而是为了让代码更自然。

16.1 重载规则:别踩红线

运算符重载不是你想怎么来就怎么来的。C++ 有一整套规则,我帮你梳理一下最核心的几条。

  • 不能发明新运算符:你只能重载已有的运算符,比如 +-*/,不能自己造个 ** 出来。
  • 不改变优先级和结合性:重载后的 + 依然是左结合,优先级不变。我曾经见过有人试图让 +* 优先,结果编译器直接报错。
  • 不能重载的运算符::..*?:sizeoftypeid。这些是 C++ 的“禁区”,别去碰。
  • 至少有一个操作数是用户定义类型:你不能重载 int + int 的行为,那是语言内置的。
  • 保持语义一致:重载 + 就应该做加法或拼接,别搞成减法。我见过一个项目里有人把 + 重载成“删除元素”,那代码简直没法维护。

核心原则:运算符重载应该让代码更直观,而不是更混乱。如果你重载后别人看不懂,那还不如写个普通函数。

16.2 常见运算符重载:+ - * / == << >>

这部分是实战中最常用的。我一个个讲,每个都带代码示例。

16.2.1 算术运算符:+ - * /

通常重载为非成员函数,这样支持隐式类型转换。比如 3 + objobj + 3 都能工作。

class Complex {
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    // 复合赋值运算符通常重载为成员函数
    Complex& operator+=(const Complex& other) {
        real += other.real;
        imag += other.imag;
        return *this;
    }

    // 友元声明,让非成员函数能访问私有成员
    friend Complex operator+(const Complex& lhs, const Complex& rhs);

private:
    double real, imag;
};

// 非成员函数实现
Complex operator+(const Complex& lhs, const Complex& rhs) {
    Complex temp(lhs);
    temp += rhs;  // 复用 +=
    return temp;
}

我个人习惯把 += 实现成成员函数,然后 + 基于 += 实现。这样代码复用率高,而且 += 返回引用,效率更高。

小技巧-*/ 的模式完全一样。你只需要改一下运算符和内部逻辑,其他结构照搬就行。

16.2.2 比较运算符:==

重载 == 时,记得也要重载 !=。C++20 之后有 operator<=> 可以自动生成,但 C++17 及之前你得手动写。

bool operator==(const Complex& lhs, const Complex& rhs) {
    return (lhs.real == rhs.real) && (lhs.imag == rhs.imag);
}

bool operator!=(const Complex& lhs, const Complex& rhs) {
    return !(lhs == rhs);
}

我曾经在项目里只重载了 ==,忘了 !=,结果编译时一堆错误。嗯,从那以后我都是成对重载。

16.2.3 输入输出运算符:<< >>

这两个必须重载为非成员函数,因为左操作数是 ostreamistream,你不能修改标准库的类。

// 输出运算符
std::ostream& operator<<(std::ostream& os, const Complex& c) {
    os << "(" << c.real << ", " << c.imag << "i)";
    return os;
}

// 输入运算符
std::istream& operator>>(std::istream& is, Complex& c) {
    double r, i;
    is >> r >> i;
    if (is) {  // 检查输入是否成功
        c.real = r;
        c.imag = i;
    }
    return is;
}

注意:输入运算符一定要检查输入状态。我曾经没加 if (is),结果用户输入非法字符时,对象被污染了,后续计算全错。血的教训。

16.3 函数调用运算符(仿函数)

重载 operator() 可以让对象像函数一样被调用。这种对象叫“仿函数”或“函数对象”。

class Adder {
public:
    int operator()(int a, int b) const {
        return a + b;
    }
};

// 使用
Adder add;
int result = add(3, 4);  // 像函数一样调用
std::cout << result;     // 输出 7

仿函数最大的优势是可以携带状态。普通函数做不到这一点。

class ThresholdFilter {
public:
    explicit ThresholdFilter(int threshold) : threshold_(threshold) {}

    bool operator()(int value) const {
        return value > threshold_;
    }

private:
    int threshold_;
};

// 使用
ThresholdFilter filter(10);
std::vector<int> data = {5, 12, 8, 20};
auto it = std::find_if(data.begin(), data.end(), filter);
// it 指向第一个大于 10 的元素,即 12

我在项目里用仿函数做自定义排序特别多。比如按某个字段排序,把比较逻辑封装在仿函数里,传给 std::sort,代码干净又灵活。

为什么用仿函数而不是 lambda? C++11 之后 lambda 确实更方便,但仿函数在需要多次复用同一逻辑携带复杂状态时,可读性更好。我个人习惯:简单场景用 lambda,复杂场景用仿函数。

16.4 类型转换运算符

类型转换运算符允许你的类隐式或显式地转换成其他类型。语法是 operator 目标类型()

class Rational {
public:
    Rational(int num = 0, int den = 1) : numerator_(num), denominator_(den) {}

    // 隐式转换为 double
    operator double() const {
        return static_cast<double>(numerator_) / denominator_;
    }

    // 显式转换为 int(C++11 起)
    explicit operator int() const {
        return numerator_ / denominator_;
    }

private:
    int numerator_, denominator_;
};

// 使用
Rational r(3, 4);
double d = r;       // 隐式转换,d = 0.75
int i = static_cast<int>(r);  // 显式转换,i = 0(整数除法)

小心隐式转换的陷阱:我曾经在项目里写了一个 operator bool(),结果导致 if (obj) 能编译通过,但 obj + 5 也能编译通过(因为 bool 可以隐式转 int)。从那以后,我所有类型转换运算符都加 explicit,除非有非常明确的理由不这么做。

你想想看,类型转换运算符就像一把双刃剑。用得好,代码简洁;用不好,隐式转换会让你 debug 到怀疑人生。我建议:默认加 explicit,需要隐式转换时再去掉

知识体系图

下面这张图帮你理清运算符重载的整体脉络。我画的时候特意把“规则”放在最上面,因为那是地基,地基不稳什么都白搭。

运算符重载知识体系 重载规则(红线不能踩) 常见运算符重载 + - * / == != << >> += -= 等 函数调用运算符 operator() 重载 可携带状态 用于 STL 算法 类型转换运算符 operator double() explicit 关键字 隐式转换陷阱 核心:让代码更自然,而不是更花哨

好了,运算符重载的核心内容就这些。记住规则,多写多练,你很快就能掌握。我在项目里用这些技术写过矩阵库、复数类、甚至一个简单的 DSL 解析器,每次都觉得 C++ 的运算符重载真是把双刃剑——用好了代码优雅,用砸了就是灾难。嗯,希望你成为前者。


公众号:蓝海资料掘金营,微信 deep3321