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 容器中存储的指针如果你用QListQObject*之类的容器存了一堆指针容器本身不会帮你delete里面的元素。容器析构时只是释放了存储指针的内存指针指向的对象还在。QListQWidget* widgets; for (int i 0; i 10; i) { widgets.append(new QWidget()); } // 容器析构后10个QWidget对象全部泄漏解决办法要么给它们指定parent要么在容器销毁前手动遍历deleteqDeleteAll(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没有parentdelete 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-findChildQPushButton*(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管理共享所有权QPointerQPushButton btn new QPushButton(Test, window); // 某处delete btn后 if (btn) { btn-setText(Still alive); // 不会执行到这里 }5.2 内存泄漏的检测方法Qt本身没有内置的内存泄漏检测工具但可以借助外部工具Linux下用ValgrindWindows下用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或手动delete6. 现代C与Qt结合的内存管理实践6.1 智能指针在Qt中的适用边界Qt的对象树机制和C智能指针有时候会打架。比如std::unique_ptrQPushButton btn(new QPushButton(Test, window));这里btn是unique_ptr同时又有parentwindow。当window析构时它会deletebtn指向的对象然后unique_ptr析构时又会delete一次双重删除。所以不要对已经有parent的QObject使用智能指针。智能指针适合管理没有parent的对象或者非QObject类型。// 正确用法没有parent的对象 std::unique_ptrQFile file(new QFile(data.txt)); // 正确用法非QObject类型 std::shared_ptrQImage image std::make_sharedQImage(100, 100, QImage::Format_RGB32);6.2 QScopedPointer与QPointer的选用QScopedPointer类似于std::unique_ptr但更轻量适合管理没有parent的QObjectQScopedPointerQFile file(new QFile(data.txt)); if (file-open(QIODevice::ReadOnly)) { // 使用file } // 作用域结束自动deleteQPointer则是一种弱引用它不管理生命周期只是在对象被删除后自动置空QPointerQPushButton btn new QPushButton(Test, window); // 某处delete btn后 if (btn.isNull()) { // 对象已被删除 }QPointer适合在不确定对象是否还活着的时候使用比如跨模块传递指针。6.3 自定义类的内存管理注意事项如果你自己写了一个继承自QObject的类需要注意构造函数里传parent给基类析构函数里不要手动delete有parent的子对象如果类里有非QObject的堆成员记得在析构函数里deleteclass 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 项目中的内存管理规范建议根据我自己的经验团队里最好定几条简单规则所有QObject派生类对象创建时尽量指定parent没有parent的临时对象优先用栈对象或智能指针禁止对已有parent的对象使用智能指针槽函数里删除对象一律用deleteLater()跨线程对象删除必须通过事件循环定期用内存分析工具跑一遍别等出问题才查这些规则看起来简单但真到写代码的时候尤其是赶进度的时候很容易忘。我的做法是在代码审查清单里加上这几条每次提交前过一遍。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 QListQWidget* 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的时候就是这么干的比看文档管用多了。