文章

C++重载运算符

重载运算符允许自定义类支持运算符操作,实现语义清晰的对象交互和表达。

C++重载运算符

C++ 重载运算符

在 C++ 中,重载运算符(Operator Overloading)允许为自定义类型(类/结构体)定义运算符的行为,从而使对象像内建类型一样参与运算操作。常用于提升代码可读性和可维护性。

基本语法

运算符重载的函数格式如下:

1
2
3
返回类型 operator运算符(参数列表) {
    // 实现
}

它可以是成员函数,也可以是友元函数普通函数(特别是当左操作数不是当前类对象时)。

常见示例

1. 重载加号 +(成员函数)

1
2
3
4
5
6
7
8
9
10
class Point {
public:
    int x, y;

    Point(int x, int y) : x(x), y(y) {}

    Point operator+(const Point& other) const {
        return Point(x + other.x, y + other.y);
    }
};

2. 重载等于号 ==

1
2
3
bool operator==(const Point& other) const {
    return x == other.x && y == other.y;
}

3. 重载输入输出运算符 <<>>(友元函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

class Point {
public:
    int x, y;

    Point(int x = 0, int y = 0) : x(x), y(y) {}

    // 输出运算符重载
    friend std::ostream& operator<<(std::ostream& os, const Point& p);

    // 输入运算符重载
    friend std::istream& operator>>(std::istream& is, Point& p);
};

// 输出格式: (x, y)
std::ostream& operator<<(std::ostream& os, const Point& p) {
    os << "(" << p.x << ", " << p.y << ")";
    return os;
}

// 输入格式:两个整数,自动赋值给 x 和 y
std::istream& operator>>(std::istream& is, Point& p) {
    if (!(is >> p.x >> p.y)) {
        std::cerr << "[输入错误] 请输入两个整数!" << std::endl;
        // 设置输入流为错误状态
        is.setstate(std::ios::failbit);
    }
    return is;
}

int main() {
    Point p;

    std::cout << "请输入一个点的坐标(格式:x y): ";
    if (std::cin >> p) {
        std::cout << "你输入的点是: " << p << std::endl;
    } else {
        std::cerr << "输入失败,无法解析坐标。" << std::endl;
    }

    return 0;
}
  • <<>>(输入输出运算符)在 C++ 中几乎总是作为非成员函数(通常是友元函数)来重载
  • 运算符重载应该关注数据的“表示”,而不是格式的“配置”。格式应该交给用户来控制。不应该打印换行符。
  • 重载输入运算符应该包含输入合法性判断

4. 重载前置后置的自增运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
using namespace std;

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

    // 前置 ++,返回修改后的对象的引用
    Counter& operator++() {
        ++value;      // 先自增
        return *this;
    }

    // 后置 ++,参数为 int,区分前置和后置
    Counter operator++(int) {
        Counter temp = *this; // 记录自增前的值
        ++value;              // 自增
        return temp;          // 返回自增前的值(值拷贝)
    }

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

int main() {
    Counter c(5);

    cout << "初始值: " << c.getValue() << "\n";

    ++c;  // 调用前置++
    cout << "前置++后: " << c.getValue() << "\n";

    c++;  // 调用后置++
    cout << "后置++后: " << c.getValue() << "\n";

    return 0;
}
  • 前置++没有参数,返回*this的引用,表示修改自身后返回。
  • 后置++带一个int参数(只是为了区分),返回自增前的旧对象的拷贝。

5. 重载函数调用运算符

最简单的函数调用运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

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

int main() {
    Adder add;
    cout << add(3, 4) << endl;  // 输出 7
    return 0;
}
用于 std::sort 的自定义排序函数对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Descend {
    bool operator()(int a, int b) {
        return a > b;
    }
};

int main() {
    vector<int> nums = {1, 5, 3, 2, 4};
    sort(nums.begin(), nums.end(), Descend());

    for (int n : nums) cout << n << " ";  // 输出:5 4 3 2 1
    return 0;
}
捕获状态的函数对象(比函数指针强)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

class MultiplyBy {
    int factor;
public:
    MultiplyBy(int f) : factor(f) {}
    int operator()(int x) const {
        return x * factor;
    }
};

int main() {
    MultiplyBy mul3(3);
    cout << mul3(10) << endl;  // 输出 30
    return 0;
}
结合 std::function 和 Lambda(现代写法)
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <functional>
using namespace std;

int main() {
    function<int(int, int)> f = [](int a, int b) {
        return a * b;
    };

    cout << f(6, 7) << endl;  // 输出 42
    return 0;
}

可重载和不可重载的运算符

| 运算符类别 | 运算符符号 | 是否可重载 | 建议重载 | 备注 | | —————- | ——————————— | ———————– | ———- | ———————————- | | 算术运算符 | +, -, *, /, % | ✅ 可重载 | 推荐 | 常用且直观 | | 赋值运算符 | =, +=, -=, *=, /=, %= | ✅ 可重载 | 推荐 | = 必须重载 | | 自增自减 | ++, -- | ✅ 可重载 | 推荐 | 前置和后置均可 | | 关系运算符 | ==, !=, <, >, <=, >= | ✅ 可重载 | 推荐 | | | 逻辑运算符 | &&, ` | | , ! | ✅ 可重载 | 一般不建议 | 破坏短路求值,语义易混淆 | | **位运算符** | &, | , ^, ~, «, » | ✅ 可重载 | 一般不建议 | 书上建议避免重载,避免语义混淆 | | **成员访问** | -> | ✅ 可重载 | 视情况 | 常用于智能指针等 | | **下标访问** | [] | ✅ 可重载 | 推荐 | 必须成员函数 | | **函数调用** | () | ✅ 可重载 | 推荐 | 必须成员函数 | | **逗号运算符** | , | ✅ 可重载 | 一般不建议 | 重载后改变求值顺序,易引起代码混淆 | | **间接成员访问** | .* | ❌ 不可重载 | — | C++ 标准禁止重载 | | **条件运算符** | ?: | ❌ 不可重载 | — | | | **作用域解析** | :: | ❌ 不可重载 | — | | | **成员访问** | . | ❌ 不可重载 | — | | | **大小写转换** | sizeof, typeid, alignof` | ❌ 不可重载 | — | |

作为成员或者非成员

运算符通常作为成员函数通常作为非成员函数(友元)
=(赋值)✅ 必须成员
[](下标)✅ 必须成员
()(函数调用)✅ 必须成员
->✅ 必须成员
一元运算符 ++ -- - * &✅ 推荐成员❌(除非左操作数是内置类型)
二元运算符 + - * / %可成员,也可非成员✅ 非成员常用于对称运算
比较运算符 == != < > <= >=❌ 推荐非成员✅ 非成员(通常为友元)
输入输出 << >>❌ 不能成员(左侧不是类)✅ 非成员(通常为友元)
赋值类运算符 += -= *=✅ 推荐成员

判断标准

是否必须是成员函数

有些运算符语法要求必须为成员函数:

  1. operator=(赋值运算符)
1
MyClass& operator=(const MyClass&);
  • 因为赋值表达式 a = b修改对象 a 的内部状态
  • 语言规范要求:赋值运算符左侧必须是对象本身(非 const),并且右侧的参数不能影响左侧对象的构造。
  • 非成员函数没有权限直接访问并修改左侧对象(a)的内部状态。
  1. operator[](下标访问运算符)
1
T& operator[](size_t i);
  • 表达式 a[i] 会被编译器解释为 a.operator[](i)
  • 下标运算符的左侧必须是类对象(即你定义的 a)才能调用这个函数。
  • 如果写成非成员函数,就无法绑定 athis 对象。
  • 如果一个类包含下标运算符,则它通常会定义两个版本。分别是:
1
2
T& operator[](size_t index);           // 非 const 版本,允许修改元素
const T& operator[](size_t index) const; // const 版本,保证只读访问
  1. operator()(函数调用运算符)
1
ReturnType operator()(参数列表);
  • 表达式 a(...) 会被解释为 a.operator()(...)
  • 这意味着调用者 a 是函数对象,必须是类实例本身。
  • 只有成员函数才能将 a 作为 this 指针调用。
是否需要访问私有成员

如果写的运算符需要访问类的私有成员,非成员函数需要加 friend,否则无法访问。

是否左右操作数都对称

两个操作数的类型是否相同,或者是否具有互换性(例如 a + bb + a 是否都有意义)。这个特性会影响把运算符重载写成成员函数,还是非成员函数。

对称操作数:类型相同或意义对等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
public:
    int x, y;
    Point(int x, int y): x(x), y(y) {}

    Point operator+(const Point& other) const {
        return Point(x + other.x, y + other.y);
    }
};

int main() {
    Point a(1, 2), b(3, 4);
    Point c = a + b; // OK:a.operator+(b)
}
  • 左右都是 Point 类型,对称
  • 所以你可以选择成员函数或非成员函数(都可以);
  • 甚至你还可以让用户写 b + a,没有语义冲突。

结论:操作数对称时,两边谁在前都没问题,成员函数/非成员函数都可以

非对称操作数:一边必须是类,一边是内置类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point {
public:
    int x;
    Point(int x): x(x) {}

    // 成员函数:支持 Point + int
    Point operator+(int val) const {
        return Point(x + val);
    }

    // 非成员函数:支持 int + Point
    friend Point operator+(int val, const Point& p) {
        return Point(val + p.x);
    }
};
1
2
3
4
5
int main() {
    Point p(10);
    auto a = p + 5;  // ✅ 成员函数调用
    auto b = 5 + p;  // ✅ 友元函数调用
}
  • p + 5:类对象在左,成员函数可以处理;
  • 5 + p:类对象在右,不能写成 5.operator+(p),所以必须写成非成员函数。

结论:操作数非对称时(如 int + Point),要用 非成员函数 来支持类对象在右侧的情况。

对称运算符的一个实际应用:比较运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
public:
    int age;

    Person(int a): age(a) {}

    // friend 比较函数
    friend bool operator==(const Person& a, const Person& b) {
        return a.age == b.age;
    }

    friend bool operator<(const Person& a, const Person& b) {
        return a.age < b.age;
    }
};

这里的 ==<

  • 两边都是 Person 类型;
  • 所以左右对称,完全可以写成非成员函数;
  • 甚至推荐这样写,因为:
    • 语义自然;
    • 支持隐式类型转换;
    • 可支持 std::sortstd::set 等 STL 要求的比较。
是否左操作数不是自定义类型
1
std::ostream& operator<<(std::ostream&, const MyClass&); // ostream 是左侧,不能写成员函数

所以像 << >> 的输入输出运算符只能是非成员函数

最佳实践建议

  • 成员函数:用于 修改自身或本身是主角的运算符(如 =, +=, [], ())。
  • 非成员函数(通常配合 friend):用于 对称或左操作数不是类对象的运算符(如 +, ==, <<)。
本文由作者按照 CC BY 4.0 进行授权