13. C++基本运算符重载

基本上我们进行运算符重载时有两种形式,类内的运算符重载和顶层函数位置的运算符重载。

操作符重载指的是将C++提供的操作符进行重新定义,使之满足我们所需要的一些功能。
运算符重载的格式为:

返回值类型 operator 运算符名称 (形参表列){   }

operator是关键字,专门用于定义重载运算符的函数。我们可以将operator 运算符名称这一部分看做函数名,对于上面的代码,函数名就是operator+。

运算符重载函数除了函数名有特定的格式,其它地方和普通函数并没有区别。
在C++中可以重载的操作符有:

+  -  *  /  %  ^  &  |  ~  !  =  <  >  +=  -=  *=  /=  %=  ^=  &=  |= 
<<  >>  <<=  >>=  ==  !=  <=  >=  &&  ||  ++  --  ,  ->*  ->  ()  [] 
new  new[]  delete  delete[]

上述操作符中,[]操作符是下标操作符,()操作符是函数调用操作符。自增自减操作符的前置和后置形式都可以重载。长度运算符“sizeof”、条件运算符“:?”成员选择符“.”、对象选择符“.*”和域解析操作符“::”不能被重载。


这里我们重点看几个运算符的重载
13.1 重载输入输出
13.2 重载赋值
13.3 重载下标
13.4 重载函数调用
13.5 重载自增自减
13.6 重载转型

为了介绍基本操作符的重载,我们先来看一个操作符重载的示例。
在这个例子中,我们定义了一个复数类,一个复数包含实部和虚部两部分,我们分别用real和imag来表示复数的实部和虚部,并将这两个变量作为复数类的成员变量,并设置为private属性。在复数类中,我们定义了三个构造函数用于初始化复数类的对象。之后声明了四个操作符重载函数,分别重载加减乘除四种操作符。最后定义了一个打印复数的函数display。

#include 
using namespace std;

class complex
{
public:
    complex();
    complex(double a);
    complex(double a, double b);
    complex operator+(const complex & A)const;
    complex operator-(const complex & A)const;
    complex operator*(const complex & A)const;
    complex operator/(const complex & A)const;
    void display()const;
private:
    double real;   //复数的实部
    double imag;   //复数的虚部
};

complex::complex()
{
    real = 0.0;
    imag = 0.0;
}

complex::complex(double a)
{
    real = a;
    imag = 0.0;
}

complex::complex(double a, double b)
{
    real = a;
    imag = b;
}

//打印复数
void complex::display()const
{
    cout<

与istream一样,ostream用于表示输出流,同样为了能够直接访问complex类的私有成员变量,我们将其在类内部声明为complex类的友元函数,同样该输出操作符重载函数可以实现链式输出。

重载输出运算符

#include 
using namespace std;

class complex
{
public:
    complex();
    complex(double a);
    complex(double a, double b);
    friend complex operator+(const complex & A, const complex & B);
//    friend istream & operator>>(istream & in, complex & A);
//    friend ostream & operator<<(ostream & out, complex & A);
    ostream & operator<<(ostream & out);
    void display()const;
private:
    double real;   //复数的实部
    double imag;   //复数的虚部
};

complex::complex()
{
    real = 0.0;
    imag = 0.0;
}

complex::complex(double a)
{
    real = a;
    imag = 0.0;
}

complex::complex(double a, double b)
{
    real = a;
    imag = b;
}

//打印复数
void complex::display()const
{
    cout<>(istream & in, complex & A)
//{
//    in >> A.real >> A.imag;
//    return in;
//}

////重载输出操作符
//ostream & operator<<(ostream & out, complex & A)
//{
//    out << A.real <<" + "<< A.imag <<" i ";;
//    return out;
//}

//输出的成员函数重载
ostream& complex::operator <<(ostream & out)
{
    out << real << "+" << imag << "i" < 
  
image.png

关于输入输出我们习惯用类外的友元函数重载形式。


13.2 重载赋值操作符

赋值操作符“=”可以用来将一个对象拷贝给另一个已经存在的对象。当然拷贝构造函数同样也会有此功能,拷贝构造函数可以将一个对象拷贝给另一个新建的对象。如果我们没有在类中显式定义拷贝构造函数,也没有重载赋值操作符,则系统会为我们的类提供一个默认的拷贝构造函数和一个赋值操作符。系统为我们提供的默认的拷贝构造函数只是将源对象中的数据一一拷贝给目标对象,而系统为类提供的赋值操作符也是这样的一种功能。

complex c1(4.3, -5.8);
complex c2;
c2 = c1;
cout<

在前面定义复数类时我们并未定义拷贝构造函数,也没有重载过赋值操作符,但是在例子中“c2 = c1”并未有语法错误,并且根据函数输出结果也可以得知可以完成我们所需要的赋值操作。这是因为系统默认为类提供了一个拷贝构造函数和一个赋值操作符,而数据一对一的拷贝也满足我们复数类的需求了。

系统提供的默认拷贝构造函数有一定缺陷,当类中的成员变量包含指针的时候就会有问题,会导致一些意想不到的程序漏洞,此时则需要重新定义一个拷贝构造函数,同样的此时系统提供的赋值操作符也已经不能满足我们的需求了,必须要进行重载。

#include
using namespace std;

class Array
{
public:
    Array(){length = 0; num = NULL;}
    Array(int * A, int n);
    Array(Array & a);
    Array & operator= (const Array & a);
    void setnum(int value, int index);
    int * getaddress();
    void display();
    int getlength(){return length;}
private:
    int length;
    int * num;
};

Array::Array(Array & a)
{
    if(a.num != NULL)
    {
        length = a.length;
        num = new int[length];
        for(int i=0; i
13. C++基本运算符重载_第1张图片
image.png

例子中我们以类成员函数的形式重载了赋值操作符,从arr1 = arr2语句开始看起。这个语句就会调用类中的操作符重载函数,我们可以将这一语句理解为:

    arr1.operator=( arr2 );

然后就会执行赋值操作符重载函数的函数体中的代码,在该函数体中我们为arr1重新开辟了一个内存空间,因此就可以规避arr1和arr2中的num指向同一块存储区域的风险。如此一来使用系统默认提供的赋值操作符所带来的风险就可以避免了。在这之后的语句中,我们还修改了arr2中的数据,但是这样的修改并没有影响到arr1。

当然,如果在类中并没有包含需要动态分配内存的指针成员变量时,我们使用系统提供的默认拷贝构造函数和赋值操作符也就可以了,无需再自己多此一举的重新定义和重载一遍的。


13.3 C++重载下标操作符

下标操作符是必须要以类的成员函数的形式进行重载的。其在类中的声明格式如下:

    返回类型 & operator[] (参数)
或
    const 返回类型 & operator[] (参数)

如果使用第一种声明方式,操作符重载函数不仅可以访问对象,同时还可以修改对象。
如果使用第二种声明方式,则操作符重载函数只能访问而不能修改对象。

在我们访问数组时,通过下标去访问数组中的元素并不具有检查边界溢出功能,我们可以重载下标操作符使之具有相应的功能。

#include
#include
using namespace std;

class Array
{
public:
    Array(){length = 0; num = NULL;}
    Array(int n);
    int & operator[]( int );
    const int & operator[]( int )const;
    int getlength() const {return length;}
private:
    int length;
    int * num;
};

Array::Array(int n)
{
    try
    {
        num = new int[n];
    }
    catch(bad_alloc)
    {
        cerr<<"allocate storage failure!"<= length){
        throw string("out of bounds");
    }
    return num[i];
}

const int & Array::operator[](int i) const
{
    if(i < 0 || i >= length){
        throw string("out of bounds");
    }
    return num[i];
}

int main()
{
    Array A(5);
    int i;
    try
    {
        for(i = 0; i < A.getlength(); i++){
            A[i] = i;
        }
        for(i = 0 ;i < 6; i++ ){
            cout<< A[i] < 
  
image.png

本例中定义了一个Array类,表示的是一个整形数组,在类中我们重载了下标操作符,使之具备检测下标溢出的功能。重载下标操作符,我们提供了两个版本的重载下标操作符函数:

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

注意:
第一个下标操作符重载函数最后面不带有const,加上const意味着该成员函数是常成员函数,如果第一个函数后面也加上了const,则两个函数仅有返回值不相同,这个不足以用于区分函数,编译器会提示语法错误。

这两种版本的下标操作符重载函数其实很好理解,一个是可以修改类对象,下面一个则只可以访问对象而不能修改对象。对于上面一种下标操作符重载函数的声明,以下两个语句都是有效的:

    arr[5] = 7;
    int var = arr[3];

换言之,我们既可以访问类对象,同时又能修改类对象。“arr[5]”其实可以理解为:

    arr.operator[]( 5 )

而对于下面一种下标操作符重载函数,我们不能修改对象,也就是说语句“arr[5] = 7;”语句是无效的,但是它依然可以用于访问对象,因此“int var = arr[3];”语句仍然有效。

我们再来看一下下标操作符重载函数的定义,在函数体内部,先进行下标越界检测,如果出现越界则抛出异常,否则就返回下标 i 所对应的数据。这两种版本的下标操作符重载函数的函数定义都是如此。

注意:
非const成员函数不能处理const对象,因此通常我们在设计程序时,会同时提供两种版本的操作符重载函数。
display顶层函数,用于打印对象数组中的所有元素

void display(const Array & A)
{
    for(int i=0; i < A.getlength(); i++)
        cout<< A[i] <

此时如果没有定义const版本的下标操作符重载函数,则将会出现语法错误而无法编译通过的。


13.4 C++函数调用操作符重载

与下标操作符重载函数相同,我们同样需要以类成员函数的形式对函数调用操作符“()”进行重载。其声明语法只有一种形式:

    返回类型 operator()( 参数列表 );
#include
#include
using namespace std;

class Array
{
public:
    Array(){len1 = 0; len2 = 0; num = NULL; }
    Array(int m, int n);
    int & operator()(int, int);
    const int & operator()(int, int)const;
    int getlen1()const {return len1;}
    int getlen2()const {return len2;}
private:
    int len1;
    int len2;
    int * num;
};

Array::Array(int m, int n)
{
    int size = m * n;
    try
    {
        num = new int[size];
    }
    catch(bad_alloc)
    {
        cerr<<"allocate storage failure!"<= len1)
        throw string("1 out of bounds!");
    if(j < 0 || j >= len2)
        throw string("2 out of bounds!");
    return num[ i*len2 + j ];
}

const int & Array::operator()(int i, int j)const
{
    if(i < 0 || i >= len1)
        throw string("1 out of bounds!");
    if(j < 0 || j >= len2)
        throw string("2 out of bounds!");
    return num[ i*len2 + j ];
}

int main()
{
    Array A(3,4);
    int i,j;
    for(i = 0; i < A.getlen1(); i++){
        for(j = 0; j < A.getlen2(); j++){
            A(i,j) = i * A.getlen2() + j;
        }
    }
    for(i = 0; i < A.getlen1(); i++){
        for(j = 0; j < A.getlen2(); j++){
            cout<< A(i,j)<<" ";
        }
        cout<
image.png

定义了一个Array类,这个类描述的是一个二维的数组,在类中我们先定义了一个默认构造函数,之后声明了一个带参数的构造函数“Array(int m, int n);”,所带的这两个参数分别是数组的两个维度的大小。
之后声明了一个函数调用操作符重载函数“int & operator()(int, int);”和“const int & operator()(int, int)const;”,同样的,因为只有常成员函数才能处理常对象,故依然在类中提供两个版本的函数调用操作符重载函数。
可以去看一下两个函数的函数定义,在它们的函数体中,我们先是做一个越界检测,当然对于二维数组而言,边界是有两个的,因此有两次边界检测的。如果没有越界则会返回对应的值。有了这两个函数调用操作符重载函数,我们就可以用A(i,j)的形式访问二维数组中的数据了。

当我们用A(i,j)的形式访问二维数组中的数据时,A(i,j)会调用类中的函数调用操作符重载函数,此时A(i,j)可以理解为:

    A.operator()(i, j);

主函数中异常捕获语句,我们先运行的是A(5, 3),故而是第一个边界越界了,因此先抛出“1 out of bounds!”的异常,而后又运行A(2, 6),此时为第二个边界越界,抛出“2 out of bounds!”的异常。


13.5 C++重载自增与自减操作符

自增“++”与自减“--”都是一元操作符,其前置和后置两种形式都可以被重载。有了前面介绍操作符重载的基础,我们就直接以示例的形式介绍自增与自减操作符的前置与后置重载方法。

#include 
#include 
using namespace std;

class stopwatch
{
public:
    stopwatch(){ min = 0; sec = 0;}
    void setzero() { min = 0; sec = 0; }
    stopwatch run();               // 运行
    stopwatch operator++();        // ++ i
    stopwatch operator++(int);     // i ++
    friend ostream & operator<<( ostream &, const stopwatch &);
private:
    int min; //分钟
    int sec; //秒钟
};

stopwatch stopwatch::run()
{
    ++ sec;
    if( sec == 60 )
    {
        min ++;
        sec = 0;
    }
    return * this;
}

stopwatch stopwatch::operator++()
{
    return run();
}

stopwatch stopwatch::operator++(int n)
{
    stopwatch s = *this;
    run();
    return s;
}

ostream & operator<<( ostream & out, const stopwatch & s)
{
    out<< setfill('0')<< setw(2) << s.min
       << ":" <
image.png

定义了一个简单的秒表类,该类有两个私有成员变量min和sec,分别代表分钟和秒钟。在类中声明的成员函数setzero是用于秒表清零,run函数是用于描述秒针向前进一秒的动作,之后是三个操作符重载函数,前两个分别是重载自增操作符,最后一个是重载输出操作符。
先来看一下run函数的实现,run函数一开始让秒针自增,如果此时自增结果等于60了,则应该进位,分钟加1,秒针置零。
再来看一下operator++()函数的实现,该函数时实现自增的前置形式,因此直接返回run()函数运行结果即可。
对于operator++ ( int n )函数,这是实现自增的后置形式,自增的后置形式返回值是对象本身,但是之后再次使用该对象时,该对象自增了,因此在该函数的函数体中,先将对象保存,然后调用一次run函数,之后再将先前保存的对象返回,在这个函数中参数n是没有任何意义的,它的存在只是为了区分是前置还是后置形式。
最后我们还重载了输出操作符,以便于我们打印计时结果。

对照主函数来看程序运行结果,主函数一开始我们定义了两个对象s1和s2,第一次操作是s1 = s2 ++; 采用的是后置形式,这可以理解为s1 = s2 并且s2自增,输出结果是s1是处于置零状态,s2自增了一秒钟。之后两个对象都清零,清零后s1 = ++ s2; 这个可以理解为s2自增并将自增结果赋给s1,如此一来两个对象都自增一秒钟。

自减操作符的重载跟自增操作符类似,这里就不再赘述了。


13.6 C++重载转型操作符

重载转型操作符。转型操作符重载函数的声明语法如下:

 operator 类型名 ();

转型操作符重载函数有几点需要注意的:

  • 函数没有返回类型;
  • 虽然没有返回类型但是函数体中必须有return语句,其返回类型是由类型名来指定的;
  • 转型操作符重载函数只能以类的成员函数的形式进行重载,而不能以友元函数或顶层函数的形式进行重载。
#include 
using namespace std;

class clock
{
public:
    clock(){hour = min = ap = 0;}
    clock(int h, int m, int ap);
    operator int();
private:
    int hour;
    int min;
    int ap;  // 0表示am, 1表示pm
};

clock::clock(int h, int m, int ap)
{
    hour = h;
    min = m;
    this->ap = ap;
}

//转型操作符重载函数
clock::operator int()
{
    int time = hour;
    if(time == 12){
        time = 0;
    }
    if(ap == 1){
        time += 12;
    }
    time *= 100;
    time += min;
    return time;
}

int main()
{
    clock c(5,7,1);
    int time = c;
    cout<

我们重载了一个时钟类clock,该类中我们声明了一个转型操作符重载函数,该函数可以将类类型的时间转换为一个整形,转换后的整数是军事时间。在主函数中我们定义了一个clock类的对象c,之后将其赋给一个整形变量time,因为我们定义了转型操作符重载函数,因此这一句话并没有出现语法错误。

转型操作符重载可以给程序带来一定的方便,但是建议还是谨慎使用。因为系统通常在需要的时候就会调用转型操作符重载函数,该函数的调用时隐式的,有时候会给程序带来一些意想不到的问题。


13.7 C++运算符重载注意事项

* 重载后运算符的含义应该符合原有用法习惯,重载应尽量保留运算符原有的特性。
* C++ 规定,运算符重载不改变运算符的优先级。
* 以下运算符不能被重载:.   .*   ::   ? :    sizeof。
* 重载运算符()、[]、->、或者赋值运算符=时,只能将它们重载为成员函数,不能重载为全局函数。

运算符重载的实质是将运算符重载为一个函数使用,运算符可以重载为全局函数。此时函数的参数个数就是运算符的操作数个数,运算符的操作数就成为函数的实参。运算符也可以重载为成员函数。此时函数的参数个数就是运算符的操作数个数减一,运算符的操作数有一个成为函数作用的对象,其余的成为函数的实参。

必要时需要重载赋值运算符=,以避免两个对象内部的指针指向同一片存储空间。

运算符可以重载为全局函数,然后声明为类的友元。
<<和>>是在 iostream 中被重载,才成为所谓的“流插入运算符”和“流提取运算符”的。

类型的名字可以作为强制类型转换运算符,也可以被重载为类的成员函数。它能使得对象被自动转换为某种类型。
自增、自减运算符各有两种重载方式,用于区别前置用法和后置用法。

你可能感兴趣的:(13. C++基本运算符重载)