Qt浅谈之一:内存泄露

2023-11-05

一、简介

Qt内存管理机制:Qt 在内部能够维护对象的层次结构。对于可视元素,这种层次结构就是子组件与父组件的关系;对于非可视元素,则是一个对象与另一个对象的从属关系。在 Qt 中,在 Qt 中,删除父对象会将其子对象一起删除。

C++中delete 和 new 必须配对使用(一 一对应):delete少了,则内存泄露,多了麻烦更大。Qt中使用了new却很少delete,因为QObject的类及其继承的类,设置了parent(也可在构造时使用setParent函数或parent的addChild)故parent被delete时,这个parent的相关所有child都会自动delete,不用用户手动处理。但parent是不区分它的child是new出来的还是在栈上分配的。这体现delete的强大,可以释放掉任何的对象,而delete栈上对象就会导致内存出错,这需要了解Qt的半自动的内存管理。另一个问题:child不知道它自己是否被delete掉了,故可能会出现野指针。那就要了解Qt的智能指针QPointer。

二、关联图

(1)Linux内存图,主要了解堆栈上分配内存的不同方式。

(2)在Qt中,最基础和核心的类是:QObject,QObject内部有一个list,会保存children,还有一个指针保存parent,当自己析构时,会自己从parent列表中删除并且析构所有的children。

三、详解

1、Qt的半自动化的内存管理

(1)QObject及其派生类的对象,如果其parent非0,那么其parent析构时会析构该对象。

(2)QWidget及其派生类的对象,可以设置 Qt::WA_DeleteOnClose 标志位(当close时会析构该对象)。

(3)QAbstractAnimation派生类的对象,可以设置 QAbstractAnimation::DeleteWhenStopped。

(4)QRunnable::setAutoDelete()、MediaSource::setAutoDelete()。

(5)父子关系:父对象、子对象、父子关系。这是Qt中所特有的,与类的继承关系无关,传递参数是与parent有关(基类、派生类,或父类、子类,这是对于派生体系来说的,与parent无关)。

2、内存问题例子

例子一

#include <QApplication>
#include <QLabel>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QLabel *label = new QLabel("Hello Qt!");
label->show();
return a.exec();
}

分析:(1)label 既没有指定parent,也没有对其调用delete,所以会造成内存泄漏。书中的这种小例子也会出现指针内存的问题。

改进方式:(1)分配对象到栈上而不是堆上

#include <QApplication>
#include <QLabel>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QLabel label("Hello Qt!");
label.show();
return a.exec();
}

(2)设置标志位,close()后会delete label。

label->setAttribute(Qt::WA_DeleteOnClose);

(3)new后手动delete

#include <QApplication>
#include <QLabel>

int main(int argc, char *argv[])
{
int ret = 0;
QApplication a(argc, argv);
QLabel *label = new QLabel("Hello Qt!");
label->show();
ret = a.exec();
delete label;
return ret;
}

例子二

#include <QApplication>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QLabel label("Hello Qt!");
label.show();
label.setAttribute(Qt::WA_DeleteOnClose);
return app.exec();
}

运行:

分析:程序崩溃,因为label被close时,delete &label;但label对象是在栈上分配的内存空间,delete栈上的地址会出错。

有些朋友理解为label被delete两次而错误,可以测试QLabel label("Hello Qt!"); label.show();delete &label;第一次delete就会出错。

例子三

#include <QApplication>
#include <QLabel>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QLabel label("Hello Qt!");
QWidget w;
label.setParent(&w);
w.show();
return app.exec();
}

分析:Object内部有一个list,会保存children,还有一个指针保存parent,当自己析构时,会自己从parent列表中删除并且析构所有的children。

w比label先被析构,当w被析构时,会删除chilren列表中的对象label,但label是分配到栈上的,因delete栈上的对象而出错。

改进方式:(1)调整一下顺序,确保label先于其parent被析构,label析构时将自己从父对象的列表中移除自己,w析构时,children列表中就不会有分配在stack中的对象了。

#include <QApplication>
#include <QLabel>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QWidget w;
QLabel label("Hello Qt!");
label.setParent(&w);
w.show();
return app.exec();
}

(2)将label分配到堆上

QLabel *label = new QLabel("Hello Qt!");
label->setParent(&w)

本文福利,费领取Qt开发学习资料包、技术视频,内容包括(C++语言基础,Qt编程入门,QT信号与槽机制,QT界面开发-图像绘制,QT网络,QT数据库编程,QT项目实战,QT嵌入式开发,Quick模块等等)↓↓↓↓↓↓见下面↓↓文章底部点击费领取↓↓

或者QLabel *label = new QLabel("Hello Qt!",this);

例子四:野指针

#include <QApplication>
#include <QLabel>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QWidget *w = new QWidget;
QLabel *label = new QLabel("Hello Qt!");
label->setParent(w);
w->show();
delete w;
label->setText("go"); //野指针
return app.exec();
}

(上述程序不显示Label,仅作测试)

分析:程序异常结束,delete w时会delete label,label成为野指针,调用label->setText("go");出错。

改进方式:QPointer智能指针

#include <QApplication>
#include <QLabel>
#include <QPointer>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QWidget *w = new QWidget;
QLabel *label = new QLabel("Hello Qt!");
label->setParent(w);
QPointer<QLabel> p = label;
w->show();
delete w;
if (!p.isNull()) {
label->setText("go");
}
return app.exec();
}

例子五:deleteLater

当一个QObject正在接受事件队列时如果中途被你销毁掉了,就是出现问题了,所以QT中建大家不要直接Delete掉一个QObject,如果一定要这样做,要使用QObject的deleteLater()函数,它会让所有事件都发送完一切处理好后马上清除这片内存,而且就算调用多次的deletelater也不会有问题。

发送一个删除事件到事件系统:

void QObject::deleteLater()
{
QCoreApplication::postEvent(this, new QEvent(QEvent::DeferredDelete));
}

3、智能指针

如果没有智能指针,程序员必须保证new对象能在正确的时机delete,四处编写异常捕获代码以释放资源,而智能指针则可以在退出作用域时(不管是正常流程离开或是因异常离开)总调用delete来析构在堆上动态分配的对象。

Qt家族的智能指针:

智能指针

引入

QPointer

Qt Object 模型的特性(之一)

注意:析构时不会delete它管理的资源

QSharedPointer

带引用计数

Qt4.5

QWeakPointer

Qt4.5

QScopedPointer

Qt4.6

QScopedArrayPointer

QScopedPointer的派生类

Qt4.6

QSharedDataPointer

用来实现Qt的隐式共享(Implicit Sharing)

Qt4.0

QExplicitlySharedDataPointer

显式共享

Qt4.4

std::auto_ptr

std::shared_ptr

std::tr1::shared_ptr

C++0x

std::weak_ptr

std::tr1::weak_ptr

C++0x

std::unique_ptr

boost::scoped_ptr

C++0x

(1)QPointer

QPointer是一个模板类。它很类似一个普通的指针,不同之处在于,QPointer 可以监视动态分配空间的对象,并且在对象被 delete 的时候及时更新。

QPointer的现实原理:在QPointer保存了一个QObject的指针,并把这个指针的指针(双指针)交给全局变量管理,而QObject 在销毁时(析构函数,QWidget是通过自己的析构函数的,而不是依赖QObject的)会调用QObjectPrivate::clearGuards 函数来把全局 GuardHash 的那个双指针置为*零,因为是双指针的问题,所以QPointer中指针当然也为零了。用isNull 判断就为空了。

// QPointer 表现类似普通指针
QDate *mydate = new QDate(QDate::currentDate());
QPointer mypointer = mydata;
mydate->year(); // -> 2005
mypointer->year(); // -> 2005

// 当对象 delete 之后,QPointer 会有不同的表现
delete mydate;

if(mydate == NULL)
printf("clean pointer");
else
printf("dangling pointer");
// 输出 dangling pointer

if(mypointer.isNull())
printf("clean pointer");
else
printf("dangling pointer");
// 输出 clean pointer

(2)std::auto_ptr

// QPointer 表现类似普通指针
QDate *mydate = new QDate(QDate::currentDate());
QPointer mypointer = mydata;
mydate->year(); // -> 2005
mypointer->year(); // -> 2005

// 当对象 delete 之后,QPointer 会有不同的表现
delete mydate;

if(mydate == NULL)
printf("clean pointer");
else
printf("dangling pointer");
// 输出 dangling pointer

if(mypointer.isNull())
printf("clean pointer");
else
printf("dangling pointer");
// 输出 clean pointe
auto_ptr被销毁时会自动删除它指向的对象。
std::auto_ptr<QLabel> label(new QLabel("Hello Dbzhang800!"));

(3)其他的类参考相应文档。

4、自动垃圾回收机制

(1)QObjectCleanupHandler

Qt 对象清理器是实现自动垃圾回收的很重要的一部分。QObjectCleanupHandler可以注册很多子对象,并在自己删除的时候自动删除所有子对象。同时,它也可以识别出是否有子对象被删 除,从而将其从它的子对象列表中删除。这个类可以用于不在同一层次中的类的清理操作,例如,当按钮按下时需要关闭很多窗口,由于窗口的 parent 属性不可能设置为别的窗口的 button,此时使用这个类就会相当方便。

#include <QApplication>
#include <QObjectCleanupHandler>
#include <QPushButton>

int main(int argc, char* argv[])
{
QApplication app(argc, argv);
// 创建实例
QObjectCleanupHandler *cleaner = new QObjectCleanupHandler;
// 创建窗口
QPushButton *w = new QPushButton("Remove Me");
w->show();
// 注册第一个按钮
cleaner->add(w);
// 如果第一个按钮点击之后,删除自身
QObject::connect(w, SIGNAL(clicked()), w, SLOT(deleteLater()));
// 创建第二个按钮,注意,这个按钮没有任何动作
w = new QPushButton("Nothing");
cleaner->add(w);
w->show();
// 创建第三个按钮,删除所有
w = new QPushButton("Remove All");
cleaner->add(w);
QObject::connect(w, SIGNAL(clicked()), cleaner, SLOT(deleteLater()));
w->show();
return app.exec();
}

在上面的代码中,创建了三个仅有一个按钮的窗口。第一个按钮点击后,会删除掉自己(通过 deleteLater() 槽),此时,cleaner 会自动将其从自己的列表中清除。第三个按钮点击后会删除 cleaner,这样做会同时删除掉所有未关闭的窗口。

(2)引用计数

应用计数是最简单的垃圾回收实现:每创建一个对象,计数器加 1,每删除一个则减 1。

class CountedObject : public QObject
{
Q_OBJECT
public:
CountedObject()
{
ctr=0;
}

void attach(QObject *obj)
{
ctr++;
connect(obj, SIGNAL(destroyed(QObject*)), this, SLOT(detach()));
}

public slots:
void detach()
{
ctr--;
if(ctr <= 0)
delete this;
}

private:
int ctr;
};

利用Qt的信号槽机制,在对象销毁的时候自动减少计数器的值。但是,我们的实现并不能防止对象创建的时候调用了两次attach()。

(3)记录所有者

更合适的实现是,不仅仅记住有几个对象持有引用,而且要记住是哪些对象。例如:

class CountedObject : public QObject
{
public:
CountedObject() {}

void attach(QObject *obj) {
// 检查所有者
if(obj == 0)
return;
// 检查是否已经添加过
if(owners.contains(obj))
return;
// 注册
owners.append(obj);
connect(obj, SIGNAL(destroyed(QObject*)), this, SLOT(detach(QObject*)));
}
public slots:
void detach(QObject *obj) {
// 删除
owners.removeAll(obj);
// 如果最后一个对象也被 delete,删除自身
if(owners.size() == 0)
delete this;
}
private:
QList owners;
};

现在我们的实现已经可以做到防止一个对象多次调用 attach() 和 detach() 了。然而,还有一个问题是,我们不能保证对象一定会调用 attach() 函数进行注册。毕竟,这不是 C++ 内置机制。有一个解决方案是,重定义 new 运算符(这一实现同样很复杂,不过可以避免出现有对象不调用 attach() 注册的情况)。

四、总结

Qt 简化了我们对内存的管理,但是,由于它会在不太注意的地方调用 delete,所以,使用时还是要当心。

原文链接:https://blog.csdn.net/taiyang1987912/article/details/29271549

本文福利,费领取Qt开发学习资料包、技术视频,内容包括(C++语言基础,Qt编程入门,QT信号与槽机制,QT界面开发-图像绘制,QT网络,QT数据库编程,QT项目实战,QT嵌入式开发,Quick模块等等)↓↓↓↓↓↓见下面↓↓文章底部点击费领取↓↓

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Qt浅谈之一:内存泄露 的相关文章

  • C# 中的协变和逆变

    首先我要说的是 我是一名正在学习 C 编程的 Java 开发人员 因此 我会将我所知道的与我正在学习的进行比较 我已经使用 C 泛型几个小时了 我已经能够在 C 中重现我在 Java 中知道的相同内容 除了几个使用协变和逆变的示例 我正在读
  • 如何为 Windows 安装开源 Qt 库 5 二进制版本

    这个问题具体是关于Qt libraries 5 0 0 for Windows VS 2010 406 MB at http qt project org downloads http qt project org downloads 但我
  • 关闭 XDOCUMENT 的实例

    我收到这个错误 该进程无法访问文件 C test Person xml 因为它是 被另一个进程使用 IOException 未处理 保存文件内容后如何关闭 xml 文件的实例 using System using System Collec
  • 如何使用汇编获取BIOS时间?

    我正在从头开始实现一个小型操作系统 用于教育目的 现在 我想使用汇编来获取 BIOS 时间 我对此进行了很多搜索 但找不到任何代码示例来执行此操作 如果有人可以提供任何参考或代码示例或与此相关的任何内容 我将非常感激 See 时钟中断 1a
  • 为什么假设 send 可能返回的数据少于在阻塞套接字上传输的请求数据?

    在流套接字上发送数据的标准方法始终是调用 send 并写入一大块数据 检查返回值以查看是否发送了所有数据 然后再次调用 send 直到整个消息被接受 例如 这是一个常见方案的简单示例 int send all int sock unsign
  • C 中的模仿函数重写

    具体来说 函数重写能够调用基本重写方法 这有两部分 一个是预编译的库代码 1 另一个是库的用户代码 2 我在这里实现了一个尽可能最小的经典 Person 和 Employee 示例 非常感谢了解 OOP 概念的铁杆 C 开发人员的回应 我正
  • 特定设备的不同字体大小

    我目前正在开发通用应用程序 我需要分别处理移动设备和桌面的文本框字体大小 我找到了一些方法 但都不能解决问题 使用 VisualStateManager 和 StateTrigger 为例
  • 在“using”语句中使用各种类型 (C#)

    自从C usingstatements只是try finally dispose 的语法糖 为什么它接受多个对象仅当它们属于同一类型时 我不明白 因为它们需要的只是 IDisposable 如果它们都实现 IDisposable 应该没问题
  • 为什么重载方法在 ref 仅符合 CLS 方面有所不同

    公共语言规范对方法重载非常严格 仅允许根据其参数的数量和类型来重载方法 如果是泛型方法 则根据其泛型参数的数量进行重载 根据 csc 为什么此代码符合 CLS 无 CS3006 警告 using System assembly CLSCom
  • 数据损坏 C++ 和 Python 之间的管道

    我正在编写一些代码 从 Python 获取二进制数据 将其通过管道传输到 C 对数据进行一些处理 在本例中计算互信息度量 然后将结果通过管道传输回 Python 在测试时 我发现如果我发送的数据是一组尺寸小于 1500 X 1500 的 2
  • 不要声明只读可变引用类型 - 为什么不呢?

    我一直在阅读这个问题 https stackoverflow com questions 2274412 immutable readonly reference types fxcop violation do not declare r
  • 如何将字符串转换为 Indian Money 格式?

    我正在尝试将字符串转换为印度货币格式 例如如果输入为 1234567 则输出应为 12 34 567 我编写了以下代码 但它没有给出预期的输出 CultureInfo hindi new CultureInfo hi IN string t
  • 理解 C++11 中的 std::atomic::compare_exchange_weak()

    bool compare exchange weak T expected T val compare exchange weak 是 C 11 中提供的比较交换原语之一 它是weak即使对象的值等于 它也会返回 falseexpected
  • 将 bignum 类型结构转换为人类可读字符串的有效方法是什么?

    我有一点问题 为了增长我的 C 知识 我决定尝试实现一个基本的 bigint 库 bigint 结构的核心将是一个 32 位整数数组 选择它们是因为它们适合寄存器 这将允许我在数字之间进行操作 这些操作将在 64 位整数中溢出 这也将适合寄
  • 为什么 C# 接口名称前面加上“I”

    这种命名约定背后的基本原理是什么 我没有看到任何好处 额外的前缀只会污染 API 我的想法与康拉德一致response https stackoverflow com a 222502 9898与此相关的question https sta
  • 如何强制执行特定的 UserControl 设计

    我正在编写一个基本用户控件 它将由一堆其他用户控件继承 我需要对所有这些后代控件强制执行某种设计 例如 顶部必须有几个按钮以及一个或两个标签 后代用户控件区域的其余部分可以自由放置任何内容 最初 我认为我可以将一个面板放到 Base Use
  • c# 替代方案中 cfusion_encrypt 中填充的密钥是什么?

    我找到了从这里复制 C 中的 cfusion encrypt 函数的答案 ColdFusion cfusion encrypt 和 cfusion decrypt C 替代方案 https stackoverflow com questio
  • 使用剪贴板 SetText 换行

    如何使用 SetText 方法添加换行符 I tried Clipboard SetText eee n xxxx 但当我将剪贴板数据粘贴到记事本中时 它没有给我预期的结果 预期结果 eee xxxx 我怎样才能做到这一点 Windows
  • 如何从函数返回矩阵(二维数组)? (C)

    我创建了一个生成宾果板的函数 我想返回宾果板 正如我没想到的那样 它不起作用 这是函数 int generateBoard int board N M i j fillNum Boolean exists True initilize se
  • 如何确定给定方法可以抛出哪些异常?

    我的问题和这个真的一样 找出 C 中方法可能抛出的异常 https stackoverflow com questions 264747 finding out what exceptions a method might throw in

随机推荐

  • 减少域名DNS解析时间将网页加载速度提升新层次-DNS缓存/预读取/拆分域名

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 我们知道在用户访问网站时先得经过域名DNS解析这一过程 可能很多人对于DNS解析时间平常并没有太在意 性能稳定 响应时间快的DNS域名解析服务与不稳定 响应过慢的DNS的域
  • vue中使用(瀑布流)vue-waterfall-easy插件

    参考文档 效果图如下 1 安装 npm install vue waterfall easy save dev 2 引入 import vueWaterfallEasy from vue waterfall easy export defa
  • Nacos配置中心原理(一)客户端部分

    基本概念 配置服务 在服务或者应用运行过程中 提供动态配置或者元数据以及配置管理的服务提供者 配置项 个具体的可配置的参数与其值域 通常以 param key param value 的形式存在 例如我们常 配置系统的日志输出级别 logL
  • OpenCV3.3中主成分分析(Principal Components Analysis, PCA)接口简介及使用

    OpenCV3 3中给出了主成分分析 Principal Components Analysis PCA 的实现 即cv PCA类 类的声明在include opencv2 core hpp文件中 实现在modules core src p
  • SAS9.4安装简易教程(保姆级)附带报错处理

    SAS安装教程 正常安装 环境准备 文件准备及安装 增强编辑器问题 一些报错处理方法 1 安装后处理 解决方案1 解决方案2 2 日期超过了SAS系统的最后截至日期 解决方案 3 逻辑库问题 解决方案 4 sid及产品许可问题 解决方案 卸
  • JT格式介绍(转换)

    JT Jupiter Tessellation 是一种高效 专注于行业且灵活的 ISO 标准化 3D 数据格式 由 Siemens PLM Software 开发 航空航天 汽车工业和重型设备的机械 CAD 领域使用 JT 作为其最领先的
  • 我的世界服务器无限刷东西指令,我的世界无限刷物品命令方块指令

    发布时间 2015 09 11 精华文章推荐 合成表大全 前期生存图文指南 怪物图鉴及属性一览 敖厂长生存解说视频 新手建筑指导班 豪华建筑建造教程 俾斯麦号建造方法 WE建筑辅助教程 创建服务器方法指南 加入服务器联机教程 多 标签 攻略
  • 学习实践-Alpaca-Lora (羊驼-Lora)(部署+运行+微调-训练自己的数据集)

    Alpaca Lora模型GitHub代码地址 1 Alpaca Lora内容简单介绍 三月中旬 斯坦福发布的 Alpaca 指令跟随语言模型 火了 其被认为是 ChatGPT 轻量级的开源版本 其训练数据集来源于text davinci
  • elasticsearch介绍

    什么是elasticsearch Elasticsearch是一个基于Lucene的搜索服务器 它提供了一个分布式多用户能力的全文搜索引擎 基于RESTful web接口 Elasticsearch是用Java语言开发的 并作为Apache
  • 知道这10个让你的API接口突然超时的原因吗?

    前言 不知道你有没有遇到过这样的场景 我们提供的某个API接口 响应时间原本一直都很快 但在某个不经意的时间点 突然出现了接口超时 也许你会有点懵 到底是为什么呢 今天跟大家一起聊聊接口突然超时的10个原因 希望对你会有所帮助 1 网络异常
  • CSS高级用法

    收藏一些css的高级用法 1 黑白图像 这段代码会让你的彩色照片显示为黑白照片 1 2 3 4 5 6 7 img desaturate filter grayscale 100 webkit filter grayscale 100 mo
  • java出现圅_java获取汉字拼音首字母A

    public class GetChinessFirstSpell 汉字拼音首字母列表 本列表包含了20901个汉字 用于配合 GetChineseSpell 函数使用 本表收录的字符的Unicode编码范围为19968至40869 南京
  • mac移动硬盘未装载解决方案

    一 现象 外置移动硬盘桌面不显示 只在磁盘工具应用中置灰显示 坑爹的是你无法进行任何操作只能查看详细信息 二 尝试解决方法 1 尝试了很多修复工具也没有用 包括Tuxera 因为你压根没有装载成功谈何其它操作 2 尝试手动装载 卸载 1 使
  • IC新人必看:芯片设计流程最全讲解!

    对于消费者而言 一个可以使用的系统 有数字集成电路部分 模拟集成电路部分 系统软件及上层应用部分 关于各个部分的功能 借用IC 咖啡胡总的精品图可以一目了然 外部世界是一个模拟世界 故所有需要与外部世界接口的部分都需要模拟集成电路 模拟集成
  • Kafka 入门三问

    目录 1 Kafka 是什么 1 1 背景 1 2 定位 1 3 产生的原因 1 4 Kafka 有哪些特征 消息和批次 模式 主题和分区 生产者和消费者 broker 和 集群 1 5 Kafka 可以做什么 Kafka作为消息系统 Ka
  • java开发中手动设置logback、jvm、容器的时区

    一 Logback xml配置日志输出时区为东八区 1 在日志格式配置后添加 CTT 或 GMT 8
  • electron 调试、问题追踪

    文章目录 前言 一 调试工具 1 生产环境调试工具 bugtron 2 日志 1 业务日志 2 网络日志 3 崩溃报告 二 捕获全局异常 1 开发网页时 2 在electron中全局异常捕获 3 从异常中恢复 保护用户界面 总结 前言 开发
  • Unity-世界坐标与屏幕坐标

    transform position x和transform position y的值含义是世界坐标 世界坐标与屏幕坐标有时一样 有时不同 这和Canvas的渲染模式有关 Canvas共有三种渲染模式 Screen Space Overla
  • 预处理等等

    预处理 define 宏定义是个演技非常高超的替身演员 但也会经常耍大牌的 所以我们用它要慎之又慎 它可以出现在代码的任何地方 从本行宏定义开始 以后的代码就就都认识这个宏了 也可以把任何东西定义成宏 因为编译器会在预编译的时候用真身替换替
  • Qt浅谈之一:内存泄露

    一 简介 Qt内存管理机制 Qt 在内部能够维护对象的层次结构 对于可视元素 这种层次结构就是子组件与父组件的关系 对于非可视元素 则是一个对象与另一个对象的从属关系 在 Qt 中 在 Qt 中 删除父对象会将其子对象一起删除 C 中del