在 C++11 之后,智能指针成为了管理动态资源的核心工具。它们大大简化了内存管理,减少了悬空指针和内存泄漏的风险。虽然 std::unique_ptr 和 std::shared_ptr 都属于智能指针,但它们的使用场景和语义差异显著。本文将从语义、性能、安全性以及使用实例等方面对两者进行对比,帮助开发者在项目中做出更合适的选择。
一、基本语义
| 指针 | 所有权 | 共享性 | 典型用途 |
|---|---|---|---|
std::unique_ptr |
独占 | 否 | 对象生命周期严格由单一拥有者控制 |
std::shared_ptr |
共享 | 是 | 对象需要被多个所有者引用 |
- unique_ptr:只能有一个
unique_ptr拥有指向某个对象的所有权。它不能被复制,只能移动。对象在最后一个unique_ptr被销毁或重置时被自动析构。 - shared_ptr:使用引用计数实现多重所有权。只要有一个
shared_ptr存活,对象就不会被析构。引用计数的读写需要同步,导致一定的性能开销。
二、性能对比
-
内存占用
unique_ptr:只需保存裸指针(通常 8 字节)。shared_ptr:除了指针,还需要存储引用计数(通常 8 字节)和一个控制块,整体占用更大。
-
运行时开销
unique_ptr:创建、销毁几乎无开销。shared_ptr:每次拷贝/移动都涉及引用计数的递增/递减。递减到 0 时还需执行delete,并且多线程环境下需要原子操作,导致锁竞争。
-
编译器优化
unique_ptr可以被编译器更好地优化,例如可以被移动到栈上、在返回值中进行 NRVO(返回值优化)等。
结论:在不需要共享所有权的情况下,优先使用
unique_ptr。只有在确实需要多重所有权时,才考虑shared_ptr。
三、安全性与可读性
-
异常安全
unique_ptr在异常抛出时能够保证资源被正确释放,避免泄漏。shared_ptr同样如此,但异常后引用计数的正确性依赖于所有操作都能完成。 -
生命周期可视化
unique_ptr明确表示对象的所有权归属,易于追踪。shared_ptr隐含的共享关系可能导致资源被意外提前释放或持久化,难以追踪。 -
线程安全
shared_ptr的引用计数实现是原子操作,天然线程安全。但这并不意味着shared_ptr线程安全;指向的对象仍需自行同步。unique_ptr本身不保证线程安全,但在单线程或独占资源的情况下更易于使用。
四、使用实例
4.1 仅需独占所有权的场景
#include <memory>
#include <iostream>
class Buffer {
public:
Buffer(size_t size) : data(new int[size]), size(size) {}
~Buffer() { delete[] data; }
private:
int* data;
size_t size;
};
std::unique_ptr <Buffer> createBuffer(size_t size) {
return std::make_unique <Buffer>(size);
}
int main() {
auto buf = createBuffer(1024);
// buf 自动析构,资源释放
}
- 通过
make_unique创建,避免手动new,降低错误率。
4.2 需要共享所有权的场景
#include <memory>
#include <iostream>
#include <vector>
struct Node {
int value;
std::vector<std::shared_ptr<Node>> children;
};
void addChild(std::shared_ptr <Node> parent, std::shared_ptr<Node> child) {
parent->children.push_back(child);
}
int main() {
auto root = std::make_shared <Node>();
root->value = 0;
auto child = std::make_shared <Node>();
child->value = 1;
addChild(root, child);
// root 与 child 共享 ownership
}
- 由于树结构中可能存在父子节点之间的循环引用,实际项目中需要配合
std::weak_ptr避免内存泄漏。
五、何时避免 shared_ptr?
- 对象生命周期严格受控:例如函数内部临时对象,或者资源池模式下的对象。
- 性能敏感:高频率创建/销毁、实时系统、游戏渲染等。
- 多线程共享:虽然引用计数线程安全,但并不意味着对象本身线程安全。若需要高并发访问,应考虑使用
std::atomic或std::shared_mutex。
六、最佳实践
- 默认使用
unique_ptr:除非业务逻辑明确要求共享,否则首选unique_ptr。 - 对外接口使用引用:函数参数、返回值尽量使用引用或指针而不是
shared_ptr。 - 避免循环引用:使用
weak_ptr断开循环。 - 在容器中使用
unique_ptr:如std::vector<std::unique_ptr<T>>,可保持容器元素唯一性且不引入额外引用计数。 - 适当的工厂函数:封装
make_unique或make_shared,保证统一的创建方式。
七、结语
C++ 的智能指针为资源管理提供了强大而灵活的工具。std::unique_ptr 与 std::shared_ptr 各有千秋,正确的选择取决于程序的所有权模型、性能需求以及安全性考量。熟练掌握两者的语义与细节,能够让你在 C++ 开发中写出更安全、更高效、更易维护的代码。