简单整理一下运算符重载的相关知识
基本概念
重载运算符是一种特殊的函数,使得类可以依靠运算符进行特点的操作。它的定义形式如下:
1 |
|
- 参数的数量与该运算符作用的运算对象数量一样多
- 若运算符函数是成员函数,则它左侧(第一个)运算对象绑定到this指针上,故它的(显式)参数数量要少一个
- 运算符函数要么是类成员,要么含有一个类类型的参数
- 重载不改变运算符的优先级和结合律,也不能改变所需的操作数
有五个运算符不能被重载:::
,.*
,。
,?:
、sizeof
。而有两个不应该被重载:,
,&
(取地址),&&
(逻辑与),||
(逻辑或),因为它们有特殊含义。(注:new
和delete
是可以被重载的!)
大部分重载函数可以是类的成员函数,也可以是友元函数。只有小部分只能是成员函数:=
、()
、[]
、->
。
成员or非成员
对于声明为成员函数还是友元函数,应遵循以下原则:
- 赋值
=
、下标[]
、调用()
、成员访问箭头->
运算符必须是成员函数 - 具有对称性的运算符应该是非成员(比如加减乘除、相等性、关系、位运算符)
- 改变对象状态的运算符应该是成员(比如递增、递减、解引用)
具体的运算符
输入输出运算符(«、»)
考虑<<
的使用方法:cout<<类类型
,所以第一个对象是ostream
,第二个对象是我们的类,故我们声明为非成员函数,并且声明为类的友元:
1 |
|
由于ostream
无法被复制,所以它的形参和返回值都是引用;而由于我们一般不改变类的数据,所以类用const
修饰。
考虑>>
的用法:cin>>类类型
,所以第一个对象为istream
,第二个对象是我们的类,故和<<
的重载方法差不多:
1 |
|
同样,istream
不能被复制,故形参和返回值也都是引用;而我们需要改变原类类型的值,故类的形参是引用。特别的,我们在定义输入重载函数时,需要考虑输入失败的情况,并要从失败中恢复,并将流状态设置为failbit
(见《C++ Pimer》496页)
算术和关系运算符(+、-、==、!=、<、>等)
算术运算符有三个特点:
- 左右对象可交换(故一般定义为友元函数)
- 不改变操作对象的值(故形参为常量引用)
- 返回一个临时的类类型(故返回局部变量的副本)
以加法为例:
1 |
|
《C++ Primer》 497页:类如果同时定义了算术运算符和复合赋值运算符,通常情况下应用复合赋值来实现算术运算符。
关系运算符也大同小异,唯一不同是返回值是bool值。并且要遵循如下设计准则:
- 相等运算符
==
应具有传递性 - 定义了
==
,则相应的要定义!=
;同理,定义了<
,相应的要定义>
递增和递减运算符(++、–)
虽然没必要,但还是建议将递增和递减运算符定义为成员函数。因为它们只改变所操作的对象。
前置版本(++a)
应该返回递增或递减后的对象的引用,这样才能作为左值使用。
1 |
|
后置版本(a++)
应该返回递增或递减前的对象的原值,而非引用。
1 |
|
需要注意的是,我们的参数中有一个int
,但我们并不会用到它,所以无需为其命名,编译器会为它提供一个值为0的实参。也就等价于:
1 |
|
赋值运算符(=)
之前本来是要讲拷贝赋值的,不过基础部分还是不要太深入了,其实和复制构造函数是一样的,也就是编译器会为我们自动重载赋值运算符,但那只是浅复制。要实现深复制,则需要手动对赋值运算符进行重载。要求如下:
- 必须是成员函数
- 返回左侧运算对象的引用(
return *this
)
例子和深复制那部分差不多,就不给了。
下标运算符( [] )
貌似最近写程序都没用过这个,就大概讲一下要求:
- 必须是成员函数
- 返回的是对应元素的引用(这样可以作为左值或右值)
- 最好定义多一个常量版本(函数后面加const),并且返回常量引用
成员访问运算符(*)
(待定)
函数调用运算符( ( ) )
(待定)
类类型转换
假如我们需要将类和一个整数相加,则编译器会利用构造函数,将整数转化为类,再相加。那么有没有办法能让类转化为整数呢?这就是类类型转换。
类型转换运算符类似于一般的运算符重载,它的声明格式如下:
1 |
|
它有几个特点:
- 类类型转换函数必须是成员函数;
- 不能声明返回类型(返回类型只能为type);
- 不能有形参;
- 通常应该是const
类类型转换一般是隐式执行的,因此无法给这些函数传递实参(强制执行的话可以用int([类类型])
,或者[类型].operator type()
)。
注意
首先,不是所有类都需要类类型转换,比如某个类表示Date,某一天Aug 13, 2000既可以解释为20000813,也可以解释为从Jan 1, 2000经过的天数。在这种情况下,应该定义两个成员函数负责返回不同的格式。
其次,要避免二义性。比如:类A定义了接受类B的转换构造函数,而类B定义了转换到类A的函数。这种情况下,我们只能显式的使用对应的函数,否则编译器无法确定要用哪个。并且我们不能使用强制类型转换,因为这种情况下,强制类型转换也面临二义性。
关于如何避免这些情况,见
显式的类类型转换
为了防止某些情况下,隐式的类类型转换造成错误,可以在类类型转换函数声明前加explicit
:
1 |
|
此时,如果想要将A转化为int,需要显式的强制转换:
1 |
|
但是,在下列位置,显式的类类型转换会被隐式执行:
- if、while及do语句的条件部分
- for语句头的条件表达式
- 逻辑运算符的运算对象
- 条件运算符(?:)的条件表达式
实际上,cin有一个向bool转换的显式的类型转换函数,所以我们可以在条件表达式中用cin,并且在其他位置用cin不会出错。
题外话
顺便说一句:NASA有一个把名字刻在火星漫游者芯片上 的活动,也就是说,我的名字要上火星咯~