C++重载运算符

来源:互联网 发布:淘宝网官方版下载安装 编辑:程序博客网 时间:2024/06/07 06:56

        当运算符被用于类类型的对象时,C ++语言定允许我们为其指定新的含义;同时,我们也能自定义类类型之间的转换规则。和内置类型的转换一样,类类型转换隐式的将一种类型的对象转换成另一种我们所需类型的对象。当运算符作用于类类型的运算对象时,可以通过运算符重载重新定义该运算符的含义。

        重载运算符是具有特殊名字的函数:它们的名字由关键字 operator 和其后要定义的运算符号共同组成。和其他函数一样,重载的运算符也包含返回类型、参数列表以及函数体。

        如果一个运算函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的 this 指针上,成员运算符函数的(显示)参数数量比运算对象的数量少一个。

运算符函数定义的一般格式如下:

  <返回类型说明符> operator <运算符符号>(<参数表>)

{

     <函数体>

}

1. 成员或者非成员的选择

定义类的重载运算符时,有的运算符作为成员,而有些作为类的非成员函数。下面的准则有助于我们将运算符定义为成员函数还是普通的非成员函数:

1.赋值(=)、下标([  ])、调用(())、和成员访问箭头(->)运算符必须是成员。

2.复合赋值运算符一般来说应该是成员,但非必须,这一点与赋值运算符略有不同。

3.改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减、和解引用运算符,通常应该是成员。

4.具有对称性的运算符可能是转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。

一般情况下,单目运算符最好重载为类的成员函数,而双目运算符一般重载为非成员函数。

2. 输入输出运算符

重载输出运算符<<

重载输出运算符的第一个形参是一个非常量 ostream 对象的引用,第二个形参是一个常量的引用。为了与其他输出运算符保持一致,operator <<一般要返回它的 ostream 形参。

例如:

ostream &operator <<( ostream &os, const  sales_data  &item)

{

         os<<item.isbn()<<"  "<<" item.units_sold  "<<"  "<< item.revenue;

          return os;

}

注意:如上所示,输出运算符应该主要打印对象的内容而非控制格式,输出运算符不应该打印换行符。

重载输入运算符>>

输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的非常量对象的引用。

例如:

istream &operator >>(istream &is,sales_data  &item)

{

        double  price;

        is>> item.bookno>>item.price;

        if( is )

               item.revenue = item.units_sold >> price;

        else

               item = sales_data();

         return  is;

}

注意:输入运算符必须要处理输入可能失败的情况,而输出运算符不需要。


输入输出运算符必须是非成员函数,与 iostream 标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员函数。否则他们的左侧运算对象将是我们的类的一个对象:

sales_data  data;

data << cout;           // 如果 operator <<是sales_data 的成员

假设输入输出运算符是某个类的成员,则他们也必须是 istream 和 ostream 的成员。然而,这两个类属于标准库,无法给标准库中的类添加任何成员。

3. 算术和关系运算符

算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换。这些运算符一般不需要改变运算对象的状态,形参是常量的引用。

例如:

sales_data  operator+(const  sales_data  &lhs, const  sales_data  &rhs)

{

        sales_data  sum = lhs;

        sum+=rhs;

        return  sum;

}

当同时定义了算术运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值运算符来实现算术运算符。


相等运算符

C++ 中的类通过定义相等运算符来检验两个对象是否相等,即它们会比较对象的每一个数据成员,只有当所有对应的成员都相等时才认为两个对象相等。

例如:

bool  operator ==( const  sales_data  &lhs,const  sales_data  &rhs)

{

          return  lhs.isbn() == rhs.isbn() && lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue;

}

bool  operator !=( const  sales_data  &lhs,const  sales_data  &rhs)

{

          return  !(lhs == rhs);

}


关系运算符

定义了相等运算符的类也常常包含关系运算符,关联容器和一些算法要用到小于运算符,所以定义 operator < 比较有用。

通常情况下关系运算符应该:

1. 定义顺序关系,令其与关联容器中对关键字的要求一致。

2. 如果类同时也含有 == 运算符的话,则定义一种关系令其与 == 保持一致。特别是,如果两个对象是 ! = 的,那么一个对象应该 < 另外一个。

4. 赋值运算符

拷贝赋值和移动赋值运算符可以把类的一个对象赋值给该类的另一个对象。此外,类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象。

比如,在拷贝赋值和移动赋值运算符之外,标准库 vector 类还定义了第三种赋值运算符,该运算符接受花括号内的元素列表作为参数。我们都能以如下的形式使用该运算符:

vector <string> v;

v = {"a","an","the"};

同样也可以把这个运算符添加到 strvec 类中:

class  strvec {

public:

     strvec  &operator = (initializer_list <string> );

}

为了与内置类型的赋值运算符保持一致,这个新的赋值运算符将返回其左侧对象的引用:

strvec  &strvec :: operator = ( initializer_list <string> il)

{

        auto data = alloc_n_copy(il.begin( ),  il.end( ));

        free();

        elements = data.first;

        first_free = cap =data.second;

        return  *this;

}

和拷贝赋值及移动赋值运算符一样,其他重载的赋值运算符也必须先释放当前内存空间,再创建一片新空间。

5. 下标运算符

表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符 operator [ ]。下标运算符必须是成员函数。且如果一个类包含下标运算符,则它通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用。

例如:

class  strvec {

public:

         string  &operator [ ]( size_t  n )

               {   return  elements [ n ];   }

         const  string  &operator [ ](size_t  n)  const

               {  return  elements [ n ];   }

private:

       string  *elements;

};

下标运算符返回的是元素的引用,所以当 strvec 是非常量时,我们可以给元素赋值;而当我们对常量对象取下标时,不能为其赋值。

6. 递增和递减运算符

定义前置递增/ 递减运算符

定义递增和递减运算符的类应该同时定义前置版本和后置版本。这些运算符通常应该被定义为类的成员。

例如:

class  strvec {

  public :

        strvec&  operator++ ();

        strvec&  operator - -();

};

为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。

递增和递减运算符的工作机理非常相似: 它们首先调用 check 函数检验 strvec 是否有效,如果是,接着检查给定的索引值是否有效。如果 check 函数没有抛出异常,则运算符返回对象的引用。

例如:

strvec&  strvec : : operator++ ( )

{

       check( curr, "increment  past  end of strvec");

       ++curr;

       return  *this;

}

strvec&  strvec : : operator - -( )

{

         - -curr;

         check(curr,"decrement  past  begin  of  strvec");

         return  *this;

区分前置和后置运算符

为了区分前置和后置运算符,后置版本接受一个额外的(不被使用)int 类型的形参。

例如:

class  strvec {

public:

     strvec  operator++(int);

     strvec  operator-  -(int);

}

为了与内置版本一致,后置运算符应该返回对象的原值,返回形式是一个值而非引用。对于后置版本来说,在递增对象之前需要首先记录对象的状态:

strvec  strvec : : operator++ (int)

{

       strvec  ret = *this;

       ++ *this;

       return  ret;

}

strvec  strvec : : operator - - (int)

{

      strvec  ret = *this;

      - - *this;

      return  ret;

}


7. 成员访问运算符

在迭代器及智能指针类中常常用到解引用运算符(*)和箭头运算符(->)。我们以如下方式向 strvec 类添加这两种运算符:

class  strvec {

public:

      string &operator* () const

       {     auto p=check(curr,"dereference past  end");

               return (*p)[curr];

       }

      string* operator ->() const

       {

              return  & this ->operator*();

        }

}

箭头运算符不执行自己的操作,而是调用解引用运算符并返回解引用结果元素的地址。通常情况下箭头运算符和解引用运算符也是类的成员。

箭头运算符最基本的含义是成员访问。当我们重载箭头时,可以改变的是箭头从哪个对象中获取成员,而箭头获取成员这一事实则永远不变。对于形如 point -> mem 的表达式来说,point 必须是指向类对象的指针或者是一个重载了 operator -> 的类的对象。根据 point 类型的不同,point ->mem 分别等价于

(*point).mem;            // point 是一个内置的指针类型

point.operator () ->mem;   //point 是类的一个对象

除此之外,代码都将发生错误。point ->mem 的执行过程如下所示:

1. 如果 point 是指针,则我们应用内置的箭头运算符,表达式等价于(*point).mem 。首先解引用该指针,然后从所得的对象中获取指定的成员。如果 point 所指的类型没有名为mem 的成员,程序会发生错误。

2. 如果 point 是定义了 operator-> 的类的一个对象,则我们使用 point.operator->() 的结果来获取 mem。其中,如果该结果是一个指针,则执行第一步;如果该结果本身含有重载的 operator ->() ,则重复调用当前步骤。最终,当这一过程结束时程序或者返回了所需的内容,或者返回一些表示程序错误的信息。

8. 函数调用运算符

如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通函数相比它们更加灵活。

举个例子:

struct  absInt {

            int  operator  ()(int  val) const {

                  return  val < 0 ?  -val  :   val ;

             }

}

int  i  = -42;

absInt  absObj;      //含有函数调用运算符的对象

int  ui = absObj( i );     //将 i 传递给 absObj.operator ()

即使 absObj 只是一个对象而非函数,我们也能“调用”该对象。调用该对象实际上是在运行重载的调用运算符。在此例中,该运算符接受一个 int 值,并返回其绝对值。函数调用运算符必须是成员函数。如果类定义了调用运算符,则该类的对象称作函数对象。因为可以调用这种对象,所以我们说这些对象的行为像函数一样。

和其他类一样,函数对象除了 operator () 之外也可以包含其他成员。函数对象类通常含有一些数据成员,这些成员被用于定制调用运算符中的操作。举个例子,我们将定义一个打印 string 实参内容的类。默认情况下,我们的类会将内容写入到 cout 中,每个 string 之间以空格隔开。同时也允许类的用户提供其他科写入的流及其他分隔符。我们将该类定义如下:

class  printString  {

      public:

            printString  (ostream  &o = cout, char   c = '  '): os(o),sep(c) {    }

            void  operator ()(const  string  &s)  const  { os << s<< sep; }

      private:

            ostream  &os;

            char  sep;

};

我们的类有一个构造函数,他接受一个输出流的引用以及一个用于分隔符的字符,这两个形参的默认实参分别是 cout 和空格。之后的函数调用运算符使用这些成员协助其打印给定的 string。

当定义 printString 的对象时,对于分隔符及输出流既可以使用默认值也可以提供我们自己的值:

printString  printer;                   // 使用默认值打印到 cout

printer (s);                                  // 在 cout 中打印s, 后面跟一个空格

printString  errors(cerr,' \n ');

errors (s);                                   // 在cerr中打印s,后面跟一个换行符

函数对象常常作为泛型算法的实参。例如,可以使用标准库 for_each 算法和我们自己的 printSring 类来打印容器的内容:

for_each (vs.begin (),vs.end (),printString (cerr,' \n ' ));

for_each 的第三个实参是类型 printString 的一个临时对象,其中我们用 cerr 和换行符初始化了该对象。当程序调用 for_each 时,将会把 vs 中的每个元素依次打印到 cerr 中,元素之间以换行符分隔。

1 0
原创粉丝点击