为此写程序实验了一下,得出结论如下:
1. 编译的时候,编译器自动为每个有虚函数的类生成vtable,此vtable类似于静态常量数据,并编译到最终的可执行文件中。
2. 具体实例的vptr在构造函数中赋值,指向vtable地址。
3. 构造函数中如果调用虚函数,只会调用该类的虚函数。
程序如下:
view plaincopy to clipboardprint? virtual ~B() p->vf(); |
27: D(int i, int j) : B(i), _j(j) 28: { 004012D0 push ebp 004012D1 mov ebp,esp 004012D3 push 0FFh 004012D5 push offset __ehhandler$??0D@@QAE@HH@Z (0041d779) 004012DA mov eax,fs:[00000000] 004012E0 push eax 004012E1 mov dword ptr fs:[0],esp 004012E8 sub esp,44h 004012EB push ebx 004012EC push esi 004012ED push edi 004012EE push ecx 004012EF lea edi,[ebp-50h] 004012F2 mov ecx,11h 004012F7 mov eax,0CCCCCCCCh 004012FC rep stos dword ptr [edi] 004012FE pop ecx 004012FF mov dword ptr [ebp-10h],ecx 00401302 mov eax,dword ptr [ebp+8] 00401305 push eax 00401306 mov ecx,dword ptr [ebp-10h] 00401309 call @ILT+125(B::B) (00401082) 0040130E mov dword ptr [ebp-4],0 00401315 mov ecx,dword ptr [ebp-10h] 00401318 mov edx,dword ptr [ebp+0Ch] 0040131B mov dword ptr [ecx+8],edx 0040131E mov eax,dword ptr [ebp-10h] 00401321 mov dword ptr [eax],offset D::`vftable' (0042f01c) 29: vf(); 00401327 mov ecx,dword ptr [ebp-10h] 0040132A call @ILT+155(D::vf) (004010a0) 30: } |
B(int i) : _i(i) 7: { 00401380 push ebp 00401381 mov ebp,esp 00401383 sub esp,44h 00401386 push ebx 00401387 push esi 00401388 push edi 00401389 push ecx 0040138A lea edi,[ebp-44h] 0040138D mov ecx,11h 00401392 mov eax,0CCCCCCCCh 00401397 rep stos dword ptr [edi] 00401399 pop ecx 0040139A mov dword ptr [ebp-4],ecx 0040139D mov eax,dword ptr [ebp-4] 004013A0 mov ecx,dword ptr [ebp+8] 004013A3 mov dword ptr [eax+4],ecx 004013A6 mov edx,dword ptr [ebp-4] 004013A9 mov dword ptr [edx],offset B::`vftable' (0042f028) 8: vf(); 004013AF mov ecx,dword ptr [ebp-4] 004013B2 call @ILT+210(B::vf) (004010d7) 9: } |
其中能够看到两处对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 RAW DATA #2 |
其中我们可以看到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就像程序定义的其他全局变量一样,编译到最终的可执行文件中,在载入运行的时候载入 .