在看《Inside The C++ Object Model》时想到一个问题, vtable是在什么时候生成的?运行时对象的vptr又是什么时候赋值的?如何得知相应的函数的地址的?
为此写程序实验了一下,得出结论如下:
1. 编译的时候,编译器自动为每个有虚函数的类生成vtable,此vtable类似于静态常量数据,并编译到最终的可执行文件中。
2. 具体实例的vptr在构造函数中赋值,指向vtable地址。
3. 构造函数中如果调用虚函数,只会调用该类的虚函数。
程序如下:
view plaincopy to clipboardprint?
#include <iostream>
class B
{
public:
B(int i) : _i(i)
{
vf();
}
virtual void vf()
{
std::cout << "B::vf called" << std::endl;
}
virtual ~B()
{
vf();
}
private:
int _i;
};
class D : public B
{
public:
D(int i, int j) : B(i), _j(j)
{
vf();
}
virtual void vf()
{
std::cout << "D:vf called" << std::endl;
}
virtual ~D()
{
vf();
}
private:
int _j;
};
int main()
{
B* p = new D(1, 2);
p->vf();
delete p;
return 0;
}
其中能够看到两处对vptr的赋值
B::B中的
00401321 mov dword ptr [eax],offset D::`vftable' (0042f01c)
和D::D中的
004013A9 mov dword ptr [edx],offset B::`vftable' (0042f028)
这是对instance的vptr赋值的语句, 通过计算,可以得知,此时的mov命令目标地址就是传入的this指针(通过ECX传入)所指的第一个单元,(这个要认真算一下,跳来跳去比较麻烦)
还能看到对_i和_j的赋值语句
004013A0 mov ecx,dword ptr [ebp+8]
在B::B执行完毕时对象的状态如下
-------------------
| vptr (B:vftable)|
--------------------
| _i (1) |
------------------
| _j (未赋值) |
--------------------
D::D执行完毕后对象状态如下:
-------------------
| vptr (D:vftable)|
--------------------
| _i (1) |
------------------
| _j (2) |
--------------------
由此可以得出结论
构造函数中调用虚函数是不会产生“虚”效果的,编译的时候就写死了,只调用自己的虚函数。如上
0040132A call @ILT+155(D::vf) (004010a0)
至于原因,很多地方都有解释: 此时子类尚未构造,因此无法去调用子类的虚函数。
此外,我们看到了vtable的地址
B::`vftable' (0042f028)
D::`vftable' (0042f01c)
通过watch窗口能看到起内容,我们再使用dumpbin /all test.exe > test_all.txt,然后打开test_all.txt,我们能够看到同样的内容
SECTION HEADER #2
.rdata name
2DBF virtual size
2F000 virtual address
3000 size of raw data
2F000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
Read Only
RAW DATA #2
0042F000: 00 00 00 00 D1 2C 50 4A 00 00 00 00 02 00 00 00 .....,PJ........
0042F010: 32 00 00 00 00 00 00 00 00 70 03 00 A0 10 40 00 2........p....@.
0042F020: D2 10 40 00 00 00 00 00 D7 10 40 00 23 10 40 00 ..@.......@.#.@.
0042F030: 00 00 00 00 42 3A 3A 76 66 20 63 61 6C 6C 65 64 ....B::vf called
0042F040: 00 00 00 00 44 3A 76 66 20 63 61 6C 6C 65 64 00 ....D:vf called.
其中我们可以看到0042f01c处的几个地址A0 10 40 00 D2 10 40 00, 分别是0x004010A0, 和0x04010D2,对照汇编以及Debug时的Variables窗口,可以看到这两个地址是D::vf和D::~D的地址, 这就是D类的vtable的内容,两个虚函数的地址在看B的D7 10 40 00 23 10 40 00, 0x004010D7 和00401023是B::vf和B::~B的地址,可以在构造函数中设断点看到这些信息。
为此,我们可以认为编译的是很编译器做了这些工作:
1. 为有虚函数的类,生成虚函数表 ,可以认为插入到相应的C++代码中(C++编译器往用户代码中插入不少东西,比如构造函数、析构函数的调用)
可以认为如下形式:
static const void* _vtable_B[2] = { (void*)B::vf,
(void*)B::~B}
static const void* _vtable_D[2] = { (void*)D::vf,
(void*)D::~D}
2. 然后再在类的构造函数中插入对vptr的赋值
this->vptr = _vtable_B[0];
3. 其中的vtable就像程序定义的其他全局变量一样,编译到最终的可执行文件中,在载入运行的时候载入 .