运算符重载:重载规则、重载+、-、<<、>>、++、--
运算符重载,说白了就是让C++的运算符能「听懂」你自定义的类型。比如你写了个复数类,想让两个复数能直接用+相加,而不是写c1.add(c2)这种啰嗦的调用。嗯,这就是运算符重载的用武之地。
我记得刚学C++那会儿,觉得这东西挺神奇的。后来做项目才发现,用好了代码确实优雅,用不好就是给自己挖坑。今天咱们就聊聊运算符重载的那些事儿。
重载规则:别乱来,有规矩
运算符重载不是你想怎么来就怎么来的。C++定了一套规则,你得遵守。
- 不能发明新运算符:你只能重载已有的运算符,比如
+、-、*这些。别想着自己造个**出来,编译器不认。 - 优先级和结合性不变:重载不会改变运算符的优先级。比如
*永远比+优先,不管你重载成啥样。 - 不能改变操作数个数:一元运算符只能有一个操作数,二元运算符只能有两个。你不能把
+重载成三个参数的。 - 有些运算符不能重载:比如
::、.、.*、?:、sizeof。这些是C++的「禁区」,碰不得。
注意:我曾经见过有人试图重载&&和||,结果短路求值特性丢了,代码跑起来跟预期完全不一样。这两个运算符最好别碰。
重载+和-:让对象能加减
咱们先来个简单的例子——复数类。复数有实部和虚部,两个复数相加就是实部加实部,虚部加虚部。
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 display() const {
std::cout << real << " + " << imag << "i" << std::endl;
}
};
你看,operator+和operator-就是普通的成员函数,只不过名字特殊了点。调用的时候,c1 + c2实际上就是c1.operator+(c2)。
小技巧:我个人习惯把不修改当前对象的运算符(比如+、-)声明为const成员函数。这样既能保证原对象不被意外修改,也能让const对象调用这些运算符。
重载<<和>>:让对象能输入输出
这个有点特殊。<<和>>的左侧是ostream或istream对象,不是你的类对象。所以你不能把它们重载为成员函数——除非你修改标准库的类,但那不可能。
正确的做法是重载为友元函数或普通函数。
class Complex {
// ... 前面的代码不变 ...
// 声明为友元
friend std::ostream& operator<<(std::ostream& os, const Complex& c);
friend std::istream& operator>>(std::istream& is, Complex& c);
};
// 重载<<运算符
std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}
// 重载>>运算符
std::istream& operator>>(std::istream& is, Complex& c) {
std::cout << "请输入实部和虚部: ";
is >> c.real >> c.imag;
return is;
}
这里有个关键点:返回引用。为什么?因为你要支持链式调用,比如cout << c1 << c2。如果返回的是值,那第二次<<就不知道往哪写了。
重点:operator<<的第一个参数必须是std::ostream&,第二个参数是你的类对象(通常用const引用)。返回值必须是std::ostream&。这个模式是固定的,别搞反了。
重载++和--:前置和后置的区别
这个坑我踩过。前置++和后置++怎么区分?C++用了一个小技巧:后置版本加一个int参数,但这个参数不用传值,它只是个标记。
class Counter {
private:
int value;
public:
Counter(int v = 0) : value(v) {}
// 前置++
Counter& operator++() {
++value;
return *this;
}
// 后置++
Counter operator++(int) {
Counter temp = *this;
++value;
return temp;
}
// 前置--
Counter& operator--() {
--value;
return *this;
}
// 后置--
Counter operator--(int) {
Counter temp = *this;
--value;
return temp;
}
int getValue() const { return value; }
};
注意看区别:
- 前置版本:先自增,再返回自身。返回的是引用。
- 后置版本:先保存副本,再自增,返回副本。返回的是值。
为什么会这样?你想想看,++c返回的是自增后的对象,而c++返回的是自增前的对象。后置版本必须保存一份旧值,所以不能返回引用(引用指向的是当前对象,但当前对象已经变了)。
性能建议:我个人习惯在不需要后置语义时,尽量用前置++。尤其是迭代器这种复杂对象,后置++会多一次拷贝,性能差不少。我在项目中优化过一段代码,把后置++改成前置,循环快了将近一倍。
知识结构图
下面这张图帮你理清运算符重载的核心脉络:
避坑指南
我做了这么多年C++,在运算符重载上栽过不少跟头。分享几个经验:
- 别滥用:运算符重载的目的是让代码更直观。如果你重载
+来做减法,那别人读你代码会疯掉的。 - 保持语义一致:
++就是自增,--就是自减。别搞出++obj其实是清空数据这种骚操作。 - 注意const正确性:不修改对象的运算符一定要加
const。我见过一个bug,就是因为operator+没加const,导致const对象无法相加。 - 友元函数 vs 成员函数:如果运算符的第一个操作数不是你的类对象(比如
<<),必须用友元或普通函数。如果第一个操作数是你的类对象,用成员函数更自然。
曾经踩过的坑:我曾经重载了一个类的==运算符,但忘了处理const版本。结果在std::find算法里死活找不到匹配,排查了半天才发现是const对象调不了非const的operator==。从那以后,我所有不修改对象的运算符都加const,成了肌肉记忆。
好了,运算符重载的核心内容就这些。记住规则,掌握常见运算符的写法,再注意一下const和引用的问题,基本就能应付日常开发了。这东西说白了就是个语法糖,用好了代码优雅,用不好就是灾难。