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

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

这将输出

 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 ++机制。 我想知道的是这个设计决定背后的见解。

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

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

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

那么你已经链接到了常见问题解答的讨论 ,但这主要是面向问题的,没有进入理论基础, 为什么

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

这是C ++在类型安全方面胜过Java和C#的少数情况之一。 😉

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

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

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

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

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

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

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

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

希望这会有所帮助:

当您的new Derived()执行时,首先发生的是内存分配。 该程序将分配一大块内存,足以容纳BaseDerrived的成员。 此时, 没有任何对象 。 这只是未初始化的记忆。

Base的构造函数完成后,内存将包含一个Base类型的对象, Base的类不变量应该成立。 该内存中仍然没有Derived对象。

构造base 期间Base对象处于部分构造状态,但语言规则信任您足以让您在部分构造的对象上调用自己的成员函数。 Derived对象不是部分构造的。 它不存在。

您对虚函数的调用最终会调用基类的版本,因为在该时间点, Base是对象的派生类型最多。 如果要调用Derived::virt ,它将使用不是Derrived类型的this-pointer调用Derived的成员函数 ,从而破坏类型安全性。

从逻辑上讲,类是构造的,具有在其上调用的函数,然后被破坏。 您不能在尚未构造的对象上调用成员函数,也不能在对象被销毁后调用它上的成员函数。 这对于OOP来说是相当基础的,C ++语言规则只是帮助你避免做违反这个模型的事情。

在Java中,方法调用基于对象类型,这就是为什么它的行为(我不太了解c ++)。

这里你的对象是Derived类型,所以jvm在Derived对象上调用方法。

如果清楚地理解虚拟概念,java中的等价物是抽象的,那么你现在的代码实际上并不是java术语中的虚拟代码。

如果出错了,我很乐意更新我的回答。

实际上我想知道这个设计决策背后的洞察力是什么

可能在Java中,每个类型都派生自Object,每个Object都是某种叶子类型,并且有一个JVM,其中构造了所有对象。

在C ++中,许多类型根本不是虚拟的。 此外,在C ++中,基类和子类可以分别编译为机器代码:因此基类可以完成它所做的事情,而不管它是否是其他类的超类。

在C ++和Java语言的情况下,构造函数不是多态的,而在两种语言中,方法都可以是多态的。 这意味着,当构造函数中出现多态方法时,设计者将有两个选择。

  • 要么严格遵循非多态构造函数的语义,要么将构造函数中调用的任何多态方法视为非多态。 这就是C ++的§
  • 或者,妥协非多态构造函数的严格语义,并遵守多态方法的严格语义。 因此,构造函数的多态方法总是多态的。 这就是Java的作用。

由于没有任何策略提供或妥协任何真正的好处,相比其他,但Java的方式减少了大量的开销(不需要根据构造函数的上下文区分多态),并且由于Java是在C ++之后设计的,我认为,Java的设计者选择了第二个选项,看到了更少实现开销的好处。

2016年12月21日新增


§ 为了避免语句“在构造函数中调用的方法为非多态的……这就是C ++的作用”如果没有仔细审查上下文可能会让人感到困惑,我正在添加一个forms化以精确地限定我的意思。

如果C类具有某个虚函数F的直接定义并且其ctor具有对F的调用,则对子类T的实例的C的ctor的任何(间接)调用将不会影响F的选择; 事实上, C::F将始终从C的ctor中调用。 从这个意义上讲,虚拟F调用是不那么多态的(相比之下,Java将根据T选择F
此外,重要的是要注意,如果C从某个父PinheritanceF并且没有覆盖F ,那么C的ctor将调用P::F ,甚至可以静态地确定IMHO。