C++ 智能指针的核心并不是“自动释放内存”,而是用类型明确表达对象的所有权。当智能指针离开作用域时,它会自动完成资源释放,这正是 RAII(Resource Acquisition Is Initialization,资源获取即初始化)的典型应用。
1. 为什么需要智能指针
使用裸指针管理动态内存时,程序员必须确保每一条执行路径都能正确调用 delete:
|
|
一旦中途抛出异常、增加新的 return,或者所有权在多个函数之间传递,就容易产生:
- 内存泄漏;
- 重复释放;
- 悬空指针;
- 所有权不清晰。
使用 std::unique_ptr 后,析构过程由对象生命周期自动管理:
|
|
2. std::unique_ptr:独占所有权
std::unique_ptr<T> 表示:当前资源只有一个所有者。它不能复制,只能移动。
|
|
移动后,model 变为空指针,资源由 model2 管理。
函数接口如何表达所有权
|
|
如果函数只临时使用对象,不要为了“看起来安全”而传递智能指针;使用引用或裸指针作为非拥有型观察者即可。
数组
|
|
现代代码更推荐 std::vector<float>,因为它同时保存长度信息,也更方便传给算法接口。
自定义删除器
智能指针不只能管理 new 创建的内存,还能管理文件、系统句柄或 C API 资源:
|
|
对于 ONNX Runtime、OpenVINO 或相机 SDK,也可以用同样方式把对应的释放函数放入删除器。
cv::Mat、std::vector、std::string本身已经使用 RAII,不需要再套一层智能指针。
3. std::shared_ptr:共享所有权
std::shared_ptr<T> 允许多个对象共同拥有同一资源。最后一个所有者销毁时,资源才会释放。
|
|
它通常包含两个部分:
- 指向实际对象的指针;
- 控制块,其中保存强引用计数、弱引用计数和删除器。
优先使用 std::make_shared:
|
|
这通常能把对象和控制块放在一次内存分配中,同时避免构造过程中的异常安全问题。
不要因为“不确定”就使用 shared_ptr
引用计数会增加内存占用,并带来原子计数操作。更重要的是,它容易掩盖本应明确的生命周期设计。只有资源确实需要被多个长期存在的对象共同拥有时,才应该使用它。
线程安全不等于对象安全
不同 shared_ptr 实例对同一控制块进行复制和销毁通常是线程安全的,但它指向的对象并不会因此自动变成线程安全:
|
|
4. std::weak_ptr:观察共享对象
std::weak_ptr<T> 不增加强引用计数,用来观察由 shared_ptr 管理的对象。
|
|
不要先调用 expired() 再使用对象,因为两次操作之间对象仍可能被其他线程释放。直接调用 lock(),得到一个临时 shared_ptr。
解决循环引用
|
|
如果 Parent 和 Child 都使用 shared_ptr 指向对方,它们的引用计数永远不会归零。将不负责拥有对方的一侧改成 weak_ptr,即可打破循环。
5. 常见错误
用同一个裸指针创建多个 shared_ptr
|
|
正确做法是复制已有的 shared_ptr:
|
|
随意调用 get()
|
|
get() 返回的裸指针不能被 delete,也不应该被保存到超过智能指针生命周期的位置。
混淆 release() 和 reset()
|
|
除非必须把资源交给接管所有权的旧式 C API,否则应谨慎使用 release()。
对 this 重新创建 shared_ptr
如果对象已经由 shared_ptr 管理,不要写:
|
|
需要安全获得自身的 shared_ptr 时,应继承 std::enable_shared_from_this<T>:
|
|
6. C ABI 推理库中的用法
C++ 智能指针不能直接跨 C ABI 导出,也不适合直接暴露给 C# P/Invoke。常见做法是对外提供不透明句柄,对内使用 RAII。
|
|
在 DLL 内部,Runtime 可以继续使用智能指针管理模型资源:
|
|
关键原则是:在哪个模块创建,就在哪个模块释放。C# 只保存句柄,并确保最终调用 DestroyRuntime;不要让 C# 直接释放由 C++ DLL 分配的对象。
如果一个模型会被多个推理任务长期共享,可以在内部使用 shared_ptr<Session>;如果每个 Runtime 独占自己的模型会话,则优先使用 unique_ptr。
7. 如何选择
| 需求 | 推荐类型 |
|---|---|
| 单一对象独占资源 | std::unique_ptr<T> |
| 多个对象共同决定资源生命周期 | std::shared_ptr<T> |
| 观察共享资源,不延长生命周期 | std::weak_ptr<T> |
| 必须可为空,但不拥有资源 | 裸指针 T* |
| 必须存在且不拥有资源 | 引用 T& |
| 连续动态数组 | std::vector<T> |
实际工程中可以遵循一个简单顺序:
- 默认让对象直接按值存在;
- 必须动态分配时,优先
unique_ptr; - 确实存在共享所有权时,才使用
shared_ptr; - 用
weak_ptr表达非拥有关系并打破循环引用; - 除非接口明确转移所有权,否则裸指针只作为观察者。
智能指针真正解决的是资源生命周期和所有权表达问题。选对所有权模型,比单纯把 new/delete 替换成 shared_ptr 更重要。