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

在爪哇:

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();
    }
}

这将输出

Base::Base()
Derived::virt()
Derived::Derived()
Derived::virt()

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

Base::Base()
Base::virt() // ← Not Derived::virt()
Derived::Derived()
Derived::virt()

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

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

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


答案 1

这两种方法显然都有缺点:

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

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


答案 2

好吧,您已经链接到FAQ的讨论,但这主要是以问题为导向的,而不是深入基本原理,为什么

简而言之,这是为了类型安全

这是C++在类型安全方面击败Java和C#的少数情况之一。;-)

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

因此,无需在每种方法中进行进一步的检查。

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

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

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

FAQ 项“好的,但是有没有办法模拟该行为,就好像动态绑定在我的基类的构造函数中的 this 对象上工作一样?”对此进行了一些介绍。

有关最常见情况的更全面处理,另请参阅我的博客文章“如何使用零件工厂避免后期施工”。


推荐