面向对象程序设计语言C++中的多态性
C++是以C语言为基础,支持数据抽象和面向对象的程序设计语言。C++对C语言的扩充部分汲取了
许多著名语言中最优秀的特征,如从Algo168中吸取了操作符重载机制等。由于C++语言具有与C语言一
样的高执行效率,并容易被熟悉C语言的软件人员接受,因而很快得以流行。但这种混合型面向对象的
程序设计语言是一种新的程序设计语言,人们对它许多潜在的性能(封装、继承、多态等)还没有充分
地理解和应用,没有充分发挥其优势。多态性是面向对象系统的重要概念之一,它指的是同样的消息
能被发送到父类的对象和它的子类的对象。本文重点讨论多态性在程序设计中的应用。
1 多态性的实现形式
从广义上说,多态性是指一段程序能够处理多种类型对象的能力。在C++语言中,这种多态性可以
通过强制多态、重载多态、类型参数化多态、包含多态4种形式来实现。类型参数化多态和包含多态统
称为一般多态性,用来系统地刻画语义上相关的一组类型。重载多态和强制多态统称为特殊多态性,
用来刻画语义上无关联的类型间的关系。
包含多态是指通过子类型化,1个程序段既能处理类型T的对象,也能够处理类型T的子类型S的对
象,该程序段称为多态程序段。公有继承能够实现子类型。在包含多态中,1个对象可以被看作属于不
同的类,其间包含关系的存在意味着公共结构的存在。包含多态在不少语言中存在,如整数类型中的
子集构成1个子类型。每一个子类型中的对象可以被用在高一级的类型中,高一级类型中的所有操作可
用于下一级的对象。在C++中公有继承关系是一种包含多态,每一个类可以直接公有继承父类或多个父
类,如语句class Dpublic P1,public P2{……};表示类D分别是类P1和类P2的子类型。
类型参数化多态是指当1个函数(类)统一地对若干类型参数操作时,这些类型表现出某些公共的语
义特性,而该函数(类)就是用来描述该特性的。在类型参数化多态中,1个多态函数(类)必须至少带有
1个类型参数,该类型参数确定函数(类)在每次执行时操作数的类型。这种函数(类)也称类属函数(类)
。类型参数化多态的应用较广泛,被称为最纯的多态。
重载是指用同一个名字命名不同的函数或操作符。函数重载是C++对一般程序设计语言中操作符重
载机制的扩充,它可使具有相同或相近含义的函数用相同的名字,只要其参数的个数、次序或类型不
一样即可。例如:
int min(int x,int y); //求2个整数的最小数
int min(int x,int y,int z); //求3个整数的最小数
int min(int n,int a[]); //求n个整数的最小数
当用户要求增加比较2个字符串大小的功能时,只需增加:
char*min(char*,char*);
而原来如何使用这组函数的逻辑不需改变,min的功能扩充很容易,也就是说维护比较容易,同时也提
高了程序的可理解性,“min”表示求最小值的函数。
强制是指将一种类型的值转换成另一种类型的值进行的语义操作,从而防止类型错误。类型转换
可以是隐式的,在编译时完成,如语句D=I把整型变量转换为实型;也可以是显式的,可在动态运行
时完成。
从总体上来说,一般多态性是真正的多态性;特殊多态性只是表面的多态性。因为重载只允许某
一个符号有多种类型,而它所代表的值分别具有不同的、不相兼容的类型。类似地,隐式类型转换也
不是真正的多态,因为在操作开始前,各值必须转换为要求的类型,而输出类型也与输入类型无关。
相比之下,子类与继承却是真正的多态。类型参数化多态也是一种纯正的多态,同一对象或函数在不
同的类型上下文中统一地使用而不需采用隐式类型转换、运行时检测或其它各种限制。
2 多态性应用
2.1 包含多态
C++中采用虚拟函数实现包含多态,虚拟函数为C++提供了更为灵活的多态机制,这种多态性在程
序运行时才能确定,因此虚拟函数是多态性的精华,至少含有一个虚拟函数的类称为多态类。包含多
态在程序设计中使用十分频繁。
派生类继承基类的所有操作,或者说,基类的操作能被用于操作派生类的对象,当基类的操作不
能适应派生类时,派生类需重载基类的操作,见下例中的void circle::showarea()。
#include <iostream.h>
class point //屏幕上的点类
{int x,y;
public;
point(int x1,int y1)
{x=x1;y=y1;}
void showarea()
{cout<<″Area of point is:″<<0.0<<endl;}
};
class circle:public point//圆类
{int radius;
public:
circle(int x,int y,int r):point(x,y){ radius=r;}
void showarea(){cout<<″Area of circle is:″<<3.14
*radius*radius<<endl;}
};
void disparea(const point*p) //多态程序段
{p->showarea();}
void main()
{circle c1(1,1,1);disparea(&c1);
}
程序的运行结果为0.0(正确结果应为3.14),出错的原因是:表达式p->showarea()中的函数调
用在编译时被束定到函数体上,使得这个表达式中的函数调用执行point类的showarea()。为此,当程
序员在实现一个派生类而变动了基类中的操作实现时,C++提供的虚函数机制可将这种变动告诉编译器
,即将关键字virtual放在类point中该函数的函数说明之前(virtual void showarea()),程序其它部
分保持不变(circle::showarea()自动地成为虚函数),编译器就不会对函数调用p->showarea()进
行静态束定(在编译/连接时进行的束定)而产生有关的代码,使函数调用与它所应执行的代码的束定
工作在程序运行时进行,这样上述程序的运行结果即为3.14。在程序运行时进行的束定被称为动态束
定。
利用虚函数,可在基类和派生类中使用相同的函数名定义函数的不同实现,从而实现“一个接口
,多种方式”。当用基类指针或引用对虚函数进行访问时,软件系统将根据运行时指针或引用所指向
或引用的实际对象来确定调用对象所在类的虚函数版本。
C++语言还增加了纯的虚函数机制用来更好地设计包含多态性。对于如图1(a)所示结构的类层次,
假如每个类中都有一个函数“void display(void);”,那么,怎样对它们按多态性进行统一处理呢
?对这类问题应先设计一个抽象的类,使它成为所有类的祖先类,如图1(b)所示。设置类A的目的是由
它说明统一使用该层次中的display()函数的方法(赋值兼容规则从语法上保证了A的子孙类可按A说明
的方式使用display()函数;多态性则从语义上保证了在执行时,根据实际的对象访问相应对象类中的
display()函数)。
为了保证在类A中设置的display()函数是抽象动作,并能说明类A是一个抽象的类,在C++中,可用纯
的虚函数语言机制在类A中声明1个成员函数“virtual void display(void)=0;”。请注意,在类A
的子孙类中要么给出display()的定义,要么重新将该函数声明为纯的。
从上面的分析可以看出,类A的设计尽管是用继承性语法表达的,但它的主要目的不是为代码共享而设
计的,而是为了提高多态性而设计的,它是另一个维度的抽象。
2.2 类型参数化多态
参数化多态又称非受限类属多态,即将类型作为函数或类的参数,避免了为各种不同的数据类型
编写不同的函数或类,减轻了设计者负担,提高了程序设计的灵活性。
模板是C++实现参数化多态性的工具,分为函数模板和类模板二种。
类模板中的成员函数均为函数模板,因此函数模板是为类模板服务的。类模板在表示数组、表、
矩阵等类数据结构时,显得特别重要,因为这些数据结构的表示和算法的选择不受其所包含的元素的
类型的影响。下面是一个通用数组类模板的定义。
template <class T,int N>
class array
{T elem[N];
public:
array(){for(int j=0;j<N;j++)elem[j]=0;}
T& operator[](int index){return elem[index];}
void modi(int index,T value){elem[index]=value;}
};
其中,T是类型参数,N是常量参数。T和N的实际值是在生成具体类实例时指定的。类模板的< >
可以包括任意个类型参数或常量参数,但至少应有一个参数。在类模板定义中,可在程序中通常使用
类型指定的任何地方使用类型参数,可在通常使用特定类型常量表达式的任何地方使用常量参数。
成员函数模板可放在类模板中定义,也可放在类外定义,例如:
template <class T,int N>
T& array<T,N>::operator[](int index){return elem[index];}
当由类模板生成一个特定的类时,必须指定参数所代表的类型(值)。例如,1个元素类型为int、
长度为100的数组类使用类型表达式array<int,100>来表示,这个类型表达式被用于说明数组类对
象。例如:
array<int,100> a: //生成特定类的对象a
a.modi(1,34); //对象a访问成员函数
类模板一旦生成了对象和指定了参数表中的类型,编译器在以后访问数据成员和调用成员函数时
完全强制为这些类型。
在C++中可以重载定义多个同名的函数模板,也可以将1个函数模板与1个同名函数进行重载定义。
例如:
template <class T> T min(T a,T b){return a<b﹖a:b;}
template <class T>
T min(T a,T b,T c){T x=min(a,b);return min(x,c);}
int min(int a,int b)[return a<b﹖a:b;}
调用min(3,7),则调用第3个函数;调用min(3.8.5.9),编译器将根据带2个参数的模板生成新函
数min(double,double);调用min(4,90,76),则编译器根据带3个参数的模板生成新函数min(int,
int,int);而调用min(56.3,48,71),编译将给出错误信息,说明无法从上面的模板中生成函数
min(double,double,double),因为编译器在类型推导时,不存在类型强制。
模板描述了1组函数或1组类,它主要用于避免程序员进行重复的编码工作,大大简化、方便了面
向对象的程序设计。
2.3 重载多态
重载是多态性的最简形式,而且把更大的灵活性和扩展性添加到程序设计语言中,它分成操作符
重载和函数重载。
C++允许为类重定义已有操作符的语义,使系统预定义的操作符可操作类对象。C++语言的一个非
常有说服力的例子是count对象的插入操作(<<)。由于其类中定义了对位左移操作符“<<”进行重
载的函数,使C++的输出可按同一种方式进行,学习起来非常容易。并且,增加一个使其能输出复数类
的功能(扩充)也很简单,不必破坏原输出逻辑。
C++规定将操作符重载为函数的形式,既可以重载为类的成员函数,也可以重载为类的友员函数。
用友员重载操作符的函数也称操作符函数,它与用成员函数重载操作符的函数不同,后者本身是类中
成员函数,而它是类的友员函数,是独立于类的一般函数。注意重载操作符时,不能改变它们的优先
级,不能改变这些操作符所需操作数的个数。
重定义已有的函数称为函数重载。在C++中既允许重载一般函数,也允许重载类的成员函数。如对
构造函数进行重载定义,可使程序有几种不同的途径对类对象进行初始化。还允许派生类的成员函数
重载基类的成员函数,虚函数就属于这种形式的重载,但它是一种动态的重载方式,即所谓的“动态
联编(束定)”。
2.4 强制多态
强制也称类型转换。C++语言定义了基本数据类型之间的转换规则,即:
char->short->int->unsigned->long->unsigned long->float->double->long
double
赋值操作是个特例,上述原则不再适用。当赋值操作符的右操作数的类型与左操作数的类型不同
时,右操作数的值被转换为左操作数的类型的值,然后将转换后的值赋值给左操作数。
程序员可以在表达式中使用3种强制类型转换表达式:①static_cast<T>(E);②T(E);③(T)E
。其中任意一种都可改变编译器所使用的规则,以便按自己的意愿进行所需的类型强制。其中E 代表
一个运算表达式,T代表一个类型表达式。第三种表达形式是C语言中所使用的风格,在C++中,建议不
要再使用这种形式,应选择使用第一种形式。例如,设对象f的类型为double,且其值为3.14。则表达
式static_cast<int>(f)的值为3,类型为int。
通过构造函数进行类类型与其它数据类型之间的转换必须有一个前提,那就是此类一定要有一个
只带1个非缺省参数的构造函数,通过构造函数进行类类型的转换只能从参数类型向类类型转换,而想
将一个类类型向其它类型转换是办不到的。类类型转换函数就是专门用来将类类型向其它本类型转换
的,它是一种类似显式类型转换的机制。转换函数的设计有以下几点要特别注意:①转换函数必须是
类的成员函数;②转换函数不可以指定其返回值类型;③转换函数其参数行不可以有任何参数。
强制使类型检查复杂化,尤其在允许重载的情况下,导致无法消解的二义性,在程序设计时要注
意避免由于强制带来的二义性。
本文地址:http://com.8s8s.com/it/it25400.htm