C++智能指针怎么选?unique_ptr、shared_ptr、weak_ptr所有权边界

编程狮 2026-09-20 10:52:17 浏览数 (23)
反馈

C++ 智能指针的选择只看所有权:单一所有者用 unique_ptr,多个共同所有者用 shared_ptr,只观察不拥有用 weak_ptr。先确定谁负责释放,再写类型。

C++ 智能指针选型封面

你把裸指针替换成 shared_ptr 后,程序可能不再崩溃,却出现对象迟迟不析构;把所有地方都改成 unique_ptr,又会卡在复制和共享关系上。问题不在语法,而在对象生命周期没有画清。本文基于 C++17 的智能指针语义,用 Repository、Service 和双向节点三个完整例子说明选择边界。本机没有 C++ 编译器,代码给出预期结果(未在本机执行),不声称完成本地编译。

一、先看结论:三类智能指针怎么选

关系 推荐类型 能否复制 是否影响对象寿命 典型场景
唯一负责人 unique_ptr 不能,只能 move 工厂返回值、独占资源
多个共同负责人 shared_ptr 可以 是,引用计数归零时释放 会话、缓存、共享配置
非拥有观察者 weak_ptr 可以 否,需要 lock 后访问 反向引用、观察者、缓存
仅访问不拥有 T& / const T& 不适用 函数参数
可空访问不拥有 T* 不适用 可选参数、非拥有指针

一句话:先确定谁负责释放,再写类型;单一所有者优先 unique_ptr,真实共享才用 shared_ptr,只观察用 weak_ptr

二、先把所有权和访问权分开

所有权回答“谁负责让对象活着并最终释放”;访问权只回答“谁现在可以使用对象”。裸指针和引用很适合表达访问,却不会自动说明释放责任。智能指针把所有权写进类型,让异常路径和提前返回也能依靠析构完成清理。

如果构造、析构和指针基础还不稳,先过一遍 C++ 基础教程。智能指针不是“更安全的星号”,而是 API 对生命周期的承诺。

概念 回答的问题 常见表达
所有权 谁负责释放? unique_ptrshared_ptr
访问权 谁现在可以用? T&const T&T*
观察权 谁只想看看? weak_ptr、裸指针

三、unique_ptr:默认从单一所有者开始

大多数动态对象都应从 unique_ptr 开始。下面的 Repository 只由 main 拥有,Service 暂时借用它,不需要参与释放。

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

class Repository {
public:
    std::string find_name(int id) const {
        return id == 1 ? "w3cschool" : "unknown";
    }
};

class Service {
public:
    // Service 只借用 Repository 的引用,不拥有它
    explicit Service(const Repository& repo) : repo_(repo) {}

    void print(int id) const {
        std::cout << repo_.find_name(id) << '\n';
    }

private:
    // 引用成员:表示“我能访问,但不负责释放”
    const Repository& repo_;
};

int main() {
    // 创建唯一所有者:main 负责释放 Repository
    auto repo = std::make_unique<Repository>();

    // 把引用传给 Service,Service 不参与所有权
    Service service(*repo);

    service.print(1);
}

预期输出(未在本机执行)是:

w3cschool

Service 使用引用表达“我能访问,但我不拥有”。若要把 Repository 的所有权转交给另一个组件,应使用 std::move;move 后旧 unique_ptr 为空,不能再解引用。

// 所有权从 repo 转移到另一个组件
auto another = std::move(repo);

// 此时 repo 为空,不能再解引用
// repo->find_name(1); // 错误:repo 已为空

unique_ptr 的优势不只是没有引用计数开销,更重要的是结构清楚:任何时刻只有一个释放负责人。工厂函数也应优先返回 unique_ptr,让调用方明确接管对象。

操作 是否允许 说明
复制 unique_ptr 不允许 所有权唯一
移动 unique_ptr 允许 所有权转移,原指针置空
解引用 允许,但需非空 move 后不可解引用
作为函数参数 按需 转移所有权用 std::move,只访问用引用

四、shared_ptr:只有共同决定寿命时才使用

shared_ptr 适合多个独立组件都需要保证对象存活的场景。例如后台任务和界面控制器都持有同一 Session,任意一方结束都不能立即销毁对象。

#include <iostream>
#include <memory>

struct Session {
    // 析构时打印,观察释放时机
    ~Session() { std::cout << "session closed\n"; }
};

void background_job(std::shared_ptr<Session> session) {
    // use_count 仅用于演示,不应作为并发业务判断
    std::cout << "job owners=" << session.use_count() << '\n';
}

int main() {
    // 创建共享所有者:引用计数初始为 1
    auto session = std::make_shared<Session>();
    std::cout << "main owners=" << session.use_count() << '\n';

    // 传值会增加引用计数,函数返回后恢复
    background_job(session);
    std::cout << "after job=" << session.use_count() << '\n';

    // main 结束时引用计数归零,Session 析构
}

预期输出(未在本机执行):

main owners=1
job owners=2
after job=1
session closed

进入 background_job 时计数增加,函数返回后回到 1,main 结束时打印 session closeduse_count 适合演示,不适合作为并发业务判断,因为读取后计数可能立即变化。

⚠️ 注意:不要从同一个裸指针分别构造两个 shared_ptr。它们会形成两个控制块,最终对同一地址释放两次。优先使用 make_shared

// 错误:两个 shared_ptr 从同一个裸指针构造,会重复释放
// Session* raw = new Session();
// std::shared_ptr<Session> a(raw);
// std::shared_ptr<Session> b(raw);

// 正确:使用 make_shared 创建单一控制块
auto a = std::make_shared<Session>();
auto b = a; // 共享同一控制块
写法 控制块数量 是否安全
std::make_shared<T>() 1 安全
shared_ptr<T>(new T) 1 安全,但效率略低
同一裸指针构造两个 shared_ptr 2 危险,重复释放

五、weak_ptr:打断循环引用并安全观察

shared_ptr 最大的边界是循环引用。父节点拥有子节点,子节点若也 shared_ptr 回指父节点,两边计数都无法归零。回指通常只表示观察关系,应改成 weak_ptr

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

struct Parent;

struct Child {
    // 子节点只观察父节点,不拥有它
    std::weak_ptr<Parent> parent;
};

struct Parent {
    std::string name = "root";

    // 父节点拥有子节点
    std::shared_ptr<Child> child = std::make_shared<Child>();
};

int main() {
    // 创建父节点,引用计数为 1
    auto parent = std::make_shared<Parent>();

    // 子节点弱引用父节点,不增加强引用计数
    parent->child->parent = parent;

    // lock 成功则返回 shared_ptr,失败则返回空
    if (auto owner = parent->child->parent.lock()) {
        std::cout << owner->name << '\n';
    }
}

预期输出(未在本机执行)是:

root

weak_ptr 不增加强引用计数,lock 会在对象仍存活时返回 shared_ptr,已释放时返回空指针。它适合缓存、观察者和反向关系,不适合承担必然存活的前置条件。

操作 shared_ptr weak_ptr
增加强引用计数
直接解引用 可以 不可以
访问对象 直接访问 先 lock 再判断
影响对象寿命

理解裸指针、地址和值传递时,也可以旁查 C 语言指针教程。但在 C++ API 中,裸指针更适合表达非拥有访问,释放职责应由对象、容器或智能指针清晰承担。

六、C++ 智能指针的选型与常见失败

做设计评审时,可以按顺序提问:

顺序 问题 推荐选择
1 对象能否直接放在栈上或作为成员? 不需要动态分配
2 如果必须动态创建,是否只有一个所有者? unique_ptr
3 多个组件是否真的共同决定寿命? 是才用 shared_ptr
4 某条关系是否只想观察? 引用、裸指针或 weak_ptr
5 图结构是否存在环? 至少一条回边用 weak_ptr

失败排查时,重复释放通常来自混用手动 delete 与智能指针,内存不释放通常来自 shared_ptr 环,偶发空访问则要检查 weak_ptr lock 的返回值。把所有权图画出来,往往比盯着崩溃栈更快。

常见失败 现象 原因 修复方向
重复释放 崩溃、double free 手动 delete 与智能指针混用 只用智能指针管理释放
内存不释放 对象迟迟不析构 shared_ptr 循环引用 回边改为 weak_ptr
空访问 偶发崩溃 weak_ptr lock 失败未判断 先判断再访问
复制错误 编译报错 试图复制 unique_ptr 改为引用或 std::move
控制块分裂 重复释放 同一裸指针构造多个 shared_ptr 统一用 make_shared

用一个生命周期测试验证选型

前置条件是准备支持 C++17 的编译器,并打开警告选项。先为示例对象增加构造、析构日志,再分别运行单一所有者、共同所有者和循环引用三组程序:

测试场景 预期结果
unique_ptr 移交后 原指针为空,新指针有效
最后一个 shared_ptr 离开作用域 析构日志恰好出现一次
双向关系一端改为 weak_ptr 两个对象都正常析构
异常路径 栈展开仍释放对象
weak_ptr lock 失败 返回空,不崩溃

unique_ptr 移交后,原指针应为空;最后一个 shared_ptr 离开作用域后,析构日志应恰好出现一次;将双向关系的一端改为 weak_ptr 后,两个对象都应正常析构。若编译阶段报告复制 unique_ptr,应把接口改为引用或显式 std::move,而不是换成 shared_ptr 绕过错误。

验证结果还要覆盖异常路径:在创建资源后主动抛出异常,确认栈展开仍会释放对象;对 weak_ptr 调用 lock 前先让最后一个 shared_ptr 离开作用域,预期 lock 返回空。本文代码按 C++17 语义检查,但当前环境没有 C++ 编译器,因此没有声称实际执行;复制到项目后应以编译警告、析构次数和内存检测工具结果作为最终证据。

C++ 三类智能指针的所有权关系图

七、函数参数传递建议

参数类型 是否拥有 是否可空 适用场景
T& 函数内访问,且对象必然存在
const T& 只读访问,且对象必然存在
T* 可选访问,不参与释放
unique_ptr<T> 转移所有权 函数接管对象
shared_ptr<T> 共享所有权 函数需要延长对象寿命
weak_ptr<T> 函数需要观察但可能已释放

只有函数需要延长对象寿命或保存所有权时才传 shared_ptr。只在调用期间访问,优先传 T&const T& 或指针表达可空访问。

总结

C++ 智能指针不是按“功能多少”选择,而是按所有权数量选择。unique_ptr 是默认起点,shared_ptr 只用于真实共同所有,weak_ptr 负责观察和打断环。

接口设计时先尝试值、引用和栈对象,再考虑动态分配;已经使用 shared_ptr 的代码,则重点审查控制块来源和循环引用。类型把生命周期说清之后,异常安全和资源释放才会自然变简单。

延伸学习

  1. 通过 C++ 入门课程 系统补齐类、构造函数与 STL;
  2. 阅读 C++ 入门路径笔记,安排指针和对象模型的学习顺序;
  3. 对照 Python 与 C++ 选择指南,理解自动内存管理与显式所有权的差异。

常见问题

Q:shared_ptr 是不是比 unique_ptr 更通用?

A:功能更多不代表更适合。shared_ptr 增加控制块、原子计数和环引用风险;单一所有权用 unique_ptr 语义更清楚、成本也更低。

Q:函数参数应该传 shared_ptr 还是引用?

A:只有函数需要延长对象寿命或保存所有权时才传 shared_ptr。只在调用期间访问,优先传 T&const T& 或指针表达可空访问。

Q:weak_ptr 能直接解引用吗?

A:不能。先调用 lock 获取临时 shared_ptr,再判断是否为空。这样能避免观察对象已经析构后继续访问。

Q:可以对智能指针指向的对象调用 delete 吗?

A:不可以。对象的释放由智能指针控制,手动 delete 会造成重复释放或悬空指针。若需要定制释放方式,应在构造智能指针时提供删除器。

C++

0 人点赞