在 C++ 开发中,动态内存管理一直是容易出错的环节。资源申请后忘记释放、异常导致清理代码未执行、对象之间循环引用等问题,都可能引发内存泄漏甚至程序崩溃。为了更安全地管理资源,C++ 引入了 RAII 思想和智能指针机制。本文将从资源管理面临的问题出发,逐步介绍智能指针的设计思路、使用方式以及底层实现原理,并结合实际案例分析循环引用、线程安全等常见问题,帮助理解现代 C++ 中资源管理的核心思想。
一、智能指针的使用场景分析
智能指针这一章,核心不是先记接口,而是先明白它为什么会出现。
最常见的场景就是:资源已经申请成功,但中途发生异常,导致释放代码没有执行,最后形成内存泄漏。普通 new 和 delete 的写法,在没有异常时看起来没有问题,一旦中间某一步抛异常,后面的清理代码就可能走不到。
例如:
cpp
double Divide(int a, int b)
{
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
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;
}
这段代码的问题在于,array1 和 array2 申请后,Divide、输入操作、new 本身都有可能抛异常。只要中间某一步出错,手动 delete 的逻辑就会被打断。
智能指针的作用,就是把这类资源的释放动作交给对象自己管理,让释放动作跟着对象生命周期自动执行。
二、RAII和智能指针的设计思路
2.1 RAII是什么
RAII 是 Resource Acquisition Is Initialization 的缩写,意思是:资源获取即初始化。
通俗一点说,就是把资源交给一个对象管:
- 对象活着,资源就活着
- 对象析构,资源就释放
RAII 可以管理的资源不只是内存,还包括:
- 文件句柄
- 网络连接
- 互斥锁
- 数据库连接
- 其他需要成对释放的资源
它的核心价值是:把资源释放责任交给析构函数,让异常不再轻易导致泄漏。
2.2 智能指针的设计思路
智能指针本质上就是一种 RAII 类。它除了负责资源释放,还要尽量像普通指针一样好用,所以通常还会重载:
operator*operator->operator[]
下面是一个简化版智能指针:
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;
};
这段代码的关键点
- 构造时接管资源
- 析构时自动释放资源
- 重载运算符,让它像普通指针一样访问资源
这就是智能指针最核心的设计思路。
2.3 用智能指针优化代码
cpp
double Divide(int a, int b)
{
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
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;
}
这里最重要的变化是:
- 不再手写
delete[] - 异常发生时,智能指针对象析构,资源自动释放
- 代码结构更清晰
三、C++标准库智能指针的使用
C++ 标准库中的智能指针都在 <memory> 头文件里。
常见的有:
auto_ptrunique_ptrshared_ptrweak_ptr
其中 auto_ptr 已经不建议使用,现代代码基本都用 unique_ptr 和 shared_ptr,weak_ptr 则是配合 shared_ptr 解决循环引用。
3.1 auto_ptr
auto_ptr 是 C++98 的产物,它的设计思路是:拷贝时转移资源管理权。
这个设计最大的问题是:被拷贝对象会悬空,极容易出错,所以 C++11 之后强烈不推荐使用。
cpp
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1);
// ap1 已经悬空
这个行为太危险,现代项目基本不用。
3.2 unique_ptr
unique_ptr 是 C++11 引入的,名字就说明了它的特点:唯一拥有。
它的特点很明确:
- 不支持拷贝
- 支持移动
- 适合独占资源的场景
cpp
unique_ptr<Date> up1(new Date);
//unique_ptr<Date> up2(up1);
unique_ptr<Date> up3(move(up1));
注意:move 之后,原对象通常也会变成空状态,所以移动操作虽然高效,但使用时要注意对象后续是否还会被访问。
3.3 shared_ptr
shared_ptr 是共享指针,核心特点是:
- 支持拷贝
- 支持移动
- 使用引用计数管理资源
- 多个对象可以共同拥有同一份资源
cpp
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
use_count() 可以查看当前有多少个 shared_ptr 在共享这份资源。
3.4 weak_ptr
weak_ptr 叫弱指针,它和前面几种指针有本质区别:
- 不参与资源管理
- 不增加引用计数
- 不直接释放资源
- 主要用途是打破
shared_ptr的循环引用
weak_ptr 不能直接像普通智能指针那样用 * 和 -> 访问资源,因为它本身不负责资源生命周期。
3.5 删除器
智能指针默认会用 delete 释放资源,但有些资源不是 new 出来的,比如:
new[]fopen- 自定义资源句柄
这时就需要自定义删除器,也就是一个可调用对象,告诉智能指针该怎么释放资源。
数组删除器
cpp
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
文件删除器
cpp
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
使用示例
cpp
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
auto delArrOBJ = [](Date* ptr) { delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
这里最重要的是:
unique_ptr的删除器在模板参数里shared_ptr的删除器在构造函数参数里new[]管理数组时,默认删除方式不能写错
3.6 make_shared
shared_ptr 还支持 make_shared,它可以直接传构造参数来创建对象。
cpp
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
auto sp3 = make_shared<Date>(2024, 9, 11);
相比直接 new,make_shared 更推荐使用, 它相对于速度更快,更加安全,有原子性。
3.7 operator bool
shared_ptr 和 unique_ptr 都支持布尔上下文判断:
cpp
shared_ptr<Date> sp4;
if (sp1)
cout << "sp1 is not nullptr" << endl;
if (!sp4)
cout << "sp4 is nullptr" << endl;
它的意义是:
- 有资源时返回 true
- 空对象时返回 false
这让智能指针可以直接用于 if 判断。
3.8 explicit
shared_ptr 和 unique_ptr 的构造函数通常都用了 explicit。
目的很简单:
防止普通指针自动转换成智能指针
比如下面这种写法会报错:
cpp
shared_ptr<Date> sp5 = new Date(2024, 9, 11);
unique_ptr<Date> sp6 = new Date(2024, 9, 11);
这是为了避免隐式转换带来的歧义和风险。
四、智能指针的原理

4.1 auto_ptr 的实现思路
auto_ptr 的核心思想是:拷贝时把资源管理权从原对象转移给新对象。
cpp
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
这个设计的问题
拷贝以后原对象会被掏空,容易出现悬空指针,所以这个设计后来被放弃了。
4.2 unique_ptr 的实现思路
unique_ptr 的思想更干净:
- 不允许拷贝
- 只允许移动
- 独占资源
cpp
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
unique_ptr(unique_ptr<T>&& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& sp)
{
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
return *this;
}
private:
T* _ptr;
};
这个实现的核心就是两个字:独占。
4.3 shared_ptr 的实现思路
shared_ptr 的重点在于引用计数。
核心逻辑
- 创建对象时,引用计数初始化为 1
- 拷贝一个
shared_ptr,计数加 1 - 析构一个
shared_ptr,计数减 1 - 当计数减到 0 时,真正释放资源
cpp
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _del(sp._del)
{
++(*_pcount);
}
void release()
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
~shared_ptr()
{
release();
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) { delete ptr; };
};
这段实现最关键的地方
- 资源指针
_ptr - 引用计数
_pcount - 删除器
_del - 析构时调用
release()
这就是 shared_ptr 的本质。
4.4 weak_ptr 的实现思路
weak_ptr 的设计目标不是管理资源,而是观察资源。
cpp
template<class T>
class weak_ptr
{
public:
weak_ptr()
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr = nullptr;
};
它不负责释放资源,所以也不参与引用计数增长。
五、shared_ptr和weak_ptr
5.1 shared_ptr循环引用问题
shared_ptr 很好用,但有一个经典问题:循环引用。
典型场景是双向链表或图结构。两个对象互相持有对方的 shared_ptr 时,引用计数永远不可能归零,资源就不会释放。

cpp
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
cpp
int main()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
为什么会泄漏
因为:
n1持有n2n2持有n1- 两边都认为自己还有人在用
- 所以都不会释放
注意,这种泄漏不是忘记 delete,而是设计结构上形成了互相依赖。
5.2 weak_ptr
weak_ptr 的作用就是打破这种循环引用。
它的特点是:
- 不增加引用计数
- 不能直接管理资源
- 可以观察资源是否还存在
- 可以通过
lock()临时转成shared_ptr
常用接口
expired():判断资源是否过期use_count():查看当前引用计数lock():获取一个可用的shared_ptr
cpp
int main()
{
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
wp = sp1;
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
return 0;
}
关键理解
weak_ptr 不是用来直接用的,而是用来辅助 shared_ptr 管理复杂关系。
六、shared_ptr的线程安全问题
shared_ptr 的引用计数通常放在堆上,多个线程同时拷贝和析构同一个 shared_ptr 时,引用计数的增减就可能发生竞争。
这意味着:
- 引用计数本身不是天然线程安全的
- 需要加锁
- 或者改成原子计数
例子
cpp
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA()
{
cout << "~AA()" << endl;
}
};
int main()
{
bit::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
bit::shared_ptr<AA> copy(p);
{
unique_lock<mutex> lk(mtx);
copy->_a1++;
copy->_a2++;
}
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << p->_a1 << endl;
cout << p->_a2 << endl;
cout << p.use_count() << endl;
return 0;
}
这里要区分两件事
| 对象 | 线程安全责任 |
|---|---|
| shared_ptr 的引用计数 | 需要线程安全控制 |
| shared_ptr 管理的对象本身 | 不归 shared_ptr 管 |
也就是说,shared_ptr 只能保证自己那一层的管理逻辑,不能替对象本身兜底。
七、内存泄漏
7.1 什么是内存泄漏,内存泄漏的危害
内存泄漏指的是:程序申请了内存,但后面没有正确释放,或者因为异常中断导致释放动作没有执行。
它不是物理内存消失,而是程序失去了对那块内存的控制,造成浪费。
危害分两类
| 场景 | 影响 |
|---|---|
| 短生命周期程序 | 影响较小,进程结束后系统会回收资源 |
| 长生命周期程序 | 危害很大,会越跑越慢,最终卡死 |
例如操作系统、后台服务、长期运行的客户端,都会非常怕这种问题。
cpp
int main()
{
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
这段代码申请了 1G 内存,但程序马上结束,所以影响没那么明显。真正危险的是长时间运行的服务程序。
7.2 如何避免内存泄漏
避免内存泄漏,建议从三个层面入手:
- 编码阶段做好资源配对,申请就释放
- 尽量使用智能指针管理资源
- 定期做泄漏检测,尤其是项目上线前
更稳妥的思路是:
- 能用智能指针就用智能指针
- 特殊资源使用 RAII 自己封装管理类
- 不把异常处理和资源释放逻辑写得过于分散
九、总结
new和delete在异常场景下不安全- RAII 的本质是对象生命周期管理资源
unique_ptr适合独占资源shared_ptr适合共享资源weak_ptr用来打破循环引用make_shared更推荐shared_ptr的引用计数在多线程里要注意安全- 内存泄漏最适合用智能指针和 RAII 去预防
完