C/C++程序设计
1
多态是面向对象理论的三大支柱之一。
多态是不能凭空独立的支柱,需要另外两个支柱构成三足鼎立之势,另外两个支柱是封装和继承。
封装是数据和算法的抽象描述,继承是关于抽象描述的演化发展,多态则是对于程序流程前瞻性的刻画。
简单的说多态是基对象的指针调用派生类的虚函数。
C/C++程序设计
2
一、函数覆盖和函数重载二、虚函数的声明三、多态类层次之间的适应关系四、静态联编和动态绑定
C/C++程序设计
3
一、函数覆盖和函数重载覆盖函数是同一继承树层次上类域分辨符不同、函数名相同形参类型、位置、个数相同返回类型相同的函数。重载函数是相同作用域内仅函数名相同而形参类型有所不同的函数。
考虑名称为 Draw的成员函数,基类和派生类的两个版本的全限定名格式为:
void CBase::Draw (int,int); void CBase::Draw (long);
void CDerive::Draw(int,int); void CDerive::Draw (long);
C/C++程序设计
4
CBase::Draw(int,int)与 CBase::Draw(long)之间的关系是函数重载,表示同级别同名函数之间的平辈关系,都进行描画操作但处理的重点不同,Draw(int,int)可理解将当前类的数据映射到二维平面,Draw(long)可理解将当前类的数据投影到一条直线。
CDerive::Draw (int,int)相对于基类版本 CBase::Draw(int,int)是覆盖关系,
CDerive::Draw(long) 相对于基类版本 CBase::Draw(long)
也是覆盖关系。
交叉的 CBase::Draw (int,int)和 CDerive::Draw (long)
是没有联系的不同的函数。
C/C++程序设计
5
对于编译器而言重载根据形参类型进行名称细分,上下覆盖的函数通过类域分辨符进行名称界定。覆盖的含义在于派生类的数据集合不但包含基类的数据还包含派生类新添的数据,因此派生类的覆盖版本对于基类的版本是一个超越的关系。派生类的 Draw函数的定义通常是如下的格局:
void CDerive::Draw (int x,int y)
{ CBase::Draw(x,y);处理派生类的新添成员的绘画代码 ; }
函数重载可以与类无关,而函数覆盖必须与类的继承和成员的超越相联系,重载函数可以是全局函数,覆盖函数只能是成员函数。非虚拟重载函数的匹配在编译阶段确定,虚拟函数的调用一般在运行时确定。
C/C++程序设计
6
二、虚函数的声明虚函数是构筑多态的基石,存在虚函数的类称为多态类。虚函数是通过关键字 virtual来前置界定的,仅在一个非静态的成员函数声明语句前冠以关键字 virtual则这个成员函数就声明为虚函数,格式为:
class ClassA
// ret_type,type1,type2,typen是已声明的类型名
{ //// ;...;////
virtual ret_type vfunct (type1 v1,type2 v2,...,typen vn);
};
C/C++程序设计
7
关键字 virtual一般位于函数的返回类型之前,虚函数的定义与非静态的成员函数没有差别。在成员函数 vfunct前加一个关键字 virtual,则 vfunct是虚函数。
去掉前面的 virtual,vfunct是非虚函数。不要在类外的虚函数的定义部分再加 virtual,否则是修饰非法错误。
派生类相关的虚拟函数是覆盖函数。
基类中声明的虚函数派生类的覆盖版本自动成为虚函数,可以不再加关键字 virtual修饰。在虚函数的覆盖版本前加上 virtual是清晰的风格。
C/C++程序设计
8
在类内虚函数的声明处可以直接给出函数的定义体,但不意味虚函数是内联函数,虚 函数一般是不内联展开的函数。
只有虚拟的成员函数,virtual决不修饰数据成员。
虚函数可以似非虚成员函数一样调用,虚函数同样可以重载只要形参列表不同。
静态的成员函数本质上是加上类域分辨符的全局函数,
因而不能是虚函数。
const可以后置修饰非静态成员函数,包括虚函数。
C/C++程序设计
9
三、多态类层次之间的适应关系对于指针不鼓励实施类型转换,即不允许把此类型的指针支付给彼类型的指针,除非强制类型转换。
但对于一个继承树上下之间的对象和相关的指针情形有所变动。
C/C++程序设计
10
设继承树层次上的类从上到下为,
ClassA,ClassB,ClassC,...,ClassX,ClassY,ClassZ。
ClassA是顶层基类,ClassZ是最晚派生类。
相应层次的对象为,
obja,objb,objc,...,objx,objy,objz
相应层次的对象指针为,
pobja,pobjb,pobjc,...,pobjx,pobjy,pobjz
相应层次的对象引用为,
robja,robjb,robjc,...,robjx,robjy,robjz
C/C++程序设计
11
1,对象赋值兼容规则下面的规则说明继承树层次对象、对象指针或引用的隐含类型转换或映射的规则:
a,基类对象的指针可以指向派生类对象而不需类型转换
b,基类的对象可以等于派生类的对象而不需类型转换
c,基类的对象引用可以等于派生类的对象而不需类型转换只要存在一个可访问的无歧义的类,派生类对象的指针隐含的转换为基类的指针。派生类的对象可以隐含的赋给基类的对象。这称为向上变换或向上映射。
C/C++程序设计
12
具体的有,pobjd=&objx;
pobjf=new ClassY();
objb=objd;
这种隐含的类型转换规则表示这样一种思想:基类中的数据成员和函数成员是派生类的一个有效子集合,这个子集合是确定性的先于派生类的完整集合而存在的。
对象的等号赋值转换如 objc=objz,objb=objy称为对象的切片,就是调用基类的等号运算符函数,派生类长于基类部分的数据被舍弃,前面等长的数据拷贝给基类的对象。
反过来派生类的对象 objz= objc等于基类的对象则是没有根据的运算,这将导致派生类扩展的数据没有得到有效的赋值。
C/C++程序设计
13
基类对象的地址或引用必须通过强制类型转换映射为派生类的对象指针或引用,此称为向下映射 downcast 或向下变换。 例如:
pobjx=( ClassX*) &obja;
ClassX& robjx=(ClassX&)obja;
这种向下映射的结果是危险的。因为基类不具备派生类的成员数据和成员函数。必须小心翼翼地进行向下映射。以确保映射后上下类层次间成员的可操作性。
C/C++程序设计
14
2,静态类型和动态类型对象指针的静态类型是该指针定义点所声明的类型。
例如:
ClassD *pobjd;或者 void f (ClassD *pobjd){...}
对象指针 pobjd的静态类型就是 ClassD*,而 &objd返回一个 ClassD*型的地址。对象指针获得的对象地址的类型就是对象指针的动态类型。例如,pobjd=&objd;或 f(&objd);
pobjd具有动态类型为 ClassD*。对象指针的静态类型应等于动态类型。但是对象指针可以指向派生类对象,如,
pobjd=&objx; 或者 f (&objx);
此时 pobjd的动态类型是 ClassX*。
C/C++程序设计
15
对象指针的动静概念适用于对象引用。指针可以冻结为 0,引用必须关联到一个对象。对象引用在用等号声明的时候必须初始化。 如,
ClassF& robjf=objf;
或者 void g(ClassF& robjf ){...},.,g(objf);
robjf静态类型是 ClassF&,robjf是 objf的等价别名,robjf的动态类型也是 ClassF&。
ClassF & robjf=objx; g (objx);
//派生类的对象映射到基类的引用此时引用 robjf的静态类型是 ClassF&,动态类型是 ClassX&.
C/C++程序设计
16
四,静态联编和动态绑定对象本身是凝固的,因此动静合一的对象不具备调度派生类的能力,对象调用成员函数就是调用对象自身拥有的成员函数包括继承得来的成员函数。
多态通过对象别名实现。
对象的别名存在两种:一种是滑动的别名即对象指针,
另一种是粘附的别名即对象引用。
静态联编 (static binding)和动态绑定 (dynamic binding)
是面向对象理论关于对象别名调用成员函数的一种具体分解的路由策略。静态联编又称为早期绑定 (early binding),动态绑定也称为滞后联编 (late binding)。
C/C++程序设计
17
上层的基类是早些时间诞生的类,下层的派生类是晚些时间建立的类。一般地对象别名的静态类型是基类的类型即早期建立的类型,对象别名的动态类型是派生类的类型即晚后演化出来的类型。
对于编译器而言关键字 virtual是一个动和静的开关参量,非静态的成员函数分为两种:
静态联编的非虚函数和动态绑定的虚函数。
关系式 [pobjd=&objx;或 pobjd=new ClassX();]导致隐含类型转换后,系统将对象指针的动态类型定为 ClassX*,静态类型定为 ClassD*。
编译器扫描到 F()是 ClassD类拥有的成员函数。
C/C++程序设计
18
那么对于下面的函数调用编译器作何反映?
pobjd->F();
若 F()是非虚成员函数,则指针的静态类型确定向上搜寻的切入点,此时向上搜寻的切入点是静态类型即 ClassD
类。如果 F()在 ClassD 中出现则导致函数调用,
pobjd->ClassD::F();
但如果 F()仅在基类 ClassB,ClassC(ClassC更靠近 ClassD)
中出现,则导致函数调用,
pobjd->ClassC::F();
这一过程就是静态联编即非虚成员函数由对象指针或引用的静态类型确定调用匹配。
C/C++程序设计
19
若 F()是虚拟函数,则向上搜寻的切入点由指针的动态类型决定,此时指针的动态类型为 ClassX*,搜寻的切入点为 ClassX类。
如果 F()在 ClassX 中出现则导致函数调用,
(&objx)->ClassX::F();
但如果 F()仅在间接基类 ClassH,ClassL(ClassL更靠近
ClassX)中出现则导致函数调用:
(&objx)->ClassL::F();
这一过程就是动态绑定即虚函数根据对象指针或引用的动态类型确定调用匹配。
C/C++程序设计
20
静态联编对应直接的函数调用,动态绑定由于编译器幕后建立一个纪录虚函数族的函数指针数组,这个函数指针数组称为虚表,因此动态绑定映射迂回的函数调用。
对于对象或作为形参的数值对象系统不跟踪动态信息。
对象调用成员函数是静态类型确定的直接调用,关键字
virtual不起作用。
一般地多态类的虚函数由基类虚函数和派生类相应的覆盖函数构成,覆盖函数仅是类域分辨符不同其它要素都相同的成员函数。
这种共性是建立虚函数调用统一接口的基础。
C/C++程序设计
21
设 pBase是 vfunct虚函数基类定义的基对象指针,调用虚函数族的统一接口形式是:
基对象指针 ->虚函数 (实参列表 );
//不含类域分辨符的隐约调用形式构成统一接口
pBase ->vfunct(x1,x2,...xn);
//( x1,x2,...xn是与形参类型一一匹配的实参)
实际激活的虚函数由 pBase的动态指向所确定。虚函数动态绑定机制可以用类域分辨符明确地采用全限定函数名称方式加以限制。
例如:
pBase ->ClassA::vfunct(x1,x2,...xn)明确表示调用基类的虚函数版本,而不管 pBase的动态类型如何。
C/C++程序设计
22
类似地 [pobjz->F();]是成员函数的隐约调用,
[pobjz->ClassX::F();]是成员函数的全限定名的显式调用。
全限定名的显式调用是静态联编的直接调用。动态绑定只在虚函数的隐约调用下才有效。
如果隐约调用改为全限定名的显式调用,则虚函数动态绑定不起作用。
本章静态联编或动态绑定术语不特别说明主要作狭义理解。对术语 static binding存在广义的理解,可称为静定。
静定是指变量、对象包括函数指针等的值或类型在编译或连接阶段就可确定的性质;
C/C++程序设计
23
dynamic binding可广义地称为动定,动定的广义解释是指数据的状态在运行时才确定的特性。编译器不知道左值变量 (包括对象指针、函数指针 )千千万万的动态运行的数据状态。 例如对于成员函数调用:
pobj->F();
无论是静态联编或动态绑定,对象指针 pobj是否及时赋予初始值不是编译器所能控制。
pobj可以静态地初始化,也可以动态地根据菜单选定。
对于非多态类仅指针的静态类型起作用,系统仔细跟踪多态类对象别名的动静类型进行静态联编和动态绑定的分流。
但编译器对于对象别名的动态值不进行越界检查。
C/C++程序设计
24
程序员可以在运行时给对象指针赋一个合理值,这个对象指针既可以调用静态联编的成员函数也可以调用动态绑定的虚函数。
虚函数的动态绑定一般是运行时动态确定的。但如果基对象指针的实际值在可在编译阶段静态确定,虚函数的滞后绑定可以静定化,即编译器可以通过消除函数指针的别名机制,将虚函数的间接调用转换为直接调用。
C/C++程序设计
25