支持重载操作符是c++的一个特性,先不管好不好用,这起码能让它看起来比其他语言NB很多,但真正了解重载操作符后,就会发现这个特性...就这?本文分两个部分
c++支持重载的操作符有
算术操作符 | +、-、*、/、% |
关系操作符 | ==、!=、>、<、>=、<= |
逻辑操作符 | !、&&、|| |
位操作符 | ~、&、|、^、<<、>> |
赋值操作符 | =、+=、-=、*=、/=、%=、&=、|=、^=、>>=、<<= |
下标操作符 | [] |
函数调用操作符 | () |
成员访问操作符 | -> |
指针操作符 | *(解引用)、&(取地址) |
逗号操作符 | , |
这些操作符按照操作数个数不同分为单目操作符、双目操作符、多目操作符
单目操作符 | !、~、->、*(解引用)、&(取地址) |
双目操作符 | 其他 |
多目操作符 | () |
为什么要按这个分类?别急,继续看...
重载操作符实际上就是重新定义操作符的行为函数,不过这里需要用到一个关键字operator
。大多数操作符有两种重载方式
例如
// 成员函数重载Point类的"+"操作符和"~"操作符 —— 定义在类内部
struct Point {
Point operator+(const Point& oth) const { ... }
Point operator~() const { ... }
};
// 全局函数重载Point类的"+"操作符和"~"操作符 —— 定义在类外部
Point operator+(const Point& self, const Point& oth) { ... }
Point operator~(const Point& self) { ... }
有的程序员不喜欢使用重载操作符,因为它的特性实在难以琢磨。但实际上它只是在定义函数的基础上又一些限制
()
、[]
、->
单目操作符 | 双目操作符 | 多目操作符 | |
全局函数 | 1 | 2 | - |
类成员函数 | 0 | 1 | 0个或多个 |
以上内容,足够正确使用重载操作符。但有些同学会觉得很难理解和正确使用,下面一起理解一下重载操作符的本质,理解本质之后再回头来看就会发现重载操作符原来...就这?
重载操作符本质上是特殊的函数。在c++中,函数具有以下形式
返回值 函数名 (形参列表) { 函数体 }
可以看到,函数由四个部分组成:返回值、函数名、形参列表、函数体。重载操作符本质上也是函数,只是在 函数名、形参列表 两部分具有特殊性,另外 调用方式 也很特殊。
重载操作符的函数名是由operator
关键字和操作符符号组成的,例如 operator+、
operator!
等等。
形参列表的参数数量是固定的,具体见下面的表格
单目操作符 | 双目操作符 | 操作符-特殊 | |
全局函数 | 1 (self) | 2 (self, 任意类型) | - |
类成员函数 | 0 | 1 (任意类型) | 0个或多个 |
一个没用的小知识
大家可能已经发现了,对于同一个操作符而言全局函数重载总是比类成员函数重载多一个参数,这个多出来的参数有两个特点:
- 一定是全局函数的第一个形参。
- 类型一定是重载操作符的目标类型。例如
成员函数重载:
Point operator+(const Point& oth) const { ... }
全局函数重载:
Point operator+(const Point& self, const Point& oth) { ... }
这里隐藏了一个成员函数的秘密:在c++中,成员函数默认第一个参数是this指针,只不过写法上忽略了。大家感兴趣的话可以研究下成员函数的汇编码,其中的奥秘就一目了然了。熟悉python语法的同学应该能很容易理解,python的类成员函数必须把第一个参数写成self,这样才能在函数体内访问成员变量。
操作符的使用和函数调用有直观上的差别,如何使用操作符大家应该都很熟悉,这里就不举例子了。值得一提的是操作符的使用本质上还是函数调用。举个例子,用全局函数重载"+"
操作符
struct Point {
Point(int x, int y) : x(x), y(y) { }
int x;
int y;
};
// 重载Point的“+”操作符
Point operator+(const Point& self, const Point& oth) { return Point(self.x + oth.x, self.y + oth.y); }
int main() {
Point p1(20, 60);
Point p2(2, 5);
Point pAdd = p1 + p2; // 使用Point的“+”操作符
}
上面这段代码的汇编代码如下(为了方便大家能抓住重点,对汇编码做了精简)
Point::Point(int, int) [base object constructor]:
...
ret
operator+(Point const&, Point const&):
...
ret
main:
...
mov rsi, rdx
mov rdi, rax
call operator+(Point const&, Point const&)
...
ret
可以看到,编译器把Point pAdd = p1 + p2;
这句c++代码编译成了call operator+(Point const&, Point const&)
,也就是调用函数operator+(Point const&, Point const&)
。
看到这里,大家脑子里会不会闪过一个大胆的想法——在c++代码中直接调用函数operator+(Point const&, Point const&)
会怎么样?就像...
...
Point pAdd = operator+(p1, p2); // Point pAdd = p1 + p2;
...
然后我们会发现代码竟然可以 正!常!运!行!而且对应的汇编代码也一!模!一!样!所以,操作符也可以通过函数掉调用的方式使用。
前面研究的是全局函数重载的行为,那么成员函数重载呢?我们一起来看看
struct Point {
Point(int x, int y) : x(x), y(y) {}
// 重载Point的“+”操作符
Point operator+(const Point& oth) const { return Point(x + oth.x, y + oth.y); }
int x;
int y;
};
int main() {
Point p1(20, 60);
Point p2(2, 5);
Point pAdd = p1 + p2; // 使用Point的“+”操作符
}
对应的汇编代码
Point::Point(int, int) [base object constructor]:
...
ret
Point::operator+(Point const&) const:
...
ret
main:
...
mov rsi, rdx
mov rdi, rax
call Point::operator+(Point const&) const
...
ret
编译器把Point pAdd = p1 + p2;
这句c++代码编译成了call Point::operator+(Point const&) const
。不难发现,全局函数重载和成员函数重载对应的汇编代码的operator+
符号不一样:
Point
。成员函数重载操作符也可以像函数调用一样使用,只不过需要遵守成员函数的调用规则
...
Point pAdd = p1.operator+(p2); // Point pAdd = p1 + p2;
...
下面做个简单对比
全局函数重载 | 类成员函数重载 | |
c++代码 | | |
汇编代码 |
|
|
函数式调用 |
|
|
以上就是重载操作符相对于普通函数的特殊所在,除了这几个特殊点其他方面没有任何不同。在使用的时候我们可以对返回值、形参类型、函数体为!所!欲!为!
struct Cat { ... };
struct Dog { ... };
Dog operator+(const Cat& self, const Cat& oth) {
...
return Dog();
}
一只猫加另一只猫,得到一条狗?