在C++里,内置类型支持加减乘除等运算,但自己写的类默认不具备这些能力。编译器遇到两个对象用加号相连时,并不知道该怎样合并它们。operator关键字正是用来把运算符语法翻译成普通函数调用的桥梁,让自定义类型也能拥有直观的数学或逻辑语义。

运算符重载的基本规则与限制
C++规定运算符重载本质就是函数定义,函数名必须以operator后接运算符符号组成,例如operator+、operator==。它可以是类的成员函数,也可以是类外的普通函数或友元函数。作为成员时,左侧运算对象自动绑定到this指针,因此一元运算符没有显式参数,二元运算符只写一个右操作数参数。
并非所有运算符都能重载。作用域解析符::、成员访问符.、成员指针符.*、三元条件符?:以及sizeof是明确禁止重载的。其余大部分运算符允许重载,但优先级和结合性始终保持与原生一致,不能通过改变参数个数来创造新运算符。重载后应保证行为符合直觉,例如重载加号就不要偷偷做减法,否则会严重破坏代码可读性。
另一个容易忽略的点是,重载运算符至少涉及一个用户自定义类型。你不能写两个int相加的重载来替换编译器已有逻辑,这既不被允许也没必要。同时,如果以成员函数形式提供,要注意隐式类型转换只发生在右侧参数,左侧必须是该类类型本身,否则需要借助全局友元函数来获得双向转换能力。
成员函数与友元函数的取舍
当运算符对称性很重要时,比如两个不同类型对象相加,成员函数会导致左操作数受限。假设有String类和const char*字面量,若只用成员operator+,只能写str + "abc"而不能写"abc" + str,因为字面量不是类对象无法触发成员调用。此时把运算符定义为类外友元函数,左右参数都显式声明,就能同时支持两种顺序。
友元函数虽能打破封装,但应仅在对私有成员有必要时使用。如果只读接口已足够,优先用非友元全局函数调用公有方法实现。下面示例展示复数类用成员函数重载加号,以及用友元函数重载输出流,既保证内聚又兼顾使用便利。
#include <iostream>
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 std::ostream& operator<<(std::ostream& os, const Complex& c);
};
std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}
int main() {
Complex a(1.0, 2.0);
Complex b(3.0, 4.0);
Complex c = a + b;
std::cout << c << std::endl;
return 0;
}
上述代码中,operator+返回新对象而非引用,因为局部结果在函数结束后不应悬空。输出流重载返回ostream&引用,以支持链式输出。这种返回类型的选择是运算符重载里非常关键的设计,错误返回临时对象的引用会直接导致未定义行为。
赋值与复合运算符的重载细节
赋值运算符operator=比较特殊,若类未显式定义,编译器会生成浅拷贝版本。当类管理资源如动态内存时,必须自写深拷贝并防范自赋值。典型写法先判断this != &rhs,再释放旧资源、分配新资源、拷贝内容,最后返回*this引用。复合运算符如operator+=通常先修改自身再返回引用,而operator+可借助+=实现,避免逻辑重复。
下面给出一个简化字符串类片段,演示赋值与加等如何协作,并说明自赋值检查为何不能省。若省略检查,释放自身内存后立刻访问已失效指针便会崩溃。
#include <cstring>
#include <iostream>
class MyString {
char* buf;
public:
MyString(const char* s = "") {
buf = new char[strlen(s) + 1];
strcpy(buf, s);
}
MyString& operator=(const MyString& rhs) {
if (this != &rhs) {
delete[] buf;
buf = new char[strlen(rhs.buf) + 1];
strcpy(buf, rhs.buf);
}
return *this;
}
MyString& operator+=(const MyString& rhs) {
char* tmp = new char[strlen(buf) + strlen(rhs.buf) + 1];
strcpy(tmp, buf);
strcat(tmp, rhs.buf);
delete[] buf;
buf = tmp;
return *this;
}
friend std::ostream& operator<<(std::ostream& os, const MyString& s);
};
std::ostream& operator<<(std::ostream& os, const MyString& s) {
os << s.buf;
return os;
}
从示例可以看出,良好的运算符重载应让接口符合内建类型的习惯:赋值返回左值引用、加等返回自身、加号产生新值。这样做能使用户代码保持自然,如a = b = c或a += b += c都能按预期工作。此外,标记const正确性是防止意外修改的关键,只读运算应统一加const修饰函数和参数。
常见陷阱与性能考量
过度重载会让代码难以调试,因为一行简单表达式可能暗中调用多个函数。尤其是转换运算符如operator int()容易引发歧义,编译器在多种重载间选择时可能做出非预期转换。建议对单参数构造函数加explicit,对危险转换运算符谨慎提供,或改用命名函数替代隐式转换。
在性能敏感场景,运算符返回大对象会带来拷贝开销。C++11后可通过移动语义优化,例如operator+返回右值引用或使用移动构造。同时,若类较小如复数,直接按值返回已被编译器优化得很好,不必过早用手写引用扭曲接口。权衡可读性与效率,才是运算符重载真正成熟的标志。
operator_overloadingC++class_operator修改时间:2026-08-18 01:02:33