当运算符作用于类类型的运算对象时,可以通过运算符重载重新定义该运算符的含义
- 重载的运算符是具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号共同组成。和其他函数一样,重载的运算符也包含返回类型,参数列表以及函数体
- 重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
- 如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此,成员运算符函数的显式参数数量比运算符的运算对象总数少一个
- 对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数,这一约定意味着当运算符作用于内置类型的运算对象时,我们无法改变该运算符的含义。
- 我们只能重载已有的大多数运算符,而无权发明新的运算符号。
- 对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。
- 我们既可以直接将运算符作用于类型正确的实参,从而间接“调用”重载的运算符函数,也可以像调用普通(成员)函数一样调用运算符函数。
- 某些运算符指定了运算对象求值的顺序。因为使用重载的运算符本质上是一次函数调用,所以这些求值顺序的规则无法应用到重载的运算符上。特别是,逻辑与&&,逻辑或||和逗号运算符。同时也不应该重载取地址符。
- 重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容。一般情况下,只有当操作的含义对于用户来说清晰明了时才重载运算符。
- 当我们定义重载的运算符时,必须首先决定是将其声明为类的成员函数还是声明为一个普通的非成员函数,下面是一些有助于抉择的准则:
- 赋值(=),下标([]),调用(())和成员访问箭头运算符(->)必须是成员
- 复合赋值运算符一般是成员,但并非必须
- 改变对象状态的运算符或者与给定类型密切相关的运算符,通常应该是成员
- 具有对称性的运算符应该是普通的非成员函数
- 当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。
- 如我们所知,IO标准库分别使用了<<和>>执行输出和输入操作,对于这两个运算符来说,IO库定义了用其读写内置类型的版本,而类则需要自定义适合其对象的新版本以支持IO操作。
- 举个例子:
ostream& operator<<(ostream &os,const Sales_data &item){
os<<item.isbn()<<" "<<items.units_sold;
return os;
}
与iostream标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员函数。否则,它们的左侧运算对象将是我们的类的一个对象,这显然不可能。当然,IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元。
- 对于输入运算符而言,也比较类似,不过第二个参数是非常量对象的引用,同时函数体内要对流进行检测(比如通过if),避免一些输入错误的影响。
- 通常情况下,我们把算术和关系运算符定义成非成员函数以允许运算对象位置的转换,因为这些运算符一般不会改变运算对象的状态,所以形参都是常量的引用。
- 如果类定义了operator==,则这个类也应该定义operator!=,同时其中之一的任务应该委托给另外一方,而不用重复书写一套非常相似的逻辑。
- 如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符。如果类同时还包括==,则当且仅当<的定义和==产生的结果一致时才定义<运算符。
- 之前已经介绍过拷贝赋值和移动赋值运算符,此外,类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象。
比如,vector可以使用花括号的元素列表作为参数,实际上是利用了std::iniitializer_list<T> 这个类型作为参数,以此类推。 - 为了与下标的原始定义兼容,下标运算符通常以所访问元素的引用作为返回值。同时,我们最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用确保我们不会赋值。
- 定义递增和递减运算符的类应该同时定义前置和后置版本,并且通常被定义为成员。
- 要想同时定义前置和后置,需要解决一个问题,即普通的重载形式无法区分这两种情况。为了解决这个问题,后置版本接受一个额外的(不被使用)的int类型的形参,当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参。
ClassName operator++(int);
ClassName operator--(int);
很多时候,后置版本可以通过调用前置版本来完成实际的工作。
同时,如果想要显式地调用后置版本,需要为那个不被使用的int参数传入一个值。
- 与大多数其他运算符一样,我们能令operator*完成任何我们指定的操作。但是箭头运算符则不是这样,它永远不能丢掉成员访问这个最基本的含义,当我们重载箭头时,可以改变的是箭头从哪个对象获取成员。
对于形如point->mem的表达式来说,point必须是指向类对象的指针或者是一个重载operator->的类的对象。根据point类型的不同,point->mem分别等价于:
(*point).mem; //point是一个内置的指针类型
point.operator()->mem; //point是类的一个对象
所以很显然,重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
这两个运算符往往定义成const成员,因为他们一般不改变对象的状态。
- 如果类定义了调用运算符,则该类的对象称作函数对象,因为可以调用这种对象,我们说这些的对象”行为像函数一样”。
- 当我们编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象,在产生的类中有一个重载的函数调用运算符。
- 当一个lambda表达式通过引用捕获变量时,将由程序确保所引对象确实存在,因此编译器可以直接使用。而如果通过值捕获变量,产生类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数用于初始化这些成员。
- 标准库还定义了一组表示算术,关系,逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。由于属于函数式编程的范畴,此处不赘述。
- 前面的笔记提到过,如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换规则,这种构造函数也被称为转换构造函数。
- 类型转换运算符(conversion operator)是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。一般形式如下:
operator type() const;
其中type表示某种类型,类型转换运算符可以面向任意类型(除了void)进行定义,只要该类型能作为函数的返回类型。
比如:
class SmallInt{
public:
SmallInt(int i=0):val(i){}
operator int() const{return val;}
private:
size_t val;
};
其中,构造函数将算术类型的值转换成SmallInt对象,而类型转换运算符将SmallInt转换成int。
- 一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空,且通常是const的。
- 在实践中,类很少提供类型转换运算符,因为大多数情况下如果类型转换自动发生用户可能会感到意外而不是受到了帮助。不过定义向bool的类型转换还是比较普遍的现象。
在早期的版本中,因为bool是一个算术类型,所以类类型被转换成bool后能被用于任何需要算术类型的上下中,比如:
int i=42;
cin << i;
该代码能使用istream的bool转换,接着提升至int并左移42位,这一结果不可谓不出人意料。
- 为了防止上述异常发生,C++11标准引入了显式的类型转换运算符
class SmallInt{
public:
explicit operator int() const {return val;}
...
}
和显式的构造函数一样,编译器不会将一个显式的类型转换运算符用于隐式类型转换。
SmallInt s1=3; //正确,构造函数非显式
s1+3; //错误:此处需要隐式的类型转换,但运算符是显式的
static_cast<int>(s1)+3; //正确,显式地请求类型转换
该规定存在一个例外,如果表达式被用作条件,则编译器会将显式的类型转换自动应用于它。由于向bool的类型转换通常用在条件部分,所以operator bool一般也定义成explicit的。
- 类类型转换的定义非常容易出现二义性,除了显式地向bool类型的转换之外,我们应该尽量避免定义类型转换函数。具体的二义性规则此处不赘述,可以简单地人为判断出来。
- 重载的运算符也是重载的函数,但候选函数集要比我们使用调用运算符调用函数时更大,因为我们无法通过语法形式来区分到底使用的是成员函数还是非成员函数,这同样会有可能引发二义性问题。一般来说,如果我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。