智能指针是 C++11 引入的资源管理工具,帮助开发者自动管理堆内存,避免内存泄漏和悬空指针等错误。C++ 标准库中提供了三种最常用的智能指针:std::unique_ptr、std::shared_ptr 与 std::weak_ptr。下面将从语义、使用场景、实现细节以及常见陷阱四个维度进行深入剖析。
1. 语义与基本特征
| 指针类型 | 所有权模型 | 复制语义 | 线程安全 | 典型用法 |
|---|---|---|---|---|
unique_ptr |
独占所有权 | 只能移动,复制被删除 | 线程安全的移动、销毁 | 临时对象、局部资源、单线程场景 |
shared_ptr |
共享所有权 | 复制拷贝计数 | 线程安全的计数操作 | 需要多方共享、生命周期难以预估 |
weak_ptr |
弱引用 | 复制拷贝计数 | 线程安全的计数操作 | 防止循环引用、观察者模式、缓存 |
- unique_ptr:指针拥有唯一所有权,不能被复制,只能通过
std::move转移。销毁时会立即释放指向的对象。 - shared_ptr:使用引用计数实现共享所有权。每次拷贝都会增计数,销毁时计数归零才真正释放对象。
- weak_ptr:不参与引用计数,单独存储指向对象的
std::shared_ptr所共享的原始指针。通过lock()可安全地获得shared_ptr,若对象已被销毁则返回空指针。
2. 典型使用场景
| 场景 | 推荐指针 |
|---|---|
| 临时所有权、不可共享、性能敏感 | unique_ptr |
| 需要跨多个函数或线程共享同一对象、生命周期不确定 | shared_ptr |
防止 shared_ptr 循环引用、实现观察者/事件模式 |
weak_ptr |
| 对象内部需要指向自己但不增加所有权计数 | weak_ptr |
举例 1:文件资源管理
std::unique_ptr<std::FILE, decltype(&std::fclose)> file(
std::fopen("log.txt", "a"), std::fclose);
if (!file) throw std::runtime_error("open failed");
fputs("Hello, world!\n", file.get());
这里 unique_ptr 与自定义删除器组合使用,确保文件及时关闭。
举例 2:共享对象
std::shared_ptr <Widget> p1 = std::make_shared<Widget>();
std::shared_ptr <Widget> p2 = p1; // 计数 +1
只要 p1 与 p2 任何一个被销毁,计数会递减,计数归零后真正销毁对象。
举例 3:观察者模式
class Subject {
std::vector<std::weak_ptr<Observer>> observers;
public:
void addObserver(const std::shared_ptr <Observer>& obs) {
observers.emplace_back(obs);
}
void notify() {
for (auto it = observers.begin(); it != observers.end(); ) {
if (auto sp = it->lock()) { // 仍然存活
sp->update();
++it;
} else {
it = observers.erase(it); // 已销毁,移除
}
}
}
};
weak_ptr 防止 Subject 与 Observer 形成循环引用。
3. 实现细节
unique_ptr本质上是裸指针加自定义删除器,删除器可为函数指针、函数对象或 lambda。shared_ptr内部维护一个 control block(控制块)来存储引用计数、弱引用计数、删除器以及自定义内存分配器。计数操作使用原子操作,保证多线程安全。weak_ptr与shared_ptr共用同一个控制块,但不影响引用计数。它们的lock()操作会在尝试提升计数时使用 CAS(原子比较并交换),如果计数已经为 0 则返回空shared_ptr。
4. 常见陷阱与最佳实践
-
循环引用导致内存泄漏
- 只要两对象相互持有
shared_ptr,计数永远不为 0。 - 解决方案:在至少一条链使用
weak_ptr。
- 只要两对象相互持有
-
不恰当的删除器
unique_ptr的默认删除器是delete,若指针指向数组或文件等需自定义删除器,否则会导致未定义行为。
-
多线程共享
shared_ptrshared_ptr本身线程安全,但其指向的对象并不一定线程安全。- 在多线程访问时请使用同步原语(如
std::mutex)或设计无锁访问。
-
性能权衡
unique_ptr轻量,适用于所有不需要共享的场景。shared_ptr在高性能需求时可能带来计数更新的开销,尤其在多线程环境。- 对于大对象,最好使用
std::shared_ptr<T[]>或std::shared_ptr<std::vector<T>>以避免多次拷贝。
-
make_sharedvsnewstd::make_shared在一个分配中同时创建对象和控制块,减少内存碎片并提高缓存命中率。- 仅当需要自定义删除器或控制块时才考虑手动
new。
5. 小结
unique_ptr:最简洁的所有权管理,适合单一所有者。shared_ptr:实现共享所有权,计数机制使对象生命周期自动管理。weak_ptr:防止循环引用,提供对共享对象的安全观察。
熟练运用这三种智能指针是现代 C++ 开发的基石,它们帮助我们写出更安全、更易维护、并行友好的代码。掌握其语义、使用场景与实现细节,能够在复杂项目中灵活选择最合适的资源管理策略。