前言:
在学习C++《new & delete(动态地制造对象)》的时候,我们了解了在堆中创建的对象是需要手动管理内存的(如有遗忘请参考以下文章)
「面向对象程序设计-C++」学习笔记(上半部分)https://blog.csdn.net/YMGogre/article/details/126759839 在Qt中,我们可以不用再为此事烦心了,这是因为Qt提供了一种基于对象的层次结构的内存管理机制。熟悉Qt框架的小伙伴可能曾经注意过,我们创建对象的时候都会提供一个 parent 对象指针,比如我们创建一个按钮控件“btn”对象:这是为什么?这个 parent 的作用到底是啥?先偷懒问问AI吧:
ChatGPT 和 New Bing 的回答:
那么,显而易见,parent 的作用就是设置当前对象的父对象嘛,以此建立了父子关系。接下来再着重讲讲 Qt 中的对象模型 —— 对象树。
对象树:
- QObject 是以对象树的形式组织起来的。
- 当我们创建一个 QObject 对象时,会看到 QObject 的构造函数接受一个 QObject 指针作为参数,这个参数就是 parent,也就是父对象的指针。这相当于,在创建 QObject 对象时,可以提供其一个父对象,我们创建的这个 QObject 对象会自动添加到其父对象的 children() 列表。
- 当父对象析构时,children() 列表中的所有对象也会被析构(注意,这里说的父对象并不是在继承概念中的父类对象)。这种机制在 GUI 程序设计中相当有用。例如:一个按钮有一个 QShortcut (快捷键)对象作为其子对象。当我们删除按钮的时候,这个快捷键理应被删除,这是合理的。
- QWidget 是能够在屏幕上显示的一切组件的父类。
- QWidget 继承自 QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界裁剪。例如:当用户关闭一个对话框的时候,应用程序将其删除。那么,我们希望属于这个对话框的按钮、图标等应该一起被删除。当然,事实就是如此,因为这些都是对话框的子组件。
- 当然,我们也可以自己删除子对象,它们会自动从其父对象的 children() 列表中删除。比如,当我们删除了一个工具栏时,其所在的主窗口会自动将该工具栏从其子对象列表中删除,并且自动调整屏幕显示。
一点细节:
有时候,即便我们没有看到显式地设置对象的父对象,其实也是可能加入了对象树管理的。比如我们写一个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 应该是没有纳入对象树管理的,需要我们手动管理其内存。事实果真如此吗🧐?
经过我的测试,这里我就不卖关子了直接说结论:
- 对于以上的测试代码,类似于子节点第二列的 combobox,如果控件明确指定了其父对象是 treeWidget,那么我们可以在 treeWidget 的 children() 列表找到它;而类似于顶层节点第二列的 checkbox,如果控件没有指定父对象的话,treeWidget 的 children() 列表就没有它,需要手动管理内存。这符合上面讲的对象树的概念;
- 但是,当我们使用 QTreeWidget::setItemWidget() 方法将控件添加到 treeWidget 中时,treeWidget 会将其自动添加到自己的一个子对象 —— qt_scrollarea_viewport 对象的 children() 列表里,从而管理控件对象的内存(无论控件之前的父对象是谁)。也就是说,控件对象成为了 treeWidget 子对象的子对象(以我们的测试代码为例,析构 treeWidget 时,qt_scrollarea_viewport 也会被析构;析构 qt_scrollarea_viewport 时,checkbox 和 combobox 也会被析构):
- 结合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;
- 我们可以使用
移除顶层节点;使用QTreeWidgetItem *QTreeWidget::takeTopLevelItem(int index)
或者void QTreeWidgetItem::removeChild(QTreeWidgetItem *child)
移除子节点;使用QTreeWidgetItem *QTreeWidgetItem::takeChild(int index)
移除设置在 QTreeWidget 对象中的控件 。但是这四个方法都仅仅是移除 UI,不会释放内存(不会进行 delete 操作);void QTreeWidget::removeItemWidget(QTreeWidgetItem *item, int column)
- 不移除 UI 直接 delete 是似乎是安全的,delete 之后 UI 同样会被移除,但通常不建议这么做;
- 由于子节点是通过
方法添加到顶层节点的,所以析构顶层节点时也会去析构它所有的子节点,但不会去析构 checkbox 和 combobox。void QTreeWidgetItem::addChild(QTreeWidgetItem *child)
- 综合以上6点,如果我们想在不删除 QTreeWidget 对象的情况下删除一个带有子节点和控件项的顶层节点,通常的操作是:①移除所有控件对象UI;②使用 delete 关键字释放所有控件对象占用的内存;③删除顶层节点(移除UI并释放内存);④无需担心子节点内存泄漏因为Qt的内存管理机制会自动帮我们删除顶层节点的所有子节点。
以上7点感兴趣的小伙伴也可以自行验证(比如单步调试查看指针变量的值和 children() 列表的变化以及使用 qDebug() 方法打印变量值)👻,以我提供的代码为例,顶层节点(TopLevelItem)、子节点(ChildItem)、checkbox、combobox的父子关系如下图所示:
这里我们注意到,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,给了差不多的回答:)