我有时会注意到程序在我的计算机上崩溃并出现错误:“纯虚函数调用”。
当无法从抽象类创建对象时,这些程序如何编译?
我有时会注意到程序在我的计算机上崩溃并出现错误:“纯虚函数调用”。
当无法从抽象类创建对象时,这些程序如何编译?
如果您尝试从构造函数或析构函数进行虚函数调用,则可能会导致它们。由于您不能从构造函数或析构函数调用虚函数(派生类对象尚未构造或已被销毁),因此它调用基类版本,在纯虚函数的情况下,不会不存在。
(在这里查看现场演示)
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 上) . 显然,这是一件非常糟糕的事情,但如果您将抽象类作为接口使用并且您搞砸了,那么您可能会看到它。如果您使用引用的计数接口并且您有一个引用计数错误,或者如果您在多线程程序中存在对象使用/对象销毁竞争条件,则可能更有可能......关于这些类型的纯调用的事情是通常不太容易弄清楚发生了什么,因为对 ctor 和 dtor 中虚拟呼叫的“通常嫌疑人”的检查会变得干净。
为了帮助调试这些类型的问题,您可以在各种版本的 MSVC 中替换运行时库的 purecall 处理程序。您可以通过使用此签名提供您自己的函数来做到这一点:
int __cdecl _purecall(void)
并在链接运行时库之前链接它。这使您可以控制检测到纯调用时发生的情况。一旦你有了控制权,你就可以做一些比标准处理程序更有用的事情。我有一个处理程序,可以提供纯调用发生位置的堆栈跟踪;请参阅此处: http: //www.lenholgate.com/blog/2006/01/purecall.html了解更多详情。
(请注意,您也可以调用 _set_purecall_handler() 在某些版本的 MSVC 中安装您的处理程序)。
我遇到了纯虚函数因为对象被破坏而被调用的场景,Len Holgate
已经有一个很好的答案,我想用一个例子添加一些颜色:
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
强调:
如果对象被完全删除,这意味着析构函数被调用,并且 memroy 被回收,我们可能会简单地得到 a Segmentation fault
,因为内存已经返回给操作系统,而程序就无法访问它。所以这种“纯虚函数调用”的场景通常发生在对象被分配到内存池上,而对象被删除时,底层内存实际上并没有被操作系统回收,它仍然可以被进程访问。
通常当你通过一个悬空指针调用一个虚函数时——很可能该实例已经被销毁了。
也可能有更多“创造性”的原因:也许您已经设法切掉了实现虚函数的对象部分。但通常只是实例已经被销毁。
我猜出于某种内部原因(可能需要某种运行时类型信息)为抽象类创建了一个 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。
PS。原始函数调用在 [C++ Builder]\source\cpprtl\Source\misc\pureerr.cpp
这是它发生的一种偷偷摸摸的方式。我今天基本上发生了这种情况。
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();