在 C++ 中,std::shared_ptr
和 std::unique_ptr
是两种核心的智能指针,它们解决了不同的所有权管理问题。理解它们各自的使用场景对于编写安全、高效的现代 C++ 代码至关重要。
一、std::unique_ptr
(独占指针)使用场景
1. 独占资源所有权
场景:当资源有单一明确的所有者时
特点:
- 资源只能被一个指针拥有
- 所有权可以通过
std::move
转移 - 不支持复制
std::unique_ptr<DatabaseConnection> createConnection() {
return std::make_unique<DatabaseConnection>("server:3306");
}
void processData() {
auto conn = createConnection(); // 获得连接所有权
auto result = conn->query("SELECT * FROM users");
// conn 离开作用域时自动关闭连接
}
2. 工厂模式返回值
场景:工厂函数创建对象并转移所有权
std::unique_ptr<Shape> createShape(ShapeType type) {
switch(type) {
case Circle: return std::make_unique<Circle>();
case Square: return std::make_unique<Square>();
default: return nullptr;
}
}
auto shape = createShape(Circle); // 获得图形对象所有权
shape->draw();
3. 类成员资源管理
场景:类独占管理其成员资源
class Renderer {
public:
Renderer() : context(std::make_unique<GLContext>()) {}
private:
std::unique_ptr<GLContext> context; // 独占OpenGL上下文
};
4. 在容器中存储资源
场景:容器拥有其元素的所有权
std::vector<std::unique_ptr<Player>> players;
players.push_back(std::make_unique<Player>("Alice"));
players.push_back(std::make_unique<Player>("Bob"));
// 玩家对象生命周期与容器绑定
5. 实现 PIMPL 模式
场景:隐藏实现细节
// Widget.h
class Widget {
public:
Widget();
~Widget(); // 必须声明析构函数
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
// Widget.cpp
struct Widget::Impl {
int data;
std::string config;
};
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // 在实现文件中定义
二、std::shared_ptr
(共享指针)使用场景
1. 共享资源所有权
场景:多个实体需要访问同一资源
特点:
- 资源在所有使用者销毁后自动释放
- 内部使用引用计数
class TextureCache {
public:
std::shared_ptr<Texture> getTexture(const std::string& path) {
if (!cache.count(path)) {
cache[path] = std::make_shared<Texture>(path);
}
return cache[path];
}
private:
std::unordered_map<std::string, std::shared_ptr<Texture>> cache;
};
// 多个对象共享同一纹理
auto texture = cache.getTexture("grass.png");
auto obj1 = std::make_shared<GameObject>(texture);
auto obj2 = std::make_shared<GameObject>(texture);
2. 观察者模式
场景:主题与观察者共享状态
class Subject {
public:
void addObserver(std::shared_ptr<Observer> obs) {
observers.push_back(obs);
}
void notify() {
for (auto& obs : observers) {
obs->update();
}
}
private:
std::vector<std::shared_ptr<Observer>> observers;
};
3. 缓存系统
场景:缓存条目共享访问
class DataCache {
public:
std::shared_ptr<Data> fetch(int id) {
std::lock_guard<std::mutex> lock(mutex);
if (auto it = cache.find(id); it != cache.end()) {
return it->second.lock(); // 从weak_ptr提升
}
auto data = std::make_shared<Data>(loadData(id));
cache[id] = data; // 存储weak_ptr
return data;
}
private:
std::unordered_map<int, std::weak_ptr<Data>> cache;
std::mutex mutex;
};
4. 循环引用解决方案
场景:需要打破循环引用
组合:shared_ptr
+ weak_ptr
class Node {
public:
void setParent(std::shared_ptr<Node> parent) {
this->parent = parent; // shared_ptr
parent->children.push_back(shared_from_this());
}
void addChild(std::shared_ptr<Node> child) {
child->parent = weak_from_this(); // weak_ptr
children.push_back(child);
}
private:
std::weak_ptr<Node> parent; // 弱引用打破循环
std::vector<std::shared_ptr<Node>> children;
};
5. 异步操作共享状态
场景:多个异步操作访问共享数据
void asyncProcess(std::shared_ptr<Data> data) {
std::thread([data] {
// 在独立线程中使用数据
process(*data);
}).detach();
}
auto sharedData = std::make_shared<Data>();
asyncProcess(sharedData);
asyncProcess(sharedData); // 多个操作共享同一数据
三、两种指针对比与决策指南
特性 | std::unique_ptr | std::shared_ptr |
---|---|---|
所有权 | 独占 | 共享 |
复制 | 禁止 | 允许 |
移动 | 支持 | 支持 |
开销 | 接近原始指针(极小) | 引用计数(原子操作) |
循环引用 | 无风险 | 需配合 weak_ptr |
空状态 | 支持 (nullptr ) | 支持 (nullptr ) |
定制删除器 | 支持 | 支持 |
典型大小 | 8 字节(单指针) | 16-24 字节(控制块+指针) |
决策树:如何选择智能指针
graph TD
A[需要管理动态资源?] --> B{所有权是否可共享?}
B -->|单一明确所有者| C[unique_ptr]
B -->|多个实体需要访问| D{存在循环引用风险?}
D -->|是| E[shared_ptr + weak_ptr]
D -->|否| F[shared_ptr]
A -->|仅观察不拥有| G[原始指针或 weak_ptr]
四、性能考量与最佳实践
1. 性能影响
unique_ptr
:运行时开销几乎为零,适合性能关键路径shared_ptr
:- 引用计数操作是原子的(线程安全但开销较大)
- 控制块分配额外内存(
make_shared
可优化) - 不适合高频创建/销毁的场景
2. 创建最佳实践
// 优先使用 make_ 系列函数
auto uptr = std::make_unique<MyClass>(args...);
auto sptr = std::make_shared<MyClass>(args...);
// 避免这种用法(潜在内存泄漏风险)
std::shared_ptr<MyClass> bad(new MyClass(args...));
3. 参数传递指南
// unique_ptr 作为参数(所有权转移)
void takeOwnership(std::unique_ptr<Resource> res);
// shared_ptr 作为参数
void shareResource(const std::shared_ptr<Resource>& res); // 不获取所有权
void storeResource(std::shared_ptr<Resource> res); // 共享所有权
4. 避免常见陷阱
错误1:不必要的共享所有权
// 不好:使用 shared_ptr 但实际只需 unique_ptr
std::shared_ptr<Logger> logger = std::make_shared<Logger>();
// 更好:单一所有权
std::unique_ptr<Logger> logger = std::make_unique<Logger>();
错误2:混合使用智能指针和原始指针
Resource* raw = new Resource();
std::shared_ptr<Resource> sptr(raw); // 危险!
// 如果其他地方使用相同的 raw 指针
std::shared_ptr<Resource> sptr2(raw); // 双重释放!
错误3:忽略循环引用
class Parent {
std::shared_ptr<Child> child; // 应使用 weak_ptr
};
class Child {
std::shared_ptr<Parent> parent; // 循环引用!
};
五、总结:核心使用原则
-
默认使用
unique_ptr
- 适用于大多数单一所有权场景
- 性能最优,语义最清晰
-
需要共享时使用
shared_ptr
- 配合
weak_ptr
避免循环引用 - 用于缓存、观察者等共享场景
- 配合
-
避免裸
new/delete
- 始终使用智能指针管理动态资源
-
优先使用
make_shared/make_unique
- 提供异常安全保证
- 更高效的内存布局(特别是
make_shared
)
-
接口设计明确所有权
- 函数参数和返回值清晰表达所有权转移意图
通过合理选择智能指针类型,可以显著提高 C++ 代码的安全性、可读性和可维护性,同时减少内存管理相关的错误。