很简单的测试代码:
class A
{
public:
A() : m_a(111)
{
bar();
this->bar();
foo();
}
virtual void bar() { std::cout << "A::bar" << std::endl; }
void foo()
{
bar();
}
int m_a;
};
class B : public A
{
public:
B() : m_b(111)
{
bar();
this->bar();
foo();
}
virtual void bar() { std::cout << "B::bar" << std::endl; }
int m_b;
};
int _tmain(int argc, _TCHAR* argv[])
{
B b;
return 0;
}
输出跟预想的一样:
A::bar
A::bar
A::bar
B::bar
B::bar
B::bar
看一下在调用B::B()时编译器产生的代码在干啥:
B() : m_b(111)
{
01251590 push ebp
01251591 mov ebp,esp
01251593 sub esp,0CCh
01251599 push ebx
0125159A push esi
0125159B push edi
0125159C push ecx
0125159D lea edi,[ebp-0CCh]
012515A3 mov ecx,33h
012515A8 mov eax,0CCCCCCCCh
012515AD rep stos dword ptr es:[edi]
012515AF pop ecx
012515B0 mov dword ptr [ebp-8],ecx
012515B3 mov ecx,dword ptr [this]
012515B6 call A::A (1251140h)
012515BB mov eax,dword ptr [this]
012515BE mov dword ptr [eax],offset B::`vftable' (1257804h) /// 在调用基类构造函数A::A()之后,这条指令将vptr指向B的vtable
012515C4 mov eax,dword ptr [this]
012515C7 mov dword ptr [eax+8],6Fh /// <- 这里将m_b初始化为111
bar();
012515CE mov ecx,dword ptr [this]
012515D1 call B::bar (12511E5h)
this->bar();
012515D6 mov ecx,dword ptr [this]
012515D9 call B::bar (12511E5h)
foo();
012515DE mov ecx,dword ptr [this]
012515E1 call A::foo (125128Fh)
}
注意上面红色的部分,它们告诉我们:
1)在编译构造函数时,编译器会自动加入指令将vptr指向当前类的vtable,并且这发生在由构造函数产生的任何指令之前;
2)如果在构造函数里调用虚函数,编译器会按照调用普通函数来处理,在这里并没有使用vptr;
那么再看一下A::A()在干啥:
A() : m_a(111)
{
003C1BC0 push ebp
003C1BC1 mov ebp,esp
003C1BC3 sub esp,0CCh
003C1BC9 push ebx
003C1BCA push esi
003C1BCB push edi
003C1BCC push ecx
003C1BCD lea edi,[ebp-0CCh]
003C1BD3 mov ecx,33h
003C1BD8 mov eax,0CCCCCCCCh
003C1BDD rep stos dword ptr es:[edi]
003C1BDF pop ecx
003C1BE0 mov dword ptr [ebp-8],ecx
003C1BE3 mov eax,dword ptr [this]
003C1BE6 mov dword ptr [eax],offset A::`vftable' (3C7810h)
003C1BEC mov eax,dword ptr [this]
003C1BEF mov dword ptr [eax+4],6Fh /// <- 这里将m_a初始化为111
bar();
003C1BF6 mov ecx,dword ptr [this]
003C1BF9 call A::bar (3C11EAh)
this->bar();
003C1BFE mov ecx,dword ptr [this]
003C1C01 call A::bar (3C11EAh)
foo();
003C1C06 mov ecx,dword ptr [this]
003C1C09 call A::foo (3C128Fh)
}
红色部分再次证实了编译器会在构造函数最前面插入指令将vptr指向当前类的vtable。有没有注意我们还在A的构造函数里调用了一个非虚函数foo(),这个foo()里调用了虚函数bar()?那么究竟哪个bar()会被调用呢?
void foo()
{
003C16E0 push ebp
003C16E1 mov ebp,esp
003C16E3 sub esp,0CCh
003C16E9 push ebx
003C16EA push esi
003C16EB push edi
003C16EC push ecx
003C16ED lea edi,[ebp-0CCh]
003C16F3 mov ecx,33h
003C16F8 mov eax,0CCCCCCCCh
003C16FD rep stos dword ptr es:[edi]
003C16FF pop ecx
003C1700 mov dword ptr [ebp-8],ecx
bar();
003C1703 mov eax,dword ptr [this]
003C1706 mov edx,dword ptr [eax]
003C1708 mov esi,esp
003C170A mov ecx,dword ptr [this]
003C170D mov eax,dword ptr [edx]
003C170F call eax
003C1711 cmp esi,esp
003C1713 call @ILT+455(__RTC_CheckEsp) (3C11CCh)
}
显然这里用到了vptr,也就是说会调用vptr当前指向的vtable中的函数。既然foo()是在A的构造函数里调用的,这个时候vptr指向A::vtable,所以A::bar()会被调用。
以上代码在VS2008里测试。