项目场景:

代码集成三方库后,在程序即将退出时,报错崩溃,程序没有正常退出。提示如下错误: pure virtual method called terminate called without an active exception

使用gdb调试,查看程序堆栈

问题描述:

问题并不是必现,大多数情况下都能正常执行完,且异常基本都出现在程序即将退出时。崩溃时打印pure virtual method called
字面翻译是:纯虚函数被调用。

继续查找__cxa_pure_virtual函数,在gcc/cp/decl.c +4405中

最终在gcc/cp/class.c +9277,看到这样的描述,程序在执行时会给纯虚函数赋值,这个值就是__cxa_pure_virtual。

当你构造一个派生类的实例时,具体发生了什么?如果类包含虚函数表,过程会像下面这样:
第一步:构造最顶层的基类部分
a、让实例指向基类的虚函数表
b、构造基类实例成员变量
c、执行基类构造函数
第二步:构造派生部分(递归的)
a、让实例指向派生类的虚函数表
b、构造派生类实例成员变量
c、执行派生类构造函数
析构时则是按相反的顺序,就像这样:
第一步:析构派生部分(递归的)
a、(实例已经指向派生类的虚函数表)
b、执行派生类析构函数
c、析构派生类实例成员变量
第二步:析构基类部分(递归的)
a、让实例指向基类的虚函数表
b、执行基类析构函数
c、析构基类实例成员变量

看下下面代码中的例子,主要是打印了虚函数表的地址,来验证在构造和析构的过程中,实例分别指向了不同的虚函数表。

#include <iostream>
#include <thread>
#include <unistd.h>

using namespace std;
//extern "C" void __cxa_pure_virtual(){return ;};
//extern "C" void __cxa_pure_virtual(){while(1) ;};


typedef void(*Fun)(void);
struct base
{
        virtual void v_func()=0;
        virtual ~base();
        base();
};

base::base()
{
        cout << "base construct 虚函数表 — 的地址:" << (long long*)*(long long *)(this) << endl;
        //::usleep(200);
}

base::~base()
{
        cout << "base disconstruct 虚函数表 — 的地址:" << (long long*)*(long long *)(this) << endl;
         ::sleep(2);
        std::cout<<"base disconstruct enter"<<std::endl;
}
struct derived : public base
{
        virtual void v_func(){};
        derived();
        ~derived();
};

derived::derived(){
        cout << "derived construct 虚函数表 — 的地址:" << (long long*)*(long long *)(this) << endl;
        cout << "derived construct enter"<<std::endl;
}

derived::~derived()
{
        cout << "derived disconstrcut 虚函数表 — 的地址:" << (long long*)*(long long *)(this) << endl;
        std::cout<<"derived disconstruct enter"<<std::endl;
}

int main(){
        //derived *p1 =new derived;
        base *p1 =new derived;
        //std::thread t([&] () {
                        delete p1;
                        std::cout<<"p1==nullptr"<<std::endl;
                        p1=nullptr;
                       // });
        usleep(100);
                p1->v_func();
        //t.join();
        exit(0);
}

 输出结果如下:

base construct 虚函数表 — 的地址:0x563f3e44ed08
derived construct 虚函数表 — 的地址:0x563f3e44ece0
derived construct enter
derived disconstrcut 虚函数表 — 的地址:0x563f3e44ece0
derived disconstruct enter
base disconstruct 虚函数表 — 的地址:0x563f3e44ed08
base disconstruct enter
p1==nullptr
Segmentation fault (core dumped)

从结果可以看出,基类的构造和析构的调用过程中,都是指向了一个虚函数表,

而在子类的构造和析构的过程中,也都指向了另外的一个虚函数表,是两个不同的虚函数表。

总结:

1.调用delete后,以及智能指针的reset后,会马上调用析构函数,析构函数全部结束后,才会走下面的代码。

2.上面的代码是我delete指针后,都已经是nullptr了再调用空指针造成的。

3.我现在要复现下,pure virtual method called报错,代码如下:我只是修改下main,然其单独启动线程进行delete,然后还没有执行到  p1=nullptr,因为在基类的析构函数里睡了2s.也是故意的营造的这种环境。

int main(){
        //derived *p1 =new derived;
        base *p1 =new derived;
        std::thread t([&] () {
                        delete p1;
                        std::cout<<"p1==nullptr"<<std::endl;
                        p1=nullptr;
                        });
        usleep(100);
                p1->v_func();
        t.join();
        exit(0);
}

这个时候就复现了标题中的错误如下:

base construct 虚函数表 — 的地址:0x55fe3ed58ca8
derived construct 虚函数表 — 的地址:0x55fe3ed58c58
derived construct enter
derived disconstrcut 虚函数表 — 的地址:0x55fe3ed58c58
derived disconstruct enter
base disconstruct 虚函数表 — 的地址:0x55fe3ed58ca8
pure virtual method called
terminate called without an active exception
Aborted (core dumped)

现在总结下:这种错误的原因:

主要有两种,第一种一般不常见:

第一种:

1、在基类的构造函数里直接调用虚函数
2、在基类的析构函数里直接调用虚函数
3、 在基类的构造函数里间接调用虚函数
4、在基类的析构函数里间接调用虚函数

第二种:

根据上面的流程,构造函数和析构函数执行过程中,都有一段时间对象的虚函数指针指向基类虚函数表,如果在构造或者析构没有完成的时候调用了该对象的虚函数,则是调用了基类的纯虚函数。这种情况一般发生在多线程调用,构造或析构在一个线程,虚函数调用在另一个线程。

如果父类的函数不是纯虚函数,而是有实现的虚函数,则是调用父类的虚函数,不会crash,只是达不到多态的效果。

解决思路针对第二种

1、在调用对象的函数时,对象指针进行有效性判断(p==nullptr);

但是有的时候,因为析构函数的耗时,导致p1=nullptr没有执行到,所以可能还是解决不了问题。
2、不要再构造函数和析构函数中执行睡眠操作;

因为这个影响了构造函数,析构函数的快速执行,有可能卡在父类里出不来,导致其他线程在使用对象调用子类的虚函数时候,正好虚表指针指向虚基类,导致出现问题。
3、对对象指针加锁,然后再判断指针对象是否为空;

就是在上面的例子中,在下面两个地方加锁.

int main(){
        //derived *p1 =new derived;
        base *p1 =new derived;
        std::thread t([&] () {
                        {
                        autolock lock;
                        delete p1;
                        std::cout<<"p1==nullptr"<<std::endl;
                        p1=nullptr;
                        }
                        
                        });
        usleep(100);
                {
                autolock lock;
                if(p1!=nullptr)
                    p1->v_func();
                }
                
        t.join();
        exit(0);
}


4、先停止使用类对象指针进行多态调用的线程,再执行类对象的析构,留一个时间隔离度,

不要在析构函数里停止线程,因为停止线程可能会比较好使,导致析构时间比较长。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐