施工过程中的虚函数。为什么Java与C ++不同

时间:2012-08-26 21:00:04

标签: java c++

我今天进行了测试,其中一个问题是在C ++构造函数中使用虚方法。我没有回答这个问题,我回答说应该没有任何问题,但是在阅读this后我发现我错了。

所以我理解不允许这样做的原因是因为派生对象没有完全初始化,因此调用它的虚方法会导致无效后果。

我的问题是如何在Java / C#中解决的?我知道我可以在我的基础构造函数中调用派生方法,我会假设这些语言具有完全相同的问题。

6 个答案:

答案 0 :(得分:15)

Java与C ++有着截然不同的对象模型。在Java中,您不能拥有作为类类型对象的变量 - 相反,您只能将引用赋予对象(类类型)。因此,类的所有成员(仅作为引用)从null开始,直到整个派生对象已在内存中设置。只有这样才能运行构造函数。因此,当基础构造函数调用虚函数时,即使重写了该函数,重写的函数也至少可以正确引用派生类的成员。 (那些成员可能不会分配,但至少他们存在。)

(如果有帮助,你也可以认为每个没有final成员的类在技术上是默认构造的,至少原则上是这样的:与C ++不同,Java没有这样的作为常量或引用的东西(必须在C ++中初始化),实际上根本没有初始化列表.Java中的变量根本不需要初始化。它们是原始的从0开始,或以null开头的类类型引用。一个例外来自非静态final类成员,它们无法反弹,必须通过精确一个来“初始化” 赋值语句在每个构造函数的某处[感谢@josefx指出这个!]。)

答案 1 :(得分:2)

  

理解不允许这样做的原因是因为派生对象没有完全初始化,因此调用它的虚方法会导致无效后果

错误。 C ++将调用基类的方法实现,而不是派生类的实现。没有“无效后果”。避免构造的唯一有效理由是行为有时会让人感到意外。

这与Java的不同因为Java调用派生类的实现。

答案 2 :(得分:1)

每个Java构造函数都是这样的:

class Foo extends Bar {
  Foo() {
    super(); // creates Bar
    // do things
  }
}

因此,如果您在do things中放置代码处理派生方法,似乎是逻辑,在super();

中调用其构造函数后,该基础对象已正确初始化

答案 3 :(得分:1)

在C ++中,每个多态类(至少有一个虚函数的类)都有一个隐藏指针(通常命名为v-table或类似的东西),它将被初始化为虚拟表(函数数组)指向该类的每个虚函数的主体)当你调用虚函数C ++时只需调用((v-table*)class)[index of your function]( function-parameters ),所以如果你在基类构造函数v-table中调用虚函数指向基类的虚拟表class因为你的类是基类的,它仍然需要一些初始化才能成为子类,因此你将从base调用函数的实现,而不是从子函数调用,如果这是纯虚函数,你将获得访问冲突。
但是在java中这不是这样的,在java中,整个类就像std::map<std::string, JValue>,在这种情况下,JValue是一种变体类型(例如union或boost::variant)在base的构造函数中的一个函数,它将在map中找到函数名称并调用它,它仍然不是来自子元素的值,但你仍然可以调用它,如果你在prototype中更改了它,那么之前创建的是原型您的构造函数可以成功调用子函数,但如果函数需要从子函数的构造函数初始化,您仍然会收到错误或结果无效。
所以一般来说,从基类中调用子函数(例如虚函数)并不是一个好习惯。如果你的类需要这样做,添加一个initialize方法并从你的子类的构造函数中调用它。

答案 4 :(得分:0)

我认为Java / C#通过从派生类向后构建而不是从基类前向构建C ++来避免这个问题。

Java在类构造函数中隐式调用super(),所以当调用派生类构造函数中的第一行编写代码时,所有继承类的构造函数都被保证已被调用,因此新实例将具有已完全初始化。

我认为在C ++中,类的一个新实例开始作为基类生活,并在继承链向下移动时“升级”为最终的类类型。这意味着当您在构造函数中调用虚函数时,您实际上将调用基类的该函数的版本。

在Java中,可能是C#,新实例作为必需的类类型启动生命,因此将调用正确的虚拟方法版本。

答案 5 :(得分:0)

Java并不能完全避免这个问题。

在初始化这些字段之前,将调用从依赖于子类字段的超类构造函数调用的重写方法。

如果您控制整个类层次结构,当然可以确保您的覆盖不依赖于子类字段。但是不要从构造函数中调用虚方法更安全。