9.6 运算符重载范例
本节重点演示几种特殊运算符的重载示例,运算符的重载是很灵活的工具,使用得当,会产生意想不到的效果。
9.6.1 赋值运算符
赋值运算是一种很常见的运算,如果不重载赋值运算符,编译器会自动为每个类生成一个默认的赋值运算符重载函数,如下所示。
对象1=对象2;
实际上是完成了由对象2各个成员到对象1相应成员的复制,其中包括指针成员,这和第8章中复制构造函数和默认复制构造函数有些类似,如果对象1中含指针成员,并且涉及类内指针成员动态申请内存时,就会出现问题。
注意
下述两个代码的不同之处。
类名对象1=对象2;和 类名对象1; 对象1=对象2;
第一个代码是调用类的复制构造函数,完成对象1的创建并初始化,第二个代码是先调用对象1的无参构造函数(或所有参数都由默认值的构造函数)完成对象1的创建,而后调用赋值运算符将对象2的所有成员的值复制到对象1中。
来看一个第8章用过的例子。
代码9.9 赋值运算符重载AssignmentOverload
<—————————————-文件名:example909.h——————————————-> 01 #include<iostream> 02 using namespace std; 03 class computer 04 { 05 private: 06 char*brand;//字符指针brand 07 float price; 08 public: 09 computer(const char*sz,float p) 10 { 11 brand=new char[strlen(sz)+1];//构造函数中为brand分配一块动态内存 12 strcpy(brand,sz);//字符串复制 13 price=p; 14 cout<<"带参构造函数被调用"<<endl; 15 } 16 computer()//无参构造函数 17 { 18 brand=NULL;//brand初始化为NULL 19 price=0; 20 cout<<"无参构造函数被调用"<<endl; 21 } 22 computer(const computer&cp)//复制构造函数 23 { 24 brand=new char[strlen(cp. brand)+1];//为brand分配动态内存 25 strcpy(brand,cp. brand);//字符串复制 26 price=cp. price; 27 cout<<"复制构造函数被调用"<<endl; 28 } 29 ~computer()//析构函数,释放动态内存,delete[]NULL不会出错 30 { 31 delete[]brand; 32 cout<<"析构函数被调用"<<endl; 33 } 34 void print()//成员函数,输出信息 35 { 36 cout<<"品牌:"<<brand<<endl; 37 cout<<"价格:"<<price<<endl; 38 } 39 }; <————————————-文件名:example909.cpp——————————————-> 40 #include"example909.h" 41 int main() 42 { 43 computer com1("Dell",2000);//调用含参构造函数声明对象com1 44 computer com2=com1;//赋值运算符调用 45 if(false) 46 { 47 computer com3; 48 com3=com1; 49 } 50 return 0; 51 }
输出结果如下所示。
带参构造函数被调用
复制构造函数被调用
析构函数被调用
析构函数被调用
【代码解析】代码第45~49行,if(false)块中的代码并没有被执行,程序没有任何问题,复制构造函数很好地解决了使用诸如“computer com2=com1;”或“computer com2(com1);”等形式的指针赋值,为每个指针成员开辟了单独的动态内存,不会出现多个指针指向同一个内存的情况,避免了多重释放一块内存或使用已释放动态内存的错误,这在第8章中都已经提及,这里仅仅作为回顾与复习。
如果将代码9.9中example909.cpp中的if(false)结构更改为if(true)结构,编译链接并运行,会出现如图9.1所示的内存错误。
问题出现在if(true)结构的代码块内,“computer com3;”调用computer类的无参构造函数创建了com3,并进行了初始化,而后,代码第48行,语句“com3=com1;”将com1中所有成员的值复制给了com3,包括指针brand,这样,com3和com1中的brand都指向同一块内存,随着if(true)代码块结束,com3被撤销,com3.brand对应的动态内存被释放,此时,com1.brand便无所指,成了野指针,程序出错。
图 9.1 对象赋值引发内存错误
注意
复制构造函数中的特殊操作应同等定义到赋值运算符重载中。
使用赋值运算符重载可解决上述问题,需要特别注意的是,赋值运算符只能重载为类成员方式,如下所示。
computer&computer:operator=(const computer&cp)//成员函数赋值运算符重载的实现 { if(this==&cp)//首先判断是否为自赋值,若是则返回当前对象 return(*this); price=cp.price;//如果不是自赋值,先对price赋值 delete[]brand;//防止内存泄露,先释放brand指向的内容 brand=new char[strlen(cp.brand)+1];//为brand重新开辟一块内存空间 if(brand!=NULL)//如果开辟成功 { strcpy(brand,cp.brand);//复制字符串 } return(*this);//返回当前对象的引用,为的是实现链式赋值 }
在以上短短的几行程序中,有很多初学者容易忽视的地方,如下所述。
❑判断是否为自赋值,自己给自己赋值是没有意义的,不仅如此,如果不加判断就为指针重新申请内存,原来所指的动态内存块就泄露了。
❑释放brand所指的内存,delete一个NULL指针是不会出问题的。为了有效防错(防野指针),delete后立即将brand置为NULL。
❑为brand重新申请动态内存,该内存的大小由源对象决定。
❑出错处理,判断新的动态内存是否申请成功,若申请成功(brand不为NULL),实施字符串的复制,否则,进行出错处理,本函数什么也不做,返回时brand=NULL,还可以进行异常处理,关于这方面的内容将在第16章进行介绍。
❑返回引用(*this),为实现链式赋值,如“com1=com2=com3;”,根据赋值运算符的结合性,编译器会首先用com3为com2赋值,然后将修改后的com2为com1赋值。
如果将赋值操作符重载为友元形式,非左值(比如说一些常量)会被编译器隐式转换成一个临时对象,非左值出现在等号左边而编译器却认为合理,这破坏了赋值操作符的语义,因此,赋值操作符只能重载为成员形式。