`
weiyinchao88
  • 浏览: 1235180 次
文章分类
社区版块
存档分类
最新评论

虚函数表放在哪里?

 
阅读更多

引言:近日CSDN的"C/C++语言"版的一个问题引 起了我的注意:"请问虚函数表放在哪里?"。我也曾经思考过这个问题,零零散散也有一定的收获,这次正好趁这个机会把我对这一部分的理解整理一下。 首先值得声明的是,本文的编译环境是VS2002+WinXP。C++标准并没有对虚函数的实现作出任何的说明,甚至都没有提到虚函数的实现需要用虚表来 实现,只不过主流的C++编译器的虚函数机制都是通过虚表来实现的,所以用虚表来实现虚函数就成了"不是标准的标准"。但是这并不代表所有编译器在实现细 节上的处理都是完全一致的,它们或多或少都存在一定的个体差异。所以,本文的结论不一定适用于其他的编译情况。

虚函数/虚表的基础知识
一 个类存在虚函数,那么编译器就会为这个类生成一个虚表,在虚表里存放的是这个类所有虚函数的地址。当生成类对象的时候,编译器会自动的将类对象的前四个字 节设置为虚表的地址,而这四个字节就可以看作是一个指向虚表的指针。虚表里依次存放的是虚函数的地址,每个虚函数的地址占4个字节。

编译模块内部虚表存放的位置
如 果一个模块定义了拥有虚表的类,那么这个类的虚表存放在那里呢?要回答这个问题,我们还是需要用汇编代码入手,我首先建立了一个简单的Win32 Console Application,然后定义了一个带虚函数的类,在相应的汇编代码中,我找到了重要的破解虚表存放位置的重要线索:
CONSTSEGMENT
??_7CDerived@@6B@;CDerived::`vftable'
DDFLAT:?foobar@CDerived@@UAEXXZ
DDFLAT:
?callMe@CDerived@@UAEXXZ
;Functioncompileflags:
/Odt/RTCsu/ZI
CONSTENDS

以上的汇编代码给了我们这样的信息:
1> 虚表存放的位置应该实在模块的常量段中;
2> 这个类有两个虚函数,它们分别是?foobar@CDerived@@UAEXXZ和?callMe@CDerived@@UAEXXZ。

外部模块虚表存放的位置
当一个模块导出了一个带虚表的类,而另外一个模块又使用了这个导出类,这时候情况又是什么样的呢?这里存在两种很自然的处理方式:
1。维护一份虚表。虚表放在定义导出类的那个模块,任何使用这个导出类的其他模块都要通过这个模块来使用导出类。
2。维护多份虚表。这时候每一个使用导出类的模块都会有一份虚表的拷贝。
VS2002是使用那一种情况呢?在假设存在多份虚表的前提下,我们可以使用这样的策略来判断VS2002使用那种方式:
1。在类定义模块中创建一个类对象,并在另外一个模块中使用这个类对象。在类定义模块中创建类对象保证编译器用类定义模块中的虚表来初始化类对象。
2。在模块(非类定义模块)中创建并类对象并使用它。这样就保证编译器会用模块中的虚表来初始化类对象。
3。分别获取两种情况下两个类对象的虚表指针。如果它们的值相等,就说明只存在一份虚表;如果它们的值不等,就说明存在多份虚表。
4。如果两个虚表指针的值相等,则虚表来自于两个模块中的一个模块,判断这个虚表来自于那个模块。

应用上面的策略,我们首先建立一个Win32 DLL工程导出一个带虚表的类,再建立一个Win32 Consle Application使用这个导出类。在Win32 Consle Application的主函数中,我写了以下的代码:
CDllInDepth*pObjInAnotherDLL=createObject();
intvTableAdress=*reinterpret_cast<int*>(pObjInAnotherDLL);
intvFuncAddress=*reinterpret_cast<int*>(vTableAdress);
pObjInAnotherDLL->dumpMe();

CDllInDepth*pObjInMyApp=
newCDllInDepth;
intvTableAdress2=*reinterpret_cast<int*>(pObjInMyApp);
intvFuncAddress2=*reinterpret_cast<int*>(vTableAdress);
pObjInMyApp->dumpMe();

对这段代码做如下的解释:
1。createObject()是DLL导出了一个全局函数。这个全局函数实现的功能就是生成一个类对象并将类对象的地址传出。这样做的目的就是为了在类定义模块中生成一个类对象。
2。 获得虚表指针和虚函数的代码可以这样分析:由于虚表指针存放在类对象的前4个字节中,我们首先需要将类对象的首地址转化成int型指针,并通过这个int 型指针获得前4个字节的内容,这个内容就是虚表的地址。接着我们将这个虚表的地址再转化成int型指针,并通过这个int型指针获得虚表的前4个字节的内 容,这个内容就是虚表的第一项的值,也就是一个虚函数的地址。

通过调试,我们得出这样的结果:
vTableAdress=0x1001401CvFuncAddress=0x1001103C
vTableAdress2
=0x1001401CvFuncAddress2=0x1001103C

比 较vTableAdress和vTableAdress2的值我们发现它们的值是完全一样的,这就说明我们的假设是不正确的,这里是存在一份虚表。那最后 的一个问题就是这个虚表是来自于那个模块呢?这个答案我们需要通过比较虚表的地址以及模块所占的内存空间来解答。在调试状态下,打开"模块"窗口,我们就 可以找到模块的地址:
TestApp.exe00400000-00417000
DllInDepth.dll
10000000-10019000

其中的DllInDepth.dll模块就是定义导出类的模块,而TestApp.exe就是使用这个类的模块。通过比较不难发现,虚表的地址落在DllInDepth.dll的地址范围内,这就说明了虚表来在于类定义的模块。

到 了现在,关于虚表存放的问题基本上都得到了圆满的解决,但是我又有了一个新的问题:为什么会是这样的情况呢?我想,大概应该是这样的原因吧:类对象虚表指 针的初始化应该发生在构造函数被调用的时候,更具体的说应该实在进入到构造函数"{"之前,这个时机就是通常所说的构造函数"初始化列表"被执行的时候。 由于构造函数是在类定义模块中执行的,当然虚表也应该是类定义模块的虚表,对于其他的模块而言就是导入函数的调用,这样就没有必要维护多份虚表了。

后记

虽 然本文的主要内容是讨论虚表的位置,实际上本文涉及到DLL导出类的内容。在论坛上也经常看到一些网友对DLL导出类的内容感到迷惑。相对于简单的函数和 数据,类的构成将显得比较复杂,类声明中可以包含任意类型的数据,成员函数,虚函数,静态函数,我们就不禁迷惑这些东西是以什么样的方式导出并让其他的模 块使用的?对于这个问题,我不禁想到了一个很有名的缩写"KISS(Keep It Simple, Stupid)"。这是一个很有用的思维方式,我们就不妨尝试使用这种思维方法从简单的出发点开始思考。对于DLL来说,作为一个模块,用户感兴趣的无非是代码和数据:
  • 对于代码来说,DLL是以函数符号的形式导出的。
  • 对于数据来说,DLL是以数据符号的形式导出的。

在对C++类的结构(或者说模型)进行深入分析的基础上,我们知道,对于C++类,它既有代码,也有数据:

  • 代码是以类的成员函数,类的虚函数和类的静态函数的形式存在的;
  • 数据包含类的静态成员变量和类的虚表。

由此可见,从本质上来说,DLL导出类的情况就是导出函数和数据,并没有什么神秘的。如果我们再加上类的特殊性的分析,问题的答案就清晰了:

  1. 对于成员函数,虚函数,静态函数和静态数据,他们都处于类的作用域内,所以他们导出的函数符号中应该包含类的信息。
  2. 对于成员函数和虚函数,他们的第一个参数应该是指向类对象的指针,并且他们以"__thiscall"的调用习惯(calling convention)调用。
  3. 对于类的静态函数和静态数据,DLL按照全局函数和全局数据的处理方式一样处理他们。
  4. 虚表是以常量的形式导出的。

^_^,DLL导出类的情况尽是如此的简单,没有想到吧,不过"情况就是这样的"。

参考文献
1.提到C++对象模型,就不得不提这本书:《深度探索C++对象模型》。 对这本书的评价我就不罗嗦了,反正是只要涉及到C++对象模型的问题,很多人告诉你去看这本书就好了。相对于很多人这本书几乎痴迷的崇拜,我保留我自己一 点小小的看法。C++对象模型的细节太依赖于C++编译器,各个不同厂商的编译器之间,甚至是同一厂商不同版本编译器之间,都可能存在这样或者那样的差 别。对于不同的编译器,我们还是要"就事论事",通过自己的实践来获得某个编译器下的"第一手资料",而不能100%迷信书中的说法。
2.无意中发现一篇网友的BLOG文章,内容正好也是关于DLL中导出C++类,我发现写的比我的详细,对这个问题特别感兴趣的朋友可以看看这篇文章:
Balon白话MSDN:从普通DLL中导出C++类(2) – 细看导出C++类的底层机制

分享到:
评论

相关推荐

    C++类虚函数表

    1. **虚函数表的位置**:在具有虚函数的对象中,虚函数表的地址通常被放在对象的最前面,以方便快速访问。 2. **访问虚函数表**:可以通过对象的地址来获取虚函数表的地址,进而通过虚函数表来调用具体的虚函数。 ...

    深入剖析C++虚函数表

    《深入剖析C++虚函数表》 C++的虚函数是多态性的重要实现机制,它使得通过基类指针可以调用派生类重写的成员函数,从而实现了动态绑定。这种特性使得C++能够处理复杂的面向对象设计,提供代码的复用性和灵活性。 ...

    vf.rar_虚函数

    4. **覆盖与继承**:派生类可以重写基类的虚函数,此时编译器会将派生类版本的函数地址放入派生类的虚函数表中,而不是基类的表。这样,通过派生类对象调用同一虚函数时,就会执行派生类的实现。 5. **纯虚函数**:...

    安卓虚函数解密。。。

    当通过基类指针调用虚函数时,会先查找该指针所指向对象的虚函数表,再调用对应的函数。虽然这比直接调用非虚函数慢,但在大多数情况下,这种开销是可以接受的,因为它带来的灵活性远超其性能损失。 在实际的...

    浅谈C++对象的内存分布和虚函数表

    在对象创建时,会为其分配一个指向虚函数表的指针,这个指针通常放在对象内存的起始位置。这样,通过这个指针,非静态成员函数就可以找到相应的函数实现,实现了运行时多态性。 单继承的情况相对简单,每个继承自...

    金山WPS面试题目

    - 编译器自动生成虚函数表,并将其指针放在对象实例的起始位置。 - 通过虚函数表可以实现通过基类指针调用派生类中的函数。 - 在不同的编译环境下,虚函数表的结束标志可能有所不同。 以上就是对金山WPS面试题目中...

    详解C++虚函数的工作原理

    此外,虚函数表的设计允许编译器进行优化,例如,对于没有被覆盖的基类虚函数,可以直接将它们的地址放入子类的虚函数表中,从而避免了重复的函数地址。这种优化提高了程序的效率。 总的来说,C++的虚函数和虚函数...

    4.2C++之虚函数共5页.pdf.zip

    C++编译器为每个含有虚函数的类创建一个虚函数表(vtable),存储虚函数的地址。这样,通过基类指针调用虚函数时,可以通过vtable找到实际的函数地址。 7. **C++11及以后的特性**: C++11引入了`override`关键字...

    7.多继承不同继承顺序类对象布局不同1

    这里,无论继承顺序如何,`BaseB`的虚函数表都会放在较高地址,而`BaseA`的虚函数表放在较低地址。这意味着,即使`BaseA`在前,`BaseB`在后继承,`BaseB`的虚函数表指针也会先于`BaseA`的出现。这是因为这些编译器...

    C++、C_、C面试真题超经典

    - **虚函数机制**:当一个类包含虚函数时,C++编译器会为该类生成一个虚函数表,并在每个对象实例中存储一个指向虚函数表的指针。这是为了支持动态绑定,即允许通过基类指针调用派生类中的方法。虚函数表的存在增加...

    浅析C++类的底层实现

    而虚函数的底层实现则涉及到运行时类型信息(RTTI)和虚函数表,使得子类能够重写父类的函数并正确调用。 总的来说,理解C++类的底层实现有助于我们写出更高效、更健壮的代码。通过封装性,我们可以更好地控制数据...

    C++ 内存对象布局

    对于仅包含虚函数的类,编译器会在对象内存中添加一个虚函数指针(vptr),用于指向虚函数表。虚函数表则包含了类中的所有虚函数地址。即使类中没有其他成员变量,也会为vptr分配空间,因此这类对象的大小通常至少为...

    C++大公司笔试题2

    在这个阶段,编译器根据类的定义创建虚函数表,将每个虚函数的地址放入一个数组中。 - **运行时期:** 对象的虚函数表指针是在对象被创建时初始化的,即在运行时期。这意味着在对象构造函数被调用时,该对象的虚函数...

    程序员面试C++笔试题(附部分答案)

    此时编译器根据类定义生成虚函数表,并将每个虚函数的地址放入表中相应的槽(slot)中。 - **Run-Time(运行期)**: 对象的虚函数表指针(vptr)是在运行时初始化的。当对象被创建时,它的构造函数会被调用,此时vptr被...

    C++程序设计,主要为C++语言简介、面向对象的基本概念、类和对象进阶、运算符重载、类的继承与派生、多态与虚函数等章节总结

    编译时多态通过函数重载实现,运行时多态通过虚函数和虚函数表实现。 10. **输入/输出流** 和 **文件操作**:C++的`ifstream`和`ofstream`类用于文件的读写,`getline`函数可用于读取一行文本,`eofbit`标志表示...

    Explanation about “pure virtual function call” on Win32 platform.pdf

    在编译阶段,编译器会为每个类创建一个虚函数表(vtable),其中包括指向各个虚函数的指针。当尝试调用纯虚函数时,编译器会在 vtable 中查找对应的函数地址,但由于纯虚函数没有实现,因此该地址是空的。此时,程序...

    用C语言模拟了 c++ 封装、继承、多态三大特性。

    这样,通过基类指针调用函数时,实际上会根据指针指向的虚函数表来决定调用哪个实际的函数。 ```c typedef void (*VirtualFunc)(void*); typedef struct { VirtualFunc virtualMethod; } Base; typedef struct {...

    MFC学习笔记

    - **虚函数表**:在C++中,编译器会为包含虚函数的类生成一个虚函数表(Vtable),这个表包含了指向所有虚函数的指针。当通过基类指针调用虚函数时,实际调用的是指向派生类对象的虚函数。 示例代码: ```cpp class...

    C++类继承内存布局文档打包

    - 如果基类中包含虚函数,编译器会在基类中插入一个虚函数表指针(vptr)。子类也会有一个vptr,指向其自己的虚函数表,这样就可以实现多态性。子类的vptr通常放在对象内存的第一个位置,以便快速访问。 4. 构造...

Global site tag (gtag.js) - Google Analytics