最近在解一个bug,其在退出的时候会出现崩溃,这种崩溃全部指向析构。经过定位,发现如果QT的对象存在父子关系,是不能够在析构中强行delete这种对象,否则就会出现循环析构,这种循环析构就会导致应用崩溃。本文基于此问题分析,强调一下QT关于对象的编程注意事项
关于为什么会出现循环析构,其理论出现的路径应该是如下:
可以看到,在常规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
那么可以梳理其回收的流程有几条线如下
那么从m_placeHolder的角度,这样的代码m_placeHolder释放了3次。而在实际工程中,如果对象的嵌套比较深,这种双重释放会更加严重。
最后,解决问题很简单,只要理清楚了对象的关系,直接删除不必要的释放即可。
这个例子本身很简单,但是问题又很容易出现。本文明确出来能够很好的提醒自己这类细节问题。