2.5 运算符重载
尽管C++语言有丰富的数据类型和运算符,但仍然不能满足应用编程的一些需要,复数及其操作就是这样的一个例子。虽然用户可以定义一个复数类,然后利用成员函数实现数据之间的运算操作,但总没有运算符操作来得更为简捷。
运算符重载就是赋予已有的运算符多重含义,是一种静态联编的多态。通过重新定义运算符,使其能够用于特定类对象执行特定的功能,从而增强了C++语言的扩充能力。
2.5.1 运算符重载函数
事实上,C++运算符本身具有简单的多态能力,例如算术运算符可以用于整型、浮点型等混合的操作对象。但对于类对象的运算操作,许多运算符的操作就显得能力不足了。为此,需要对运算符进行重载。也就是说,运算符重载的目的是实现类对象的运算操作。
重载时,一般是在类中定义一个特殊的函数,以便通知编译器,遇到该重载运算符时调用该函数,并由该函数来完成该运算符应该完成的操作。这种特殊的函数称为运算符重载函数,它通常是类的成员函数或友元函数,运算符的操作数通常也是该类的对象。
在类中,定义一个运算符重载函数与定义一般成员函数相类似,只不过函数名必须以operator开头,其一般形式如下:
<函数类型><类名>::operator <重载的运算符>(<形参表>) { … } // 函数体
运算符重载函数的函数是以特殊的关键字开始的,编译很容易与其他的函数名区分开来。这里先来看一个实例,它用来定义一个复数类CComplex,然后重载“+”运算符,使这个运算符能直接完成复数的加运算。
【例Ex_Complex】 运算符的简单重载
#include <iostream.h> class CComplex { public: CComplex(double r = 0, double i = 0) { realPart=r; imagePart=i; } void print() { cout<<"实部 ="<<realPart<<", 虚部 ="<<imagePart<<endl; } CComplex operator+(CComplex&c); // 重载运算符+ CComplex operator+(double r); // 重载运算符+ private: double realPart; // 复数的实部 double imagePart; // 复数的虚部 }; CComplex CComplex::operator+(CComplex&c) // 参数是CComplex引用对象 { CComplex temp; temp.realPart = realPart + c.realPart; temp.imagePart = imagePart + c.imagePart; return temp; } CComplex CComplex::operator+(double r) // 参数是double型数据 { CComplex temp; temp.realPart = realPart + r; temp.imagePart = imagePart; return temp; } int main() { CComplex c1(12,20), c2(50,70), c; c=c1+c2; c.print(); c=c1+20; c.print(); return 0; }
程序运行结果如下:
实部 = 62, 虚部 = 90
实部 = 32, 虚部 = 20
分析:
(1)程序中,对运算符“+”作了两次重载,一个用于实现两个复数的加法,另一个用于实现一个复数与一个实数的加法。
(2)从main函数中的对象表达式可以看出,经重载后的运算符的使用方法与普通运算符基本一样。但编译总会自动完成相应的运算符重载函数的调用过程。例如表达式“c = c1 + c2”,编译首先将“c1 + c2”解释为“c1.operator + (c2)”,从而调用运算符重载函数operator + (CComplex&c),然后再将运算符重载函数的返回值赋给c。同样,对于表达式“c = c1 + 20”,编译器将“c1+ 20”解释为“c1.operator + (20)”,调用运算符重载函数operator + (double r) ,然后再将运算符重载函数的返回值赋给c。
(3)在编译解释“c1 + c2”时,由于成员函数都隐含一个this指针,因此解释的“c1.operator+ (c2)”等价于“operator + (&c1, c2)”。正是因为this指针的存在,当重载的运算符函数是类的成员函数时,运算符函数的形参个数要比运算符操作数个数少一个。对于双目运算符(如“+”)重载的成员函数来说,它应只有一个参数,用来指定其右操作数。而对于单目运算符重载的成员函数来说,由于操作数就是该类对象本身,因此运算符函数不应有参数。
需要说明的是:运算符重载函数的返回值类型和参数取决于运算符的含义和结果,它们可能是类、类引用、类指针或其他类型。
2.5.2 运算符重载限制
在C++中,运算符重载还有以下一些限制:
(1)重载的运算符必须是一个已有的合法的C++运算符,如“+”、“-”、“*”、“/”、“++”等,且不是所有的运算符都可以重载。在C++中不允许重载的运算符有?:(条件)、.(成员)、*.(成员指针)、::(域作用符)、sizeof(取字节大小)。
(2)不能定义新的运算符,或者说,不能为C++没有的运算符进行重载。
(3)当重载一个运算符时,该运算符的操作数个数、优先级和结合性不能改变。
(4)运算符重载的方法通常有类的操作成员函数和友元函数两种,但=(赋值)、()(函数调用)、[](下标)和->(成员指针)运算符不能重载为友元函数。
2.5.3 友元重载
友元重载方法既可用于单目运算符,也可以用于双目运算符,其一般格式如下:
friend<函数类型>operator<重载的运算符>(<形参>) // 单目运算符重载 { … } // 函数体 friend<函数类型>operator<重载的运算符>(<形参1, 形参2>) // 双目运算符重载 { … } // 函数体
其中,对于单目运算符的友元重载函数来说,只有一个形参,形参类型既可能是类的对象,也可能是类的引用,这取决于不同的运算符。对于“++”、“--”等来说,这个形参类型是类的引用对象,因为操作数必须是左值。对于单目“-”(负号运算符)等来说,形参类型可以是类的引用,也可以是类的对象。对于双目运算符的友元重载函数来说,它有两个形参,这两个形参中必须有一个是类的对象。
下面来看一个实例,这个例子是在【例Ex_Complex】的基础上,用友元函数实现双目运算符“+”、单目运算符“-”的重载,而用成员函数实现“+=”运算。
【例Ex_ComplexFriend】 运算符的友元重载
#include <iostream.h> class CComplex { public: CComplex(double r = 0, double i = 0) { realPart=r; imagePart=i; } void print() { cout<<"实部 ="<<realPart<<", 虚部 ="<<imagePart<<endl; } CComplex operator+(CComplex&c); //A重载运算符+ CComplex operator+(double r); //B重载运算符+ friend CComplex operator+(double r,CComplex&c); //C友元重载运算符+ friend CComplex operator-(CComplex&c); // 友元重载单目运算符- void operator += (CComplex &c); private: double realPart; // 复数的实部 double imagePart; // 复数的虚部 }; CComplex CComplex::operator + (CComplex &c) { CComplex temp; temp.realPart = realPart + c.realPart; temp.imagePart = imagePart + c.imagePart; return temp; } CComplex CComplex::operator + (double r) { CComplex temp; temp.realPart = realPart + r; temp.imagePart = imagePart; return temp; } CComplex operator + (double r, CComplex &c) { CComplex temp; temp.realPart = r + c.realPart; temp.imagePart = c.imagePart; return temp; } CComplex operator - (CComplex &c) { return CComplex(-c.realPart, -c.imagePart); } void CComplex::operator += (CComplex &c) { realPart+=c.realPart; imagePart+=c.imagePart; } int main() { CComplex c1(12,20), c2(30,70), c; c=c1+c2; c.print(); c=c1+20; c.print(); c=20+c2; c.print(); c2+=c1; c2.print(); c1=-c1; c1.print(); return 0; }
程序运行结果如下:
实部 = 42, 虚部 = 90
实部 = 32, 虚部 = 20
实部 = 50, 虚部 = 70
实部 = 42, 虚部 = 90
实部 = -12, 虚部 = -20
分析和比较:
(1)类CComplex中,对于双目运算符“+”的重载分别用成员函数和友元函数的方法定义了3个重载函数,如代码注释中所标的A、B和C。
(2)当“c = 20+c2”时,这里的“20+c2”是无法解释成“20.operator + (c2)”的,因为“20”不是一个对象。因此,当左操作数是数值而不是对象时,是无法用成员函数来实现运算符重载的,必须用友元函数才能实现。正因为这种区别,当“c = 20+c2”时会自动调用A版本的友元运算符重载函数。
(3)类似地,当“c = c1+c2”时,这里的“c1+c2”可以被编译解释为“c1.operator + (c2)”,即“operator +(&c1, c2)”,显然,它会调用A版本的运算符重载函数。若此时用友元定义这样的运算符“+”的重载函数:
friend CComplex operator+(CComplex&c1,CComplex c2);
则会与A版本的运算符重载函数相冲突,出现二义性。因此,运算符重载要避免二义性的产生。
2.5.4 转换函数
类型转换是将一种类型的值映射为另一种类型的值。C++的类型转换包含自动隐含和强制转换两种方法。转换函数是实现强制转换操作的手段之一,它是类中定义的一个非静态成员函数,其一般格式为:
class <类名> { public: operator <类型>(); //… }
其中,类型是要转换后的一种数据类型,它可以是基本数据类型,也可以是构造数据类型。operator和类型一起构成了转换函数名,它的作用是将“class <类名>”声明的类对象转换成类型指定的数据类型。当然,转换函数既可以在类中定义也可在类体外实现,但声明必须在类中进行,因为转换函数是类中的成员函数。
下面来看一个示例,它将金额的小写形式(数字)转换成金额的大写形式(汉字)。
【例Ex_Money】 转换函数的使用
#include <iostream.h> #include <string.h> typedef char* USTR; class CMoney { double amount; public: CMoney(double a = 0.0) { amount = a; } operator USTR (); }; CMoney::operator USTR () { USTR basestr[15] = {"分", "角", "元", "拾", "佰", "仟", "万", "拾", "佰", "仟", "亿", "拾", "佰", "仟", "万"}; USTR datastr[10] = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"}; static char strResult[80]; double temp, base = 1.0; int n = 0; temp = amount * 100.0; strcpy(strResult, "金额为: "); if (temp < 1.0) strcpy(strResult,"金额为: 零元零角零分"); else { while (temp>= 10.0) { // 计算位数 base=base*10.0; temp=temp/10.0; n++; } if(n>=15) strcpy(strResult,"金额超过范围!"); else { temp = amount * 100.0; for (int m=n; m>=0; m--) { int d = (int)(temp / base); temp= temp-base*(double)d; base= base/10.0; strcat(strResult, datastr[d]); strcat(strResult, basestr[m]); } } } return strResult; } int main() { CMoney money(1234123456789.123); cout<<(USTR)money<<endl; return 0; }
程序中,转换的类型是用typedef定义的USTR类型。调用该转换函数是直接采用强制转换方式,如程序中的(USTR)money或USTR(money)。程序运行的结果如下:
金额为:壹万贰仟叁佰肆拾壹亿贰仟叁佰肆拾伍万陆仟柒佰捌拾玖元壹角贰分
需要说明的是:转换函数重载用来实现类型转换的操作,但转换函数只能是成员函数,而不能是友元函数。转换函数可以被派生类继承,也可以被说明为虚函数,且在一个类中可以定义多个转换函数。
2.5.5 赋值运算符的重载
C++中,相同类型的对象之间可以直接相互赋值,但不是所有的同类型对象都可以这么操作。当对象的成员中有数组或动态的数据类型时,就不能直接相互赋值,否则在程序的编译或执行过程中出现编译或运行错误。因此,必须对赋值运算符“=”进行重载,并在重载函数中重新开辟内存空间或添加其他代码,以保证赋值的正确性。
【例Ex_Evaluate】 赋值运算符的重载
#include <iostream.h> #include <string.h> class CName { public: CName (char *s) { name =new char[strlen(s)+1]; strcpy(name,s); } ~CName () { if (name) { delete[]name; name=NULL; } } void print() { cout<< name <<endl; } C Name&operator=(CName&a) // 赋值运算符重载 { if (name) { delete[]name; name=NULL; } if (a.name) { name = new char[strlen(a.name) + 1]; strcpy(name, a.name); } return *this; } private: char *name; }; int main() { CName d1("Key"), d2("Mouse"); d1.print(); d1 = d2; d1.print(); return 0; }
程序运行结果如下:
Key
Mouse
需要说明的是:
(1)赋值运算符重载函数operator = ()的返回类型是CName&,注意它返回的是类的引用而不是对象。这是因为,C++要求赋值表达式左边的表达式是左值,它能进行诸如下列的运算:
int x,y=5; //y是左值 (x=y)++; //x是左值
由于引用的实质就是对象的内存空间,所以通过引用可以改变对象的值。而如果返回的类型仅是类的对象,则操作的是对象的值而不是对象的内存空间,因此赋值操作后,不能再作为左值,从而导致程序运行终止。
(2)赋值运算符不能重载为友元函数,只能重载为一个非静态成员函数。
(3)赋值运算符重载函数是唯一的一个不能被继承的运算符函数。
2.5.6 自增自减运算符的重载
自增“++”和自减“--”运算符是单目运算符,它们又有前缀和后缀运算符两种。为了区分这两种运算符,在重载时应将后缀运算符视为双目运算符。即
obj++或obj--
应被看做:
obj++0或obj--0
又由于这里前缀“++”中的obj必须是一个左值,因此运算符重载函数的返回类型应是引用而不能是对象。
设类为X,当用成员函数方法来实现前缀“++”和后缀“++”运算符的重载时,则可有下列一般格式:
X& operator++(); // 前缀++ X operator++(int); // 后缀++
若用友元函数方法来实现前缀“++”和后缀“++”运算符的重载时,则可有下列格式:
friend X& operator++(X&); // 前缀++ friend X operator++(X&,int); // 后缀++
下面来说明前缀“++”和后缀“++”运算符的重载,对于“--”运算符也可类似进行。
【例Ex_Increment】 前缀“++”和后缀“++”运算符的重载
#include <iostream.h> class CCounter { public: CCounter( int n = 0) { unCount = n; } CCounter&operator++(); // 前缀++运算符重载声明 friend CCounter operator++(CCounter&one,int);// 后缀++运算符友元重载声明 void print() { cout<<unCount<<endl; } private: unsigned unCount; }; CCounter&CCounter::operator++() // 前缀++运算符重载实现 { unCount++; return *this; } CCounter operator++(CCounter&one,int) // 后缀++运算符友元重载实现 { CCounter temp = one; one.unCount++; return temp; } int main() { CCounter d1(8), d2; d2=d1++; d1.print(); d2.print(); d2=++d1; d1.print(); d2.print(); ++++d1; d1.print(); return 0; }
程序中,类CCounter的运算符“++”重载函数分为前缀和后缀。对于前缀“++”运算符来说,它是通过成员函数重载来实现的,而对于后缀“++”运算符来说,它是通过友元函数来实现的。
程序运行结果如下:
9
8
10
10
12
需要说明的是:
(1)在友元重载的后缀“++”运算符函数中,由于函数中的形参仅用来在格式上区分前缀“++”运算符,本身并不使用,因此不必为形参指定形参名。
(2)在后缀“++”运算符友元重载实现中,先定义一个临时的CCounter对象temp,其初值设为one,然后对形参one进行自增运算,最后函数返回的是temp的值而不是one值。这样,当有:
d2 = d1++;
时,d2的值就等于友元重载函数的返回值temp,也就等于d1原来的值,满足了后缀“++”运算符的本质。由于d1 本身还要自增,因此友元重载函数的形参应是引用对象,而不能是普通对象。
(3)由于前缀“++”操作后仍可以作为左值,也就是说“++++d1”是成立的。因此,当用成员函数或友元函数来实现前缀“++”运算符的重载时,返回的必须是引用对象。