6. 运算符重载:让自定义类型像内置类型一样自然
运算符重载,说白了就是让咱们自己写的类也能用上 +、-、[] 这些运算符。我刚开始学的时候觉得这玩意儿挺花哨的,后来在项目里写了一个矩阵类,要是没有运算符重载,你想想看,每次做加法都得写 matrix.add(other),那代码得多难看。
嗯,这里要注意:运算符重载不是创造新的运算符,只是给已有的运算符赋予新的含义。而且,它必须作用于自定义类型上——你不能去改变 int 的 + 行为。
6.1 运算符重载的规则与语法
先说说规则。C++ 对运算符重载有一些硬性约束,我列几个关键的:
- 不能改变优先级和结合性——你重载了
+,它还是比*低,别想改。 - 不能改变操作数个数——一元运算符只能有一个参数,二元运算符只能有两个。
- 不能发明新运算符——比如你不能造一个
**出来。 - 不能重载的运算符:
::、.、.*、?:、sizeof、typeid。
语法其实就两种形式:
// 作为成员函数
class Complex {
public:
Complex operator+(const Complex& rhs) const;
};
// 作为非成员函数(通常是友元)
Complex operator+(const Complex& lhs, const Complex& rhs);
我个人习惯是:一元运算符尽量做成成员函数,二元运算符尽量做成非成员函数。为什么?因为非成员函数能支持隐式类型转换——比如 3 + obj,如果 + 是成员函数,左边必须是你的类型,那就写不了。
+ 重载成减法,那样写代码的人会骂你的。
6.2 一元运算符重载
一元运算符包括 ++、--、!、~、-(负号)等。这里有个坑:++ 和 -- 有前置和后置之分,怎么区分?
C++ 用了一个小技巧——后置版本加一个 int 占位参数:
class Counter {
int value_;
public:
// 前置 ++
Counter& operator++() {
++value_;
return *this;
}
// 后置 ++(注意那个 int)
Counter operator++(int) {
Counter tmp = *this;
++value_;
return tmp;
}
};
我在项目中遇到过有人把后置 ++ 返回了引用,结果出了诡异的 bug。记住:后置返回的是旧值,必须返回临时对象,不能返回引用。
++ 效率更高,因为它不需要拷贝。迭代器遍历时,我习惯写 ++it 而不是 it++。
6.3 二元运算符重载
二元运算符是最常用的,比如 +、-、*、/、==、< 等。我建议把对称的运算符(比如 +、==)写成非成员函数,把复合赋值运算符(比如 +=)写成成员函数。
class Vector2D {
double x_, y_;
public:
Vector2D(double x, double y) : x_(x), y_(y) {}
// 复合赋值:成员函数
Vector2D& operator+=(const Vector2D& rhs) {
x_ += rhs.x_;
y_ += rhs.y_;
return *this;
}
};
// 普通加法:非成员函数
Vector2D operator+(const Vector2D& lhs, const Vector2D& rhs) {
Vector2D result = lhs;
result += rhs;
return result;
}
你看,+ 的实现复用了 +=,这样代码更干净。我一般都会让 + 调用 +=,- 调用 -=,以此类推。
== 和 != 最好成对出现。我曾经只重载了 ==,结果别人用 != 时编译器报错,那场面挺尴尬的。
6.4 输入输出运算符重载
输入输出运算符 << 和 >> 比较特殊——它们必须是非成员函数,因为左操作数是 ostream 或 istream,不是你的类。
class Point {
int x_, y_;
public:
friend ostream& operator<<(ostream& os, const Point& p);
friend istream& operator>>(istream& is, Point& p);
};
ostream& operator<<(ostream& os, const Point& p) {
os << "(" << p.x_ << ", " << p.y_ << ")";
return os;
}
istream& operator>>(istream& is, Point& p) {
is >> p.x_ >> p.y_;
return is;
}
这里有个细节:operator>> 的第二个参数不能是 const,因为你要修改它。另外,记得返回 ostream& 或 istream&,这样才能链式调用——比如 cout << a << b。
(1, 2) 比 1 2 清晰多了。
6.5 类型转换运算符
类型转换运算符让你能控制自定义类型如何转换成其他类型。语法有点特别:
class Rational {
int num_, den_;
public:
// 转换成 double
operator double() const {
return static_cast<double>(num_) / den_;
}
// 转换成 bool(C++11 起可以用 explicit)
explicit operator bool() const {
return num_ != 0;
}
};
嗯,这里要注意:类型转换运算符没有返回类型,因为返回类型就是你要转换的目标类型。而且它不能有参数。
我个人强烈建议:能用 explicit 就用 explicit。为什么?因为隐式转换经常带来意想不到的 bug。比如你写了个 if (r),如果 operator bool() 不是 explicit,那 r + 1 这种表达式也可能莫名其妙地编译通过——因为 r 先转成了 bool,再转成 int,最后加 1。这逻辑完全不对。
operator int(),结果不小心把对象传给了需要 int 参数的函数,编译器一声不吭就转了。后来查了半天才发现是隐式转换搞的鬼。从那以后,所有单参数构造函数和类型转换运算符我都加 explicit。
知识体系总览
下面这张图把运算符重载的核心脉络梳理了一下,你可以对照着看:
这张图把五个核心知识点串起来了。你从上往下看:先搞清楚规则,再分别掌握一元、二元、IO 和类型转换。每个分支都有对应的实现建议,照着做基本不会踩坑。
运算符重载用好了,代码读起来就像在操作内置类型一样自然。但用不好——嗯,我见过有人把 + 重载成字符串拼接,把 - 重载成删除字符,结果代码维护起来简直噩梦。所以记住一句话:重载运算符,先想语义,再想语法。
公众号:蓝海资料掘金营,微信deep3321