关于C++中虚函数表存放位置的思考
其实这是我前一段时间思考过的一个问题,是在看《深入探索C++对象模型》这本书的时候我产生的一个疑问,最近在网上又看到类似的帖子,贴出来看看:
我看到了很多有意思的答案,都回答的比较好,下面贴出一些具有代表性的:
Answer 1:
Answer 2:
我们都知道,虚函数是多态机制的基础,就是在程序在运行期根据调用的对象来判断具体调用哪个函数,现在我们来说说它的具体实现原理,主要说一下我自己的理解,如果有什么不对的地方请指正
在每个包含有虚函数的类的对象的最前面(是指这个对象对象内存布局的最前面,至于为什么是最前面,说来话长,这里就不说了,主要是考虑到效率问题)都有一个称之为虚函数指针(vptr)的东西指向虚函数表(vtbl),这个虚函数表(这里仅讨论最简单的单一继承的情况,若果是多重继承,可能存在多个虚函数表)里面存放了这个类里面所有虚函数的指针,当我们要调用里面的函数时通过查找这个虚函数表来找到对应的虚函数,这就是虚函数的实现原理。这里我假设大家都了解了,如果不了解可以去查下资料。好了,既然我们知道了虚函数的实现原理,虚函数指针vptr指向虚函数表vtbl,而且vptr又在对象的最前面,那么我们很容易可以得到虚函数表的地址,下面我写了一段代码测试了一下:
#include <iostream> #include <stdio.h> typedef void (*fun_pointer)(void); using namespace std; class Test { public: Test() { cout<<"Test()."<<endl; } virtual void print() { cout<<"Test::Virtual void print1()."<<endl; } virtual void print2() { cout<<"Test::virtual void print2()."<<endl; } }; class TestDrived:public Test { public: static int var; TestDrived() { cout<<"TestDrived()."<<endl; } virtual void print() { cout<<"TestDrived::virtual void print1()."<<endl; } virtual void print2() { cout<<"TestDrived::virtual void print2()."<<endl; } void GetVtblAddress() { cout<<"vtbl address:"<<(int*)this<<endl; } void GetFirstVtblFunctionAddress() { cout<<"First vbtl funtion address:"<<(int*)*(int*)this+0 << endl; } void GetSecondVtblFunctionAddress() { cout<<"Second vbtl funtion address:"<<(int*)*(int*)this+1 << endl; } void CallFirstVtblFunction() { fun = (fun_pointer)* ( (int*) *(int*)this+0 ); cout<<"CallFirstVbtlFunction:"<<endl; fun(); } void CallSecondVtblFunction() { fun = (fun_pointer)* ( (int*) *(int*)this+1 ); cout<<"CallSecondVbtlFunction:"<<endl; fun(); } private: fun_pointer fun; }; int TestDrived::var = 3; int main() { cout<<"sizeof(int):"<<sizeof(int)<<"sizeof(int*)"<<sizeof(int*)<<endl; fun_pointer fun = NULL; TestDrived a; a.GetVtblAddress(); cout<<"The var's address is:"<<&TestDrived::var<<endl; a.GetFirstVtblFunctionAddress(); a.GetSecondVtblFunctionAddress(); a.CallFirstVtblFunction(); a.CallSecondVtblFunction(); return 0; }
这里我们通过得到虚函数表的地址调用了里面的虚函数。
这几天又查了下资料,终于搞清楚虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata),这与微软的编译器将虚函数表存放在常量段存在一些差别。将上面的文件编译生成最终的可执行文件,然后利用命令:
objdump -s -x -d a.out | c++filt | grep "vtable" 可以得到以下输出
上面已经很清楚了这两个类Test和TestDrived都存放在.rodata内,至于上面这条命令,稍微解释以下,objdump可以读取
可执行文件中的详细信息,包括可执行文件的header, section, symbol等等,用objdump获得了可执行文件的符号很多都是
我们看不懂的,或者说与我们源代码中的函数或者变量不太一样,这是因为C++支持函数重载,C++对所有的符号都做了
修饰,很多资料称之为“函数签名”或者“符号修饰”类似的概念,但是我们要将其转换为我们源代码中的符号,这就要用到
c++filt命令了,好了,到这里告一段落了,总之关于虚函数表的具体细节就介绍到这里。
可执行文件中的详细信息,包括可执行文件的header, section, symbol等等,用objdump获得了可执行文件的符号很多都是
我们看不懂的,或者说与我们源代码中的函数或者变量不太一样,这是因为C++支持函数重载,C++对所有的符号都做了
修饰,很多资料称之为“函数签名”或者“符号修饰”类似的概念,但是我们要将其转换为我们源代码中的符号,这就要用到
c++filt命令了,好了,到这里告一段落了,总之关于虚函数表的具体细节就介绍到这里。
几个值得注意的问题
- 虚函数表是class specific的,也就是针对一个类来说的,这里有点像一个类里面的staic成员变量,即它是属于一个类所有对象的,不是属于某一个对象特有的,是一个类所有对象共有的。
- 虚函数表是编译器来选择实现的,编译器的种类不同,可能实现方式不一样,就像前面我们说的vptr在一个对象的最前面,但是也有其他实现方式,不过目前gcc 和微软的编译器都是将vptr放在对象内存布局的最前面。
- 虽然我们知道vptr指向虚函数表,那么虚函数表具体存放在内存哪个位置呢,虽然这里我们已经可以得到虚函数表的地址。实际上虚函数指针是在构造函数执行时初始化的,而虚函数表是存放在可执行文件中的。下面的一篇博客测试了微软的编译器将虚函数表存放在了目标文件或者可执行文件的常量段中,http://blog.csdn.net/vicness/article/details/3962767,不过我在gcc下的汇编文件中没有找到vtbl的具体存放位置,主要是对可执行文件的装载和运行原理还没有深刻的理解,相信不久有了这些知识之后会很轻松的找到虚函数表到底存放在目标文件的哪一个段中。
- 经过测试,在gcc编译器的实现中虚函数表vtable存放在可执行文件的只读数据段.rodata中。