news 2026/9/8 19:31:38

【C++ 第二阶段:】智能指针与现代 RAII

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C++ 第二阶段:】智能指针与现代 RAII

C++ 第二阶段:智能指针与现代 RAII

前言

第一阶段我们手动管理过:

FILE* → fopen / fclose char* → new[] / delete[]

第二阶段的目标不是“把裸指针换成智能指针”,而是建立清晰的资源所有权模型

谁拥有资源? 谁负责释放? 什么时候释放? 能不能共享? 能不能转移?

本阶段重点:

RAII unique_ptr / make_unique get / release / reset shared_ptr / 控制块 weak_ptr / 循环引用 自定义删除器 异常安全 Rule of Five / Rule of Zero

一、RAII

RAII 的核心:

把资源生命周期绑定到对象生命周期。

voidTest(){FileSessionsession("log.txt",1024);// 正常退出、提前 return、异常退出// 都不依赖手动 Close()}

只要session已成功构造,离开作用域时就会析构,进而释放资源。

RAII 不只管理内存,还适用于:

FILE* Windows HANDLE HKEY SOCKET 互斥锁 数据库连接 第三方库句柄

二、std::unique_ptr:独占所有权

autop=std::make_unique<Data>();

含义:

同一时刻只有一个 owner 负责资源生命周期。

unique_ptr

不能拷贝 可以移动 析构自动释放资源
autop1=std::make_unique<Data>();autop2=std::move(p1);

移动后:

p1 → 空 p2 → Data

注意:

std::move()本身不移动资源,真正转移所有权的是unique_ptr的移动构造或移动赋值。


2.1make_unique

推荐:

autop=std::make_unique<Data>(100);

而不是:

std::unique_ptr<Data>p(newData(100));

优点:

更简洁 减少裸 new 暴露 创建后立即进入 RAII 管理

版本注意:

unique_ptr → C++11 make_unique → C++14

所以 VS2008 / C++03 老项目不能直接使用。


2.2 管理数组

autobuffer=std::make_unique<char[]>(1024);

对应:

new[] / delete[]

可以下标访问:

buffer[0]='A';

unique_ptr<T[]>不记录数组长度,因此通常还要单独保存:

std::size_t bufferSize;

三、unique_ptr 的 get / release / reset

get()

int*raw=p.get();

作用:

取得内部裸指针,但不转移所有权

p → 资源(owner) raw → 同一资源(观察)

不能:

deleteraw;

否则p后续析构时会再次释放。

常用于:

LegacyApi(p.get());

release()

int*raw=p.release();

作用:

返回裸指针,并让unique_ptr放弃所有权,但不会释放资源

结果:

p → 空 raw → 原资源

后续必须由其他代码接管,否则泄漏。


reset()

p.reset();

作用:

释放当前资源,并让p为空。

也可以:

p.reset(newint(20));

表示:

释放旧资源 ↓ 接管新资源

一句话:

get() → 看,不拿走 release() → 拿走,不释放 reset() → 释放当前资源 / 换资源

四、std::shared_ptr:共享所有权

autop1=std::make_shared<Data>();autop2=p1;

关系:

p1 ─┐ ├──→ Data p2 ─┘

多个shared_ptr可以共同拥有一个对象。

最后一个强引用消失时,对象销毁。


五、shared_ptr 控制块

shared_ptr通常关联一个控制块:

shared_ptr │ ├──→ 被管理对象 │ └──→ 控制块 ├── 强引用计数 ├── 弱引用计数 ├── 删除器 └── 其他管理信息

例如:

autop1=std::make_shared<Data>();// strong = 1autop2=p1;// strong = 2

当:

strong → 0

被管理对象销毁。

如果仍有weak_ptr,控制块可能继续存在。

所以:

对象生命周期和控制块生命周期不是一回事。


5.1use_count()

p1.use_count();

可以观察强引用计数。

但不建议用它作为复杂业务逻辑判断条件,尤其在并发环境中。


六、make_shared

推荐:

autop=std::make_shared<Data>(100);

而不是:

std::shared_ptr<Data>p(newData(100));

优点:

代码更简洁 异常安全更自然 对象和控制块通常可以一次分配

注意:

“通常一次分配”属于主流实现方式,不要说成 C++ 标准规定绝对必须如此。


七、shared_ptr 的严重陷阱:两个控制块

错误:

int*raw=newint(10);std::shared_ptr<int>p1(raw);std::shared_ptr<int>p2(raw);

可能形成:

p1 → 控制块 A ─┐ ├──→ 同一个 int p2 → 控制块 B ─┘

两个控制块都认为自己拥有同一个资源。

最终可能:

p1 析构 → delete p2 析构 → 再 delete

导致:

Double Free / UB

正确:

autop1=std::make_shared<int>(10);autop2=p1;

这样两个shared_ptr共享同一个控制块。


八、std::weak_ptr:观察,不拥有

autop=std::make_shared<Data>();std::weak_ptr<Data>weak=p;

特点:

不增加强引用计数 不延长对象生命周期 不能直接解引用

8.1lock()

if(autop2=weak.lock()){p2->Run();}

如果对象还活着:

lock() → 得到有效 shared_ptr → 强引用计数临时 +1

如果对象已销毁:

lock() → 得到空 shared_ptr

lock()的价值不仅是检查:

它还能保证拿到的shared_ptr存在期间,对象不会被销毁。


8.2expired()

weak.expired();

可以判断对象是否已经失效。

真正访问对象时,通常优先:

lock()

而不是:

先 expired() 再访问

因为两步之间对象状态可能发生变化。


九、循环引用

classB;classA{public:std::shared_ptr<B>b;};classB{public:std::shared_ptr<A>a;};

如果:

a->b=b;b->a=a;

形成:

A ──shared_ptr──→ B ↑ │ └──shared_ptr────┘

即使外部shared_ptr都消失:

A 仍持有 B B 仍持有 A

强引用计数无法归零,导致对象无法析构。


9.1 用 weak_ptr 打破环

例如:

classB{public:std::weak_ptr<A>a;};

变成:

A ──shared_ptr──→ B ↑ └────weak_ptr────

此时:

A 拥有 B B 只观察 A

核心不是机械记“其中一边 weak_ptr”,而是先判断:

谁真正拥有谁? 谁只是回调 / 观察 / 父子关联?

十、shared_ptr 的线程安全边界

面试高频:

shared_ptr的引用计数管理具有线程安全保证,但被管理对象本身不会因此自动线程安全。

例如:

autop=std::make_shared<std::vector<int>>();

多个线程分别持有自己的shared_ptr副本,引用计数管理没问题。

但多个线程同时:

p->push_back(...);

仍然可能产生数据竞争,需要:

mutex 其他同步机制

所以:

shared_ptr 线程安全 ≠ T 对象线程安全

十一、自定义删除器

智能指针不只能管理:

new / delete

例如文件:

structFileCloser{voidoperator()(std::FILE*file)constnoexcept{if(file!=nullptr){std::fclose(file);}}};

定义:

usingFilePtr=std::unique_ptr<std::FILE,FileCloser>;

创建:

FilePtrfile(std::fopen("log.txt","a"));

离开作用域:

FilePtr 析构 ↓ FileCloser ↓ fclose

Windows 客户端同样可以封装:

HANDLE → CloseHandle HKEY → RegCloseKey SOCKET → closesocket

十二、RAII 与异常安全

裸资源:

voidFunc(){char*buffer=newchar[1024];DoSomething();// 抛异常delete[]buffer;}

如果DoSomething()抛异常:

delete[] 没执行 → 泄漏

使用:

autobuffer=std::make_unique<char[]>(1024);

异常离开作用域:

栈展开 ↓ buffer 析构 ↓ 资源自动释放

所以 RAII 的核心价值之一是:

让异常路径也能依赖对象析构完成资源清理,而不是依赖手写 cleanup。


12.1 构造函数中途抛异常

如果外层对象构造函数中途抛异常:

外层对象没有构造成功 → 外层析构函数不会执行

但:

已经成功构造完成的成员对象会正常析构。

因此成员使用:

unique_ptr string vector 其他 RAII 类型

比直接持有裸资源更安全。


十三、Rule of Three / Rule of Five

如果类自己直接管理资源,需要认真考虑:

析构函数 拷贝构造 拷贝赋值

这是:

Rule of Three

C++11 后再加:

移动构造 移动赋值

形成:

Rule of Five

重点不是背“五个函数”,而是:

一旦类直接承担资源所有权,就必须明确复制、移动和销毁语义。


十四、Rule of Zero

现代 C++ 更推荐:

把资源交给已经正确实现 RAII 的成员,让外层类不再手写资源管理。

例如:

classSession{private:std::string m_name;std::vector<char>m_buffer;};

这些成员已经正确处理:

析构 拷贝 移动

外层类往往不需要自己写特殊成员函数。

所以:

能 Rule of Zero 就优先 Rule of Zero

十五、FileSession:先改造 buffer

原来:

char*m_buffer=nullptr;

改成:

std::unique_ptr<char[]>m_buffer;

创建:

m_buffer=std::make_unique<char[]>(bufferSize);

之后不再需要:

delete[]m_buffer;

移动:

m_buffer=std::move(other.m_buffer);

unique_ptr自动完成:

释放目标旧缓冲区 接管源缓冲区 让源 unique_ptr 变空

十六、FileSession:文件也交给 RAII

定义:

structFileCloser{voidoperator()(std::FILE*file)constnoexcept{if(file!=nullptr){std::fclose(file);}}};usingFilePtr=std::unique_ptr<std::FILE,FileCloser>;

最终成员:

classFileSession{public:FileSession(conststd::string&filename,std::size_t bufferSize):m_file(std::fopen(filename.c_str(),"a")),m_buffer(std::make_unique<char[]>(bufferSize)),m_bufferSize(bufferSize){if(bufferSize==0){throwstd::invalid_argument("缓存大小不能为 0");}if(!m_file){throwstd::runtime_error("打开文件失败");}}voidLogWrite(conststd::string&text){if(!m_file){throwstd::runtime_error("文件不可用");}if(std::fprintf(m_file.get(),"%s\n",text.c_str())<0){throwstd::runtime_error("写入失败");}}private:FilePtr m_file;std::unique_ptr<char[]>m_buffer;std::size_t m_bufferSize=0;};

现在:

m_file → 自动 fclose m_buffer → 自动 delete[]

不再需要:

手写析构释放 手写异常 cleanup 手动清空移动后的裸指针

这就是现代 RAII + Rule of Zero 的方向。


十七、智能指针如何选择

能直接创建普通对象 → 直接创建 需要动态生命周期且只有一个 owner → unique_ptr 确实有多个 owner → shared_ptr 只观察 shared_ptr 管理的对象 → weak_ptr

不要为了“安全”把所有指针都改成shared_ptr

因为它会增加:

控制块 引用计数成本 生命周期复杂度 循环引用风险

默认优先:

unique_ptr

十八、第二阶段高频面试回答

1. unique_ptr 和 shared_ptr 的区别?

unique_ptr表示独占所有权,不能拷贝,只能移动;shared_ptr表示共享所有权,通过控制块中的强引用计数决定对象生命周期。工程里默认优先unique_ptr,只有确实需要多个 owner 时才使用shared_ptr

2. weak_ptr 解决什么问题?

weak_ptr是非拥有型观察者,不增加强引用计数,主要用于观察shared_ptr管理的对象以及打破循环引用。使用前通常通过lock()获取临时shared_ptr,保证访问期间对象仍然存活。

3. get / release / reset 区别?

get()只取得裸指针,不改变所有权;release()返回裸指针并让unique_ptr放弃所有权,但不释放资源;reset()会释放当前资源,并可以让智能指针为空或接管新资源。

4. shared_ptr 控制块是什么?

控制块通常保存强引用计数、弱引用计数、删除器等管理信息。多个共享同一所有权的shared_ptr会关联同一个控制块。最后一个强引用消失时对象销毁,而如果还有weak_ptr,控制块可能继续存在。

5. 为什么会循环引用?

两个对象如果通过shared_ptr相互持有,会互相增加强引用计数,即使外部引用消失,强引用仍不能归零,所以对象无法析构。通常把非拥有的一侧改成weak_ptr

6. shared_ptr 是线程安全的吗?

控制块中的引用计数管理具有线程安全保证,但被管理对象本身不会因为使用shared_ptr就自动线程安全。多个线程并发修改对象仍需要自己的同步机制。

7. Rule of Five 和 Rule of Zero?

Rule of Five 是资源管理类需要认真处理析构、拷贝构造、拷贝赋值、移动构造和移动赋值。现代 C++ 更推荐 Rule of Zero:把资源交给unique_ptrvectorstring等 RAII 成员,让成员和编译器自然提供正确的生命周期行为。


十九、第二阶段验收清单

如果下面问题可以稳定回答,第二阶段就可以结束:

1. RAII 真正解决什么问题? 2. unique_ptr 为什么不能拷贝但可以移动? 3. get / release / reset 区别? 4. shared_ptr 如何知道什么时候销毁对象? 5. 什么是控制块? 6. 强引用归零后,为什么控制块可能还存在? 7. weak_ptr 为什么不延长生命周期? 8. lock() 为什么比单独 expired() 更适合访问对象? 9. 什么是 shared_ptr 循环引用? 10. 为什么一个裸指针不能分别构造两个独立 shared_ptr? 11. shared_ptr 的线程安全边界是什么? 12. 自定义删除器能解决什么问题? 13. 为什么 RAII 对异常安全重要? 14. Rule of Five 和 Rule of Zero 的区别? 15. 为什么现代 C++ 默认优先 unique_ptr?

总结

第二阶段真正要建立的是:

普通对象 → 生命周期最简单,优先使用 unique_ptr → 一个 owner shared_ptr → 多个共同 owner weak_ptr → 观察,不拥有

现代 C++ 的资源管理目标:

RAII + 清晰所有权 + Rule of Zero

而不是:

new / delete 到处出现 手写 cleanup 手写复杂资源释放逻辑
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 19:27:20

应用(客户端)开发、框架开发、驱动/系统开发的关系

最近找工作&#xff0c;好多猎头给我推荐的岗位跟我几乎完全不匹配&#xff0c;有鉴于此&#xff0c;特作此文&#xff0c;以供诸猎头参考。编程语言&#xff1a;打个比方&#xff0c;我想写一本书&#xff0c;可以用中文&#xff0c;也可以用英文&#xff0c;还可以用俄文&…

作者头像 李华
网站建设 2026/9/8 19:24:41

opencode:终端里的AI编程代理,从安装到实战全解析

1. 先把opencode放在正确的位置上&#xff1a;它到底是个什么东西 1.1 从终端里长出来的AI编程代理 第一次听说opencode的时候&#xff0c;我其实有点麻木了&#xff0c;毕竟这两年AI编程工具出来一个接一个&#xff0c;从最早的GitHub Copilot到后来的Cursor&#xff0c;再到…

作者头像 李华
网站建设 2026/9/8 19:23:43

硬件工程师如何一次性做好设计?从原理图到量产的避坑指南

1. 为什么硬件总在返工&#xff1a;根源不在调试能力&#xff0c;而在设计习惯先说一个我印象很深的例子。前年团队里来个新人&#xff0c;做一块电源板&#xff0c;原理图改了四版&#xff0c;PCB投板两次&#xff0c;样板焊完上电发现纹波超标&#xff0c;又来回调了好几天。…

作者头像 李华
网站建设 2026/9/8 19:21:50

从被动应答到主动执行:Clawdbot智能体如何落地流程自动化

Clawdbot这名字第一次看到的时候&#xff0c;我愣了一下。单看这个拼法&#xff0c;它既不是传统意义上的聊天机器人&#xff0c;也不是那种只能按固定脚本走的RPA机器人。“Claw”本身是爪子&#xff0c;这意味着它有抓取、拾取、操作真实对象的能力&#xff0c;而“bot”又说…

作者头像 李华