C++primer(第五版)---14章(重载运算与类型转换)

发布于:2022-10-13 ⋅ 阅读:(1165) ⋅ 点赞:(1)

目录

​编辑

重载运算符的基本概念:

调用重载运算符:

而有些运算符不应该被重载:

应该保持与内置类型一致的含义:

是否成员成员函数:

重载io(<<和>>)运算符:

重载<<运算符:

重载>>运算符:

算术运算符:

 关系运算符:

赋值运算符(要定义为成员函数):

下标运算符(必须为成员函数):

前置和后置递减递加运算符:

前置递增递减运算符(基本为成员函数):

 后置递增递减运算符:

 成员访问运算符(通常用于智能指针或是一些有指针的数据成员时):

箭头运算符的限定:

函数调用运算符(必须为成员函数)函数对象和lambda表达式:

 而函数对象常常用于泛型算法的实参

lambda是函数对象:


30e60a5f1f7c452387fb9f2c0d8b12a9.png

重载运算符的基本概念:

我们不能重载内置类型的运算符,且不能创造一个新的运算符进行重载。正常的重载运算符参数数量应该与其运算对象一样多,而只有重载()运算符外,其他不能有默认实参。如果重载运算符为成员函数,其第一个左侧对象默认为绑定的this指针上。参数数量与运算对象少一个(为默认为this指针)。

调用重载运算符:

间接方式调用:s1+s2;

直接函数调用:operator+(s1,s2);

二者是等价的。如果在成员函数中:

data1+=data2;

data1.operator+(data2);(data1为左侧运算对象)

而有些运算符不应该被重载:

有些运算符有其隐含的运算规则和求值顺序,像&,|有其短路特性,即如果&左侧为假不判断右侧对象的真假,|如果左侧对象为真,则不判断右侧对象的真假。还有逗号运算符。而重载后可能无法保证其求值顺序被保留,还有求址和逗号运算符,其本身就定义了对象为类的操作。故不应该对以上情况重载。

应该保持与内置类型一致的含义:

最好重载相关的运算符实现的功能相似与内置类型使用其的功能。也最好保持其特性一致。像赋值运算符=,赋值后,左侧对象与其右侧对象的值相等,返回左侧对象的引用。因此重载时也要实现此功能并返回左侧对象的引用。或者是+=运算符,应该为先+在赋值,保持其特性。而应该配套运算符相关联的运算符,例如要重载<运算符,应该把相应的比较运算符也重载进去。如果有==,也应该重载!=运算符。

是否成员成员函数:

像会具体改变左侧对象的值的情况下,作为成员函数的。->,[ ],(),=等应该作为成员函数重载的,而复合运算符不一定一定是成员函数。而具有对称性的运算符,即如关系,相等性,算术运算和位运算等,一般不为成员函数。或者需要其左右侧类对象类型不同,可交换位置,通常设置为非成员函数。

重载io(<<和>>)运算符:

重载<<运算符:

一般用于输出,即第一个参数为相应的非常量ostream对象的引用,非常量是因为其向流输入内容会改变状态,而引用则是ostream不可复制(第13章的禁止拷贝复制,一个流不能被复制),而第二个参数为其复制对象常量的引用,输出不改变其内容,引用节省空间。为了后续的运算(连续<<运算),返回类型为其ostream的引用。

格式 ostream& operator <<(ostream&,const data&);

最好不要在重载<<运算符函数体内容添加格式控制的语句,类似换行符,只要负者输出内容而不负责控制格式。这样就不能实现将后续内容放在同一行,也不能为成员函数,因为其左侧对象要为ostream,而如果为成员函数,其左侧绑定为当前类实例化对象(this)。因此要访问类对象,就定义为友元函数即可。

重载>>运算符:

第一个参数为要读取流的引用,第二个参数为将要读取到的数据的非常量(因为要读入数据,会改变)引用,返回相应流的引用。

istream&operator>>(operator&,data&);

与<<运算符不同,>>运算符重载时必须能够处理输入失败的情况。例如读入错误的数据类型,当与输入要求的类型不一致时,会发生错误,从而导致后续的输入无法正常运行。当读到文件末尾或者其他问题的发生错误。我们要负者处理错误,或是将其要保存的数据初始化。最好向io库报告错误。

b366941b826f427ca12eaa5ac90b59c7.png

算术运算符:

可以支持左右对象的交换,不为类成员函数,一般不需要改变变量的状态,故参数类型为常量引用。而它的计算结果为一个临时量,如果一个类要定义重载算术运算符,一般要先定义其复合赋值运算符,用复合赋值运算符来实现算术运算符的定义。

例:

625fb4629e414d28878736d1068e4c1f.png

 而当类需要比较相等时,最好定义重载==运算符,这样方便记忆的同时也十分快捷,同时要定义其对应的!=运算符,并且其中一个运算符要用另一个运算符来简化定义。

0ff636b8bd054d8cbcc03913811c849a.png

 49077682ec9941c5a2b82a8b50752836.jpg

 关系运算符:

相关的容器(vector。。。)会要求所放入的元素支持一定的关系运算(<运算符),即比较大小。但要注意,如果一个类同时定义了<运算符和==运算符,那么不仅是要满足<比较,还要满足其==配套的!=时,如果两个类实例!=,则其应该是具有<关系的。这并不是都能满足的,因为<是相对于其中一个数据进行比较的,而当!=符号是对于多个数据进行判等的,有时会导致其中有的数据不一样但是对于比较的数据是相同的,所以这种情况下最好不要定义<运算符。(例如一个图书类,有书名号和价格,比较的时候是比较价格大小排序,但是!=是比较书名号和价格,而在书名号不同但是价格相同时,<运算符没用了)

赋值运算符(要定义为成员函数):

像之前的拷贝复制运算符和移动赋值运算符(13章),还可以定义其他的赋值运算符,类似vector标准库还定义了第三种赋值,以列表形式进行赋值。

56bd271998c94c13ae4ebcc4a73a98a2.png

 1b9f79fb1bc246b5a67e335d54a136c9.png

 0e681814ae9a43cb9b59bac4040a2e19.png

而像复合赋值运算符,虽然可以不定义为成员函数,但最好都定义为成员函数。并且返回其引用。

下标运算符(必须为成员函数):

类似于内置数据类型的数组,【】返回的是类中特定顺序的元素,与内置类型也一样,返回的是元素引用,保障其能放在表达式的左右, 而且要定义常量版本和非常量版本的[]运算符。保证在常量对象使用下标时不可改变其元素。

9fe80b731a7c4f06b1192b1cd646816a.png

前置和后置递减递加运算符:

前置递增递减运算符(基本为成员函数):

需要先检查其移动是否合法,有无超出范围。而前置是返回的是递增递减后元素的引用。

7e3eb161c4884f9b9a3a77933313941a.png

 ad4e1779d85f4ab5a190edc85fbe041d.png

 后置递增递减运算符:

后置与前置不同,不会返回递增后元素的引用,而是返回一个临时量用于保存未移动前的数据。而二者重载区分的标志为后置的参数列表中会多一个int型的参数(无需为其命名)。编译器会为其提供一个实参为0的数。fb02fcbf4f404d58814fe6d940072021.png

 而当我们要显式用函数的形式调用后置运算符时,必须传给它一个int实参,来告诉编译器我们调用的是后置运算符。

c37fe7482db04a1db53bb9042c074947.png

 成员访问运算符(通常用于智能指针或是一些有指针的数据成员时):

重载*和->为访问类的成员的运算符,而->一定要是成员函数,而*通常是成员函数。

a140f0128f5a4027b380ad193cc07d25.png

而我们可以将他定义为常量版本的,适用于常量的实例化对象,因为我们只是得到其引用或是指针,并不会改变他的内部数据的状态。(而如果*或是->得到其内部数据要改变时,也会因为是指针形式的,如果常量类型则只是其指针所绑定的地址不能更改,而可以改变其指针所绑定的值)

箭头运算符的限定:

而我们可以将*重载运算符定义为任何我们想要的操作,返回的类型可以任意,但是->运算符不行,他一定要是成员访问的作用。所以重载的->运算符一定要是返回一个指针或者是重载了->的类,其他会报错。

函数调用运算符(必须为成员函数)函数对象和lambda表达式:

一个类可以定义函数调用运算符,使其行为像函数一样,但比函数更加灵活(还可以存储状态)。

e5b52d2725c742d08fc612ffb7111c03.png

c389b2f19beb4c928d0720f07431d83f.png

 可以定义多个函数调用运算符,要其参数数量和类型不一致。而这种类定义了函数调用运算符也被称作函数对象。同时也能定义数据成员。实现定制相应不同的操作。其可以定义0或者多个形参。

2d88259425ba4a6eb633b7d397e75ef9.png

 而函数对象常常用于泛型算法的实参

e4cf4b16e53649e08fa0bc02d59b8a98.png

我们可以向标准库中的算法传递任何类别的可调用对象(只要一个对象或者一个表达式,只要能对其使用调用运算符则其就是可调用的,像:函数,函数指针,lambda表达式,重载了调用运算符的类),在其作为参数时,会自动调用其调用运算符进行传参

lambda是函数对象:

lambda表达式实质就是被编译器翻译成一个未命名的类的未命名对象,该类中有一个重载的函数调用运算符。

stable_sort(words.begin(),words.end(),[](const string &a,const string &b){return a.size()<b.size();});

其行为类似于下面:
 

class ShortString{
public:
    bool operator()(const string &s1,const string &s2)const
    {return    s1.size()<s2.size();
    }
}; 

其产生的类只有一个函数调用运算符成员,而且不含有默认构造函数,赋值运算符及默认析构函数,是否含有默认的拷贝/移动构造函数视捕获的数据成员类型而定(在13章有提到默认拷贝和移动的规则)。且默认情况下lambda不能改变它捕获的变量。因此在lambda产生的类中函数调用运算符是一个const成员函数。如果为可变的,则调用运算符就不是const。

等价于:

stable_sort(words.begin(),words.end(),ShorterString());

而当lambda捕获函数外的变量时:

当一个函数lambda表达式通过引用来捕获变量,由程序负责确保lambda执行时引用的对象确实存在。因此编译器可以直接用该引用而无须在lambda产生的类中将其存储为数据成员。

而当通过值捕获的方式拷贝到lambda中,会被值捕获的变量拷贝到lambda。这时lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数。将其捕获的变量的值来初始化数据成员。

b94a32a868ef4246a0f441adc3154d42.png

a3f7ffa60d1a41feb73fa5a9deec4bb6.png

d7a9406d8d154186a738f9ecd7e71f95.png

这个合成的类不含有默认构造函数,因此必须提供一个实参初始化。

auto wc = find_if(words.begin(),words.end(),SizeComp(sz));

lambda是通过匿名的函数对象来实现的,是其函数对象在使用方式上的简化。当代码需要一个简单的函数且并不会在其他地方被使用时,就可以使用lambda表达式。而如果需要多次使用,并且需要保存某些状态的话,使用函数对象会更加合适。

后续继续补充。。。(明天)

本文含有隐藏内容,请 开通VIP 后查看