Qt_一个由单例引发的崩溃

头图

Qt_一个由单例引发的崩溃

文章目录

  • Qt_一个由单例引发的崩溃
    • 摘要
    • 关于 Q_GLOBAL_STATIC
    • 代码测试
    • 布局管理器源码分析
    • Demo 验证
    • 关于布局管理器析构
    • Qt 类声明周期探索
    • 更新代码获取父类
    • 分析Qt 单例宏源码

关键字: QtQ_GLOBAL_STATIC单例UI崩溃

摘要

今天简直是令人心力交瘁的一天,在公司被一个顽固的Bug纠缠了整整一天。一开始,我对这个问题的认知并不深刻,只是觉得有点小瑕疵,于是比较轻松地着手解决。我开始摸索着定位问题,态度上也没太在意,毕竟在我看来,这只是一场小小的技术挑战。

然而,随着时间的推移,我逐渐意识到问题的严重性。逐渐加深的烦躁和困扰让我开始感到不安。在一度对问题轻描淡写的态度下,我终于被迫正视这个Bug所可能引发的连锁反应。随着这个问题的逐渐显露出其庞大的影响,我仿佛看到了一个漩涡,正在悄然蔓延着,威胁着整个系统的稳定性。

随着深入的调查和排查,我开始逐渐理解这个Bug的神秘本质,而这个认识过程伴随着我的焦虑逐渐升温。我发现这并非是一个简单的技术故障,而是一个可能牵动公司正常运营的重大问题。这时,我终于感受到了来自责任的重压,因为解决这个问题不仅关乎我的个人技术能力,更涉及到公司业务的持续稳健。

最终,当我对问题的了解达到顶峰时,我不禁意识到这一天的崩溃并非仅限于我的个人情绪,更是对整个系统运行稳定性的一次极大考验。这场对抗Bug的战斗让我感受到了技术领域的不可预测性和挑战性,也让我更加深刻地明白在这个领域中的学无止境。

image-20231127214955902

关于 Q_GLOBAL_STATIC

我在创建单例上偷了懒,使用了Qt Q_GLOBAL_STATIC宏来创建单例。关于Q_GLOBAL_STATIC的解释如下:

Q_GLOBAL_STATIC 是 Qt 框架中用于创建全局静态变量的宏。这个宏的目的是确保在多线程环境下安全地初始化和访问静态变量。在 C++ 中,全局静态变量的初始化顺序可能会带来一些问题,特别是在多线程环境下。

使用 Q_GLOBAL_STATIC 宏,Qt 提供了一种线程安全的机制来创建全局静态变量。这个宏可以确保静态变量只在第一次访问时被初始化,而且在初始化过程中是线程安全的。

具体用法如下:

Q_GLOBAL_STATIC(Type, variable)

其中,Type 是要创建的静态变量的类型,variable 是变量的名称。

举例说明:

#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 代码

头文件:

#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

源文件:

#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;
}

调用文件:

#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;
}


布局管理器源码分析

看到这里不知道你发现问题所在了没有呢。其实到这里,程序运行什么都都是没有问题的,就是不要退出程序,因为一旦退出程序,就会遇到崩溃问题。那么开始我一度以为是使用了布局管理器导致的这个问题,但是我看了源码也却突然发现,布局管理器是不会对其管理的控件进行资源释放的,部分源码如下,我这里就合并显示了。

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.里面明确说明,布局会破坏,但是里面的控件不会释放。所谓问题来,到底是哪里的问题了,我尝试了另一种写法,代码如下,希望你还可以跟得上

#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;
}

这样我得到的结果同样是崩溃的析构函数,如下图所示

image-20231127223544817

关于布局管理器析构

正当我一头雾水的手,我随机搜了一下Qt 布局管理器在析构是会析构子控件吗这个问题,得到的回答如下

在 Qt 中,布局管理器在析构时通常不会主动析构其管理的子控件。布局管理器负责管理控件的位置和大小,而控件的生命周期管理通常是由其父控件负责的。

当一个 QWidget 对象(可能是窗口或其他容器控件)被析构时,它会负责删除其子控件。这是因为 QWidget 继承自 QObject,而 QObject 有一个父子对象关系系统,负责自动管理子对象的内存。

布局管理器在 Qt 中主要用于确定子控件的布局,而不是控制它们的生命周期。因此,当布局管理器本身被析构时,它不会主动销毁其管理的子控件。子控件的析构通常是由其父控件来处理。

如果你希望在删除布局管理器的同时也删除其管理的子控件,你需要手动迭代子控件并将其删除,或者使用智能指针等工具来管理控件的生命周期。这可以确保在删除布局管理器时,相关的子控件也会被正确地释放。

总的来说,Qt 的布局管理器并不负责子控件的析构,而是留给控件的父对象来管理。

结合上面对布局管理器源码的分析,可以看到,我们的崩溃不是有布局管理器引起的,而是被父类析构析构了。

Qt 类声明周期探索

本着刨根问题的原则,我有问了另一个问题设置了布局管理器就等于设置了父类吗

在 Qt 中,设置了布局管理器并不等同于设置了父类。虽然布局管理器通常是作为容器控件的子对象来管理的,但它们在概念上是不同的。

  1. 父子关系:

    • 当你将一个控件(例如 QWidget)设置为另一个控件的子控件时,它们建立了父子关系。这样做的话,父控件会负责管理子控件的生命周期,包括在父控件销毁时自动删除子控件。
    • 布局管理器是一种机制,用于控制子控件的布局,但它们通常不会成为父控件。布局管理器可以安排子控件的位置和大小,但并不会负责管理它们的生命周期。
  2. 布局管理器的作用:

    • 布局管理器是用来管理布局的工具。它们通常被设置在容器类控件上,比如 QWidget、QMainWindow、QDialog 等。布局管理器负责决定子控件的排列方式,但不负责管理它们的析构。

下面是一个简单的例子,演示了如何在 QWidget 中使用布局管理器:

#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 仍然是这两个按钮的父控件,而不是布局管理器。

总的来说,在设置布局管理器时,确保理解父子关系的变化,并注意生命周期管理的责任。

更新代码获取父类

虽然回答的不是那么一回事,但是还是启发了,所以我对我的代码做了一点更新,如下:

#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;
}


得到打印信息如下

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 单例宏源码

那么还需要解决另一个问题,就是我的单例宏做了什么。

#define Q_GLOBAL_STATIC(TYPE, NAME)                                         \
    Q_GLOBAL_STATIC_WITH_ARGS(TYPE, NAME, ())
#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;

#if defined(Q_COMPILER_CONSTEXPR)
#  define Q_BASIC_ATOMIC_INITIALIZER(a)     { a }
#else
#  define Q_BASIC_ATOMIC_INITIALIZER(a)     { ATOMIC_VAR_INIT(a) }
#endif

#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 说明符和内存模型的一些操作。

让我们逐步解释这个宏的各个部分:

  1. Q_GLOBAL_STATIC_INTERNAL(ARGS): 这是主要的宏定义,用于创建全局静态变量。ARGS 是传递给 Type 类型构造函数的参数。

  2. Q_GLOBAL_STATIC_INTERNAL_DECORATION: 这是一个在宏中使用的修饰符,可能是空的,取决于编译器对 C++11 特性的支持情况。

  3. Type *innerFunction(): 这是一个内部的静态函数,负责实际创建和返回全局静态变量的指针。它使用了一个内部的 Holder 类,以确保在全局静态变量的析构期间正确地管理生命周期。

  4. struct HolderBase: 这是一个基础结构,其中的析构函数负责在全局静态变量被销毁时,检查并更新一个状态标志。这个标志在构造时被设置为 Initialized,而在析构时被设置为 Destroyed。

  5. struct Holder : public HolderBase: 这是一个派生自 HolderBase 的结构。它包含了具体的 Type 类型的实例(value),并在构造函数中将状态标志设置为 Initialized。这确保在全局静态变量的生命周期内,HolderBase 的析构函数将被正确调用,以更新状态标志。

整个机制的目的是确保在多线程环境下,全局静态变量的创建是线程安全的。通过在 HolderBase 的析构函数中检查状态标志,可以防止在全局静态变量析构时重复析构。

这是一个比较复杂的实现,主要是为了保证全局静态变量的正确创建和销毁,并且在多线程环境下能够安全使用。

所以现在应该明白了,这就是资源双重释放了,未来的你,加油。


博客签名2021

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/193603.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深入解析:Peft Adapter与LLM融合

在增量预训练阶段或有监督微调阶段使用高效微调方法(Lora)时会产生adapter文件,相当于是一个“补丁”。那么如何将“补丁”与原始模型合并呢? 下面将对模型合并代码进行解读。 相关代码将全部上传到github: https://github.com/hjandlm/LLM_Train 欢迎关注公众号 代码…

HarmonyOS应用开发者基础认证【题库答案】

HarmonyOS应用开发者高级认证【题库答案】 一、判断 首选项preferences是以Key-Value形式存储数据&#xff0c;其中Key是可以重复。&#xff08;错&#xff09;使用http模块发起网络请求时&#xff0c;必须要使用on(‘headersReceive’&#xff09;订阅请求头&#xff0c;请…

PHP 双门双向门禁控制板实时监控源码

本示例使用设备&#xff1a; 实时网络双门双向门禁控制板可二次编程控制网络继电器远程开关-淘宝网 (taobao.com) <?PHPheader("content-type:text/html;charsetGBK");$ThisIpget_local_ip(); //获取电脑IP地址 $server udp://.$ThisIp.:39192; $sock…

每天一道算法题:51. N 皇后

难度 困难 题目 按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。 n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回所有不同的 n_ _皇后问题 的…

FPGA模块——AD高速转换模块(并行输出转换的数据)

FPGA模块——AD高速转换模块&#xff08;并行输出转换的数据&#xff09; &#xff08;1&#xff09;AD9280/3PA9280芯片&#xff08;2&#xff09;代码 &#xff08;1&#xff09;AD9280/3PA9280芯片 AD9280/3PA9280芯片的引脚功能&#xff1a; 工作电压2.7到5.5v 数据对应&a…

代码随想录算法训练营第五十七天|739. 每日温度、496.下一个更大元素 I

LeetCode 739. 每日温度 题目链接&#xff1a;739. 每日温度 - 力扣&#xff08;LeetCode&#xff09; 单调栈开始&#xff0c;为什么要用栈&#xff0c;因为栈是先入后出&#xff0c;当我们遍历从前往后的时候&#xff0c;每次遍历的元素都是添加至栈尾&#xff0c;方便我们进…

西南科技大学电路分析基础实验A1(一阶电路的设计)

目录 一、实验目的 二、实验设备 三、预习内容(如:基本原理、电路图、计算值等) 四、实验数据及结果分析(预习写必要实验步骤和表格) 1. 观测一阶电

搜索百度可以直接生成代码拉

先看效果图&#xff1a; 使用示例&#xff1a; 比如我要搜索“JS取一个数在两个数更近”的方法&#xff0c;直接搜“JS取一个数在两个数更近”&#xff0c;点击百度一下&#xff0c;就会出现想要的代码&#xff0c;如上图。

《金融科技行业2023年专利分析白皮书》发布——科技变革金融,专利助力行业发展

金融是国民经济的血脉&#xff0c;是国家核心竞争力的重要组成部分&#xff0c;金融高质量发展成为2023年中央金融工作的重要议题。《中国金融科技调查报告》中指出&#xff0c;我国金融服务业在科技的助力下&#xff0c;从1.0时代的“信息科技金融”、2.0时代的“互联网金融”…

坚鹏:数字金融——金融行业的数字科技和智能化转型培训圆满结束

中国邮政储蓄银行拥有优良的资产质量和显著的成长潜力&#xff0c;是中国领先的大型零售银行。2016年9月在香港联交所挂牌上市&#xff0c;2019年12月在上交所挂牌上市。中国邮政储蓄银行拥有近4万个营业网点&#xff0c;服务个人客户超6.5亿户。2022年&#xff0c;在《银行家》…

力扣hot100 滑动窗口最大值 单调队列

&#x1f468;‍&#x1f3eb; 题目地址 &#x1f37b; AC code class Solution {public int[] maxSlidingWindow(int[] nums, int k){int n nums.length;int[] res new int[n - k 1]; // 单调递减队列int[] q new int[n];// q数组维护的是元素在 nums 数组对应的下标int…

从零构建属于自己的GPT系列1:预处理模块(逐行代码解读)、文本tokenizer化

1 训练数据 在本任务的训练数据中&#xff0c;我选择了金庸的15本小说&#xff0c;全部都是txt文件 数据打开后的样子 数据预处理需要做的事情就是使用huggingface的transformers包的tokenizer模块&#xff0c;将文本转化为token 最后生成的文件就是train_novel.pkl文件&a…

动态网页从数据库取信息,然后展示。

把数据库的驱动放在bin目录下。 通过servlet 读取数据库的内容&#xff0c;生成session,然后跨页面传给展示页。 package src;import java.io.IOException; import java.io.PrintWriter; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSe…

sprintf函数

1.头文件&#xff1a;#include <stdio.h> 2.函数原型&#xff1a;int sprintf ( char * str, const char * format, ... ) 3.函数功能&#xff1a;将数据格式化为字符串&#xff0c;再写入到字符串中 4.参数分析&#xff1a; str&#xff1a;是字符串指针&#xff0c…

SpringBoot——国际化

优质博文&#xff1a;IT-BLOG-CN 一、Spring 编写国际化时的步骤 【1】编写国际化配置文件&#xff1b; 【2】使用ResourceBundleMessageSource管理国际化资源文件&#xff1b; 【3】在页面使用ftp:message取出国际化内容&#xff1b; 二、SpringBoot编写国际化步骤 【1】创…

JavaScript WebApi(二) 详解

监听事件 介绍 事件监听是一种用于在特定条件下执行代码的编程技术。在Web开发中&#xff0c;事件监听器可以用于捕获和响应用户与页面交互的各种操作&#xff0c;如点击、滚动、输入等。 事件监听的基本原理是&#xff0c;通过在特定元素上注册事件监听器&#xff0c;当事件…

每日一题:LeetCode-202.快乐数(一点都不快乐)

每日一题系列&#xff08;day 06&#xff09; 前言&#xff1a; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f50e…

【古诗生成AI实战】之三——任务加载器与预处理器

本章内容属于数据处理阶段&#xff0c;将分别介绍任务加载器task和预处理器processor。 [1] 数据集 在深入探讨数据处理的具体步骤之前&#xff0c;让我们先了解一下我们将要使用的数据集的形式。 本项目采用的是七绝数据集&#xff0c;总计83072条古诗&#xff0c;其形式如下&…

单片机学习10——独立按键

独立按键输入检测&#xff1a; #include<reg52.h>sbit LED1P1^0; sbit KEY1P3^4;void main() {KEY11;while(1){if(KEY10) //KEY1按下{LED10; //LED1被点亮}else{LED11;}} } 按键 #include<reg52.h>#define uchar unsigned char #define uint unsigned intsbit …

Java LeetCode篇-深入了解关于数组的经典解法

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 轮转数组 1.1 使用移位的方式 1.2 使用三次数组逆转法 2.0 消失的数字 2.1 使用相减法 2.2 使用异或的方式 3.0 合并两个有序数组 3.1 使用三指针方式 3.2 使用合…