C++ 多态分析

貌似公司面试都喜欢问多态,今天做个总结记录。

1.什么是多态

多态就是Polymorphism,一个接口的多种实现。在不同的上下问下,接口的实现表现出不同的特征。

2.多态的好处

多态带来两个明显的好处:一是不用记大量的函数名了,二是它会依据调用时的上下文来确定实现。确定实现的过程由C++本身完成另外还有一个不明显但却很重要的好处是:带来了面向对象的编程。
3.多态的实现

函数重载,宏多态,模板函数,虚函数。

3.1函数重载(function overloading)

不同的参数列表,不同的返回类型,实现同名调用不同实现的静态多态。

3.2宏多态

带变量的宏可以实现简单的多态。比如下面的ADD宏,传入数字表示相加,传入字符串表示连接。
#define ADD(A, B) (A) + (B);

3.3模板函数

模板函数可以处理不同的数据类型,通过传入不同类型来实现不同效果。

3.4虚函数实现

这就是通常所说的动态多态。实现机制在于继承和虚函数。常用方法是创建一个父类的指针来指向不同的子类对象,调用虚函数时,会先找到子类虚表,通过子类虚表调用对应函数,实现运行时多态。

4.静态多态和动态多态

静态多态又叫编译时多态,动态多态又叫运行时多态。编译时的多态性为我们提供了运行速度快的特点,而运行时的多态性则带来了高度灵活和抽象的特点。

David Vandevoorde和Nicolai M. Josuttis在他们的著作C++ Templates: The Complete Guide一书中系统地阐述了静态多态和动态多态技术。因为认为“和其他语言机制关系不大”,这本书没有提及“宏多态”(以及“函数多态”)。(需要说明的是,笔者本人是这本书的繁体中文版译者之一,本文正是基于这本书的第14章The Polymorphic Power of Templates编写而成)

动态多态只需要一个多态函数,生成的可执行代码尺寸较小,静态多态必须针对不同的类型产生不同的模板实体,尺寸会大一些,但生成的代码会更快,因为无需通过指针进行间接操作。静态多态比动态多态更加类型安全,因为全部绑定都被检查于编译期。正如前面例子所示,你不可将一个错误的类型的对象插入到从一个模板实例化而来的容器之中。此外,正如你已经看到的那样,动态多态可以优雅地处理异质对象集合,而静态多态可以用来实现安全、高效的同质对象集合操作。

静态多态为C++带来了泛型编程(generic programming)的概念。泛型编程可以认为是“组件功能基于框架整体而设计”的模板编程。STL就是泛型编程的一个典范。STL是一个框架,它提供了大量的算法、容器和迭代器,全部以模板技术实现。从理论上讲,STL的功能当然可以使用动态多态来实现,不过这样一来其性能必将大打折扣。 

静态多态还为C++社群带来了泛型模式(generic patterns)的概念。理论上,每一个需要通过虚函数和类继承而支持的设计模式都可以利用基于模板的静态多态技术(甚至可以结合使用动态多态和静态多态两种技术)而实现。正如你看到的那样,Andrei Alexandrescu的天才作品Modern C++ Design: Generic Programming and Design Patterns Applied(Addison-Wesley)和Loki程序库已经走在了我们的前面。

5.虚函数和虚表

5.1虚函数

对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。
如果基类有虚函数,那么:
     1、每一个派生类都有虚表。
     2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
     3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。

5.2纯虚函数

纯虚函数不实现函数,让子类继承实现(必须实现)。定义方式如下:
virtual void fun()=0;

包含纯虚函数的类是抽象类,不能生成对象。

5.3虚表

每个含有虚函数的类有一张虚函数表(vtbl),表中每一项指向一个虚函数的地址,实现上是一个函数指针的数组。
虚函数表既有继承性又有多态性。每个派生类的vtbl继承了它各个基类的vtbl,如果基类vtbl中包含某一项,则其派生类的vtbl中也将包含同样的一项,但是两项的值可能不同。如果派生类重载(override)了该项对应的虚函数,则派生类vtbl的该项指向重载后的虚函数,没有重载的话,则沿用基类的值。
在类对象的内存布局中,首先是该类的vtbl指针,然后才是对象数据。在通过对象指针调用一个虚函数时,编译器生成的代码将先获取对象类的vtbl指针,然后调用vtbl中对应的项。对于通过对象指针调用的情况,在编译期间无法确定指针指向的是基类对象还是派生类对象,或者是哪个派生类的对象。但是在运行期间执行到调用语句时,这一点已经确定,编译后的调用代码能够根据具体对象获取正确的vtbl,调用正确的虚函数,从而实现多态性。分析一下这里的思想所在,问题的实质是这样,对于发出虚函数调用的这个对象指针,在编译期间缺乏更多的信息,而在运行期间具备足够的信息,但那时已不再进行绑定了,怎么在二者之间作一个过渡呢?把绑定所需的信息用一种通用的数据结构记录下来,该数据结构可以同对象指针相联系,在编译时只需要使用这个数据结构进行抽象的绑定,而在运行期间将会得到真正的绑定。这个数据结构就是vtbl。可以看到,实现用户所需的抽象和多态需要进行后绑定,而编译器又是通过抽象和多态而实现后绑定的。

下面说一下多重继承。多重继承的两个基类如果继承了同一个类,则其派生类相当于继承了该类两次,vtbl也继承了两次。对象布局中,该类的数据有两份,vtbl指针有两个,分别指向两次被继承的vtbl。但派生类重载该类的虚函数时只能重载一次,那么重载后的函数地址将占据vtbl的哪个位置?通过写程序测试,我觉得应该是同时出现在所继承的两个vtbl的相应位置,有待进一步验证。

说到虚函数机制,对象指针的类型转换也是要弄清的,这里就不说了。还有一个this指针的问题,提一下。虚函数调用的时候也是需要传递this指针的,这没什么奇怪,但是这时的this指针就隐含着一个问题,它要和实际调用的虚函数相一致,即this指针也要实现多态性。



上一篇:c++ builder 简单读、分析网页数据


下一篇:View and Data API Tips : Conversion between DbId and node