在 C++11 之后,智能指针成为了管理动态内存的重要工具。最常用的两种智能指针是 std::unique_ptr 和 std::shared_ptr。它们虽然都可以自动释放资源,但在所有权模型、引用计数、性能以及使用场景上存在显著差异。本文从定义、语义、实现细节、典型用法以及最佳实践四个维度进行比较,并给出实际编程中的决策建议。
1. 基本定义与语义
| 特性 | std::unique_ptr | std::shared_ptr |
|---|---|---|
| 所有权 | 只能有一个所有者 | 共享所有权,多个指针可指向同一对象 |
| 复制 | 禁止复制,支持移动 | 支持复制,内部维护引用计数 |
| 内存释放 | 立即销毁对象 | 当引用计数归零时销毁 |
| 适用场景 | 资源必须唯一拥有 | 需要多处引用,生命周期难以确定 |
- std::unique_ptr:实现了独占式所有权。通过
std::move可以转移所有权,但不能复制。适合资源必须被唯一拥有的情况,如文件句柄、网络连接、单例模式等。 - std::shared_ptr:实现了共享式所有权。每一次拷贝都会递增引用计数,拷贝销毁时递减。适合资源需要在多处共享或存在循环引用的场景。
2. 典型实现细节
2.1 unique_ptr
template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
public:
explicit unique_ptr(T* ptr = nullptr) noexcept : ptr_(ptr) {}
~unique_ptr() { if (ptr_) deleter_(ptr_); }
unique_ptr(const unique_ptr&) = delete; // 禁止拷贝
unique_ptr& operator=(const unique_ptr&) = delete;
unique_ptr(unique_ptr&& other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; }
unique_ptr& operator=(unique_ptr&& other) noexcept {
reset(other.release());
return *this;
}
T* get() const noexcept { return ptr_; }
T& operator*() const noexcept { return *ptr_; }
T* operator->() const noexcept { return ptr_; }
void reset(T* ptr = nullptr) noexcept {
if (ptr_ != ptr) { deleter_(ptr_); ptr_ = ptr; }
}
T* release() noexcept { T* tmp = ptr_; ptr_ = nullptr; return tmp; }
private:
T* ptr_;
Deleter deleter_;
};
关键点:
- 禁止拷贝构造和拷贝赋值。
- 支持移动构造和移动赋值。
- 内部没有引用计数,因而开销极小。
2.2 shared_ptr
template <typename T>
class shared_ptr {
public:
explicit shared_ptr(T* ptr = nullptr) noexcept : ptr_(ptr), ref_count_(new size_t(1)) {}
~shared_ptr() { release(); }
shared_ptr(const shared_ptr& other) noexcept : ptr_(other.ptr_), ref_count_(other.ref_count_) {
if (ptr_) ++(*ref_count_);
}
shared_ptr& operator=(const shared_ptr& other) noexcept {
if (this != &other) {
release();
ptr_ = other.ptr_;
ref_count_ = other.ref_count_;
if (ptr_) ++(*ref_count_);
}
return *this;
}
shared_ptr(shared_ptr&& other) noexcept : ptr_(other.ptr_), ref_count_(other.ref_count_) {
other.ptr_ = nullptr;
other.ref_count_ = nullptr;
}
// ...
private:
void release() noexcept {
if (ptr_ && --(*ref_count_) == 0) {
delete ptr_;
delete ref_count_;
}
}
T* ptr_;
size_t* ref_count_;
};
关键点:
- 拷贝构造和拷贝赋值都会递增计数。
- 需要维护引用计数对象,导致内存分配额外开销。
- 引用计数实现必须是原子操作(线程安全),或者使用
std::atomic.
3. 性能对比
| 项目 | unique_ptr | shared_ptr |
|---|---|---|
| 复制成本 | O(1),无计数操作 | O(1),但需原子计数递增/递减 |
| 内存占用 | 仅指针 | 指针 + 计数指针 + 计数值 |
| 线程安全 | 不需要 | 需要原子计数(C++11 之后默认实现) |
| 销毁开销 | 仅析构 | 计数递减 + 可能析构 + 计数器析构 |
在大多数情况下,unique_ptr 的性能要明显优于 shared_ptr。当资源所有权不需要共享时,应首选 unique_ptr。
4. 典型用例
4.1 unique_ptr 用例
std::unique_ptr <File> file(new File("data.txt"));
auto process = [file = std::move(file)](const std::string& line) {
file->write(line);
};
File对象只能由process处理,所有权通过std::move转移。- 代码简洁,且避免了手动
delete。
4.2 shared_ptr 用例
struct Node {
std::vector<std::shared_ptr<Node>> children;
std::weak_ptr <Node> parent;
};
std::shared_ptr <Node> root = std::make_shared<Node>();
auto child = std::make_shared <Node>();
child->parent = root;
root->children.push_back(child);
- 父子节点共享同一内存块。
- 为防止循环引用使用
std::weak_ptr。
5. 何时选择?
| 场景 | 推荐指针 |
|---|---|
| 需要唯一所有权且无共享 | std::unique_ptr |
| 需要共享所有权,生命周期难以确定 | std::shared_ptr |
| 需要跨线程共享且引用计数线程安全 | std::shared_ptr(配合 std::atomic) |
| 需要自定义删除器 | 两者均可;unique_ptr 对单一删除器更简洁 |
小结:unique_ptr 是最轻量级、最安全的智能指针,适合大多数需要 RAII 的情况;shared_ptr 在需要共享所有权时才使用,注意避免循环引用。掌握两者的语义差异和使用场景,可让 C++ 程序更健壮、更高效。