C++ pure virtual method called报错,与原因分析
C++ pure virtual method called
项目场景:
代码集成三方库后,在程序即将退出时,报错崩溃,程序没有正常退出。提示如下错误: 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、先停止使用类对象指针进行多态调用的线程,再执行类对象的析构,留一个时间隔离度,
不要在析构函数里停止线程,因为停止线程可能会比较好使,导致析构时间比较长。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)