我有时会在我的电脑上注意到崩溃并出现错误信息:“纯虚函数调用”。
当一个抽象类无法创建对象时,这些程序是如何编译的呢?
我有时会在我的电脑上注意到崩溃并出现错误信息:“纯虚函数调用”。
当一个抽象类无法创建对象时,这些程序是如何编译的呢?
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
}
doIt()
调用很容易被静态地解析并分派到Base::doIt()
,从而导致链接错误。我们真正需要的是在动态分派期间动态类型是抽象基础类型的情况。 - Kerrek SBBase::Base
调用非虚拟的f()
,然后再调用(纯)虚拟的doIt
方法。 - Frerich Raabe除了在具有纯虚函数的对象的构造函数或析构函数中调用虚函数的标准情况外,如果在对象被销毁后调用虚函数,则也可能发生纯虚函数调用(至少在MSVC上是如此)。显然这是一件非常糟糕的事情,但如果你使用抽象类作为接口并且搞砸了,那么你可能会遇到这种情况。如果你正在使用引用计数的接口,并且有一个引用计数错误,或者在多线程程序中存在一个对象使用/对象销毁竞争条件,那么这种情况可能更容易发生...这些纯调用的问题在于往往不容易弄清楚发生了什么,因为在构造函数和析构函数中进行虚拟调用的检查通常会通过。
为了帮助调试这些问题,你可以在各个版本的MSVC中替换运行时库的purecall处理程序。你可以提供一个具有此签名的自定义函数:
int __cdecl _purecall(void)
在链接运行时库之前,链接对象文件。这样一来,您就可以控制检测到purecall时会发生什么了。一旦您掌握了控制权,就可以做比标准处理程序更有用的事情了。我有一个处理程序,可以提供纯虚函数调用发生位置的堆栈跟踪;请参见此处:http://www.lenholgate.com/blog/2006/01/purecall.html 以获取更多详细信息。__declspec(novtable)
优化(仅限于Microsoft),则通常在调用已删除实例的方法时发生的_purecall()
调用将不会发生。有了这个,完全有可能在对象被删除后调用重写的虚拟方法,这可能会掩盖问题,直到以其他形式咬你。_purecall()
陷阱是你的朋友! - Dave Ruske我遇到了这样一种情况,纯虚函数由于已销毁的对象而被调用,Len Holgate
已经有了一个非常好的回答,我想通过一个示例来进行说明:
Derived类的析构函数将vptr重置为Base类的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
要点:
如果对象被完全删除,也就是析构函数被调用并且回收了内存,那么我们可能会因为内存已经归还给操作系统,而在程序中无法访问它而导致分段错误
。因此,这种“纯虚函数调用”情况通常发生在对象在内存池上分配时,当对象被删除时,底层内存实际上并没有被操作系统回收,而是仍然可以被进程访问。
extern "C" void _RTLENTRY _pure_error_()
{
//_ErrorExit("Pure virtual function called");
throw Exception("Pure virtual function called");
}
我使用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 */
};
我猜测为了某些内部原因(可能需要某种运行时类型信息),抽象类会创建一个vtbl,然后出现了问题,真正的对象得到了它。这是一个错误。仅此就足以说明发生了不可能发生的事情。
纯属猜测
编辑:看起来在这个案例中我是错的。另一方面,如果我没记错,有些语言确实允许在构造函数析构函数中调用vtbl。
这里有一个巧妙的方法可以实现它。今天我基本上就遇到了这种情况。
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();
callFoo()
时才会调用纯虚函数,因为此时对象仍处于A阶段(或已经处于A阶段)。这里有一个运行版本,其中B b();
的语法错误已被修复-括号使其成为函数声明,而你需要的是一个对象。 - Wolf