重载操作符的作用:
通过操作符重载,程序员能够针对类类型的操作数定义不同的操作符版本。程序用移位操作符(>> 和 <<)进行输入输出,用加号操作符(+)将两个 Sales_items 相加。
通过操作符重载,可以定义大多数操作符,使它们用于类类型对象。明智地使用操作符重载可以使类类型的使用像内置类型一样直观。标准库为容器类定义了几个重载操作符。这些容器类定义了下标操作符以访问数据元素,定义了 * 和 -> 对容器迭代器解引用。这些标准库的类型具有相同的操作符,使用它们就像使用内置数组和指针一样。允许程序使用表达式而不是命名函数,可以使编写和阅读程序容易得多。
cout << "The sum of " << v1 << " and " << v2 << " is " << v1 + v2 << endl;
和以下更为冗长的代码相比较就能够看到。如果 IO 使用命名函数,类似下面的代码将无法避免:
// hypothetical expression if IO used named functions cout.print("The sum of ").print(v1). print(" and ").print(v2).print(" is "). print(v1 + v2).print(" ").flush();
重载操作符是具有特殊名称的函数:保留字 operator 后接需定义的操作符号。像任意其他函数一样,重载操作符具有返回类型和形参表,如下语句:
Sales_item operator+(const Sales_item&, const Sales_item&);
声明了加号操作符,可用于将两个 Sales_item 对象“相加”并获得一个 Sales_item 对象的副本。除了函数调用操作符之外,重载操作符的形参数目(包括成员函数的隐式 this 指针)与操作符的操作数数目相同。函数调用操作符可以接受任意数目的操作数。
可重载的操作符:
不可重载的操作符:
通过连接其他合法符号可以创建新的操作符。例如,定义一个 operator** 以提供求幂运算是合法的。
- 重载操作符必须有一个类类型操作数
用于内置类型的操作符,其含义不能改变。例如,内置的整型加号操作符不能重定义:
// error: cannot redefine built-in operator for ints int operator+(int, int);
也不能为内置数据类型重定义加号操作符。例如,不能定义接受两个数组类型操作数的 operator+。重载操作符必须具有至少一个类类型或枚举类型的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含义。
- 优先级和结合性是固定的
操作符的优先级、结合性或操作数目不能改变。不管操作数的类型和操作符的功能定义如何,表达式
x == y +z;
总是将实参 y 和 z 绑定到 operator+,并且将结果用作 operator== 右操作数。有四个符号(+, -, * 和 &)既可作一元操作符又可作二元操作符,这些操作符有的在其中一种情况下可以重载,有的两种都可以,定义的是哪个操作符由操作数数目控制。除了函数调用操作符 operator() 之外,重载操作符时使用默认实参是非法的。
重载操作符并不保证操作数的求值顺序,尤其是,不会保证内置逻辑 AND、逻辑 OR和逗号操作符的操作数求值。在 && 和 || 的重载版本中,两个操作数都要进行求值,而且对操作数的求值顺序不做规定。因此,重载 &&、|| 或逗号操作符不是一种好的做法。
- 类成员与非成员
大多数重载操作符可以定义为普通非成员函数或类的成员函数。作为类成员的重载函数,其形参看起来比操作数数目少 1。作为成员函数的操作符有一个隐含的 this 形参,限定为第一个操作数。重载一元操作符如果作为成员函数就没有(显式)形参,如果作为非成员函数就有一个形参。类似地,重载二元操作符定义为成员时有一个形参,定义为非成员函数时有两个形参。类 Sales_item 中给出了成员和非成员二元操作符的良好例子。我们知道该类有一个加号操作符。因为它有一个加号操作符,所以也应该定义一个复合赋值(+=)操作符,该操作符将一个 Sales_item 对象的值加至另一个 Sales_item 对象。
一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员:
// member binary operator: left-hand operand bound to implicit this pointer Sales_item& Sales_item::operator+=(const Sales_item&); // nonmember binary operator: must declare a parameter for each operand Sales_item operator+(const Sales_item&, const Sales_item&);
加和复合赋值都是二元操作符,但这些函数定义了不同数目的形参,差异的原因在于 this 指针。当操作符为成员函数,this 指向左操作数,因此,非成员 operator+ 定义两个形参,都引用 const Sales_item 对象。即使复合赋值是二元操作符,成员复合赋值操作符也只接受一个(显式的)形参。使用操作符时,一个指向左操作数的指针自动绑定到 this,而右操作符限定为函数的唯一形参。复合赋值返回一个引用而加操作符返回一个 Sales_item 对象,这也没什么。当应用于算术类型时,这一区别与操作符的返回类型相匹配:加返回一个右值,而复合赋值返回对左操作数的引用。
- 操作符重载和友元关系
操作符定义为非成员函数时,通常必须将它们设置为所操作类的友元。在这种情况下,操作符通常需要访问类的私有部分。Sales_item 类也是说明为何有些操作符需要设置为友元的一个好例子。它定义了一个成员操作符,并且有三个非成员操作符。有两个非成员操作符需要访问私有数据成员,需声明为友元:
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&);
输入和输出操作符需要访问 private 数据不会令人惊讶,毕竟,它们的作用是读入和写出那些成员。另一方面,不需要将加操作符设置为友元,它可以用 public 成员 operator+= 实现。
- 使用重载操作符
使用重载操作符的方式,与内置类型操作数上使用操作符的方式一样。假定 item1 和 item2 是 Sales_item 对象,可以打印它们的和,就像打印两个 int 的和一样:
cout << item1 + item2 << endl;
这个表达式隐式调用为 Sales_items 类而定义的 operator+。也可以像调用普通函数一样调用重载操作符函数,指定函数并传递适当类型适当数目的形参:
// equivalent direct call to nonmember operator function cout << operator+(item1, item2) << endl;
这个调用与 item1 和 item2 相加的表达式等效。
调用成员操作符函数与调用任意其他函数是一样的:指定运行函数的对象,然后使用点或箭头操作符获取希望调用的函数,同时传递所需数目和类型的实参。对于二元成员操作符函数的情况,我们必须传递一个操作数:
item1 += item2; // expression based "call" item1.operator+=(item2); // equivalent call to member operator function
两个语句都将 item2 的值加至 item1。第一种情况下,使用表达式语法隐式调用重载操作符函数:第二种情况下,在 item1 对象上调用成员操作符函数。
#include <iostream> #include <string> using namespace std; class Sales_item { public: Sales_item() :product_name("UnNamed"), product_price(0){} Sales_item(string spn, int spp) :product_name(spn), product_price(spp){} Sales_item& operator+=(const Sales_item&); private: string product_name; int product_price; friend std::istream& operator>> (std::istream&, Sales_item&); friend std::ostream& operator<< (std::ostream&, const Sales_item&); }; Sales_item& Sales_item::operator+=(const Sales_item&si) { product_price += si.product_price; return *this; } Sales_item operator+(const Sales_item&si1, const Sales_item&si2) { /*int totalprice = si1.product_price + si2.product_price;*///非成员函数不能访问private Sales_item si("Cakes", 0); si += si1; si += si2; return si; } std::ostream& operator<<(std::ostream&out, const Sales_item&si){ out << si.product_name << ":" << si.product_price << endl; return out;//保证可以连续输出 } std::istream& operator>>(std::istream&in, Sales_item&si){ cout << "input price of "<<si.product_name << endl; in >> si.product_price; return in; } int main() { Sales_item si("Cake",15); Sales_item si2("Cake", 16); Sales_item si3("Cake", 17); Sales_item si4("Cake", 18); cout << si << endl; cin >> si; cout << si << endl; si += si2; cout << si << endl; cout << si3 + si4 << endl; system("pause"); return 0; }
输出结果:
- 重载操作符的设计
不要重载具有内置含义的操作符。赋值操作符、取地址操作符和逗号操作符对类类型操作数有默认含义。如果没有特定重载版本,编译器就自己定义以下这些操作符。//重载操作符的一个原因就是这个操作符对类类型没有操作。比如说“+”,把两个类相加,加号不知道该如何操作,所以需要给这两个类重载加号操作符。但是有些操作符已经有了对类的操作,这时就不必再给这个操作符定义类的操作了。
合成赋值操作符进行逐个成员赋值:使用成员自己的赋值:使用成员自己的赋值操作依次对每个成员进行赋值;默认情况下,取地址操作符(&)和逗号操作符(,)在类类型对象上的执行,与在内置类型对象上的执行一样。取地址操作符返回对象的内存地址,逗号操作符从左至右计算每个表达式的值,并返回最右边操作数的值;内置逻辑与(&&)和逻辑或(||)操作符使用短路求值。如果重新定义该操作符,将失去操作符的短路求值特征。
通过为给定类类型的操作数重定义操作符,可以改变这些操作符的含义。重载逗号、取地址、逻辑与、逻辑或等等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义。
有时我们需要定义自己的赋值运算。这样做时,它应表现得类似于合成操作符:赋值之后,左右操作数的值应是相同的,并且操作符应返回对左操作数的引用。重载的赋值运算应在赋值的内置含义基础上进行定制,而不是完全绕开。
大多数操作符对类对象没有意义。为类设计操作符,最好的方式是首先设计类的公用接口。定义了接口之后,就可以考虑应将哪些操作符定义为重载操作符。那些逻辑上可以映射到某个操作符的操作可以考虑作为候选的重载操作符。例如:相等测试操作应使用 operator==;一般通过重载移位操作符进行输入和输出;测试对象是否为空的操作可用逻辑非操作符 operator! 表示。
复合赋值操作符。如果一个类有算术操作符或位操作符,那么,提供相应的复合赋值操作符一般是个好的做法。例如,Sales_item 类定义了 + 操作符,逻辑上,它也应该定义 +=。不用说,操作符的行为应定义为与内置操作符一样:复合赋值的行为应与 + 之后接着 = 类似。
相等和关系操作符。将要用作关联容器键类型的类应定义 < 操作符。关联容器默认使用键类型的 < 操作符。即使该类型将只存储在顺序容器中,类通常也应该定义相等(==)和小于(<)操作符,理由是许多算法假定这个操作符存在。例如 sort 算法使用 < 操作符,而 find 算法使用 == 操作符。
如果类定义了相等操作符,它也应该定义不等操作符 !=。类用户会假设如果可以进行相等比较,则也可以进行不等比较。同样的规则也应用于其他关系操作符。如果类定义了 <,则它可能应该定义全部的四个关系操作符(>,>=,<,<=)。
- 选择成员或非成员实现
为类设计重载操作符的时候,必须选择是将操作符设置为类成员还是普通非成员函数。在某些情况下,程序员没有选择,操作符必须是成员;在另一些情况下,有些经验原则可指导我们做出决定。下面是一些指导原则,有助于决定将操作符设置为类成员还是普通非成员函数:
赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误;像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误;改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员;//自增自减是为了修改自身,所以必须有对自身操作的权限,那么设为成员函数就可以修改自身的private成员,这样设计师最好的。对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
- 输出操作符重载
为了与 IO 标准库一致,操作符应接受 ostream& 作为第一个形参,对类类型 const 对象的引用作为第二个形参,并返回对 ostream 形参的引用。重载输出操作符一般的简单定义如下:
// general skeleton of the overloaded output operator ostream& operator <<(ostream& os, const ClassType &object) { // any special logic to prepare object // actual output of members os << // ... // return ostream object return os; }
第一个形参是对 ostream 对象的引用,在该对象上将产生输出。ostream 为非 const,因为写入到流会改变流的状态。该形参是一个引用,因为不能复制 ostream 对象。第二个形参一般应是对要输出的类类型的引用。该形参是一个引用以避免复制实参。它可以是 const,因为(一般而言)输出一个对象不应该改变对象。使形参成为 const 引用,就可以使用同一个定义来输出 const 和非 const 对象。返回类型是一个 ostream 引用,它的值通常是输出操作符所操作的 ostream 对象。
ostream& operator<<(ostream& out, const Sales_item& s) { out << s.isbn << " " << s.units_sold << " " << s.revenue << " " << s.avg_price(); return out; }
输出 Sales_item,就需要输出它的三个数据成员以及计算得到的平均销售价格,每个成员用制表符间隔。输出值之后,该操作符返回对所写 ostream 对象的引用。
输出操作符格式化尽量少。关于输出,类设计者面临一个重要决定:是否格式化以及进行多少格式化。一般而言,输出操作符应输出对象的内容,进行最小限度的格式化,它们不应该输出换行符。
- IO操作符必须为非成员函数
我们不能将该操作符定义为类的成员,否则,左操作数将只能是该类类型的对象:
// if operator<< is a member of Sales_item Sales_item item; item << cout;//因为成员函数是类对象发起调用,所以对象会写到前面。
如果想要支持正常用法,则左操作数必须为 ostream 类型。这意味着,如果该操作符是类的成员,则它必须是 ostream 类的成员,然而,ostream 类是标准库的组成部分,我们(以及任何想要定义 IO 操作符的人)是不能为标准库中的类增加成员的。
- 输入操作符>>的重载
与输出操作符类似,输入操作符的第一个形参是一个引用,指向它要读的流,并且返回的也是对同一个流的引用。它的第二个形参是对要读入的对象的非 const 引用,该形参必须为非 const,因为输入操作符的目的是将数据读到这个对象中。更重要但通常重视不够的是,输入和输出操作符有如下区别:输入操作符必须处理错误和文件结束的可能性。
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; }
这个操作符从 istream 形参中读取三个值:一个 string 值,存储到 isbn 成员中;一个 unsigned 值,存储到 Sales_item 形参的 units_sold 成员中;一个 double 值,存储到 Sales_item 形参的 price 成员中。假定读取成功,操作符用 price 和 units_sold 来设置 Sales_item 对象的 revenue 成员。
检查是否发生错误:Sales_item 的输入操作符将读入所期望的值并检查是否发生错误。可能发生的错误包括如下种类:任何读操作都可能因为提供的值不正确而失败。例如,读入 isbn 之后,输入操作符将期望下两项是数值型数据。如果输入非数值型数据,这次的读入以及流的后续使用都将失败;任何读入都可能碰到输入流中的文件结束或其他一些错误。
处理输入错误:如果输入操作符检测到输入失败了,则确保对象处于可用和一致的状态是个好做法。如果对象在错误发生之前已经写入了部分信息,这样做就特别重要。例如,在 Sales_item 的输入操作符中,可能成功地读入了一个新的 isbn,然后遇到流错误。在读入 isbn 之后发生错误意味着旧对象的 units_sold 和 revenue 成员没变,结果会将另一个 isbn 与那个数据关联。在这个操作符中,如果发生了错误,就将形参恢复为空 Sales_item 对象,以避免给它一个无效状态。用户如果需要输入是否成功,可以测试流。即使用户忽略了输入可能错误,对象仍处于可用状态——它的成员都已经定义。类似地,对象将不会产生令人误解的结果——它的数据是内在一致的。设计输入操作符时,如果可能,要确定错误恢复措施,这很重要。//这些都是在设计一个类时应该考虑到的细节。
- 算术操作符和关系操作符
一般而言,将算术和关系操作符定义为非成员函数,像下面给出的 Sales_item 加法操作符一样:
// assumes that both objects refer to the same isbn 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 的副本。我们使用 Sales_item 的复合赋值操作符来加入 rhs 的值。注意,为了与内置操作符保持一致,加法返回一个右值,而不是一个引用。算术操作符通常产生一个新值,该值是两个操作数的计算结果,它不同于任一操作数且在一个局部变量中计算,返回对那个变量的引用是一个运行时错误。既定义了算术操作符又定义了相关复合赋值操作符的类,一般应使用复合赋值实现算术操作符。根据复合赋值操作符(如 +=)来实现算术操作符(如 +),比其他方式更简单且更有效。例如,我们的 Sales_item 操作符。如果我们调用 += 来实现 +,则可以不必创建和撤销一个临时量来保存 + 的结果。
- 相等操作符
通常,C++ 中的类使用相等操作符表示对象是等价的。即,它们通常比较每个数据成员,如果所有对应成员都相同,则认为两个对象相等。与这一设计原则一致,Sales_item 的相等操作符应比较 isbn 以及销售数据:
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!=。用户会期待如果可以用某个操作符,则另一个也存在;相等和不操作符一般应该相互联系起来定义,让一个操作符完成比较对象的实际工作,而另一个操作符只是调用前者。
定义了 operator== 的类更容易与标准库一起使用。有些算法,如 find,默认使用 == 操作符,如果类定义了 ==,则这些算法可以无须任何特殊处理而用于该类类型。