C++中构造函数能调用虚函数吗?

时间:2022-09-09 18:48:14

      环境:XPSP3 VS2005

        今天黑总给应聘者出了一个在C++的构造函数中调用虚函数的问题,具体的题目要比标题复杂,大体情况可以看如下的代码:

class Base
{
public:
Base()
{
Fuction();
}

virtual void Fuction()
{
cout << "Base::Fuction" << endl;
}
};

class A : public Base
{
public:
A()
{
Fuction();
}

virtual void Fuction()
{
cout << "A::Fuction" << endl;
}
};

// 这样定义一个A的对象,会输出什么?
A a;

        首先回答标题的问题,调用当然是没有问题的,但是获得的是你想要的结果吗?或者说你想要什么样的结果?

        有人说会输出:

A::Fuction
A::Fuction

        如果是这样,首先我们回顾下C++对象模型里面的构造顺序,在构造一个子类对象的时候,首先会构造它的基类,如果有多层继承关系,实际上会从最顶层的基类逐层往下构造(虚继承、多重继承这里不讨论),如果是按照上面的情形进行输出的话,那就是说在构造Base的时候,也就是在Base的构造函数中调用Fuction的时候,调用了子类A的Fuction,而实际上A还没有开始构造,这样函数的行为就是完全不可预测的,因此显然不是这样,实际的输出结果是:

Base::Fuction
A::Fuction

        据说在JAVA中是上一种输出(感觉有点匪夷所思)。

        我们来单步看一下到底发生了什么?在A的构造函数里面首先会去调用Base的构造函数,Base的构造函数如下:

class Base
{
public:
 Base()
00411600  push        ebp 
00411601  mov         ebp,esp
00411603  sub         esp,0CCh
00411609  push        ebx 
0041160A  push        esi 
0041160B  push        edi 
0041160C  push        ecx 
0041160D  lea         edi,[ebp-0CCh]
00411613  mov         ecx,33h
00411618  mov         eax,0CCCCCCCCh
0041161D  rep stos    dword ptr es:[edi]
0041161F  pop         ecx 
00411620  mov         dword ptr [ebp-8],ecx
00411623  mov         eax,dword ptr [this]
00411626  mov         dword ptr [eax],offset Base::`vftable' (41770Ch)
 {
  Fuction();
0041162C  mov         ecx,dword ptr [this]
0041162F  call        Base::Fuction (4111A9h)

 }
00411634  mov         eax,dword ptr [this]
00411637  pop         edi 
00411638  pop         esi 
00411639  pop         ebx 
0041163A  add         esp,0CCh
00411640  cmp         ebp,esp
00411642  call        @ILT+460(__RTC_CheckEsp) (4111D1h)
00411647  mov         esp,ebp
00411649  pop         ebp 
0041164A  ret

        从单步跟踪来看,注意黑色加粗的那部分汇编代码,ecx中存放的是对象的地址(0x0012ff60,我的机器上的情况看下图,有图有真相),首先是设置vtable的地址到对象的前四个字节(不同的编译器可能不同),然后就直接调用了Base::Fuction函数,并没有走虚机制,而我们此时看虚表中的状态,虚表已经填充的是0x4111a9,注意虚表的地址0x0041770c,而此时对象地址0x0012FF60前四个字节存放的正是0x0041770c。
C++中构造函数能调用虚函数吗?
        继续跟踪,流程又回到A的构造函数中,再次注意加粗部分的代码,从基类Base的构造函数返回后,在A的构造函数中,重设了虚表指针,现在的虚表指针是(0x417700h),同样调用Fuction的时候直接调用了A::Fuction函数,并没有使用虚机制,而且此时虚表0x417700h指向的位置存放的0x41110e正是A::Fuction的地址。

C++中构造函数能调用虚函数吗?

class A : public Base
{
public:
 A()
00411590  push        ebp 
00411591  mov         ebp,esp
00411593  sub         esp,0CCh
00411599  push        ebx 
0041159A  push        esi 
0041159B  push        edi 
0041159C  push        ecx 
0041159D  lea         edi,[ebp-0CCh]
004115A3  mov         ecx,33h
004115A8  mov         eax,0CCCCCCCCh
004115AD  rep stos    dword ptr es:[edi]
004115AF  pop         ecx 
004115B0  mov         dword ptr [ebp-8],ecx
004115B3  mov         ecx,dword ptr [this]
004115B6  call        Base::Base (411140h)
004115BB  mov         eax,dword ptr [this]
004115BE  mov         dword ptr [eax],offset A::`vftable' (417700h)
 {
  Fuction();
004115C4  mov         ecx,dword ptr [this]
004115C7  call        A::Fuction (41110Eh)
 }
004115CC  mov         eax,dword ptr [this]
004115CF  pop         edi 
004115D0  pop         esi 
004115D1  pop         ebx 
004115D2  add         esp,0CCh
004115D8  cmp         ebp,esp
004115DA  call        @ILT+460(__RTC_CheckEsp) (4111D1h)
004115DF  mov         esp,ebp
004115E1  pop         ebp 
004115E2  ret


        其实事情就是这么简单。