编辑
2025-08-15
记录知识
0

目录

为什么会出现循环析构
解决问题
总结

最近在解一个bug,其在退出的时候会出现崩溃,这种崩溃全部指向析构。经过定位,发现如果QT的对象存在父子关系,是不能够在析构中强行delete这种对象,否则就会出现循环析构,这种循环析构就会导致应用崩溃。本文基于此问题分析,强调一下QT关于对象的编程注意事项

为什么会出现循环析构

关于为什么会出现循环析构,其理论出现的路径应该是如下:

  1. 首先,编写代码时将对象进行申请,并为其绑定了父对象
  2. 然后,某一次统一的内存泄漏合规性排查,发现其new和free不成对,但是忽略了QT本身的对象内存管理机制
  3. 于是乎为了满足表面语义的内存泄漏,强行在析构中进行内存删除
  4. 此时,代码看似内存申请和释放对称了,也就解决了内存泄漏问题

可以看到,在常规QT的编程中,如果代码编写者对QT内存对象管理不是很清晰的话,这种情况非常常见。

那么发现循环析构的方法也很明确,本例中,gdb调试的堆栈如下

(gdb) bt #0 UKUITaskBar::~UKUITaskBar (this=0x558c9a0310, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbar.cpp:214 #1 0x00000055717f6764 in UKUITaskBar::~UKUITaskBar (this=0x558c9a0310, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbar.cpp:214 #2 0x00000055718067ac in UKUITaskBarPlugin::~UKUITaskBarPlugin (this=0x558c987700, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbarplugin.cpp:42 #3 0x00000055718067dc in UKUITaskBarPlugin::~UKUITaskBarPlugin (this=0x558c987700, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbarplugin.cpp:40 #4 0x00000055717f64b4 in UKUITaskBar::~UKUITaskBar (this=0x558c9a0310, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbar.cpp:230 #5 0x00000055717f6764 in UKUITaskBar::~UKUITaskBar (this=0x558c9a0310, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbar.cpp:214 #6 0x00000055718067ac in UKUITaskBarPlugin::~UKUITaskBarPlugin (this=0x558c987700, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbarplugin.cpp:42 #7 0x00000055718067dc in UKUITaskBarPlugin::~UKUITaskBarPlugin (this=0x558c987700, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbarplugin.cpp:40 #8 0x00000055717f64b4 in UKUITaskBar::~UKUITaskBar (this=0x558c9a0310, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbar.cpp:230 #9 0x00000055717f6764 in UKUITaskBar::~UKUITaskBar (this=0x558c9a0310, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbar.cpp:214 #10 0x00000055718067ac in UKUITaskBarPlugin::~UKUITaskBarPlugin (this=0x558c987700, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbarplugin.cpp:42 #11 0x00000055718067dc in UKUITaskBarPlugin::~UKUITaskBarPlugin (this=0x558c987700, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbarplugin.cpp:40 ......

此时,第一时间发现这样的堆栈肯定很疑惑,明明代码只是如下操作,而为什么对象反复在析构呢?

if (m_allFrame) { delete m_allFrame; m_allFrame = nullptr; } if (m_placeHolder) { delete m_placeHolder; m_placeHolder = nullptr; }

这个其实就是QT的默认对象管理

  • 当某个对象声明了父对象,则其内存声明周期由父对象管理,只有父对象删除时,其本身对象才会得到删除。

上述的含义反过来理解就是

  • 如果父对象尚未销毁时,子对象被强行删除,那么当父对象销毁时,会寻找子对象进行销毁

此时子对象会销毁两次,问题也就出现了。

但是通常一个代码工程中,例子不可能这么简单,如果qt的对象绑定的父子关系比较深,那么在析构过程中,就会全部在同一个函数堆栈上析构,那么这样就看到了上图的函数堆栈。

#0 UKUITaskBar::~UKUITaskBar (this=0x558c9a0310, __in_chrg=<optimized out>) at ./plugin-taskbar/ukuitaskbar.cpp:214

解决问题

根据堆栈可以很明显找到代码位置为类UKUITaskBar的析构,那么定位代码非常容易,下面梳理一下对象关系

首先m_placeHolder是继承的QWidget,父对象是当前类,如下

m_placeHolder(new QWidget(this))

而m_placeHolder绑定了父对象是m_allFrame

m_allFrame->addWidget(m_placeHolder);

这里出现m_allFrame的父对象也是当前类

m_allFrame = new QWidget(this);

当前类是UKUITaskBar。

下面梳理一下父对象关系

UKUITaskBar--->m_placeHolder UKUITaskBar--->m_allFrame--->m_placeHolder

这种父子关系,意味着m_placeHolder和m_allFrame都可以不主动delete。那么一切正常。

如果我们在~UKUITaskBar中主动调用如下

delete m_placeHolder delete m_allFrame

那么可以梳理其回收的流程有几条线如下

  1. 默认走QT的对象管理,由QT处理每个对象释放(情况比较复杂)
  2. delete m_allFrame会主动释放m_allFrame和其子对象m_placeHolder
  3. delete m_placeHolder会主动释放自身

那么从m_placeHolder的角度,这样的代码m_placeHolder释放了3次。而在实际工程中,如果对象的嵌套比较深,这种双重释放会更加严重。

最后,解决问题很简单,只要理清楚了对象的关系,直接删除不必要的释放即可。

总结

这个例子本身很简单,但是问题又很容易出现。本文明确出来能够很好的提醒自己这类细节问题。