“纯虚函数调用”崩溃从何而来?


Answers:


107

如果您尝试从构造函数或析构函数进行虚拟函数调用,则可能会导致它们。由于您不能从构造函数或析构函数进行虚拟函数调用(派生类对象尚未构建或已被销毁),因此它将调用基类版本,在纯虚拟函数的情况下,它不会不存在。

在此处查看现场演示)

class Base
{
public:
    Base() { doIt(); }  // DON'T DO THIS
    virtual void doIt() = 0;
};

void Base::doIt()
{
    std::cout<<"Is it fine to call pure virtual function from constructor?";
}

class Derived : public Base
{
    void doIt() {}
};

int main(void)
{
    Derived d;  // This will cause "pure virtual function call" error
}

3
一般而言,编译器无法捕捉到此任何原因?
托马斯

21
通常情况下无法捕获它,因为来自ctor的流可以流到任何地方,任何地方都可以调用纯虚函数。这是暂停问题
101。– shoosh

9
答案略有错误:仍然可以定义纯虚函数,有关详细信息,请参阅Wikipedia。正确的措词:可能不存在
-MSalters

5
我认为这个示例过于简单:doIt()构造函数中的调用很容易被虚拟化并Base::doIt()静态分配,这只会导致链接器错误。我们真正需要的是一种情况,其中动态调度期间的动态类型是抽象基本类型。
Kerrek SB 2012年

2
如果您添加了额外的间接级别,则可以使用MSVC触发此事件:已Base::Base调用非虚拟方法f(),后者又调用了(纯)虚拟doIt方法。
Frerich Raabe 2014年

64

与从具有纯虚函数的对象的构造函数或析构函数中调用虚函数的标准情况一样,如果在对象被销毁后调用虚函数,则还可以获得纯虚函数调用(至少在MSVC上) 。显然,这是一件很不好的尝试,但是如果您使用抽象类作为接口并且搞砸了,那么您可能会发现。如果您使用的是引用计数接口,并且有引用计数错误,或者在多线程程序中有对象使用/对象破坏竞争的情况,则这种可能性更大。关于这些purecall的事情是通常不容易弄清发生了什么,因为检查ctor和dtor中虚拟调用的“通常可疑对象”会变得干净。

为了帮助调试此类问题,您可以在各种版本的MSVC中替换运行时库的purecall处理程序。您可以通过为自己的函数提供以下签名来做到这一点:

int __cdecl _purecall(void)

和链接它,然后再链接运行时库。这使您可以控制检测到纯调用时发生的情况。一旦有了控制权,您就可以执行比标准处理程序更有用的操作。我有一个处理程序,可以提供纯调用发生位置的堆栈跟踪;有关更多详细信息,请参见此处:http : //www.lenholgate.com/blog/2006/01/purecall.html

(请注意,您也可以调用_set_purecall_handler()在某些版本的MSVC中安装处理程序)。


1
感谢您提供有关在已删除实例上进行_purecall()调用的指示;我没有意识到这一点,只是通过一些测试代码向我证明了这一点。看着WinDbg中的一个事后转储,我以为我正在处理一个竞赛,在该竞赛中另一个线程在完全构造派生对象之前试图使用派生对象,但这为该问题提供了新的亮点,并且似乎更符合证据。
Dave Ruske'3

1
我还要添加的另一件事是:如果已经通过优化声明了基类(特定于Microsoft)_purecall(),则通常不会在调用已删除实例的方法时发生调用__declspec(novtable)。这样一来,完全有可能在删除对象后调用覆盖的虚拟方法,这可能会掩盖问题,直到以其他形式咬住您。该_purecall()陷阱是你的朋友!
Dave Ruske 2015年

了解Dave很有用,最近我见过一些情况,当我以为应该打来电话的时候,我并没有打过电话。也许我对这种优化感到反感。
Len Holgate 2015年

1
@LenHolgate:非常有价值的答案。这恰好是我们的问题案例(由比赛条件引起的错误裁判计数)。非常感谢您为我们指明了正确的方向(我们怀疑是v-table损坏,然后疯狂地试图寻找罪魁祸首)
BlueStrat

7

通常,当您通过悬空指针调用虚拟函数时,很可能实例已被破坏。

也可能有更多的“创造性”原因:也许您已经设法将对象的实现虚拟功能的部分切开了。但是通常只是实例已被销毁。


4

我遇到了这样的情况:由于损坏的对象而调用了纯虚函数,Len Holgate已经有了一个很好的答案,我想用一个示例添加一些颜色:

  1. 创建派生对象,并将指针(作为基类)保存在某处
  2. 派生对象已删除,但仍以某种方式引用了指针
  3. 指向删除的派生对象的指针被调用

派生类析构函数将vptr点重置为具有纯虚函数的基类vtable,因此,当我们调用虚函数时,它实际上会调用纯虚函数。

发生这种情况的原因可能是明显的代码错误,或者是多线程环境中竞争条件的复杂场景。

这是一个简单的示例(关闭优化功能的g ++编译-简单的程序可以轻松地优化掉):

 #include <iostream>
 using namespace std;

 char pool[256];

 struct Base
 {
     virtual void foo() = 0;
     virtual ~Base(){};
 };

 struct Derived: public Base
 {
     virtual void foo() override { cout <<"Derived::foo()" << endl;}
 };

 int main()
 {
     auto* pd = new (pool) Derived();
     Base* pb = pd;
     pd->~Derived();
     pb->foo();
 }

堆栈跟踪如下所示:

#0  0x00007ffff7499428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007ffff749b02a in __GI_abort () at abort.c:89
#2  0x00007ffff7ad78f7 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007ffff7adda46 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007ffff7adda81 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007ffff7ade84f in __cxa_pure_virtual () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x0000000000400f82 in main () at purev.C:22

突出:

如果对象被完全删除,这意味着调用了析构函数,并且回收了memroy,Segmentation fault那么当内存返回到操作系统时,我们可能会简单地得到a ,而程序则无法访问它。因此,这种“纯虚函数调用”场景通常发生在将对象分配到内存池上的情况下,而删除对象时,底层内存实际上并没有被OS回收,它仍然可以被进程访问。


0

我猜是由于某种内部原因(为某种运行时类型信息可能需要)为抽象类创建了一个vtbl,并且出现问题并由实际对象获取。这是一个错误。仅此而已就意味着不可能发生的事情。

纯粹的猜测

编辑:看起来我在有关案件中错了。OTOH IIRC某些语言允许vtbl调用出构造函数析构函数。


如果那是您的意思,那不是编译器中的错误。
托马斯

您的猜想是对的-C#和Java允许这样做。在这些语言中,正在构建的对象确实具有最终类型。在C ++中,对象在构造期间会更改类型,这就是为什么以及何时可以使对象具有抽象类型。
MSalters

所有抽象类和从其派生的实际对象都需要一个vtbl(虚拟函数表),列出应在其上调用的虚拟函数。在C ++中,对象负责创建自己的成员,包括虚拟函数表。构造函数是从基类调用到派生的,而析构函数是从派生调用到基类的,因此在抽象基类中,虚拟函数表尚不可用。
FuzzyTew 2009年

0

我使用VS2010,每当尝试直接从公共方法调用析构函数时,在运行时都会收到“纯虚函数调用”错误。

template <typename T>
class Foo {
public:
  Foo<T>() {};
  ~Foo<T>() {};

public:
  void SomeMethod1() { this->~Foo(); }; /* ERROR */
};

因此,我将〜Foo()内部的内容移到了单独的私有方法中,然后它像一个魅力一样起作用了。

template <typename T>
class Foo {
public:
  Foo<T>() {};
  ~Foo<T>() {};

public:
  void _MethodThatDestructs() {};
  void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
};

0

如果您使用Borland / CodeGear / Embarcadero / Idera C ++ Builder,则可以实施

extern "C" void _RTLENTRY _pure_error_()
{
    //_ErrorExit("Pure virtual function called");
    throw Exception("Pure virtual function called");
}

在调试时,在代码中放置一个断点,并在IDE中查看调用堆栈,否则,如果您具有相应的工具,则将调用堆栈记录在异常处理程序(或该函数)中。我个人为此使用MadExcept。

PS。原始函数调用位于[C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cpp中


-2

这是一种偷偷摸摸的实现方式。今天我基本上发生了这种情况。

class A
{
  A *pThis;
  public:
  A()
   : pThis(this)
  {
  }

  void callFoo()
  {
    pThis->foo(); // call through the pThis ptr which was initialized in the constructor
  }

  virtual void foo() = 0;
};

class B : public A
{
public:
  virtual void foo()
  {
  }
};

B b();
b.callFoo();

1
至少不能在我的vc2008上复制它,当在A的构造函数中首次初始化时,vptr确实指向A的vtable,但是当B完全初始化时,vptr更改为指向B的vtable,这没关系
Huang

用vs2010 / 12来重现它
-makc

I had this essentially happen to me today显然不是正确的,因为根本就是错误的:纯虚函数仅在callFoo()构造函数(或析构函数)中被调用时才被调用,因为此时对象仍然(或已经)处于A阶段。这是代码的运行版本,其中没有语法错误B b();-括号使它成为函数声明,需要一个对象。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.