English 中文(简体)
构造函数中的虚拟函数,为什么语言不同?
原标题:
  • 时间:2008-08-31 12:37:24
  •  标签:

在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完全一样,但他们选择了另一种方式,这可能是最小惊喜原则的唯一原因

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

最佳回答

语言如何定义一个对象的生命周期是有根本区别的。在Java和.Net中,在运行任何构造函数之前,对象成员都是零/null初始化的,此时对象生命周期就开始了。因此,当您进入构造函数时,您已经得到了一个初始化的对象。

在C++中,对象生命周期只有在构造函数完成时才开始(尽管成员变量和基类在它开始之前已经完全构造好了)。这解释了调用虚拟函数时的行为,也解释了如果构造函数主体中出现异常,则析构函数不运行的原因。

Java/.Net对对象生存期的定义的问题在于,如果不考虑对象初始化但构造函数尚未运行的特殊情况,就很难确保对象始终满足其不变量。C++定义的问题是,有一个奇怪的周期,对象处于不确定状态,并且没有完全构建。

问题回答

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

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

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

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

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

我认为C++在拥有最正确的行为方面提供了最好的语义。。。然而,编译器需要做更多的工作,而且对于以后阅读的人来说,代码显然是不直观的。

对于.NET方法,函数必须非常有限,不能依赖于任何派生的对象状态。

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;

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

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

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

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

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

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

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





相关问题