智能指针(std::unique_ptr、std::shared_ptr、std::weak_ptr)是 C++11 之后为了解决手动内存管理带来的风险而推出的一组 RAII 对象。它们内部采用了不同的机制来实现资源的自动释放,从而在保持 C++ 的高性能的同时极大地降低了内存泄漏和悬空指针的风险。下面我们从实现原理、常见误区以及使用技巧三个层面来深入剖析。
一、实现原理
1. std::unique_ptr
unique_ptr 是一种独占式智能指针,它在对象生命周期内只允许一个指针实例指向同一块资源。实现上,它内部只维护一个裸指针 T* ptr,没有任何引用计数。unique_ptr 的拷贝构造和拷贝赋值被删除,只允许移动语义(std::move)来转移所有权。
std::unique_ptr <int> p1(new int(10));
std::unique_ptr <int> p2 = std::move(p1); // p1 变为 nullptr,p2 拥有资源
由于不存在引用计数,unique_ptr 的销毁速度非常快,适用于短生命周期的对象或所有权显式管理的场景。
2. std::shared_ptr
shared_ptr 是一种共享式智能指针,内部维护一个计数器来追踪有多少个 shared_ptr 实例指向同一块资源。实现方式通常是:
| 成员 | 说明 |
|---|---|
T* ptr |
指向实际资源的裸指针 |
| `std::atomic | |
* ref_count| 共享计数器,使用std::atomic` 以保证线程安全 |
当创建或复制 shared_ptr 时,计数器自增;当 shared_ptr 被销毁或被重新赋值时,计数器自减;当计数器归零时,资源被 delete 或通过自定义删除器销毁。
std::shared_ptr <int> p1(new int(20));
{
std::shared_ptr <int> p2 = p1; // ref_count 变为 2
} // p2 被销毁,ref_count 变为 1
计数器是原子操作,保证了多线程环境下的安全性,但其成本比 unique_ptr 更高。
3. std::weak_ptr
weak_ptr 用于观察 shared_ptr 所管理的对象而不影响引用计数。它内部持有一个指向同一计数器的弱引用。weak_ptr 的典型用途是解决循环引用导致的内存泄漏。
std::shared_ptr <A> a = std::make_shared<A>();
std::shared_ptr <B> b = std::make_shared<B>();
a->set_b(b);
b->set_a(a); // 循环引用
如果 B 的 set_a 采用 weak_ptr 作为成员,就能打破循环。
二、常见误区
| 误区 | 正确做法 |
|---|---|
将 shared_ptr 用作全局变量 |
只在需要共享所有权的场景下使用。全局使用容易导致资源释放不及时或提前。 |
在 shared_ptr 的自定义删除器里使用 delete |
自定义删除器应与对象分配方式对应(如 new 与 delete,new[] 与 delete[])。 |
忽略 weak_ptr 的空指针检查 |
weak_ptr::lock() 返回 shared_ptr,若原对象已销毁返回空指针,必须检查。 |
频繁复制 shared_ptr |
在性能敏感代码中避免不必要的拷贝,可使用 std::move 或 `std::shared_ptr |
的make_shared`。 |
三、使用技巧
-
使用
std::make_shared
make_shared在一次堆分配中同时为对象和计数器分配内存,减少了内存碎片并提升分配效率。auto sp = std::make_shared <MyClass>(constructor_args); -
使用
std::unique_ptr代替裸指针
在函数参数、返回值或容器中,尽量使用unique_ptr表达所有权意图。std::vector<std::unique_ptr<Node>> children; -
自定义删除器
当资源不是通过new分配时(如malloc、文件句柄、网络连接),可以为shared_ptr指定自定义删除器。std::shared_ptr <FILE> filePtr(fopen("data.txt","r"), [](FILE* f){ fclose(f); }); -
与
std::optional结合
对于可选资源,optional<unique_ptr<T>>可以避免在栈上保留空指针。std::optional<std::unique_ptr<Widget>> optWidget; -
使用
std::scoped_lock
在多线程访问共享资源时,使用shared_ptr与scoped_lock配合,避免手动计数错误。std::mutex mtx; void safe_increment(std::shared_ptr <Counter> cnt) { std::scoped_lock lock(mtx); ++(*cnt); } -
避免在容器中存储裸指针
容器元素的生命周期不受容器管理,容易导致悬空指针。使用shared_ptr或unique_ptr代替裸指针。std::vector<std::shared_ptr<Foo>> vec;
四、总结
智能指针是 C++ 现代化内存管理的核心工具,它们在保证资源安全的同时保留了 C++ 的性能优势。了解其内部实现(计数器、原子操作、移动语义)能帮助开发者避免常见错误;掌握最佳实践(make_shared、自定义删除器、与容器结合)则能进一步提升代码质量与可维护性。只要遵循“所有权表达清晰、资源生命周期可追踪”的原则,智能指针将成为你可靠的“资源守护者”。