9. 运算符重载
运算符只是一种语法上的方便,也就是另外一种函数的调用方式,这种函数调用的参数不在()中,而是在运算符的附近,由编译器决定调用哪个运算符函数。例如使用+做floating-point运算,编译器调用浮点运算的加法运算(一般插入内联代码,或浮点运算处理命令)。如果用+运算整形和浮点型,编译器调用一个特殊函数将int转换成float再调用浮点加法运算函数代码。
C++中可以创建自定义类的运算符,这就是运算符重载了。运算符重载的定义与普通函数一样,只是在函数名(运算符)前加上关键字operator。当编译器遇到合适的模式时会调用相应函数。
1)敬告和语法
不应该滥用运算符重载,它是一种语法上的方便,是另外一种调用函数的方式而已。因此只有涉及类的代码更易写,尤其是更易读时才有理由重载运算符。如果不是这样,就不要bother了。
仅在包含内置数据类型的表达式中的所有运算符是不可能改变的。我们不能重载如下运算符改变其行为:1<<4;1.414<<2;,只有包含了自定义类型的表达式才能有重载运算符。
定义重载运算符就像定义函数一样,只是该函数名字是operator@,@代表重载的运算符。函数参数列表中的个数取决于:运算符是一元还二元的;运算符被定义为全局函数还是成员函数。
2)可重载的运算符
可重载的一元运算符有:+,-,~,*,&,!,++,--。其中重载的++和―运算符,是通过函数参数的不同区分前缀或后缀的。编译器会为int参数传递一个亚元变量值来为后缀版本产生不同的标记。
可重载的二元运算符有:+,-,*,%,/,^,&,|,<<,>>和=,+=,-=,*=,%=,/=,^=,&=,|=,<<=,>>=和==,!=,>,<,>=,<=,&&,||。其中和%,/相关的运算符重载函数中要考虑除数为0的情况(例如assert())。
3)参数和返回值
对于重载运算符函数的参数传递和返回方式是遵循一种合乎逻辑的模式。
对于参数的传递,普通运算符(+,-,~,&,!)和布尔运算符不会改变参数,所以const引用传递是主要传递方式。当函数是一个成员函数时,就转换成一个const函数。只有赋值相关运算符,为改变左参数值,因此左参数不是const,仍按地址传递。
返回值的类型取决于运算符的含义,如果运算符是为了产生一个新值,那么你要产生一个新的对象作为返回值(如+,-,……),这个对象为临时对象不能当做左值来处理,那么它就就要是const的。
赋值运算符会修改左边的参数,为了有像a=b=c;这样的表达式,赋值运算符会返回一个左参数的引用(返回对象的话,会调用构造函数,这很浪费资源),这个引用时const还是non const的取决于你的需求,如果为使(a=b).func();表达式可行,因此返回的是non const引用。
自增自减,无论前缀还后缀均修改了对象,所以这个对象不能做为常量类型。前缀只需要返回一个引用*this,而后缀必须通过传值方式返回。返回是const还是non const的就要就事论事了。
这里还有一个返回值优化的问题,例如ruturnInteger(left.i + right.i);与Integer tmp(left.i + right.i); return tmp;相比,前者高效,因为编译器知道仅仅返回这样一个对象,因此编译器直接在外部返回值的地方创建这个对象。而后者将发生三件事:创建tmp;拷贝tmp到外部返回值的存储单元里;tmp在作用域尾时析构。
4)不常用运算符
不常用运算符有:[],new,delete,“,”,->,->*。
下表运算符operator[],必须是成员函数并且它只接受一个参数。这个运算符返回一个引用,所以可以方便的用于等号左侧。运算符new和delete用于控制动态存储分配,并能按多种不同的方式进行重载,后面会讲到。
operator,只是对象间的隔开符,而不是函数参数中的或变量声明中的。下面是operator,的一个例子:
class After{
public:
constAfter& operator,(const After&) const {
return*this;
}
};
class Before {};
Before& operator,(int, Before& b) {
returnb;
}
int main(void) {
Aftera, b;
a,b; //operator comma called
Beforec;
1,c; //operator comma called
}
operator->在希望对象表现的像一个指针时,通常就要用到。如果想用类包装一个指针以使指针安全,或是在迭代器通用的用法中,这样做会特别有用。迭代器是一个对象,这个对象可以在其它对象的容器或者集合上移动,而不用对容器进直接的访问。
指针间接运算符一定是一个成员函数,它必须返回一个对象(或对象引用),该对象也有一个指针间接运算符;或者必须返回一个指针,被用于选择指针间接引用运算符箭头所指向的内容。下面是一个例子(指针间接运算符返回的是指针):
class Obj {
staticint i, j;
public:
voidf() const {cout << i++ << endl;}
voidg() const {cout << j++ << endl;}
};
int Obj::i;
int Obj::j;
class ObjContainer {
vector<Obj*>a;
public:
voidadd(Obj* obj) {
a.push_back(obj);
}
classSmartPointer;
friendSmartPointer;
classSmartPointer {
ObjContainer&oc;
unsignedint index;
public:
SmartPointer(ObjContainer&objc) : oc(objc) {index = 0;}
booloperator++() {
if(index>= oc.a.size()) return false;
if(oc.a[++index]== 0) return false;
returntrue;
}
booloperator++(int) {
returnoperator++();
}
Obj*operator->() const {
if(oc.a[index]== 0) return null;
returnoc.a[index];
}
};
SmartPointerbegin() {
returnSmartPointer(*this);
}
};
int main(void) {
constin sz = 100;
Objo[sz];
ObjContaineroc;
for(inti=0; i<sz; i++)
oc.add(&o[i]);
ObjContainer::SmartPointersp = oc.begin();
do{
sp->f();
sp->g();
}while(++sp);
}
operator->*是一个二元运算符,它是专门为模仿前面介绍的指向成员的指针。(1)和operator->一样,指向成员的指针间接运算符通常同某种代表smartpointer的对象一起使用;(2)在定义operator->*时要注意它必须返回一个对象,对于这个对象,可以用正在调用的成员函数为参数调用operator();(3)operator()函数的调用必须是成员函数,它是唯一的允许在它里面有任意个参数的函数,这使得对象看起来像一个真正的函数,从重载operator->*的和未重载的比较可以看出,->*和对象连接在一起了,使得对象在运行时可以改变操作;(4)要创建一个operator->*,必须首先创建带有operator()的类,这是operator->*将返回的类型,该类必须获取一些必要的信息,以使当operator被调用时,指向的成员的指针可以对对象进行间接调用。
下面是一个例子:
class Dog{
public:
intrun(int i) const {
returni;
}
int eat(int i)const {
return i;
}
int sleep(int i)const {
return i;
}
typedefint(Dog::*PFM)(int) const;
//operator->*must return an object that has an operator()
classFunctionObject {
Dog* ptr;
PFM pmem;
public:
FunctionObject(Dog*mp, PMF pmf) :ptr(mp), pmem(pmf) {}
//make the call using the objectpointer and member pointer
int operator()(int i) const {
return (ptr->*pmem)(i);//call
}
};
FunctionObjectoperator->*(PMF pmf) {
return FunctionObject(this, pmf);
}
};
int main(void) {
Dogw;
Dog::PMFpmf = &Dog::run;
cout<< (w->*pmf)(1) << endl;
pmf= &Dog::eat;
cout<< (w->*pmf)(2) << endl;
pmf =&Dog::sleep;
cout<< (w->*pmf)(3) << endl;
}
一段英文原文:
Notice that whatyou are doing here, just as with operator->, is inserting yourself in themiddle of the call to operator->*. This allows you to perform some extra operations if you need to.
5)不能重载的运算符
成员选择运算符operator.,点在类中对任何成员都有一定的意义。如果允许重载它,就不能用普通的方法访问成员,只能用指针和operator->访问。
成员指针间接引用operator.*,因为与operator.同样的原因而不能重载。
没有幂运算符,可以用函数实现。
不存在用户自定义的运算符,不这样做的原因是难以决定其优先级,另也没有必要增加麻烦。
不能改变优先级规则,否则很难记住他们。
6)非成员运算符
有时候重载运算符时需要使左操作数是另一个类的对象,这样就需要将运算符重载函数放到类的外部,而在类中声明其为friend。常见的地方是重载运算符<<和>>为iostreams。由于iostreams是一个基础C++库,你很肯能回想为你自己的类而重载这些运算符。
7)Basic guidelines
Operator |
Recommended use |
All unary operators |
Member |
=,(),->,->* |
Must be member |
+=,-=,*=,%=,/=,^=,&=,|=,>>=,<<= |
Member |
All other binary operators |
Non-member |
8)赋值运算符重载
先看这样几个表达式:MyType b;MyType a = b; a = b;。在第一个=是调用的拷贝构造函数,第二个=才调用的赋值运算符。
前面讲过operator=只能是成员函数,它是和“=”左边的对象紧密联系的。如果可以将operator定义为全局的,那么你就可能会定义内置的“=”:int operator=(int, MyType);//not allowed。
当你创建一个operator=,你必须从右边拷贝所有需要的信息到当前对象(调用运算符对象)以完成类的“赋值”,对于简单对象这是显然的。operator=返回一个引用,使得更复杂的表达式能够实现(=结合方向从右到左)。同样operator<<类似,但结合方向左到右。
operator=函数中一定要检测自我赋值,因为当类的实现修改了,可能会引起对象的改变,如果没有检查,可能会出现一个很难找的bug。
如果没有创建自己的operator=,编译器将自动创建一个,这个运算符模仿自动创建的构造函数:如果包含对象(或从别的类继承而来),对于这些对象,operator=被递归调用,这被称作memberwise assignment。
9)运行时拷贝
当类中包含了一个其它类型的指针时,如果简单的拷贝一个指针,就会出现两个对象同时指向一个空间,要解决这个问题就要自己标记了。这样的话对于一个含有其它类型指针的类来说,类中有四个函数要定义:必要的构造函数、拷贝构造函数、operator=(either define it or disallow it)和析构函数。下面是一个解决自己标记问题的例子,采用的是引用计数的方法,又叫做运行时拷贝:
class Dog {
stringname;
intrefcount;
Dog(const string& name) : name(name), refcount(1) {}
//Prevent assignment:
Dog& operator=(const Dog&);
public:
//Dogs can only be created on the heap:
static Dog* make(const string& name) {
return new Dog(name);
}
Dog(constDog& d) : name(d.name), refcount(1) {}
~Dog() {}
void attatch() {
refcount++;
}
void detatch() {
assert(refcount != 0);
if(--refcount == 0) delete this;
}
Dog* unalias() {
if(refcount ==1) return this;
--refcount;
return newDog(*this);
}
voidrename(const string& newname) {
name = newname;
}
};
class DogHouse {
Dog*p;
stringhousename;
void unalias() {
p = p->unalias();
}
public:
DogHouse(Dog* dog, const string& house) : p(dog),housename(house) {}
DogHouse(const DogHouse& dh) : p(dh.p),housename(dh.housename) {
p->attatch();
}
DogHouse& operator=(const DogHouse& dh) {
if(this == & dh) return dh;
p->detatch();
p = dh.p;
housename = dh.housename;
p->attatch();
return *this;
}
~DogHouse() {
p->detatch();
}
voidrenamehouse(const string& newname) {
housename= newname;
}
voidrenamedog(const string& newname) {
unalias();
p->rename(newname);
}
Dog* getdog() {
unalias();
return p;
}
};
10) 自动类型转换
在C和C++中,如果编译器看到一个表达式和函数调用中使用了一个不合适的类型,它经常会执行一个自动类型转换,从现在的类型到所要求的的类型。在C++中可以通过定义自动类型转换函数来为用户自定义类型达到相同效果,有两种方式可以做:特殊类型的构造函数和重载运算符。
构造函数自动类型转换例子:
class One {
public:
One(){}
};
class Two {
public:
Tow(constOne&) {}
};
void f(Two) {}
int main() {
Oneone;
f(one);//wantsa Two, has a One
}
在这个例子的情况下,自动类型转换避免了定义两个f()重载版本的麻烦。然而代价是调用Two的构造函数(隐藏调用),如果关系f()的效率,就不要使用这种方法。有时为了避免自动类型转换为用户带来麻烦,可以将自动类型转换的构造函数用explicit声明,这样编译器被告知不能使用该构造函数执行任何自动类型转换。如果用户想进行转换就必须写出代码如:f(Tow(one))。
重载运算符转换例子:
class Three {
inti;
public:
Three(intii=0; int=0) : i(ii) {}
};
class Four {
intx;
public:
Four(intxx) : x(xx) {}
operatorThree() const {return Three(x);}
};
void g(Three) {}
int main(void) {
Fourfour(1);
g(four);
g(1); //calls Three(1,0)
}
用构造函数技术,目的类执行转换;而使用运算符技术,是源类执行转换。
使用全局运算符而不用成员运算符最大的好处是在全局版本中的自动类型转换可以针对左右任一操作数。如果想两个操作数都被转换,全局运算符重载可以节省很多代码,下面是一个例子:
class Number {
intI;
public:
Number(intii=0) : i(ii) {}
constNumber operator+(const Number& n) const {
returnNumber(i + n.i);
}
friend constNumber operator-(const Number& n);
}
const Number operator-(const Number&n1, const Number& n2) {
returnNumber(n1.i �C n2.i);
}
int main(void) {
Numbera(30), b(23);
a+b;//ok
a+1;//2ndarg converted to Number
//!1+a;//Wrong! 1st arg not of type Number
a-b;//ok
a-1;//2ndarg converted to Number
1-a;//1starg converted to Number
}
当类中含有一个字符串时,那自动类型转换就显得非常有用。如果没有自动类型转换,那么要用到C库中的字符串处理函数,就必须为库中的每个函数创建成员函数,就像下面的例子一样:
class Stringc {
strings;
public:
Stringc(conststring& str=””) : s(str) {}
intstrcmp(const string& str) const {
return::strcmp(s.c_str(), str.s.c_str());
}
//……ect.,forevery function in cstring.h
};
但是如果提供一个自动类型转换,就能够使用<cstring>中的所有函数,如下例子:
class Stringc {
strings;
public:
Stringc(conststring& str=””) : s(str) {}
operatorconst char*() const {
returns.c_str();
}
};
int main() {
Stringcs1(“Hello”), s2(“there”);
strcmp(s1,s2); //sdandard c function
}
自动类型转换也有它的缺陷,当在源类型和目的类型中都含有相同功能的自动类型转换是,编译器就不知道调用哪一个。最好提供单一的从一个类型到另一个类型的自动转换方法。
当类提供了不止一种类型的自动转换时,会发生一个很难发现的错误,假如一个Apple类,它提供了向Orange类型和Pear类型的自动转换,而又在类的外部定义了两个重载函数,如void eat(Orange){} 和void eat(Pear){},那么当有表达式Apple c; eat(c);时,编译器会调用两个重载函数中的哪一个呢?
通常对于自动类型转换的解决方法是,只是提供一个从某类型像另一个类型转换的自动类型转换版本。当然也可以有多个向其他类型的转换,但它们不应该是自动转换,而是应该用如makeA()和makeB这样的名字来显式的调用函数,也可以使用目的类型中的explicit构造函数来解决。
自动类型转换还有一个隐藏的行为,先看下面的例子:
class Fi {};
class Fee {
Fee(int){}
Fee(constFi&) {}
};
class Fo {
inti;
public:
Fo(intx=0) i(x) {}
operatorFee() const {
returnFee(i);
}
};
int main(void) {
Fofo;
Feefee = fo;
}
在用fo对象去初始化fee对象的时候,Fo类中的向Fee自动类型转换函数被调用,然而在Fee类中没有用Fee类型的对象去初始化一个Fee对象的拷贝构造函数,那么编译器将创建这样的一个函数。
自动类型转换应当小心使用。同所有的重载运算符相比,它在减少代码方面是非常出色的,但不值得无缘无故的使用。
10. 动态对象创建
在C中我们使用malloc和free在运行时从堆中分配和释放存储单元。malloc和free是用到操作系统提供的系统调用来实现的,它从堆里搜索一块足够大的内存来满足请求(这个过程可能很快,也可能要试探几次,因此每次运行malloc的时间是不一样的),并在返回分配的内存的地址和大小记录下来,这样以后调用malloc就不会使用它,而且当调用free释放这块内存时,系统就知道在哪释放多大的内存。
但是malloc和free在C++中不能很好的工作,原因是在C++中一块内存被分配后,C++总是试图去初始化它,而当一块内存不需要的时候,总是先析构然后释放存储空间。我们知道在C++中构造函数和析构函数的调用都是由编译器完成的,编译器会将一块存储空间的地址隐式的传递给构造函数或析构函数。如果malloc和free在C++使用,那么意味着用户自己就要向构造函数或析构函数传递存储单元的地址,这样是不安全的,因为用户不能保证每次都能够操作正确,因而应该将存储空间的分配和初始化、析构和存储空间的释放放在一起,使他们成为一个机械的步骤,这样的话就应该把动态对象创建和释放交给编译器来做,成为语言的核心。
1)new和delete
当用new运算符动态的创建一个对象时,有两件事发生了,存储空间的分配以及构造函数的调用。new使得在堆中创建对象的过程变得简单了,它带有内置的长度计算,类型转换和安全检查。
delete用于释放一个动态创建的对象,调用delete时也有两件事发生,析构函数调用,释放存储空间。如果删除的对象的指针是0,将不发生任何事情。为此,建议在删除指针后立即把指针赋值为0以避免对它删除多次,对一个对象删除多次可能会产生某些问题。delete一个指针的时候需要知道该指针的类型,如果delete一个void指针那么就仅仅释放了存储空间,而没有调用析构函数,这样的话,如果一个对象的数据成员中有指针,那么仅仅释放了存储指针的存储空间,而指针指向的空间就没有释放(这个过程应该在析构函数中完成的)。
new和delete也可用于数组,当new用于数组时,动态申请了多少个对象,就会调用构造函数多少次。除了在栈上的集合初始化外,在堆上的数组创建,必须有一个默认的构造函数,因为没有参数的构造函数必须为每个对象调用。释放动态数组时,使用delete[],这个[]告诉编译器生成取出数组中对象个数的代码,然后调用那么多个数的析构函数。
2)内存耗尽时的动作
当operatornew()找不到足够大的连续内存块时,一个new-handler的特殊函数将会被调用,或者更常用的是用自定义的new-handler函数。new-handler默认动作是产生一个异常,然而如果我们在堆分配,至少要用“内存耗尽”的信息代替new-handler,并异常中断程序。
通过包含new.h来替换new-handler,然后以想装入的函数地址为参数调用set_new_handler()函数。下面是一个例子:
#include<iostream>
#include<cstdlib>
#include<new>
using namespace std;
int count = 0;
void out_of_memory() {
cerr<< “Memory exhausted after” <<
<<count<<”allocations”<<endl;
exit(1);
}
int main(void) {
set_new_handler(out_of_memory);
while(1){
count++;
newint[1000]; //exhausts memory
}
}
new-handler函数必须不带参数且其返回值为void。new-handler的行为是与operatornew紧密联系的,所以如果重载了operator new,那么new-handler将不会按默认调用。如果仍想调用new-handler,则我们不得不在重载了的operator new()的代码中加上做这些工作的代码。
当然可以写更复杂的new-handler,甚至它可以回收内存。
3)重载new和delete
使用C++内置的new和delete在一些特殊的情况下并不能满足需求,最常见的是出于对效率的考虑,比如当要创建和销毁一个特定类的非常多的对象以至于new和delete的存储空间分配方案成了程序效率的瓶颈的时候;另一个问题是堆碎片的问题,分配不同大小的内存可能会在堆上产生很多碎片,以至于很快用完内存。C++提供了对new和delete重载的支持,这样我们就可以改变原有的分配方法,编译器将用重载的new代替默认的版本去分配内存,然后调用构造函数,但重载new也仅仅改变了内存分配部分,delete也是类似的。
当重载new时,也就替换了当内存消耗殆尽的行为,所以必须在operatornew()里决定做什么:返回0,写一个调用new-handler的循环试着再分配,或者产生一个bad-alloc的异常。
(1) 重载全局new和delete
对全局版本的new和delete重载,就意味着默认版本完全不能被访问,甚至在这个从新定义的版本里也不能访问它。
重载的new必须有一个size_t的参数(sizes的标准C类型)。这个参数由编译器产生并传递给我们,它是要分配内存的长度。operator new()的正常返回值是一个void*指针,而不是指向特定类型的指针,因为在这里做的仅仅是内存分配,要在构造函数调用(编译器的工作)完成后,才完成了对象的创建。operator delete()的参数是一个有operator new()分配的内存的void*。参数是一个void*,是对对象析构后的到的指针。operator deleted()返回类型是void。
下面是一个例子:
#include <cstdio>
#include <cstdlib>
using namespace std;
void* operator new(size_t sz) {
printf(“operatornew : %dBytes\n”, sz);
void*m = malloc(sz);
if(!m)puts(“out of memory”);
returnm;
}
void operator delete(void* m) {
puts(“operatordelete\n”);
free(m);
}
class S {
inti[100];
public:
S(){puts(“S::S()\n”);}
~S(){puts(“S::~S()\n”);}
};
int main(void) {
puts(“Creating& destroying an int\n”);
int*p = new int(56);
deletep;
puts(“Creating& destroying an S\n”);
S*s = new S;
deletes;
puts(“Creating& destroying S[3]\n”);
S*sa = new S[3];
delete[]sa;
}
在main中的对内部类型,自定义类型以及数组的创建和销毁均使用了全局重载版本的new和delete。
(2) 对于一个类重载new和delete
为一个类重载new和delete时,尽管不必显式使用static,但实际上仍是在创建static成员函数。当编译器看到使用new创建自定义的类的对象时,它选择成员版的operator new()而不是全局版本。但全局版本的new和delete仍为所有其它类型对象使用,除非他们有自己的new和delete。
下面是一个例子:
class Framis {
enum{sz = 10};
charc[sz]; //just for space taken
static unsigned char pool[];
static bool alloc_map[];
public:
enum{psize = 100};
Framis(){}
~Framis(){}
void* operator new(size_t) throw(bad_alloc);
void operator delete(void*);
};
unsigned charFramis::pool[psize*sizeof(Framis)];
boll Framis::alloc_map[psize]= {false};
void* Framis::operator new(size_t) throw(bad_alloc){
for(inti=0; i<psize; i++) {
if(!alloc_map[i]){
alloc_map[i]= true;
returnbool+(i*sizeof(Framis));
}
}
throwbad_alloc();
}
void operator delete(void* m) {
if(!m)return;
unsignedlong block = (unsigned long)m-(unsigned long)pool;
block/= sizeof(Framis);
alloc_map[block]= false;
}
在这个例子中无论何时我们动态的创建单个的Framis对象(而不是数组),都将调用局部的operator new()。而全局版本的new()在创建数组时使用。因此用户在用operator delete删除一个数组时,偶然的忘记了使用[]语法,而这就会出现问题,这样就调用了局部版本的delete,而在上面的例局部版本的delete删除的一块静态内存。如果考虑这样的事情,那么在局部版本的delete中应该加入检查删除指针的地址是否在该类的这块静态内存范围内,这样重载new和delete也可以用于检查代码中的内存泄露。
(3) 为数组重载new和delete
重载operator new[]和operatordelete[],来控制对对象数组的分配。下面是一个例子:
class Widget {
enum{sz = 10};
inti[sz];
public:
Widget(){}
~Widget(){}
void*operator new(size_t sz) {
return ::new char[sz];
}
voidoperator delete(void* p) {
::delete[]p;
}
void*operator new[](size_t sz) {
return::new char[sz];
}
voidoperator delete[](void* p) {
::delete[]p;
}
};
operator new[]()和operatordelete[]()只为整个数组调用一次,但对于数组中的每一个对象,都调用了默认构造函数和析构函数。
(4) 构造函数调用
如果new的内存分配没有成功,将会出现什么状况呢?在这种情况下,构造函数将不会被调用,所以虽然没有成功的创建对象,但至少没有调用构造函数并传给它一个为0的this指针。
4)定位new和delete
有时候我们想在一个指定内存的位置上放置一个对象或是我们想在调用new时,能够选择不同的内存分配方案。这两个特性可以用在重载operator new时多带一个参数,放置对象的地址的参数。下面是一个例子:
class X {
inti;
public:
X(intii=0) : i(ii) {}
~X(){}
void*operator new(size_t, void* loc) {
returnloc;
}
};
int main(void) {
intl[10];
X*xp = new(l)X(34); //X location at l
xp->X::~X();//Explict destructor call, and this is only used for placement
}
main中的这种显式的调用析构函数的方法,其实就是为支持operator new的定位而形成的。
本文出自 “Remys” 博客,谢绝转载!