废话不多说,这个BUG也是自己写代码设计线程类,偶然之间触发的代码
#include <pthread.h>
#include <stdio.h>
class thread_base
{
pthread_t thread = -1;
static void* thread_entry(void* thiz)
{
((thread_base*)thiz)->run();
return NULL;
}
public:
void start()
{
pthread_create(&thread,NULL,thread_entry,this);
}
thread_base() {}
virtual void run() = 0;
virtual ~thread_base()
{
if (thread != -1)
pthread_join(thread,NULL);
}
};
class mythread : public thread_base
{
virtual void run() override
{
printf("Test
");
}
};
int main()
{
mythread thread;
thread.start();
}
这个代码其实也挺简单,就是一个线程对象先启动线程,然后马上在析构函数里面等待线程结束
但是,这样会导致程序终止,我在VSCode跑下来的结果是这样子的:
这里直接提示调用了纯虚函数,起初我也困惑为啥会这样,其实原理也很简单
既然是和虚函数有关,我们修改下代码,尝试打印虚表地址,修改之后的代码是这样,打印的地方看注释:
#include <pthread.h>
#include <stdio.h>
class thread_base
{
pthread_t thread = -1;
static void* thread_entry(void* thiz)
{
((thread_base*)thiz)->run();
return NULL;
}
public:
void start()
{
pthread_create(&thread,NULL,thread_entry,this);
}
thread_base() {}
virtual void run() = 0;
virtual ~thread_base()
{
//等待时的虚表地址
printf("virtual table address of this: %p
", *((void**)this));
if (thread != -1)
pthread_join(thread,NULL);
}
};
class mythread : public thread_base
{
virtual void run() override
{
printf("Test
");
}
};
int main()
{
mythread thread;
//执行前的虚表地址
printf("virtual table address of mythread: %p
", *((void**)&thread));
thread.start();
}
这个代码的运行结果是这样:

到了这一步已经很明显了,析构的地方虚表地址确实变了,然后执行了纯虚函数
但是为啥会这样,还有很多细节没有确认,这里就要用到一点操作系统的知识来解释,直接分析这个bug,代码的流程可以认为是这样:
1.类初始化,创建线程,但是线程并未马上得到调度,子类的run方法未执行,此时虚表地址正常,就是子类的地址
2.main函数马上结束,类对象析构,子类的析构先执行,然后是父类的析构,但是这里注意,到了父类的析构函数,虚表地址就被替换为父类的虚表地址,这个时候等待线程,让线程得到了调度,但是虚表地址变了,执行到了纯虚函数,程序终止
这里用到了一些操作系统知识和一些编译器的实现细节,其实这句话也印证了《Effective C++》里面的一条建议:不要在构造函数和析构函数里面调用虚函数,只不过我这里不是直接调用,而是因为线程调度间接调用虚函数,因为这个BUG挺有意思,特此记录下