首页 > 代码库 > C++primer第十四章 重载操作符与转换
C++primer第十四章 重载操作符与转换
这些标准库的类型具有相同的操作符,使用它们就像使用内置数组和指针一样。允许程序使用表达式而不是命名函数,可以使编写和阅读程序容易得多。将
cout << "The sum of " << v1 << " and " << v2 << " is " << v1 + v2 << endl;
和以下更为冗长的代码相比较就能够看到。如果 IO 使用命名函数,类似下面的代码将无法避免:
// hypothetical expression if IO used named functionscout.print("The sum of ").print(v1).print(" and ").print(v2).print(" is ").print(v1+ v2).print("\n").flush();
14.1. 重载操作符的定义
重载操作符是具有特殊名称的函数:保留字 operator 后接需定义的操作符号。像任意其他函数一样,重载操作符具有返回类型和形参表,如下语句:
Sales_item operator+(const Sales_item&, const Sales_item&);
声明了加号操作符,可用于将两个 Sales_item 对象“相加”并获得一个Sales_item 对象的副本。
重载的操作名
可以重载的操作符
+ | - | * | / | & | ^ |
& | | | ~ | ! | , | = |
< | > | <= | >= | ++ | -- |
<< | >> | == | != | && | || |
+= | -= | %= | *= | |^= | &= |
|= | *= | <<= | >>= | [] | () |
-> | ->* | new | new[] | delete | delete[] |
不能重载的操作符
:: | .* | . | ?: |
重载操作符必须具有一个类类型操作数
用于内置类型的操作符,其含义不能改变。例如,内置的整型加号操作符不能重定义:
// error: cannot redefine built-in operator for intsint operator+(int, int);
优先级和结合性是固定的
操作符的优先级(第 5.10.1 节)、结合性或操作数目不能改变。不管操作数的类型和操作符的功能定义如何,表达式
x == y +z;
总是将实参 y 和 z 绑定到 operator+,并且将结果用作 operator== 右操作数。
不再具备短路求值特性
重载操作符并不保证操作数的求值顺序,尤其是,不会保证内置逻辑 AND、逻辑 OR(第 5.2 节)和逗号操作符(第 5.9 节)的操作数求值。在 && 和 ||的重载版本中,两个操作数都要进行求值,而且对操作数的求值顺序不做规定。因此,重载 &&、|| 或逗号操作符不是一种好的做法。
类成员与非成员
大多数重载操作符可以定义为普通非成员函数或类的成员函数。
作为类成员的重载函数,其形参看起来比操作数数目少 1。作为成员函数的操作符有一个隐含的 this 形参,限定为第一个操作数。
重载一元操作符如果作为成员函数就没有(显式)形参,如果作为非成员函数就有一个形参。类似地,重载二元操作符定义为成员时有一个形参,定义为非成员函数时有两个形参。
一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员(在本章的后面部分,将给出操作符可以定义为非成员的两个原因。)
操作符重载和友元关系
操作符定义为非成员函数时,通常必须将它们设置为所操作类的友元
class Sales_item { friend std::istream& operator>>(std::istream&, Sales_item&); friend std::ostream& operator<<(std::ostream&, const Sales_item&);public: Sales_item& operator+=(const Sales_item&);};Sales_item operator+(const Sales_item&, const Sales_item&);
使用重载操作符
使用重载操作符的方式,与内置类型操作数上使用操作符的方式一样。
调用成员操作符函数与调用任意其他函数是一样的:指定运行函数的对象,然后使用点或箭头操作符获取希望调用的函数,同时传递所需数目和类型的实参。对于二元成员操作符函数的情况,我们必须传递一个操作数:
item1 += item2; // expression based "call"item1.operator+=(item2); // equivalent call to member operator function
两个语句都将 item2 的值加至 item1。第一种情况下,使用表达式语法隐式调用重载操作符函数:第二种情况下,在 item1 对象上调用成员操作符函数。
14.1.1. 重载操作符的设计
设计类的时候,需要记住一些有用的经验原则,可以有助于确定应该提供哪些重载操作符(如果需要提供)。
不要重载具有内置含义的操作符
赋值操作符、取地址操作符和逗号操作符对类类型操作数有默认含义。如果没有特定重载版本,编译器就自己定义以下这些操作符。
• 合成赋值操作符(第 13.2 节)进行逐个成员赋值:使用成员自己的赋值:使用成员自己的赋值操作依次对每个成员进行赋值。
• 默认情况下,取地址操作符(&)和逗号操作符(,)在类类型对象上的执行,与在内置类型对象上的执行一样。取地址操作符返回对象的内存地址,逗号操作符从左至右计算每个表达式的值,并返回最右边操作数的值。
• 内置逻辑与(&&)和逻辑或(||)操作符使用短路求值(第 5.2 节)。如果重新定义该操作符,将失去操作符的短路求值特征。
重载逗号、取地址、逻辑与、逻辑或等等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义。
大多数操作符对类对象没有意义
为类设计操作符,最好的方式是首先设计类的公用接口。定义了接口之后,就可以考虑应将哪些操作符定义为重载操作符。
复合赋值操作符
如果一个类有算术操作符(第 5.1 节)或位操作符(第 5.3 节),那么,提供相应的复合赋值操作符一般是个好的做法。
相等和关系操作符
将要用作关联容器键类型的类应定义 < 操作符。关联容器默认使用键类型的 < 操作符。即使该类型将只存储在顺序容器中,类通常也应该定义相等(==)和小于(<)操作符,理由是许多算法假定这个操作符存在。例如 sort 算法使用 < 操作符,而 find 算法使用 == 操作符。
选择成员或非成员实现
下面是一些指导原则,有助于决定将操作符设置为类成员还是普通非成员函数:
• 赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
• 像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
• 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
• 对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
14.2. 输入和输出操作符
支持 I/O 操作的类所提供的 I/O 操作接口,一般应该与标准库 iostream为内置类型定义的接口相同,因此,许多类都需要重载输入和输出操作符。
14.2.1. 输出操作符<< 的重载
为了与 IO 标准库一致,操作符应接受 ostream& 作为第一个形参,对类类型 const 对象的引用作为第二个形参,并返回对ostream 形参的引用。
// general skeleton of the overloaded output operatorostream& operator <<(ostream& os, const ClassType &object){ // any special logic to prepare object // actual output of membersos << // ... // return ostream object return os;}
用于内置类型的输出操作符所做格式化很少,并且不输出换行符。由于内置类型的这种既定处理,用户预期类输出操作符也有类似行为。通过限制输出操作符只输出对象的内容,如果需要执行任意额外的格式化,我们让用户决定该如何处理。尤其是,输出操作符不应该输出换行符,如果该操作符输出换行符,则用户就不能将说明文字与对象输出在同一行上。尽量减少操作符所做格式化,让用户自己控制输出细节。
IO 操作符必须为非成员函数
我们不能将该操作符定义为类的成员,否则,左操作数将只能是该类类型的对象:
// if operator<< is a member of Sales_itemSales_item item;item << cout;
14.2.2. 输入操作符>> 的重载
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;}
输入期间的错误
Sales_item 的输入操作符将读入所期望的值并检查是否发生错误。可能发生的错误包括如下种类:
1. 任何读操作都可能因为提供的值不正确而失败。例如,读入 isbn 之后,输入操作符将期望下两项是数值型数据。如果输入非数值型数据,这次的读入以及流的后续使用都将失败。
2. 任何读入都可能碰到输入流中的文件结束或其他一些错误。
如果这些读入有一个失败了,则 price 可能没有初始化。因此,在使用price 之前,我们需要检查输入流是否仍有效。如果有效,就进行计算并将结果存储到 revenue 中;如果出现了错误,我们不用关心是哪个输入失败了,相反,我们将整个对象复位,就好像它是一个空 Sales_item 对象,具体做法是创建一个新的、未命名的、用默认构造的 Sales_item 对象并将它赋值给 s。赋值之后,s 的 isbn 成员是一个空 string,它的 revenue 和 units_sold 成员为 0。
处理输入错误
如果输入操作符检测到输入失败了,则确保对象处于可用和一致的状态是个好做法。如果对象在错误发生之前已经写入了部分信息,这样做就特别重要。
在这个操作符中,如果发生了错误,就将形参恢复为空 Sales_item 对象,以避免给它一个无效状态。用户如果需要输入是否成功,可以测试流。即使用户忽略了输入可能错误,对象仍处于可用状态——它的成员都已经定义。类似地,对象将不会产生令人误解的结果——它的数据是内在一致的。
14.3. 算术操作符和关系操作符
一般而言,将算术和关系操作符定义为非成员函数,像下面给出的Sales_item 加法操作符一样:
Sales_itemoperator+(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 的副本。我们使用 Sales_item 的复合赋值操作符来加入 rhs 的值。
14.3.1. 相等操作符
通常,C++ 中的类使用相等操作符表示对象是等价的。即,它们通常比较每个数据成员,如果所有对应成员都相同,则认为两个对象相等。
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);}inline bool operator!=(const Sales_item &lhs, const Sales_item &rhs){ return !(lhs == rhs); // != defined in terms of operator==}
• 如果类定义了 == 操作符,该操作符的含义是两个对象包含同样的数据。
• 如果类具有一个操作,能确定该类型的两个对象是否相等,通常将该函数定义为 operator== 而不是创造命名函数。用户将习惯于用 == 来比较对象,而且这样做比记住新名字更容易。
• 如果类定义了 operator==,它也应该定义 operator!=。用户会期待如果可以用某个操作符,则另一个也存在。
• 相等和不操作符一般应该相互联系起来定义,让一个操作符完成比较对象的实际工作,而另一个操作符只是调用前者。
14.3.2. 关系操作符
定义了相等操作符的类一般也具有关系操作符。尤其是,因为关联容器和某些算法使用小于操作符,所以定义 operator< 可能相当有用。
14.4. 赋值操作符
类赋值操作符接受类类型形参,通常,该形参是对类类型的 const 引用,但也可以是类类型或对类类型的非 const 引用。如果没有定义这个操作符,则编译器将合成它。类赋值操作符必须是类的成员,以便编译器可以知道是否需要合成一个。
标准库的类 string 定义了 3 个赋值操作符:除了接受const string& 作为右操作数的类赋值操作符之外,类还定义了接受 C 风格字符串或 char 作为右操作数的赋值操作符
string car ("Volks");car = "Studebaker"; // string = const char*string model;model = ‘T‘; // string = char
// illustration of assignment operators for class stringclass string {public: string& operator=(const string &); // s1 = s2; string& operator=(const char *); // s1 = "str"; string& operator=(char); // s1 = ‘c‘; // ....};
赋值必须返回对 *this 的引用
而且,因为赋值返回一个引用,就不需要创建和撤销结果的临时副本。返回值通常是左操作数的引用,
14.5. 下标操作符
可以从容器中检索单个元素的容器类一般会定义下标操作符,即operator[]。标准库的类 string 和 vector 均是定义了下标操作符的类的例子。
提供读写访问
可以对 const 和非 const 对象使用下标也是个好主意。应用于 const 对象时,返回值应为 const 引用,因此不能用作赋值的目标。
类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用
原型下标操作符
下面的类定义了下标操作符。为简单起见,假定 Foo 所保存的数据存储在一个 vector<int>: 中:
class Foo {public: int &operator[] (const size_t); const int &operator[] (const size_t) const; // other interface membersprivate: vector<int> data; // other member data and private utility functions};
下标操作符本身可能看起来像这样:
int& Foo::operator[] (const size_t index){ return data[index]; // no range checking on index}const int& Foo::operator[] (const size_t index) const{ return data[index]; // no range checking on index}
14.6. 成员访问操作符
为了支持指针型类,例如迭代器,C++ 语言允许重载解引用操作符(*)和箭头操作符(->))。
C++primer第十四章 重载操作符与转换