> 虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:</P>
>class A
>那么,在使用的时候,我们可以:</P>7 [; t" {# }0 ?
>A * a = new B();/ r# u2 `5 j$ S$ z: F& Z5 ^0 D
> 这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。</P>
> 虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:</P>
>class A& T4 ~. f2 `4 F* m8 |; C
> 在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:</P>
>void bar(A * a)- C7 V; Z; X! w4 I
>这种同一代码可以产生不同效果的特点,被称为“多态”。</P><A><FONT color=#163498>1.2 多态有什么用?</FONT></A>
> 多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用途,我就不再重复这个例子了,如果你不知道这个例子,随便找本书应该都有介绍。我试图从一个抽象的角度描述一下,回头再结合那个画图的例子,也许你就更容易理解。</P>
> 在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的“bad smell”之一。</P>
> 多态可以使程序员脱离这种窘境。再回头看看1.1中的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被调用函数的代码。</P><A><FONT color=#163498>1.3 如何“动态联编”</FONT></A>
> 编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把“标准的”方式简单介绍一下。</P>. h- z4 F9 L4 D% w
> 我所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,针对1.1中的例子:</P>3 H1 P1 I( L7 c
>void bar(A * a) \6 I7 \( M. x* i, p. o
>void bar(A * a)
> 虽然实际情况远非这么简单,但是基本原理大致如此。</P><A><FONT color=#163498>1.4 overload和override</FONT></A>
> 虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混淆“overload”和“override”这两个单词。但是随着各类C++的书越来越多,后来的程序员也许不会再犯我犯过的错误了。但是我打算澄清一下:</P>
> 虚函数的标志是“virtual”关键字。</P><A><FONT color=#163498>2.1 使用virtual关键字</FONT></A> + W8 C4 |, I8 N3 `$ t
> 考虑下面的类层次:</P>+ S) v6 Y: c/ j G( s+ W! y
>class A2 m# G2 H- B$ H/ o' U
> 这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。</P><A><FONT color=#163498>2.2 纯虚函数</FONT></A>
> 如下声明表示一个函数为纯虚函数:</P>& \' y) K- s/ @; @" P/ P* N# p0 d
>class A
> 一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。</P><A><FONT color=#163498>2.3 虚析构函数</FONT></A> % }1 V1 S" R. _; J
> 析构函数也可以是虚的,甚至是纯虚的。例如:</P>
>class A
> 当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:</P>
>class A8 S; A8 U" S( i0 N
>COM技术就是基于虚函数来实现的。</P>
| 欢迎光临 数学建模社区-数学中国 (http://www.madio.net/) | Powered by Discuz! X2.5 |