在现代C++开发中,智能指针已经成为管理资源的首选工具。C++11 标准库提供了 std::unique_ptr 和 std::shared_ptr 两种最常用的智能指针,它们分别实现了独占式和共享式所有权模型。本文从语义、使用场景、性能考虑以及常见陷阱等方面,系统地阐述这两种智能指针的区别与最佳实践。
1. 基本语义对比
| 特性 | std::unique_ptr |
std::shared_ptr |
|---|---|---|
| 所有权 | 独占式(只能有一个指针拥有对象) | 共享式(可多指针共享同一对象) |
| 复制 | 只允许移动(move) |
允许复制,引用计数自动管理 |
| 线程安全 | 非原子操作 | 引用计数操作是原子性的 |
| 内存占用 | 仅指针本身 | 除指针外还需要引用计数控制块 |
| 对象销毁 | 对象立即销毁 | 当计数归零后销毁 |
| 典型用途 | 资源独占,工厂返回值 | 对象需要多方引用、生命周期跨函数 |
2. unique_ptr 的最佳实践
2.1. 适合的场景
- 所有权单一:如资源管理、RAII 对象、局部变量等。
- 避免共享计数成本:减少额外内存分配和原子操作。
- 需要显式移动:在需要将所有权转移给另一个对象时,使用
std::move。
2.2. 代码示例
#include <memory>
#include <iostream>
class File {
public:
File(const std::string& name) : m_name(name) {
std::cout << "Open file: " << m_name << std::endl;
}
~File() { std::cout << "Close file: " << m_name << std::endl; }
private:
std::string m_name;
};
std::unique_ptr <File> openFile(const std::string& name) {
return std::make_unique <File>(name); // 直接返回,所有权转移
}
int main() {
auto filePtr = openFile("data.txt"); // filePtr 拥有 File 对象
// ... use filePtr
// 结束时自动析构
}
2.3. 常见误区
unique_ptr不能共享:尝试auto ptr2 = ptr1;会编译错误,正确方式是auto ptr2 = std::move(ptr1);。- 手动
delete:不要同时使用delete与unique_ptr,会导致双重删除。 - 数组使用:需要
std::unique_ptr<T[]>,但只能通过operator[]访问。
3. shared_ptr 的最佳实践
3.1. 适合的场景
- 多方引用:对象被多个主体引用,如事件订阅、观察者模式。
- 跨线程共享:
shared_ptr的计数操作是线程安全的。 - 生命周期不确定:对象创建后由多处使用,直到最后一个指针销毁。
3.2. 代码示例
#include <memory>
#include <iostream>
class Widget {
public:
Widget(int id) : m_id(id) { std::cout << "Widget " << m_id << " created\n"; }
~Widget() { std::cout << "Widget " << m_id << " destroyed\n"; }
private:
int m_id;
};
void observer(std::shared_ptr <Widget> w) {
std::cout << "Observer sees widget id: " << w.get() << "\n";
}
int main() {
auto w = std::make_shared <Widget>(42);
std::shared_ptr <Widget> w2 = w; // 共享所有权,计数 +1
observer(w2); // 计数 +1
std::cout << "Reference count: " << w.use_count() << "\n";
// 所有指针超出作用域后自动销毁
}
3.3. 常见误区
- 循环引用:
shared_ptr互相引用会导致计数永不归零,使用std::weak_ptr解决。 - 频繁复制:每次复制都会增加计数,若在高频场景中可考虑使用
shared_ptr的make_shared+weak_ptr或者改用unique_ptr。 - 裸指针:不要将
shared_ptr的裸指针传递给外部 API,避免破坏引用计数。
4. 性能与资源考虑
| 方面 | unique_ptr |
shared_ptr |
|---|---|---|
| 内存分配 | 1 次(对象本身) | 2 次(对象 + 控制块) |
| 原子操作 | 无 | 计数增减使用原子 |
| GC 闪退 | 不需要 | 可能出现循环引用导致泄漏 |
在性能敏感的代码(如高帧率渲染、网络 I/O)中,首选 unique_ptr;在需要跨模块、跨线程共享对象的业务逻辑中,shared_ptr 更为合适。
5. 总结
unique_ptr:独占所有权,使用简洁,性能低成本,适合局部资源管理。shared_ptr:共享所有权,提供线程安全的引用计数,适合多方访问和生命周期不确定的对象,但需注意循环引用。
通过合理选择与组合这两种智能指针,能够显著提升 C++ 代码的安全性、可维护性与性能。务必遵循“所有权清晰”与“生命周期可追踪”的原则,避免资源泄漏与悬挂指针。