手动管理内存,本质是在跟"忘记释放"和"异常打断"两件事较劲。一段代码new了两块内存,中间某个函数调用抛了异常,后面的delete永远执行不到,泄漏就发生了。智能指针解决的就是这个场景------让资源有确定的、不依赖正常流程的释放时机。
目录
[1. 资源泄漏的典型场景](#1. 资源泄漏的典型场景)
[2. RAII:把资源绑在对象生命周期上](#2. RAII:把资源绑在对象生命周期上)
1. 资源泄漏的典型场景
下面这段代码很直观地展示了问题:Func里new了两个数组,然后调用Divide。如果除零抛异常,后面正常的delete不会被执行。即便用try/catch包住,如果在array2的new时也抛异常,那array1就已经泄漏了------catch里根本不知道array2有没有分配成功。
cpp
void Func() {
int* array1 = new int[10];
int* array2 = new int[10]; // 这一行也可能抛异常
try {
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
} catch (...) {
delete[] array1;
delete[] array2;
throw; // 清理后重新抛出
}
delete[] array1;
delete[] array2;
}
手动一层层套try/catch来保释放,代码臃肿不说,逻辑一复杂就容易漏。这个问题不是"程序员不细心",而是一种结构性缺陷------资源释放依赖正常控制流走到,而异常偏偏打断了控制流。
2. RAII:把资源绑在对象生命周期上
RAII(Resource Acquisition Is Initialization)的思路很简单:在构造函数里获取资源,在析构函数里释放资源。对象在栈上分配,离开作用域时析构函数一定会被调用,不管你是正常return还是异常抛出去。这个"一定"是C++语言保证的,用户不用写哪怕一行try/catch来管释放。
cpp
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr) : _ptr(ptr) {}
~SmartPtr() {
cout << "delete " << _ptr << endl;
delete[] _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
T& operator[](size_t i) { return _ptr[i]; }
private:
T* _ptr;
};
用这个最简单的SmartPtr改造Func:
cpp
void Func() {
SmartPtr<int> sp1(new int[10]);
SmartPtr<int> sp2(new int[10]);
for (size_t i = 0; i < 10; ++i)
sp1[i] = sp2[i] = i;
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
// 无论正常结束还是异常退出,sp1/sp2析构,资源释放
不需要try/catch了。Divide抛异常,栈展开析构sp2再析构sp1,两个数组被delete干净。代码量反而更少。这就是RAII对资源安全的根本价值------把不确定的手动释放变成确定的自动析构。
智能指针本质上就是RAII在指针资源上的具体应用。除了自动释放,它还要解决另一个问题:如何像原生指针一样去访问资源?于是有了operator*、operator->、operator[]等运算符重载。这两点构成了智能指针的两个基本面:资源管理与指针模拟。
RAII并不只用于内存。文件句柄、网络连接、互斥锁,凡是"获取-使用-释放"的资源,都可以用同样的思路管理。等介绍shared_ptr配合自定义删除器时,会看到一个fopen的例子。