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

我有时会注意到程序在我的计算机上崩溃,并出现错误: “纯虚函数调用”。

当不能从抽象类创建对象时,这些程序如何编译?

115616 次浏览

I'd guess there is a vtbl created for the abstract class for some internal reason (it might be needed for some sort of run time type info) and something goes wrong and a real object gets it. It's a bug. That alone should say that something that can't happen is.

纯属猜测

编辑: 看起来我在这个问题上是错的。有些语言允许构造函数析构函数调用 vtbl。

如果您尝试从构造函数或析构函数发出虚函数调用,则可能会出现这种情况。由于不能从构造函数或析构函数中调用虚函数(派生类对象尚未构造或已被销毁) ,因此它调用基类版本,在纯虚函数的情况下,基类版本不存在。

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


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


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

另见陈的 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();

As well as the standard case of calling a virtual function from the constructor or destructor of an object with pure virtual functions you can also get a pure virtual function call (on MSVC at least) if you call a virtual function after the object has been destroyed. Obviously this is a pretty bad thing to try and do but if you're working with abstract classes as interfaces and you mess up then it's something that you might see. It's possibly more likely if you're using referenced counted interfaces and you have a ref count bug or if you have an object use/object destruction race condition in a multi-threaded program... The thing about these kinds of purecall is that it's often less easy to fathom out what's going on as a check for the 'usual suspects' of virtual calls in ctor and dtor will come up clean.

为了帮助调试这些类型的问题,您可以在不同版本的 MSVC 中替换运行时库的 purerecreHandler 程序。为此,您可以使用以下签名提供自己的函数:

int __cdecl _purecall(void)

并在链接运行时库之前链接它。这使得你可以控制检测到 puretes 时会发生什么。一旦你有了控制权,你可以做一些比标准处理程序更有用的事情。我有一个处理程序,它可以提供一个堆栈跟踪,记录发生 puretewl 的位置; 请参阅这里: http://www.lenholgate.com/blog/2006/01/purecall.html了解更多细节。

(注意,您也可以调用 _ set _ puretre_ handler ()来在某些版本的 MSVC 中安装您的处理程序)。

我使用 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 */
};

如果你使用 Borland/CodeGear/Embarcadero/Idera C + + Builder,你可以直接实现

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

While debugging place a breakpoint in the code and see the callstack in the IDE, otherwise log the call stack in your exception handler (or that function) if you have the appropriate tools for that. I personally use MadExcept for that.

原始函数调用在[ C + + Builder ] Source cpprtl Source misc pureerr.cpp 中

我遇到了纯虚函数被调用的情况,因为被破坏的对象,Len Holgate已经有一个非常好的 answer,我想 to add some color with an example:

  1. 创建派生对象,并且指针(作为 Base 类)为 保存在某个地方
  2. 派生对象被删除,但指针不知何故被删除 仍然被引用
  3. The pointer which points to deleted Derived 对象被调用

派生类析构函数将 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

亮点:

如果对象被完全删除,意味着调用析构函数,并回收内存,我们可能只是得到一个 Segmentation fault,因为内存已经返回到操作系统,而程序只是无法访问它。所以这种“纯虚函数调用”的场景通常发生在对象被分配到内存池时,当一个对象被删除时,底层内存实际上并没有被操作系统回收,它仍然可以被进程访问。