虚函数实现细节
来源:互联网 发布:mysql数据库迁移方案 编辑:程序博客网 时间:2024/05/18 03:52
1.虚函数
·虚表是怎么实现的?虚表存放在哪里?
·虚表中的数据是在什么时候确定的?
·对象中的虚表指针又在什么时候赋值的?
我们很难通过 C++语言本身来找到答案。 C++标准给编译器实现者定义了语法规范,但是被并没有定义如何实现这些语法规范,不同的编译器实现者可能有不同的实现方法,可以肯定的是他们的编译器必须符合这些语法规范。汇编语言作为最接近机器语言的计算机语言,可以为我们揭示一些隐藏在编译器内部的细节。接下来本来就试图通过对 C++源码进行反汇编的方式来解答这些疑惑。
二、分析
这里我选用 WinXP 和 VS2008 作为我们这次分析的平台。我们建立一个最简单的 Win32 控制台程序,并定义两个简单的类:
接下来我们可以直接编译这些 C++源码就可以得到相应的汇编代码。 通过分析这些汇编代码我们就找到许多有用的信息。我们可以找到这样的汇编代码:
以上的汇编代码定义了两个数据段, 而这两个数据段中的内容恰好就是类的虚表。 至此虚表的"庐山真面目"完全展示在我们的面前。 根据这些信息,我们可以推理出很多有用的结论:
·拥有虚函数的类会有一个虚表,而且这个虚表存放在类定义模块的数据段中。模块的数据段通常存放定义在该模块的全局数据和静态数据,这样我们可以把虚表看作是模块的全局数据或者静态数据
·类的虚表会被这个类的所有对象所共享。类的对象可以有很多,但是他们的虚表指针都指向同一个虚表,从这个意义上说,我们可以把虚表简单理解为类的静态数据成员。值得注意的是,虽然虚表是共享的,但是虚表指针并不是,类的每一个对象有一个属于它自己的虚表指针。
·虚表中存放的是虚函数的地址。
另外一个大的疑惑就是对象的虚表指针是在什么时候被赋值的? 我们都知道,类的对象是通过构造函数来完成初始化,但是我们从来没有在构造函数中初始化虚表指针, 那么编译器在幕后又做了哪些事情呢? 我们依然还是通过反汇编来找到答案。 在这个控制台程序的 main 函数中我们构建一个类对象:
类的非静态成员函数调用时,编译器会传入一个"隐藏"的参数。 这个参数就是通常我们说的"this"指针,它的值就是对象的地址。 在上面的代码中,寄存器 ECX 保存的就是这个"
this" 指 针 , 同 时 它 的 值 又 赋 给 了 寄 存 器 EAX。"??_7CD-szBase@@6B@"就是上面提到的虚表,同时它也代表了虚表的地址。
接下来,虚表的地址被赋给了由寄存器 EAX 指定的内存中。由此可见,虚表的地址被存放在对象的起始位置,即对象的第一个数据成员就是它的虚表指针。 同时我们还可以注意到,虚表指针的初始化确实发生在构造函数的调用过程中, 但是在执行构造函数体之前,即进入到构造函数的"{"和"}"之前。 为了更好的理解这一问题, 我们可以把构造函数的调用过程细分为两个阶段,即:
1.进入到构造函数体之间。在这个阶段如果存在虚函数的话,虚表指针被初始化。如果存在构造函数的初始化列表的话,初始化列表也会被执行。
2.进入到构造函数体内。这一阶段是我们通常意义上说的构造函数
简单的搞个基类Base{void fun();virtual void print(){...};public:int a;static b;}
定义一个对象 B b;调试状态下就可以看到b包含了什么
类中只有虚表指针和普通成员(包括const成员)而普通函数,静态成员是不在类中的.
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 function
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:0044F148
Base::f
虚函数表的地址是一样的,也就是说一个类维护一张虚函数表,每个对象拥有一个虚函数表指针指向虚函数表
通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
- 虚函数实现细节
- 虚函数表的实现细节
- 虚函数表的实现细节
- 虚函数表的实现细节
- 虚函数表的实现细节
- vmalloc函数实现细节
- platform_get_resource函数实现细节
- platform_get_resource函数实现细节
- C++虚函数的实现细节、虚析构函数 汇编解析
- C++虚函数的实现细节、虚析构函数 汇编解析
- C++虚函数的实现细节、虚析构函数 汇编解析
- 变参函数实现细节
- platform_get_resource函数实现细节------参数num
- 4.1.4 main函数实现细节
- C语言中变参函数的实现细节
- 模拟实现字符串拷贝函数小细节
- 实验: 虚函数细节验证.
- 从printf了解C语言中变参函数实现细节
- 论高斯消元实现
- 杭电2199 Can you solve this equation?(二分)
- “网购后悔权”为何与“机动车撞人负全责”一样扯淡?
- 高大上的数据挖掘方法论
- 设计模式六大原则(1):单一职责原则
- 虚函数实现细节
- 栈(课本)
- 如何分析JAVA字节码--Oolong反编译
- Ubuntu 12.10 配置远程桌面访问(不用配置VNC或RDP服务器)
- 使用国外 DNS 造成国内网站访问慢的解决方法
- 嵌入式Linux学习计划
- 【Linux】iptables&ebtables
- C语言回顾--“太极拳”--结构体
- 用户界面线程AfxBeginThread的使用