C++中的运算符重载
operator是C++的一个关键字,它和C++的运算符连用,构成一个运算符函数名,例如operator++(),operator++在这里就是一个函数名。
现在我们有一个num类:
class num { public: num(){}
~num(){} set,get方法 private: int n; };
创建num类的一个对象:num i;
如果我们在程序中想通过++i来实现对象i的成员n的自加,那么我们就必须要重载前置自加运算符,我们可以简单的定义为如下形式:
void operator++(){++n;}
关键字operator的前面是它的返回类型,后面的++表示重载的是++运算符。那么为什么我执行++i就会调用operator++()呢?一般来说,C++中的任何运算都是通过函数来实现的,例如1+2,那么程序会自动将这个表达式解释为
operator(1,2);
接着再找一下有没有以operator+(int,int)为原型的函数,因为C++已经定义了一个这样的函数,因此会自动调用这个函数。如果是两个浮点数相加,则会调用以operator+(float,float)为原型的函数。同理,当我们对一个变量或对象自加时,如++i,那么就会自动调用i.operator++(),在函数体中对变量的数值或者对象的成员变量进行自加。
但上面我们重载的前置自加运算符有个问题,如果我们这样写num n=++i;是有错误的,因为我们定义的operator++()函数返回值为void,因此我们不能把一个空值赋给一个对象。为了解决这个问题,我们可以创建一个临时对象,将自加后的对象赋给另一个对象:
num operator++() { ++n; num t; t.setN(n); return t; }
但事实上如果我们的num类有一个带参的构造函数,那我们的程序就可创建一个无名的临时对象以简化我们的代码:
num operator++() { ++n; return num(n); }
无论上面那种方法,我们都需要创建一个对象,而这将调用构造函数,来为该对象开辟内存空间,而销毁该对象又要调用析构函数释放内存,因此我们每创建一次对象,系统都要牺牲一定的速度和内存空间。那么可不可以不创建临时对象呢,当然可以。我们知道this指针就是当前对象的标签,那么我们只要将当前对象的成员变量自加,然后返回this指针指向的当前对象即可:
num operator++() { ++n; return *this; }
至此,我们的前置自加运算符基本上算完成了,但其实这个函数还存在一个问题,就是上述我们定义的函数是按值返回的,这将创建一个*this的临时副本,为了避免创建这个临时副本,我们最好将operator++()的返回值定义为按别名返回,并且由于我们不能执行++++i这样的操作,因此我们最好还将它的返回值定义为常量
const num& operator++() { ++n; return *this; }
有了前置自加运算符,我们接下来重载后置自加运算符,所谓后置自加就是先返回当前值再自加。为了实现后置自加,我们可以先建立一个临时对象,将当前对象的值复制到临时对象中,然后将原始对象的值进行加1,最后返回临时对象。这里我们返回的是临时对象,所以应将其按值返回,而不能用别名的方式,因为按值返回会创建一个临时副本,并返回该副本,而按别名返回不会创建临时副本,那么我们创建的对象超出作用域会就会被析构掉,返回一个空对象。
const num operator++() { num tmp(*this); ++n; return tmp; }
到这里,问题又产生了,就是我们定义的前置自加和后置自加操作符的函数名是一样的,而且参数也是一样的,如果num类同时包含这两个函数,编译器将无法区分它们,因此,我们还需要对后置自加运算符进行一下改造:
const num operator++(int 0) { num tmp(*this); ++n; return tmp; }
我们给它加了一个参数,实际上该参数没有任何意义,它只是operat++()函数设置的一个信号,它提醒编译器目前添加了参数的函数是一个后置自加运算符函数。
同理我们还可以重载加法运算符:
const num operator+(const num& r) { return num(n+r.getN()); }
接下来我们重载赋值运算符函数operator=()。
假如现在有两个对象num a,b;然后将对象b赋给对象a:a=b,我们要实现这样的操作就要定义一个函数,这个函数由a调用,同时把b作为参数传递到函数中,在函数体中将b对象的成员变量赋给对象a,最后我们返回this对象:
const num operator=(const num& r) { n=r.getN(); return *this; }
在函数体中,我们是将一个对象的成员变量的值赋给另一个对象,这看起来貌似是没有问题的。我们知道C++中的类会默认提供构造函数,析构函数还有复制构造函数,而这个默认的复制构造函数他提供的复制方法也就是将一个对象的成员变量的值赋给另一个对象,而这样的复制只是一个浅层复制,如果我们的对象的成员变量是指针,那么这样的复制就存在重大问题。
经过浅层复制,这两个对象的成员变量---指针,将指向同一块内存区域,这样一个对象的成员指针指向的数据被修改,就会影响另一个,更为严重的是,如果我们删除了一个对象,那么另一个对象的成员指针将变成迷途指针。为了解决这个问题,我们在重载赋值运算符函数operator=()时,要使用深层复制。由于我们定义的函数是按值返回,所以在返回时会调用复制构造函数,而这个复制出的新对象又没有用上,复制构造函数执行完毕后又会调用析构函数来释放复制好的对象,因此假如我们没有使用深层复制,那么将导致原始成员指针成为了迷途指针。所以这里我们要对复制构造函数重新定义:
num(num const &s) { n=new int;//n为成员指针 *n=*(s.n); }
这样我们的operator=()在返回对象时调用复制构造函数将重新开辟一块空间,这样临时对象的指针保存的是一个新空间的地址,从而避免了两个对象指向同一地址的情况。
成员变量有指针的情况下,如果我们的重载赋值运算符按值返回,那么我们必须改造复制构造函数为深层复制,这样做有点麻烦,我们还有跟好的办法就是将赋值运算符按别名返回,这样将不会调用复制构造函数:
const num& operator=(const num& r) { *n=r.getN(); return *this; }
这回我们的复制运算符完美了吗?没有。如果我们无意将一个对象自赋值:a=a,在某些情况下,一个赋值运算符必须首先释放掉一个旧值,然后才能根据新值得类型分配新的数据。我们分析这个赋值语句,左边的变量与右边的变量是同一个变量,因此当左边的变量释放了自己的内存,然后根据右边的变量来给自己分配空间和赋值时,我们会发现右边的变量不见了。要解决这个问题,我们就得在operator=()函数的开头加入一条判断语句:
const num& operator=(const num& r) { if(this==&r) return *this; *n=r.getN(); return *this; }
这样我们的赋值运算符函数operator=()就算完成了。
最后,说明一下什么运算符不能重载。C++的运算符大部分都可以重载,但以下的不可以
.运算符
::运算符
*运算符
?符号
#预处理标志