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 } // 作用域结束自动deleteQPointer则是一种弱引用,它不管理生命周期,只是在对象被删除后自动置空:
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 项目中的内存管理规范建议
根据我自己的经验,团队里最好定几条简单规则:
- 所有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 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的时候就是这么干的,比看文档管用多了。