运算符重载是C++区别于许多其他语言的重要特性之一。当我们定义了一个复数类、矩阵类或者分数类时,往往希望这些对象能直接使用+、==、<<等运算符进行运算,而不是调用add()、compare()这样冗长的成员函数。运算符重载正是为了让自定义类型获得和内置类型一致的语法体验而存在的。本文将从基本规则讲起,通过多个实例演示如何正确地重载运算符。

运算符重载的基本规则与语法
运算符重载的本质是函数重载的一种特殊形式。重载一个运算符,实际上就是定义一个名为operator+、operator==这样的特殊函数。编译器在遇到a + b这样的表达式时,会将其转换为对相应函数的调用,比如a.operator+(b)或者operator+(a, b)。
语法上,运算符重载函数的函数名由关键字operator加上运算符符号组成。例如重载加号,函数名就是operator+。需要注意的是,重载不能改变运算符的优先级和结合性,也不能改变运算符操作数的个数。一个二元运算符重载后依然接收两个操作数,你不能凭空给加号增加第三个参数。
此外,C++明确规定了一部分运算符不允许重载,包括成员访问运算符.、作用域运算符::、三目条件运算符?:、sizeof、typeid以及alignof等。同时,只能对已有运算符进行重载,不能发明新的运算符符号。重载后的运算符至少要有一个操作数是自定义类型,这也意味着无法改变int + int的含义。
成员函数实现与友元函数实现的区别
重载运算符有两种主要途径:作为类的成员函数,或者作为友元(普通)函数。成员函数方式将运算符定义在类内部,此时左操作数就是对象本身,函数只需接收右操作数一个参数。友元函数方式则在类外部定义普通函数,需要显式接收两个参数,并通过friend关键字访问类的私有成员。
两种方式在大多数场景下可以互换,但存在一些关键差异。当左操作数不是该类的对象时,比如重载<<用于流输出,左操作数是ostream对象,这种情况下只能使用友元函数或普通函数,不能写成成员函数。反之,赋值运算符=、下标运算符[]、函数调用运算符()和类型转换运算符则必须定义为成员函数。下面的例子用两种方式分别实现复数加法:
#include <iostream>
using namespace std;
class Complex {
public:
double real, imag;
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);
}
// 友元函数方式重载减法
friend Complex operator-(const Complex& a, const Complex& b);
};
Complex operator-(const Complex& a, const Complex& b) {
return Complex(a.real - b.real, a.imag - b.imag);
}
int main() {
Complex c1(3, 4), c2(1, 2);
Complex c3 = c1 + c2; // 调用成员函数 operator+
Complex c4 = c1 - c2; // 调用友元 operator-
cout << c3.real << " + " << c3.imag << "i" << endl;
cout << c4.real << " + " << c4.imag << "i" << endl;
return 0;
}从上面的代码可以看出,成员函数版本的operator+末尾加了const修饰,这表示加法运算不会修改左操作数。养成这个习惯非常重要,否则const对象将无法参与运算,编译时会报错。参数使用const Complex&常量引用而不是值传递,既能避免不必要的拷贝开销,又能接受临时对象作为实参。
常见运算符的重载实例
重载比较运算符与流输出运算符
比较运算符的重载非常常见,比如给自定义的分数类实现==和<。流输出运算符<<的重载几乎是每个类的标配,标准写法是返回ostream的引用,这样可以支持链式调用,即cout << a << b的连续输出。
#include <iostream>
#include <sstream>
using namespace std;
class Fraction {
int num, den; // 分子、分母
public:
Fraction(int n, int d) : num(n), den(d) {}
bool operator==(const Fraction& other) const {
return num * other.den == other.num * den;
}
bool operator<(const Fraction& other) const {
return num * other.den < other.num * den;
}
friend ostream& operator<<(ostream& os, const Fraction& f) {
os << f.num << "/" << f.den;
return os; // 返回引用支持链式调用
}
};
int main() {
Fraction f1(1, 2), f2(2, 4), f3(1, 3);
cout << (f1 == f2 ? "相等" : "不相等") << endl;
cout << (f1 < f3 ? "f1更小" : "f3更小") << endl;
cout << f1 << " 和 " << f3 << endl;
return 0;
}重载自增自减运算符
前置与后置自增的重载是最容易混淆的地方。C++通过在函数参数中添加一个仅作区分用途的int占位参数来区分两者:无参数版本是前置自增,带int参数的是后置自增。前置版本返回自增后的对象引用,后置版本则需要先保存旧值再自增,最后按值返回旧对象。
class Counter {
int value;
public:
Counter(int v = 0) : value(v) {}
// 前置自增:先加1,再返回自身
Counter& operator++() {
++value;
return *this;
}
// 后置自增:int是占位参数,先返回旧值再加1
Counter operator++(int) {
Counter temp = *this;
++value;
return temp;
}
int get() const { return value; }
};可以看到后置版本会产生一次对象拷贝,因此在性能敏感的循环中优先使用前置自增是更好的习惯。这也解释了为什么在遍历STL容器时,经验丰富的开发者总是写++it而不是it++。
重载时的常见坑点与最佳实践
第一个常见的坑是返回值类型的选择。算术运算符如+、-应该返回新对象(按值返回),而复合赋值运算符如+=、赋值运算符=则应该返回对象的引用,这是为了支持(a += b) += c这样的连续操作,同时避免多余的拷贝构造。
第二个坑是赋值运算符的重载必须处理自我赋值。如果a = a这样的操作先释放了自身资源再从对方拷贝,就会出现未定义行为。经典写法是先判断this != &other,或者使用拷贝并交换技术。如果一个类没有显式重载赋值运算符,编译器会自动生成一个逐成员拷贝的默认版本,只有当类管理动态资源时才需要手动重写。
最后要提醒的是,运算符重载应当遵循语义一致性原则:重载+就应该让它表现出类似加法的语义,重载==就应该满足等价关系的数学性质。如果把<<重载成发送网络数据的功能,代码的可读性会大打折扣。C++创始人Bjarne Stroustrup多次强调,运算符重载的目的是让程序更清晰,而不是炫技。掌握好这个度,运算符重载就能成为提升代码表达力的利器。
C++运算符重载运算符重载规则operator重载修改时间:2026-09-06 16:56:40