曲径通幽论坛
标题: 静态联编,动态联编以及虚函数的工作原理 [打印本页]
作者: beyes 时间: 2013-10-23 08:00
标题: 静态联编,动态联编以及虚函数的工作原理
将源代码中的函数调用解释为执行特定的函数代码块称为函数名联编(binding)。在 C 语言中,这非常简单,因为每个函数名都对应一个不同的函数。但在 C++ 中,情况又会变得复杂些,如对于函数的重载,编译器必须查看函数参数以及函数名才能确定使用哪个函数。不过 C++ 编译器可以在编译过程中完成这种联编。在编译过程中进行联编的称为静态联编(static binding),又称早期联编(early binding)。
由于引进了虚函数的机制,使得情况变得更复杂。由于虚函数具有同一接口,不同方法这个特点,基类和派生类中的实现方法不一样,编译器不知道用户将选择哪种类型的对象,因此使用哪一个函数在编译时无法确定。所以,编译器必须生成能够在程序运行时可选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。
如下代码:- DerivedClass myclass;
- BaseClass *bp;
- bp = &myclass;
- bp->View_Account();
复制代码 在上面代码中,如果基类中的 View_Account() 没有声明是虚的,则 bp->View_Account(); 将根据指针的类型(这里是 BaseClass *)调用 BaseClass::View_Account()。也就是,指针类型在编译时是已知的,因此编译器在编译时,会将 View_Account() 关联到 BaseClass::View_Account() 。注意,即使在基类和派生类中同时声明了同名函数,基类指针指向的函数仍然是基类中的函数,而不是派生类函数。总之,编译器对非虚方法使用的是静态联编。
如果将上面基类中的 View_Account() 声明为虚的,那么 bp->View_Account(); 将根据对象类型来调用,在这里由 bp = &myclass; 知道,对象类型是 DerivedClass ,因此它调用的是 DerivedClass::View_Account() 。通常,只有在运行时才能确定对象的类型。所以,编译器生成的代码在执行时,根据对象类型将 View_Account() 关联到 BaseClass::View_Account() 或 DerivedClass::View_Account() 。总之,编译器对虚方法使用动态联编。
动态联编这样方便,那为什么还需要静态联编,而且还要设置静态联编为默认的方式呢?
为了使程序能够在运行阶段进行决策,必须采用一些方法来跟踪基类指针或引用指向的对象类型(这是虚函数的工作原理,下面会讨论到),这就会增加额外的处理开销。如果类不用作基类,则不需要动态联编。同样,如果派生类不重新定义基类的任何方法(虚方法),也不需要使用动态联编。在这些情况下,使用静态联编更合理,而且效率也更高。因此,C++ 将静态联编设置成了默认的选择。
如果要在派生类中重新定义基类的方法,就将它设置为虚方法,否则设置为非虚方法。
下面讨论虚函数的工作原理。在其它的帖子中,会给出其反汇编分析的版本。
C++ 规定了虚函数的行为,但如何实现,就留给了编译器的作者,程序员一般情况下只需要知道如何用就行,但如果了解其工作原理,则有助于更好地理解概念。
一般的,编译器处理虚函数的方法是:
给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针,这种数组称之为虚函数表( Virtual function table, vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含了一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该 vtbl 将保存虚函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到 vtbl 中。注意,不论类中包含的虚函数是 1 个还是 10 个,都只需要在对象中添加 1 个地址成员,只是表的大小不同而已。下面是虚函数表的原理图:[attach]2203[/attach]
上图中,2008 是基类 Scientist 的虚函数表(指针数组)地址;2096 是派生类 Physicist 虚函数表地址。不管是基类 Scientist 对象还是派生类 Physicist 的对象,它们都隐藏了一个指针成员 vptr,该成员分别指向了 Scientist 和 Physicist 对象的虚函数表,即值为 2008 和 2096 。
4064 是基类方法 Scientist::show_name() 的地址;6400 是基类方法 Scientist::show_all() 的地址;6820 是派生类 Physicist::show_all() 方法的地址;7280 是派生类 Physicist::show_field() 方法的地址。
在派生类 Physicist 虚函数表中,Physicist ::show_name() 直接继承自基类方法,并没有重新定义过,因此它的地址和基类中的方法 Scientist::show_name() 是一样的,也就是说两个方法是同一个。
在派生类 Physicist 虚函数表中,Physicist::show_all() 方法是重新定义过的,因此它有自己的地址。
在派生类 Physicist 虚函数表中, Physicist::show_field() 是派生类自己的新方法,它不来自基类的继承和重定义。
当有如下代码时:
- Physicist adam("Adam Crusher", "nuclear structure");[/align]
- Scientist * psc = &adam;
- psc->show_all();
复制代码首先,找出 psc->vptr 的值,即 2096,该值就是虚函数表地址。然后找到表中的 show_all() 这个方法的地址,这里是 6820。最后,程序跳转到 6820 这个代码块(函数)去执行。
上面就是虚函数的工作原理。
总之,使用虚函数时,在内存和执行速度方面有一定的成本,包括:
1. 每个对象都将增大,增大量为存储地址的空间。
2. 对于每个类,编译器都创建一个虚函数地址表(数组)。
3. 对于每个函数调用,都需要执行一项额外的查表操作。
非虚函数的效率比虚函数稍高,但不具备动态联编的功能。
欢迎光临 曲径通幽论坛 (http://www.groad.net/bbs/) |
Powered by Discuz! X3.2 |