3. 重载关系运算符:实现自定义类型的比较逻辑

关系运算符,说白了就是 ==!=<> 这些家伙。在 C++ 里,基本类型可以直接比大小、判相等。但换成自定义类型,比如你写了个 Student 类,编译器就懵了——它不知道两个学生怎么比。

这时候就需要我们亲自告诉编译器:“按这个规则来比较”。这就是关系运算符重载的用武之地。

为什么需要重载关系运算符?

我刚开始学 C++ 时,觉得这东西可有可无。直到有一次写一个成绩管理系统,需要对学生按总分排序。我直接用 if (stu1 > stu2),结果编译器报错。嗯,那时候我才意识到:自定义类型没有默认的比较行为

重载关系运算符的好处很明显:

  • 代码更自然,像操作基本类型一样操作自定义类型
  • 可以配合 STL 容器和算法(比如 std::sortstd::find
  • 让代码的语义更清晰,别人一看就懂
核心原则:重载后的运算符应该符合直觉。比如 == 表示“相等”,< 表示“小于”,不要搞反逻辑。

基本语法:如何重载关系运算符?

关系运算符可以重载为成员函数非成员函数。我个人习惯用非成员函数(友元形式),因为这样左右操作数都能隐式转换,更灵活。

先看一个最简单的例子:

class Point {
public:
    Point(int x, int y) : m_x(x), m_y(y) {}

    // 声明为友元,方便非成员函数访问私有成员
    friend bool operator==(const Point& lhs, const Point& rhs);
    friend bool operator<(const Point& lhs, const Point& rhs);

private:
    int m_x, m_y;
};

// 实现 ==
bool operator==(const Point& lhs, const Point& rhs) {
    return lhs.m_x == rhs.m_x && lhs.m_y == rhs.m_y;
}

// 实现 <
bool operator<(const Point& lhs, const Point& rhs) {
    if (lhs.m_x != rhs.m_x)
        return lhs.m_x < rhs.m_x;
    return lhs.m_y < rhs.m_y;
}

你看,== 比较两个坐标是否完全相等,< 先比 x 再比 y——这叫字典序比较,很常用。

如何实现 !=、>、<=、>=?

这里有个小技巧:不要每个运算符都从头写。你可以利用已经实现的运算符来推导其他的。

比如:

  • a != b 等价于 !(a == b)
  • a > b 等价于 b < a
  • a <= b 等价于 !(b < a)
  • a >= b 等价于 !(a < b)

代码实现起来就很简单:

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

bool operator>(const Point& lhs, const Point& rhs) {
    return rhs < lhs;
}

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

bool operator>=(const Point& lhs, const Point& rhs) {
    return !(lhs < rhs);
}
我的习惯:只实现 ==<,其他四个用它们推导出来。这样既减少代码量,也避免逻辑不一致。

一个完整的例子:学生成绩比较

我在项目中遇到过这样一个场景:需要对学生按总分排名,总分相同则按语文成绩排。来看看怎么实现:

#include <iostream>
#include <vector>
#include <algorithm>

class Student {
public:
    Student(const std::string& name, int chinese, int math, int english)
        : m_name(name), m_chinese(chinese), m_math(math), m_english(english) {}

    int totalScore() const {
        return m_chinese + m_math + m_english;
    }

    friend bool operator==(const Student& lhs, const Student& rhs);
    friend bool operator<(const Student& lhs, const Student& rhs);
    friend std::ostream& operator<<(std::ostream& os, const Student& s);

private:
    std::string m_name;
    int m_chinese, m_math, m_english;
};

bool operator==(const Student& lhs, const Student& rhs) {
    return lhs.totalScore() == rhs.totalScore()
        && lhs.m_chinese == rhs.m_chinese
        && lhs.m_math == rhs.m_math
        && lhs.m_english == rhs.m_english;
}

bool operator<(const Student& lhs, const Student& rhs) {
    // 先按总分排,总分相同按语文排
    if (lhs.totalScore() != rhs.totalScore())
        return lhs.totalScore() < rhs.totalScore();
    return lhs.m_chinese < rhs.m_chinese;
}

std::ostream& operator<<(std::ostream& os, const Student& s) {
    os << s.m_name << " 总分:" << s.totalScore()
       << " 语文:" << s.m_chinese
       << " 数学:" << s.m_math
       << " 英语:" << s.m_english;
    return os;
}

int main() {
    std::vector<Student> students = {
        {"张三", 90, 85, 88},
        {"李四", 92, 80, 86},
        {"王五", 90, 85, 88}
    };

    // 排序(使用重载的 <)
    std::sort(students.begin(), students.end());

    // 查找(使用重载的 ==)
    Student target("张三", 90, 85, 88);
    auto it = std::find(students.begin(), students.end(), target);
    if (it != students.end()) {
        std::cout << "找到: " << *it << std::endl;
    }

    return 0;
}
注意:重载 < 时一定要保证严格弱序(strict weak ordering)。简单说就是:不能出现 a < b 且 b < a 同时为 true 的情况。否则 std::sort 会崩溃。

关系运算符重载的常见陷阱

我曾经踩过一个坑:重载 == 时只比较了部分成员变量,结果两个“相等”的对象其实内部数据不同。调试了半天才发现问题。

这里总结几个要点:

  • 一致性==!= 必须互斥,不能同时为 true 或 false
  • 对称性a == bb == a 结果必须一致
  • 传递性:如果 a < b 且 b < c,那么 a < c 必须成立
  • 不要重载 && 和 ||:这两个运算符有短路求值特性,重载后会丢失这个特性,容易出 bug

知识体系图

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

关系运算符重载 重载方式 成员函数 / 非成员函数(友元) 六个关系运算符 == != < > <= >= 核心原则 一致性 / 对称性 / 传递性 实际应用场景 STL排序(std::sort) / 查找(std::find) / 容器比较 / 自定义排序规则 实现技巧:只实现 == 和 <,其余四个用它们推导

总结

关系运算符重载,说白了就是让自定义类型也能像基本类型一样比较。你只需要记住:

  • 用非成员函数(友元)重载,更灵活
  • 只实现 ==<,其他四个推导出来
  • 保证严格弱序,否则 STL 会出问题
  • 保持语义一致,别搞反逻辑

嗯,掌握了这些,你的自定义类型就能和 STL 无缝配合了。下次写排序、查找代码时,你会觉得特别顺手。