c++ primer读书笔记-第十四章 重载操作符与转换
2015-07-13 18:56
344 查看
C++ 允许我们重定义操作符用于类类型对象时的含义。如果需要,可以像内置转换那样使用类类型转换,将一个类型的对象隐式转换到另一类型。
例如标准库为容器类定义了几个重载操作符。这些容器类定义了下标操作符以访问数据元素,定义了 * 和 -> 对容器迭代器解引用。
声明了加号操作符,可用于将两个 Sales_item 对象“相加”并获得一个 Sales_item 对象的副本。
除了函数调用操作符之外,重载操作符的形参数目(包括成员函数的隐式
this 指针)与操作符的操作数数目相同。函数调用操作符可以接受任意数目的操作数。
可重载和不可重载的操作符
2.重载操作符必须具有至少一个类类型或枚举类型(第 2.7 节)的操作数。这条规则表明:重载操作符不能重新定义用于内置类型对象的操作符的含义。
3.重载后的操作符优先级和结合性是固定的
在 && 和 || 的重载版本中,两个操作数都要进行求值,而且对操作数的求值顺序不做规定。因此,4.重载 &&、|| 或逗号操作符不是一种好的做法。
5.类成员与非成员
作为类成员的重载函数,其形参看起来比操作数数目少1。作为成员函数的操作符有一个隐含的 this 形参,限定为第一个操作数。
类成员操作符:
非类成员操作符
6.操作符重载和友元关系
操作符定义为非成员函数时,通常将它们设置为所操作类的友元函数,以便该操作符函数可以访问该类的私有成员。
但若无需访问类私有成员时,可不必设为友元函数。
类中声明==操作符为友元函数:
在函数中可以直接访问Sales_item的私有成员:
7.使用重载操作符
使用重载操作符的方式,与内置类型操作数上使用操作符的方式一样。
这个表达式隐式调用为 Sales_items 类而定义的 operator+。
也可以像调用普通函数一样调用重载操作符函数,指定函数并传递适当类型适当数目的形参(下面调用了一个非成员操作符+):
调用成员操作符函数:
第一种情况下,使用表达式语法隐式调用重载操作符函数:第二种情况下,在 item1 对象上调用成员操作符函数。
• 合成赋值操作符(第 13.2节)进行逐个成员赋值:使用成员自己的赋值:使用成员自己的赋值操作依次对每个成员进行赋值。
• 默认情况下,取地址操作符(&)和逗号操作符(,)在类类型对象上的执行,与在内置类型对象上的执行一样。取地址操作符返回对象的内存地址,逗号操作符从左至右计算每个表达式的值,并返回最右边操作数的值。
• 内置逻辑与(&&)和逻辑或(||)操作符使用短路求值(第 5.2节)。如果重新定义该操作符,将失去操作符的短路求值特征。
• 相等测试操作应使用 operator==。
• 一般通过重载移位操作符进行输入和输出。
• 测试对象是否为空的操作可用逻辑非操作符 operator! 表示。
• 赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
• 像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
• 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
• 对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
重载输出操作符一般的简单定义如下:
第一个形参是对 ostream 对象的引用,在该对象上将产生输出。ostream 为非 const,因为写入到流会改变流的状态。
第二个形参一般应是对要输出的类类型的引用。该形参是一个引用以避免复制实参。
一般而言,输出操作符应输出对象的内容,进行最小限度的格式化,它们不应该输出换行符。
则只能这样使用:
输入和输出操作符有如下区别:输入操作符必须处理错误和文件结束的可能性。
Sales_item 的输入操作符如下:
任何读入都可能碰到输入流中的文件结束或其他一些错误。
Sales_item 加法操作符一样:
加法操作符并不改变操作符的状态,操作符是对 const 对象的引用;相反,它产生并返回一个新的 Sales_item对象,该对象初始化为 lhs 的副本。
算术操作符通常产生一个新值,该值是两个操作数的计算结果,它不同于任一操作数且在一个局部变量中计算,返回对那个变量的引用是一个运行时错误。
个数据成员,如果所有对应成员都相同,则认为两个对象相等。
设计原则:
• 如果类定义了 == 操作符,该操作符的含义是两个对象包含同样的数据。
• 如果类具有一个操作,能确定该类型的两个对象是否相等,通常将该函数定义为 operator== 而不是创造命名函数。用户将习惯于用 == 来比较对象,而且这样做比记住新名字更容易。
• 如果类定义了 operator==,它也应该定义operator!=。用户会期待如果可以用某个操作符,则另一个也存在。
因为 < 的逻辑定义与 == 的逻辑定义不一致,所以根本不定义 < 会更好。
如果没有定义这个操作符,则编译器将合成它。类赋值操作符必须是类的成员,以便编译器可以知道是否需要合成一个。
赋值操作符可以重载。无论形参为何种类型,赋值操作符必须定义为成员函数,这一点与复合赋值操作符有所不同。
下标操作符必须定义为类成员函数。
下标操作符出现在左边,必须生成左值,可以指定引用作为返回类型而得到左值。只要下标操作符返回引用,就可用作赋值的任意一方。
可以对 const 和非 const 对象使用下标也是个好主意。应用于 const 对象时,返回值应为 const 引用,因此不能用作赋值的目标。
类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用。
箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员,但将它作为成员一般也是正确的。
对这个ScreenPtr类对象使用解引用操作符( * )和箭头操作符(->)时,可以直接操作到实际的对象。
使用:
ps的ptr成员指向一个ScrPtr对象,然后这个对象指向上述的Screen对象
像下标操作符一样,我们需要解引用操作符的 const 和非 const 版本。它们的区别在于返回类型:const 成员返回 const 引用以防止用户改变基础对象。
这里没有第二个形参,因为 ->的右操作数不是表达式,相反,是对应着类成员的一个标识符。没有明显可行的途径将一个标识符作为形参传递给函数,相反,由编译器处理获取成员的工作。
重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。
这个自增操作符根据 end 检查 curr,从而确保用户不能将 curr 增量到超过数组的末端。如果 curr 增量到超过 end,就抛出一个 out_of_range 异常;否则,将 curr 加 1 并返回对象引用。
为了解决这一问题,后缀式操作符函数接受一个额外的(即,无用的)int 型形参。使用后缀式操作符进,编译器提供 0 作为这个形参的实参。
后缀式操作符可以这样实现:
操作符的后缀式必须记住对象在加 1/减 1之前的当前状态。这些操作符定义了一个局部 CheckedPtr对象,将它初始化为 *this 的副本,即 ret是这个对象当前状态的副本。接着将原对象增量后,返回未增量的的对象副本ret。
参值,而前缀不用:
这个类定义了一个操作:函数调用操作符,该操作符有一个形参
并返回形参的绝对值。
通过为类类型的对象提供一个实参表而使用调用操作符,所用的方式看起来像一个函数调用:
函数调用操作符必须声明为成员函数。一个类可以定义函数调
用操作符的多个版本,由形参的数目或类型加以区别。
定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象。
使用 GT6 函数 作为传给 count_if 算法的实参,以计算使 GT6 返回 true 的单词的数目:
直接使用函数名作参数等效于取函数地址 :>6
通过将 GT6 定义为带函数调用成员类(GT_cls),可以获得所需的灵活性。
使用 GT_cls 函数对象:
向GT_cls构造函数传递不同的参数可以进行不同的比较。
plus 模板中的调用操作符对两个操作数应用 + 运算。
有两个一元函数对象类:一元减(negate))和逻辑非(logical_not))。其余的标准库函数对象都是表示二元操作符的二元函
数对象类。
传递函数对象greater,该类将产生一个调用操作符,调用基础对象的大于操作符。
绑定器,是一种函数适配器,它通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象。
求反器,是一种函数适配器,它*将谓词函数对象的真值求反
标准库定义了两个绑定器适配器:bind1st 和 bind2nd。
bind1st 将给定值绑定到二元函数对象的第一个实参,bind2nd将给定值绑定到二元函数对象的第二个实参。
该适配器bind2nd返回一个函数对象less_equal,该对象用 10 作右操作数应用 <= 操作符。
两个求反器:not1 和 not2。你可能已经想到的,not1 将一元函数对象的真值求反,not2 将二元函数对象的真值求反。
果是对不 <= 10 的
那些元素进行计数。
除了定义到类类型的转换之外,我们还可以定义从类类型的转换。即,我们可以定义转换操作符,给定类类型的对象,该操作符将产生其他类型的对象。
假定想要定义一个名为 SmallInt的类,该类实现安全小整数,这个类将使我们能够定义对象以保存与 8 位 unsigned char 同样范围的值,即,0 到 255。这个类可以捕获下溢和上溢错误,因此使用起来比内置 unsigned char 更安全。
我们希望这个类定义 unsigned char支持的所有操作。具体而言,我们想定义 5个算术操作符(+、-、*、/、%)及其对应的复合赋值操作符,4个关系操作符(<、<=、>、>=),以及相等操作符(==、!=)。
因为存在从任意算术类型到 int 的转换,这三个函数可以涵盖支持
SmallInt 对象的混合模式使用的要求。但是,它不能适当处理浮点类型等非int型的混合模式操作。
将 对象si 转换为 int 值。
将所得 int 结果转换为 double 值并与双精度字面值常量 3.14159 相加,得到 double 值。
转换函数必须是成员函数,不能指定返回类型(例如: int operator int(); ),并且形参表必须为空。
转换函数一般不应该改变被转换的对象,通常应定义为 const 成员。
• 在表达式中:
• 在条件中:
• 在显式类型转换中:
但是不能完成从 Integral 到 int 的直接转换,因为它需要两次类类型转换。
为了调用函数 calc(),应用标准转换将 dobj 从 double 类型转换
为 int 类型,然后调用构造函数 SmallInt(int) 将转换结果转换为 SmallInt 类型。
考虑最简单的调用非重载函数的情况:
最后一个对 extended_compute的调用有二义性。可以使用任一转换函数,但每个都必须跟上一个标准转换来获得 long double,因此,没有一个转换比其他的更好,调用具有二义性
第三个调用具有二义性。没有构造函数完全匹配于long。使用每一个构造函数之前都需要对实参进行转换:
1. 标准转换(从 long 到 double)后跟 SmallInt(double) 的构造函数。
2. 标准转换(从 long 到 int)后跟 SmallInt(int)。
避免二义性最好的方法是避免编写互相提供隐式转换的成对的类。
1. 确定候选函数集合:这些是与被调用函数同名的函数。
2. 选择可行的函数:这些是形参数目和类型与函数调用中的实参相匹配的候选函数。选择可行函数时,如果有转换操作,编译器还要确定需要哪个转换操作来匹配每个形参。
3. 选择最佳匹配的函数。
Integral 和 SmallInt 这两个类都提供接受 int 参数的构造函数,其中任意一个构造函数都可以与 manip的一个版本相匹配,因此,函数调用有二义性。
1. 不要定义相互转换的类,即如果类 Foo 具有接受类 Bar 的对象的构造函数,不要再为类 Bar 定义到类型 Foo 的转换操作符。
2. 避免到内置算术类型的转换。具体而言,如果定义了到算术类型的转换,则
o 不要定义接受算术类型的操作符的重载版本。如果用户需要使用这些操作符,转换操作符将转换你所定义的类型的对象,然后可以使用内置操作符。
o 不要定义转换到一个以上算术类型的转换。让标准转换提供到其他算术类型的转换。
试图进行混合模式运算,将会遇到二义性问题:
第二个加有二义性,问题在于,可以将 0 转换为 SmallInt 并使用 + 的 SmallInt 版本,也可以将 s3 转换为 int 值并使用 int 值上的内置加操作符。
## 小结:
成员操作符有一个隐式 this 指针,该指针一定是第一个操作数,即,一元操作符唯一的操作数,二元操作符的左操作数。
重载了 operator()(即,函数调用操作符)的类的对象,称为“函数对象”。这种对象通常用于定义与标准算法结合使用的谓词函数。
内置类型通过调用类构造函数进行隐式转换, 类类型可以通过重载转换操作符进行隐式转换。
必须注意避免设计对用户而言不明显的操作符和转换,而且应避免定义一个类型与另一类型之间的多个转换。
例如标准库为容器类定义了几个重载操作符。这些容器类定义了下标操作符以访问数据元素,定义了 * 和 -> 对容器迭代器解引用。
重载操作符的定义
1.重载操作符是具有特殊名称的函数:保留字 operator后接需定义的操作符号。像任意其他函数一样,重载操作符具有返回类型和形参表,如下语句(+操作符为非类成员操作符,需显示声明两个形参表明是左右操作数):Sales_item operator+(const Sales_item&, const Sales_item&);
声明了加号操作符,可用于将两个 Sales_item 对象“相加”并获得一个 Sales_item 对象的副本。
除了函数调用操作符之外,重载操作符的形参数目(包括成员函数的隐式
this 指针)与操作符的操作数数目相同。函数调用操作符可以接受任意数目的操作数。
可重载和不可重载的操作符
2.重载操作符必须具有至少一个类类型或枚举类型(第 2.7 节)的操作数。这条规则表明:重载操作符不能重新定义用于内置类型对象的操作符的含义。
3.重载后的操作符优先级和结合性是固定的
在 && 和 || 的重载版本中,两个操作数都要进行求值,而且对操作数的求值顺序不做规定。因此,4.重载 &&、|| 或逗号操作符不是一种好的做法。
5.类成员与非成员
作为类成员的重载函数,其形参看起来比操作数数目少1。作为成员函数的操作符有一个隐含的 this 形参,限定为第一个操作数。
类成员操作符:
Sales_item& Sales_item::operator+=(const Sales_item&);
非类成员操作符
Sales_item operator+(const Sales_item&, const Sales_item&);
6.操作符重载和友元关系
操作符定义为非成员函数时,通常将它们设置为所操作类的友元函数,以便该操作符函数可以访问该类的私有成员。
但若无需访问类私有成员时,可不必设为友元函数。
类中声明==操作符为友元函数:
class Sales_item { friend bool operator==(const Sales_item&, const Sales_item&); //... }
在函数中可以直接访问Sales_item的私有成员:
inline bool operator==(const Sales_item &lhs, const Sales_item &rhs) { // must be made a friend of Sales_item return lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue && lhs.same_isbn(rhs); }
7.使用重载操作符
使用重载操作符的方式,与内置类型操作数上使用操作符的方式一样。
cout << item1 + item2 << endl;
这个表达式隐式调用为 Sales_items 类而定义的 operator+。
也可以像调用普通函数一样调用重载操作符函数,指定函数并传递适当类型适当数目的形参(下面调用了一个非成员操作符+):
cout << operator+(item1, item2) << endl;
调用成员操作符函数:
item1 += item2; item1.operator+=(item2);
第一种情况下,使用表达式语法隐式调用重载操作符函数:第二种情况下,在 item1 对象上调用成员操作符函数。
重载操作符的设计
通常不重载具有内置含义的操作符
赋值操作符、取地址操作符和逗号操作符对类类型操作数有默认含义。如果没有特定重载版本,编译器就自己定义以下这些操作符。• 合成赋值操作符(第 13.2节)进行逐个成员赋值:使用成员自己的赋值:使用成员自己的赋值操作依次对每个成员进行赋值。
• 默认情况下,取地址操作符(&)和逗号操作符(,)在类类型对象上的执行,与在内置类型对象上的执行一样。取地址操作符返回对象的内存地址,逗号操作符从左至右计算每个表达式的值,并返回最右边操作数的值。
• 内置逻辑与(&&)和逻辑或(||)操作符使用短路求值(第 5.2节)。如果重新定义该操作符,将失去操作符的短路求值特征。
大多数操作符对类对象没有意义
为类设计操作符,最好的方式是首先设计类的公用接口。定义了接口之后,就可以考虑应将哪些操作符定义为重载操作符。例如:• 相等测试操作应使用 operator==。
• 一般通过重载移位操作符进行输入和输出。
• 测试对象是否为空的操作可用逻辑非操作符 operator! 表示。
复合赋值操作符
如果一个类有算术操作符(第 5.1 节)或位操作符(第 5.3 节),那么,提供相应的复合赋值操作符一般是个好的做法。例如,Sales_item 类定义了 += 操作符。选择成员或非成员实现
下面是一些原则:• 赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
• 像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
• 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
• 对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
输入和输出操作符
输出操作符 << 的重载
为了与 IO 标准库一致,操作符应接受 ostream& 作为第一个形参(像是经常用标准库的cout作为实参),对类类型 const对象的引用作为第二个形参,并返回对 ostream 形参的引用。重载输出操作符一般的简单定义如下:
ostream& operator <<(ostream& os, const ClassType &object) { // actual output of members os << // ... // return ostream object return os; }
第一个形参是对 ostream 对象的引用,在该对象上将产生输出。ostream 为非 const,因为写入到流会改变流的状态。
第二个形参一般应是对要输出的类类型的引用。该形参是一个引用以避免复制实参。
一般而言,输出操作符应输出对象的内容,进行最小限度的格式化,它们不应该输出换行符。
IO 操作符必须为非成员函数
若将该操作符定义为类的成员,左操作数将只能是该类型的对象。则只能这样使用:
Sales_item item; item << cout;
输入操作符 >> 的重载
输出操作符类似,输入操作符的第一个形参是一个引用,指向它要读的流,并且返回的也是对同一个流的引用。它的第二个形参是对要读入的对象的非 const 引用,该形参必须为非const,因为输入操作符的目的是将数据读到这个对象中。输入和输出操作符有如下区别:输入操作符必须处理错误和文件结束的可能性。
Sales_item 的输入操作符如下:
istream& operator>>(istream& in, Sales_item& s) { double price; in >> s.isbn >> s.units_sold >> price; // check that the inputs succeeded if (in) s.revenue = s.units_sold * price; else s = Sales_item(); // input failed: reset object to default state return in; }
输入期间的错误
任何读操作都可能因为提供的值不正确而失败。例如,读入 isbn 之后,输入操作符将期望下两项是数值型数据。如果输入非数值型数据,这次的读入以及流的后续使用都将失败。任何读入都可能碰到输入流中的文件结束或其他一些错误。
处理输入错误
如果输入操作符检测到输入失败了,则确保对象处于可用和一致的状态是个好做法。在Sales_item的输入操作符中,如果发生了错误,就将形参恢复为空 Sales_item 对象。算术操作符和关系操作符
一般而言,将算术和关系操作符定义为非成员函数,像下面给出的Sales_item 加法操作符一样:
Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs) { Sales_item ret(lhs); // copy lhs into a local object that we'll return ret += rhs; // add in the contents of rhs return ret; // return ret by value }
加法操作符并不改变操作符的状态,操作符是对 const 对象的引用;相反,它产生并返回一个新的 Sales_item对象,该对象初始化为 lhs 的副本。
算术操作符通常产生一个新值,该值是两个操作数的计算结果,它不同于任一操作数且在一个局部变量中计算,返回对那个变量的引用是一个运行时错误。
相等操作符
C++ 中的类使用相等操作符表示对象是等价的。即,它们通常比较每个数据成员,如果所有对应成员都相同,则认为两个对象相等。
设计原则:
• 如果类定义了 == 操作符,该操作符的含义是两个对象包含同样的数据。
• 如果类具有一个操作,能确定该类型的两个对象是否相等,通常将该函数定义为 operator== 而不是创造命名函数。用户将习惯于用 == 来比较对象,而且这样做比记住新名字更容易。
• 如果类定义了 operator==,它也应该定义operator!=。用户会期待如果可以用某个操作符,则另一个也存在。
关系操作符
但是,如果将 operator< 仅定义为对 isbn 的比较,该定义将与前面 == 的定义不相容。(相同isbn的对象两者都不小于对方,一般认定为相等)因为 < 的逻辑定义与 == 的逻辑定义不一致,所以根本不定义 < 会更好。
赋值操作符
类赋值操作符接受类类型形参,通常,该形参是对类类型的 const 引用,但也可以是类类型或对类类型的非 const 引用。如果没有定义这个操作符,则编译器将合成它。类赋值操作符必须是类的成员,以便编译器可以知道是否需要合成一个。
赋值操作符可以重载。无论形参为何种类型,赋值操作符必须定义为成员函数,这一点与复合赋值操作符有所不同。
class string { public: string& operator=(const string &); // s1 = s2; string& operator=(const char *); // s1 = "str"; string& operator=(char); // s1 = 'c'; // .... };
赋值必须返回对 *this 的引用
因为赋值返回一个引用,就不需要创建和撤销结果的临时副本。返回值通常是左操作数的引用。下标操作符
可以从容器中检索单个元素的容器类一般会定义下标操作符,即 []。下标操作符必须定义为类成员函数。
提供读写访问
定义下标操作符比较复杂的地方在于,它在用作赋值的左右操作符数时都应该能表现正常。下标操作符出现在左边,必须生成左值,可以指定引用作为返回类型而得到左值。只要下标操作符返回引用,就可用作赋值的任意一方。
可以对 const 和非 const 对象使用下标也是个好主意。应用于 const 对象时,返回值应为 const 引用,因此不能用作赋值的目标。
类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用。
int &operator[] (const size_t); const int &operator[] (const size_t) const;
成员访问操作符
为了支持指针型类,例如迭代器,C++语言允许重载解引用操作符(*)和箭头操作符(->))。箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员,但将它作为成员一般也是正确的。
支持指针操作
重载解引用操作符(*)和箭头操作符(->))可以帮助实现智能指针类(第 13.5.1 节),来对多个对象进行计数。class ScreenPtr { public: ScreenPtr(Screen *p): ptr(new ScrPtr(p)) { } ScreenPtr(const ScreenPtr &orig): ptr(orig.ptr) { ++ptr->use; } Screen &operator*() { return *ptr->sp; } //->优先级大于* Screen *operator->() { return ptr->sp; } const Screen &operator*() const { return *ptr->sp; } const Screen *operator->() const { return ptr->sp; } private: ScrPtr *ptr; // points to use-counted ScrPtr class };
对这个ScreenPtr类对象使用解引用操作符( * )和箭头操作符(->)时,可以直接操作到实际的对象。
使用:
ScreenPtr ps(new Screen(4,4));
ps的ptr成员指向一个ScrPtr对象,然后这个对象指向上述的Screen对象
重载解引用操作符
解引用操作符是个一元操作符。在这个类中,解引用操作符定义为成员,因此没有显式形参,该操作符返回对 ScreenPtr 所指向的 Screen 的引用。像下标操作符一样,我们需要解引用操作符的 const 和非 const 版本。它们的区别在于返回类型:const 成员返回 const 引用以防止用户改变基础对象。
重载箭头操作符
它可能表现得像二元操作符一样:接受一个对象和一个成员名。对对象解引用以获取成员。不管外表如何,箭头操作符不接受显式形参。这里没有第二个形参,因为 ->的右操作数不是表达式,相反,是对应着类成员的一个标识符。没有明显可行的途径将一个标识符作为形参传递给函数,相反,由编译器处理获取成员的工作。
使用重载箭头
可以这样使用 ScreenPtr 对象访问 Screen 对象的成员:ScreenPtr p(&myScreen); // copies the underlying Screen p->display(cout);
重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。
自增操作符和自减操作符
自增(++)和自减(–)操作符经常由诸如迭代器这样的类实现,这样的类提供类似于指针的行为来访问序列中的元素。定义前自增/前自减操作符
CheckedPtr& CheckedPtr::operator++() { if (curr == end) throw out_of_range ("increment past the end of CheckedPtr"); ++curr; // advance current state return *this; }
这个自增操作符根据 end 检查 curr,从而确保用户不能将 curr 增量到超过数组的末端。如果 curr 增量到超过 end,就抛出一个 out_of_range 异常;否则,将 curr 加 1 并返回对象引用。
区别操作符的前缀和后缀形式
同时定义前缀式操作符和后缀式操作符存在一个问题:它们的形参数目和类型相同,普通重载不能区别所定义的前缀式操作符还是后缀式操作符。为了解决这一问题,后缀式操作符函数接受一个额外的(即,无用的)int 型形参。使用后缀式操作符进,编译器提供 0 作为这个形参的实参。
后缀式操作符可以这样实现:
// postfix: increment/decrement object but return unchanged value CheckedPtr CheckedPtr::operator++(int) { CheckedPtr ret(*this); // save current value ++*this; // advance one element, checking the increment return ret; // return saved state }
操作符的后缀式必须记住对象在加 1/减 1之前的当前状态。这些操作符定义了一个局部 CheckedPtr对象,将它初始化为 *this 的副本,即 ret是这个对象当前状态的副本。接着将原对象增量后,返回未增量的的对象副本ret。
显式调用前缀式操作符
如果想要使用函数调用来调用后缀式操作符,必须给出一个整型实参值,而前缀不用:
parr.operator++(0); // call postfix operator++ parr.operator++(); // call prefix operator++
调用操作符和函数对象
可以为类类型的对象重载函数调用操作符。一般为表示操作的类重载调用操作符。例如,可以定义名为 absInt 的结构,该结构封装将 int 类型的值转换为绝对值的操作:struct absInt { int operator() (int val) { return val < 0 ? -val : val; } };
这个类定义了一个操作:函数调用操作符,该操作符有一个形参
并返回形参的绝对值。
通过为类类型的对象提供一个实参表而使用调用操作符,所用的方式看起来像一个函数调用:
int i = -42; absInt absObj; // object that defines function call operator unsigned int ui = absObj(i); // calls absInt::operator(int)
函数调用操作符必须声明为成员函数。一个类可以定义函数调
用操作符的多个版本,由形参的数目或类型加以区别。
定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象。
将函数对象用于标准库算法
函数对象经常用作通用算法的实参。bool GT6(const string &s) { return s.size() >= 6; }
使用 GT6 函数 作为传给 count_if 算法的实参,以计算使 GT6 返回 true 的单词的数目:
vector<string>::size_type wc = count_if(words.begin(), words.end(), GT6);
直接使用函数名作参数等效于取函数地址 :>6
函数对象可以比函数更灵活
GT6 函数将 6 这个数字固化在它的定义中。count_if 算法运行只用一个形参且返回 bool 的函数。通过将 GT6 定义为带函数调用成员类(GT_cls),可以获得所需的灵活性。
class GT_cls { public: GT_cls(size_t val = 0): bound(val) { } bool operator()(const string &s) { return s.size() >= bound; } private: std::string::size_type bound; };
使用 GT_cls 函数对象:
cout << count_if(words.begin(), words.end(), GT_cls(6)) << " words 6 characters or longer" << endl;
向GT_cls构造函数传递不同的参数可以进行不同的比较。
标准库定义的函数对象
这些标准库函数对象类型是在 functional 头文件中定义的。每个类表示一个给定操作符
每个标准库函数对象类表示一个操作符,即,每个类都定义了应用命名操作的调用操作符。plus 模板中的调用操作符对两个操作数应用 + 运算。
有两个一元函数对象类:一元减(negate))和逻辑非(logical_not))。其余的标准库函数对象都是表示二元操作符的二元函
数对象类。
表示操作数类型的模板类型
每个函数对象类都是一个类模板,我们需要为该模板提供一个类型。plus<int> intAdd; negate<int> intNegate; int sum = intAdd(10, 20); // sum = 30 sum = intAdd(10, intNegate(10)); // sum = 0
在算法中使用标准库函数
sort(svec.begin(), svec.end(), greater<string>());
传递函数对象greater,该类将产生一个调用操作符,调用基础对象的大于操作符。
函数对象的函数适配器
标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。绑定器,是一种函数适配器,它通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象。
求反器,是一种函数适配器,它*将谓词函数对象的真值求反
标准库定义了两个绑定器适配器:bind1st 和 bind2nd。
bind1st 将给定值绑定到二元函数对象的第一个实参,bind2nd将给定值绑定到二元函数对象的第二个实参。
count_if(vec.begin(), vec.end(), bind2nd(less_equal<int>(), 10));
该适配器bind2nd返回一个函数对象less_equal,该对象用 10 作右操作数应用 <= 操作符。
两个求反器:not1 和 not2。你可能已经想到的,not1 将一元函数对象的真值求反,not2 将二元函数对象的真值求反。
count_if(vec.begin(), vec.end(), not1(bind2nd(less_equal<int>(), 10)));
果是对不 <= 10 的
那些元素进行计数。
转换与类类型
在第 12.4.4 节介绍过,可用一个实参调用的非 explicit 构造函数定义一个隐式转换,转换为类类型。当提供了实参类型的对象而需要一个类类型的对象时,编译器将使用该转换。除了定义到类类型的转换之外,我们还可以定义从类类型的转换。即,我们可以定义转换操作符,给定类类型的对象,该操作符将产生其他类型的对象。
假定想要定义一个名为 SmallInt的类,该类实现安全小整数,这个类将使我们能够定义对象以保存与 8 位 unsigned char 同样范围的值,即,0 到 255。这个类可以捕获下溢和上溢错误,因此使用起来比内置 unsigned char 更安全。
我们希望这个类定义 unsigned char支持的所有操作。具体而言,我们想定义 5个算术操作符(+、-、*、/、%)及其对应的复合赋值操作符,4个关系操作符(<、<=、>、>=),以及相等操作符(==、!=)。
支持混合类型表达式
int operator+(int, const SmallInt&); int operator+(const SmallInt&, int); SmallInt operator+(const SmallInt&, const SmallInt&);
因为存在从任意算术类型到 int 的转换,这三个函数可以涵盖支持
SmallInt 对象的混合模式使用的要求。但是,它不能适当处理浮点类型等非int型的混合模式操作。
转换减少所需操作符的数目
C++ 提供了一种机制,利用这种机制,一个类可以定义自己的转换,应用于其类类型对象。SmallInt si(3); si + 3.14159; // convert si to int, then convert to double
将 对象si 转换为 int 值。
将所得 int 结果转换为 double 值并与双精度字面值常量 3.14159 相加,得到 double 值。
转换操作符
转换操作符是一种特殊的类成员函数。它定义将类类型值转变为其他类型值的转换。转换操作符在类定义体内声明,在保留字 operator 之后跟着转换的目标类型:class SmallInt { public: SmallInt(int i = 0): val(i) { if (i < 0 || i > 255) throw std::out_of_range("Bad SmallInt initializer"); } operator int() const { return val; } private: std::size_t val; };
转换函数必须是成员函数,不能指定返回类型(例如: int operator int(); ),并且形参表必须为空。
转换函数一般不应该改变被转换的对象,通常应定义为 const 成员。
使用类类型转换
只要存在转换,编译器将在可以使用内置转换的地方自动调用它。• 在表达式中:
SmallInt si; double dval; si >= dval // si converted to int and then convert to double
• 在条件中:
if (si) // si converted to int and then con vert to bool
• 在显式类型转换中:
int ival; SmallInt si = 3.541; // instruct compiler to cast si to int ival = static_cast<int>(si) + 3;
只能应用一个类类型转换
假设Integral对象能转换成SmallInt对象,SmallInt对象能转换成int型对象。但是不能完成从 Integral 到 int 的直接转换,因为它需要两次类类型转换。
标准转换可放在类类型转换之前
void calc(SmallInt); short sobj; SmallInt(int) calc(sobj);
为了调用函数 calc(),应用标准转换将 dobj 从 double 类型转换
为 int 类型,然后调用构造函数 SmallInt(int) 将转换结果转换为 SmallInt 类型。
实参匹配和转换
SmallInt到int 的转换使 SmallInt 的用户能够对 SmallInt 对象使用所有算术和关系操作符,而且,用户可以安全编写将 SmallInt 和其他算术类型混合使用的表达式。定义一个转换操作符就能代替定义 48 个(或更多)重载操作符,类实现者的工作就简单多了。实参匹配和多个转换操作符
class SmallInt { public: SmallInt(int = 0); SmallInt(double); operator int() const { return val; } operator double() const { return val; } private: std::size_t val; };
考虑最简单的调用非重载函数的情况:
void compute(int); void fp_compute(double); void extended_compute(long double); SmallInt si; compute(si); // SmallInt::operator int() const fp_compute(si); // SmallInt::operator double() const extended_compute(si); // error: ambiguous
最后一个对 extended_compute的调用有二义性。可以使用任一转换函数,但每个都必须跟上一个标准转换来获得 long double,因此,没有一个转换比其他的更好,调用具有二义性
实参匹配和构造函数转换
void manip(const SmallInt &); double d; int i; long l; manip(d); // ok: use SmallInt(double) to convert the argument manip(i); // ok: use SmallInt(int) to convert the argument manip(l); // error: ambiguous
第三个调用具有二义性。没有构造函数完全匹配于long。使用每一个构造函数之前都需要对实参进行转换:
1. 标准转换(从 long 到 double)后跟 SmallInt(double) 的构造函数。
2. 标准转换(从 long 到 int)后跟 SmallInt(int)。
避免二义性最好的方法是避免编写互相提供隐式转换的成对的类。
重载确定和类的实参
正如我们看到的,在需要转换函数的实参时,编译器自动应用类的转换操作符或构造函数。因此,应该在函数确定期间考虑类转换操作符。函数重载确定(第 7.8.2 节)由三步组成:1. 确定候选函数集合:这些是与被调用函数同名的函数。
2. 选择可行的函数:这些是形参数目和类型与函数调用中的实参相匹配的候选函数。选择可行函数时,如果有转换操作,编译器还要确定需要哪个转换操作来匹配每个形参。
3. 选择最佳匹配的函数。
显式强制转换消除二义性
compute(static_cast<int>(si)); // ok: convert and call compute(int)
标准转换和构造函数
class SmallInt { public: SmallInt(int = 0); }; class Integral { public: Integral(int = 0); }; void manip(const Integral&); void manip(const SmallInt&); manip(10); // error: ambiguous
Integral 和 SmallInt 这两个类都提供接受 int 参数的构造函数,其中任意一个构造函数都可以与 manip的一个版本相匹配,因此,函数调用有二义性。
显式构造函数调用消除二义性
manip(SmallInt(10)); // ok: call manip(SmallInt) manip(Integral(10)); // ok: call manip(Integral)
重载、转换和操作符
重载操作符就是重载函数。使用与确定重载函数调用一样的过程来确定将哪个操作符(内置的还是类类型的)应用于给定表达式。转换和操作符
下面几条经验规则:1. 不要定义相互转换的类,即如果类 Foo 具有接受类 Bar 的对象的构造函数,不要再为类 Bar 定义到类型 Foo 的转换操作符。
2. 避免到内置算术类型的转换。具体而言,如果定义了到算术类型的转换,则
o 不要定义接受算术类型的操作符的重载版本。如果用户需要使用这些操作符,转换操作符将转换你所定义的类型的对象,然后可以使用内置操作符。
o 不要定义转换到一个以上算术类型的转换。让标准转换提供到其他算术类型的转换。
转换可能引起内置操作符的二义性
operator+(const SmallInt&, const SmallInt&);
试图进行混合模式运算,将会遇到二义性问题:
SmallInt s1, s2; SmallInt s3 = s1 + s2; // ok: uses overloaded operator+ int i = s3 + 0; // error: ambiguous
第二个加有二义性,问题在于,可以将 0 转换为 SmallInt 并使用 + 的 SmallInt 版本,也可以将 s3 转换为 int 值并使用 int 值上的内置加操作符。
## 小结:
成员操作符有一个隐式 this 指针,该指针一定是第一个操作数,即,一元操作符唯一的操作数,二元操作符的左操作数。
重载了 operator()(即,函数调用操作符)的类的对象,称为“函数对象”。这种对象通常用于定义与标准算法结合使用的谓词函数。
内置类型通过调用类构造函数进行隐式转换, 类类型可以通过重载转换操作符进行隐式转换。
必须注意避免设计对用户而言不明显的操作符和转换,而且应避免定义一个类型与另一类型之间的多个转换。
相关文章推荐
- C与C++中定义结构体的不同方式
- 学习C语言结构体
- 两个类相互调用的问题
- C语言基础问题1 数据在内存上的表现形式
- 黑马程序员——IOS基础之C语言---C语言基础知识介绍
- HDU 1754
- makefile 适用中小项目,cpp & c --- wjd test
- C++的左值和右值
- C++第一章总结
- 【C语言】[变量]:变量在内存的存储位置
- Introduce to algorithm--------pseudo code to C/C++ code(chapter 12)
- 《转》c++学习路线与推荐书籍
- c++ sort的使用 数字排序
- [转载]值得推荐的C/C++框架和库
- c++ map的使用 出现次数最多的数
- C语言strcasecmp()函数:判断字符串是否相等(忽略大小写)
- c++类中的虚函数
- 括号匹配 c++
- c++语言常用转义序列符号
- C++实现python标准库中的Counter