在构造函数中调用虚方法:Java和C++之间的区别

Xia*_*Jia 15 c++ java virtual-functions object-lifetime vptr

在Java中:

class Base {
    public Base() { System.out.println("Base::Base()"); virt(); }
    void virt()   { System.out.println("Base::virt()"); }
}

class Derived extends Base {
    public Derived() { System.out.println("Derived::Derived()"); virt(); }
    void virt()      { System.out.println("Derived::virt()"); }
}

public class Main {
    public static void main(String[] args) {
        new Derived();
    }
}
Run Code Online (Sandbox Code Playgroud)

这将输出

Base::Base()
Derived::virt()
Derived::Derived()
Derived::virt()
Run Code Online (Sandbox Code Playgroud)

但是,在C++中,结果是不同的:

Base::Base()
Base::virt() // ? Not Derived::virt()
Derived::Derived()
Derived::virt()
Run Code Online (Sandbox Code Playgroud)

(有关C++代码,请参阅http://www.parashift.com/c++-faq-lite/calling-virtuals-from-ctors.html)

什么导致Java和C++之间的这种差异?这是vtable初始化的时间吗?

编辑:我确实理解Java和C++机制.我想知道的是这个设计决定背后的见解.

Kon*_*lph 14

这两种方法都显然存在缺陷:

  • 在Java中,调用转到一个无法this正常使用的方法,因为它的成员尚未初始化.
  • 在C++中,如果您不知道C++如何构造类,则会调用一个不直观的方法(即不是派生类中的方法).

为什么每种语言都做它所做的事情是一个悬而未决的问题,但两者都可能声称是"更安全"的选择:C++的方式阻止了未初始成员的使用; Java的方法允许在类的构造函数(这是一个完全有效的用例)内的多态语义(在某种程度上).

  • 一旦你接受C++实例像洋葱一样构建(从内到外,一步一步),C++调用base :: virtual方法就非常直观了.因此,当构造base时,不会覆盖其方法.它发生在以后洋葱生长的时候...... (5认同)
  • 还有一件事要解决:短语“即不是虚拟的”误导性地表明来自构造函数的调用不是虚拟的,而是静态解析的。但是虚拟调用机制在构造函数中的工作方式与其他地方完全相同。特别是,如果有一个基类方法的调用反过来又调用了一个虚方法 `v`,并且 `v` 在这个类中已经被覆盖,那么调用的是 `v` 的覆盖器。 (2认同)

Che*_*Alf 11

那么你已经链接到常见问题解答的讨论,但这主要是面向问题,而不是进入理由,为什么.

简而言之,它适用于类型安全.

这是C++在类型安全方面胜过Java和C#的少数几种情况之一.;-)

当您创建一个类时A,在C++中,您可以让每个A构造函数初始化新实例,以便所有关于其状态的常见假设(称为类不变量)成立.例如,类不变量的一部分可以是指针成员指向某些动态分配的内存.当每个公开可用的方法保留类不变量时,它保证在每个方法的入口处也保持,这极大地简化了事情 - 至少对于精心选择的类不变量!

在每种方法中都不需要进一步检查.

相比之下,使用两阶段初始化(例如在Microsoft的MFC和ATL库中),当调用方法(非静态成员函数)时,您永远无法确定是否所有内容都已正确初始化.这与Java和C#非常相似,只是在那些语言中缺少类不变保证来自这些语言仅仅是启用但不主动支持类不变量的概念.简而言之,从基类构造函数调用的Java和C#虚方法可以在尚未初始化的派生实例上调用,其中(派生)类不变量尚未建立!

因此,这种对类不变量的C++语言支持非常棒,有助于消除大量的检查和许多令人沮丧的令人困惑的错误.

但是,在基类构造函数中执行派生类特定初始化会有点困难,例如在最顶层的GUI Widget类构造函数中执行常规操作.

FAQ项目"好的,但有没有办法模拟这种行为,好像动态绑定在我的基类的构造函数中对这个对象起作用?".

有关最常见情况的更全面处理,请参阅我的博客文章"如何通过使用零件工厂避免后期构建".


Pet*_*ker 7

无论它是如何实现的,语言定义所说的都应该发生变化.Java允许您在尚未完全初始化的派生对象上调用函数(它已被零初始化,但其构造函数尚未运行).C++不允许这样做; 直到派生类的构造函数运行,没有派生类.