C++ primer 读书笔记 chapter 14 重载运算与类型转换
本文最后更新于:2021年11月2日 上午
C++ Primer - 重载运算与类型转换
基本概念
重载的运算符是具有特殊名字的函数(关键字operator接要定义的运算符号)。
既然是函数,所以也有返回类型、参数列表和函数体。其中参数数量与该运算符作用的运算对象一样多。一元运算符有一个参数,二元运算符有两个(唯一的一个三元运算符不能重载)。对二元运算符来说,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数。除了重载的函数调用运算符operator()
之外,其他重载运算符不能含有默认实参。
有的运算符既可以当一元也可以当二元,这个时候要根据重载参数个数来判断语义。
如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此,成员运算符函数(显式)的参数数量比运算符的运算对象总是少一个。
运算符函数要么是类的成员,要么至少含有一个类类型参数,这就意味着无法对内置类型的运算对象进行运算符重载。
比如
int operator+(int, int);
就是错误的语法,不能改变内置类型的运算符行为。
只能重载已有的运算符,不能搞新发明。不是所有的运算符都能重载。
可重载运算符 | |||||||
---|---|---|---|---|---|---|---|
+ | - | * | / | % | ^ | ||
& | \ | ~ | ! | , | = | ||
< | > | <= | >= | ++ | – | ||
<< | >> | == | != | && | \ | \ | |
+= | -= | /= | %= | ^= | &= | ||
\ | = | *= | <<= | >>= | [] | () | |
-> | ->* | new | new[] | delete | delete[] | ||
不能重载运算符 | |||||||
:: | .* | . | ? : |
运算符函数一般通过间接调用,当然也可以直接调用。
1 |
|
尽管有些运算符可以重载,但大多数情况下不建议重载,它们是, & || &&
。
运算符重载虽然可以为所欲为,但最好让他们的含义与内置类型一致,不要违直觉定义。
- 如果类执行IO操作,那么<<和>>就应该与内置类型的IO一致。
- 如果类的某个操作是检查相等性,则定义operator==,通常也应该有operator!=。
- 如果类包含一个内在的单序比较操作,则定义operator<,如果有了<,一般也有其他关系操作。
- 重载运算符的返回类型通常应与内置版本的返回类型兼容:逻辑运算符和关系运算符应该返回bool,算术运算符应该返回类类型的值,赋值运算符和复合赋值运算符应该返回左侧运算对象的一个引用。
定义重载运算符时,必须要先决定是声明为类成员函数还是普通的非成员函数。对此,有一些准则:
- 赋值、下标、调用和成员访问箭头运算符必须是成员。
- 复合赋值运算符一般来说应该是成员,但并非必须。
- 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,应该是成员。
- 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,它们应该是普通的非成员函数(成员函数则会引发
string u = "hi" + s;
错误的灾难)。 - 输入输出运算符必须是非成员函数。
是否成员函数 | 运算符类型 |
---|---|
必须是成员函数 | 赋值运算、下标运算、调用、箭头运算符 |
应该是成员函数 | 复合赋值、递增递减、解引用 |
应该非成员函数(应该友元函数) | 算术运算符、关系运算符、位运算 |
必须非成员函数(必须友元函数) | 输入输出 |
输入和输出运算符
重载<<
通常输出运算符的第一个形参是一个非常量ostream对象引用。非常量是因为向流写入内容会改变其状态,而引用则是因为ostream不能拷贝。第二个形参一般是一个常量的引用,该常量是我们想要打印的类类型。这里的引用是因为我们希望避免复制实参,常量则是因为打印对象通常不会改变对象内容。
为了与其他<<一致,operator<<一般返回它的ostream形参。
1 |
|
IO运算符往往需要读写类的非公有数据成员,所以IO运算符一般被声明为友元。
重载>>
1 |
|
和<<类似,但>>要额外考虑失败的情况。
流含有错误类型的数据读取操作,或是到达文件末尾或遇到输入流的其他错误时会失败。
算术和关系运算符
重载==、!=
1 |
|
重载关系运算符
定义了相等运算符的类一般也包含关系运算符。特别的,关联容器需要用到小于运算符,所以定义operator<很有用。
通常情况下,关系运算符应该:
- 定义顺序关系,令其与关联容器中对关键字的要求一致,并且
- 如果类同时含有
==
运算符的话,则定义一种关系令其与==
保持一致。特别是,如果两个对象是!=
的,那么一个对象应该<
另一个。
对Sales_data类来说,关系运算符没有什么必要,因为语义上违直觉。
赋值运算符
除了拷贝赋值和移动赋值运算符以外,类还可以定义其他赋值运算符,把别的类型作为右侧运算对象。
比如vector支持操作:
1 |
|
之所以可以这样赋值,是因为vector类似这样重载了=运算符:
1 |
|
复合赋值运算符虽然不一定非要是类成员,但语义上作为类成员函数更符合直觉。
下标运算符
如果一个类包含下标运算符,则通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用。
1 |
|
常量对象取下标会匹配调用const版本。
递增和递减
这个比较特别,有前置版本和后置版本,所以也要定义两个版本。语义上建议作为成员函数。
前置
1 |
|
前置运算符返回的是递增或递减后的对象引用。
后置
为了区分前置和后置,后置接受一个额外的不被使用的int型形参。
1 |
|
重载*/->
1 |
|
重载()
最特别的一个。
1 |
|
只能作为类成员定义,可以重载多个函数,以参数区分。
类如果定义了调用运算符,那么该类的对象就被称为函数对象。
lambda会被编译器翻译成一个未命名类的未命名对象。lambda表达式产生的类中含有一个重载的函数调用运算符,所以lambda表达式实际上是函数对象。
标准库也定义了一组函数对象,plus类定义了一个函数调用运算符用于对一对运算对象执行+操作,modules类定义了调用运算符执行二元%操作,equal_to类执行==等。
这些类都是类模板,需要使用时指定具体应用类型。
1 |
|
它们定义在functional头文件中。
算术 | 关系 | 逻辑 |
---|---|---|
plus<Type> |
equal_to<Type> |
logical_and<Type> |
minus<Type> |
not_equal_to<Type> |
logical_or<Type> |
multiplies<Type> |
greater<Type> |
logical_not<Type> |
divides<Type> |
greater_equal<Type> |
|
modules<Type> |
less<Type> |
|
negate<Type> |
less_equal<Type> |
函数对象的一个坑:
1 |
|
后者可以用指针地址值来排序,标准库规定其函数对象对于指针同样适用,而手写的lambda就不行了。
C++语言中几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。调用形式指明了调用返回的类型以及传递给调用的实参类型。不同的可调用对象可能具有相同的调用形式。
标准库function
类型是一个模板,定义在头文件functional中,用来表示对象的调用形式。
function的操作 | |
---|---|
function<T> f; |
f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与函数类型T相同(即T是retType(args) ) |
function<T> f(nullptr); |
显式地构造一个空function |
function<T> f(obj); |
在f中存储可调用对象obj的副本 |
f |
将f作为条件:当f含有一个可调用对象时为真,否则为假 |
f(args) |
调用f中的对象,参数是args |
定义为function<T> 的成员的类型 |
|
result_type | 该function类型的可调用对象返回的类型 |
argument_type | T有一个或两个实参时定义的类型。如果T只有一个实参, |
first_argument_type | 则argument_type是该类型的同义词;如果T有两个实参, |
second_argument_type | 则first_argument_type和second_argument_type分别代表两个实参的类型 |
1 |
|
重载、类型转换与运算符
转换构造函数和类型转换运算符共同定义了类类型转换(class-type conversion)。
类型转换运算符
这是类的一种特殊成员函数,负责将一个类类型的值转为其他类型。它不能声明返回类型,形参列表也必须为空,形式如下:
1 |
|
类型转换运算符可以面向除了void以外的任意类型进行定义。
1 |
|
应该避免过度使用类型转换函数。如果在类类型和转换类型之间不存在明显的映射关系,则这样的类型转换可能具有误导性。
C++11引入了显示的类型转换运算符(explicit conversion operator)。和显式构造函数一样,编译器通常不会将显式类型转换运算符用于隐式类型转换。
一旦给了类型转换运算符explicit标志,那么:
1 |
|
如果表达式被用作条件,则编译器会隐式地执行显式类型转换。
- if、while、do语句的条件部分。
- for语句头的条件表达式。
- 条件运算符
? :
的条件表达式。 - 逻辑非运算符
!
、逻辑或运算符||
、逻辑与运算符&&
的运算对象。
在两种情况下可能产生多重转换路径:
- A类定义了一个接受B类对象的转换构造函数,同时B类定义了一个转换目标是A类的类型转换运算符。
- 类定义了多个类型转换规则,而这些转换涉及的类型本身可以通过其他类型转换联系在一起。
可以通过显式调用类型转换运算符或转换构造函数解决二义性问题,但不能使用强制类型转换,因为强制类型转换本身也存在二义性。
所以,请避免有二义性的类型转换。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!