18

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

4

7 回答 7

15

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

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

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

于 2012-11-18T13:24:35.390 回答
13

好吧,您已经链接到常见问题解答的讨论,但这主要是面向问题的,而不是进入基本原理,为什么

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

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

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

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

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

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

然而,在基类构造函数中进行特定于派生类的初始化有点困难,例如在最顶层的 GUIWidget类的构造函数中进行一般性操作。

FAQ 项目“好的,但是有没有办法模拟这种行为,就好像动态绑定在我的基类构造函数中的 this 对象上起作用一样?” 稍微深入一点。

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

于 2012-11-18T13:27:34.350 回答
7

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

于 2012-11-18T13:13:52.877 回答
2

希望这会有所帮助:

当你的行new Derived()执行时,首先发生的是内存分配。该程序将分配一块足够大的内存来容纳 和 的Base成员Derrived。此时,没有对象。这只是未初始化的内存。

Base的构造函数完成时,内存将包含一个类型的对象Base,并且类不变量Base应该保持不变。那个记忆中仍然没有Derived对象。

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

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

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

于 2012-11-18T14:28:55.170 回答
0

在 Java 中,方法调用是基于对象类型的,这就是它表现得那样的原因(我对 c++ 了解不多)。

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

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

如果有问题,很高兴更新我的答案。

于 2012-11-18T13:11:44.503 回答
0

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

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

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

于 2012-11-18T13:22:00.317 回答
0
于 2016-09-22T08:06:16.387 回答