第11章 友元与运算符重载
更新时间:2024-07-06 18:10:01 阅读量: 综合文库 文档下载
第11章 友元与运算符重载 ·275·
第11章 友元与运算符重载
通过本章的学习,应理解友元的概念,掌握将普通函数、成员函数定义为类友元函数的方法,学会友元函数的使用方法。理解运算符重载的概念,掌握运算符重载函数的定义方法、调用过程及实际应用。掌握多态性技术的概念及实现方法。了解虚函数与纯虚函数的概念、定义格式及使用方法。
11.1 友元函数
为了数据的安全,通常将类中数据成员的访问权限定义为私有或保护,使数据成员只能在类或其子类中访问。若要在类外访问,则必须通过该类的公有成员函数才能访问这些数据成员。这种做法虽然保证了数据的安全性,但也给使用带来许多不便,为此,在C++中提供了友元函数,允许在类外访问类中的任何成员。
友元函数可以是普通函数,也可以是某个类的成员函数,甚至可以将某个类说明成另一个类的友元。下面就这三种友元进行讨论。
11.1.1定义普通函数为友元函数
在定义一个类时,若在类中用关键词friend修饰普通函数,则该普通函数就成为该类的友元函数,它可以访问该类中所有的成员。定义普通友元函数的格式为:
friend <类型> <友元函数名> (形参表); 即:只要将用关键词friend修饰的普通函数原型说明写在类中,则该普通函数可以使用类的所有成员。下面用例题进行说明。
【例11.1】 用友元函数的方法求长方体的体积。
分析:先定义一个描述长方体的类Cuboid,其私有数据成员为长方体的长(Length)、宽(Width)、高(High),通过构造函数对长方体的数据成员进行初始化,见下列程序A行处。再定义一个求长方体体积的普通函数Volume():
float Volume(Cuboid &c)
{return c.Length*c.Width*c.High;}
为了计算长方体对象c的体积,该函数的形参必须是长方体类的对象c,并用该长方体对象c的长、宽、高计算长方体的体积。由于Volume()为普通函数,因此在其函数体内不能使用长方体类Cuboid的私有数据成员c.Length、c.Width、c.High,因而上述函数在编译时会发生错误。解决问题方法之一就是将计算长方体体积的普通函数Volume()定义为长方体类的友元函数,即在长方体类Cuboid中增加一条将普通函数Volume()定义成友元函数的语句:
friend float Volume(Cuboid &);
则在普通函数Volume()内就可使用长方体类Cuboid的私有数据成员c.Length、c.Width、c.High。用友元函数方法求长方体体积的程序如下:
第11章 友元与运算符重载
# include
class Cuboid //A { private:
float Length,Width,High; public:
Cuboid(float l,float w,float h) { Length=l;Width=w; High=h;}
friend float Volume(Cuboid &); //B };
float Volume(Cuboid &c) //C { return c.Length*c.Width*c.High;} void main (void)
{ Cuboid c(10,20,30);
cout<<\长方体积=\olume(c)< 程序执行后输出: 长方体积=6000 ·276· 在程序中,计算长方体体积的函数Volume()是返回类型为float的普通函数。由于在类Cuboid中B行,用关键词friend将Volume()说明为Cuboid类的友元函数。因此,在Volume()函数中可以使用类中的私有数据成员Length、Width、High来计算长方体的体积。又因为Volume()不是Cuboid类的成员函数,它不带有this指针。所以必须用对象名或对象的引用(Cuboid &c)作为友元函数的形参,并在函数体内使用运算符“.”来访问对象的成员,如c.Length、c.Width与c.High。对友元函数需说明如下: (1)友元函数并不是类的成员函数,它不带有this指针。所以必须用对象名或对象的引用作为友元函数的形参,并在函数体内使用运算符“.”来访问对象的成员。 (2)友元函数必须在类内进行函数的原型说明,其函数的定义部分应写在类外。 (3)友元函数与一般函数的区别 友元函数可访问类内的任一数据成员或成员函数,如:矩形类的私有数据Length、Width与High。而一般函数只能访问类的公有数据成员或公有成员函数。 (4)由于友元函数不是类的成员函数,所以类的访问权限对友元函数不起作用。即友元函数的原型说明写在类的任一位置对其访问权限没有影响。 (5)由于友元函数可使用类内的所有成员,破坏了数据的安全性,所以使用友元函数必须谨慎,不要通过友元函数对数据成员进行危险的操作。 11.1.2定义成员函数为友元函数 下面通过例子来说明将成员函数定义为友元函数的方法。 【例11.2】将长方体类中计算长方体体积的成员函数定义为矩形类的友元函数,以便在长方体中使用矩形类中的私有数据成员计算长方体体积。 分析:先定义描述矩形的类Rectangle,其私有数据成员为矩形的长(Length)与宽(Width), 第11章 友元与运算符重载 ·277· 通过构造函数对矩形的数据成员长与宽初始化,如下列程序中D行所示。再定义描述长方体的类Cubiod,其私有数据成员为长方体的高Hing。如下列程序中B行所示。再定义计算长方体体积的成员函数Volume()如下: float Cuboid::Volume(Rectangle &r) { return r.Length*r.Width*High;} 在该函数内使用矩形对象r的私有数据成员Length与Width来计算长方体体积。由于矩形对象r的私有数据成员不能在矩形类外使用,所以编译时会产生错误。解决问题的方法之一就是将长方体类的成员函数Volume()定义为矩形类的友元函数,即在矩形类中增加一条将成员函数Volume()定义为友元函数的语句: friend float Cuboid::Volume(Rectangle &); //如程序中E行所示 则在成员函数Volume()内就可使用矩形类Rectangle的私有数据成员r.Length、r.Width。用友元函数方法求长方体体积的程序如下: # include class Rectangle; class Cuboid { private: float High; public: Cuboid(float h) { High=h;} float Volume(Rectangle &r); }; class Rectangle { private: float Length,Width; public: Rectangle(float l,float w) { Length=l;Width=w;} friend float Cuboid::Volume(Rectangle &); }; float Cuboid::Volume(Rectangle &r) { return r.Length*r.Width*High;} void main(void) { Rectangle r(10,20); Cuboid c(30); cout<<\长方体积=\olume(r)< 执行程序后输出: 长方体积=6000 //A //B //C //D //E 第11章 友元与运算符重载 ·278· 主函数执行时,先定义矩形对象r,系统自动调用构造函数初始化矩形对象r后,矩形r的长r.Legnth=10、宽r.Width=20。然后再定义长方体对象c,系统自动调用构造函数初始化长方体对象c后,长方体c的高High=30。最后调用计算长方体体积的函数Volume(),由于Volume()是矩形类的友元函数,所以可使用矩形类中私有数据成员 r.Legnth、r.Width作为长方体的长与宽,计算长方体的体积。 关于成员函数定义为友元的几点说明: (1)因为矩形类Rectangle定义在长方体类Cuboid之后,而长方体类Cuboid中要用到矩形类Rectangle的引用,如C行所示,所以在A行给出对矩形类Rectangle的引用性说明。 (2)E行将长方体类Cuboid中的成员函数Volume()说明为类Rectangle中的友元函数,因此在其函数体内可以使用类Rectangle中的私有成员r.Length、r.Width。 (3)将类Cuboid的成员函数Volume()定义为类Rectangle的友元函数时,在C行只能给出函数的原型说明,不能给出函数体,因为类Rectangle还没有定义。 从上例可以看出,将类C的一个成员函数说明成另一个类D的友元函数的一般格式: class D; class C { ? public: <类型> <成员函数>(D &); }; class D { ? public: friend <类型> C:: <成员函数>(D &); //B //A }; <类型> C:: <成员函数>(D &d); //E {?}; 上述定义格式中,在B行定义成员函数为友元函数,E行开始定义成员函数。定义为友元的成员函数参数必须是类D的引用、类D的对象或指向类D的指针。 综上所述,一个类可以定义若干个友元函数,可以将一个类的任一个成员函数说明为另一个类的友元函数,以便通过该类成员访问另一个类中的成员,亦可将一个类中的所有成员函数都说明为另一个的友元函数。将一个类中的所有成员函数都说明为另一个的友元函数的最好方法是将这个类定义为另一个类友元。 11.1.3一个类定义成另一个类的友元 要将一个类中所有的成员函数定义成另一个类的友元函数,只要将一个类说明成另一个的友元即可,而不必将该类每一个成员函数说明成另一个类的友元函数。 将类M说明成类N的友元的格式: class N { ? 第11章 友元与运算符重载 friend class M; ? ·279· }; 经上述定义后,在M类中可使用N类的所有成员。 【例11.3】在例11.2中将定义成员函数为友元函数的语句(即E行)改为将长方体类定义为矩形类的友元的语句:friend class Cuboid; 以便在长方体中使用矩形类中的私有数据成员计算长方体体积。 # include float High; public: Cuboid(float h) { High=h;} float Volume(Rectangle &r); }; class Rectangle { private: float Length,Width; public: Rectangle(float l,float w) { Length=l;Width=w;} friend class Cuboid; }; float Cuboid::Volume(Rectangle &r) { return r.Length*r.Width*High;} void main(void) { Rectangle r(10,20); Cuboid c(30); cout<<\长方体积=\olume(r)< 执行程序后输出: 长方体积=6000 在上面的程序中,由于将长方体类定义为矩形类的友元,所以在长方体中,可以使用矩形类中的私有数据成员计算长方体体积。 11.1.4友元注意事项 (1)友元关系是不传递的 第11章 友元与运算符重载 ·280· 类A是类B的友元,类B是类C的友元,但类A并不是类C的友元。 (2)友元关系不具有交换性 类A是类B的友元,但类B并不一定是类A的友元。 (3)友元关系是不能继承的 例如:函数f()是类A的友元,类A派生出类B,函数f()并不是类B的友元。除非在类B中作了特殊说明。 11.2 运算符重载 11.2.1 运算符重载的概念 为了介绍运算符重载的概念,先看一个运算符重载的引例。 引例 用“+”运算符完成两个实数、两个复数、两个字符串的相关运算。 (1)实数 设有两个实数:x1=10 ,x2=20,则两个实数相加的结果是:x1+x2=10+20=30。 (2)复数 设有两个复数:x1=10+10i,x2=20+20i,则两复数相加结果是:x1+x2=30+30i 。 (3)字符串 设有两个字符串:x1=\,x2=\,则两字符串连接的结果: x1+x2=\ 由上例可以看出,同一个运算符“+”可用于完成实数加法、复数加法及字符串连接等不同的运算,得到完全不同的结果。这就是“+”运算符的重载。因此,所谓运算符重载就是用同一个运算符完成不同的运算操作。在C++这类面向对象的程序设计语言中,运算符重载可以完成两个对象的复杂操作(如:两个复数的算术运算等)。而运算符重载是通过运算符重载函数来完成的。当编译器遇到重载运算符,如复数加法:x1+x2 中的加号运算符“+”时,自动调用“+”运算符的重载函数完成两个复数对象的加法操作。由于二元运算符的重载函数与一元运算符的重载函数有较大区别,因此分开介绍。先介绍二元运算符重载函数,再介绍一元运算符重载函数。 11.2.2 二元运算符重载函数 运算符重载函数通常是类的成员函数或者是友元函数,下面先介绍运算符重载函数为类的成员函数的定义格式及使用方法,然后介绍运算符重载函数为友元函数的定义格式及使用方法。 1.运算符重载函数为类的成员函数 运算符重载函数为类的成员函数一般定义格式为: <类型><类名>:: 其中,类型为运算符重载函数的返回类型。类名为成员函数所属类的类名。关键词“operator”加上“重载运算符”为重载函数名,即:重载函数名= operator重载运算符。形参常为参加运算的对象或数据。 第11章 友元与运算符重载 ·281· 【例11.4】定义一个复数类,重载“=”运算符,使这个运算符能直接完成复数的赋值运算。 # include float Real,Image; public: Complex(float r=0,float i=0) { Real=r;Image=i;} // 缺省构造函数 void Show(int i) //显示输出复数 { cout<<\ void operator =(Complex &c) //“=”运算符重载函数完成复数赋值操作 { Real=c.Real; Image=c.Image; } }; void main(void) { Complex c1(25,50),c2; c1.Show(1); c2=c1; c2.Show(2); } 程序执行后输出: c1=25+50i c2=25+50i 在程序中,定义了一个赋值运算符“=”的重载函数: void operator =(Complex &c) { Real=c.Real; Image=c.Image; } 该重载函数的函数名为“operator =”,返回类型为void,形参为复数类对象的引用Complex &c。当程序执行主函数中的赋值语句c2=c1而遇到赋值运算符“=”时,自动调用赋值运算符“=”重载函数“operator =()”,并将“=” 运算符右边的操作数c1作为实参,左边操作数c2作为调用重载函数的对象,即:作了一次 c2.operator=(c1) 的函数调用。在函数的调用过程中,实参c1传给形参c,在函数体内完成了复数实部与虚部的赋值操作: Real=c1.Real; Image=c1.Image; 因为重载函数是复数对象c2的成员函数,所以上式中复数的实部Real与虚部Image为c2的实部c2.Real与虚部c2.Image。因此,上式是将复数对象c1的实部与虚部赋给复数对象c2的实部与虚部。即完成了两个复数的赋值工作。 第11章 友元与运算符重载 ·282· 【例11.5】在例11.4定义复数类中,重载“+”、“-”运算符,使这二个运算符能直接完成复数的加、减运算。 # include float Real,Image; public: Complex(float r=0,float i=0) { Real=r;Image=i;} void Show(int i) //显示输出复数 { cout<<\ Complex operator + (Complex &c); //“+”运算符重载函数完成两个复数加法 Complex operator - (Complex &c); //“-”运算符重载函数完成两个复数减法 Complex operator + (float s); //“+”运算符重载函数完成复数实部加实数 void operator +=(Complex &c); //“+=”运算符重载函数完成复数=复数+c void operator =(Complex &c); //“=”运算符重载函数完成两个复数赋值 }; Complex Complex::operator + (Complex &c) { Complex t; t.Real=Real+c.Real; t.Image=Image+c.Image; return t; } 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; } 第11章 友元与运算符重载 void Complex::operator = (Complex &c) { Real=c.Real; Image=c.Image; } void main(void) { Complex c1(25,50),c2(100,200) ,c3,c4; c1.Show(1); c2.Show(2); c3=c1+c2; //c3=(25+50i)+(100+200i)=125+250i c3.Show(3); c4=c2-c1; //c4=(100+200i)-(25+50i)=75+150i c4.Show(4); c2+=c1; c2.Show(2); c1=c1+200; c1.Show(1); } 执行程序后输出: c1=25+50i c2=100+200i c3=125+250i c4=75+150i c2=125+250i c1=225+50i 图11.1 c3=c1+c2的运算 ·283· //c2=c2+c1= (100+200i)+ (25+50i)=125+250i //c1=(25+50i)+200=225+50i 对象c3 Real=125 Image=250 对象c1 Real=25 Image=50 对象c2 Real=100 Image=200 = + 在上例中重载了运算符“+”、“-”“+=”、“=”,可以实现复数的加法、减法、赋值等操作。从主函数中可以看出,经重载后运算符的使用方法与普通运算符一样方便。如复数c1加c2赋给c3的加法运算:c3=c1+c2与普通实数加法形式上完全相同,如图11.1所示。但实际执行过程中确是完全不同的。实现复数加法运算是通过调用加法运算符重载函数来完成,而对加法运算符重载函数的调用是由系统自动完成的。如主函数中表达式:c3=c1+c2;编译器先将c1+c2解释为对“+”运算符重载函数:c1.operator+(c2) 的调用。再将该表达式解释为对“=”运算符重载函数:c4.operator=(c1.operator+(c2))的调用。由c1.operator+(c2)成员函数求出复数c1+c2的值t,并返回一个计算结果t,然后再由成员函数c3.operator=(t),完成复数c3=t的赋值运算,将运算结果赋给c3。 对于运算符重载,必须说明以下几点: (1)运算符重载函数名必须为:operator <运算符> (2)运算符的重载是通过调用运算符重载函数实现的,调用函数时,左操作数为调用重载函数的对象,右操作数作为函数的实参,实参可以是对象、实数等其它类型。 (3)形参说明 若重载函数为成员函数,则参加二元运算的左操作数为调用重载函数的对象。因此, 第11章 友元与运算符重载 ·284· 重载函数为成员函数的参数通常为一个,即右操作数。如在上例中,二元加法运算:c1+c2被解释为对重载成员函数c1.operator+(c2)的调用,此时重载函数只有一个参数。 (4)运算符重载函数的返回类型 若二个同类对象进行二元运算后的结果类型仍为原类型,则运算符重载函数的返回类型应为原类型。如在上例中,由于两个复数运算的结果仍为复数,因此上述运算符重载函数的返回类型均为复数类型Complex。 (5)C++中允许重载的运算符如下表11.1所示: 表11.1 C++中允许重载的操作符 + ~ ++ += <<= - ! -- -= >>= * , << *= [ ] / = >> /= ( ) % < = = %= -> ^ > != ^= ->* & <= && &= new | >= || |= delete (6)C++中不允许重载的运算符如表11.2所示: 表11.2 C++中不允许重载的运算符 运算符 ?: · * :: sizeof 运算符的含义 三目运算符 成员操作符 成员指针操作符 作用域运算符 求字节数操作符 不允许重载的原因 在C++中没有定义一个三目运算符的语法 为保证成员操作符对成员访问的安全性,故不允许重载 同上 因该操作符左边的操作数是一个类型名,而不是一个表达式 其操作数是一个类型名,而不是一个表达式 (7)只能对C++中已定义了的运算符进行重载,当重载一个运算符时,该运算符的优先级和结合律是不能改变的。 2.运算符重载函数为友元函数 运算符重载函数为友元函数的一般定义格式为: <类型> 其中,类型为运算符重载函数的返回类型。operator<重载运算符>为重载函数名。形参1与形参2常为参加运算的两个对象的引用。对于形参需说明如下: 当重载函数为友元普通函数时,该重载函数不能用对象调用,因此参加运算的二个对象必须以形参方式传送到重载函数体内,所以运算符重载函数为友元函数时,形参通常为二个参加运算的对象。 【例11.6】用友元运算符重载函数实现复数的加、减运算。 # include float Real,Image; public: 第11章 友元与运算符重载 ·285· Complex(float r=0,float i=0) {Real=r;Image=i;} void Show(int i) { cout<<\ friend Complex operator + (Complex & ,Complex &); //“+”重载函数为友元函数 friend Complex operator-(Complex &, Complex &); //“-”重载函数为友元函数 friend Complex operator + (Complex &,float); }; 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, Complex &c2) { Complex t; t.Real=c1.Real-c2.Real; t.Image=c1.Image-c2.Image; return t; } Complex operator + (Complex &c,float s) { Complex t; t.Real=c.Real+s; t.Image=c.Image; return t; } void main(void) { Complex c1(25,50),c2(100,200),c3,c4; c1.Show(1); c2.Show(2); c3=c1+c2; //c3=(25+50i)+(100+200i)=125+250i c3.Show(3); c4=c2-c1; //c4=(100+200i)-(25+50i)=75+150i c4.Show(4); c1=c1+200; c1.Show(1); } 程序执行后输出: c1=25+50i //c1=25+50i+200=225+50i 第11章 友元与运算符重载 c2=100+200i c3=125+250i c4=75+150i c1=225+50i ·286· 在此例中,“+”与“-”运算符的重载函数均为普通函数,并在复数类中说明成复数类的友元函数。因此在三个运算符重载函数中可以使用复数类对象的私有数据成员,即复数的实部与虚部参加相应的算术运算。 从主函数可以看出,用成员函数与友元函数为运算符重载函数,就运算符的使用来讲是一样,但编译器处理方法是不同的,例如对表达式:c3=c1+c2;的处理是,先将c1+c2变换为对友元函数的调用:operator+(c1,c2);再将函数返回结果即两复数的和t赋给复数c3,因此表达式c3=c1+c2; 实际执行了c3= operator+(c1,c2) 的函数调用及赋值工作。 友元函数与成员函数作为二元运算符重载函数的另一个区别是: 当重载函数为成员函数时,二元运算符的左操作数为调用重载函数的对象。右操作数为实参。 当重载函数为友元函数时,二元运算符的左操作数为调用重载函数的第一个实参。右操作数为第二个实参。 11.2.3 一元运算符的重载 所谓一元运算符是只有一个操作数的运算符,如自加运算符“++”,自减运算符“--”等等。与二元运算符的重载类似,一元运算符重载函数也分为类的成员函数与友元函数两类。 1.一元运算符重载函数为类的成员函数 一元运算符重载函数为类的成员函数的格式: <类型><类名>::operator <一元运算符>(形参) { 函数体} 现对比较典型的一元运算符“++”、“--”进行讨论。对于一元运算符“++”、“--”存在前置与后置问题,因此定义函数时会有所区别。 (1)“++”为前置运算符时,函数格式为: <类型><类名>::operator ++( ) {函数体} (2)“++”为后置运算符时,函数格式为 <类型><类名>::operator ++( int ) {函数体} 由于是用运算符重载函数来实现“++”运算,所以这里的“++”是广义上的增量运算符。在后置运算符重载函数中,形参int仅用作区分前置还是后置,并无实际意义,可以给一个变量名,也可不给出变量名。 【例11.7】定义一个描述时间计数器的类,其三个数据成员分别用于存放时、分和秒。用成员函数重载“++”运算符,实现计数器对象的加1运算。 # include 第11章 友元与运算符重载 class TCount { private: int Hour,Minute,Second; public: TCount (int h=0,int m=0,int s=0) //定义缺省值为0的构造函数 { Hour=h;Minute=m;Second=s;} TCount operator ++( ); //定义“前置++”运算符重载成员函数 TCount operator ++( int ); //定义“后置++”运算符重载成员函数 void Show(int i ) //定义显示时:分:秒的成员函数 ·287· {cout<<\}; TCount TCount ::operator ++ () { Second++; if (Second==60) { Second=0; t1=*this t1=*this Minute++; Hour = 10 Hour = 10 this this if (Minute==60) Minute=25 Minute=25 { Minute=0; Second=50 Second=51 Hour++; if (Hour==24) (a) t1自加前的内容 (b) t1自加后的内容 { Hour=0;} 图11.2 系统为t1分配的内存空间及this指针 } } return *this; } TCount TCount::operator++ (int ) { TCount temp=*this; temp=*this t1=*this Second++; if (Second==60) Hour = 10 Hour = 10 this { Second=0; Minute=25 Minute=25 Minute++; Second=51 Second=52 if (Minute==60) 临时对象temp 自加后的内容 (a) (b) t1 { Minute=0; 图11.3 t1对象、this指针与临时对象temp Hour++; if (Hour==24) { Hour=0;} } } return temp; 第11章 友元与运算符重载 } void main(void) { TCount t1(10,25,50),t2,t3; //定义时间计数器对象t1=10:25:50 t1.Show(1); t2=++t1; t1.Show(1); t2.Show(2); t3=t1++; ·288· //先加后用,即:先将t1自加,然后将t1赋给t2 //先用后加,即:先将t1赋给t3,然后将t1自加 t1.Show(1); t3.Show(3); } 程序执行后输出: t1=10:25:50 t1=10:25:51 t2=10:25:51 t1=10:25:52 t3=10:25:51 说明: (1)TCount为描述时间计数器的类,其数据成员Hour、Minute、Second分别代表时、分、秒。在主函数中定义时间计数器对象t1、t2、t3,t1的初始值为10时25分50秒。 (2)对对象的自加操作“++”,是对时间计数器的秒加1运算。当秒计满60后,将其清0并对分加1。当分计满60后,将其清0并对时加1。当时计满24后,将其清0。 (3)“前置++”运算符重载成员函数的说明 在主函数中执行t2=++t1语句时,先将t1自加,然后将t1赋给t2。该语句操作是通过调用“前置++”运算符重载成员函数来实现的。在执行t2=++t1语句时,编译系统将t2=++t1解释为对重载函数的调用: t2=t1.operator ++ (); 由于重载函数为对象t1成员函数,所以函数体对Hour、Minute、Second的自加操作就是对t1的数据成员Hour、Minute、Second的自加操作,因而可完成对计数器对象t1的加1操作。 为了实现前置“++”运算,应将加1后的对象值t1作为返回值,即用return t1语句返回当前对象t1值。但在重载函数体内并不能直接使用对象t1,因而无法使用return t1语句。这时必须使用指向当前对象t1的指针this,如图11.2所示。由于*this=t1,所以用return *this 语句可将自加后的t1值返回给调用函数,并赋给对象t2。由于将对象值t1值作为函数返回值,所以重载函数的类型应与t1的类型相同,为TCount类型。 (4)“后置++”运算符重载成员函数的说明 在主函数中执行t3=t1++语句时,先将t1赋给t3,然后将t1自加。该语句操作是通过调用“后置++”运算符重载成员函数来实现的。在执行t3=t1++语句时,编译系统将t3=t1++解释为对重载函数的调用: 第11章 友元与运算符重载 ·289· t3=t1.operator ++ (1); 为了实现后置“++”运算,应将加1前的对象值t1作为返回值,这时应使用指向当前对象t1的指针this。在后置重载函数中先用TCount类定义一个临时对象temp,并将t1值(即*this值)赋给temp,在函数最后用return temp语句将加1前的t1值返回给函数,并赋给对象t2。如图11.3所示。 (5)用成员函数实现一元运算符的重载时,运算符的左操作数或右操作数为调用重载函数的对象。因为要用到隐含的this指针,所以运算符重载函数不能定义为静态成员函数,因为静态成员函数中没有this指针。 2.一元运算符重载函数为友元函数 重载一元运算符友元函数的一般格式为: <类型> operator <一元运算符>(类名 &对象) {函数体} 对于“++”、“――”运算符存在前置运算与后置运算的问题,因此,运算符重载函数必须分为两类。以“++”运算符为例,用友元函数来实现“++”运算符的重载时 前置“++”运算符重载的一般格式为:<类型> operator ++ ( 类名 &); 后置“++”运算符重载的一般格式为:<类型> operator ++ ( 类名 &,int ); 其中:形参为要实现“++”运算的对象,int 只是用于区分是前置还是后置运算符,并无整型数的含义。 【例11.8】用一个类来描述时间计数器,用三个数据成员分别存放时、分和秒。用友元函数重载“++”运算符,实现计数器对象的加1运算符。 # include int Hour,Minute,Second; public: TCount() { Hour=Minute=Second=0;} TCount (int h,int m,int s) { Hour=h;Minute=m;Second=s;} friend TCount operator ++(TCount &t ); //定义“前置++”运算符重载友元函数 friend TCount operator ++( TCount &t ,int ); //定义“后置++”运算符重载友元函数 void Show(int i ) { cout<<\}; TCount operator ++ (TCount & t) { t.Second++; if (t.Second==60) { t.Second=0; t.Minute++; 第11章 友元与运算符重载 ·290· if (t.Minute==60) { t.Minute=0; t.Hour++; if (t.Hour==24) { t.Hour=0;} } } return t; } TCount operator++ (TCount & t,int ) { TCount temp=t; t.Second++; if (t.Second==60) { t.Second=0; t.Minute++; if (t.Minute==60) { t.Minute=0; t.Hour++; if (t.Hour==24) { t.Hour=0;} } } return temp; } void main(void) { TCount t1(10,25,50),t2,t3; t1.Show(1); t2=++t1; t1.Show(1); t2.Show(2); t3=t1++; t1.Show(1); t3.Show(3); } 程序执行后输出: t1=10:25:50 t1=10:25:51 t2=10:25:51 t1=10:25:52 //t1=10:25:50 //先加后用 //先用后加 第11章 友元与运算符重载 t3=10:25:51 ·291· 说明: (1)对“前置++”运算符重载友元函数的说明 在主函数中t2=++t1语句的含义是:先将t1自加,然后将自加后的t1值赋给t2。该语句操作是通过调用“前置++”运算符重载友元函数来实现的。在执行t2=++t1语句时,编译系统将t2=++t1解释为对重载函数的调用: t2=operator ++ (t1); 为了实现对t1的自加操作,重载函数的形参t必须与实参t1占用同一内存空间,使对形参t的自加操作变为对实参t1的自加操作。为此,形参t必须定义为时间计数器类TCount的引用,即:TCount & t。此外,为了能将t自加的结果通过函数值返回给t2,重载函数的返回类型必须与形参t相同,即为时间计数器类TCount的引用。故“前置++”运算符重载友元函数定义为: TCount & operator ++ (TCount & t) //函数返回类型与形参t相同,均为TCount & { t.Second++; ? return t; } 当系统自动调用“前置++”运算符重载友元函数时,对形参t与实参t1自加后,用return t语句将自加的结果通过函数返回并赋给t2。从而实现对t1先加后赋值给t2的操作。 (2)对“后置++”运算符重载友元函数的说明 在主函数中t3=t1++语句的含义是:先将t1当前值赋给t3,然后再对t1自加。该语句操作是通过调用“后置++”运算符重载友元函数来实现的。在执行t3=t1++语句时,编译系统将t3=t1++解释为对重载函数的调用: t3=operator ++ (t1,1); 为了实现对t1的自加操作,重载函数的形参t必须与实参t1占用同一内存空间,使对形参t的自加操作变为对实参t1的自加操作。为此,形参t必须定义为时间计数器类TCount的引用,即:TCount & t。此外,为了能将t自加前的结果通过函数值返回给t3,在重载函数内第一条语句定义了TCount 类的临时对象temp,并将自加前t值赋给temp,在函数的最后用return temp语句返回自加前的t值。重载函数的返回类型必须与对象temp相同,即为TCount类型。故“后置++”运算符重载友元函数定义为: TCount operator ++ (TCount & t,int) //函数返回类型与temp相同,均为TCount类型 { TCount temp=t; t.Second++; ? return temp; } 当系统自动调用“后置++”运算符重载友元函数时,对形参t与实参t1自加后,用return temp语句将自加前的结果通过函数返回并赋给t3。从而实现先将t1赋给t3后将t1自加的操作。 第11章 友元与运算符重载 ·292· 【例11.9】用一个类来描述人民币币值,用两个数据成员分别存放元和分。重载“++”运算符,用运算符重载成员函数实现对象的加1运算。 # include float Dollars,Cents; public: Money() { Dollars=Cents=0;} Money(float,float); Money(float); Money operator ++( ); Money operator ++( int ); float GetAmount(float & n) //定义数据成员元与分 //定义默认的构造函数 //定义双参数构造函数 //定义单参数构造函数 //定义前置“++”运算符重载成员函数 //定义后置“++”运算符重载成员函数 //通过形参n返回元,通过函数返回分 { n=Dollars; return Cents; } ~ Money( ) { }; //缺省的析构函数 void Show( ) //定义显示元与分的成员函数 { cout< }; Money::Money(float n) //初始值n中整数部分为元,小数部分为分 { float Frac,num; Frac=modff(n,&num); // modff(n,&num)将实数n分为解为整数与小数两部分, //返回小数值给Frac,整数值送到num单元中 Cents=Frac*100; //存分值 Dollars=num; //存元值 } Money::Money(float d,float c) { float sum,dd,cc; sum=d+c/100; cc=modff(sum,&dd); Dollars=dd; Cents=cc*100; } Money Money::operator ++ () //d以元为单位(如d=10.5元), // c以分为单位(如c=125分) //将d与c转换为以元为单位, //并存入sum(如sum=10.5+125/100=11.75) //将整数(即:元)存入dd,小数(即:分)存入cc //元存入Dollars //分存入Cents //定义前置“++”重载函数 第11章 友元与运算符重载 { Cents++; if (Cents>=100) { Dollars++; Cents=Cents-100; } return *this; ·293· //分加1 //若分大于100,则元加1,分减100 //返回自加后的人民币对象值 } Money Money::operator++ (int ) { Money temp=*this; Cents++; if (Cents>=100) //将自加前人民币对象值存入临时对象temp //分加1 //若分大于100,则元加1,分减100 { Dollars++; Cents-=100; } return temp; } void main( void) { Money m1(25,50),m2(105.7),m3(10.5,125); //m1=25元50分,m2=105元70分, //m3=10.5+125/100=11.75元 Money c,d; float e1,f1,e2,f2; m1.Show(); c= ++m1; //先加后用,即:先将m1加1,然后将m1赋给c (c=m1=25元51分) d= m1++; //先用后加,即:先将m1赋给d (d=m1=25元51分) // 然后将m1加1 (m1=25元52分) c.Show(); d.Show(); c=++m2; d=m2++; //c=m2=105元71分 //d=105元71分,m2=105元72分 c.Show(); d.Show(); e1=m2.GetAmount(f1); // m2=105元72分,f1=105,e1=72 e2=m3.GetAmount(f2); // m3=11元75分 ,f2=11, e2=75 cout< 程序执行后输出: 25元50分 25元51分 //e1+e2=72+75=147 第11章 友元与运算符重载 25元51分 105元71分 105元71分 116元147分 ·294· 说明: (1)Money为描述人民币的类,其数据成员Dollars、Cents分别代表元与分。 (2)在单参数的构造函数中,使用标准函数modff(n,&num)将实数n分为解为整数与小数两部分,返回小数值,整数值送到num所指单元中。最后将整数存入元Dollars中,小数部分乘100后存入分Cents中。 (3)前置“++”运算符重载函数中,先对人民币的分加1运算,分加1存在进位问题,当分加满100后,将分Cents减100(即分清零),再将元Dollars加1,最后通过return *this语句返回自加后的人民币币值。 (4)后置“++”运算符重载函数中,先将当前人民币币值赋给临时对象temp,然后对人民币的分加1运算,当分加满100后,将分Cents减100,再将元Dollars加1,最后通过return temp 返回自加前的人民币币值。 (5)主函数中 c= ++m1语句应解释为对前置重载函数的调用:c=m1.opreator(); d= m1++语句应解释为对后置重载函数的调用:d=m1.opreator(1); 【例11.10】定义描述三维空间点(x,y,z)的类,用友元函数实现“++”运算符的重载。 # include ThreeD( float a=0,float b=0,float c=0) { x=a;y=b;z=c;} ThreeD operator + (ThreeD & t) //二个点坐标相加的“+”运算符重载成员函数 { ThreeD temp; temp.x=x+t.x; temp.y=y+t.y; temp.z=z+t.z; return temp; } friend ThreeD & operator ++(ThreeD &); //坐标点前置“++”运算符重载友元函数 friend ThreeD operator ++(ThreeD &,int);//坐标点后置“++”运算符重载友元函数 ~ ThreeD() {} void Show() { cout<<\ }; ThreeD & operator ++ (ThreeD & t) 第11章 友元与运算符重载 { t.x++; t.y++;t.z++; return t; } ThreeD operator ++ (ThreeD & t,int i) { ThreeD temp =t; t.x++; t.y++;t.z++; return temp; } void main(void) { ThreeD m1(25,50,100),m2(1,2,3),m3; m1.Show(); ++m1; m1.Show(); m2++; m2.Show(); m3=++m1+m2++; m3.Show(); } 程序执行后输出: x=25 y=50 z=100 x=26 y=51 z=101 x=2 y=3 z=4 x=29 y=55 z=106 ·295· 程序中定义的类ThreeD描述一个空间点的三维坐标,对对象执行“++”运算,即对该点坐标的三个分量(x,y,z)分别完成加1运算。主函数中: ++m1 语句被解释为对前置++运算符重载函数的调用: opreator++(m1); 运算后m1=(26,51,101)。 m2++ 语句被解释为对后置++运算符重载函数的调用: opreator++(m2,1);运算后m2=(2,3,4)。 m3=++m1+m2++语句的执行将分为三步, 第一步执行对m1的前置++运算:++m1 运算结果是返回m1自加后的一个对象,若将此对象记作t1,则t1=++m1=(27,52,102)。 第二步执行对m2的后置++运算:m2++ 运算结果将返回m2自加前的对象,若将此对象记作t2,则t2=m2++=(2,3,4)。 第三步执行将二个对象t1与t2的“和”赋给m3运算:m3=t1+t2 该运算被解释为对“+”运算符重载函数的调用: m3=t1.preator+(t2); 运算的结果为m3=(27,52,102)+(2,3,4)=(29,55,106) 。 第11章 友元与运算符重载 ·296· 11.2.4 转换函数 有时需要将类类型数据转换成另一种数据类型,如将人民币类中的元、角、分转换成以分为单位的实数。为此,C++提供了相应的类型转换函数,这种类型转换函数必须由用户在类中定义为成员函数,其一般格式为: <类名>::operator <转换后数据类型>( ) {函数体} 该转换函数的函数名为:operator <转换后数据类型>,且无参数,其返回类型为<转换后数据类型>,转换函数的作用是将对象内的数据成员转换成“转换后数据类型”。 【例11.11】定义一个时间计数器类TCount,类中数据成员为时、分、秒。编写类型转换函数,将时、分、秒变成一个以秒为单位的等价实数。 # include int Hour,Minute,Second; public: TCount (int h=0,int m=0,int s=0) { Hour=h;Minute=m;Second=s;} operator float(); //A { float second; second=Hour*3600+Minute*60+Second; return second; } }; void main(void) { TCount t(1,20,5); float s1,s2,s3; s1=t; //B s2=float (t); //C s3=(float ) t; //D cout<<\} 程序执行后输出: s1=4805 s2=4805 s3=4805 说明: (1)A行定义的转换函数将对象中三个数据成员时、分、秒转换为以秒为单位的实数,并返回实数值。 (2)B行中表达式s1=t 由编译器将其转换为对转换函数的调用: 第11章 友元与运算符重载 ·297· s1=t.operator float(); 通过调用转换函数将对象t中的数据成员转换为实数后赋给s1。即进行如下运算: s1=Hour*3600+Minute*60+Second=1*3600+20*60+5=4805 (3)C行中表达式s2= float(t)由编译器将其变换为对转换函数的调用: s2=t.operator float (); 同理D行中表达式也调用了转换函数: s3==t.operator float (); 在B、C、D行中虽然转换方式不同,但都调用转换函数operator float (),完成时间计数器类到实数的数据转换工作。 11.2.5 字符串类运算符重载 C++系统提供的字符串处理能力比较弱,字符串复制、连接、比较等操作不能直接通过“=”、“+”、“>”等运算操作符完成,而必须通过字符处理函数来完成的。例如,有字符串s1=”ABC”,s2=”DEF”,要完成s=s1+s2=“ABCDEF”的工作,则需要调用字符串处理函数:strcpy(s,s1)与strcat(s,s2)才能完成两个字符串的拼接工作。但通过C++提供的运算符重载机制,可以提供对字符串直接操作的能力,使得对字符串的操作与对一般数据的操作一样方便。如字符串s1与s2拼接成字符串s的工作,用“+”与“=”运算符组成的表达式:s=s1+s2即可完成。下面通过例题说明字符串运算符重载函数的编写方法,及重载后字符串运算符的使用方法。 【例11.12】编写字符串运算符“=”、“+”、“>”的重载Sp→ “A” 函数,使运算符“=”、“+”、“>”分别用于字符串的赋值、“B” Length 拼接、比较运算,实现字符串直接操作运算。 “C” 分析:字符串可用指向字符串的指针Sp及字符串长度? Length来描述,如图11.4所示。因此描述字符串类的数据成“\\0” 员为字符指针Sp及其长度Length。设计缺省构造函数、拷贝 图11.4 描述字符串类的数据成员 构造函数及初始化构造函数。再设计“=”、“+”、“>”运算 符重载函数,分别完成字符串赋值、拼接、比较运算。在主函数中先定义字符串对象,并调用构造函数完成初始化工作。然后使用“=”、“+”、“>”运算符,直接完成字符串的赋值、拼接、比较运算。程序设计如下: # include //定义字符串类 { protected : int Length; char *Sp; public: String() {Sp=0;Length=0;} String(const String &); //定义缺省的构造函数 //定义拷贝构造函数 第11章 友元与运算符重载 String(const char *s) //定义初始化构造函数 { Length=strlen(s); Sp=new char[Length +1]; strcpy(Sp,s); } ~String() { if (Sp) delete [] Sp;} void Show() ·298· //定义析构函数 //定义显示字符串函数 { cout< void operator = (String &); //定义字符串赋值成员函数 friend String operator + ( const String &, const String &); //定义字符串拼接友元函数 int operator > (const String &) ; //定义字符串比较成员函数 }; String::String(const String &s) { Length=s.Length; if (s.Sp) { Sp=new char [Length+1]; strcpy(Sp,s.Sp); } else Sp=0; } void String::operator=(String &s) { if (Sp) delete []Sp; Length=s.Length; if (s.Sp) { Sp=new char [Length+1]; strcpy(Sp,s.Sp); } else Sp=0; } String operator + (const String &s1,const String &s2) { String t; t.Length=s1.Length+s2.Length; t.Sp=new char [t.Length+1]; strcpy(t.Sp,s1.Sp); strcat(t.Sp,s2.Sp); return t; } int String::operator >(const String &s) 第11章 友元与运算符重载 { if (strcmp(Sp,s.Sp)>0 ) return 1; else 0; } void main (void) { String s1(\ String s4(s1),s5,s6,s7; s5=s2; s6=s4+s3; s7=s5+s3; s6.Show(); s7.Show(); if (s4>s5) s4.Show(); else s5.Show(); } 程序执行后输出: software design hardware design software ·299· 关于上述程序有几点说明如下: (1)定义初始化构造函数中: String(const char *s) { Length=strlen(s); Sp=new char[Length +1]; strcpy(Sp,s); } 形参为字符串指针变量s,为了防止在构造函数内修改实参字符串的值,特在形参类型前加关键词const,表示在构造函数内,s所指字符串是不能修改的。初始化构造函数体内,先用字符串函数strlen求出字符串s的长度,并赋给Length。然后用new运算符动态建立字符数组,将字符数组首地址赋给字符串指针Sp,最后用字符串拷贝函数strcpy将字符串s拷贝到Sp所指字符串中。完成String类对象数据成员Length与Sp的初始化工作。 (2)字符串赋值“=”运算符重载成员函数中: void String::operator=(String &s) { if (Sp) delete []Sp; Length=s.Length; if (s.Sp) { Sp=new char [Length+1]; strcpy(Sp,s.Sp); } else Sp=0; 第11章 友元与运算符重载 ·300· } 形参为String类的引用s。在函数体内先删除当前字符串内容。然后将形参字符串长度赋给当前对象的字符串长度Length。将形参字符串内容赋给当前对象。 (3)因为字符串“+”运算符重载函数为友元函数,因此参加运算的两个字符串必须以形参方式输入函数体内,所以重载函数的形参为两个String类型的对象的引用。函数体内先定义一个String类型的临时对象t,用于存放两个字符串拼接的结果。再将两个字符串的长度之和赋给t的长度t.Length,用new运算符动态分配长度为t.Length+1的内存空间,并将其地址赋给t.Sp。再用strcopy()函数将s1拷贝到t,用strcat()将s2拼接到t中,完成t=s1+s2的字符串拼接工作,最后将t返回给调用对象。由于函数返回值为对象t,所以,重载函数的返回类型为String。如图11.5所示。 s1.Sp→ “s” “o” ? “e” “\\0” s2.Sp→ s1.Length=8 “d” “e” ? “n” “\\0” t.Sp→ s2.Length=6 “s” “o” ? “n” “\\0” t.Length=14 图11.5 字符串“+”运算符拼接运算示意图 (4)在主函数中: 字符串赋值运算语句s5=s2;被编译器解释为对“=”运算符重载函数的调用:s5.opreator=(s2); 字符串拼接运算语句s6=s4+s3;被编译器解释为对“+”与“=”运算符重载函数的调用:s6.opreator=(opreator+(s4,s3)); 字符串比较运算语句s4>s5;被编译器解释为对“>”运算符重载函数的调用:s4.opreator>(s5); 11.2.6 赋值运算符和赋值运算符重载 在相同类型的对象之间是可以直接赋值的,在前面的程序例子中已多次使用。但当对象的成员中使用了动态数据类型时,就不能直接相互赋值,否则在程序执行期间会出现运算错误。 【例11.13】对象间直接赋值导致程序执行的错误。 # include char *ps; public: String() {ps=0;} 第11章 友元与运算符重载 String(char * s) { ps =new char [strlen(s)+1]; strcpy(ps,s); } ~String() { if (ps) delete [] ps;} char * GetS() { return ps;} }; void main (void ) { String s1(\ cout<<\ cout<<\ s1=s2; cout<<\ cout<<\ char c; cin>> c; } 程序执行后输出: s1=China! s2=Computer! s1=Computer! s2=Computer! ·301· 程序执行到cin>>c语句输入任意字符(如:a)时发生错误,这是因为执行s1=s2后 ,使s1、s2中的ps均指向字符串“Computer!”,当系统撤消s1时调用析构函数回收了ps所指的字符串存储空间,当撤消s2调用析构函数时,已无空间可回收,出现错误。 解决上述错误的方法是用重载运算符“=”。在上例的类String中应增加如下的赋值运算符重载函数: String & operator= (String &b) { if (ps) delete [] ps; if (b.ps) { ps= new char [strlen(b.ps)+1]; strcpy(ps,b.ps); } else ps=0; return * this; } 第11章 友元与运算符重载 ·302· 11.3 多态性与虚函数 11.3.1多态性技术 1.多态性技术的概念 函数重载是指用同名函数完成不同的函数功能,例如:编写求两实数最大值与两整数最大值的同名函数Max()如下: float Max(float a,float b) {return a>b?a:b;} int Max(int a,int b) {return a>b?a:b;} main() { cout< 在上例中,调用函数Max(1.2,4.5)中的实参为实数时,系统会根据实参类型自动调用求两实数最大值的函数。而调用函数Max(1,4)中实参为整数时,系统会自动调用求两整数最大值函数。即:用同名函数完成了求实数最大值与求整数最大值的问题,当用户在调用函数Max()的实参中给出实数则求实数的最大值,给出整数则求出整数的最大值。无疑函数重载给用户带来了使用上便利。 运算符重载是指用同名运算符完成不同的运算操作。例如:编写求两复数和的“+”运算符重载函数: Complex operator+(Complex &c) { Complex temp; temp.Real=Real+c.Real; temp.Image=Image+c.Image; return temp; } void main(void) { Complex c1(10,20),c2(15,30),c3; c3=c1+c2; float a1=10,a2=20,a3; a3=a1+a2; } 在上例中表达式c1+c2 将解释为对“+”运算符重载函数的调用:c1.operator+(c2) ; 而算术表达式a1+a2 则完成普通算术加法操作。显然,运算符重载使得用户可用同名运算符“+”完成不同数据类型变量的加法操作,这给用户使用带来了具大的方便。 以上所述函数重载与运算符重载就是C++中的多态性技术,所谓多态性技术是指调用同名函数完成不同的函数功能,或使用同名运算符完成不同的运算功能。它常用重载技术 第11章 友元与运算符重载 ·303· 与虚函数来实现。在C++中,将多态性分为两类:编译时的多态性和运行时的多态性。 2.编译时的多态性 编译时的多态性是通过函数的重载或运算符的重载来实现的。函数的重载是根据函数调用时,给出不同类型的实参或不同的实参个数,在程序执行前就能确定调用哪一个函数。对于运算符的重载,是根据不同的运算对象在编译时就可确定执行哪一种运算。如在上例中的“+”运算:c3=c1+c2 与 a3=a1+a2,根据参加“+”运算对象是复数c1、c2还实数a1、a2,就可确定是调用复数加法重载函数还是进行普通实数的加法运算。 3.运行时的多态性 运行时的多态性是指在程序执行前,根据函数名和参数无法确定应该调用哪一个函数,必须在程序执行过程中,根据具体执行情况来动态地确定。这种多态性是通过类的继承关系和虚函数来实现的,主要用于实现一些通用程序的设计。 11.3.2 虚函数 在基类中用关键字 virtual修饰的成员函数称为虚函数,虚函数的定义格式为: virtual <类名> <函数名>(参数) {函数体} 用虚函数实现“运行时多态性”的方法是:在派生类中定义与基类虚函数同名同参数同返回类型的成员函数,在派生类中定义的这个成员函数也称为虚函数。虽然基类中的虚函数与各派生类中虚函数同名同参数,但由于各虚函数的函数体是不同,因而可用同名虚函数在运行时完成对不同对象的操作,从而实现“运行时多态性”。下面举例说明用虚函数实现“运行时多态性”的方法: 【例11.14】定义基类High,其数据成员为高H,定义成员函数Show()为虚函数。然后再由High派生出长方体类Cuboid与圆柱体类Cylinder。并在两个派生类中定义成员函数Show()为虚函数。在主函数中,用基类High定义指针变量p,然后用指针p动态调用基类与派生类中虚函数Show(),显示长方体与圆柱体的体积。 # include High(float h) { H=h;} virtual void Show() //在基类中定义虚函数Show() { cout<<\}; class Cuboid:public High { private: float Length,Width; public: 第11章 友元与运算符重载 ·304· Cuboid(float l=0,float w=0,float h=0):High(h) { Length=l; Width=w;} void Show() //在长方体派生类中定义虚函数Show() { cout<<\ cout<<\ cout<<\ cout<<\olume=\ } }; class Cylinder:public High { private: float R; public: Cylinder(float r=0,float h=0):High(h) {R=r;} void Show() //在圆柱体派生类中定义虚函数Show() { cout<<\ cout<<\ cout<<\olume=\ } }; void main(void) { High h(10),*p; Cuboid cu(10,10,10); Cylinder cy(10,10); h.Show(); cu.Show(); cy.Show(); p=&h; p->Show(); p=&cu; p->Show(); p=&cy; p->Show(); } 执行程序后输出: High=10 Length=10 Width=10 High=10 Cubiod Volume=1000 第11章 友元与运算符重载 Radius=10 High=10 Cylinder Volume=3141.5 High=10 Length=10 Width=10 Cubiod Volume=1000 Radius=10 High=10 Cylinder Volume=3141.5 High=10 ·305· 在主函数中通过调用三个不同对象的成员函数 h.Show(); cu.Show(); cy.Show(); 分别输出高、长方体、圆柱体的值。因在编译时,根据对象名,就可确定调用哪一个成员函数,所以这是编译时的多态性。 在主函数中将三个不同类型的对象起始地址依次赋给基类的指针变量p, p=&h; p->Show(); p=&cu; p->Show(); p=&cy; p->Show(); 这在C++中是允许的,即可将由基类所派生出来的派生类对象的地址&cu与&cy赋给基类类型的指针变量p。当基类指针变量指向不同的对象时,尽管调用的形式完全相同,均为p->Show(); 但确调用了不同对象中函数。因此输出了不同的结果,所以这是运行时的多态性。 关于虚函数有几点说明如下: (1)当基类中将成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中虚函数同名同参数同返回类型,如上例中基类与派生类中的虚函数名均为Show,均无参数,返回类型均为void。在定义派生类中的虚函数时,可不加关键词virtual。 (2)实现动态的多态性时,必须使用基类类型的指针变量,使该指针指向不同派生类的对象,并通过调用指针所指向的虚函数才能实现动态的多态性。 (3)虚函数必须是类的一个成员函数,不能是友元函数,也不能是静态的成员函数。 (4)在派生类中没有重新定义虚函数时,与一般的成员函数一样,当调用这种派生类对象的虚函数时,则调用基类中的虚函数。 (5)可将析构函数定义为虚函数,但不能将构造函数定义为虚函数。通常在释放基类中和派生类中动态申请的存储空间时,也要将析构函数定义为虚函数,以便实现撤消对象时的多态性。 (6)虚函数与一般函数相比较,调用时执行速度要慢一些。为了实现多态性。在每一个派生类中均要保持相应虚函数的入口地址表,函数调用机制也是间接实现的。因此除了要编写一些通用的程序并一定要使用虚函数才能完成其功能要求外,通常不必使用虚函数。 第11章 友元与运算符重载 ·306· 11.3.3 纯虚函数 在定义一个基类时,若无法定义基类中虚函数的具体操作,虚函数的具体操作完全取决于其不同的派生类。这时,可将基类中的虚函数定义为纯虚函数。定义纯虚函数的一般格式为: virtual <类型> <纯虚函数名>(形参表)=0; 由纯虚函数的定义格式可以看出如下几点: (1)由于纯虚函数无函数体,所以在派生类中没有重新定义纯虚函数之前,是不能调用这种函数的。 (2)将函数名赋值为0的含义是,将指向函数体的指针值赋初值0。 (3)将至少包含一个纯虚函数的类称为抽象类。这种类只能作为派生类的基类,不能用来说明对象。其理由很明显;因为虚函数没有实现部分,所以不能产生对象。但可定义指向抽象类的指针,即指向这种基类的指针。当用这种基类指针指向其派生类的对象时,必须在派生类中重载纯虚函数,否则会产生程序的运行错误。 【例11.15】定义抽象基类High,其数据成员为高H,定义Show()为纯虚函数。然后再由High派生出长方体类Cuboid与圆柱体类Cylinder。并在两个派生类中重新定义虚函数Show()。在主函数中,用基类High定义指针变量p,然后用指针p动态调用派生类中虚函数Show(),显示长方体与圆柱体的体积。 # include High(float h) {H=h;} virtual void Show()=0; //在基类中定义纯虚函数Show() }; class Cuboid:public High { private: float Length,Width; public: Cuboid(float l=0,float w=0,float h=0):High(h) {Length=l; Width=w;} void Show() //在长方体派生类中定义虚函数Show() { cout<<\ cout<<\ cout<<\ cout<<\olume=\ } }; 第11章 友元与运算符重载 ·307· class Cylinder:public High { private: float R; public: Cylinder(float r=0,float h=0):High(h) { R=r;} void Show() //在圆柱体派生类中定义虚函数Show() { cout<<\ cout<<\ cout<<\olume=\ } }; void main(void) { High *p; Cuboid cu(10,10,10); Cylinder cy(10,10); p=&cu; p->Show(); p=&cy; p->Show(); } 执行程序后输出: Length=10 Radius=10 Width=10 High=10 High=10 Cubiod Volume=1000 Cylinder Volume=3141.5 若在主函数中增加说明: High h; 则因为抽象类High不能产生对象,编译时将给出错误信息。 (4)在以抽象类作为基类的派生类中必须有纯虚函数的实现部分,即必须有重载纯虚函数的函数体。否则,这样的派生类也是不能产生对象的。 综上所述,可将纯虚函数归结为:抽象类的唯一用途是为派生类提供基类,纯虚函数的作用是作为派生类的成员函数的基础,并实现动态多态性。 下面通过例子来说明抽象类的简单应用。 *【例11.16】建立一个双向链表,要完成插入一个结点、删除一个结点、查找某一个结点操作,并输出链表上各结点值。设结点只有一个整数。 分析:因链表的插入、删除、查找等操作都是相同的,只是结点上的信息随着不同的应用有所不同,所以可将实现链表操作部分设计成通用的程序。一个结点的数据结构用两个类来表示。如图11.6所示。类IntObj的数据成员描述结点信息,成员函数完成两个结点 第11章 友元与运算符重载 ·308· 比较,输出结点数据等。类Node的数据成员中,包括要构成双向链表时,指向后一个结点的后向指针Next,指向前一个结点的前向指针Prev,指向描述结点数据的指针Info。另外定义一个类List,把它作为类Node的友元,它的成员数据包括指向链表的首指针Head,指向链尾的指针Tail,成员函数实现链表的各种操作,如插入一个结点,删除一个结点等。由于类List是类Node的友元,因此它的成员函数可以访问Node的所有成员。 指针 指针 前向指针 前向指针 后向指针 后向指针 图11.6 链表中的数据指针 # include { public: Object(){} virtual int IsEqual(Object &)=0; virtual void Show()=0; virtual ~Object() {} }; class Node //缺省的构造函数 //实现两个结点数据比较的纯虚函数 //输出一个结点上数据的纯虚函数 //析构函数定义为虚函数 //定义结点类 //指向描述结点的数据域 //用于构成链表前、后指针 //定义缺省的构造函数 //完成拷贝功能的构造函数 描述结点信息 描述结点信息 { private: Object *Info; Node *Pre, *Next; public: Node() { Info=0;Pre=0;Next=0;} Node (Node &node) { Info=node.Info; Pre=node.Pre; Next=node.Next; } void FillInfo(Object *obj) { Info=obj;} friend class List; //使Info指向数据域 //定义List为Node的友元类 第11章 友元与运算符重载 }; class List { private: Node *Head, *Tail; public: List() //实现双向链表操作的类 //定义链表首和链表尾指针 //置空链表 ·309· { Head=Tail=0;} ~List() { DelList();} void AddNode(Node *); Node * DelNode(Node *); Node * LookUp(Object &); void ShowList (); void DelList(); //释放链表所占的存储空间 //在链表尾增加一个结点的成员函数 //删除链表中指定结点的成员函数 //在链表中查找指定结点的成员函数 //输出整条链表上的数据的成员函数 //删除整条链表的成员函数 }; void List::AddNode(Node * node) { if (Head==0) { Head=Tail=node; node->Next=node->Pre=0; } else { Tail->Next=node; //链表为空表时 //链表首、尾指针指向结点 //该结点前、后指针置为空 // 链表非空 //将结点加入到链表尾 node->Pre=Tail; Tail=node; node->Next=0; } } Node * List::DelNode(Node * node) { if (node==Head) if (node==Tail) Head=Tail=0; else { Head=node->Next; Head->Pre=0; } else { node->Pre->Next=node->Next; //删除指定结点 //删除链表首结点 //链表只有一个结点 // 删除链表首结点 //删除非首结点 if (node!=Tail) node->Next->Pre=node->Pre; else Tail=node->Pre; 第11章 友元与运算符重载 ·310· } node->Pre=node->Next=0; return(node); } Node * List::LookUp(Object &obj) { Node *pn=Head; while (pn) { if (pn->Info->IsEqual(obj)) return pn; pn=pn->Next; } return 0; } void List::ShowList() { Node *p=Head; while (p) { p->Info->Show(); p=p->Next; } } void List::DelList() { Node *p, *q; p=Head; while(p) { delete p->Info; q=p; p=p->Next; delete q; } } class IntObj: public Object { private: int data; public: IntObj(int x=0) { data=x;} void SetData(int x) { data=x;} int IsEqual(Object &); void Show() //从链表中查找一个结点 //输出链表上各结点的数据值 //删除整条链表 //由抽象类派生出描述结点数据的类 第11章 友元与运算符重载 { cout<<\重新定义虚函数 }; int IntObj::IsEqual(Object &obj) { IntObj & temp=(IntObj &) obj; return (data==temp.data); } void main(void) { IntObj *p; Node *pn , *pt, node; List list; for (int i=0;i<5;i++) { p=new IntObj(i+100); pn=new Node; pn->FillInfo(p); list.AddNode(pn); } list.ShowList(); ·311· //重新定义比较两个结点是否相等的虚函数 //建立包括五个结点的双向链表 //动态建立一个IntOb类的对象 //建立一个新结点 //填写结点的数据域 //将新结点加到链表尾 //输出链表上各结点 cout< da.SetData(102); pn=list.LookUp(da); if (pn) pt=list.DelNode(pn); list.ShowList(); cout< if (pn) list.AddNode(pt); list.ShowList(); cout< 执行程序后输出 Data=101 Date=101 Data=101 Date=102 Data=103 Date=103 //给要查找的结点置数据值 //从链表上查找指定的结点 //若找到,则从链表上删除该结点 //输出已删除结点后的链表 //将结点加入链表尾 //输出已增加一个结点的链表 Data=103 Date=104 Data=104 Date=104 Date=102 该例子只提供了双向链表的基本操作:连续五次将新建的结点加到链表尾,显示整个链表上各结点的数据值,从链表上查找一个指定的结点,再删除之,并显示删除这个结点后的链表结点数据,再将删除的这个结点加到链表尾并显示新的链表。 例中的类IntObj是由抽象类Object派生而来的,可以根据实际数据结构的需要来定义从基类中继承来的虚函数IsEqual()和Show()的具体实现。在上例中,链表上的结点只有一个整数,所以只要判断两个结点上整数是否相同。由抽象类Object派生出来的不同的派生类均可重新定义这两个纯虚函数,这样就可以实现对不同类的对象使用相同的接口实现不 第11章 友元与运算符重载 ·312· 同的操作。在程序中加入注解,说明了每个函数的功能及主要语句的作用。为此不对每一个函数作进一步的说明。 11.4 类与对象的特性 在第9章到第11章中,我们学习了类与对象的许多特性,如:封装性、派生与继承、多态性等。本节再对这些特性作如下小结。 1.封装性 面向对象的程序设计方法(OOP),是将描述某一类事物的数据与所有处理这些数据的函数封装成一个类。这样做的好处是,可以将描述一个事物的数据隐藏起来,可以做到只有通过类中的函数才能修改类中的数据。数据结构的变化,仅影响封装在一起的函数。同样地,修改函数时仅影响封装在一起的数据。真正实现了封装在一起的函数和数据不受外界的影响。 用类可以定义对象,对象数据与数据处理函数组成一个完全独立的模块,对象中的私有数据只能由对象中函数进行处理,其它任何函数都不能对其进行处理,这种特性称封装性。封装性使对象中的私有数据在对象的外部是不可见的,外部只能通过公共的接口(对象中公有函数)与对象中的私有数据发生联系。从而可以显著地提高程序模块的独立性和可维护性。 2.派生与继承性 一个类可以派生出子类。子类可以从它的父类中部分或全部地继承各种函数(形为)与数据(属性),并增加新的形为或属性。类的封装性为类的继承提供了基础。 例如,VC++中的按钮基类可派生出普通按钮、图标按钮、快捷按钮3个子类按钮。这3种按钮都继承了按钮类的全部数据与程序代码。利用派生与继承性可从基类派生出子类,减少程序设计的编程量。 3.多态性 对象之间传送的信息称为消息,同一个消息为不同对象所接收时,可以导致完全不同的行为,这种特性称为多态性。例如,在类中定义两个取绝对值同名函数:abs (int x), abs (double x),然后由对象调用取绝对值函数abs(-3)。此时,对象会将消息(即实参-3)传送给函数的形参,而对象会根据消息的数据类型(整型或实型)调用对应函数完成对整数或实数取绝对值的任务。这就是用重载函数完成多态性的例子。 多态性的重要性在于允许一个类体系的不同对象,各自以不同的方式响应同一个消息,这样就可以实现“同一接口,多种方法”。 类的多态性是通过多态性技术实现的,多态性技术是指调用同名函数完成不同的函数功能,或使用同名运算符完成不同的运算功能。它常用重载函数与虚函数来实现。函数重载或运算符重载均属于编译时的多态性,而虚函数由属于运行时的多态性。 4.对象的消息机制 对象是类的一个实例,类在程序运行时被用作样板来建立对象。对象是动态产生和动态消亡的。对象之间的通信是通过系统提供的消息机制来实现的。系统或对象可把一个消息发送给一个指定的对象或某一类对象。接收到消息的对象必须处理所接收到的消息,对 第11章 友元与运算符重载 ·313· 象对消息的处理是通过激活本对象内相应的函数来实现,并根据所处理的情况返回适当的结果。 面向对象的程序设计VC++就是用类来描述客观世界中的事物。VC++类库(MFC)为用户提供大量的类(如:窗口类、输出类、图形类、文档模板类),供用户编程开发使用。用户只要知道这些类的接口功能、作用及用法,并使用这些类去定义对象,便可实现Windows中窗口、图形、文档模板等设计,而不需要了解类中程序代码与数据结构。这无疑为用户设计复杂问题的程序提供了便利。 本章小结 1.友元的概念 由于类的封装性与安全性,类的私有数据成员只能在类中直接使用,而在类外必须要通过公有接口函数才能使用,这会给用户编程带来许多不便。为了能在类外直接使用类的私有成员或保护成员,C++提供了友元。某类的友元可使用该类的所有数据成员或成员函数。虽然友元方便了用户,但却破坏了数据的安全性,因此友元的使用要适度。 2.友元的定义 友元有三类:普通函数、成员函数、类,这三类友元的定义方式如下: (1)将普通函数定义为某类的友元函数的方法,是在该类中增加用freind修饰普通函数原型说明:friend <类型><普通函数名>(形参); 其中形参应包含用类定义的对象或对象的引用。 (2)类C的成员函数定义为类D的友元函数的方法见11.1.2节。 (3)类C定义为类D的友元的方法是在类D中增加语句:freind class C; 即可。 3.运算符重载 运算符重载是指用同一运算符完成不同的运算操作。运算符重载是通过运算符重载函数来实现的。运算符重载函数分为一元运算符重载函数和二元运算符重载函数。运算符重载函数可通过成员函数或友元函数来实现。 (1)二元运算符重载函数 ①用成员函数重载运算符 <类型><类名>:: 在执行运算符操作时,编译器将对运算符的操作解释为对运算符成员重载函数的调用,并将运算符左操作数作为调用重载函数的对象,右操作数作为重载函数的实参。因此,重载函数形参常设置为:<类名> &c。 ②用友元函数重载运算符 在类中作引用性说明:friend <类型> 重载函数作为普通友元函数一般应写在类外。在执行运算符操作时,编译器将对运算符的操作解释为对运算符友元重载函数的调用,并将运算符左、右操作数作为调用友元重载函数的实参。因此,友员重载函数形参常设置为:<类名> &c1, <类名> &c2 (2)一元运算符重载函数 ①用成员函数重载“++”运算符 前置++:<类型><类名>:: 第11章 友元与运算符重载 ·314· 后置++:<类型><类名>:: 前置++:friend <类型><类名>:: 其中形参中的int 只用于区别前置++重载函数,还是后置++重载函数,并无整型参数的含义。对于前置++成员函数,必须用this指针返回自加结果。 (3)字符串运算符重载函数 使用字符串运算符重载函数,可使字符串拷贝、拼接、比较等操作直接用字符串运算符“=”、“+”、“>”、“<”来进行。字符串常进行二元运算,其重载函数的定义格式与二元运算符重载函数相同。 4.类型转换函数 当需要将类类型的数据转换成其它类型数据时,要用到类型转换函数,其定义格式: 类名::operator <转换后数据类型>( ) {函数体} 5.多态性技术 多态性技术是指调用同名函数完成不同的函数功能,或使用同名运算符完成不同的运算功能。它常用重载函数与虚函数来实现。函数重载或运算符重载均属于编译时的多态性,而虚函数则属于运行时的多态性。 6.虚函数 在基类中用关键字 virtual修饰的成员函数称为虚函数,定义格式为: virtual <类名> <函数名>(参数){函数体} 用虚函数实现“运行时的多态性”的方法是:在派生类中定义与基类虚函数同名同参数同返回类型的虚函数,用基类定义指针变量p,将基类或派生类对象的地址赋给p(即p=&对象)后,用p->虚函数,则可实现“运行时的多态性”。 7.纯虚函数 将函数名赋0值且无函数体的虚函数称为纯虚函数,定义格式为: virtual <类名> <函数名>(参数)=0; 含有纯虚函数的类称为抽象类,不能用抽象类定义对象。因为纯虚函数无函数体,所以纯虚函数不能调用,因此必须在派生类中重新定义虚函数。 习题11 11.1 为何要使用友元?使用友元有哪些利与蔽?友元有哪三类?如何定义? 11.2 定义学生成绩类Score,其私有数据成员有学号、姓名、物理、数学、外语、平均成绩。再定义一个能计算学生平均成绩的普通函数Average(),并将该普通函数定义为Score类友元函数。在主函数中定义学生成绩对象,通过构造函数输入除平均成绩外的其它信息,然后调用Average()函数计算平均成绩,并输出学生成绩的所有信息。 11.3 定义描述圆的类Circle,其数据成员为圆心坐标(X,Y)与半径R。再定义一个描述圆柱体的类Cylinder,其私有数据成员为圆柱体的高H。定义计算圆柱体体积的成员函数Volume(),并将Volume()定义为圆类Circle的友元函数,该函数使用圆类对象的半径R来计算圆柱体的体积。在主函数中定义圆的对象ci,圆心坐标为(12,15),半径为10。再定义圆
正在阅读:
第11章 友元与运算符重载07-06
产科门诊规章制度03-01
2016年教师公招考试复习资料带有全真模拟试卷参考答案全套备考资03-08
我的烦恼的三年级范例05-01
中学课程管理制度05-26
电视节目制作技艺复习提纲05-06
勤劳节俭主题班会(中低年级)05-18
澎湃新闻网是谁办的 澎湃新闻网背景揭秘05-19
病历书写规范试题及答案03-02
- 多层物业服务方案
- (审判实务)习惯法与少数民族地区民间纠纷解决问题(孙 潋)
- 人教版新课标六年级下册语文全册教案
- 词语打卡
- photoshop实习报告
- 钢结构设计原理综合测试2
- 2014年期末练习题
- 高中数学中的逆向思维解题方法探讨
- 名师原创 全国通用2014-2015学年高二寒假作业 政治(一)Word版
- 北航《建筑结构检测鉴定与加固》在线作业三
- XX县卫生监督所工程建设项目可行性研究报告
- 小学四年级观察作文经典评语
- 浅谈110KV变电站电气一次设计-程泉焱(1)
- 安全员考试题库
- 国家电网公司变电运维管理规定(试行)
- 义务教育课程标准稿征求意见提纲
- 教学秘书面试技巧
- 钢结构工程施工组织设计
- 水利工程概论论文
- 09届九年级数学第四次模拟试卷
- 运算符
- 重载
- 为什么要做酒店客史档案管理
- 3-小编教你怎样报考自考本科
- 经典生活语录 智慧篇
- 在交通系统工作会议上的讲话()
- 班干部值日表
- 人教版选修(3-1)1.7《静电现象的应用》word学案
- 2018年中国齿轮现状分析及市场前景预测(目录)
- Candence原理图库设计指南
- 新东方面试经历2
- 江西版一年级上册美术教案(1)
- 褐煤加工安全生产制度汇编 Microsoft Word 文档
- 安装工程综合费率表
- 《公共危机管理》教案
- 游泳专业毕业论文
- 数控加工工艺及其编程课程设计 - 图文
- 牛津上海版小学四年级上同步练习4A练习
- 学校突发水污染事件应急预案
- xxx光伏电站监控系统安全防护实施方案
- VB学生成绩查询统计系统实验报告
- 尔雅 大学启示录:如何读大学? 课堂检测答案