编译器又是怎么实现的呢?


  1. 这样写肯定是不推荐的,根据《effective C++》上的解释,构造和析构阶段调动虚函数无法产生多态特性,这往往违背了开发者的本意。
  2. 是不是静态绑定?我认为也不是。在执行构造函数user code的时候,其实基类和派生类的vftable都已经初始化完成,但此时,基类part的vfptr装载的是基类的vftable地址,若此时调用虚函数,仍然会去基类虚表里找jmp地址,因此,找到的是base virtual function。如果你反汇编一下代码,会发现,在基类part构造完成之后,派生part构造执行compiler code的最后一步,是这样一条汇编指令:

mov dword ptr [eax],offset derived::`vftable (0FD9B50h)

这一步,是把对象中的vfptr覆盖成derived::vfptr。因此,在这之后,你才能正确的触发多态。这种情况和静态绑定的表现一样,但我认为不能划等号,无论如何,它也仍然是通过vftable找函数jmp地址,而不是直接jmp。

上面有朋友说这么写代码是UB,其实这说法是不对的,C++里这么写代码,产生的行为是确定的,一定会调用基类方法,不会产生多态,但也不会有什么access violation或者crash的问题。相比较而言,java在基类构造函数的compiler code倒是会把虚表和虚指针更新完整,多态倒是没问题了,但如果这么玩,就等于是调用未初始化派生对象的方法,产生的行为就是UB了。

总之,无论什么语言,都不推荐这么做。


静态多态请看 CRPT。


构造/析构函数中对this的虚函数调用基本上可以理解为静态绑定。而对并非指向当前对象的指针/引用仍然使用动态绑定。

gcc对此的实现是:对于构造函数,先调用基类构造函数,然后将this的虚表指针指向本类虚表,再执行初始化列表的成员部分以及构造函数体;对于析构函数,将虚表指针指向本类虚表,然后执行析构函数体。

虽然我只看了gcc的实现,但并不像楼上所说是未定义行为。标准[1]指定:

When a virtual function is called directly or indirectly from a constructor or from a destructor (including during the construction or destruction of the class』s non-static data members, e.g. in a member initializer list), and the object to which the call applies is the object under construction or destruction, the function called is the final overrider in the constructor』s or destructor』s class and not one overriding it in a more-derived class.

有一个小特例:在有多个分支的继承结构中,对不属于构造/析构函数所在分支的指针/引用进行虚函数调用,是未定义行为。例如:类A、B虚继承V,类D继承A和B,如果在B的构造函数中调用((A*)this)的虚函数,结果就是取决于编译器的了(gcc编译结果会调用到B中的实现)。从这里可以看出,构造/析构函数内仍然是使用动态绑定机制,只不过对虚表指针的处理使得效果与静态绑定几乎一致。

参考

  1. ^https://en.cppreference.com/w/cpp/language/virtual#During_construction_and_destruction


当然是动态绑定的。但是此时虚函数表指针指向的是当前正在构造/析构的虚函数表,从而与静态绑定的行为一致。

至于为什么是这么做呢?构造/析构函数内如果调用非虚成员函数,而这个非虚成员函数实现内会使用动态绑定的方式调用虚函数,编译器无法修改其他非虚成员函数实现的前提下,只能统一实现为动态绑定,表现为静态绑定。


构造时虚表还没做出来呢,虚指针可能指向基类的虚表。


从结果上来说,所有正确实现的 C++ 编译器都会在构造、析构函数中调用虚函数的本地版本,但不能理解为是静态绑定的。

关于编译器的大致实现可以看一下我的另一个回答:

C++ 关于子类重写父类方法,并在构造函数中调用的问题??

www.zhihu.com图标

或者干脆读一下 C++ FAQ 的有关部分,有详细的讨论:

Standard C++?

isocpp.org


这个是未定义行为。我说一下vs下是怎么做的。

vs下的类的首地址的内容为虚表指针,占4个或者8个位元组,取决于是32位还是64位程序。

在基类初始化时,虚表指针指向基类虚表,子类初始化时,将其改写为子类虚表地址。因此在基类中调用虚方法一定是调用了基类的虚函数。

析构函数中也一样,只不过虚表指针的改写顺序是相反的。

所以无论如何不要在析构函数或者构造函数中调用虚函数。


推荐阅读:
相关文章