Qt_一个由单例引发的崩溃
文章目录
关键字: Qt
、 Q_GLOBAL_STATIC
、 单例
、 UI
、 崩溃
摘要
今天简直是令人心力交瘁的一天,在公司被一个顽固的Bug纠缠了整整一天。一开始,我对这个问题的认知并不深刻,只是觉得有点小瑕疵,于是比较轻松地着手解决。我开始摸索着定位问题,态度上也没太在意,毕竟在我看来,这只是一场小小的技术挑战。
然而,随着时间的推移,我逐渐意识到问题的严重性。逐渐加深的烦躁和困扰让我开始感到不安。在一度对问题轻描淡写的态度下,我终于被迫正视这个Bug所可能引发的连锁反应。随着这个问题的逐渐显露出其庞大的影响,我仿佛看到了一个漩涡,正在悄然蔓延着,威胁着整个系统的稳定性。
随着深入的调查和排查,我开始逐渐理解这个Bug的神秘本质,而这个认识过程伴随着我的焦虑逐渐升温。我发现这并非是一个简单的技术故障,而是一个可能牵动公司正常运营的重大问题。这时,我终于感受到了来自责任的重压,因为解决这个问题不仅关乎我的个人技术能力,更涉及到公司业务的持续稳健。
最终,当我对问题的了解达到顶峰时,我不禁意识到这一天的崩溃并非仅限于我的个人情绪,更是对整个系统运行稳定性的一次极大考验。这场对抗Bug的战斗让我感受到了技术领域的不可预测性和挑战性,也让我更加深刻地明白在这个领域中的学无止境。
关于 Q_GLOBAL_STATIC
我在创建单例上偷了懒,使用了Qt Q_GLOBAL_STATIC
宏来创建单例。关于Q_GLOBAL_STATIC
的解释如下:
Q_GLOBAL_STATIC
是 Qt 框架中用于创建全局静态变量的宏。这个宏的目的是确保在多线程环境下安全地初始化和访问静态变量。在 C++ 中,全局静态变量的初始化顺序可能会带来一些问题,特别是在多线程环境下。
使用 Q_GLOBAL_STATIC
宏,Qt 提供了一种线程安全的机制来创建全局静态变量。这个宏可以确保静态变量只在第一次访问时被初始化,而且在初始化过程中是线程安全的。
具体用法如下:
cpp
Q_GLOBAL_STATIC(Type, variable)
其中,Type
是要创建的静态变量的类型,variable
是变量的名称。
举例说明:
cpp
#include <QGlobalStatic>
class MyClass {
public:
MyClass() {
// 构造函数
}
// 其他成员函数和数据成员
};
Q_GLOBAL_STATIC(MyClass, myGlobalInstance)
在上面的例子中,myGlobalInstance
就是一个全局静态变量,它的类型是 MyClass
。使用 Q_GLOBAL_STATIC
宏,Qt 会在需要时确保 myGlobalInstance
被安全地初始化,并在整个程序运行期间保持其存在。
这种机制的好处是,在多线程环境下,多个线程可以同时访问 myGlobalInstance
,而不必担心因为初始化顺序而导致的问题。Qt 在内部使用了一些技巧,比如使用双重检查锁定(double-checked locking)等,以确保在多线程环境下的性能和正确性。
代码测试
所以,按照上面的解释,我使用Q_GLOBAL_STATIC
来创建一个单例,应该是没有问题的。但是。
下面看下的Demo 代码
头文件:
cpp
#ifndef FORM_H
#define FORM_H
#include <QWidget>
namespace Ui {
class Form;
}
class Form : public QWidget
{
Q_OBJECT
public:
explicit Form(QWidget *parent = nullptr);
static Form* getInstance();
~Form();
private:
Ui::Form *ui;
};
#endif // FORM_H
源文件:
cpp
#include "form.h"
#include "ui_form.h"
Q_GLOBAL_STATIC(Form,m_Form)
Form::Form(QWidget *parent) :
QWidget(parent),
ui(new Ui::Form)
{
ui->setupUi(this);
}
Form *Form::getInstance()
{
return m_Form;
}
Form::~Form()
{
delete ui;
}
调用文件:
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "form.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->verticalLayout->addWidget(Form::getInstance());
}
MainWindow::~MainWindow()
{
delete ui;
}
布局管理器源码分析
看到这里不知道你发现问题所在了没有呢。其实到这里,程序运行什么都都是没有问题的,就是不要退出程序,因为一旦退出程序,就会遇到崩溃问题。那么开始我一度以为是使用了布局管理器导致的这个问题,但是我看了源码也却突然发现,布局管理器是不会对其管理的控件进行资源释放的,部分源码如下,我这里就合并显示了。
cpp
QBoxLayoutPrivate::~QBoxLayoutPrivate()
{
}
/*!
Destroys this box layout.
The layout's widgets aren't destroyed.
*/
QHBoxLayout::~QHBoxLayout()
{
}
/*!
The layout's widgets aren't destroyed.
*/
QVBoxLayout::~QVBoxLayout()
{
}
Demo 验证
保险起见,我还特意谷歌翻译了一下Destroys this box layout.The layout's widgets aren't destroyed.
里面明确说明,布局会破坏,但是里面的控件不会释放。所谓问题来,到底是哪里的问题了,我尝试了另一种写法,代码如下,希望你还可以跟得上
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "form.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
Form * temp = Form::getInstance();
temp->setParent(this);
temp->show();
}
MainWindow::~MainWindow()
{
delete ui;
}
这样我得到的结果同样是崩溃的析构函数,如下图所示
关于布局管理器析构
正当我一头雾水的手,我随机搜了一下Qt 布局管理器在析构是会析构子控件吗
这个问题,得到的回答如下
在 Qt 中,布局管理器在析构时通常不会主动析构其管理的子控件。布局管理器负责管理控件的位置和大小,而控件的生命周期管理通常是由其父控件负责的。
当一个 QWidget 对象(可能是窗口或其他容器控件)被析构时,它会负责删除其子控件。这是因为 QWidget 继承自 QObject,而 QObject 有一个父子对象关系系统,负责自动管理子对象的内存。
布局管理器在 Qt 中主要用于确定子控件的布局,而不是控制它们的生命周期。因此,当布局管理器本身被析构时,它不会主动销毁其管理的子控件。子控件的析构通常是由其父控件来处理。
如果你希望在删除布局管理器的同时也删除其管理的子控件,你需要手动迭代子控件并将其删除,或者使用智能指针等工具来管理控件的生命周期。这可以确保在删除布局管理器时,相关的子控件也会被正确地释放。
总的来说,Qt 的布局管理器并不负责子控件的析构,而是留给控件的父对象来管理。
结合上面对布局管理器源码的分析,可以看到,我们的崩溃不是有布局管理器引起的,而是被父类析构析构了。
Qt 类声明周期探索
本着刨根问题的原则,我有问了另一个问题设置了布局管理器就等于设置了父类吗
在 Qt 中,设置了布局管理器并不等同于设置了父类。虽然布局管理器通常是作为容器控件的子对象来管理的,但它们在概念上是不同的。
-
父子关系:
- 当你将一个控件(例如 QWidget)设置为另一个控件的子控件时,它们建立了父子关系。这样做的话,父控件会负责管理子控件的生命周期,包括在父控件销毁时自动删除子控件。
- 布局管理器是一种机制,用于控制子控件的布局,但它们通常不会成为父控件。布局管理器可以安排子控件的位置和大小,但并不会负责管理它们的生命周期。
-
布局管理器的作用:
- 布局管理器是用来管理布局的工具。它们通常被设置在容器类控件上,比如 QWidget、QMainWindow、QDialog 等。布局管理器负责决定子控件的排列方式,但不负责管理它们的析构。
下面是一个简单的例子,演示了如何在 QWidget 中使用布局管理器:
cpp
#include <QHBoxLayout>
#include <QPushButton>
#include <QWidget>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QWidget *mainWidget = new QWidget;
QHBoxLayout *layout = new QHBoxLayout(mainWidget);
QPushButton *button1 = new QPushButton("Button 1");
QPushButton *button2 = new QPushButton("Button 2");
layout->addWidget(button1);
layout->addWidget(button2);
mainWidget->setLayout(layout);
mainWidget->show();
return app.exec();
}
在这个例子中,mainWidget
是一个 QWidget,而 layout
是一个 QHBoxLayout。布局管理器 layout
被设置为 mainWidget
的布局管理器。然而,mainWidget
仍然是这两个按钮的父控件,而不是布局管理器。
总的来说,在设置布局管理器时,确保理解父子关系的变化,并注意生命周期管理的责任。
更新代码获取父类
虽然回答的不是那么一回事,但是还是启发了,所以我对我的代码做了一点更新,如下:
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "form.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
Form * temp = Form::getInstance();
ui->verticalLayout->addWidget(temp);
qDebug()<< "temp->parent()->" << temp->parent() << endl
<< "temp->parent()->parent()->" << temp->parent()->parent() << endl
<< "this->"<< this << endl
<< "this->parent()->" <<this->parent() << endl
<< "ui->verticalLayout->parent()" << ui->verticalLayout->parent();
}
MainWindow::~MainWindow()
{
delete ui;
}
得到打印信息如下
cpp
temp->parent()-> QWidget(0x948920, name = "centralwidget")
temp->parent()->parent()-> MainWindow(0x8ffd20, name = "MainWindow")
this-> MainWindow(0x8ffd20, name="MainWindow")
this->parent()-> QObject(0x0)
ui->verticalLayout->parent() QWidget(0x948920, name = "centralwidget")
这下基本明白了,其实对象的声明周期在Qt里面还是受气父控件的控制,而并非是布局管理器,今天我之所碰巧解决了这个Bug,仅仅是因为当我们吧控件添加到布局管理器时。不管理器把他的费空间做了传递、现在终于明白了。
分析Qt 单例宏源码
那么还需要解决另一个问题,就是我的单例宏做了什么。
cpp
#define Q_GLOBAL_STATIC(TYPE, NAME) \
Q_GLOBAL_STATIC_WITH_ARGS(TYPE, NAME, ())
cpp
#define Q_GLOBAL_STATIC_WITH_ARGS(TYPE, NAME, ARGS) \
namespace { namespace Q_QGS_ ## NAME { \
typedef TYPE Type; \
QBasicAtomicInt guard = Q_BASIC_ATOMIC_INITIALIZER(QtGlobalStatic::Uninitialized); \
Q_GLOBAL_STATIC_INTERNAL(ARGS) \
} } \
static QGlobalStatic<TYPE, \
Q_QGS_ ## NAME::innerFunction, \
Q_QGS_ ## NAME::guard> NAME;
cpp
#if defined(Q_COMPILER_CONSTEXPR)
# define Q_BASIC_ATOMIC_INITIALIZER(a) { a }
#else
# define Q_BASIC_ATOMIC_INITIALIZER(a) { ATOMIC_VAR_INIT(a) }
#endif
cpp
#define Q_GLOBAL_STATIC_INTERNAL(ARGS) \
Q_GLOBAL_STATIC_INTERNAL_DECORATION Type *innerFunction() \
{ \
struct HolderBase { \
~HolderBase() noexcept \
{ if (guard.loadRelaxed() == QtGlobalStatic::Initialized) \
guard.storeRelaxed(QtGlobalStatic::Destroyed); } \
}; \
static struct Holder : public HolderBase { \
Type value; \
Holder() \
noexcept(noexcept(Type ARGS)) \
: value ARGS \
{ guard.storeRelaxed(QtGlobalStatic::Initialized); } \
} holder; \
return &holder.value; \
}
#else
// We don't know if this compiler supports thread-safe global statics
// so use our own locked implementation
QT_END_NAMESPACE
#include <QtCore/qmutex.h>
#include <mutex>
QT_BEGIN_NAMESPACE
#define Q_GLOBAL_STATIC_INTERNAL(ARGS) \
Q_DECL_HIDDEN inline Type *innerFunction() \
{ \
static Type *d; \
static QBasicMutex mutex; \
int x = guard.loadAcquire(); \
if (Q_UNLIKELY(x >= QtGlobalStatic::Uninitialized)) { \
const std::lock_guard<QBasicMutex> locker(mutex); \
if (guard.loadRelaxed() == QtGlobalStatic::Uninitialized) { \
d = new Type ARGS; \
static struct Cleanup { \
~Cleanup() { \
delete d; \
guard.storeRelaxed(QtGlobalStatic::Destroyed); \
} \
} cleanup; \
guard.storeRelease(QtGlobalStatic::Initialized); \
} \
} \
return d; \
}
#endif
让我们稍作翻译
这段宏定义了一个模板化的全局静态变量创建机制,通过 Q_GLOBAL_STATIC_INTERNAL
宏可以方便地创建一个全局静态变量。这个机制使用了 C++11 的特性,包括 noexcept 说明符和内存模型的一些操作。
让我们逐步解释这个宏的各个部分:
-
Q_GLOBAL_STATIC_INTERNAL(ARGS)
: 这是主要的宏定义,用于创建全局静态变量。ARGS
是传递给 Type 类型构造函数的参数。 -
Q_GLOBAL_STATIC_INTERNAL_DECORATION
: 这是一个在宏中使用的修饰符,可能是空的,取决于编译器对 C++11 特性的支持情况。 -
Type *innerFunction()
: 这是一个内部的静态函数,负责实际创建和返回全局静态变量的指针。它使用了一个内部的 Holder 类,以确保在全局静态变量的析构期间正确地管理生命周期。 -
struct HolderBase
: 这是一个基础结构,其中的析构函数负责在全局静态变量被销毁时,检查并更新一个状态标志。这个标志在构造时被设置为 Initialized,而在析构时被设置为 Destroyed。 -
struct Holder : public HolderBase
: 这是一个派生自 HolderBase 的结构。它包含了具体的 Type 类型的实例(value
),并在构造函数中将状态标志设置为 Initialized。这确保在全局静态变量的生命周期内,HolderBase 的析构函数将被正确调用,以更新状态标志。
整个机制的目的是确保在多线程环境下,全局静态变量的创建是线程安全的。通过在 HolderBase 的析构函数中检查状态标志,可以防止在全局静态变量析构时重复析构。
这是一个比较复杂的实现,主要是为了保证全局静态变量的正确创建和销毁,并且在多线程环境下能够安全使用。
所以现在应该明白了,这就是资源双重释放了,未来的你,加油。