运算符重载

使用c++编写程序时,不仅要使用基本数据类型,还要设计新的数据类型->

一般情况下,基本数据类型的运算都是用运算符来表达,语义简单并且直观,如:

int a,b,c;a=b+c;

对于基本数据类型运算的汇编语言实现,其中就隐含着运算符重载的概念,如:

int a,b,c;c=a+b;float a,b,c;c=a+b;

整型加法的汇编代码为:
mov eax,dword ptr[ebp-4]
add eax,dword ptr[ebp-8]
mov dword ptr[ebp-0Ch],eax

实数型加法的汇编代码为:
fld eax,dword ptr[ebp-4]
fadd eax,dword ptr[ebp-8]
fstp dword ptr[ebp-0Ch],eax

虽然都是+,整型和实数型的基本操作指令是不同的,但是对于高级程序语言使用者而言则完全感受不到
这是因为通过对运算符的重载,在底层实现了相关运算的具体操作

如果将运算符直接作用于类对象,编译器将无法识别运算符的语义,因为对类对象的运算并没有提前实现,如:

Complex ret,c1,c2;ret=c1+c2; 编译错误

因此,需要一种机制重新定义运算符作用在类类型上的含义,即运算符重载,让需要的运算符在自定义类型上按要求进行操作

运算符重载的实质是函数重载,即实现过程中把指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参,然后根据实参的类型匹配函数
因此,可以在类中使用成员函数的形式来定义运算符成员函数,例:

class Complex
{
    double re,im;
public:
    Complex(double r=0.0,double i=0.0):re(r),im(i){}
    Complex add(Complex c){return Complex(re+c.re,im+c.im);}          //通过定义成员函数的方法实现加法操作,但这种方法不够直观
    Complex operator+ (Complex c){re+=c.re,im+=c.im;return *this;}    //使用operator关键字重载Complex类的+运算符为成员函数形式
};                                                                    //函数体实现方法与成员函数相同
int main()
{
    Complex complex1(1,2),complex2(3,4),temp1,temp2;
    temp1=complex1.add(complex2),temp2=(complex1+complex2);           //运算符+的隐式调用
    cout<<temp1.re<<'+'<<temp1.im<<'i'<<' ';                          //temp2等效于显式调用形式complex1.operator+(complex2)
    cout<<temp2.re<<'+'<<temp2.im<<'i'<<endl;                         //输出结果:4+6i 4+6i
}                                                  //采用运算符重载的方式更加直观,并且可以达到与基本数据类型的运算形式一样的效果

使用成员函数的情况下必须先单独构造一个类对象,再调用运算符成员函数进行运算,即第一个操作数必须是已定义运算符的类对象或类对象的引用(可以理解为这个对象作为运算符函数的调用者)
(此外,对象调用成员函数运算符实际上相当于默认将this绑定到操作数)

同时也可以考虑定义为友元函数的形式,从而可以将所有操作数直接作为参数进行运算,例:

class Complex
{
public:
    double re,im;
    Complex(double r=0.0,double i=0.0):re(r),im(i){}
    Complex add(Complex c){return Complex(re+c.re,im+c.im);}
    friend Complex operator+(Complex a,Complex b){return Complex(a.re+b.re,a.im+b.im);} //重载Complex类的+运算符为友元函数形式
};
int main()
{
    Complex complex(5,6);
    cout<<(1+complex).re<<'+'<<(1+complex).im<<'i'<<endl;        //编译器在见到操作数为complex后认为此处的+是友元运算符重载函数
}                                                                //因此在将1作为参数传递时隐式地将1强制类型换为(Complex)1

从此例可以看出,友元函数是将算术表达式中的操作数按顺序传给参数的

友元函数和成员函数的抉择

多数情况下,类的运算符可以重载为类的成员函数,也可以重载为友元函数,但一般说来:

  • 单目运算符最好被重载为成员函数
  • 双目运算符最好被重载为友元函数,双目运算符重载为友元函数比重载为成员函数更方便,但也存在些例外
    • 有的双目运算符不能重载为类的友元函数:= () [] -> 这是c++语言标准的特殊规定
      • 主要因为这几个运算符c++都定义了默认的成员函数,对编译器来说每当一个对象后面出现这些运算符时会默认调用这些默认或重载的成员函数运算符
      • 而友元函数的运算符的行为是与成员函数的不同的(很重要的一点是没有this指针)
        • 想象一下如果允许这种重载,那么类似于1=obj,4.5(obj),5[obj]等等在编译上都是可行的(如定义:friend void operator=(int,OBJ))
        • 很明显这会导致程序极其混乱,因此c++在语法层面直接杜绝这种情况
  • 类型转换函数(圆括号)只能定义为类的成员函数,不能定义为类的友元函数(实际上就是圆括号运算符的行为都是默认的)
  • 如果一个运算的操作需要修改对象的状态,则重载为成员函数较好
    • 因为成员函数自带this指针
  • 如果运算符所需要的操作数(尤其是第一个操作数)希望有隐式类型转换,或者其必须是一个不同类的对象或者基本数据类型,则只能选择友元函数
    • 成员函数是无法对调用对象转换类型的
  • 当需要重载的运算符具有交换性时,一般重载为友元函数
    • 因为友元函数的参数之间是等价的,随便哪个在前在后都行(只要符合定义的行为),满足交换律
  • 对于单目运算符++,--来说前缀式和后缀式是不一样的,一般编译器默认在重载++,--时可以向运算符函数传入一个int占位参数以做区分
    • 此时在调用自增自减的运算符函数时使用前缀式(++a,--a)调用的是不带参数的运算符函数,使用后缀式(a++,a--)调用的是带占位参数的运算符函数
    • 用int做占位符的原因,个人猜测是因为在算术表达式中++\--作为单目运算符只跟一个操作数结合,如果++\--跟a结合后又跟着一个操作数,说明这是后缀的情况,因此再额外接收一个变量并且不需要其参与运算符的定义

最后,其实之所以定义为友元函数对访问对象私有成员的需求,如果重载的运算符不需要访问私有成员的话,那么定义为普通的函数也是一样的。

运算符重载示例程序1:实现计数器(Counter)类的自增,自减,取值,输出输入计数的运算

#include<iostream>
using namespace std;
class Counter
{
    int count;
public:
    Counter(int c):count(c){}
    Counter operator++()                      //也可以重载为友元函数friend Counter operator++(Counter &c);
    {                                         // Counter operator++(Counter &c){c.count++;return c;}
        count++;                                               
        return *this;                         //重载为友元函数时需注意应使用引用传参,不然无法对操作对象的值进行修改
    }                                         //另外其实返回引用的话效率更高
    Counter operator++(int)                  
    {
        Counter temp=*this;
        this->count++;
        return temp;                          //注意后置的++关键点在于必须用一个新的对象存储自增前的状态
    }
    Counter operator--()
    {
        count--;
        return *this;   
    }
    Counter operator--(int)
    {
        Counter temp=*this;
        this->count--;
        return temp;
    }
    int operator()()
    {
        return count;
    }
    friend ostream &operator<<(ostream &ocounter,Counter c)    //重载operator<<(或>>)必须使用引用作为返回类型
    {                                                          //因为系统默认的<<和>>的构造函数是全局保护成员,是不可以创建局部变量的
        ocounter<<c.count;                                     //同理ocounter也必须使用引用,因为无法创建临时ocounter对象
        return ocounter;
    }
    friend istream &operator>>(istream &icounter,Counter &c)   //重载operator>>时还要注意Counter对象也要传引用
    {                                                          //不然输入的值是无法修改Counter对象的
        icounter>>c.count;
        return icounter;
    }
};
int main()
{
    Counter counter(0);
    int v=counter();
    cin>>counter;
    cout<<v<<endl;
    cout<<++counter<<' '<<counter<<' '<<--counter<<' '<<counter<<endl;
    cout<<counter++<<' '<<counter<<' '<<counter--<<' '<<counter;
}

对于赋值运算符而言,如果没有为类重载赋值运算符,那么编译器会生成一个缺省的赋值运算符函数,其作用是通过位拷贝方式将源对象复制到目的对象,在这一点上,赋值运算符和拷贝构造函数的情况非常类似,相同之处在于都是将一个对象的数据成员复制到另一个对象中,不同点在于拷贝构造函数是在初始化一个新对象,而赋值运算符是在为这个已经创建的对象进行修改
编译器的缺省赋值运算符重载函数同样存在一些缺陷,当类持有一些特殊资源时,如:

  • 动态分配的内存、打开的文件、指向其他数据的指针、网络连接等,默认的赋值运算符就很难处理
    • 此时如果使用默认赋值运算符的话对象的值会被直接修改而原来的地址空间不一定被释放,会出现很多未知的问题(说白了就是默认的没有那么多幺蛾子,不会搞这些额外操作)

运算符重载示例程序2:重载赋值运算符使赋值的同时释放掉原本的内存空间

#include<iostream>
using namespace std;
class Cstring
{
private:
    int size;
    char *str;
public:
    Cstring(int len)
    {
        str=new char[size];
    }
    Cstring(Cstring &cstring):size(cstring.size)
    {
        str=new char[size];
        strcpy(str,cstring.str);
    }
    Cstring operator=(Cstring cs);
    ~Cstring();
};
Cstring Cstring::operator=(Cstring cs)
{
    if(this==&cs)return *this;
    delete[]str;                             //释放原本动态分配的空间
    size=cs.size;
    str=new char[size];
    strcpy(str,cs.str);                      //这里相当于重新构造了一个Cstring对象
    return *this;
}

重载运算符遵循的规则

  • 大多数预定义的操作符可以被重载,重载后的优先级结合性,及所需操作数数目都不变
  • 少数几个c++运算符不可以重载,如:作用域符::,预处理符#,三目运算符?:,访问成员符.,指针或取值运算符 *,等等
  • 不能重载非运算符的符号,如:分号;
  • c++不允许重载不存在的运算符(无法自创运算符),如:$,**等
  • 当运算符被重载时,是被绑定在一个特定的类之上的,当此运算符不作用在特定类型上时将保持原有含义
  • 应当尽可能保持重载运算符原来的语义,比如某个程序中用+表示减,用*表示/,程序可读性会非常的差

特殊运算符的重载

[]的重载

该重载函数在类中的声明格式如下:

返回值类型&operator;

或者可以声明为常成员

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

  • 使用第一种声明方式,[]不仅可以访问元素,还可以修改元素。
  • 使用第二种声明方式,[]只能访问而不能修改元素。

在实际开发中应该同时提供以上两种形式,这样做是为了适应const对象,因为通过const对象只能调用const成员函数,如果不提供第二种形式,那么将无法访问const对象的任何元素。

例:实现变长的数组Array类,重载成员访问符[]

#include <iostream>
using namespace std;
class Array{
public:
    Array(int length = 0);
    ~Array();
public:
    int & operator[](int i);
    const int & operator[](int i) const;
public:
    int length() const { return m_length; }
    void display() const;
private:
    int m_length;  //数组长度
    int *m_p;  //指向数组内存的指针
};
Array::Array(int length): m_length(length){
    if(length == 0){
        m_p = NULL;
    }else{
        m_p = new int[length];
    }
}
int &  Array::operator[](int i){
    return m_p[i];
}
const int & Array::operator[](int i) const{
    return m_p[i];
}
Array::~Array(){
    if(m_p)delete[] m_p;
    delete this;
}
void Array::display() const{
    for(int i = 0; i < m_length; i++){
        if(i == m_length - 1){
            cout<<m_p[i]<<endl;
        }else{
            cout<<m_p[i]<<", ";
        }
    }
}
int main(){
    int n;
    cin>>n;
    Array A(n);
    for(int i = 0, len = A.length(); i < len; i++){
        A[i] = i * 5;
    }
    A.display();
    const Array B(n);
    cout<<B[n-1]<<endl;  //访问最后一个元素   
    return 0;
}

()的重载

在c++中,类型的名字(包括类的名字)本身也是一种运算符,即类型强制转换运算符。

例:重载Complex类的(double)运算符,返回Complex的实部

#include <iostream>
using namespace std;
class Complex
{
    double real, imag;
public:
    Complex(double r = 0, double i = 0) :real(r), imag(i) {};
    operator double() { return real; }  //重载强制类型转换运算符 double
};
int main()
{
    Complex c(1.2, 3.4);
    cout << (double)c << endl;  //输出 1.2
    double n = 2 + c;  //根据类型提升规则将表达式中所有运算符转为double,因此调用c的类型转换符operator.double()
    cout << n;  //输出 3.2
}

new/delete的重载

new、new[]、delete、delete[]虽然也都是系统定义的运算符,但均可以重载,并且既可以重载为友元函数也可以重载为成员函数。
需要注意的是在重载new或new[]时,无论是作为成员函数还是作为友元函数,它的第一个参数必须是size_t(unsigned或unsigned long,具体看实现)类型,返回类型为void*。size_t表示的是要分配空间的大小,对于new[]的重载函数而言,size_t则表示所需要分配的所有空间的总和。重载new函数也可以有其他参数,但都必须有默认值,并且第一个参数的类型必须是size_t。(因为重载运算符必须与c++中该运算符原本的用法相同)

void*operator new(unsigned long length){
    Array*newa=(Array*)malloc(sizeof(Array));
    newa->m_length=length;
    newa->m_p=(int*)malloc(length*sizeof(int));
    return newa;
}
void*operator new[](unsigned long length){
    Array*news=(Array*)malloc(length*sizeof(Array*));
    return news;

delete和delete[]的返回值都是void类型,并且都必须有一个void*作为参数,该指针指向需要释放的内存空间。

void operator delete(void*ptr){
    free(((Array*)ptr)->m_p);
    free(ptr);
}
void operator delete[](void*ptr){
    Array*p=(Array*)ptr;
    while(p->m_length--)delete p;
}