「Qt」内存管理机制

前言:

        在学习C++《new & delete(动态地制造对象)》的时候,我们了解了在堆中创建的对象是需要手动管理内存的(如有遗忘请参考以下文章)
「面向对象程序设计-C++」学习笔记(上半部分)https://blog.csdn.net/YMGogre/article/details/126759839
        在Qt中,我们可以不用再为此事烦心了,这是因为Qt提供了一种基于对象的层次结构的内存管理机制。熟悉Qt框架的小伙伴可能曾经注意过,我们创建对象的时候都会提供一个 parent 对象指针,比如我们创建一个按钮控件“btn”对象:

        这是为什么?这个 parent 的作用到底是啥?先偷懒问问AI吧:

ChatGPT 和 New Bing 的回答:

ChatGPT
New Bing

        那么,显而易见,parent 的作用就是设置当前对象的父对象嘛,以此建立了父子关系。接下来再着重讲讲 Qt 中的对象模型 —— 对象树。

对象树:

  • QObject 是以对象树的形式组织起来的。
    • 当我们创建一个 QObject 对象时,会看到 QObject 的构造函数接受一个 QObject 指针作为参数,这个参数就是 parent,也就是父对象的指针。这相当于,在创建 QObject 对象时,可以提供其一个父对象,我们创建的这个 QObject 对象会自动添加到其父对象的 children() 列表。
    • 当父对象析构时,children() 列表中的所有对象也会被析构(注意,这里说的父对象并不是在继承概念中的父类对象)。这种机制在 GUI 程序设计中相当有用。例如:一个按钮有一个 QShortcut (快捷键)对象作为其子对象。当我们删除按钮的时候,这个快捷键理应被删除,这是合理的。
  • QWidget 是能够在屏幕上显示的一切组件的父类。
    • QWidget 继承自 QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界裁剪。例如:当用户关闭一个对话框的时候,应用程序将其删除。那么,我们希望属于这个对话框的按钮、图标等应该一起被删除。当然,事实就是如此,因为这些都是对话框的子组件。
    • 当然,我们也可以自己删除子对象,它们会自动从其父对象的 children() 列表中删除。比如,当我们删除了一个工具栏时,其所在的主窗口会自动将该工具栏从其子对象列表中删除,并且自动调整屏幕显示。
Qt 中的对象树

一点细节:

        有时候,即便我们没有看到显式地设置对象的父对象,其实也是可能加入了对象树管理的。比如我们写一个C++成员方法示例代码,该成员方法可以向一个 QTreeWidget 对象添加一个顶层节点及它的一个子节点,并在顶层节点的第二列设置一个 QCheckBox 对象,子节点的第二列设置一个QComboBox 对象:

/** 假设类的名称为MainWindow,成员方法名称为add_TreeWidgetItem **/

/**
 * @brief 向一个QTreeWidget对象添加QTreeWidgetItem对象项方法
 * @param QTreeWidget对象treeWidget
 */
QTreeWidgetItem* MainWindow::add_TreeWidgetItem(QTreeWidget* treeWidget)
{
    //创建顶层节点
    QTreeWidgetItem* TopLevelItem = new QTreeWidgetItem(QStringList() << "顶层节点");
    //添加顶层节点
    treeWidget->addTopLevelItem(TopLevelItem);
    //初始化一个checkbox(未指定其父对象)
    QCheckBox* checkbox = new QCheckBox();
    //添加checkbox于顶层节点第二列
    treeWidget->setItemWidget(TopLevelItem, 1, checkbox);

    /*
     * 定义子节点 ———— ChildItem
     */
    QTreeWidgetItem* ChildItem = new QTreeWidgetItem(QStringList() << "子节点");
    //添加子节点
    TopLevelItem->addChild(ChildItem);
    //初始化一个combobox(指定其父对象为treeWidget)
    QComboBox* combobox = new QComboBox(treeWidget);
    //为combobox添加内容
    combobox ->addItems(QStringList() << "我是ComboBox");
    //添加combobox于子节点的第二列
    treeWidget->setItemWidget(ChildItem, 1, combobox);

    return TopLevelItem;
}
运行结果

        在这段示例代码中,我们很容易就察觉到位于顶层节点第二列的 checkbox 在初始化时并未指定其父对象;而位于子节点第二列的 combobox 在初始化时明确指定了其父对象是 treeWidget。按照目前为止我们的理解,那么 checkbox 应该是没有纳入对象树管理的,需要我们手动管理其内存。事实果真如此吗🧐?

        经过我的测试,这里我就不卖关子了直接说结论

  1. 对于以上的测试代码,类似于子节点第二列的 combobox,如果控件明确指定了其父对象是 treeWidget,那么我们可以在 treeWidgetchildren() 列表找到它;而类似于顶层节点第二列的 checkbox,如果控件没有指定父对象的话,treeWidgetchildren() 列表就没有它,需要手动管理内存。这符合上面讲的对象树的概念;
  2. 但是,当我们使用 QTreeWidget::setItemWidget() 方法将控件添加到 treeWidget 中时,treeWidget 会将其自动添加到自己的一个子对象 —— qt_scrollarea_viewport 对象的 children() 列表里,从而管理控件对象的内存(无论控件之前的父对象是谁)。也就是说,控件对象成为了 treeWidget 子对象的子对象(以我们的测试代码为例,析构 treeWidget 时,qt_scrollarea_viewport 也会被析构;析构 qt_scrollarea_viewport 时,checkbox 和 combobox 也会被析构):

  3. 结合1、2两点,在未 delete treeWidget 的情况下,如果我们想删除 checkbox 和 combobox(移除UI并释放内存),就只能手动删除了;
    /** 下面是删除示例代码中QCheckBox对象代码演示(该段代码仅作演示,实际并不完善,比如没有做空指针判断就直接拿来使用了) **/
    
    //获取顶层节点
    QTreeWidgetItem* TopLevelItem = treeWidget->topLevelItem(0);
    //获取顶层节点第二列的对象
    QWidget* _checkbox = treeWidget->itemWidget(TopLevelItem, 1);
    //类型转换为QCheckBox对象(类型转换是有必要的,保证了我们delete对象时调用的是QCheckBox类的析构而不是QWidget类的析构)
    QCheckBox* checkbox = qobject_cast<QCheckBox*>(_checkbox);
    //移除UI
    treeWidget->removeItemWidget(TopLevelItem, 1);
    //释放内存
    delete checkbox;
  4. 我们可以使用
    QTreeWidgetItem *QTreeWidget::takeTopLevelItem(int index)
    移除顶层节点;使用
    void QTreeWidgetItem::removeChild(QTreeWidgetItem *child)
    或者
    QTreeWidgetItem *QTreeWidgetItem::takeChild(int index)
    移除子节点;使用
    void QTreeWidget::removeItemWidget(QTreeWidgetItem *item, int column)
    移除设置在 QTreeWidget 对象中的控件 。但是这四个方法都仅仅是移除 UI,不会释放内存(不会进行 delete 操作);
  5. 不移除 UI 直接 delete 是似乎是安全的,delete 之后 UI 同样会被移除,但通常不建议这么做;
  6. 由于子节点是通过
    void QTreeWidgetItem::addChild(QTreeWidgetItem *child)
    方法添加到顶层节点的,所以析构顶层节点时也会去析构它所有的子节点但不会去析构 checkbox combobox
  7. 综合以上6点,如果我们想在不删除 QTreeWidget 对象的情况下删除一个带有子节点和控件项的顶层节点,通常的操作是:①移除所有控件对象UI;②使用 delete 关键字释放所有控件对象占用的内存;③删除顶层节点(移除UI并释放内存);④无需担心子节点内存泄漏因为Qt的内存管理机制会自动帮我们删除顶层节点的所有子节点。

        以上7点感兴趣的小伙伴也可以自行验证(比如单步调试查看指针变量的值和 children() 列表的变化以及使用 qDebug() 方法打印变量值)👻,以我提供的代码为例,顶层节点(TopLevelItem)、子节点(ChildItem)、checkboxcombobox的父子关系如下图所示:

示例代码的对象树模型(部分)

        这里我们注意到,TopLevelItem 的上一层是 nullptr(0x0),这表明它似乎没有被纳入对象树管理。其实不是这样的,这是因为 QTreeWidgetItem::parent() 方法返回的是该节点的父节点对象,如果该节点是顶层节点,则返回 nullptr

        但是,与我们上面说的控件对象类似,当我们通过

QTreeWidgetItem(QTreeWidgetItem *parent, const QStringList &strings, int type = Type)

方法在创建 QTreeWidgetItem 对象时指定其父对象;

或者

        通过

QTreeWidgetItem(const QStringList &strings, int type = Type)

方法在创建 QTreeWidgetItem 对象时未指定其父对象而后使用

void QTreeWidget::addTopLevelItem(QTreeWidgetItem *item)

QTreeWidgetItem 对象作为顶层节点追加到 QTreeWidget 对象中时;

        QTreeWidget 对象都会成为 QTreeWidgetItem 对象的父对象从而管理其内存(也就是说以我们上面的示例代码为例,delete treeWidget 时 TopLevelItem 也会被 delete 掉的)。这与顶层节点对象的父节点对象是 nullptr 并不矛盾

        当然,如果我们在创建 QTreeWidgetItem 对象时未指定其父对象并且没有调用 addTopLevelItem() 方法将其作为顶层节点追加到 QTreeWidget 对象中时, QTreeWidget 对象就不会成为 QTreeWidgetItem 对象的父对象从而管理其内存,UI界面 QTreeWidget 对象控件当然也不会显示该顶层节点。这是十分合理的,因为我们根本没有告诉编译器 QTreeWidgetItem 对象属于哪个 QTreeWidget 对象嘛。


        (尝试问了问 New Bing,给了差不多的回答:)

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值