3. new与delete的黄金法则:谁分配谁释放、RAII原则在Qt中的应用

内存泄漏,说白了就是「借了东西不还」。

你 new 了一块内存,用完了没 delete,这块内存就永远「失联」了。程序跑得越久,吃内存越多,最后界面卡死、崩溃,用户直接骂娘。

我早年接手过一个 Qt 项目,界面操作半小时后就开始掉帧,点个按钮要等两秒。一查内存,好家伙,每个对话框关闭时都漏掉了几十 KB,积少成多,最后把堆给撑爆了。

所以今天咱们就聊聊,怎么用「谁分配谁释放」和 RAII 这两把刀,把内存泄漏斩草除根。

3.1 黄金法则:谁分配,谁释放

这条规则听起来简单,做起来却容易忘。

你想想看,你在函数 A 里 new 了一个对象,然后把它传给了函数 B。函数 B 用完了,谁来 delete?

答案是:谁 new 的,谁负责 delete。

核心原则:分配内存的代码块,必须负责释放内存。不要指望别人替你擦屁股。

举个例子:

// 错误示范:A new,B delete,责任不清
void A() {
    int* p = new int(42);
    B(p);  // 把 p 交给 B,但 B 不一定知道要 delete
}

void B(int* ptr) {
    // 用一下 ptr
    // 忘了 delete?或者不知道要不要 delete?
}

我个人的习惯是:谁 new,谁就在同一作用域内安排 delete。如果实在要跨函数传递,那就用智能指针,把所有权说清楚。

// 正确做法:谁分配谁释放
void A() {
    int* p = new int(42);
    // 使用 p
    delete p;  // 同一作用域内释放
    p = nullptr;
}

注意:delete 之后记得把指针置为 nullptr,防止野指针。虽然这不是强制要求,但能避免很多「悬空指针」的坑。

3.2 RAII:让析构函数替你「擦屁股」

RAII 的全称是 Resource Acquisition Is Initialization,翻译过来就是「资源获取即初始化」。名字挺绕,但思想很简单:把资源的生命周期绑定到对象的生命周期上。

对象构造时获取资源,对象析构时释放资源。你只管用,不用管释放。

为什么会这样?因为 C++ 保证局部对象在离开作用域时一定会调用析构函数。这是语言层面的铁律。

class Buffer {
public:
    Buffer(size_t size) {
        data = new char[size];
        m_size = size;
    }
    ~Buffer() {
        delete[] data;  // 析构时自动释放
        data = nullptr;
    }
private:
    char* data;
    size_t m_size;
};

void useBuffer() {
    Buffer buf(1024);  // 构造时分配
    // 使用 buf...
    // 函数结束,buf 析构,自动释放内存
}

你看,不用手动 delete,代码干净多了。我在项目中遇到最多的内存泄漏,就是「忘记写析构函数」或者「析构函数里忘了释放」。RAII 直接从根源上解决了这个问题。

3.3 RAII 在 Qt 中的应用

Qt 本身对 RAII 有很好的支持,但很多人没用好。

Qt 的对象树机制,本质上就是一种 RAII 的变体。当你把 QWidget 或 QObject 的子对象挂到父对象上时,父对象析构时会自动删除所有子对象。

// Qt 对象树:父对象负责释放子对象
QWidget* mainWidget = new QWidget();
QPushButton* btn = new QPushButton("Click me", mainWidget);
// btn 是 mainWidget 的子对象
// mainWidget 析构时,btn 会被自动 delete

嗯,这里要注意:对象树只适用于 QObject 体系。如果你在 Qt 项目里用了 raw pointer 管理非 QObject 资源,那还是得自己操心。

我建议的做法是:

  • 能用 Qt 对象树就用对象树,省心省力。
  • 对于非 QObject 资源(比如 OpenGL 纹理、文件句柄、网络连接),自己封装 RAII 类。
  • 避免混用:不要既用对象树又手动 delete,容易 double free。

小技巧:在 Qt 中,如果你 new 了一个 QObject 子类对象,并且没有指定父对象,记得在合适的时机手动 delete。或者直接用 QScopedPointer、QSharedPointer 等智能指针来管理。

3.4 实战:用 RAII 封装 Qt 资源

我曾经写过一个图像处理模块,需要频繁创建和销毁 QPixmap。一开始直接 new/delete,结果内存碎片化严重,界面越来越卡。

后来我封装了一个 RAII 风格的图片资源类:

class ScopedPixmap {
public:
    ScopedPixmap(const QString& path) {
        m_pixmap = new QPixmap(path);
        if (m_pixmap->isNull()) {
            // 加载失败处理
            delete m_pixmap;
            m_pixmap = nullptr;
        }
    }
    
    ~ScopedPixmap() {
        delete m_pixmap;  // 自动释放
        m_pixmap = nullptr;
    }
    
    QPixmap* get() const { return m_pixmap; }
    
    // 禁止拷贝,防止 double delete
    ScopedPixmap(const ScopedPixmap&) = delete;
    ScopedPixmap& operator=(const ScopedPixmap&) = delete;
    
private:
    QPixmap* m_pixmap;
};

// 使用
void processImage() {
    ScopedPixmap pix(":/images/photo.png");
    if (pix.get()) {
        // 处理图片...
    }
    // 函数结束,pix 析构,QPixmap 自动释放
}

你看,代码量没增加多少,但内存安全系数直线上升。这就是 RAII 的魅力——把「记得释放」这个心理负担,交给编译器去扛。

3.5 避坑指南

我曾经踩过一个坑:在 Qt 的槽函数里 new 了一个 QDialog,然后忘了设置 Qt::WA_DeleteOnClose 属性。结果对话框关闭后,内存没释放,每次打开都 leak 一次。

解决方案很简单:

void showDialog() {
    QDialog* dlg = new QDialog(this);
    dlg->setAttribute(Qt::WA_DeleteOnClose);  // 关闭时自动 delete
    dlg->show();
}

还有一点:不要混用 malloc/free 和 new/delete。Qt 内部用的是 new/delete,你如果混用,轻则内存泄漏,重则程序崩溃。

警告:在 Qt 项目中,尽量统一使用 new/delete。如果必须用 C 风格的内存管理,请确保封装在 RAII 类中,不要让 raw pointer 到处飞。

3.6 本章小结

「谁分配谁释放」是底线,RAII 是武器。两者结合,基本能干掉 90% 的内存泄漏。

我个人习惯是:能用栈对象就不用堆对象,能用智能指针就不用 raw pointer,能用 Qt 对象树就绝不手动管理。

你想想看,代码写出来是给人看的,也是给机器跑的。与其每天提心吊胆地数 new 和 delete 有没有配对,不如让 RAII 替你兜底。

一句话总结:资源管理不是靠记忆力,而是靠机制。RAII 就是 C++ 给你最好的机制,别浪费了。

内存管理黄金法则与RAII 谁分配谁释放 黄金法则详解 同一作用域配对 避免跨函数传递 RAII原则 构造获取资源 析构释放资源 Qt中的实践 对象树机制 RAII封装资源 智能指针辅助 目标:消灭内存泄漏,提升界面稳定性
可可精品资料库