ch14 重载运算与类型转化

当运算符作用于类的对象的时,通过重载运算符,使不同的运算符对于不同的类有着特定的含义,一方面能够简化类的使用者的逻辑,另一方面也能是程序更易于编写和阅读,而不至于编写更多的函数。

14.1 基本概念

重载的运算符是特殊的函数:由operator关键字加上要定义的运算符号共同组成。作为函数,也包含返回类型、参数列表和函数体。参数列表个数与该运算符作用的算数对象数量一样多。
不应该被重载的运算符:逻辑与、逻辑或、逗号运算符、取地址运算符。
定义成成员还是非成员的判断:

  • 赋值、下标、调用、成员访问箭头等运算符必须是类的成员。
  • 复合赋值运算符一般是类的成员
  • 递增、递减、解引用等运算符。通常是类的成员
  • 具有对称性的运算符可能转换任意一端的运算对象。如算数、相等性、关系、位运算符,通常应该是普通的成员函数。

14.2 输入输出运算符

输出的运算符尽量减少格式化操作,只需要打印信息就可以了。
输入输出运算符必须是非成员函数。
输入时可能会发生错误,例如:当流含有错误类型的数据时读取可能失败,当读取操作到达文件末尾或者遇到输入流的其他错误时也会失败。所以在输入操作中,输入运算符应该负责读取错误发生时,从错误中恢复。

14.3 算术和关系运算符

代码的解释:

1
2
3
4
5
6
Sales_data
operator-(const Sales_data &lhs,const Sales_data &rhs){
Sales_data item=lhs;
item-=rhs; //调用重载的-=运算符来定义。性能相同,只是增加代码的 复用,增加可读性
return item;
}

相等运算符的定义:如果定义了==,那么这个类也要定义!=(因为对于用户来说,当他们使用了==时,他们应该也希望能使用!=),并且相等运算符和不相等运算符的一个应该把工作委托给另外一个,这样一个预算负责实际比较,另外一个负责调用。
关系运算符:一般定义了相等运算符后,也会定义关系运算符,特别是<运算符。
关系运算符的原则:如果存在唯一逻辑可靠的<定义,就考虑定义<运算符。如果该类同时包含==运算符,则当且仅当<的定义与==产生一致的结果时才定义<运算符。

14.4 赋值运算符

赋值运算符=和符合赋值运算符+=,通常都定义为类的成员,并且都应该返回左侧运算对象的引用。

14.5 下标运算符

当我们需要按元素在容器中的位置访问元素时,通常定义一个下标运算符operator[],必须是成员函数。通常会定义两个版本,一个返回普通引用,一个返回常量引用。

14.6 递增和递减运算符

后置运算符接受一个额外的(不被使用)int类型的形参,当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参
。前置运算符返回递增或者递减后的对象的引用,后置运算符返回对象的原值(即递增或递减之前的原值),返回的是一个值而非引用。

1
2
3
4
5
6
7
8
class StrBlobPtr{
public:
StrBlobPtr& operator++(); //前置递增运算符
StrBlobPtr& operator++(int); //后置递增运算符
}
...
p.operator++(0); //显式的调用后置递增运算符
p.operator++(); // 显式的调用前置递增运算符

14.7 成员访问运算符

箭头运算符必须是类的成员,解引用运算符通常也是类的成员,即使并非必须这么做。
箭头运算符永远不能丢掉成员访问的这个基本含义,当我们重载箭头运算符时,可以改变的是从哪个对象中获取成员,而获取成员这一事实永远不变。
重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。

14.8 函数调用运算符

如果类重载了函数调用运算符,我们就可以像调用函数一样使用该类的对象。函数调用运算符必须是成员函数,一个类可以定义多个函数运算符,之间用参数的数量和类型进行区分。
函数对象通常作为泛型算法的实参
lambda表达式:当我们编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象。在这个类中,含有一个重载的函数调用运算符。捕获的对象相当于类内含有数据成员。是否含有默认/移动构造函数通常视捕获的对象而定。
标准库定义了一组表示算数运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。
标准库规定其函数对象对于指针同样适用。eg:

1
2
3
4
5
vector<string *> nameTable;
sort(nameTable.begin(),nameTable.end(),
[](string *a,string *b){return a<b}); //错误,nameTable中的指针之间没有关系,所以<将产生未定义的行为

sort(nameTable.begin(),nameTable.end(),less<string* >()); //正确

C++中的 可调用对象:函数、函数指针、lambda表达式、bind创建的对象、重载了函数调用运算符的类。可调用的对象也有类型。
不同类型可能具有相同的调用形式,可以定义一个 函数表用于存储指向这些可调用对象的“指针”。
标准库function类型:是一个模板,当创建具体的function类型时,需要我们提供额外的信息。eg:function<int (int,int)>表示接受2个int、返回1个int的可调用对象。不能直接将重载函数的名字存入function类型的对象中,可存储函数指针而不是函数的名字。

14.9 重载、类型转换与运算符

转换构造函数和类型转换运算符共同定义了类类型转换,有时也被称作用户定义的类型转换。
operator type() const; //type表示某种类型因为类型转换运算符是隐式执行的,所以无法传递实参,也不能定义形参。为了避免具有误导性的类型转换,在不存在明显映射关系的时候,应不适用。
显式的类型转换运算符。eg:explicit operator int() const;当类型转换运算符是显式的,必须通过显式的强制类型转换才可以。 如果表达式被用作条件,编译器会显式的自动转换。
无论我们什么时候在条件中使用了流对象,都会使用为IO类型定义operator bool。

1
2
3
4
struct Integral{
operator const int(); //将对象转换成 const int
operator int() const; //将对象转换成 int,用的相对较多
}

应该避免使用二义性的类型转换,总体原则:除了显式的向bool类型的转换外,应该尽量避免定义类型转换函数,并且尽可能限制某些看起来显然正确的显式构造函数。
错误eg:定义了两种将B类转换为A类的方法,一种使用B的类型转换运算符,一种使用A的以B为参数的构造函数。

  • 不要另两个类执行相同的类型转换。
  • 避免转换目标是内置算数类型的类型转换。
    函数重载的类型匹配:当调用重载函数时,如果两个或多个用户定义的类型转换都提供了可行的匹配,那么这些类型转换被认为一样好。这意味着我们设计的不足。