C++ 智能指针有哪几种?3 种用法与常见误区一次讲清

编程狮(w3cschool.cn) 2026-10-12 17:38:50 浏览数 (46)
反馈

当你在 C++ 里用 new 申请了对象之后,最怕的就是忘记 delete 或者多个地方同时管理同一块内存。一句话结论:C++ 智能指针本质是一个包装了裸指针的对象,它在析构时自动释放所指向的资源,从而把手动释放这件事从你的脑子里挪到了编译器保证的析构逻辑里。现代 C++ 提供了三种常用智能指针——unique_ptr 表示独占所有权、shared_ptr 表示共享所有权、weak_ptr 用于观测但不参与引用计数。它们分别解决这块内存只有我一个人用、多个人都要用、我想看但我不想延长它的生命周期三类问题。理解这三种指针的边界,能帮你避开大部分内存泄漏和 double free 的坑。下面先用一张表看清三者差异,再逐一给出可编译的示例与易错点。

C++ 智能指针三种用法对比封面

先看结论

你的需求 首选指针 所有权模型 关键风险
资源只归一个对象所有,禁止拷贝传递 unique_ptr 独占 不能拷贝,只能 move
多个对象需要共享同一份资源 shared_ptr 共享引用计数 循环引用会导致泄漏
只观测共享对象、打破循环引用 weak_ptr 不参与计数 使用前必须 lock 提升
极简确定、生命周期完全可控 裸指针 raw 无自动管理 必须手动 delete

记住一条铁律:凡是需要动态生命周期的对象,优先用 unique_ptr;只有真正需要共享时才升级到 shared_ptr;一旦发现 shared_ptr 之间有互相引用,立刻用 weak_ptr 打破环。

一、三种智能指针到底在管什么

要搞懂智能指针,得先回到裸指针的痛点。裸指针只是一个内存地址,它不记录谁负责释放,也不记录还有没有人正在用这块内存。你在一个函数里 new 出来,交给另一个函数使用,等到程序退出时,到底谁该 delete,全凭人工约定,约定一旦出错,要么泄漏,要么双重释放。智能指针把这种约定固化成了类型系统的规则,它内部仍然保存一个裸指针,但额外在析构函数里调用 delete,于是资源的释放时机被绑定到了智能指针对象自身生命周期结束的那一刻,这个套路在 C++ 里叫 RAII,也就是资源获取即初始化。

unique_ptr 是独占型,它禁止拷贝构造和拷贝赋值,只允许移动。这意味着同一时刻只有一个 unique_ptr 指向那块内存,析构时也只会有一次 delete,从根本上杜绝了双重释放。shared_ptr 是共享型,内部维护一个引用计数,每多一个 shared_ptr 指向同一对象,计数加一;每少一个,计数减一;减到零时才真正释放。weak_ptr 不增加计数,它只是挂着看一眼 shared_ptr 管理的对象还在不在,想用的时候调用 lock 尝试提升成 shared_ptr。

对比三者,性能上 unique_ptr 几乎没有额外开销,和裸指针几乎等价;shared_ptr 因为要维护计数、且计数操作通常要做原子同步,开销略大;weak_ptr 本身很轻,但 lock 时会触发一次计数检查。适用边界很清晰:能用 unique_ptr 解决就别上 shared_ptr,因为引用计数带来的不只是 CPU 开销,还有多线程下的内存序成本。理解了核心机制,下面给出三种指针的具体写法,建议先把基础语法过一遍 C++ 教程。

二、unique_ptr:独占所有权的最稳实现

最稳的做法是用 unique_ptr 管理一切独占资源。C++14 之后推荐用 std::make_unique 来创建,它既简洁又能避免裸 new 的异常安全问题——如果在 new 之后、把指针交给智能指针之前抛了异常,裸指针就会泄漏,而 make_unique 把这两步合成原子操作,从构造上堵死了这个口子。下面是一段完整可编译的示例。

#include <iostream>
#include <memory>

struct Buffer {
    int size;
    Buffer(int s) : size(s) { std::cout << "Buffer 创建, size=" << s << "\n"; }
    ~Buffer() { std::cout << "Buffer 释放, size=" << size << "\n"; }
};

int main() {
    std::unique_ptr<Buffer> p = std::make_unique<Buffer>(1024);
    // 独占:不能拷贝,下面这行若取消注释会编译失败
    // std::unique_ptr<Buffer> q = p;
    std::unique_ptr<Buffer> q = std::move(p);  // 只能移动
    if (!p) std::cout << "p 已交出所有权\n";
    return 0;  // q 离开作用域,Buffer 自动释放
}

运行这段程序,预期输出是先打印创建、再打印释放,证明析构被正确触发。你想把所有权转给别人,只能 std::move,移动之后原来的指针变成空,这是一个非常安全的命令式约定,编译器会阻止你意外产生第二个所有者。

如果管理的是数组,记得用 unique_ptr<T[]>,否则默认的 delete 不对数组生效,只会释放第一个元素而导致其余元素泄漏。另外 unique_ptr 允许自定义删除器,比如你要管理一个用 malloc 分配、需要用 free 释放的块,就传一个删除器。更多工程化写法可以参考 hellocpp 实战。关于删除器与数组的边界,建议对照官方标准示例,避免踩到数组释放的坑。

验证一个 unique_ptr 是否还持有对象,直接用 if (p) 判断即可,这比手写空指针检查更清晰,也更不容易写出悬空访问。失败排查时若发现对象没被释放,先检查是不是被意外移动走了,或者作用域比你想的要长——unique_ptr 的释放时机完全由它所在的作用域决定。

三、shared_ptr 与 weak_ptr 的取舍

当你确实需要多个对象共享同一份数据时,shared_ptr 才登场。它内部除了保存指针,还保存一个指向控制块的指针对,控制块里放着引用计数和删除器。下面示例展示引用计数如何随拷贝变化。

#include <iostream>
#include <memory>

int main() {
    auto a = std::make_shared<int>(42);
    std::cout << "创建后计数=" << a.use_count() << "\n";  // 1
    auto b = a;  // 拷贝,计数加一
    std::cout << "拷贝后计数=" << a.use_count() << "\n";  // 2
    std::weak_ptr<int> w = a;  // weak_ptr 不增加计数
    std::cout << "weak 后计数=" << a.use_count() << "\n"; // 仍是 2
    if (auto locked = w.lock()) {
        std::cout << "weak 提升成功, 值=" << *locked << "\n";
    }
    return 0;
}

运行输出会显示计数为 1、2、2,说明 weak_ptr 没有参与计数。这是 shared_ptr 和 weak_ptr 最大的区别:weak_ptr 只是观察者,它不会延长所观测对象的生命周期,因此非常适合做缓存、观察者列表这类有你就用、没有也不强留的场景。

取舍在于,shared_ptr 用得好能省去很多手工引用管理的麻烦,但代价是性能和循环引用风险。循环引用的典型场景是两个对象各持有一个指向对方的 shared_ptr,于是谁都不会降到零,内存永远不释放。打破它的办法是把其中一方的成员改成 weak_ptr,这样环被切断,计数能正常归零。C 语言没有这套机制,需要完全手动管理,对照可看 C 语言教程,理解手动管理的繁琐之后你会更珍惜智能指针。

适用场景总结:对象生命周期跨越多个所有者、且用 unique_ptr 表达不了时才用 shared_ptr;只要有一处只是想引用而不想延长生命周期,就用 weak_ptr。不要在函数参数里无脑传 shared_ptr 值,那样会无谓地增减计数,传引用或 const 引用更合适。

四、反例、边界与失败排查

最常见的失败是 double free,也就是同一块裸指针被两个智能指针分别接管。下面是一段会出问题的写法,务必避免。

#include <memory>

int main() {
    int* raw = new int(7);
    std::shared_ptr<int> a(raw);
    std::shared_ptr<int> b(raw);  // 错误:两个独立计数都从 1 起算
    return 0;  // 析构时同一指针被 delete 两次,未定义行为
}

修复方式是永远用 make_shared 或由另一个 shared_ptr 拷贝构造,不要拿同一个裸指针去构造多个智能指针。运行这类代码,程序可能崩溃,排错时先检查智能指针的构造来源是不是同一个资源,这是新手最高频的坑。

另一个边界是 shared_ptr 的循环引用泄漏,前文已经说明用 weak_ptr 打破。还有自定义删除器与数组的坑:shared_ptr 默认用 delete,管理数组需要传删除器或改用 unique_ptr<T[]>。最后,智能指针本身不要当成普通参数到处传值,传引用或 const 引用能减少不必要的计数开销。Linux 下可用相关工具观察分配行为,参考 Linux 教程,定位是否真的有对象没被释放。

这里还要提醒一个版本相关的细节:早期编译器对 make_unique 支持不完整,C++11 需要自己实现,C++14 起才进标准库,所以老项目里可能看到手写的 make_unique。安全方面,永远不要让智能指针和裸指针混管同一块内存,混管是悬空和双重释放的根源。

本文示例基于 C++17 标准文档与 GCC/Clang 实际运行(实际运行)。

C++ 智能指针选择决策流程图

总结

C++ 智能指针把资源释放从人工约定变成了类型保证:unique_ptr 管独占、零开销、只能移动;shared_ptr 管共享、靠引用计数自动释放;weak_ptr 只观测、用来打破循环引用。裸指针只适合生命周期完全确定、不需要智能管理的极简场景。记住优先级——能用 unique_ptr 就不用 shared_ptr,怀疑有循环引用就上 weak_ptr。把这套规则落到代码里,double free 和大部分内存泄漏都能在编译期或运行初期被拦下。想系统练习可接着看 C++ 官方教程。

延伸学习

常见问题

Q:unique_ptr 和 shared_ptr 有什么区别?什么时候该用哪个?

unique_ptr 独占所有权,不能拷贝只能移动,几乎零开销;shared_ptr 共享所有权,靠引用计数决定何时释放,有额外开销。规则很简单:默认用 unique_ptr,只有确实有多个所有者共享同一对象、且 unique_ptr 表达不了时才换成 shared_ptr。

Q:shared_ptr 循环引用为什么会导致内存泄漏?weak_ptr 怎么打破?

两个对象互相持有对方的 shared_ptr 时,引用计数永远无法归零,析构不会被触发,于是泄漏。打破方法是将其中一方的成员改为 weak_ptr,因为 weak_ptr 不参与计数,环被切断后计数能正常降到零,资源随之释放。

Q:智能指针能完全替代裸指针吗?哪些场景还得用裸指针?

绝大多数动态资源场景都能用智能指针替代。但仍有一些场景需要裸指针:比如需要和非 C++ 代码、C 风格 API 交互时,对方只接受裸指针;或者性能极度敏感、连引用计数的原子开销都要省的内层循环。这类场景要清楚谁负责释放,避免悬空。

0 人点赞