[C++] Destructor Order
导言
- 一般来说,析构函数应该只处理释放资源,不处理逻辑。
- 但是PTA的代码里,在全局变量的析构函数里,写了向子线程发送结束信号的函数,和
childThread.join()
。这导致了很奇怪的问题,string demalloc等。
为此,想研究一下C++的析构函数执行顺序。包括嵌套的Class结构,和全局变量的析构时机。
顺序结构
- 全局变量是静态变量,是后构造的先析构。
- Class的多个子类成员,其析构顺序是按声明顺序来执行的。
Class 内成员
1 | class SubClass1 { |
嵌套结构
基本析构顺序是,内部先析构,再析构外部。
类的继承关系
当存在继承关系时,析构函数的调用顺序确实是先调用派生类的析构函数,再调用基类的析构函数。这样做的目的是确保派生类对象能够完全销毁自己在其析构函数中创建的资源,然后基类才会处理自己部分的资源。
以下是一个简单的例子来展示这种析构顺序:
示例代码:
1 | #include <iostream> |
输出结果:
1 | Creating Derived object... |
解释:
- 派生类对象创建:
- 创建
Derived
类型的对象时,首先会调用 基类Base
的构造函数。 - 然后调用 派生类
Derived
的构造函数。
- 派生类对象销毁:
- 当
Derived
对象超出作用域时,首先会调用 派生类Derived
的析构函数。 - 然后,派生类的析构函数结束后,才会调用 基类
Base
的析构函数。
关键点:
- 析构函数调用顺序:基类的析构函数会在派生类析构函数之后被调用。这样做确保派生类可以完全处理自己特有的资源释放,基类的析构函数才会处理基类部分的资源。
- 虚拟析构函数:在基类中声明析构函数为
virtual
是非常重要的,因为这样做能够确保在销毁派生类对象时,正确调用派生类和基类的析构函数。如果析构函数不是虚拟的,则可能会导致析构时基类的析构函数不被调用,从而引发内存泄漏或资源释放不完全的情况。
全局变量
- 静态变量的析构会在主线程结束时立刻执行。
thread
变量并不会阻拦主线程的结束,和之后全局变量的析构;thread
的变量的作用是通过thread.join()
来阻塞主进程,如果不阻塞,则主线程析构到thread
的变量时会发现子线程没结束,报错abort()
1 | (gdb) bt |
实例分析
PTA多线程实例解释
PTA多线程在结束时的行为是:
- 主线程(python)已结束
- 二级流水线程 在等待信号
- Release线程 在轮询检查释放资源
按照前面的逻辑,这程序应该会正常结束:
- 子线程等待主线程释放信号;
- 主线程完成代码执行后析构,释放信号,并在join 处等待子线程。
实际情况是:
- 对于未修改代码,主线程正常析构,释放信号,子线程正常结束。
- 但是我加上了cpprinter后,主线程不会析构,也就不会释放信号,子线程就无限循环。
1 | #0 0x00007f21211bdbbf in __GI___poll (fds=0x7f20d3190bc0, nfds=1, timeout=timeout@entry=-1) at ../sysdeps/unix/sysv/linux/poll.c:29 |
主线程卡在python的调用栈里,没有触发析构,
析构函数里添加CPPRINTER
会有如下各种随机:
string seg fault
filesystem abort
std::cerr 打印5000+行的未知数据,看输出应该,原本要打印的static变量被释放了,把栈信息打印出来了。
猜测原因是CPPRINTER写的比较复杂,启动析构时环境已经开始销毁了,导致各种随机错误。
解决方案(测试):析构函数里调用简单的,不涉及static变量的函数。
多方测试
由于陷入了矛盾的困境,需要小实验来验证。
多线程时,子线程循环调用全局变量时,全局变量析构的时机
???