深入研究虚函数和vtable
- 格式:doc
- 大小:329.00 KB
- 文档页数:4
虚函数的实现机制
虚函数(virtual function)是 C++ 中的一种重要机制,它允许子类重新定义从父类继承的方法,这样在调用子类的方法时,会根据实际的对象类型调用相应的方法。
这种机制被称为动态绑定或运行时多态。
虚函数的实现机制主要依赖于虚表(vtable)和虚指针(vptr)。
1.虚表(vtable):每个有虚函数的类(或者从有虚函数的类派生出来的类)都有一个虚表。
虚表是一个包含指向虚函数的指针的数组。
在这个数组中,每个从父类派生出来的子类都有一个条目,每个条目都包含一个指向该类实现的相应虚函数的指针。
2.虚指针(vptr):每个包含虚函数的类的对象都有一个虚指针。
这个指针指向该对象的类的虚表。
当调用一个虚函数时,程序首先会通过这个虚指针找到对应的虚表,然后再根据虚表的索引找到正确的函数进行调用。
当定义一个含有虚函数的类时,编译器会为这个类创建一个虚表,并在该类的每个对象中嵌入一个指向该虚表的虚指针。
当创建一个类的对象时,这些对象会根据其类型使用正确的虚表。
在运行时,当调用一个虚函数时,程序会根据对象的虚指针找到正确的虚表,然后根据函数在虚表中的索引来调用正确的函数。
这种实现机制允许我们在运行时动态地决定要调用哪个函数,从而实现了多态性。
虚函数的实现原理虚函数是C++中的一种特殊函数,它允许派生类重写基类的同名函数,并根据对象的实际类型调用相应的函数。
虚函数的实现原理涉及到虚函数表(vtable)和虚函数指针(vpointer)两个重要的概念。
在C++中,每个包含虚函数的类都会生成一个与之对应的虚函数表(vtable)。
虚函数表是一个以函数指针为元素的数组,用于存储类的虚函数地址。
虚函数表中的每个元素都对应着类的一个虚函数,其中存储着该虚函数的地址。
虚函数表通常位于类的内存布局最前面的位置。
当一个类被定义为包含虚函数时,编译器会自动生成一个隐藏的虚函数指针(vpointer)并将它添加到类的内存布局中。
这个虚函数指针被添加到每一个类的对象中,并指向该对象对应的虚函数表。
通过虚函数指针,程序能够在运行时根据对象的实际类型找到正确的虚函数表,并调用相应的虚函数。
当派生类重写基类的虚函数时,它会生成一个新的函数地址并将其存储到自己对应的虚函数表中。
在派生类的虚函数表中,只有被重写的虚函数所对应的表项会被替换为新的函数地址,其它虚函数的表项仍然指向基类的虚函数地址。
这样,当通过派生类的对象调用虚函数时,程序会根据对象的实际类型找到对应的虚函数表,并调用正确的虚函数。
虚函数的实现原理可以通过以下示例代码进行说明:cpp#include <iostream>class Base {public:virtual void print() {std::cout << "Base::print()" << std::endl;}};class Derived : public Base {public:void print() override {std::cout << "Derived::print()" << std::endl;}};int main() {Base* base = new Derived();base->print(); 输出"Derived::print()"delete base;return 0;}在上述代码中,Base类包含一个虚函数print(),而Derived类重写了这个虚函数。
虚函数工作原理虚函数工作原理:在面向对象编程中,虚函数允许在基类中定义一个成员函数,然后在派生类中重写该函数。
通过使用虚函数,可以实现多态性,即在编译时无法确定要调用哪个函数,只有在运行时才能确定。
以下是虚函数的工作原理:1. 基类中声明虚函数:在基类中声明一个函数为虚函数,可以使用关键字"virtual"。
虚函数声明通常放在基类的公共部分。
2. 派生类中重写虚函数:派生类可以重写基类中的虚函数。
派生类中的函数签名必须与基类中的虚函数相匹配,包括函数名、参数类型和返回类型。
在派生类中,可以使用"override"关键字来显式表示对基类虚函数的重写。
3. 通过指针或引用调用虚函数:虚函数通常使用基类的指针或引用进行调用。
编译器会根据对象的实际类型来决定调用哪个函数。
如果对象是派生类的实例,将调用派生类中的虚函数。
如果对象是基类的实例,将调用基类中的虚函数。
4. 运行时确定函数调用:当使用基类指针或引用调用虚函数时,编译器不确定要调用哪个函数,但它会生成相应的虚函数表(vtable)来存储函数地址。
派生类则会覆盖基类虚函数表中的相应条目。
在运行时,根据对象的实际类型,使用该对象的虚函数表来确定要调用的函数。
5. 动态绑定:通过虚函数,可以实现动态绑定,即在编译时选择调用的函数,在运行时决定具体调用哪个函数。
这允许同一函数名在不同的对象上具有不同的行为。
总之,虚函数通过使用虚函数表来实现多态性,使得在编译时无法确定要调用的函数,只有在运行时才能确定。
虚函数的重写通过覆盖虚函数表中的条目来实现。
C++中虚函数工作原理和(虚)继承类的内存占用大小计算一、虚函数的工作原理虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。
典型情况下,这一信息具有一种被称为vptr(virtual table pointer,虚函数表指针)的指针的形式。
vptr 指向一个被称为vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到vtbl。
当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的vptr 指向的vtbl,然后在vtbl 中寻找合适的函数指针。
虚拟函数的地址翻译取决于对象的内存地址,而不取决于数据类型(编译器对函数调用的合法性检查取决于数据类型)。
如果类定义了虚函数,该类及其派生类就要生成一张虚拟函数表,即vtable。
而在类的对象地址空间中存储一个该虚表的入口,占4个字节,这个入口地址是在构造对象时由编译器写入的。
所以,由于对象的内存空间包含了虚表入口,编译器能够由这个入口找到恰当的虚函数,这个函数的地址不再由数据类型决定了。
故对于一个父类的对象指针,调用虚拟函数,如果给他赋父类对象的指针,那么他就调用父类中的函数,如果给他赋子类对象的指针,他就调用子类中的函数(取决于对象的内存地址)。
虚函数需要注意的大概就是这些个地方了,之前在More effective C++上好像也有见过,不过这次在Visual C++权威剖析这本书中有了更直白的认识,这本书名字很牛逼,看看内容也就那么回事,感觉名不副实,不过说起来也是有其独到之处的,否则也没必要出这种书了。
每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。
虚函数是C++中的一个非常重要的概念,它允许我们在派生类中重新定义基类中的函数,从而实现多态性。
在本文中,我们将深入探讨virtual关键字的作用,以及virtual函数和纯虚函数的使用方法。
在C++中,virtual关键字用于声明一个虚函数。
这意味着当派生类对象调用该函数时,将会调用其在派生类中的定义,而不是基类中的定义。
这种行为使得我们能够在派生类中定制化地实现函数的逻辑,从而实现不同对象的不同行为。
对于virtual函数,我们需要注意以下几点:1. 在基类中声明函数时,使用virtual关键字进行声明。
2. 派生类中可以选择性地使用virtual关键字进行重声明,但通常最好也使用virtual,以便明确表明这是一个虚函数。
3. 当使用派生类对象调用虚函数时,将会根据对象的实际类型调用适当的函数实现。
4. 虚函数的实现通过虚函数表(vtable)来实现,这是一张函数指针表,用于存储各个虚函数的位置区域。
除了普通的虚函数外,C++还提供了纯虚函数的概念。
纯虚函数是在基类中声明的虚函数,它没有函数体,只有声明。
这意味着基类不能直接实例化,只能用作其他类的基类。
纯虚函数通常用于定义一个接口,而具体的实现则留给派生类。
接下来,让我们以一个简单的例子来说明虚函数和纯虚函数的用法。
假设我们有一个基类Shape,它包含一个纯虚函数calcArea用于计算面积。
有两个派生类Circle和Rectangle,它们分别实现了calcArea 函数来计算圆形和矩形的面积。
在这个例子中,我们可以看到基类Shape定义了一个纯虚函数calcArea,它没有函数体。
而派生类Circle和Rectangle分别实现了这个函数来计算不同形状的面积。
当我们使用Shape指针指向Circle或Rectangle对象时,调用calcArea函数将会根据对象的实际类型来调用适当的实现。
除了虚函数和纯虚函数外,C++中还有虚析构函数的概念。
多态性是C++最主要的特征,多态性的实现得益于C++中的动态联编技术。
文章通过对动态联编的关键技术虚拟函数表进行深入的剖析,解析的动态联编的过程极其技术要领。
关键字多态性动态联编 VTABLE 虚函数文章正文一从多态性谈动态联编的必要性在进入主题之前先介绍一下联编的概念。
联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址。
按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。
在编译阶段就将函数实现和函数调用关联起来称之为静态联编,静态联编在编译阶段就必须了解所有的函数或模块执行所需要检测的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型。
反之在程序执行的时候才进行这种关联称之为动态联编,动态联编对成员函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。
C语言中,所有的联编都是静态联编。
C++中一般情况下联编也是静态联编,但是一旦涉及到多态性和虚函数就必须使用动态联编。
多态性是面向对象的核心,它的最主要的思想就是可以采用多种形式的能力,通过一个用户名字或者用户接口完成不同的实现。
通常多态性被简单的描述为"一个接口,多个实现。
在C++里面具体的表现为通过基类指针访问派生类的函数和方法。
下面我们看一个静态联编的例子,这种静态联编导致了我们不希望的结果。
//1.cpp1. #include <iostream.h>2. class shape{3. public:4. void draw(){cout<<"I am shape"<<endl;}5. void fun(){draw();}6. };7. class circle:public shape{8. public:9. void draw(){cout<<"I am circle"<<endl;}10. };11. main(){12. class circle oneshape;13. oneshape.fun();14. }程序的输出结果我们希望是"I am circle",但事实上却输出了"I am shape"的结果,造成这个结果的原因是静态联编。
虚函数表详解虚函数表对C++ 了解的⼈都应该知道虚函数(Virtual Function)是通过⼀张虚函数表(Virtual Table)来实现的。
简称为V-Table。
在这个表中,主是要⼀个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。
这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们⽤⽗类的指针来操作⼀个⼦类的时候,这张虚函数表就显得由为重要了,它就像⼀个地图⼀样,指明了实际所应该调⽤的函数。
这⾥我们着重看⼀下这张虚函数表。
C++的编译器应该是保证虚函数表的指针存在于对象实例中最前⾯的位置(这是为了保证取到虚函数表的有最⾼的性能——如果有多层继承或是多重继承的情况下)。
这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调⽤相应的函数。
听我扯了那么多,我可以感觉出来你现在可能⽐以前更加晕头转向了。
没关系,下⾯就是实际的例⼦,相信聪明的你⼀看就明⽩了。
假设我们有这样的⼀个类:class Base {public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }};按照上⾯的说法,我们可以通过Base的实例来得到虚函数表。
下⾯是实际例程:typedef void(*Fun)(void);Base b;Fun pFun = NULL;cout << "虚函数表地址:" << (int*)(&b) << endl;cout << "虚函数表 — 第⼀个函数地址:" << (int*)*(int*)(&b) << endl;// Invoke the first virtual functionpFun = (Fun)*((int*)*(int*)(&b));pFun();实际运⾏经果如下:(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3)虚函数表地址:0012FED4虚函数表 — 第⼀个函数地址:0044F148Base::f通过这个⽰例,我们可以看到,我们可以通过强⾏把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第⼀个虚函数的地址了,也就是Base::f(),这在上⾯的程序中得到了验证(把int* 强制转成了函数指针)。
一、构造函数不能为虚函数的理由:1,从存储空间角度虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。
问题出来了,如果构造函数是虚的,就需要通过vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
2,从使用角度虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。
构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。
所以构造函数没有必要是虚函数。
虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。
而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
3、构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它但析构却不一定,我们往往通过基类的指针来销毁对象。
这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
4、从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数5、当一个构造函数被调用时,它做的首要的事情之一是初始化它的V P T R。
因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。
当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码- -既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。
所以它使用的V P T R必须是对于这个类的V TA B L E。
而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,V P T R将保持被初始化为指向这个V TA B L E, 但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置V P T R指向它的V TA B L E,等.直到最后的构造函数结束。
虚函数类的大小-概述说明以及解释1.引言1.1 概述概述部分将引言文章的主题和范围进行简单介绍,并提供读者对接下来内容的基本了解。
本文的主题是虚函数类的大小,通过研究虚函数的概念和作用,以及虚函数表的实现和影响,探讨了虚函数类的大小的影响因素。
本文的目的是帮助读者理解虚函数类的大小的计算方式和影响因素,以便更好地优化程序设计和内存使用。
虚函数是面向对象编程中的一个重要概念,它允许派生类重写基类的函数,并且可以在运行时根据对象的实际类型调用相应的函数。
虚函数的引入提高了程序的灵活性和可维护性。
然而,虚函数的实现和使用会带来一定的性能开销和存储空间的消耗。
本文将详细介绍虚函数的概念和作用,以及它们对虚函数类大小的影响。
虚函数表是一种用于实现多态的机制,它是一个存储着虚函数地址的表格。
通过虚函数表,程序可以根据对象的类型来动态地调用对应的虚函数,无需在编译时确定。
虚函数表的存在为虚函数带来了额外的开销,同时也会增加虚函数类的大小。
本文将介绍虚函数表的实现原理以及它们对虚函数类大小的影响,以帮助读者理解虚函数类大小的计算方式。
在结论部分,本文将总结虚函数类大小的影响因素,并给出一些建议和建议,以便读者在实际的程序设计中更好地优化虚函数类大小和内存使用。
通过对虚函数类大小的深入研究,读者将能够更好地理解虚函数的实现机制,提高程序的性能和效率。
1.2文章结构1.2 文章结构:本文将从以下几个方面进行讨论虚函数类的大小问题:第一部分,引言部分将对整篇文章的内容做一个概述,明确文章的目的和意义。
我们将介绍什么是虚函数以及它的作用,以及虚函数表的实现和对类大小的影响。
第二部分,正文部分将详细介绍虚函数的概念和作用。
我们将阐述虚函数的定义、特点以及它在面向对象编程中的重要作用。
通过具体的示例和解释,读者将更好地理解虚函数的概念和作用。
接下来,在同一节的2.2小节中,我们将着重探讨虚函数表的实现和对类大小的影响。
虚函数表是用于实现动态多态性的一种机制,我们将详细介绍它的结构和用法,并讨论它对类的大小的影响。
C语言虚函数中的特定函数简介C语言是一种面向过程的编程语言,并不直接支持面向对象的概念,其中包括了“类”、“对象”、“继承”等概念。
然而,通过使用一些技巧和设计模式,我们可以在C语言中实现类似于面向对象的功能,其中一个重要的概念就是虚函数。
虚函数是一种特殊的函数,它可以在派生类中被重写,从而实现多态。
虚函数的定义、用途和工作方式是C语言中面向对象编程的重要部分,本文将详细介绍这些内容。
虚函数的定义在C语言中,虚函数的定义需要使用函数指针和结构体实现。
我们可以使用函数指针将一个函数地址赋值给一个结构体中的成员变量,从而形成一个具有特定功能的“方法”。
这样,我们就可以通过这个函数指针来调用结构体中的函数,实现类似于面向对象中对象的方法调用的功能。
下面是一个虚函数的定义示例:typedef struct {void (*function_ptr)(void);} VTable;void function1(void) {printf("This is function1\n");}void function2(void) {printf("This is function2\n");}VTable vtable = {.function_ptr = function1};在上述示例中,我们使用typedef定义了一个VTable结构体,其中有一个function_ptr成员变量,它是一个指向函数的指针。
我们定义了两个函数function1和function2,并分别赋值给了vtable中的function_ptr成员变量。
虚函数的用途虚函数的主要用途是实现多态,使不同类型的对象可以调用相同的接口名称,但执行不同的操作。
通过使用虚函数,我们可以在C语言中实现类似于面向对象的继承和多态的功能。
在面向对象的编程中,我们可以定义一个基类(或接口),然后派生出不同的子类,每个子类都可以重写基类的虚函数,以实现它们自己的特定行为。
深入研究虚函数和vtable国防科技大学计算机学院褚瑞在面向对象的C++语言中,虚函数(virtual function)是一个非常重要的概念。
因为它充分体现了面向对象思想中的继承和多态性这两大特性,在C++语言里应用极广。
比如在微软的MFC类库中,你会发现很多函数都有virtual关键字,也就是说,它们都是虚函数。
难怪有人甚至称虚函数是C++语言的精髓。
那么,什么是虚函数呢,我们先来看看微软的解释:虚函数是指一个类中你希望重载的成员函数,当你用一个基类指针或引用指向一个继承类对象的时候,你调用一个虚函数,实际调用的是继承类的版本。
——摘自MSDN 这个定义说得不是很明白。
MSDN中还给出了一个例子,但是它的例子也并不能很好的说明问题。
我们自己编写这样一个例子:#include "stdio.h"#include "conio.h"class Parent{public:char data[20];void Function1();virtual void Function2(); // 这里声明Function2是虚函数}parent;void Parent::Function1(){printf("This is parent,function1\n");}void Parent::Function2(){printf("This is parent,function2\n");}class Child:public Parent{void Function1();void Function2();} child;void Child::Function1(){printf("This is child,function1\n");}void Child::Function2(){printf("This is child,function2\n");}int main(int argc, char* argv[]){Parent *p; // 定义一个基类指针if(_getch()=='c') // 如果输入一个小写字母cp=&child; // 指向继承类对象elsep=&parent; // 否则指向基类对象p->Function1(); // 这里在编译时会直接给出Parent::Function1()的入口地址。
p->Function2(); // 注意这里,执行的是哪一个Function2?return 0;}用任意版本的Visual C++或Borland C++编译并运行,输入一个小写字母c,得到下面的结果:This is parent,function1This is child,function2为什么会有第一行的结果呢?因为我们是用一个Parent类的指针调用函数Fuction1(),虽然实际上这个指针指向的是Child类的对象,但编译器无法知道这一事实(直到运行的时候,程序才可以根据用户的输入判断出指针指向的对象),它只能按照调用Parent类的函数来理解并编译,所以我们看到了第一行的结果。
那么第二行的结果又是怎么回事呢?我们注意到,Function2()函数在基类中被virtual关键字修饰,也就是说,它是一个虚函数。
虚函数最关键的特点是“动态联编”,它可以在运行时判断指针指向的对象,并自动调用相应的函数。
如果我们在运行上面的程序时任意输入一个非c的字符,结果如下:This is parent,function1This is parent,function2请注意看第二行,它的结果出现了变化。
程序中仅仅调用了一个Function2()函数,却可以根据用户的输入自动决定到底调用基类中的Function2还是继承类中的Function2,这就是虚函数的作用。
我们知道,在MFC中,很多类都是需要你继承的,它们的成员函数很多都要重载,比如编写MFC应用程序最常用的CView::OnDraw(CDC*)函数,就必须重载使用。
把它定义为虚函数(实际上,在MFC中OnDraw不仅是虚函数,还是纯虚函数),可以保证时刻调用的是用户自己编写的OnDraw。
虚函数的重要用途在这里可见一斑。
在了解虚函数的基础之上,我们考虑这样的问题:一个基类指针必须知道它所指向的对象是基类还是继承类的示例,才能在调用虚函数时“自动”决定应该调用哪个版本,它是如何知道的?有些讲C++的书上提到,这种“动态联编”的机制是通过一个“vtable”实现的,vtable是什么?微软在关于COM的文档里这样描述:vtable是指一张函数指针表,如同C++中类的实现一样,vtable中的指针指向一个对象支持的接口成员函数。
——摘自MSDN 很遗憾,微软这次还是没有把问题说清楚,当然,上面的文档本来就是关于COM的,与我们关心的问题不同。
那么vtable是什么?我们先来看看下面的实验:在前面的示例程序中加一句printf(“%d”,size of(Child));运行,然后去掉Function2()前的virtual关键字,再运行,得到这样的结果:当Function2定义成虚函数的时候,结果是24,否则结果是20。
也就是说,如果Function2不是虚函数,一个Child类的示例所占空间的大小仅仅是它的成员变量data数组的大小,如果Function2是虚函数,结果多了4个字节。
我们使用的是32位的Visual C++6.0,4个字节恰好是一个指针,或者是一个整数所占的空间。
那么这多出来的四个字节究竟起到了什么作用?用Visual C++打开前面的示例程序,在main函数中p->Function1(); 一句前面按F9设断点,按F5开始调试,输入一个小写c,程序停到了我们设的断点上。
找到Debug工具条,按Disassembly按钮,如图所示:我们看到了反汇编后的代码。
由上图可见,对Function1和Function2的调用反汇编后生成的代码截然不同。
Function1不是虚函数,因此对它的调用仅仅被编译成为一条call指令,转向Parent::Function1子程序;而Function2是虚函数,它的代码要复杂一些,我们来仔细分析:45: p->Function2();004012CA mov eax,dword ptr [ebp-4]// eax就是我们的p指针004012CD mov edx,dword ptr [eax]// edx取child对象头部四个字节004012CF mov esi,esp004012D1 mov ecx,dword ptr [ebp-4]// 可能要检查栈,不管它004012D4 call dword ptr [edx]// 注意这里,调用了child对象头部的一个函数指针004012D6 cmp esi,esp004012D8 call __chkesp (004013b0)这里最关键的一句是call dword ptr[edx],edx是child对象头部,前面我们分析过了,child对象共有24字节,其中成员变量占用20字节,还有4个字节作用未知。
现在从这段汇编代码上看,那4个字节很可能就是child对象开头的这个函数指针,因为编译器并不知道我们的成员变量data是做什么用的,更不可能把data的任何一部分当成一个函数指针来处理。
那么这个函数指针会跳转到那里去呢?我们按F10单步运行到这个call指令,然后按F11跟进去:00401032 jmp Parent::Function2 (0040bfe0)00401037 jmp Parent::Parent (004010d0)→0040103C jmp Child::Function2 (00401250)00401041 jmp Child::Child (004011c0)光标停在了第三行,40103C的地方,执行这里的jmp指令后,又跳转到Child::Function2的位置,从而得到我们上面所看到的结果。
这并不是最终的结论,我们看看40103C周围的几行代码,连续几行全都是jmp指令,这是什么程序结构?有汇编语言编程经验的朋友可能会想起来了,这是一张入口表,分别存放着到几个重要函数的跳转指令!我们再回去看看微软对于vtable的描述:vtable是指一张函数指针表,(如同C++中类的实现一样,)vtable中的指针指向(一个对象支持的接口)成员函数。
打括号的字不要看,这句话的主干就是:vtable是一张函数指针表,指向成员函数。
种种事实证明,上面的四行代码就是我们要找的这个vtable!现在我们应该对虚函数的原理有一个认识了。
每个虚函数都在vtable中占了一个表项,保存着一条跳转到它的入口地址的指令(实际上就是保存了它的入口地址)。
当一个包含虚函数的对象(注意,不是对象的指针)被创建的时候,它在头部附加一个指针,指向vtable中相应位置。
调用虚函数的时候,不管你是用什么指针调用的,它先根据vtable找到入口地址再执行,从而实现了“动态联编”。
而不像普通函数那样简单地跳转到一个固定地址。
以上结论仅仅是针对Visual C++ 6.0编译器而言的,对于其他编译器,具体实现并不完全相同,但都大同小异。
著名的“绿色兵团”杂志上撰文介绍,Linux平台上的GNU C++编译器就把指向vtable的指针放在对象尾部而不是头部,而且vtable中仅仅存放虚函数的入口地址,而不是跳转到虚函数的指令。
具体的一些细节,篇幅所限,我们这里不再讨论,希望有兴趣的朋友能继续研究。