3、RAII与智能指针:RAII设计模式、unique_ptr使用、shared_ptr与weak_ptr、循环引用问题
内存泄漏,说白了就是「申请了资源,忘了还」。在C++里,这问题尤其突出——你new了一个对象,但delete被漏掉了,或者因为异常跳过了。我早年做Qt项目时,就吃过这个亏。一个对话框反复打开关闭,内存蹭蹭往上涨,最后用户反馈「软件用半天就卡死了」。查了半天,就是个new出来的QWidget没delete。
后来我彻底拥抱了RAII。嗯,这玩意儿救了我无数次。
RAII设计模式:资源就是对象
RAII的全称是Resource Acquisition Is Initialization。名字挺绕,但核心思想很简单:资源在构造函数中获取,在析构函数中释放。你想想看,C++的局部对象在离开作用域时会自动调用析构函数,那我们把资源的释放放在析构里,不就自动了吗?
核心原则: 资源的所有权与对象的生命周期绑定。对象活着,资源就在;对象死了,资源自动释放。
我在项目中遇到过一种情况:一个函数里开了文件、申请了内存、还锁了互斥量。如果中间抛出异常,后面的清理代码根本不会执行。用RAII包装一下,每个资源都是一个栈对象,异常发生时栈展开,析构函数自动调用——干干净净,不留痕迹。
Qt里其实到处都是RAII的影子。比如QFile、QMutexLocker,都是典型的RAII类。你打开文件,析构时自动关闭;你锁住互斥量,析构时自动解锁。这就是RAII的魅力。
unique_ptr:独占所有权
unique_ptr是C++11引入的智能指针。它的特点是:一个对象只能被一个unique_ptr拥有。不能拷贝,只能移动。说白了,就是「这把钥匙只有你一个人有,别人拿不走」。
#include <memory>
std::unique_ptr<int> p1(new int(42));
// std::unique_ptr<int> p2 = p1; // 编译错误!不能拷贝
std::unique_ptr<int> p2 = std::move(p1); // 可以移动,p1变为空
我个人习惯在Qt项目里用unique_ptr管理那些「生命周期明确」的对象。比如一个对话框,只在某个函数里使用,用完就销毁。用unique_ptr包装,连delete都不用写。
小技巧: 用std::make_unique创建对象,比直接new更安全、更简洁。C++14开始支持,Qt 5.7以上也兼容。
auto dialog = std::make_unique<QDialog>(this);
dialog->exec();
// 函数结束,dialog自动销毁
unique_ptr的性能开销几乎为零。它内部只保存一个指针,析构时调用delete。你想想看,这比手写new/delete还省心,而且不会漏。
shared_ptr与weak_ptr:共享所有权
有时候,一个对象需要被多个地方共享。比如一个全局配置对象,好几个模块都要用。这时候shared_ptr就派上用场了。
shared_ptr使用引用计数来管理生命周期。每拷贝一次,计数加1;每析构一次,计数减1。当计数降到0时,对象被删除。听起来很完美,对吧?
但有个坑——循环引用。我当年就栽过跟头。
注意: 如果两个对象互相持有对方的shared_ptr,它们的引用计数永远不会降到0,内存泄漏就发生了。
看个例子:
struct Node {
std::shared_ptr<Node> next;
// 如果两个Node互相指向,就形成了循环引用
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // 循环引用!a和b都不会被释放
怎么破?用weak_ptr。weak_ptr是一种「弱引用」,它不增加引用计数。它只能从shared_ptr构造,而且不能直接访问对象,需要先lock()成shared_ptr。
struct Node {
std::weak_ptr<Node> next; // 改用weak_ptr
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // 没问题了!weak_ptr不增加计数
weak_ptr的典型应用场景就是打破循环引用。比如Qt里的父子对象关系,父对象持有子对象的shared_ptr,子对象用weak_ptr指向父对象。这样父对象销毁时,子对象能感知到,不会出现野指针。
循环引用问题:避坑指南
我曾经在一个网络库项目里,用shared_ptr管理连接对象和会话对象。连接对象持有会话对象的shared_ptr,会话对象也持有连接对象的shared_ptr。结果程序跑了一晚上,内存涨了2个G。查了半天,就是循环引用。
后来我总结了几条经验:
- 能用unique_ptr就别用shared_ptr。独占所有权更清晰,性能更好。
- 如果必须用shared_ptr,先想想有没有循环引用的可能。父子关系、双向链表、观察者模式,都是高发区。
- 用weak_ptr打破循环。记住:weak_ptr是「观察者」,不参与生命周期管理。
- Qt的父子对象机制本身就有内存管理。如果父对象是QObject,子对象设置父对象后,父对象析构时会自动删除子对象。这时候再用智能指针反而容易乱。
我的建议: 在Qt项目中,优先使用Qt的父子对象机制管理界面组件。对于非QObject的资源(比如大数据缓冲区、第三方库对象),用unique_ptr或shared_ptr。别混用,否则容易出问题。
嗯,说到这,我想起一个更隐蔽的问题。shared_ptr的引用计数是线程安全的,但对象本身的访问不是。如果你在多线程环境下用shared_ptr,记得加锁,或者用atomic操作。不然引用计数没问题,但对象数据可能被撕成碎片。
最后,画个图总结一下RAII和智能指针的关系:
这张图把RAII和三种智能指针的关系理清楚了。RAII是指导思想,智能指针是具体工具。unique_ptr管独占,shared_ptr管共享,weak_ptr管观察。各司其职,别混用。
好了,这一章就到这。记住:资源管理不是小事,RAII和智能指针是你的护身符。下次写代码,先想想「这个资源谁来释放?」——用RAII,你就不用想了。
一句话总结: 用unique_ptr管独占资源,用shared_ptr管共享资源,用weak_ptr打破循环引用。别手写new/delete,除非你真的很清楚自己在做什么。
可可精品资料库