智能指针是 C++11 之后对资源管理进行封装的重要工具,它们通过 RAII(资源获取即初始化)机制,自动管理动态分配的内存,显著降低内存泄漏和悬空指针的风险。常见的智能指针有 std::unique_ptr、std::shared_ptr、std::weak_ptr。本文聚焦于 std::unique_ptr 与 std::shared_ptr 的细节、区别以及在实际项目中的最佳实践。
1. 设计哲学
1.1 std::unique_ptr
- 唯一所有权:每个资源只能被一个
unique_ptr持有,禁止复制,只能移动。 - 轻量级:内部仅包含裸指针(以及可选的删除器),几乎无额外开销。
- 延迟删除:对象在
unique_ptr被销毁时自动调用删除器释放资源。
1.2 std::shared_ptr
- 共享所有权:同一资源可以被多个
shared_ptr持有,内部维护引用计数。 - 线程安全:引用计数的增减操作使用原子操作,保证多线程场景安全。
- 可能产生循环引用:当相互引用的对象持有
shared_ptr时,可能导致内存泄漏。
2. 细节差异
| 特性 | std::unique_ptr | std::shared_ptr |
|---|---|---|
| 复制 | 禁止 | 允许,引用计数+1 |
| 移动 | 允许 | 允许 |
| 默认删除器 | delete |
delete |
| 自定义删除器 | 必须在构造时指定 | 必须在构造时指定 |
| 内存占用 | 1 个指针 | 2 个指针(指针 + 引用计数) |
| 线程安全 | 非线程安全 | 线程安全(计数) |
| 典型用途 | 所有权唯一的资源 | 多处共享的资源 |
3. 实际案例
3.1 资源管理示例
#include <iostream>
#include <memory>
struct File {
explicit File(const std::string& name) : name_(name) {
std::cout << "Open file: " << name_ << '\n';
}
~File() {
std::cout << "Close file: " << name_ << '\n';
}
void read() { std::cout << "Reading from " << name_ << '\n'; }
private:
std::string name_;
};
void processFile(std::unique_ptr <File> f) {
f->read();
// f 在此函数结束时自动销毁
}
int main() {
auto file = std::make_unique <File>("data.txt");
processFile(std::move(file));
// file 现在为空
}
说明:
unique_ptr确保文件只在processFile里被访问,传递时使用std::move明确所有权转移。
3.2 共享资源示例
#include <iostream>
#include <memory>
struct Logger {
Logger(const std::string& name) : name_(name) {
std::cout << "Logger created: " << name_ << '\n';
}
~Logger() {
std::cout << "Logger destroyed: " << name_ << '\n';
}
void log(const std::string& msg) {
std::cout << "[" << name_ << "] " << msg << '\n';
}
private:
std::string name_;
};
void worker(std::shared_ptr <Logger> logger, int id) {
logger->log("Worker " + std::to_string(id) + " started");
// 计数自动递增/递减
}
int main() {
auto logger = std::make_shared <Logger>("AppLogger");
worker(logger, 1);
worker(logger, 2);
// logger 在 main 结束时被销毁
}
说明:多线程或多模块共享同一个
Logger,shared_ptr自动管理生命周期。
4. 最佳实践
4.1 何时使用 unique_ptr
- 对象只需要单一拥有者,例如管理文件句柄、线程对象、单例模式的内部实现。
- 避免不必要的引用计数开销。
4.2 何时使用 shared_ptr
- 对象需要被多个独立部件共享,例如 GUI 组件、资源缓存。
- 必须保证所有权共享且对象生命周期与使用者同步。
4.3 防止循环引用
- 使用
std::weak_ptr来断开循环,例如父子关系、观察者模式。 - 示例:
struct Node {
std::weak_ptr <Node> parent; // 父节点使用 weak_ptr 避免循环
std::vector<std::shared_ptr<Node>> children;
};
4.4 自定义删除器
- 对非
new/delete分配的资源(如malloc、文件句柄)需提供自定义删除器。
auto buffer = std::unique_ptr<int[], void(*)(int*)>(reinterpret_cast<int*>(malloc(10 * sizeof(int))), [](int* p){ free(p); });
4.5 线程安全注意
- 虽然
shared_ptr的引用计数是线程安全的,但对象本身的状态不是。需要外部同步或使用std::atomic、std::mutex。
5. 性能对比
| 场景 | unique_ptr |
shared_ptr |
|---|---|---|
| 单线程 | 近乎无额外开销 | 约 1.5 倍内存 |
| 多线程 | 需要手动同步 | 计数原子操作可减少锁 |
| 频繁创建销毁 | 高效 | 由于引用计数调增/调减,略慢 |
通过
std::move的移动语义,unique_ptr在大多数单所有权场景下是最优选择。
6. 结语
智能指针的出现,使得 C++ 在资源管理方面与现代语言趋同。正确理解 std::unique_ptr 与 std::shared_ptr 的语义差异,结合具体业务场景,能显著提升代码的健壮性和可维护性。记住:所有权决定智能指针的类型,循环引用需要 weak_ptr,自定义删除器可扩展智能指针的使用范围。祝编码愉快!