C++中int、double等内置类型可以很自然地写a+b,但换成自定义的类对象如Complex c3=c1+c2时,编译器会直接报错,因为它不知道如何把两个对象加在一起。运算符重载要解决的就是这个问题。它本质上是为运算符提供一个额外的函数版本,实现方式是通过operator关键字紧接着运算符符号来定义。这样表达式c1+c2在编译时会被解释为函数调用c1.operator+(c2)或者operator+(c1,c2),具体取决于声明形式。很多人第一次看到operator+时会觉得奇怪,但只要把它理解成一个名字比较特殊的函数,整个语法就会变得非常直观。

一、运算符重载的基础规则
C++允许重载绝大多数运算符,包括算术运算符、关系运算符、赋值运算符、下标运算符、函数调用运算符等。但有几个不能重载:域解析运算符::、成员选择运算符.、成员指针选择运算符.*、条件运算符?:以及sizeof。重载不能创建新的运算符,也不能改变运算符的优先级、结合性和操作数个数。例如加号永远是二元运算符,优先级不会因为重载而改变。
运算符重载的特殊之处在于,它通过operator关键字后接运算符符号构成函数名。以加号为例,函数名的写法是operator+。如果希望通过运算符重载实现两个类对象相加,就需要为这个类定义一个名为operator+的函数,参数和返回值由运算语义决定。重载函数本质上还是函数,因此适用函数重载规则,只是调用方式从常规的函数名加括号变成了运算符表达式。
还有一个限制:重载运算符的操作数中至少有一个必须是用户定义类型。也就是说不能为纯内置类型重新定义加号,比如不能把int加int再改成其他行为。这一限制保护了语言的基本语义,避免代码变得不可预测。理解了这些规则之后,再去看加减法重载的具体写法,就会清晰很多。
二、成员函数形式与友元函数形式
同一个运算符可以以成员函数或友元函数两种方式重载。成员函数形式只需要显式声明右操作数,左操作数隐式通过this指针传入。因此对于二元运算符,成员函数只接收一个参数。假设有一个Complex类,成员形式的加号写法为Complex operator+(const Complex& other) const,使用a+b时实际调用a.operator+(b)。
友元函数形式则完全不同,它不属于类成员,需要在类内部声明为friend,然后在类外定义。友元函数必须显式接收两个操作数,所以加号的友元形式为Complex operator+(const Complex& left, const Complex& right)。使用a+b时实际调用operator+(a,b)。友元形式的主要优势是可以处理左操作数不是本类对象的场景,例如3+c这种表达式,成员函数无法处理,因为左操作数int无法调用Complex的成员,而友元函数可以通过重载匹配。
对于加减法这种对称性较强的运算符,通常两种形式都可以,但需要避免同时定义导致二义性。如果成员函数和友元函数都能匹配同一个表达式,编译器会报错。一般来说,推荐优先使用成员函数重载算术运算符,只有当左操作数可能不是本类对象或需要访问两个不同类的私有成员时,才考虑友元函数。
下面的代码展示了成员函数和友元函数的声明差异:
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;
// 友元函数形式
friend Complex operator-(const Complex& left, const Complex& right);
};
示例中加号采用成员函数形式,减号采用友元函数形式,这可以突出两种写法的区别。实际项目中同一类内最好统一风格,减少阅读成本。
三、加减法运算符重载的完整实现
接下来用一个复数类完整演示加减法重载。复数由实部和虚部组成,相加时实部与实部相加,虚部与虚部相加;相减同理。为了让结果可以连续参与运算,operator+和operator-应当返回新对象,而不是返回引用。返回新对象虽然会带来临时对象拷贝,但保证了语义正确。若返回引用,很可能指向函数内部创建的局部对象,函数结束后对象已销毁,产生悬空引用。
#include <iostream>
using namespace std;
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 print() const {
cout << real << (imag >= 0 ? " + " : " - ")
<< (imag >= 0 ? imag : -imag) << "i" << endl;
}
};
int main() {
Complex a(3, 4);
Complex b(1, -2);
Complex sum = a + b; // 调用a.operator+(b)
Complex diff = a - b; // 调用a.operator-(b)
sum.print();
diff.print();
Complex chain = a + b + Complex(2, 2); // 连续运算
chain.print();
return 0;
}
上面代码中,operator+和operator-都被声明为const成员函数,因为运算过程不会修改左操作数。参数也使用const引用,避免不必要的拷贝。返回值是Complex值对象,这样a+b会产生一个临时Complex对象,再与下一个操作数继续运算,形成自然的链式表达式。编译器在开启优化后通常会应用返回值优化或移动语义,减少额外的拷贝成本。
连续运算a+b+c的执行顺序从左到右,先计算a+b生成临时对象,再执行临时对象.operator+(c)。这一步临时对象是右值,但由于operator+是const成员函数,可以在右值上调用,因此不会出错。如果operator+没有被const修饰,某些老标准或特定场景下右值调用可能失败,所以给不修改左操作数的运算符加上const是一个必要习惯。
四、重载加减法时的常见错误与细节
最容易出现的错误是返回局部对象的引用。例如写成Complex& operator+(const Complex& other) const,并在函数内部创建Complex result并返回result,编译可能通过但运行结果错误,因为result的内存随函数结束被释放。正确做法是返回Complex值类型,让编译器负责管理临时对象的生命周期。
另一个常见问题是忘写const。如果operator+不声明为const,那么遇到const Complex c1;再写c1+c2就会编译失败,因为const对象只能调用const成员函数。给不修改状态的运算符重载加上const修饰既符合语义,也能扩大可用范围。同时参数建议加const引用,既可以接收非const对象,也可以接收const对象,还能避免一次不必要的拷贝。
还需要注意运算符重载不应过度使用。加减法重载应该符合直觉,复数相加的语义就是分量相加,不要在其中加入其他副作用。某些开发者可能想在operator+里修改操作数、输出日志或更新全局状态,这会破坏代码的可预测性。运算符重载应当保持简单、纯粹,复杂的业务逻辑应该放入普通命名函数。
最后,如果类管理动态资源,例如字符串类或矩阵类,加减法会产生多个临时对象。现代C++可以结合移动构造函数和移动赋值运算符来降低开销。编译器在返回局部对象时倾向于使用移动而不是拷贝,因此实现高效的移动语义比单纯依赖返回值优化更可靠。对于简单的值语义类,直接返回局部对象并信任编译器优化是最清晰的写法。
C++运算符重载operator关键字加减法实现修改时间:2026-09-26 04:30:10