7. unique_ptr 详解(下):作为函数参数与返回值、在容器中的使用、与原始指针互转
好,咱们接着聊 unique_ptr。上一章我们把 unique_ptr 的“独有所有权”和移动语义讲透了。这一章,我带你看看它在实际工程里最常见的几个场景:怎么传参、怎么返回、怎么塞进容器,以及——怎么跟原始指针打交道。
说实话,这些才是你每天写代码都会碰到的问题。理论再漂亮,用不对地方也是白搭。
7.1 作为函数参数:三种姿势,各有讲究
unique_ptr 作为函数参数,有三种常见写法。每种写法背后,都对应着不同的所有权语义。我见过不少同事在这上面翻车,咱们一个一个说清楚。
姿势一:传值(转移所有权)
void takeOwnership(std::unique_ptr<Resource> ptr) {
// 现在 ptr 拥有这个资源
ptr->doSomething();
// 函数结束,ptr 析构,资源释放
}
auto res = std::make_unique<Resource>();
takeOwnership(std::move(res));
// 此时 res 已经是 nullptr
这种写法,说白了就是“我把东西交给你,你全权处理”。调用方必须用 std::move 显式转移所有权。函数内部拿到资源后,想怎么用就怎么用,用完自动释放。
姿势二:传引用(借用,不转移)
void useResource(const std::unique_ptr<Resource>& ptr) {
if (ptr) {
ptr->doSomething();
}
}
auto res = std::make_unique<Resource>();
useResource(res); // 所有权还在 res 手里
我个人习惯用这种写法。为什么?因为它明确告诉调用者:“我只是借用一下,不会拿走你的东西。” 而且调用方不需要写 std::move,代码看起来更自然。
不过要注意,传引用意味着函数内部不能转移所有权。如果你需要把资源存到别处,那就得用传值。
姿势三:传原始指针(最灵活,也最危险)
void doSomething(Resource* raw_ptr) {
if (raw_ptr) {
raw_ptr->doSomething();
}
}
auto res = std::make_unique<Resource>();
doSomething(res.get()); // 取出原始指针传进去
嗯,这里要小心。传原始指针是最灵活的方式——函数不关心你用的是 unique_ptr 还是 shared_ptr,甚至可以是栈上对象。但代价是:调用方必须保证指针在函数执行期间有效。
7.2 作为返回值:工厂函数的标配
unique_ptr 作为返回值,是现代 C++ 里最优雅的模式之一。我写过很多工厂函数,几乎清一色返回 unique_ptr。
std::unique_ptr<Resource> createResource(int type) {
switch (type) {
case 1: return std::make_unique<ResourceA>();
case 2: return std::make_unique<ResourceB>();
default: return nullptr;
}
}
auto res = createResource(1);
// res 自动管理资源,不用操心释放
为什么这种写法好?因为调用方拿到 unique_ptr 后,所有权就明确了。想自己用?直接拿着。想转给别人?std::move 一下。想转成 shared_ptr?std::shared_ptr<Resource> ptr = std::move(res); 搞定。
我曾经在一个项目里看到有人返回原始指针,然后让调用方自己 delete。结果呢?三个月后,内存泄漏报表里全是那个函数的“功劳”。改成 unique_ptr 后,世界清净了。
7.3 在容器中的使用:vector<unique_ptr> 的正确姿势
把 unique_ptr 放进容器,是管理多态对象集合的经典做法。但这里有个坑——unique_ptr 不能拷贝,所以容器的很多操作会受影响。
std::vector<std::unique_ptr<Resource>> vec;
// 添加元素:必须用移动语义
vec.push_back(std::make_unique<Resource>());
auto res = std::make_unique<Resource>();
vec.push_back(std::move(res)); // 显式移动
// 遍历:用引用,别拷贝
for (const auto& ptr : vec) {
if (ptr) ptr->doSomething();
}
// 排序:需要提供移动迭代器
std::sort(vec.begin(), vec.end(),
[](const auto& a, const auto& b) {
return a->getValue() < b->getValue();
});
std::vector<std::unique_ptr<int>> vec(10); 这种代码。默认构造会尝试拷贝 unique_ptr,编译直接报错。正确做法是用 reserve 预留空间,然后逐个 push_back 或 emplace_back。
我个人习惯用 emplace_back 代替 push_back,因为前者直接在容器内构造对象,少一次移动操作。虽然现代编译器优化得很好,但好习惯总没错。
7.4 与原始指针互转:小心驶得万年船
unique_ptr 和原始指针的互转,是实际工程里绕不开的话题。毕竟很多老代码、C 库接口都只认原始指针。
unique_ptr → 原始指针(安全)
auto ptr = std::make_unique<Resource>();
Resource* raw = ptr.get(); // 取出指针,不转移所有权
// 注意:raw 的生命周期不能超过 ptr
// 如果 ptr 被销毁或 reset,raw 就变成悬空指针
用 get() 取出原始指针是安全的,前提是你遵守一个规则:不要 delete 这个指针,不要保存它超过 unique_ptr 的生命周期。
原始指针 → unique_ptr(危险,但有时必须做)
// 从 C 库函数获取原始指针
Resource* raw = some_c_function();
// 接管所有权
std::unique_ptr<Resource> ptr(raw);
// 或者用 reset
ptr.reset(raw);
- 这个原始指针不是已经被其他智能指针管理了
- 这个指针确实需要被 delete(不是栈上对象,不是数组的一部分)
- 不会有两个 unique_ptr 同时管理同一个指针
我曾经在一个遗留系统里接手过一个 bug:两个模块各自用 unique_ptr 接管了同一个原始指针,结果程序崩溃得那叫一个惨烈。从那以后,我每次做这种转换都会再三确认指针的“身世”。
释放所有权但不释放资源
auto ptr = std::make_unique<Resource>();
Resource* raw = ptr.release(); // ptr 放弃所有权,不再管理资源
// 现在 raw 需要手动 delete
delete raw;
release() 用得不多,但有时候确实需要。比如你要把资源交给一个 C 风格的 API,而那个 API 承诺会负责释放。这时候 release() 就派上用场了。
7.5 知识体系总览
下面这张图,我把本章的核心知识点串起来了。你可以把它当作一个快速参考。
7.6 避坑指南
最后,我把自己踩过的坑总结一下,希望能帮你少走弯路。
| 场景 | 常见错误 | 正确做法 |
|---|---|---|
| 函数参数 | 传 const unique_ptr& 但函数内需要转移所有权 | 用传值 + std::move |
| 容器排序 | 直接对 vector<unique_ptr> 排序报错 | 用移动迭代器或自定义比较器 |
| 原始指针转 unique_ptr | 两个 unique_ptr 管理同一指针 | 确保指针来源唯一,用 std::move 转移 |
| 返回局部 unique_ptr | 担心拷贝开销,改用原始指针返回 | 直接返回 unique_ptr,编译器会优化 |
公众号:蓝海资料掘金营,微信deep3321