调用纯虚函数

5
可能重复:在构造函数中调用虚函数 看一下这段代码。在基类的构造函数中,我们可以使用'this'指针调用纯虚函数。现在当我想创建一个指向相同类型的指针并将“this”强制转换为相同类型时,它会抛出运行时异常“纯虚函数调用异常”。这是为什么呢?
#include <iostream>

using namespace std;

class Base
{
  private:
  virtual void foo() = 0;
  public:
  Base()
  {
    //Uncomment below 2 lines and it doesn't work (run time exception)
    //Base * bptr = (Base*)this;
    //bptr->foo();
    //This call works
    this->foo();
  }
};

void
Base::foo()
{
  cout << "Base::foo()=0" << endl;
}

class Der : public Base
{
  public:
  Der()
  {
  }
  public:
  void foo()
  {
    cout << "Der::foo()" << endl;
  }
};

int main()
{
  cout << "Hello World!" << endl;
  Der d;
}

这个问题不是重复的。这里的问题具体在于为什么 this->foo() 能够正常工作,而通过强制类型转换调用则失败了。 - Agnel Kurian
2个回答

17

在构造函数内部,绝对不能调用虚函数。

虚函数的派发方式并不像你想象的那样。在构造期间,正在构造的基类子对象的动态类型是基础类型,因此函数被派发到基础函数(在你的情况下是纯虚函数)。

所以千万不要这么做。

原因很明显:在构造派生对象时,必须首先构造基类子对象,因此在基类构造期间环境中的派生对象根本不存在。


编辑:这里有更多的解释。如果编译器可以执行静态虚函数派发,它们将完全允许和鼓励这样做。在这种情况下,实际要调用的函数已经在编译时确定了。当你在 Base 构造函数中说 foo()this->foo(),或者在其他上下文中说 x.Base::foo(),其中 Derived x; 是你的对象时,就会发生这种情况。当派发静态进行时,要么直接调用 Base::foo() 的实现,要么如果没有实现的话就会得到一个链接器错误。

另一方面,如果派发是动态的,即在运行时进行,则有可能(虽然不太寻常)派发实际上最终选择了 Base::foo()。这种情况在“正常”情况下是不可能发生的,因为编译器不允许你实例化带有纯虚函数的类,所以普通动态派发的目标总是必须存在实现的函数(或者如果没有链接就会得到一个链接器错误)。

但还有一种情况,这就是问题所在:编译器决定在运行时执行派发,出于某种原因,派发在纯虚函数处结束。在这种情况下,您的程序会终止。无论函数是否已实现都无关紧要,但它在多态类层次结构中没有条目(将其视为“vtable中的空指针”,因此是= 0)。为了发生这种情况,对象的动态类型必须是抽象基类的类型,并且必须动态进行派发。前者只能在派生对象的基本构造函数内实现,而后者则需要您说服编译器不要静态地派发调用。这就是this->foo()(静态)和Base * p = this; p->foo();(动态)之间的区别。 (还要与x.Base::foo()进行对比,它是静态分派的。)
当然,所有这些都仅是实现的结果,并受到“未定义行为”的包容。如果您想从中学到一件事情,那就是动态分派无法找到纯虚函数。当然,您绝不能在构造函数内调用虚函数。

1
你可能想要添加一个链接到http://www.artima.com/cppsource/nevercall.html。 - Joachim Isaksson
1
只要知道虚函数的工作原理,就可以在构造函数中调用虚函数,这是完全可以的。因此,“永远不要”这个说法有点过于严厉了。但是,在这种情况下,被调用的函数是一个纯虚函数,在构造函数中调用纯虚函数是未定义行为。 - Alok Save
1
只要它不是一个纯虚函数,并且您知道您调用的是类本身的方法而不期望任何多态性,就可以调用虚函数。即使有实现,从构造函数中调用纯虚函数是未定义的行为。 - CashCow
我同意我们不应该使用它的原因。但我还是很好奇为什么这会调用纯虚函数,而指向相同类的指针却没有?编译器决定使用的因素是什么?尽管不是编译时错误。 - siddhusingh
@siddhusingh:这是发生的事情:当你构造一个派生类D继承自B时,首先发生的是构造B子对象,这以B的构造函数结束。此时,在B构造函数内部,this的类型为B*。接下来,构造D的所有成员,然后才执行D构造函数的主体,此时this的类型才是D*。如果在B构造函数内调用非纯虚函数,则最终会分派到B的实现。 - Kerrek SB
显示剩余3条评论

1

这不是你可能想要做的正确程序。

你需要进行两阶段构造,为此应该使用一个工厂对象来创建类,然后调用虚方法。

实际上可以从构造函数或析构函数中调用非纯虚函数,但是你必须知道将调用来自类本身的方法而不是任何多态的内容。

从构造函数中调用纯虚函数是未定义的行为。无论何时构造你的类,都会出现这种情况,无论是直接从构造函数中调用它(编译器可能能够捕获并警告),还是从构造函数调用的方法中调用它(这可能超出了编译器或链接器的范围)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接