“纯虚函数调用”崩溃从何而来?
-
01-07-2019 - |
题
我有时会注意到我的计算机上的程序崩溃并出现以下错误:“纯虚函数调用”。
当无法创建抽象类的对象时,这些程序如何编译?
解决方案
如果您尝试从构造函数或析构函数进行虚拟函数调用,则可能会出现这种情况。由于无法从构造函数或析构函数进行虚函数调用(派生类对象尚未构造或已被销毁),因此它会调用基类版本,在纯虚函数的情况下,基类版本不会不存在。
(参见现场演示 这里)
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
}
其他提示
除了从具有纯虚函数的对象的构造函数或析构函数调用虚函数的标准情况外,如果在对象被销毁后调用虚函数,您还可以获得纯虚函数调用(至少在 MSVC 上) 。显然,这是一件非常糟糕的事情,但是如果您使用抽象类作为接口并且搞砸了,那么您可能会看到这种情况。如果您使用引用计数接口并且存在引用计数错误,或者如果您在多线程程序中存在对象使用/对象销毁竞争条件,则更有可能...关于这些类型的 purecall 的问题是,通常不太容易弄清楚发生了什么,因为检查 ctor 和 dtor 中虚拟调用的“常见嫌疑人”将会得到澄清。
为了帮助调试此类问题,您可以在不同版本的 MSVC 中替换运行时库的 purecall 处理程序。您可以通过提供带有此签名的您自己的函数来实现此目的:
int __cdecl _purecall(void)
并在链接运行时库之前链接它。这使您可以控制检测到 purecall 时发生的情况。一旦获得控制权,您就可以做一些比标准处理程序更有用的事情。我有一个处理程序,可以提供 purecall 发生位置的堆栈跟踪;看这里: http://www.lenholgate.com/blog/2006/01/purecall.html 更多细节。
(请注意,您还可以调用 _set_purecall_handler() 在某些版本的 MSVC 中安装处理程序)。
通常,当您通过悬空指针调用虚拟函数时,该实例很可能已经被销毁。
也可能有更多“创造性”的原因:也许您已经成功地切掉了对象中实现虚拟函数的部分。但通常只是实例已经被销毁了。
我猜想出于某种内部原因(可能需要某种运行时类型信息)为抽象类创建了一个 vtbl,并且出现了问题,并且真实的对象获取了它。这是一个错误。仅此一点就应该说明不可能发生的事情是。
纯属猜测
编辑: 看来我在有关情况下错了。OTOH IIRC 有些语言确实允许从构造函数析构函数中调用 vtbl。
我使用 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");
}
调试时在代码中放置断点并在 IDE 中查看调用堆栈,否则如果您有适当的工具,则将调用堆栈记录在异常处理程序(或该函数)中。我个人使用 MadExcept 来实现这一点。
附言。原始函数调用位于 [C++ Builder]\source\cpprtl\Source\misc\pureerr.cpp
我遇到了由于对象被破坏而调用纯虚函数的情况, Len Holgate
已经有一个很好的答案,我想在一个示例中添加一些颜色:
- 创建了一个派生对象,并且指针(作为基类)保存在某个地方
- 派生的对象已删除,但仍引用指针
- 指向删除派生对象的指向指向的指针被调用
派生类的析构函数重置了vptr指向基类的vtable,基类的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
因为内存已返回操作系统,程序无法访问它。因此,这种“纯虚函数调用”场景通常发生在内存池上分配对象时,当对象被删除时,底层内存实际上没有被操作系统回收,它仍然可以被进程访问。
这里有一个偷偷摸摸的方法来实现它。我今天基本上就遇到了这种情况。
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();