☰
Qt中new与delete使用示例:对象树与父子关系内存管理实战
2026/10/15 1:01:23 网站建设 项目流程

1. 为什么Qt里的new和delete值得单独拎出来聊

刚接触Qt那会儿,我对new和delete的态度就是"用完就删,天经地义"。直到有一次写了个带界面的小工具,跑起来内存曲线一路往上飙,关掉窗口再打开,内存直接翻倍,我才意识到Qt这套对象模型跟标准C++压根不是一回事。Qt里有个东西叫对象树(Object Tree),还有个东西叫父子关系(Parent-Child),这两个机制直接决定了你手里的new出来的对象到底该不该自己delete。

这篇内容就是围绕"qt中new与delete使用示例"这个主题,把我在实际项目里踩过的坑、总结出来的规律,以及可以直接抄作业的代码示例,一次性讲清楚。不管你是刚学Qt的新手,还是写了几年Qt但一直对内存管理模棱两可的老手,看完应该都能有点收获。核心会覆盖:什么时候必须手动delete、什么时候交给Qt自动回收、父子对象析构顺序、栈对象与堆对象的取舍、以及几个容易翻车的典型场景。

先说结论,省得你看到后面才反应过来:在Qt里,只要你给对象指定了parent,并且这个parent本身会被正确销毁,那么你基本不需要手动delete它。但"基本不需要"不等于"永远不需要",边界情况才是真正要命的地方。

2. Qt对象模型与内存管理的底层逻辑

2.1 对象树机制到底是怎么运作的

Qt的QObject及其所有子类,都支持一种叫对象树的结构。当你创建一个QObject派生类对象时,可以给它传一个parent指针。一旦传了,这个对象就会被挂到parent的children列表里。当parent被销毁时,它的析构函数会遍历children列表,逐个delete掉所有子对象。

这个机制的核心代码逻辑大致是这样的(简化版):

QObject::~QObject() { // 遍历所有子对象,逐个删除 for (QObject *child : children()) { delete child; } }

所以你不需要写任何额外代码,只要保证parent被正确销毁,整棵树都会跟着倒。这就是为什么Qt里大量使用new却很少看到delete的原因——不是不删,是Qt帮你删了。

但这里有个关键前提:parent必须是通过delete销毁的,或者parent本身是栈对象在作用域结束时自动析构。如果parent是new出来的又没有parent,那它自己就成了孤儿,没人帮你回收。

2.2 父子关系与析构顺序的微妙之处

父子对象的析构顺序是"先子后父",这个顺序很重要。因为子对象的析构函数里可能还会访问parent的某些成员,如果parent先没了,子对象析构时就会访问野指针。

Qt保证的是:在QObject的析构函数体执行之前,所有子对象已经被删除完毕。也就是说,子对象的析构发生在parent的析构函数体执行之前。这个顺序是安全的。

但如果你在子对象析构函数里访问了parent的成员变量,而parent的成员变量在parent析构函数体里才被释放,那就要小心了。不过这种情况比较少见,大部分时候Qt的默认行为已经够用。

2.3 栈对象与堆对象的取舍逻辑

Qt里创建对象有两种方式:

// 方式一:栈对象 QPushButton btn("Click me"); // 方式二:堆对象 QPushButton *btn = new QPushButton("Click me");

栈对象的好处是自动析构,不需要操心内存。但栈对象有个致命限制:它的生命周期不能超出所在作用域。如果你在一个函数里创建了栈对象,函数返回后它就没了。而Qt的界面组件往往需要在整个窗口生命周期内存在,所以栈对象在GUI编程里用得少。

堆对象配合parent机制,才是Qt里的主流做法。你new一个按钮,指定parent为某个窗口或布局,剩下的交给Qt。窗口关闭时,按钮自动被回收。

注意:栈对象如果被设置了parent,会出现"双重删除"的风险。因为栈对象在作用域结束时自动析构,而parent析构时又会尝试delete它。Qt对此有保护机制,但最好不要这么干。

3. 必须手动delete的几种典型场景

3.1 没有parent的堆对象

这是最常见的情况。你new了一个对象,但没给它parent,那它就不在任何对象树上,Qt不会帮你回收。

// 错误示范:内存泄漏 void createDialog() { QDialog *dlg = new QDialog(); dlg->show(); // 函数结束,dlg指针丢失,但对象还在堆上 }

正确做法有两种:

// 做法一:指定parent void createDialog(QWidget *parent) { QDialog *dlg = new QDialog(parent); dlg->show(); } // 做法二:手动delete(配合WA_DeleteOnClose属性) void createDialog() { QDialog *dlg = new QDialog(); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->show(); // 窗口关闭时自动delete }

WA_DeleteOnClose这个属性特别实用。很多对话框、临时窗口都适合这么处理。设置之后,窗口关闭时会自动delete自己,不需要你手动管理。

3.2 临时计算对象

有些对象只是用来做中间计算,不需要长期存在,也没有合适的parent。这种就老老实实手动delete,或者用智能指针。

void processData() { QFile *file = new QFile("data.txt"); if (file->open(QIODevice::ReadOnly)) { QByteArray data = file->readAll(); // 处理数据 } delete file; // 必须手动删除 }

这种场景其实更推荐用栈对象:

void processData() { QFile file("data.txt"); if (file.open(QIODevice::ReadOnly)) { QByteArray data = file.readAll(); } // 自动析构 }

栈对象在这里完全够用,而且更安全。除非对象很大,栈空间放不下,否则优先考虑栈对象。

3.3 容器中存储的指针

如果你用QList<QObject*>之类的容器存了一堆指针,容器本身不会帮你delete里面的元素。容器析构时只是释放了存储指针的内存,指针指向的对象还在。

QList<QWidget*> widgets; for (int i = 0; i < 10; ++i) { widgets.append(new QWidget()); } // 容器析构后,10个QWidget对象全部泄漏

解决办法要么给它们指定parent,要么在容器销毁前手动遍历delete:

qDeleteAll(widgets); widgets.clear();

qDeleteAll是Qt提供的一个便利函数,等价于对容器里每个元素调用delete。用的时候注意,如果容器里存的是栈对象指针或者已经被删除的指针,会出问题。

3.4 跨线程场景下的对象

跨线程时,对象归属变得复杂。一个对象只能属于创建它的线程,如果要在另一个线程里delete它,必须确保线程安全。

// 在工作线程中创建的对象 QObject *worker = new QObject(); worker->moveToThread(workerThread); // 删除时需要通过信号槽或者invokeMethod QMetaObject::invokeMethod(worker, "deleteLater", Qt::QueuedConnection);

deleteLater()是Qt提供的延迟删除机制。它会在当前事件循环结束后删除对象,比直接delete更安全,特别是在信号槽连接的回调里。

实操心得:在槽函数里直接delete发送信号的对象,是经典的崩溃场景。因为信号发射后,槽函数执行期间对象还在被使用,直接delete会导致后续代码访问已释放内存。用deleteLater()可以避免这个问题。

4. 父子对象机制下的delete实战示例

4.1 标准父子对象创建与自动回收

先看一个最典型的例子,一个窗口里放一个按钮:

#include <QApplication> #include <QWidget> #include <QPushButton> int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget *window = new QWidget(); QPushButton *btn = new QPushButton("Click", window); // window是parent window->show(); int ret = app.exec(); delete window; // 删除window时,btn自动被删除 return ret; }

这里btn的parent是window,所以delete window时,btn会被自动删除。你不需要写delete btn。

如果写成这样:

QWidget *window = new QWidget(); QPushButton *btn = new QPushButton("Click"); // 没有parent window->show(); delete window; // btn不会被删除,内存泄漏

btn没有parent,delete window时它不会被回收。这就是为什么指定parent这么重要。

4.2 布局管理器中的对象归属

用布局管理器时,情况稍微有点不同:

QWidget *window = new QWidget(); QVBoxLayout *layout = new QVBoxLayout(window); // layout的parent是window QPushButton *btn1 = new QPushButton("One"); QPushButton *btn2 = new QPushButton("Two"); layout->addWidget(btn1); // btn1的parent变成window layout->addWidget(btn2); // btn2的parent变成window

当你调用layout->addWidget(btn1)时,btn1会被重新parent到布局所在的窗口上。所以即使你创建btn1时没指定parent,加入布局后它也有了归属。

但注意:layout本身的parent是window,所以delete window时,layout和两个按钮都会被删除。顺序是:先删按钮,再删布局,最后删窗口。

4.3 手动delete子对象的正确姿势

有时候你确实需要提前删除某个子对象,比如动态切换界面:

void replaceButton(QWidget *parent) { QPushButton *oldBtn = parent->findChild<QPushButton*>("myBtn"); if (oldBtn) { delete oldBtn; // 从parent的children列表里移除并删除 } QPushButton *newBtn = new QPushButton("New", parent); newBtn->setObjectName("myBtn"); }

delete oldBtn时,Qt会自动把它从parent的children列表里移除。所以不会出现"parent析构时再次删除已删除对象"的问题。

但如果你只是想把按钮从界面上移除,以后还可能再用,那就不能用delete,而应该用setParent(nullptr)或者hide():

oldBtn->setParent(nullptr); // 从对象树上摘下来,但不删除

4.4 deleteLater的适用场景与原理

deleteLater()的原理是:往事件队列里投递一个删除事件,等当前事件处理完毕后,再执行真正的delete。

QPushButton *btn = new QPushButton("Delete me", window); connect(btn, &QPushButton::clicked, btn, &QPushButton::deleteLater);

点击按钮后,按钮不会立即被删除,而是等当前信号槽处理完毕,事件循环回到主循环时再删除。这样避免了在槽函数执行期间删除发送者导致的崩溃。

适用场景包括:

  • 在槽函数里删除发送信号的对象
  • 在事件处理函数里删除接收事件的对象
  • 跨线程删除对象

不适用场景:

  • 程序退出前的清理(事件循环可能已经停止,deleteLater不会被执行)
  • 需要立即释放内存的场景

注意:deleteLater()依赖事件循环。如果对象所在线程没有运行事件循环,deleteLater()不会生效,对象永远不会被删除。

5. 常见内存问题排查与避坑指南

5.1 双重删除的典型表现与预防

双重删除的症状通常是程序崩溃,报错信息里可能有"double free"或"invalid pointer"。常见原因:

  • 栈对象被设置了parent,作用域结束时自动析构,parent析构时又删一次
  • 手动delete了有parent的对象,parent析构时又删一次(Qt对此有保护,但某些情况下仍会出问题)
  • 多个地方持有同一个指针,都尝试delete

预防方法:

  • 有parent的对象不要手动delete
  • 用QPointer跟踪对象,它在对象被删除后会自动置空
  • 用QSharedPointer或std::shared_ptr管理共享所有权
QPointer<QPushButton> btn = new QPushButton("Test", window); // 某处delete btn后 if (btn) { btn->setText("Still alive"); // 不会执行到这里 }

5.2 内存泄漏的检测方法

Qt本身没有内置的内存泄漏检测工具,但可以借助外部工具:

  • Linux下用Valgrind
  • Windows下用Visual Studio的诊断工具
  • 跨平台可以用AddressSanitizer

编译时加上调试符号,运行程序,退出后查看报告。重点关注new了但没有对应delete的地方。

另外,Qt Creator自带一个简单的内存分析器,可以在调试模式下查看堆内存变化。

5.3 对象树断裂导致的泄漏

对象树断裂是指:某个对象本来有parent,但中途被setParent(nullptr)摘下来了,之后又没有被重新挂到别的parent上,也没有手动delete。

QWidget *child = new QWidget(parent); child->setParent(nullptr); // 从对象树上摘下来 // 忘记处理child,泄漏

这种问题比较隐蔽,因为代码看起来"有parent",但实际上已经断了。排查时可以用QObject::dumpObjectTree()打印对象树,看看有没有孤儿对象。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
程序退出时崩溃双重删除检查栈对象是否设了parent移除栈对象的parent或改用堆对象
内存持续增长对象无parent且未delete用内存分析工具定位指定parent或手动delete
槽函数执行中崩溃在槽里delete了发送者检查connect的槽函数改用deleteLater
跨线程删除崩溃在非对象所属线程delete检查delete所在线程用deleteLater或invokeMethod
对象树打印有孤儿setParent(nullptr)后未处理dumpObjectTree重新指定parent或手动delete

6. 现代C++与Qt结合的内存管理实践

6.1 智能指针在Qt中的适用边界

Qt的对象树机制和C++智能指针有时候会打架。比如:

std::unique_ptr<QPushButton> btn(new QPushButton("Test", window));

这里btn是unique_ptr,同时又有parentwindow。当window析构时,它会deletebtn指向的对象,然后unique_ptr析构时又会delete一次,双重删除。

所以不要对已经有parent的QObject使用智能指针。智能指针适合管理没有parent的对象,或者非QObject类型。

// 正确用法:没有parent的对象 std::unique_ptr<QFile> file(new QFile("data.txt")); // 正确用法:非QObject类型 std::shared_ptr<QImage> image = std::make_shared<QImage>(100, 100, QImage::Format_RGB32);

6.2 QScopedPointer与QPointer的选用

QScopedPointer类似于std::unique_ptr,但更轻量,适合管理没有parent的QObject:

QScopedPointer<QFile> file(new QFile("data.txt")); if (file->open(QIODevice::ReadOnly)) { // 使用file } // 作用域结束自动delete

QPointer则是一种弱引用,它不管理生命周期,只是在对象被删除后自动置空:

QPointer<QPushButton> btn = new QPushButton("Test", window); // 某处delete btn后 if (btn.isNull()) { // 对象已被删除 }

QPointer适合在不确定对象是否还活着的时候使用,比如跨模块传递指针。

6.3 自定义类的内存管理注意事项

如果你自己写了一个继承自QObject的类,需要注意:

  • 构造函数里传parent给基类
  • 析构函数里不要手动delete有parent的子对象
  • 如果类里有非QObject的堆成员,记得在析构函数里delete
class MyWidget : public QWidget { public: MyWidget(QWidget *parent = nullptr) : QWidget(parent) , m_data(new int[100]) // 非QObject成员 { m_button = new QPushButton("Click", this); // 有parent,不用手动删 } ~MyWidget() { delete[] m_data; // 必须手动释放 // m_button不需要手动删,QWidget析构时会处理 } private: int *m_data; QPushButton *m_button; };

6.4 项目中的内存管理规范建议

根据我自己的经验,团队里最好定几条简单规则:

  1. 所有QObject派生类对象,创建时尽量指定parent
  2. 没有parent的临时对象,优先用栈对象或智能指针
  3. 禁止对已有parent的对象使用智能指针
  4. 槽函数里删除对象一律用deleteLater()
  5. 跨线程对象删除必须通过事件循环
  6. 定期用内存分析工具跑一遍,别等出问题才查

这些规则看起来简单,但真到写代码的时候,尤其是赶进度的时候,很容易忘。我的做法是在代码审查清单里加上这几条,每次提交前过一遍。

7. 几个真实项目中的踩坑记录

7.1 动态界面切换导致的内存泄漏

之前做过一个多页面切换的工具,每个页面都是动态创建的。最初写法是:

void switchPage(int index) { if (m_currentPage) { m_currentPage->hide(); // 忘记delete,以为hide就够了 } m_currentPage = createPage(index); m_currentPage->show(); }

结果每切换一次页面,内存就涨一点。因为旧页面只是hide了,对象还在。后来改成:

void switchPage(int index) { if (m_currentPage) { m_currentPage->deleteLater(); m_currentPage = nullptr; } m_currentPage = createPage(index); m_currentPage->show(); }

用deleteLater()而不是直接delete,是因为切换页面的操作可能是在某个按钮的槽函数里触发的,直接delete可能导致按钮对象被删除后代码继续访问。

7.2 信号槽连接未断开导致的悬空指针

Qt的信号槽有个特点:如果接收者被删除,连接会自动断开。但如果发送者被删除,而接收者还在,接收者不会收到通知。

QPushButton *btn = new QPushButton("Test"); connect(btn, &QPushButton::clicked, this, &MyClass::onClick); delete btn; // 连接自动断开,没问题 // 但如果是这样: MyClass *receiver = new MyClass(); connect(btn, &QPushButton::clicked, receiver, &MyClass::onClick); delete receiver; // 连接自动断开 delete btn; // 没问题

Qt在QObject析构时会自动断开所有相关连接,所以正常情况下不会出现悬空指针。但如果你用了lambda作为槽,并且lambda里捕获了外部指针,那就要自己保证捕获的指针有效。

7.3 容器析构顺序引发的崩溃

有个项目里用了全局容器存对象指针:

static QList<QWidget*> g_widgets; // 某个地方 g_widgets.append(new QWidget()); // 程序退出时 // g_widgets析构,但里面的QWidget对象没被删除

更麻烦的是,如果这些QWidget有parent,而parent在g_widgets之前析构,那g_widgets里存的就是悬空指针。程序退出时如果还访问g_widgets,就会崩溃。

解决办法是避免使用全局容器存QObject指针,或者确保容器在对象树之前清理。

7.4 跨线程deleteLater不生效的问题

有次在工作线程里创建了一个QObject,然后调用deleteLater(),结果对象一直没被删除。原因是工作线程没有运行事件循环。

QThread *thread = new QThread(); QObject *worker = new QObject(); worker->moveToThread(thread); thread->start(); // 在工作线程里 worker->deleteLater(); // 如果线程没有exec(),这行不会生效

解决办法是在线程里运行事件循环:

void workerThread() { QObject *worker = new QObject(); // ... worker->deleteLater(); exec(); // 启动事件循环,deleteLater才会被执行 }

或者直接用delete worker,但要确保没有其他线程在访问它。

8. 写在最后的一些个人体会

Qt的内存管理说复杂也复杂,说简单也简单。核心就一句话:有parent的交给Qt,没parent的自己管。但实际项目里,边界情况往往比想象的多。

我自己的习惯是,写代码的时候先想清楚这个对象的生命周期:它跟谁同生共死?如果跟某个窗口同生共死,那就给它指定那个窗口做parent。如果是临时的,那就用栈对象。如果确实需要堆对象又没有合适的parent,那就用智能指针或者手动delete。

另外,deleteLater()是个好东西,但别滥用。它依赖事件循环,在程序退出阶段可能不执行。该直接delete的时候就直接delete,该用deleteLater()的时候再用。

最后分享一个小技巧:如果你不确定某个对象有没有被正确回收,可以在析构函数里加一行日志,或者用QObject::dumpObjectTree()在程序退出前打印对象树。这样能直观地看到哪些对象还活着,哪些已经被回收了。我刚开始学Qt的时候就是这么干的,比看文档管用多了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询