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