C++中如何用智能指针管理临时对象?

来源:C++教程作者:郭世昌头衔:网络博主
导读:本期聚焦于郭世昌创作的《C++中如何用智能指针管理临时对象?》,敬请观看详情。一个刚构造出来的对象还没有名字,就被塞进接口调用,稍后谁来释放它?这类临时对象的所有权问题在 C++ 中尤其容易踩坑。智能指针的价值不只是自动释放,更在于把所有权意图写进类型。unique_ptr 适合独占接管临时对象,shared_ptr 则能把临时对象的生命周期延长到最后一个使用者,而 weak_ptr 可以安全观察而不延长生命周期。本文聚焦三种典型场景:从工厂函数返回临时堆对象、把 new 出来的临时对象直接传给容器或回调、以及用 shared_ptr 从 this 创建临时控制块。你会看到为什么直接 delete 临时指针容易导致双重释放或悬空引用,以及如何借助 make_unique、make_shared 和 enable_shared_from_this 让临时对象的管理更安全。读完可以形成一套判断标准:什么时候用 unique_ptr 转交所有权,什么时候用 shared_ptr 延长生命周期。

临时对象在 C++ 中是一个非常容易出问题的概念。它可能是一个没有名字的右值,也可能是从函数中直接 new 出来、还没来得及赋给任何变量的堆对象。比如某个接口的参数需要一个指针,调用方随手写下一个 new 表达式,这个对象的所有权立刻变得模糊:实现方要不要负责释放?调用方在接口返回之后还能不能继续使用?如果双方都释放,程序会崩溃;如果都不释放,内存就泄漏了。智能指针恰好提供了一套明确的语义来解决这类临时对象的管理问题。

C++中如何用智能指针管理临时对象?

理解临时对象被智能指针管理的关键,在于先看清裸指针的缺陷。C++ 中的裸指针本质上只是一个地址,它不携带任何所有权信息。当你看到一个函数声明为 void process(Task* task) 时,你无法从签名判断这个函数会不会删除传入的对象。这种不确定性在临时对象场景下会被放大:调用方可能写 process(new Task()),此时对象刚创建就被传入,调用方既没有保留指针,也没有明确是否应该由自己释放。如果 process 内部执行了 delete,调用方就没有任何问题;但如果调用方以为 process 不会 delete,自己又在之后补一次 delete,就会导致双重释放。反之,如果双方都不删除,临时对象就变成了一块无人回收的内存。

更隐蔽的问题发生在异常路径上。假设在创建临时对象之后、传给某个函数之前,代码抛出了异常,那么这个临时对象就永远不会被释放。智能指针的引入,正是为了把这种隐式的、靠约定维持的所有权关系,变成由类型系统强制保证的规则。

一、临时对象的所有权困境与智能指针的价值

临时对象的所有权困境可以从三个维度来理解:创建之后谁来保存指针、使用之后谁来释放、异常发生时如何兜底。在传统 C++ 代码里,这三件事往往分散在不同的逻辑中,甚至依赖注释来说明。例如一个工厂函数返回裸指针,文档里写着调用方负责释放,但代码审查时很难保证每个调用点都遵守这一约定。

智能指针通过类型来编码所有权语义。std::unique_ptr<T> 表示独占所有权:同一时刻只有一个 unique_ptr 可以指向某个对象,当这个 unique_ptr 离开作用域时,对象一定会被释放。std::shared_ptr<T> 表示共享所有权:多个 shared_ptr 可以指向同一个对象,对象会在最后一个 shared_ptr 被销毁时释放。std::weak_ptr<T> 不拥有对象,只是观察 shared_ptr 管理的对象是否还存在。这些类型一旦出现在函数签名中,调用方和实现方就能立刻明白所有权的转移规则,不需要再猜。

对于临时对象来说,最直接的好处是:你可以把一个临时堆对象交给 unique_ptr 或 shared_ptr,从这一刻起,无论后续代码走哪条路径,对象都能被正确释放。即便发生异常,栈展开过程也会调用智能指针的析构函数,不会泄漏。下面通过一个裸指针的反面示例,看看问题是如何发生的。

#include <iostream>
#include <string>

class Task {
public:
    Task(const std::string& name) : name_(name) {
        std::cout << "Task created: " << name_ << std::endl;
    }
    ~Task() {
        std::cout << "Task destroyed: " << name_ << std::endl;
    }
private:
    std::string name_;
};

void runTask(Task* task) {
    if (task != nullptr) {
        std::cout << "Running task" << std::endl;
        delete task; // 实现方擅自删除,调用方完全不知情
    }
}

int main() {
    runTask(new Task("temp")); // 临时对象所有权不清晰
    return 0;
}

这段代码看似能正常运行,但实际上把删除动作藏在了函数内部。如果将来调用方需要复用同一个任务对象,或者 runTask 被重构为异步执行,对象可能在当前作用域之后才被使用,就会产生悬空指针。进一步说,如果 runTask 的调用方也决定释放该指针,双重释放就会发生。智能指针就是用来消除这类歧义的。

二、使用 unique_ptr 接管临时对象

当临时对象的生命周期只需要一个明确的所有者时,unique_ptr 是最合适的选择。它体积小、开销低,几乎和裸指针一样,但提供了自动释放和独占语义。创建临时对象时,推荐使用 std::make_unique<T>,它能够把参数完美转发给构造函数,并返回一个已经接管对象的 unique_ptr。

工厂函数是 unique_ptr 管理临时对象的典型应用。工厂函数内部可以创建堆对象,然后把所有权以 unique_ptr 的形式返回给调用方。这样做有两个好处:第一,调用方看到返回类型是 unique_ptr,就知道自己获得了独占所有权;第二,即使调用方忘记接收返回值,临时 unique_ptr 在语句结束时也会自动释放对象,不会泄漏。例如下面的代码展示了工厂函数如何安全地返回临时任务对象。

#include <memory>
#include <iostream>
#include <string>

class Task {
public:
    Task(const std::string& name) : name_(name) {}
    void run() const {
        std::cout << "Running " << name_ << std::endl;
    }
private:
    std::string name_;
};

std::unique_ptr<Task> createTask(const std::string& name) {
    // make_unique 直接构造对象并返回独占所有权
    return std::make_unique<Task>(name);
}

void consumeTask(std::unique_ptr<Task> task) {
    if (task) {
        task->run();
    }
    // task 离开作用域时自动释放临时对象
}

int main() {
    auto task = createTask("background-job");
    consumeTask(std::move(task)); // 显式转移所有权
    if (!task) {
        std::cout << "task is now empty" << std::endl;
    }
    return 0;
}

注意 consumeTask 的参数类型是 std::unique_ptr<Task>,这意味着调用方必须使用 std::move 把所有权传入函数。进入函数后,参数 task 成为新的唯一所有者,函数返回时临时对象被自动删除。调用方的原 unique_ptr 被置为空,杜绝了重复释放的可能。

unique_ptr 也适用于接口设计中明确表达所有权的场景。例如一个容器类需要持有传入的对象,就可以把插入接口设计为接受 unique_ptr,让调用方把临时对象的所有权转移给容器。容器内部再把 unique_ptr 移动到自己的存储结构中。整个过程不需要手动 delete,所有权的转移路径清晰可见。

不过 unique_ptr 的局限在于它不能复制,只能移动。如果临时对象需要被多个模块同时使用,或者你无法确定哪个模块最后使用完对象,shared_ptr 会是更好的选择。

三、用 shared_ptr 延长临时对象的生命周期

shared_ptr 通过控制块来记录引用计数,从而允许多个 shared_ptr 共同拥有同一个对象。临时对象如果被多个接收方同时持有,直接使用裸指针或 unique_ptr 都不合适,因为无法协调释放时机。此时用 std::make_shared<T> 创建一个共享所有权的临时对象,就能让对象的生命周期自动延长到最后一个接收方释放为止。

一个常见的场景是事件监听机制。临时创建的监听对象需要注册到多个事件源中,每个事件源都保存一个 shared_ptr,只要还有事件源引用该监听对象,对象就不会被释放。下面的示例展示了如何用 shared_ptr 和 enable_shared_from_this 安全地管理这种临时对象。

#include <memory>
#include <iostream>
#include <vector>

class Listener : public std::enable_shared_from_this<Listener> {
public:
    void registerSelf(std::vector<std::shared_ptr<Listener>>& listeners) {
        // shared_from_this 从当前对象的控制块中生成新的 shared_ptr
        listeners.push_back(shared_from_this());
    }
    void notify() const {
        std::cout << "Listener notified" << std::endl;
    }
};

int main() {
    std::vector<std::shared_ptr<Listener>> listeners;
    auto listener = std::make_shared<Listener>();
    listener->registerSelf(listeners);
    listener.reset(); // 局部 shared_ptr 释放,但容器仍持有对象
    if (!listeners.empty()) {
        listeners.front()->notify();
    }
    return 0;
}

这里 registerSelf 内部调用了 shared_from_this(),它依赖于对象已经被一个 shared_ptr 管理。如果对象只是一个栈对象,或者是从裸指针创建的 shared_ptr,那么 shared_from_this() 会抛出 std::bad_weak_ptr 异常。因此使用 enable_shared_from_this 时,通常都要保证对象通过 make_shared 或 shared_ptr 构造函数创建。

shared_ptr 的控制块不仅记录引用计数,还记录弱引用计数。当临时对象需要被缓存、但又不想因为缓存而阻止对象释放时,可以使用 weak_ptr。weak_ptr 可以从 shared_ptr 构造,调用 lock() 方法尝试获取一个有效的 shared_ptr:如果对象还存在,就返回非空指针;如果对象已经被释放,就返回空。这种设计非常适合处理临时对象的观察者模式,既能安全访问,又不会造成内存长期占用。

但 shared_ptr 的灵活性也有代价。控制块需要额外分配内存,引用计数的增减在多线程环境下需要使用原子操作,因此在性能敏感的场景中,如果所有权关系简单明确,还是应该优先考虑 unique_ptr。

四、避开临时对象管理中的常见陷阱

第一个常见陷阱是从同一个裸指针构造多个 shared_ptr。由于每个 shared_ptr 都会创建一套独立的控制块,引用计数不会共享,最终会导致同一个对象被释放多次。例如下面的代码就隐藏着双重释放的风险。

#include <memory>

class Resource {
public:
    Resource() = default;
    ~Resource() = default;
};

int main() {
    Resource* raw = new Resource();
    std::shared_ptr<Resource> p1(raw);
    std::shared_ptr<Resource> p2(raw); // 错误:两个控制块,双重释放
    return 0;
}

正确的做法是只从一个 shared_ptr 创建其他 shared_ptr,或者直接使用 std::make_shared<Resource>() 创建临时对象,让所有副本共享同一个控制块。如果必须从裸指针创建 shared_ptr,那么之后就不要再使用该裸指针,也不要再创建第二个 shared_ptr。

第二个陷阱是试图用智能指针管理栈上对象。栈对象的内存由编译器自动回收,如果用一个智能指针去接管栈对象的地址,智能指针析构时会对栈内存调用 delete,造成未定义行为。例如 int value = 10; std::unique_ptr<int> p(&value); 就是典型的错误。智能指针只能管理堆上分配的对象,临时对象如果本身就是栈上右值,就不应该用智能指针去接管。

第三个陷阱是在异常安全方面过度依赖裸指针。假设你写了 foo(std::shared_ptr<Task>(new Task), bar()),如果 bar() 在参数求值过程中抛出异常,new 出来的 Task 可能不会进入 shared_ptr,从而泄漏。使用 std::make_shared<Task>() 可以避免这种问题,因为它把对象创建和控制块分配合并为一次操作,并且不会在参数求值顺序不确定时遗留裸指针。

最后一个值得注意的细节是,不要在接口中随意返回 shared_ptr 的引用或裸指针。临时对象一旦被 shared_ptr 管理,调用方就只应该通过 shared_ptr 或 weak_ptr 访问。如果你把内部的裸指针暴露给外部,外部的代码可能绕过引用计数,在对象释放后继续使用,产生悬空引用。保持所有权类型的一致性是安全使用智能指针的核心原则。

综合来看,C++ 管理临时对象时,首先要判断所有权是否需要独占。如果只有一个明确的所有者,unique_ptr 是最轻量的方案;如果多个接收方需要共享所有权,shared_ptr 配合 weak_ptr 能有效延长生命周期并避免悬空访问。无论选择哪种智能指针,都应该优先使用 make_unique 和 make_shared 来创建临时对象,避免直接接触裸指针,并保持所有权语义在接口中清晰可见。

C++智能指针临时对象unique_ptr修改时间:2026-08-19 17:53:34

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。