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,甚至可以是栈上对象。但代价是:调用方必须保证指针在函数执行期间有效。

💡 我的建议: 如果函数只是临时访问资源,不会保存指针,用原始指针最干净。如果函数需要参与资源生命周期管理,用 unique_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); 搞定。

🔑 关键点: 返回 unique_ptr 时,编译器会做 RVO(返回值优化)或者隐式移动,不会有额外的拷贝开销。你尽管返回,性能交给编译器。

我曾经在一个项目里看到有人返回原始指针,然后让调用方自己 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_backemplace_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);
⚠ 警告: 这是最危险的操作之一。你必须确保:
  1. 这个原始指针不是已经被其他智能指针管理了
  2. 这个指针确实需要被 delete(不是栈上对象,不是数组的一部分)
  3. 不会有两个 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 知识体系总览

下面这张图,我把本章的核心知识点串起来了。你可以把它当作一个快速参考。

unique_ptr 实战 函数参数 传值(转移) 传引用(借用) 传原始指针 返回值 工厂函数标配 RVO / 隐式移动 容器使用 push_back / emplace_back 遍历用引用 指针互转 get() 安全 reset() 危险 release() 特殊 核心原则 明确所有权语义:谁拥有,谁负责释放 避免裸 new / delete,让智能指针管理生命周期

7.6 避坑指南

最后,我把自己踩过的坑总结一下,希望能帮你少走弯路。

场景 常见错误 正确做法
函数参数 传 const unique_ptr& 但函数内需要转移所有权 用传值 + std::move
容器排序 直接对 vector<unique_ptr> 排序报错 用移动迭代器或自定义比较器
原始指针转 unique_ptr 两个 unique_ptr 管理同一指针 确保指针来源唯一,用 std::move 转移
返回局部 unique_ptr 担心拷贝开销,改用原始指针返回 直接返回 unique_ptr,编译器会优化
💡 最后说一句: unique_ptr 的设计哲学就是“明确所有权”。你写代码的时候,每用一次 unique_ptr,都问自己一句:“这个资源到底归谁管?” 想清楚了,代码自然就干净了。

公众号:蓝海资料掘金营,微信deep3321