运算符重载
重载的运算符是具有特殊名字的函数,除函数调用运算符外,其他重载运算符不能含有默认实参。
- 一个运算符函数,或者类的成员,或者至少含有一个类类型的参数。也就是说不能为内置类型重载运算符。
- 只能重载已有的运算符。
- 一个重载的运算符,其优先级和结合律与对应的内置运算符保持一致。
- 由于使用重载的运算符本质上是函数调用,因此对象求值顺序的规则无法应用到重载的运算符上。尤其是
&&
,||
和,
,两个运算对象总是会被求值。 - 通常情况下,不应该重载
&&
,||
,&
和,
,运算符。 - 一般来说,提供了某个重载运算符,也应该提供与此运算符相关的一系列运算符,如:算术运算符->对应的复合赋值运算符。
成员或者非成员
=
,[]
,()
和->
运算符**必须是成员。复合赋值运算符一般来说应该是成员,但非必须。
改变对象状态或与给定类型密切相关的运算符,应该是成员。如:递增、解引用。
具有对称性的运算符可能转换任意一端的运算对象,通常应该是非成员。如:算术、相等性、关系和位运算符。如果想提供含有类对象的混合类型表达式,运算符必须是非成员函数。
1 2
string s = "world"; string u = "hi" + s; // 如果是成员函数,则"hi".operator+(s),错误
对于友元要注意,在类内部虽然有友元声明,但这并非真正意义上的函数声明,因此在类外部还需要有函数声明。
输入输出运算符
«
ostream &operator<<(ostream &os, const Sales_data &item);
- 必须是非成员函数,否则左侧运算对象将是类的一个对象 就算要是某个类的成员,也只能是istream或ostream的,然而并不能为标准库中的类添加成员,因此只能是非成员函数
- 不应该打印换行符
»
istream &operator>>(istream &is, Sales_data &item);
必须处理输入可能失败的情况
1 2 3 4 5 6 7 8 9 10
istream& operator>>(istream &is, Sales_data &item) { double price = 0.0; is >> item.bookNo >> item.units_sold >> price; if (is) { item.revenue = price * item.units_sold; } else { item = Sales_data(); } return is; }
有时需要标识流的条件状态
算术和关系运算符
通常把算术和关系运算符定义为非成员函数,使得左侧或右侧的运算对象可以转换
1 2
string s = "world"; string u = "hi" + s; // 如果是成员函数,则"hi".operator+(s),错误
这些运算符一般不需要改变运算对象的状态,所以形参是常量引用
一般使用复合赋值来实现算术运算符
==
- 相等运算符应该具有传递性
- 定义了
==
,也应该定义!=
关系运算符
关系运算符应该,
- 定义顺序关系,且与关联容器中对关键字的要求一致
- Define a relation that is consistent with
==
if the class has both operators. In particular, if two objects are!=
, then one object should be<
the other.
如果存在唯一一种逻辑可靠的<语义,才考虑定义<运算符。如果类同时还包含==,则当且仅当<的定义和==产生的结果一致时才定义<运算符。
=
- 必须为成员函数
- 必须先释放当前内存空间
- 如果不是拷贝赋值运算符和移动赋值运算符,则不必检查自赋值的情况。
[]
- 必须为成员函数
- 返回元素的引用
- 通常定义const和非const版本
递增递减运算符
由于改变了对象的状态,一般应为成员函数,且应该同时定义前置版本和后置版本。
|
|
成员访问运算符
|
|
->
必须是类成员,*
非必需,但通常也是。- 定义为const是因为获取一个元素并不会改变对象的状态。
- 虽然可以让解引用返回任何想要的值或打印(不建议),但箭头运算符必须有成员访问的含义。重载箭头运算符时,可以改变的是箭头从哪个对象中获取成员。
函数调用运算符
函数调用运算符必须是成员函数。如果类定义了调用运算符,那么该类的对象叫做函数对象。调用函数对象实际上是在运行重载的调用运算符。
lambda是函数对象
编写lambda后,编译器将生成一个未命名类的未命名对象。这个类中有一个重载的函数调用运算符,且默认情况下这个成员是const(lambda不能改变捕获的变量),除非lambda被声明为mutable。
引用捕获 由程序确保lambda执行时所引用的对象确实存在,生成的类中无须保存为数据成员。
值捕获 生成的类中需建立对象的数据成员,同时创建构造函数,用捕获的变量来初始化数据成员。
lambda产生的类中,不包含默认构造函数、赋值运算符和默认析构函数,默认拷贝和默认移动构造函数视捕获的数据成员类型而定。
标准库定义的函数对象
标准库规定的函数对于指针同样适用。直接比较两个无关的指针将产生未定义的行为,但通过标准库定义的函数对象来比较是定义良好的。
关联容器使用less<key_type>
对元素排序,因此可以直接定义一个指针的set或map,而无须声明less
。
function
C++中的可调用对象多种有,它们的类型是不同的,
- 函数和函数指针 类型由返回值类型和实参类型决定
- lambda表达式 每个lambda有唯一的未命名类类型
- bind创建的对象
- 重载了函数调用运算符的类
类型不同的可调用对象可能共享同一种调用形式。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型。
定义funcion
类型时,需要指明调用形式。
|
|
重载、类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类型的隐式转换机制,这种构造函数叫做转换构造函数。
转换构造函数和类型转换运算符共同定义类类型转换,也叫用户定义的类型转换。
编译器一次只能执行一个用户定义的类型转换。
隐式类型转换运算符
operator int() const;
从类类型转换为int- 无显示的返回类型和形参
- 返回一个对应类型的值
- 必须定义为成员函数
- 通常不应该改变转换对象的内容,一般被定义为const
|
|
S虽然没有定义-
,但隐式转换为int,可以执行内置的-
。
虽然编译器一次只能执行一个用户定义的类型转换,但隐式的用户定义类型转换可以至于一个标准(内置)类型转换之前或之后。
|
|
显式类型转换运算符
explicit operator int() const;
|
|
但是当表达式作为条件时,编译器会将显式类型转换自动应用于它(仅仅是自动应用explicit operator bool() const
,以转换为bool)。
|
|
避免有二义性的类型转换
必须确保类类型和目标类型之间只存在唯一一种转换方式。无法通过使用强制类型转换来解决二义性问题,强制类型转换也会面临二义性问题。
多重转换路径可能由于,
两个类提供相同的类型转换
类定义了一组类型转换,它们的转换源(或者转换目标)类型本身可以通过其他类型转换联系在一起,由于所有算术类型转换的级别都一样,选择转换序列时会有多个转换序列,将会导致二义性。
1 2 3 4 5 6 7 8 9 10
class S { public: S(); operator int() const; operator double() const; }; S s; int a = s; // 精确匹配 float b = s; // 两个“可行函数”
如果已经定义了一个转换为算术类型的类型转换,不要再定义接受算术类型的重载运算符。在不定义以后,如果用户需要使用这样的运算符,则类型转换操作会转换此类型的对象,然后使用内置的运算符。
重载函数与转换构造函数
当调用重载的函数时,如果两个或多个类型转换都提供了可行的匹配,则这些类型转换一样好。
重载函数与用户定义的类型转换
当调用重载的函数时,如果两个或多个类型用户定义的转换都提供了可行的匹配,则这些类型转换一样好。此时,不考虑任何可能出现的标准类型转换级别。
只有当重载函数能通过同一个类型转换函数得到匹配时(所有可行函数都请求同一个用户定义的类型转换),才考虑标准类型转换级别。
函数匹配与重载运算符
表达式中运算符的候选函数集包括成员函数和非成员函数。
如果对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符和内置运算符的二义性问题。