在现代 C++ 开发中,手动管理内存已逐渐被智能指针所取代。std::shared_ptr 与 std::unique_ptr 是最常用的两种智能指针,它们各自承担不同的职责,了解它们的区别与适用场景能够帮助我们写出更安全、更高效的代码。
1. 何为智能指针?
智能指针是一种包装普通裸指针的类,负责在对象生命周期结束时自动释放资源。与裸指针相比,智能指针可以避免内存泄漏、悬空指针等问题。
- **`std::unique_ptr `**:实现单一所有权(ownership)。同一时间只能有一个 `unique_ptr` 指向同一块内存,指针移动后原对象失效。
- **`std::shared_ptr `**:实现共享所有权(reference counting)。多个 `shared_ptr` 可以指向同一块内存,引用计数为 0 时自动销毁。
2. 关键区别
| 特性 | unique_ptr |
shared_ptr |
|---|---|---|
| 所有权 | 独占 | 共享 |
| 复制 | 只允许移动(std::move) |
允许复制,计数自增 |
| 性能 | 较低(无计数开销) | 较高(计数读写) |
| 线程安全 | 计数不安全,需外部同步 | 计数操作原子化,线程安全 |
| 用途 | 资源所有权不需要共享,适合 RAII | 需要跨模块共享资源,或在容器中使用 |
3. 典型使用场景
3.1 unique_ptr
- 资源拥有者:如打开的文件、网络套接字、内存缓冲区等。
- 工厂函数返回值:让调用者获得资源所有权,避免裸指针返回。
- 局部变量:在函数内部创建对象并在结束时自动销毁。
std::unique_ptr <Socket> create_socket() {
return std::make_unique <Socket>(/* params */);
}
3.2 shared_ptr
- 对象跨模块共享:如 GUI 事件系统、插件架构。
- 链表/树等数据结构:节点可以共享同一子树。
- 容器元素:std::vector<std::shared_ptr> 等。
void register_listener(std::shared_ptr <EventListener> listener) {
listeners_.push_back(listener);
}
4. 代码示例
4.1 unique_ptr 的移动语义
std::unique_ptr <int> p1 = std::make_unique<int>(42);
std::unique_ptr <int> p2 = std::move(p1); // p1 现在为空
if (!p1) {
std::cout << "p1 is now nullptr\n";
}
4.2 shared_ptr 的引用计数
std::shared_ptr <int> sp1 = std::make_shared<int>(10);
{
std::shared_ptr <int> sp2 = sp1; // 引用计数 +1
std::cout << "use_count: " << sp1.use_count() << '\n'; // 2
} // sp2 离开作用域,计数 -1
std::cout << "use_count after scope: " << sp1.use_count() << '\n'; // 1
4.3 自定义 deleter
auto deleter = [](FILE* fp) {
if (fp) fclose(fp);
};
std::unique_ptr<FILE, decltype(deleter)> file_ptr(
fopen("log.txt", "w"), deleter);
5. 性能与安全细节
- 避免循环引用:在使用
shared_ptr时,若两个对象互相持有shared_ptr,会导致内存泄漏。可使用std::weak_ptr打破循环。 - 自定义分配器:可以通过
std::allocator或自定义std::allocator为智能指针分配内存。 - 线程安全:
shared_ptr的计数操作是原子化的,但对象本身的并发访问仍需同步。
6. 小结
std::unique_ptr适用于需要独占资源的场景,性能最优,使用简单。std::shared_ptr适合需要共享资源的复杂结构,需注意循环引用与性能成本。- 正确选择智能指针,能够显著提升 C++ 代码的可维护性与安全性。
通过深入理解这两种智能指针的设计哲学与实际使用方法,程序员可以更好地控制资源生命周期,避免常见的内存错误,编写出更健壮、更高效的 C++ 程序。