C++ Primer 4 第十四章 重载操作符与转换

来源:互联网 发布:培训包就业 知乎 编辑:程序博客网 时间:2024/05/19 05:33
 

第十四章 重载操作符与转换

1. 重载操作符的定义

         重载操作符是具有特殊名称的函数:保留字 operator 后接需定义的操作符号。像任意其他函数一样,重载操作符具有返回类型和形参表,如下语句:

     Sales_item operator+(const Sales_item&, const Sales_item&);

可以重载的操作符:

+       -        *       /        %      ^

&      |         ~       !        ,        =

<       >       <=     >=     ++     --

<<     >>     ==     !=      &&   ||

+=     -=      /=      %=    ^=     &=

|=      *=     <<=  >>=  []       ()

->      ->*    new   new []        delete          delete []

不能重载的操作符:

         ::   .*  .   ?:

通过连接其他合法符号可以创建新的操作符。例如,定义一个 operator** 以提供求幂运算是合法的。

重载操作符必须具有至少一个类类型或枚举类型的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含义

操作符的优先级、结合性或操作数目不能改变。

有四个符号(+, -, * 和 &)既可作一元操作符又可作二元操作符,这些操作符有的在其中一种情况下可以重载,有的两种都可以,定义的是哪个操作符由操作数数目控制。除了函数调用操作符 operator() 之外,重载操作符时使用默认实参是非法的。

不再具备短路求值特性:重载操作符并不保证操作数的求值顺序,尤其是,不会保证内置逻辑 AND、逻辑 OR和逗号操作符的操作数求值。在 && 和 || 的重载版本中,两个操作数都要进行求值,而且对操作数的求值顺序不做规定。因此,重载 &&、|| 或逗号操作符不是一种好的做法。

类成员与非成员:大多数重载操作符可以定义为普通非成员函数或类的成员函数。作为类成员的重载函数,其形参看起来比操作数数目少 1。作为成员函数的操作符有一个隐含的 this 形参,限定为第一个操作数。重载一元操作符如果作为成员函数就没有(显式)形参,如果作为非成员函数就有一个形参。类似地,重载二元操作符定义为成员时有一个形参,定义为非成员函数时有两个形参。

一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员:

     Sales_item& Sales_item::operator+=(const Sales_item&);

     Sales_item operator+(const Sales_item&, const Sales_item&);

操作符定义为非成员函数时,通常必须将它们设置为所操作类的友元。

也可以像调用普通函数一样调用重载操作符函数,指定函数并传递适当类型适当数目的形参:

     cout << operator+(item1, item2) << endl;

          item1.operator+=(item2);   // equivalent call to member operator function

不要重载具有内置含义的操作符:重载逗号、取地址、逻辑与、逻辑或等等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义。

大多数操作符对类对象没有意义:除非提供了重载定义,赋值、取地址和逗号操作符对于类类型操作数没有意义。设计类的时候,应该确定要支持哪些操作符。

当内置操作符和类型上的操作存在逻辑对应关系时,操作符重载最有用。使用重载操作符而不是创造命名操作,可以令程序更自然、更直观,而滥用操作符重载使得我们的类难以理解。当一个重载操作符的含义不明显时,给操作取一个名字更好。对于很少用的操作,使用命名函数通常也比用操作符更好。如果不是普通操作,没有必要为简洁而使用操作符。

如果类定义了相等操作符,它也应该定义不等操作符 !=。类用户会假设如果可以进行相等比较,则也可以进行不等比较。同样的规则也应用于其他关系操作符。如果类定义了 <,则它可能应该定义全部的四个关系操作符(>,>=,<,<=)。

选择成员或非成员实现:

         1)赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。

         2)像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。

         3)改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。

         4)对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。

2. 输入和输出操作符

输出操作符 << 的重载:为了与 IO 标准库一致,操作符应接受 ostream& 作为第一个形参,对类类型 const 对象的引用作为第二个形参,并返回对 ostream 形参的引用。

         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 对象。

一般而言,输出操作符应输出对象的内容,进行最小限度的格式化,它们不应该输出换行符。

IO 操作符必须为非成员函数:不能将该操作符定义为类的成员,否则,左操作数将只能是该类类型的对象:

     // if operator<< is a member of Sales_item

     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;

     }

3. 算术操作符和关系操作符

一般而言,将算术和关系操作符定义为非成员函数。为了与内置操作符保持一致,加法返回一个右值,而不是一个引用。

定义了 operator== 的类更容易与标准库一起使用。有些算法,如 find,默认使用 == 操作符,如果类定义了 ==,则这些算法可以无须任何特殊处理而用于该类类型。定义了相等操作符的类一般也具有关系操作符。尤其是,因为关联容器和某些算法使用小于操作符,所以定义 operator< 可能相当有用。

4. 赋值操作符

赋值操作符可以重载。无论形参为何种类型,赋值操作符必须定义为成员函数,这一点与复合赋值操作符有所不同。

string 类包含如下成员:

     class string {

     public:

         string& operator=(const string &);      // s1 = s2;

         string& operator=(const char *);        // s1 = "str";

         string& operator=(char);                // s1 = 'c';

         // ....

      };

赋值必须返回对 *this 的引用,例如,这是 Sales_item 复合赋值操作符的定义:

     Sales_item& Sales_item::operator+=(const Sales_item& rhs)

     {

        units_sold += rhs.units_sold;

        revenue += rhs.revenue;

        return *this;

     }

5. 下标操作符

下标操作符必须定义为类成员函数。

提供读写访问:定义下标操作符比较复杂的地方在于,它在用作赋值的左右操作符数时都应该能表现正常。下标操作符出现在左边,必须生成左值,可以指定引用作为返回类型而得到左值。只要下标操作符返回引用,就可用作赋值的任意一方。应用于const 对象时,返回值应为 const 引用,因此不能用作赋值的目标。

类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用。

下面的类定义了下标操作符。为简单起见,假定 Foo 所保存的数据存储在一个 vector<int>: 中:

     class Foo {

     public:

         int &operator[] (const size_t);

         const int &operator[] (const size_t) const;

     private:

         vector<int> data;

      };

下标操作符本身可能看起来像这样:

     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

     }

6. 成员访问操作符

C++ 语言允许重载解引用操作符(*)和箭头操作符(->))。箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员,但将它作为成员一般也是正确的。

像下标操作符一样,我们需要解引用操作符的 const 和非 const 版本。它们的区别在于返回类型:const 成员返回 const 引用以防止用户改变基础对象:

重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。如果返回类型是类类型的其他对象(或是这种对象的引用),则将递归应用该操作符。编译器检查返回对象所属类型是否具有成员箭头,如果有,就应用那个操作符;否则,编译器产生一个错误。这个过程继续下去,直到返回一个指向带有指定成员的的对象的指针,或者返回某些其他值,在后一种情况下,代码出错。

         class ScreenPtr {

     public:

         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

     };

7. 自增操作符和自减操作符

C++ 语言不要求自增操作符或自减操作符一定作为类的成员,但是,因为这些操作符改变操作对象的状态,所以更倾向于将它们作为成员。

对内置类型而言,自增操作符和自减操作符有前缀和后缀两种形式。也可以为我们自己的类定义自增操作符和自减操作符的前缀和后缀实例。

定义前自增/前自减操作符:

         CheckedPtr& CheckedPtr::operator++()

     {

         if (curr == end)

             throw out_of_range

                   ("increment past the end of CheckedPtr");

         ++curr;                // advance current state

         return *this;

     }

同时定义前缀式操作符和后缀式操作符存在一个问题:它们的形参数目和类型相同,普通重载不能区别所定义的前缀式操作符还是后缀式操作符。为了解决这一问题,后缀式操作符函数接受一个额外的(即,无用的)int 型形参。使用后缀式操作符进,编译器提供 0 作为这个形参的实参。尽管我们的前缀式操作符函数可以使用这个额外的形参,但通常不应该这样做。那个形参不是后缀式操作符的正常工作所需要的,它的唯一目的是使后缀函数与前缀函数区别开来:

         CheckedPtr CheckedPtr::operator++(int)

     {

         // no check needed here, the call to prefix increment will do the check

         CheckedPtr ret(*this);        // save current value

         ++*this;                      // advance one element, checking the increment

         return ret;                   // return saved state

     }

         为了与内置操作符一致,后缀式操作符应返回旧值(即,尚未自增或自减的值),并且,应作为值返回,而不是返回引用。

显式调用前缀式操作符:

         CheckedPtr parr(ia, ia + size);        // iapoints to an array of ints

     parr.operator++(0);                    // call postfix operator++

                            //所传递的值通常被忽略,但该值是必要的,用于通知编译器需要的是后缀式版本

     parr.operator++();                     // call prefix operator++

8. 调用操作符和函数对象

函数调用操作符必须声明为成员函数。一个类可以定义函数调用操作符的多个版本,由形参的数目或类型加以区别。定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象。

         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)

         标准库定义的函数对象:标准库定义了一组算术、关系与逻辑函数对象类,标准库还定义了一组函数适配器,使我们能够特化或者扩展标准库所定义的以及自定义的函数对象类。每个函数对象类都是一个类模板。这些标准库函数对象类型是在 functional 头文件中定义的。每个标准库函数对象类表示一个操作符,即,每个类都定义了应用命名操作的调用操作符。例如,plus 是表示加法操作符的模板类型。plus 模板中的调用操作符对一对操作数应用 + 运算。

         函数对象常用于覆盖算法使用的默认操作符。例如,sort 默认使用 operator< 按升序对容器进行排序。为了按降序对容器进行排序,可以传递函数对象 greater。该类将产生一个调用操作符,调用基础对象的大于操作符。如果 svec 是一个 vector<string> 对象,以下代码

     sort(svec.begin(), svec.end(), greater<string>());

         将按降序对 vector 进行排序。像通常那样,传递一对迭代器以指明被排序序列。第三个实参用于传递比较元素的谓词函数。该实参 greater<string> 类型的临时对象,是一个将 > 操作符应用于两个 string 操作符的函数对象。

函数对象的函数适配器:标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。函数适配器分为如下两类:

         1)绑定器,是一种函数适配器,它通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象。

         2)求反器,是一种函数适配器,它将谓词函数对象的真值求反。

标准库定义了两个绑定器适配器:bind1st 和 bind2nd。每个绑定器接受一个函数对象和一个值。bind1st 将给定值绑定到二元函数对象的第一个实参,bind2nd 将给定值绑定到二元函数对象的第二个实参。例如,为了计算一个容器中所有小于或等于 10 的元素的个数,可以这样给 count_if 传递值:

     count_if(vec.begin(), vec.end(),

              bind2nd(less_equal<int>(), 10));

         传给 count_if 的第三个实参使用 bind2nd 函数适配器,该适配器返回一个函数对象,该对象用 10 作右操作数应用 <= 操作符。这个 count_if 调用计算输入范围中小于或等于 10 的元素的个数。

标准库还定义了两个求反器:not1 和 not2。not1 将一元函数对象的真值求反,not2 将二元函数对象的真值求反。

为了对 less_equal 函数对象的绑定求反,可以编写这样的代码:

     count_if(vec.begin(), vec.end(),

             not1(bind2nd(less_equal<int>(), 10)));

这里,首先将 less_equal 对象的第二个操作数绑定到 10,实际上是将该二元操作转换为一元操作。再用 not1 对操作的返回值求反,效果是测试每个元素是否 <=。然后,对结果真值求反。这个 count_if 调用的效果是对不 <= 10 的那些元素进行计数。

9. 转换与类类型

我们可以定义转换操作符,给定类类型的对象,该操作符将产生其他类型的对象。像其他转换一样,编译器将自动应用这个转换。

转换操作符是一种特殊的类成员函数。它定义将类类型值转变为其他类型值的转换。转换操作符在类定义体内声明,在保留字 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;

     };

转换函数采用如下通用形式:转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空

     operator type();

type 表示内置类型名、类类型名或由类型别名定义的名字。对任何可作为函数返回类型的类型(除了 void 之外)都可以定义转换函数。一般而言,不允许转换为数组或函数类型,转换为指针类型(数据和函数指针)以及引用类型是可以的。

虽然转换函数不能指定返回类型,但是每个转换函数必须显式返回一个指定类型的值。例如,operator int 返回一个 int 值;如果定义 operator Sales_item,它将返回一个 Sales_item 对象,诸如此类。

使用转换函数时,被转换的类型不必与所需要的类型完全匹配。

只要存在转换,编译器将在可以使用内置转换的地方自动调用它:

         1)在表达式中:

                   SmallInt si;

                   double dval;

                   si >= dval          // si converted to int and then convert to double

         2)在条件中:

              if (si)                // si converted to int and then convert to bool

         3)将实参传给函数或从函数返回值:

              int calc(int);

              SmallInt si;

              int i = calc(si);      // convert si to int and call calc

         4)作为重载操作符的操作数:

        cout << si << endl;

         5)在显式类型转换中:

              int ival;

              SmallInt si = 3.541; //instruct compiler to cast si to int

              ival = static_cast<int>(si) + 3;

类类型转换之后不能再跟另一个类类型转换。如果需要多个类类型转换,则代码将出错。

例如,假定有另一个类 Integral,它可以转换为 SmallInt 但不能转换为 int:

     class Integral {

     public:

         Integral(int i = 0): val(i) { }

         operator SmallInt() const { return val % 256; }

     private:

         std::size_t val;

     };

可以在需要 SmallInt 的地方使用 Integral,但不能在需要 int 的地方使用 Integeral:

     int calc(int);

     Integral intVal;

     SmallInt si(intVal);  // ok: convert intVal to SmallInt and copy to si

     int i = calc(si);     // ok: convert si to int and call calc

     int j = calc(intVal); // error: no conversion to int from Integral

第一个 calc 调用也是正确的:将实参 si 自动转换为 int,然后将 int 值传给函数。

第二个 calc 调用是错误的:没有从 Integral 到 int 的直接转换。从 int 需要两次类类型转换:首先从 Integral 到 SmallInt,然后从 SmallInt 到 int。但是,语言只允许一次类类型转换,所以该调用出错。

标准转换可放在类类型转换之前:使用构造函数执行隐式转换的时候,构造函数的形参类型不必与所提供的类型完全匹配。例如,下面的代码调用 SmallInt(int) 类中定义的构造函数(SmallInt(int))将 sobj 转换为 SmallInt 类型:

     void calc(SmallInt);

     short sobj;

     // sobj promoted from short to int

     // that int converted to SmallInt through the SmallInt(int) constructor

     calc(sobj);

如果需要,在调用构造函数执行类类型转换之前,可将一个标准转换序列应用于实参。为了调用函数 calc(),应用标准转换将 dobj 从 double 类型转换为 int 类型,然后调用构造函数 SmallInt(int) 将转换结果转换为 SmallInt 类型。

类类型转换可能是实现和使用类的一个好处。通过为 SmallInt 定义到 int 的转换,能够更容易实现和使用 SmallInt 类。int 转换使 SmallInt 的用户能够对 SmallInt 对象使用所有算术和关系操作符,而且,用户可以安全编写将SmallInt 和其他算术类型混合使用的表达式。定义一个转换操作符就能代替定义 48 个(或更多)重载操作符,类实现者的工作就简单多了。类类型转换也可能是编译时错误的一大来源。当从一个类型转换到另一类型有多种方式时,问题就出现了。如果有几个类类型转换可以使用,编译器必须决定对给定表达式使用哪一个。在这一节,我们介绍怎样用类类型转换将实参和对应形参相匹配。首先介绍非重载函数的形参匹配,然后介绍重载函数的形参匹配。

(省略剩余高级主题:实参匹配和转换、重载确定和类的实参、重载、转换和操作符