C++运算符重载详解

C++语言的一个颇有意思的特性就是除了支持函数重载外还支持运算符重载,缘由就是在C++看来运算符也算是一种函数。好比一个 a + b 的加法表达式也能够用函数的形式:operator + (a, b)来表达。这里的operator +表明的就是加法函数。高级语言中的表达式和数学表达式很是类似,在必定的程度上经过运算符来描述表达式会比经过函数来描述表达式更加利于理解和阅读。通常状况下在重载某个运算符的实现时最好要和运算符自己的数学表示意义类似,固然你也能够彻底实现一个和运算符自己意义无关的功能或者相反的功能(好比对某个+运算符实现为相减)。运算符函数和类的成员函数以及普通函数同样,一样可分为类运算符和普通运算符。要定义一个运算符函数老是按以下的格式来定义和申明:ios

返回类型 operator 运算符(参数类型1 [,参数类型2] [,参数类型3] [, 参数类型N]);
复制代码

运算符重载须要在运算符前面加上关键字operator。通常状况下参数的个数不会超过2个,由于运算符大多只是一元或者二元运算,而只有函数运算符()以及new和delete这三个运算符才支持超过2个参数的状况。c++

可重载的运算符的种类

并非全部C++中的运算符均可以支持重载,咱们也不能建立一个新的运算符出来(好比Σ)。有的运算符只能做为类成员函数被重载,而有的运算符则只能当作普通函数来使用。算法

  • 不能被重载的运算符有:. .* :: ?: sizeof
  • 只能做为类成员函数重载的运算符有:() [] -> =

下面我将会对各类运算符重载的方法进行详细的介绍。同时为了更加表现通用性,我这边对参数类型的定义都采用模板的形式,并给出运算符的一些大致实现的逻辑。实际中进行重载时则须要根据具体的类型来进行定义和声明编程

1. 流运算符
描述
运算符种类 >> <<
是否支持类成员 YES
是否支持普通函数 YES
运算单元 二元
返回类型 左值引用

流运算符是C++特有的一种运算符。C++的标准库里面的iostream类就支持了流运算符并提供了读取流>>和插入流<<两种运算符,它们分别用来进行输入和输出操做,并且能够连续的进行输入输出,正是由于流运算符的这些特性使得函数的返回值类型必须是引用类型,并且对于普通函数来讲第一个参数也必须是引用类型。下面的例子说明了对流运算符的声明和定义方法:设计模式

//普通流运算符函数模板
template<class LeftType, class RightType> LeftType& operator << (LeftType& left, const RightType& right) { //... return left; } template<class LeftType, class RightType> LeftType& operator >> (LeftType& left, RightType& right) {
    //...
    return left;
}

//类成员函数
class CA {
     public:

     template<class RightType> CA& operator << (const RightType& right) { //... return *this; } template<class RightType> CA& operator >>(RightType& right) {
        //...
        return *this;
    }
};

复制代码

从上面的例子里面能够看出:数组

  • 流运算符的返回老是引用类型的,目的是返回值能够作左值而且进行连续的流运算操做。
  • 对于输入流运算符>>来讲咱们要求右边的参数必须是引用类型的,缘由就是输入流会修改右边参数变量的内容。若是右边参数是普通的值类型则不会起到输入内容被改变的效果。固然右边参数类型除了采用引用以外,还能够设置为指针类型。
  • 对于输出流运算符<<来讲由于并不会改变右边参数的内容,因此咱们建议右边参数类型为常量引用类型,目的是为了防止函数内部对右边参数的修改以及产生数据的副本或者产生多余的构造拷贝函数的调用。
  • 通常对流运算符进行重载能够采用普通函数也能够采用类成员函数的形式。两者的差异就是普通函数不能访问类的私有变量。固然解决的方法是将普通函数设置为类的友元函数便可。
2. 算术表达式运算符
描述
运算符种类 + - * / % ^ & | ~ >> <<
是否支持类成员 YES
是否支持普通函数 YES
运算单元 除~是一元以外其余都是二元
返回类型 普通值类型

算术表达式是最多见的数学运算符号,上面分别定义的是加(+)、减(-)、乘(*)、除(/)、取余(%)、异或(^)、与(&)、或(|)、非(~)、算术右移(>>)、逻辑左移(<<)几个运算符。除~运算符外其余运算符都是二元运算符,并且运算的结果和原来的值无关,而且不能作左值引用。下面是这些运算符重载的例子代码:bash

//普通算术运算符函数模板
template<class ReturnType, class LeftType, class RightType> ReturnType operator + (const LeftType& left, const RightType& right) {
    //...
    return 返回一个ReturnType类型的值
}

//取反运算符是一个一元运算符。
template<class ReturnType, class LeftType>
ReturnType operator ~(const LeftType& left)
{
    //...
   return 返回一个ReturnType类型的值
}

//类成员函数
class CA
{
     public:

     template<class ReturnType, class RightType>
      ReturnType operator + (const RightType& right) const
     {
           //...
            return 一个新的ReturnType类型对象。
      }

      //取反运算符是一个一元运算符。
     template<class ReturnType>
      ReturnType operator  ~ () const
     {
           //...
            return 一个新的ReturnType类型对象。
      }
};

复制代码

从上面的例子能够看出:闭包

  • 函数的返回都是普通类型而不是引用类型是由于这些运算符计算出来的结果都和输入的数据并非相同的对象而是一个临时对象,所以不能返回引用类型,也就是不能再做为左值使用。
  • 正是由于返回的值和输入参数是不一样的对象,所以函数里面的入参都用常量引用来表示,这样数据既不会被修改又能够减小构造拷贝的产生。
  • 函数的返回类型能够和函数的入参类型不一致,但在实际中最好是全部参数的类型保持一致。
  • 除了~运算符是一元运算符外其余的都是二元运算符,你能够看到上面的例子里面一元和二元运算符定义的差别性。
  • 这里面的<<和>>分别是表示位移运算而不是流运算。因此能够看出其实咱们能够彻底来自定义运算符的意义,也就是实现的结果能够和真实的数学运算符的意义彻底不一致。
3. 算术赋值表达式运算符
描述
运算符种类 += -= *= /= %= ^= &= |= >>= <<=
是否支持类成员 YES
是否支持普通函数 YES
运算单元 二元
返回类型 左值引用

算术赋值表达式除了具备上面说的算术运算的功能以外,还有保存结果的做用,也就是会将运算的结果保存起来。所以这种运算符函数的第一个参数必须是引用类型,而不能是常量,同时返回类型要和第一个参数的类型一致。下面的例子说明了运算符的声明和定义方法:函数

//普通运算符函数模板
template<class LeftType, class RightType> LeftType& operator += (LeftType& left, const RightType& right) {
    //...
    return left;
}

//类成员函数
class CA {
     public:

     template<class RightType> CA& operator += (const RightType& right) {
           //...
            return *this;
      }

     template<class RightType> CA& operator +=(RightType& right) {
        //...
        return *this;
    }
};

复制代码

从上面的例子里面能够看出:ui

  • 算术赋值运算符的返回老是引用类型,并且要和运算符左边的参数类型保持一致。
  • 函数的右边由于并不会改变右边参数的内容,因此咱们建议右边参数类型为常量引用类型,目的是为了防止函数内部对右边参数的修改以及产生数据的副本或者产生多余的构造拷贝函数的调用。
4. 比较运算符
描述
运算符种类 == != < > <= >= && || !
是否支持类成员 YES
是否支持普通函数 YES
运算单元 除!外其余的都是二元
返回类型 bool

比较运算符主要用于进行逻辑判断,返回的是bool类型的值。这些运算符并不会改变数据的内容,所以参数都设置为常量引用最佳。下面的例子说明了运算符的声明和定义方法:

//普通算术运算符函数模板
template<class LeftType, class RightType> bool operator == (const LeftType& left, const RightType& right) {
    //...
    return true or false
}

//非运算符是一个一元运算符。
template<class LeftType>
bool  operator !(const LeftType& left)
{
    //...
   return true  or false
}

//类成员函数
class CA
{
     public:

     template<class RightType>
      bool operator == (const RightType& right) const
     {
           //...
            return true or false
      }

      //取反运算符是一个一元运算符。
      bool operator  ! () const
     {
           //...
            return true or false
      }
};

复制代码

从上面的例子能够看出:

  • 条件运算符返回的通常是固定的bool类型,由于不会改变数据的值因此不管参数仍是成员函数都用常量来修饰。
5. 自增自减运算符
描述
运算符种类 ++ --
是否支持类成员 YES
是否支持普通函数 YES
运算单元 一元
返回类型 普通类型,和左值引用

自增和自减运算符都是一元运算符,并且都会改变自身的内容,所以左边参数不能是常量而只能是引用类型。又由于自增分为后缀i++和前缀++i两种形式(自减也同样,下面就只举自增的例子了)。后缀自增返回的值不能作左值而前缀自增返回的值则能够作左值。为了区分前自增和后自增,系统规定对前缀自增的运算符函数上添加一个int类型的参数做为区分的标志。下面的例子说明了运算符的声明和定义方法:

//普通函数运算符函数模板

//++i
template<class LeftType> LeftType& operator ++ (LeftType& left, int) {
    //...
    return left
}

//i++
template<class LeftType>  
LeftType operator ++ (LeftType& left)
{
    //...
    return 新的LeftType值
}

//类成员函数
class CA
{
     public:

     CA& operator  ++ (int)
     {
           //...
            return *this;
      }

    CA operator ++ ()
     {
           //...
            return 新的CA类型值
      }
};

复制代码

从上面的函数定义能够看出:

  • 自增自减函数的参数以及返回值以及函数修饰都不能带const常量修饰符。
  • 前缀自增的返回是引用类型能够作左值,然后缀自增的返回类型则是值类型不能作左值。
  • 参数中有int声明的是前缀自增而没有int声明的是后缀自增。
6.赋值运算符
描述
运算符种类 =
是否支持类成员 YES
是否支持普通函数 NO
运算单元 二元
返回类型 左值引用

赋值运算符只能用于类的成员函数中不能用于普通函数。赋值运算符重载的目的是为了解决对象的深拷贝问题。咱们知道C++中对于对象赋值的默认处理机制是作对象内存数据的逐字节拷贝,这种拷贝对于只有值类型数据成员的对象来讲是没有问题的,可是若是对象中保存有指针类型的数据成员则有可能会出现内存重复释放的问题。好比下面的代码片断:

class CA {
     public:
          int *m_a;
    
   ~CA(){ delete m_a;} 
};

void main() {
      CA a, b;
      a.m_a = new int;
      b = a;  //这里执行赋值操做,可是有危险!
}

复制代码

上面的代码能够看出当a,b对象的生命周期结束后的析构函数都会释放数据成员的m_a所占用的内存,可是由于咱们的默认对象赋值机制将会致使这部份内存被释放两次,从而产生了崩溃。所以在这种状况下咱们就需对类的赋值运算符进行重载来解决对象的浅拷贝问题。上面的状况除了要对一个类的赋值运算符进行重载外还有为这个类创建一个拷贝构造函数。这里面有一个著名的构造类的大三原则

若是一个类须要任何下列的三个成员函数之一,便三者所有要实现, 这三个成员函数是:拷贝构造,赋值运算符,析构函数. 实践中,不少类只要遵循"大二规则"便可,也就是说只要实现拷贝构造,赋值操做符就能够了,析构函数并不老是必需的.

实现大三原则的目的主要解决深拷贝的问题以及解决对象中有的数据成员的内存是经过堆分配创建的。在这里拷贝构造函数的实现通常和赋值运算符的实现类似,两者的区别在于拷贝构造函数通常用在对象创建时的场景,好比对象类型的函数参数传递以及对象类型的值的返回都会调用拷贝构造,而赋值运算符则用于对象创建后的从新赋值更新。好比下面的代码:

class CA {
       //... 
   };

CA foo(CA a) {
    return a;
}

void main() {
     CA  a, c;   //构造函数
     CA b = foo(a);   //a在传递给foo时会调用拷贝构造,foo在返回数据给b时也会调用拷贝构造,即便这里出现了赋值运算符。
     c = b;       //赋值运算符

}   

复制代码

上面的代码你能够清楚的看到构造函数、拷贝构造函数、赋值运算符函数调用的时机和差别。下面咱们来对赋值运算符以及大三原则进行定义:

class CA {
             public:
               CA(){}   //构造函数
               CA(const CA& other){}  //拷贝构造
               CA& operator =(const CA& other)  //赋值运算符重载
               {
                    //..
                     return *this;
               }
              ~CA(){}   //析构函数
     }

复制代码

从上面的定义能够看出:

  • 赋值运算符要求返回的是类的引用类型,由于赋值后的结果是能够作左值引用的。
  • 赋值运算符函数参数是常量引用代表不会修改入参的值。
7. 下标索引运算符
描述
运算符种类 []
是否支持类成员 YES
是否支持普通函数 NO
运算单元 二元
返回类型 引用

咱们知道在数组中咱们能够经过下标索引的方式来读取和设置某个元素的值好比:

int array[10] = {0};
     int a = array[0];
     array[0] = 10;
复制代码

在实际中咱们的有些类也具有集合的特性,咱们也但愿获取这个集合类中的数据元素经过下标来实现,为了解决这个问题咱们能够对在类中实现下标索引运算符。这个运算符只支持在类中定义,而且索引的下标通常是整数类型,固然你能够定义为其余类型以便实现相似于字典或者映射表的功能。具体的代码以下:

class CA
     {
          public:
                 //只用于常量对象的读取操做
                 template<class ReturnType,  class IndexType>
                 const ReturnType& operator [](IndexType index) const
                 {
                       return 某个returnType的引用
                  }
 
                 //用于通常对象的读取和写入操做
                 template<class ReturnType,  class IndexType>
                 ReturnType& operator[](IndexType index)
                 {
                        return 某个returnType的引用
                  }
      }    

复制代码

从上面的代码能够看出:

  • 这里定义了两个函数主要是前者为常量集合对象进行下标数据读取操做,然后者则为很是量集合对象进行下标数据读取和写入操做。
  • 这里返回的不是值类型而是引用类型的目的是为了减小由于读取而产生没必要要的内存复制。而写入操做则必须使用引用类型。
8. 类型转换运算符
描述
运算符种类 各类数据类型
是否支持类成员 YES
是否支持普通函数 NO
运算单元 一元
返回类型 各类数据类型

在实际的工做中,咱们的有些方法或者函数只接受特定类型的参数。而对于一个类来讲,若是这个类的对象并非那个特定的类型那么就没法将这个对象做为一个参数来进行传递,为了解决这个问题咱们必需要为类构建一个特殊的类型转换函数来解决这个问题好比:

void foo(int a){
     cout << a << endl;
}

class CA {
   private:
        int m_a;
    public:
        CA(int a):m_a(a){}
       int toInt() {
            return m_a;
       }
};

void main() {
       CA a(10);
       
       foo(a);  // wrong!!! a是CA类型而非整数,编译时报错。
       foo(a.toInt());  // ok!! 
}

复制代码

能够看出为了进行有效的参数传递,CA类必需要创建一个新的函数toInt来获取整数并传递给foo。而类型转换运算符则能够更加方便以及易读的形式来解决这种问题,经过类型转换运算符的重载咱们的代码在进行参数传递时就再也不须要借助多余的函数来完成,而是直接进行参数传递。类型转换运算符重载实际上是一种适配器模式的实现,咱们能够经过类型转换运算符的形式来实现不一样类型数据的转换和传递操做。类型转换运算符重载的定义方法以下:

class CA {
        public:
               template<class Type> operator Type() {
                     return Type类型的数据。
               }
    };

复制代码

从上面的代码中能够看出:

  • 类型转换运算符重载是不须要指定返回类型的,同时也不须要指定其余的入参,而只须要指定转换的类型做为运算符便可。
  • 类型转换运算符重载是能够用于任何的数据类型的,经过类型转换运算符的使用咱们就能够很简单的解决这种类型不匹配的问题了,下面的代码咱们来看经过类型转换运算符重载的解决方案:
class CA {
   private:
        int m_a;
    public:
        CA(int a):m_a(a){}
       operator int() {
            return m_a;
       }
};

void main() {
       CA a(10);
       foo(a);  // ok! 在进行参数传递是a会调用类型转换运算符进行类型的转换。
}

复制代码
9. 函数运算符
描述
运算符种类 ()
是否支持类成员 YES
是否支持普通函数 NO
运算单元 N元
返回类型 任意

函数运算符在STL中的算法中被大量使用。函数运算符能够理解为C++对闭包的支持和实现。 咱们能够经过函数运算符来将一个对象当作普通函数来使用,这个意思就是说咱们能够在某些接收函数地址做为参数的方法中传递一个对象,只要这个类实现的函数运算符而且其中的参数签名和接收的函数参数签名一致便可。咱们先来看下面一段代码:

//定义一个模板fn他能够接收普通函数也能够接收实现函数运算符的对象
template<class fn> void foo2(int a, fn pfn) {
      int ret = pfn(a);
      std::cout << ret << std::endl;
}

int foo1(int arg) {
     return arg + 1;
}


class CA {
    private:
          int m_a;
     public:
      CA(int a):m_a(a){}
      //定义一个函数运算符
      int operator()(int arg) {
          return arg + m_a;
     }

    //定义另一个函数运算符
    void operator()(int arg1, int arg2) {
        std::cout << arg1 + arg2 + m_a << std::endl;
   }
};

void main() {
      foo2(10, &foo1);   //普通函数做为参数传递。
    
     CA a(20);
     foo2(10, a);  //将对象传递给foo2当作普通函数来用。

    a(20, 30);    //这里将对象当作一个普通的函数来用。
}


复制代码

上面的代码能够看出来,由于CA类实现了2个函数运算符,因此咱们能够将CA的对象当作普通的函数来用,在使用时就像是普通的函数调用同样。咱们称这种实现了函数运算符的类的对象为函数对象。那么为何要让对象来提供函数的能力呢?答案就是咱们能够在对象的函数运算符内部访问一些对象自己具备的其余属性或者其余成员函数,而普通的函数则不具有这些特性。上面的例子也说明了这个问题,在类的函数运算符内部还可使用数据成员。一个类中可使用多个函数运算符的重载,并且函数运算符重载时的参数个数以及返回类型均可以彻底自定义。 咱们知道C++中不支持闭包机制,可是在某种程度上来讲咱们能够借助函数运算符重载的方式来实现这种相似闭包的能力。

10. 复引用运算符、地址运算符、成员访问运算符
描述
运算符种类 * & ->
是否支持类成员 YES
是否支持普通函数 除了* &支持外,->不支持
运算单元 1元
返回类型 任意

在C++语言中我能够能够对一个指针对象使用*运算符来实现取值操做,也就是获得这个指针所指向的对象;对一个对象使用&运算符来获得对象的指针地址;对于一个指针对象咱们可使用->运算符来访问里面的数据成员。所以这里的*运算符表示的是取值运算符(也叫复引用运算符,间接引用运算符)、&表示的是取地址运算符、->表示的是成员访问运算符。

class CA {
   public:
       int m_a;

};

void main() {
       CA a;

       CA *p = &a;   //取地址运算符
       cout << *p << endl;    //取值运算符
       p->m_a = 10;   //成员访问运算符 

}
复制代码

能够看出来上面的三个运算符的主要目的就是用于指针相关的处理,也就是内存相关的处理。这三个运算符重载的目的主要用于智能指针以及代理的实现。也是是C++从语言级别上对某些设计模式的实现。在编程中有时候咱们会构造出一个类来,这个类的目的主要用于对另一个类进行管理,除了自身的一些方法外,全部其余的方法调用都会委托给被管理类,这样咱们就要在管理类中实现全部被管理类的方法,好比下面的代码例子:

class CA {
    public:
       void foo1();
       void foo2();
       void foo3();
};

class CB {
   private:
       CA *m_p;
   public:
     CB(CA*p):m_p(p){}
     ~CB() { delete m_p;}  //负责销毁对象

   CA* getCA(){ return m_p;}
   void foo1(){ m_p->foo1();}
   void foo2(){m_p->foo2();}
   void foo3(){m_p->foo3();}
};

void fn(CA*p) {
   p->foo1();
}

void main() {
    CB b(new CA);
     b.foo1();
     b.foo2();
     b.foo3();
    //由于fn只接受CA类型因此这里CB要提供一个方法来转化为CA对象。
    fn(b.getCA());
   
}

复制代码

上面的代码能够看出CB类是一个CA类的管理类,他会负责对CA类对象的生命周期的管理。除了这些管理外CB类还实现全部CA类的方法。当CA类的方法有不少时那么这种实现的方式是低效的,怎么来解决这个问题呢?答案就是本节里面所说到的3个运算符重载。咱们来看如何实现这三个运算符的重载:

class CA {
    public:
       void foo1();
       void foo2();
       void foo3();
};

class CB {
   private:
       CA *m_p;
   public:
     CB(CA*p):m_p(p){}
     ~CB() { delete m_p;}  //负责销毁对象

  public:
    //解引用和地址运算符是互逆的两个操做
    CA&  operator *() { return *m_p;}
    CA*   operator &() {return m_p;}

    //成员访问的运算符和&运算符的实现机制很是类似
    CA*  operator ->() { return m_p;}
 };


void fn1(CA*p) {
   p->foo1();
}

void fn2(CA&r) {
    r.foo2();
}

void main() {
    CB b(new CA);
     
     b->foo1();
     b->foo2();   //这两个调用了->运算符重载

    fn1(&b);   //调用&运算符重载
    fn2(*b);   //调用*运算符重载
}


复制代码

从上面的代码能够看出正是由于实现了对三个运算符的重载使得咱们不须要在CB类中重写foo1-foo3的实现,以及咱们不须要提供特殊的类型转换方法,而是直接经过运算符的方式就能够转化为CA对象的并使用。固然一个完整的智能指针的封装不只仅是对三个运算符的重载,咱们还须要对构造函数、拷贝构造、赋值运算符、类型转化运算符、析构函数进行处理。若是你要想更加的了解智能指针就请去看看STL中的auto_ptr类

11. 内存分配和销毁运算符
描述
运算符种类 new delete
是否支持类成员 YES
是否支持普通函数 YES
运算单元 N元
返回类型 new返回指针, delete不返回

是的,你没有看错C++中对内存分配new以及内存销毁delete也是支持重载的,也就是说new和delete也是一种运算符。默认状况下C++中的new和delete都是在堆中进行内存分配和销毁,有时候咱们想对某个类的内存分配方式进行定制化处理,这时候就须要经过对new和delete进行重载处理了。而且系统规定若是实现了new的重载就必须实现delete的重载处理。关于对内存分配和销毁部分我想单独开辟一篇文章来进行详细介绍。这里面就只简单了举例如何来实现new和delete的重载:

class CA
{
    public:
     CA* operator new(size_t t){  return  malloc(t);}
     void operator delete(void *p) { free(p);}      
};
   
复制代码

关于对new和delete运算符的详细介绍请参考文章:C++的new和delete详解

相关文章
相关标签/搜索