在现代 C++ 开发中,智能指针已成为管理动态内存的核心工具。相比裸指针,智能指针不仅可以自动释放资源,避免内存泄漏,还能更好地表达所有权语义。本文将从智能指针的类型、所有权模型、常见误区以及最佳实践四个方面,帮助你掌握智能指针的正确使用方式。
一、智能指针的基本类型
| 类型 | 所有权模型 | 典型场景 | 重要特性 |
|---|---|---|---|
| `std::unique_ptr | |||
| 仅拥有者 | 独占资源,单线程或通过std::move` 传递 |
可自定义删除器、可转化为裸指针 | ||
| `std::shared_ptr | |||
| ` | 共享所有者 | 多个对象共享同一资源 | 引用计数、线程安全的计数 |
| `std::weak_ptr | |||
| 弱引用 | 防止shared_ptr循环引用 | 需要lock()转为shared_ptr` |
二、所有权语义与生命周期
- 单一所有者:
unique_ptr只允许一个对象持有指针,赋值必须使用std::move。 - 共享所有者:
shared_ptr使用引用计数来决定对象何时销毁。 - 弱引用:
weak_ptr不计数,常用于观察者模式或缓存实现。
注意:
shared_ptr的复制构造和赋值运算符会增加计数,销毁时计数减一,计数为零才真正释放。
三、常见误区与解决方案
| 误区 | 说明 | 解决方案 |
|---|---|---|
| 裸指针与智能指针混用导致析构顺序错误 | 直接将裸指针传给 shared_ptr 后继续使用裸指针 |
只使用 shared_ptr 或者在构造时采用 `std::make_shared |
| ()` | ||
| 循环引用导致内存泄漏 | 两个 shared_ptr 互相指向对方 |
使用 weak_ptr 断开循环 |
多线程读写同一 shared_ptr 引用计数不安全 |
原始操作会在多线程环境下不安全 | std::atomic 或 std::shared_ptr 内部计数已保证线程安全,只要访问对象本身时同步即可 |
unique_ptr 误用为数组 |
unique_ptr<T[]> 需要使用 delete[] |
明确使用 std::unique_ptr<T[]> 并通过 operator[] 访问 |
weak_ptr 直接转换为裸指针 |
可能导致悬空指针 | 先 lock() 成 shared_ptr,再使用 |
四、最佳实践
-
使用工厂函数
auto createObj() -> std::unique_ptr <MyClass> { return std::make_unique <MyClass>(/* ctor args */); } -
避免显式
deletestd::unique_ptr <MyClass> ptr = std::make_unique<MyClass>(); // 直接使用,不需要手动 delete -
在容器中使用智能指针
std::vector<std::shared_ptr<Node>> graph; graph.emplace_back(std::make_shared <Node>()); -
使用
std::shared_ptr的make_shared- 只做一次内存分配,性能更好,减少碎片。
-
使用
std::weak_ptr解决循环引用class B; // 前向声明 class A { std::weak_ptr <B> b_ref; }; class B { std::shared_ptr <A> a_ref; }; -
在多线程中使用
shared_ptr的线程安全特性std::shared_ptr <Data> data = std::make_shared<Data>(); std::thread t1([=](){ use(data); }); std::thread t2([=](){ use(data); }); -
在回调函数中传递
shared_ptr- 防止对象在回调执行前被销毁。
void asyncTask(std::shared_ptr <Task> task);
- 防止对象在回调执行前被销毁。
五、代码示例:实现一个简单的资源管理器
#include <iostream>
#include <memory>
#include <unordered_map>
class Resource {
public:
Resource(const std::string& name) : name_(name) {
std::cout << "Resource " << name_ << " constructed.\n";
}
~Resource() {
std::cout << "Resource " << name_ << " destroyed.\n";
}
void use() { std::cout << "Using " << name_ << ".\n"; }
private:
std::string name_;
};
class ResourceManager {
public:
std::shared_ptr <Resource> get(const std::string& key) {
auto it = resources_.find(key);
if (it != resources_.end()) {
if (auto r = it->second.lock()) { // 资源仍然存在
return r;
}
}
// 资源不存在或已销毁,创建新资源
auto res = std::make_shared <Resource>(key);
resources_[key] = res; // weak_ptr 自动保存
return res;
}
private:
std::unordered_map<std::string, std::weak_ptr<Resource>> resources_;
};
int main() {
ResourceManager manager;
auto res1 = manager.get("texture_01");
res1->use();
{
auto res2 = manager.get("texture_01");
res2->use();
// res1 和 res2 指向同一资源,计数为 2
}
// res2 作用域结束,计数变 1
res1->use();
// 当 res1 结束时计数为 0,资源被销毁
}
说明
ResourceManager内部使用weak_ptr缓存资源,避免重复创建。- 每次请求资源时先尝试
lock(),若已失效则重新创建。
六、总结
- 选择合适的智能指针:单一所有权使用
unique_ptr,共享所有权使用shared_ptr,观察者使用weak_ptr。 - 避免裸指针与智能指针混用:统一使用智能指针。
- 注意循环引用:使用
weak_ptr断开循环。 - 利用
make_unique/make_shared:提高性能与安全性。 - 在多线程中正确使用:
shared_ptr的引用计数是线程安全的,访问对象本身需同步。
掌握这些原则后,你就能在 C++ 项目中自如地使用智能指针,写出既安全又高效的内存管理代码。