12

在 C++ 中,当从构造函数中调用虚函数时,它的行为不像虚函数。

我认为第一次遇到这种行为的每个人都感到惊讶,但第二次认为这是有道理的:

只要派生构造函数尚未执行,对象就不是派生实例

那么如何调用派生函数呢?前提条件还没来得及设置。例子:

class base {
public:
    base()
    {
        std::cout << "foo is " << foo() << std::endl;
    }
    virtual int foo() { return 42; }
};

class derived : public base {
    int* ptr_;
public:
    derived(int i) : ptr_(new int(i*i)) { }
    // The following cannot be called before derived::derived due to how C++ behaves, 
    // if it was possible... Kaboom!
    virtual int foo()   { return *ptr_; } 
};

Java 和 .NET 完全一样,但他们选择了另一种方式,这可能是最小意外原则的唯一原因吗?

你认为哪个是正确的选择?

4

6 回答 6

11

语言如何定义对象的生命周期存在根本差异。在 Java 和 .Net 中,对象成员在任何构造函数运行之前被初始化为零/空,此时对象生命周期开始。所以当你进入构造函数时,你已经得到了一个初始化的对象。

在 C++ 中,对象生命周期仅在构造函数完成时开始(尽管成员变量和基类在它开始之前已完全构造)。这解释了调用虚函数时的行为,以及为什么在构造函数的主体中有异常时不运行析构函数。

Java/.Net 对象生命周期定义的问题在于,在对象已初始化但构造函数尚未运行时,如果不必在特殊情况下确保对象始终满足其不变量,则更加困难。C++ 定义的问题是你有这个奇怪的时期,对象处于不确定状态并且没有完全构造。

于 2008-09-16T18:49:55.313 回答
7

这两种方式都可能导致意想不到的结果。最好的选择是根本不在构造函数中调用虚函数。

我认为 C++ 方式更有意义,但是当有人审查您的代码时会导致预期问题。如果您知道这种情况,则为了以后的调试,您应该故意不要将您的代码置于这种情况下。

于 2008-08-31T12:47:34.727 回答
2

构造函数中的虚函数,为什么语言不同?

因为没有一个好的行为。我发现 C++ 的行为更有意义(由于首先调用基类 c-tor,因此它们应该调用基类虚函数是理所当然的——毕竟派生类 c-tor 还没有运行,所以它可能没有为派生类虚函数设置正确的先决条件)。

但有时,我想使用虚函数来初始化状态(因此在未初始化状态的情况下调用它们并不重要),C#/Java 行为更好。

于 2008-09-01T00:57:41.650 回答
1

我认为 C++ 在具有“最正确”行为方面提供了最好的语义......但是它对编译器来说需要更多的工作,并且代码对于以后阅读它的人来说肯定是不直观的。

使用 .NET 方法,功能必须非常有限,不能依赖任何派生对象状态。

于 2008-08-31T12:56:28.340 回答
0

Delphi 在 VCL GUI 框架中很好地利用了虚拟构造函数:

type
  TComponent = class
  public
    constructor Create(AOwner: TComponent); virtual; // virtual constructor
  end;

  TMyEdit = class(TComponent)
  public
    constructor Create(AOwner: TComponent); override; // override virtual constructor
  end;

  TMyButton = class(TComponent)
  public
    constructor Create(AOwner: TComponent); override; // override virtual constructor
  end;

  TComponentClass = class of TComponent;

function CreateAComponent(ComponentClass: TComponentClass; AOwner: TComponent): TComponent;
begin
  Result := ComponentClass.Create(AOwner);
end;

var
  MyEdit: TMyEdit;
  MyButton: TMyButton;
begin
  MyEdit := CreateAComponent(TMyEdit, Form) as TMyEdit;
  MyButton := CreateAComponent(TMyButton, Form) as TMyButton;
end;
于 2008-09-16T17:50:12.210 回答
0

我发现 C++ 行为非常烦人。例如,您不能编写虚函数来返回所需的对象大小,并让默认构造函数初始化每个项目。例如,这样做会很好:

BaseClass() { for (int i=0; i<virtualSize(); i++) initialize_stuff_for_index(i); }

再一次,C++ 行为的优点是它不鼓励编写像上面这样的构造函数。

我不认为调用假设构造函数已经完成的方法的问题是 C++ 的一个很好的借口。如果这确实是一个问题,那么构造函数将不允许调用任何方法,因为同样的问题可能适用于基类的方法。

反对 C++ 的另一点是该行为的效率要低得多。尽管构造函数直接知道它调用了什么,但必须将每个类的 vtab 指针从基类更改为最终类,因为构造函数可能会调用其他将调用虚函数的方法。根据我的经验,这比在构造函数中更有效地调用虚函数所节省的时间要多得多。

更烦人的是,析构函数也是如此。如果您编写了一个虚拟 cleanup() 函数,而基类析构函数执行了 cleanup(),那么它肯定不会按照您的预期执行。

这一点以及 C++ 在退出时在静态对象上调用析构函数的事实确实让我很生气很长一段时间。

于 2008-09-16T21:41:24.090 回答