再上一篇:12.3 静态成员
上一篇:12.5 指向类成员的指针
主页
下一篇:13.2 几个特殊运算符的重载
再下一篇:第十四章 输入/输出流类库
文章列表

第十三章 运算符重载

《VC++程序设计基础》,讲述C++语言的语法和标准库,以及Visual C++ 函数库和MFC类库的使用,并附相关代码示例。

在非面向对象的程序设计语言中,所有的运算符都已预先定义了它们的用法及意义,并且这种用法是不允许用户改变的。在C++中,允许程序设计者重新定义已有的运算符,并能按用户规定要求去完成特定的操作,这就是运算符的重载。运算符的重载从另一个方面体现了OOP技术的多态性,且同一运算符根据不同的运算对象可以完成不同的操作。 13.1运算符重载

对于面向对象的程序设计来说,运算符的重载可以完成两个对象之间的复杂操作,如二个对象间的加法,二个对象间的减法等。这种运算对于用户来说应该是透明的。运算符重载的原理是:一个运算符只是一个具有特定意义的符号,只要我们告诉编译程序在什么情况下如何去完成特定的操作,而这种操作的本质是通过特定的函数来实现的。

13.1.1 重载运算符

为了重载运算符,必须定义一个函数,并告诉编译器,遇到这个重载运算符就调用该函数,由这个函数来完成该运算符应该完成的操作。这种函数称为运算符重载函数,它通常是类的成员函数或者是友元函数。运算符的操作数通常也应该是对象。

定义运算符重载函数的一般格式为:

ClassName:: operator @()

{

...... ; //函数体

}

其中type为函数返回值的类型,ClassName为运算符重载函数所在的类名,@为要重载的运算符,为函数的形参表,operator是关键字,它与其后的一个运算符一起构成函数名。由于运算符重载函数的函数名是以特殊的关键字开始的,编译器很容易与其它的函数名区分开来。

例 13.1 定义一个复数类,重载+和-运算符,使这二个运算符能直接完成复数的加法和减法运算。

#include

class Complex{

float Real, Image;

public:

Complex(float r=0,float i=0){Real=r;Image=i;}

float GetR() {return Real;}

float GetI(){return Image;}

void Show()

{

cout <<"Real="<

}

Complex operator +(Complex &); //重载运算符+

Complex operator +(float ); //重载运算符+

void operator +=(Complex &); //重载运算符+=

void operator =(Complex &); ////重载运算符=};

Complex Complex::operator +( Complex &c)

{

Complex t;

t.Real=Real + c.Real;

t.Image = Image + c.Image;

return t;

}

Complex Complex::operator +(float s)

{

Complex t;

t.Real =Real + s;

t.Image = Image;

return t;

}

void Complex::operator +=(Complex &c)

{

Real= Real + c.Real;

Image = Image + c.Image;

}

void Complex::operator =(Complex &c)

{

Real = c.Real;

Image =c.Image;

}

void main(void )

{

Complex c1(25,50),c2,c3(100,200);

Complex c,c4(200,400);

c1.Show();

c2=c1;

c2.Show();

c=c1+c3;

c.Show();

c +=c1;

c.Show();

c4 += c1+c2; //A

c4.Show();

c4 =c4 + 200; //B

c4.Show();

}

执行程序后的输出为:

Real = 25 Image = 50

Real = 25 Image = 50

Real = 125 Image = 250

Real = 150 Image = 300

Real = 250 Image = 500

Real = 450 Image = 500

在上例中重载了运算符“+”、“=”、“+=”,可以实现复数的加法、复数的赋值、复数与实数的加法。其中,对运算符“+”作了二次重载,一个用于实现二个复数的加法,另一个实现一个复数与一个实数的加法。从例中可以看出,当重载一个运算符时,必须定义该运算符要完成的具体操作。本质上,运算符的重载也是函数的重载,不同之处在于系统约定了重载运算符的函数名。

从上例也可以看出,经重载后的运算符的使用方法与普通的运算符一样方便,并没有表现出要调用成员函数来实现这种操作。实际上,实现运算符重载,并对相应成员函数的调用是由系统自动完成的。如A行中的表达式:

c4 += c1+c2

编译器首先将c1+c2解释为:

c1.operator +( c)

再将这表达式解释为:

c4.operator +=( c1.operator +( c2))

由c1.operator +( c2)成员函数求出复数c1+c2的值,并返回一个计算结果的复数t,然后再由成员函数c4.operator +=(t),完成复数c4+t的运算,并将运算结果赋给c4。

对于运算符的重载,必须说明以下几点:

1、运算符重载函数的函数名必须为operator,后跟一个合法的运算符。运算符重载后,遇到这种运算符的运算,实际上是调用了一个运算符重载函数来实现的,在调用函数时,将右操作数作为函数的实参。

2、当用成员函数实现运算符的重载时,对运算符重载函数的参数有所限制,只能有二种情况:没有参数或带有一个参数。对于只有一个操作数的运算符,在重载这种运算符时,通常不能有参数;而对于有二个操作数的运算符,只能带有一个参数。运算符的左操作数一定是对象,右操作数作为调用运算符重载函数的参数,这参数可以是对象,对象的引用,或其它类型的参数。在C++中不允许重载有三个操作数的运算符。

3、在C++中,允许重载的运算符列于表13.1中。

表13.1 C++中允许重载的操作符

+ - * / % ^ & |

~ ! , = < > <= >=

++ -- << >> == != && ||

+= -= *= /= %= ^= &= |=

<<= >>= [] () -> ->* new delete

4、在C++中不允许重载的运算符列于表13.2中。在表中简要说明了为什么不允许重载的原因。

表13.2 C++中不允许重载的运算符

运算符 运算符的含义 不允许重载的原因

?: 三目运算符 在C++中没有定义一个三目运算符的语法

. 成员操作符 为保证成员操作符对成员访问的安全性,故不允

许重载

* 成员指针操作符 同上

:: 作用域操作符 因该操作符左边的操作数是一个类型名,而不是

一个表达式

Sizeof 求字节数操作符 其操作数是一个类型名,而不是一个表达式

5、只能对C++中已定义了的运算符进行重载,而且,当重载一个运算符时,该运算符的优先级和结合律是不能改变的。

13.1.2 单目运算符的重载

前面的例子说明了双目运算符的重载方法,对于所有双目运算符的重载方法都是类同的。下面说明单目运算符的重载方法。用成员函数实现单目运算符重载的一般格式为:

ClassName::operator <单目运算符>( )

{ ......;//函数体

}

但对于++和--单目运算符,存在前置和后置的问题,在定义运算符重载函数时必须有所区分,以便编译器根据这种区分来调用不同的运算符重载函数。++和--运算符的重载的方法是类同的,我们仅以++的重载为例来说明实现的方法。

++为前置运算时,它的运算符重载函数的一般格式为:

ClassName::operator ++( )

{ ......;//函数体

}

++为后置运算时,它的运算符重载函数的一般格式为:

ClassName::operator ++(int)

{ ......;//函数体

}

其中type是函数返回值的类型,它可以是类的对象或是基本类型,也可以是任一导出数据类型。由于是用运算符重载函数来实现++运算的,所以这里的++或--是广义上的增量或减量运算符。在后置运算重载函数中的参数int仅是用作区分的,并没有其它的实际意义,可以给一个变量名,也可以不给出变量名。

例 13.2 用一个类来描述人民币币值,用二个数据成员分别存放元和分。重载运算符++,实现对象的加1的运算。

#include

#include

class Money{

float Dollars; //元

float Cents; //分

public:

Money(){ Dollars = Cents =0; }

Money(float,float );

Money(float );

Money operator ++();

Money operator ++(int);

float GetAmount(float *);

~Money(){}

void Show(){cout << Dollars<<'\t'<

};

Money::Money(float n) //初始化值中整数部分为元,小数部分为分

{

float Frac,num;

Frac = modff(n,&num); //A

Cents = Frac *100;

Dollars=num;

}

Money::Money(float d,float c) //d为元,c为分

{

float sum,dd,cc;

sum = d + c/100;

cc = modff(sum, &dd);

Dollars=dd;

Cents=cc * 100 ;

}

Money Money::operator ++() //前置++

{

Cents ++;

if(Cents >= 100 ) {

Dollars ++;

Cents -=100;

}

return *this; //B

}

Money Money::operator ++(int ) //后置++

{

Money t= *this; //C

Cents ++;

if(Cents >= 100 ) {

Dollars ++;

Cents -=100;

}

return t; //D

}

float Money::GetAmount(float *n) //参数返回元,return返回分{

*n=Dollars;

return Cents;

}

void main(void )

{

Money m1(25,50),m2(105.7),m3(1002.25,200.35);

Money c,d;

float e1,f1,e2,f2;

m1.Show();

c=++m1;

d=m1++;

c.Show();d.Show();

c=++m2;

d=m2++;

c.Show();d.Show();

e1=m2.GetAmount(&f1);

e2=m3.GetAmount(&f2);

cout<

}

执行程序后的输出为:

25 50

25 51

25 51

105 70.9997

1109元 7.3476分

在上例中,定义了一个用于描述人民币币值的一个类,类中定义了元(Dollars)和分(Cents)。当对这种对象进行++运算时,认为是人民币的分加1的运算。分加1时,存在进位的问题。程序中的A行调用了数学库中的函数modff(n,&num),该函数的功能是将实数n分解成整数和小数部分两部分。返回小数值部分,将整数部分的值送到 num所指向的单元中。

实现前置++时,应将加1后的对象值作为返回值,即要返回当前对象值。这时必须使用指向当前对象的指针this,并返回* this的值,见程序中的B行。实现后置++时,应返加当前对象的值,然后完成加1运算。程序中的C行将当前对象的值保存在临时对象t中,当前对象完成加1后,在D行返回对象t的值。

用成员函数实现运算符的重载时,运算符的左操作数为当前对象,并且要用到隐含的this指针。运算符重载函数不能定义为静态的成员函数,因为静态的成员函数中没有this指针。

13.1.3 友元运算符

实现运算符重载的方法有二种:一种是用类的成员函数来实现,另一种方法是通过类的友元函数来实现。后者简称为友元运算符。

重载单目运算符的友元函数的一般格式为:

operator @(X &obj)

{

......;//函数体

}

其中@为单目运算符,X为类名,obj是对象名,运算符重载函数是类X的友元。对于++,--单目运算符来说,参数只能是类X的引用,而对于“-”单目运算符来说,参数可为引用,也可为对象。

重载双目运算符的友元函数的一般格式为:

operator @( 参数1说明,参数2说明)

{

......;//函数体

}

其中@为双目运算符,二个参数中必有一个是类X的对象,当然二个参数均可为类X的对象,该函数是类X的友元。

例 13.3 用友元运算符实现复数的运算,包括双目运算+,单目运算-;用成员函数实现+=。

#include

class Complex{

float Real, Image;

public:

Complex(float r=0,float i=0){Real=r;Image=i;}

float GetR() {return Real;}

float GetI(){return Image;}

void Show()

{

cout <<"Real="<

}

friend Complex operator +(Complex &, Complex &);

friend Complex operator +(Complex , float );

friend Complex operator -(Complex );

void operator +=(Complex &);

};

Complex operator +( Complex &c1,Complex &c2)

{

Complex t;

t.Real=c1.Real + c2.Real;

t.Image = c1.Image + c2.Image;

return t;

}

Complex operator +(Complex c1,float s)

{

Complex t;

t.Real =c1.Real + s;

t.Image = c1.Image;

return t;

}

Complex operator -(Complex c)

{

return Complex(-c.Real,-c.Image); //A

}

void Complex::operator +=(Complex &c)

{

Real= Real + c.Real;

Image = Image + c.Image;

}

void main(void )

{

Complex c1(25,50),c2,c3(100,200);

Complex c,c4(200,400),c5;

c1.Show();

c2=c1;

c2.Show();

c=c1+c3;

c.Show();

c +=c1;

c.Show();

c4 += c1+c2; //B

c4.Show();

c4 =c4 + 200;

c4.Show();

c5 = -c4;

c5.Show();

}

执行程序后的输出为:

Real = 25 Image = 50

Real = 25 Image = 50

Real = 125 Image = 250

Real = 150 Image = 300

Real = 250 Image = 500

Real = 450 Image = 500

Real = -450 Image = -500

程序中的A行产生一个临时对象,并返回该临时对象的值。从上例中可以看出,用二种方法实现运算符的重载,对于运算符的使用而言,二者的用法是相同的,但编译器所做的处理是不同的。对于B行中的表达式:

c4 += c1+c2

编译器先将c1+c2变换成对友元函数的调用:

operator + (c1, c2)

将+=变换成对成员函数的调用:

c4. operator +=( operator + (c1, c2) )

13.1.4 转换函数

转换函数(又称为类型转换函数)是类中定义的一个成员函数,其一般格式为:

ClassName:: operator ( )

{

......;//函数体

}

其中ClassName是类名;type是要转换后的一种数据类型,它可以是基本的数据类型,也可以是导出的数据类型;operator与type一起构成转换函数名。该函数不能带有参数,也不能指定返回值类型,它的返回值的类型是type。转换函数的作用是将对象内的成员数据转换成type类型的数据。

例 13.4 定义一个类,类中包含元、角、分,要把这三个数变换成一个等价的实数。

#include

class Complex{

int yuan,jiao,fen;

public:

Complex(int y=0,int j=0 , int f=0)

{ yuan=y;jiao=j;fen=f;

}

operator float();

float GetDollar();

};

float Complex ::GetDollar()

{

float amount;

amount = yuan*100.0+jiao*10.0 +fen;

amount /=100;

return amount;

}

Complex ::operator float( ) //A

{

float amount;

amount = yuan*100.0+jiao*10.0 +fen;

amount /=100;

return amount;

}

void main(void )

{

Complex d1(25,50,70),d3(100,200,55);

float s1,s2,s3,s4;

s1= d1; s2=d3; //B

s3=d1.GetDollar();

s4=d3.GetDollar();

cout <<"s1= "<< s1<<'\t' << "s2="<< s2 <<'\n';

cout <<"s3= "<< s3<<'\t' << "s4="<< s4 <<'\n';

float s5,s6;

s5= float (s1); //C

s6 = (float) s1; //D

cout <<"s5= "<< s5<<'\t' << "s6="<< s6 <<'\n';

}

执行程序后的输出为:

s1= 30.7 s2=120.55

s3= 30.7 s4=120.55

s5= 30.7 s6= 30.7

程序中的A行定义了一个转换函数,将对象中的三个数据成员元、角、分转换成实数,并返回实数值。B行中表达式s2=d3,由编译器将其变换为对转换函数的调用:

s2=d3.operator float ( )

通过调用转换函数,将对象d3中的数据成员转换成实数后赋给变量s2。C行中的表达式s5=float (s1),编译器将其变换为对转换函数的调用:

s5=s1.operator float ( )

同理,D行中表达式,也调用了转换函数:

s6=s1.operator float ( )

将转换后的结果赋给实型变量s6。

注意,转换函数只能是成员函数,不能是友元函数。转换函数的操作数是对象。转换函数可以被派生类继承,也可以被说明为虚函数。在一个类中可以定义多个转换函数。

从转换函数的定义可以看出,任何一个成员转换函数,均可以用一个成员函数来实现。如上例中的转换函数可被以下的成员函数所代替:

float Complex :: trans( )

{

float amount;

amount = yuan*100.0+jiao*10.0 +fen;

amount /=100;

return amount;

}

对于C行中的表达式s5= float (s1),使用上面的成员函数实现数据变换时,应表示为:

s5=s1.trans( )

显然,比较这二者的使用方法,转换函数比一般的成员函数要简洁。

13.1.5 赋值运算符与赋值运算符重载

在相同类型的对象之间是可以直接相互赋值的,在前面的程序例子中已多次使用。但当对象的成员中使用了动态的数据类型时,就不能直接相互赋值,否则在程序的执行期间会出现运行错误。

例 13.5 对象间的直接赋值导致程序的运行错误。

#include

#include

class A{

char *ps;

public:

A( ){ ps=0;}

A(char *s )

{

ps =new char [strlen(s)+1];

strcpy(ps,s);

}

~A( ){ if (ps) delete ps;}

char *GetS( ) {return ps;}

};

void main(void )

{

A s1("China!"),s2("Computer!");

cout <<"s1= "<< s1.GetS()<<'\t';

cout <<"s2= "<< s2.GetS()<<'\n';

s1=s2; //B

cout <<"s1= "<< s1.GetS()<<'\t';

cout <<"s2= "<< s2.GetS()<<'\n';

char c;

cin >> c; //C

}

这个程序在执行到C行,在没有输入一个字符时仍能正确执行,程序的输出是:s1= China! s2= Computer!

s1= Computer! s2= Computer!

但到程序结束时,就出现运行错误!这是因为,执行B行的赋值语句:

s1=s2;

后,使s1和s2中的ps均指向同一个字符串:Computer!。同时,为字符串“China!”动态申请的空间已无法收回。在程序结束时,假定析构函数首先撤消对象s1,收回了ps所指向字符串占用的空间,而在析构函数撤消对象s2,要收回其ps所指向字符串占用的空间,该空间已不存在,即出现二次收回同一个存储空间,因而出现了运行错误。所以,当类中的成员占用动态的存储空间时,必须重载运算符“=”。在上例的类A中应增加如下的赋值运算符重载函数:

A & operator = ( A &b)

{

if ( ps ) delete [ ] ps;

if ( b.ps) {

ps = new char [ strlen(b.ps)+1];

strcpy( ps, b.ps);

}

else ps =0;

return *this;

}