"纯虚函数调用"崩溃来自何处?

Bri*_*ndy 104 c++ polymorphism virtual-functions pure-virtual

我有时会注意到计算机崩溃的程序出现错误:"纯虚函数调用".

当无法创建抽象类的对象时,这些程序如何编译?

Ada*_*eld 106

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

(在这里查看现场演示)

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
}
Run Code Online (Sandbox Code Playgroud)

  • 在一般情况下无法捕获它,因为来自ctor的流可以随时随地调用纯虚函数.这是暂停问题101. (21认同)
  • 答案有点错误:仍然可以定义纯虚函数,有关详细信息,请参阅Wikipedia.正确的措辞:*可能*不存在 (9认同)
  • 我认为这个例子太简单了:构造函数中的`doIt()`调用很容易被虚拟化并静态分派到`Base :: doIt()`,这只会导致链接器错误.我们真正需要的是动态调度*期间动态类型*是抽象基类型的情况. (5认同)
  • 一般来说编译器无法捕获这个的任何原因? (3认同)
  • GCC 只给我一个警告: test.cpp: In constructor 'Base::Base()': test.cpp:4: warning: abstract virtual 'virtual void Base::doIt()' called from constructor but it failed at link时间。 (2认同)
  • 如果你添加一个额外的间接级别,可以用MSVC触发:让`Base :: Base`调用一个非虚拟的`f()`,然后调用(纯)虚拟`doIt`方法. (2认同)
  • 从构造函数中调用非纯虚函数是完全可以的。标准明确[允许](http://eel.is/c++draft/class.cdtor#4)。当然,调用纯虚函数是不行的(没有意义)。 (2认同)
  • @Romeno:从抽象类的构造函数或析构函数调用纯成员虚函数(直接或间接)是未定义的行为。“因‘纯虚函数调用’错误而崩溃”和“无明显效果”都是编译器在这种情况下生成的有效行为,正是因为该行为未由语言标准定义。 (2认同)

Len*_*ate 64

除了从具有纯虚函数的对象的构造函数或析构函数调用虚函数的标准情况之外,如果在对象被销毁后调用虚函数,则还可以获得纯虚函数调用(至少在MSVC上) .显然,尝试这样做是一件非常糟糕的事情,但是如果你正在使用抽象类作为接口而你搞砸了,那么你可能会看到它.如果您使用引用的计数接口并且您有一个引用计数错误或者如果您在多线程程序中有对象使用/对象破坏竞争条件,则可能更有可能...关于这些类型的纯粹调用的事情是它的通常不太容易理解正在发生的事情,因为检查ctor和dtor中虚拟呼叫的"常见嫌疑人"会变得干净.

为了帮助调试这些类型的问题,您可以在各种版本的MSVC中替换运行时库的purecall处理程序.您可以通过使用此签名提供自己的功能来执行此操作:

int __cdecl _purecall(void)
Run Code Online (Sandbox Code Playgroud)

并在链接运行时库之前链接它.这使您可以控制检测到纯调用时发生的情况.一旦掌握了控制权,就可以做一些比标准处理程序更有用的事情.我有一个处理程序,可以提供purecall发生位置的堆栈跟踪; 请参阅此处:http://www.lenholgate.com/blog/2006/01/purecall.html了解更多详情.

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

  • 感谢您提供有关在已删除实例上调用 _purecall() 的指针;我没有意识到这一点,只是用一些测试代码向自己证明了这一点。查看 WinDbg 中的事后转储,我以为我正在处理一场竞赛,其中另一个线程在完全构造之前尝试使用派生对象,但这为问题提供了新的线索,并且似乎更符合证据。 (2认同)
  • @LenHolgate:非常有价值的答案。这正是我们的问题案例(由竞争条件引起的错误引用计数)。非常感谢你为我们指明了正确的方向(我们怀疑 v-table 损坏,并疯狂地试图找到罪魁祸首代码) (2认同)

Bra*_*den 7

通常当您通过悬空指针调用虚函数时 - 很可能该实例已被销毁.

还有更多"创造性"的原因:也许你已经设法切掉了实现虚拟功能的对象部分.但通常只是实例已经被破坏了.


Bai*_*ang 6

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

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

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();
 }
Run Code Online (Sandbox Code Playgroud)

堆栈跟踪如下所示:

#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
Run Code Online (Sandbox Code Playgroud)

强调:

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