关于c ++:“纯虚函数调用”崩溃来自何处?

关于c ++:“纯虚函数调用”崩溃来自何处?

Where do “pure virtual function call” crashes come from?

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

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


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

(在这里查看现场演示)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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处理程序。您可以通过使用此签名提供自己的功能来执行此操作:

1
int __cdecl _purecall(void)

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

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


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

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


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

  • 创建Derived对象,并指针(作为Base类)
    保存在某个地方
  • Derived对象被删除,但指针是以某种方式
    仍被引用
  • 指向已删除Derived的指针
    对象被调用
  • Derived类析构函数将vptr点重置为具有纯虚函数的Base类vtable,因此当我们调用虚函数时,它实际调用纯虚函数。

    这可能是因为明显的代码错误,或多线程环境中复杂的竞争条件情况。

    这是一个简单的例子(关闭优化的g ++编译 - 一个简单的程序可以很容易地优化掉):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
     #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();
     }

    堆栈跟踪看起来像:

    1
    2
    3
    4
    5
    6
    7
    #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被回收,我们可能只是在内存返回操作系统时得到Segmentation fault,程序就无法访问它。所以这个"纯虚函数调用"场景通常发生在对象在内存池上分配时,一个对象被删除,底层内存实际上没有被OS回收,它仍然可以被进程访问。


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

    1
    2
    3
    4
    5
    extern"C" void _RTLENTRY _pure_error_()
    {
        //_ErrorExit("Pure virtual function called");
        throw Exception("Pure virtual function called");
    }

    调试时在代码中放置一个断点并在IDE中查看callstack,否则如果你有适当的工具,则在你的异常处理程序(或那个函数)中记录调用堆栈。我个人使用MadExcept。

    PS。原始函数调用位于[C ++ Builder] source cpprtl Source misc pureerr.cpp中


    我使用VS2010,每当我尝试直接从公共方法调用析构函数时,我在运行时遇到"纯虚函数调用"错误。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <typename T>
    class Foo {
    public:
      Foo< T >() {};
      ~Foo< T >() {};

    public:
      void SomeMethod1() { this->~Foo(); }; /* ERROR */
    };

    所以我把里面的内容~Foo()移动到单独的私有方法,然后它就像一个魅力。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template <typename T>
    class Foo {
    public:
      Foo< T >() {};
      ~Foo< T >() {};

    public:
      void _MethodThatDestructs() {};
      void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
    };


    我猜有一个为抽象类创建的vtbl由于某些内部原因(它可能需要某种运行时类型信息)并且出现问题并且真实对象得到它。这是一个错误。仅此一点应该说不可能发生的事情是。

    纯粹的猜测

    编辑:看起来我在这个案子中错了。 OTOH IIRC有些语言允许vtbl调用构造函数析构函数。


    这是一种偷偷摸摸的方式。我今天基本上发生了这件事。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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();

    推荐阅读