运算符重载:重载规则、重载+、-、<<、>>、++、--

运算符重载,说白了就是让C++的运算符能「听懂」你自定义的类型。比如你写了个复数类,想让两个复数能直接用+相加,而不是写c1.add(c2)这种啰嗦的调用。嗯,这就是运算符重载的用武之地。

我记得刚学C++那会儿,觉得这东西挺神奇的。后来做项目才发现,用好了代码确实优雅,用不好就是给自己挖坑。今天咱们就聊聊运算符重载的那些事儿。

重载规则:别乱来,有规矩

运算符重载不是你想怎么来就怎么来的。C++定了一套规则,你得遵守。

  • 不能发明新运算符:你只能重载已有的运算符,比如+-*这些。别想着自己造个**出来,编译器不认。
  • 优先级和结合性不变:重载不会改变运算符的优先级。比如*永远比+优先,不管你重载成啥样。
  • 不能改变操作数个数:一元运算符只能有一个操作数,二元运算符只能有两个。你不能把+重载成三个参数的。
  • 有些运算符不能重载:比如::..*?:sizeof。这些是C++的「禁区」,碰不得。

注意:我曾经见过有人试图重载&&||,结果短路求值特性丢了,代码跑起来跟预期完全不一样。这两个运算符最好别碰。

重载+和-:让对象能加减

咱们先来个简单的例子——复数类。复数有实部和虚部,两个复数相加就是实部加实部,虚部加虚部。

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

    // 重载+运算符
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }

    // 重载-运算符
    Complex operator-(const Complex& other) const {
        return Complex(real - other.real, imag - other.imag);
    }

    void display() const {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

你看,operator+operator-就是普通的成员函数,只不过名字特殊了点。调用的时候,c1 + c2实际上就是c1.operator+(c2)

小技巧:我个人习惯把不修改当前对象的运算符(比如+-)声明为const成员函数。这样既能保证原对象不被意外修改,也能让const对象调用这些运算符。

重载<<和>>:让对象能输入输出

这个有点特殊。<<>>的左侧是ostreamistream对象,不是你的类对象。所以你不能把它们重载为成员函数——除非你修改标准库的类,但那不可能。

正确的做法是重载为友元函数普通函数

class Complex {
    // ... 前面的代码不变 ...

    // 声明为友元
    friend std::ostream& operator<<(std::ostream& os, const Complex& c);
    friend std::istream& operator>>(std::istream& is, Complex& c);
};

// 重载<<运算符
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) {
    std::cout << "请输入实部和虚部: ";
    is >> c.real >> c.imag;
    return is;
}

这里有个关键点:返回引用。为什么?因为你要支持链式调用,比如cout << c1 << c2。如果返回的是值,那第二次<<就不知道往哪写了。

重点operator<<的第一个参数必须是std::ostream&,第二个参数是你的类对象(通常用const引用)。返回值必须是std::ostream&。这个模式是固定的,别搞反了。

重载++和--:前置和后置的区别

这个坑我踩过。前置++和后置++怎么区分?C++用了一个小技巧:后置版本加一个int参数,但这个参数不用传值,它只是个标记。

class Counter {
private:
    int value;
public:
    Counter(int v = 0) : value(v) {}

    // 前置++
    Counter& operator++() {
        ++value;
        return *this;
    }

    // 后置++
    Counter operator++(int) {
        Counter temp = *this;
        ++value;
        return temp;
    }

    // 前置--
    Counter& operator--() {
        --value;
        return *this;
    }

    // 后置--
    Counter operator--(int) {
        Counter temp = *this;
        --value;
        return temp;
    }

    int getValue() const { return value; }
};

注意看区别:

  • 前置版本:先自增,再返回自身。返回的是引用
  • 后置版本:先保存副本,再自增,返回副本。返回的是

为什么会这样?你想想看,++c返回的是自增后的对象,而c++返回的是自增前的对象。后置版本必须保存一份旧值,所以不能返回引用(引用指向的是当前对象,但当前对象已经变了)。

性能建议:我个人习惯在不需要后置语义时,尽量用前置++。尤其是迭代器这种复杂对象,后置++会多一次拷贝,性能差不少。我在项目中优化过一段代码,把后置++改成前置,循环快了将近一倍。

知识结构图

下面这张图帮你理清运算符重载的核心脉络:

运算符重载知识结构 运算符重载 重载规则 不能发明新运算符 优先级/结合性不变 操作数个数不变 部分运算符不可重载 二元运算符 +、-:成员函数 <<、>>:友元函数 返回引用支持链式 const成员保证安全 一元运算符 前置++:返回引用 后置++:返回值 int参数区分前后 前置性能更优 核心原则: 保持语义一致,不要滥用重载 让代码读起来像自然语言

避坑指南

我做了这么多年C++,在运算符重载上栽过不少跟头。分享几个经验:

  • 别滥用:运算符重载的目的是让代码更直观。如果你重载+来做减法,那别人读你代码会疯掉的。
  • 保持语义一致++就是自增,--就是自减。别搞出++obj其实是清空数据这种骚操作。
  • 注意const正确性:不修改对象的运算符一定要加const。我见过一个bug,就是因为operator+没加const,导致const对象无法相加。
  • 友元函数 vs 成员函数:如果运算符的第一个操作数不是你的类对象(比如<<),必须用友元或普通函数。如果第一个操作数是你的类对象,用成员函数更自然。

曾经踩过的坑:我曾经重载了一个类的==运算符,但忘了处理const版本。结果在std::find算法里死活找不到匹配,排查了半天才发现是const对象调不了非constoperator==。从那以后,我所有不修改对象的运算符都加const,成了肌肉记忆。

好了,运算符重载的核心内容就这些。记住规则,掌握常见运算符的写法,再注意一下const和引用的问题,基本就能应付日常开发了。这东西说白了就是个语法糖,用好了代码优雅,用不好就是灾难。