<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Core&apos;s ink</title><description>当打之年</description><link>https://coooredump.github.io</link><item><title>广东·潮州</title><link>https://coooredump.github.io/blog/tourism/chaozhou_2026-04-03</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/chaozhou_2026-04-03</guid><description>流放岭南</description><pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;连夜逃离厦门，流放岭南&lt;/p&gt;
&lt;p&gt;那天真的下暴雨了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260404-IY9HMC.png&quot; alt=&quot;image-20260404222018244&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;岭南太苦了，我希望你不懂&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260404-H4SlYb.JPG&quot; alt=&quot;IMG_9406&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;特种兵半日之旅&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260404-MFWtdd.JPG&quot; alt=&quot;IMG_9407&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;走过潮州古城&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260404-YYwoSw.JPG&quot; alt=&quot;IMG_9408&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/20260404-u69ABx.HSjLKWWo.jpg"/><enclosure url="/_astro/20260404-u69ABx.HSjLKWWo.jpg"/></item><item><title>2026.03.23</title><link>https://coooredump.github.io/blog/journal/2026-03-23</link><guid isPermaLink="true">https://coooredump.github.io/blog/journal/2026-03-23</guid><description>多年之后，我又梦到那天</description><pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;去年的时候，我想 26 年只要干完俩件事就行：第一是能顺利毕业，第二是试用期通过。但前年，或者大前年的时候，我还是规划了很多事情的...&lt;/p&gt;
&lt;p&gt;但是走到这个节点的时候，已全然想不出来下一年有什么好做的事情了&lt;/p&gt;
&lt;p&gt;我有在继续往前走，只是这些年走得会慢一些&lt;/p&gt;
&lt;p&gt;你要说现在这个时代很烂没红利也不绝对，比如晚买了五年房，相当于少奋斗十年&lt;/p&gt;
&lt;p&gt;在这里，35 岁以后，读书、跳槽、结婚等等，干什么都是太老了，只有突然离世，大家才会说，这么年轻就走了&lt;/p&gt;
&lt;p&gt;上班让我感到最可怕的地方是，它居然让我因为期盼退休而期待衰老，让我完全不珍惜我剩余人生里，最年轻的每一天&lt;/p&gt;
&lt;p&gt;其实我根本就没啥追求&lt;/p&gt;
&lt;p&gt;早些年，梦到和一些很好的朋友在海边散步，四五点钟的样子，有点夕阳又不是很暗，海边就在我家的门口，我感觉这样就很好，比挣很多钱都要好&lt;/p&gt;
&lt;p&gt;我对于日后买一辆十万的车还是二十万、三十万、五十万的车没有任何兴趣，倒是时常会幻想，要是本科的时候有一辆破面包车该多爽，最好能拉六个人那种，把我们一宿舍人都拉上，三轮车也行&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260323-DgCA5G.png&quot; alt=&quot;image-20260323152212099&quot;&gt;&lt;/p&gt;
&lt;p&gt;人是逃离不了焦虑的，特别如果是一个喜欢攀比，或者争强好胜的人，所以我们这行大家都很焦虑，这太正常了，因为大家都是卷上来的，这让我也想起来了刚入学厦大的时候，备战老师说你们居然都没挂过科，我当时就很想吐槽，我要是挂过科，你能在这里见到我吗&lt;/p&gt;
&lt;p&gt;实习的时候总在想要刷多少段大厂实习，是不是算法 &gt; 后端 &gt; 前端 &gt; 测试，秋招的时候想自己拿了多少 ssp，身边的朋友又拿了多少 ssp&lt;/p&gt;
&lt;p&gt;回顾我的本科，保研，读研，实习，秋招，至今，其实并没有做出太多有意义的事情，没有人会在很多年后翻开一个陌生人的动态，去看他当年拿了多少 offer，开了多少钱，这是一件很无聊的事情，我也不相信会有人在很多年后，依旧吹嘘自己当年的成就&lt;/p&gt;
&lt;p&gt;所以比起其他事，我更满意的是在本科的时候写了一些推文，帮到了一些学弟学妹，在读研的时候写了一些秋招指南，同样公开了出来，也比较满意这些年不断记录着，虽然这段时间里，更新速度大不如前&lt;/p&gt;
&lt;p&gt;其实能做完上面的事情，全都是偶然或者巧合，我最初只想写一篇我的故事，但一直没想好怎么写，所以在这过程中，杂七杂八的写了很多别的&lt;/p&gt;
&lt;p&gt;暮然回首，突然发现我的青春有些太过安静，以至于等到它结束我都没有反应过来&lt;/p&gt;
&lt;p&gt;我当年，没有不良习惯，没怎么逃课，也没有多么优秀的成绩，更没有什么可以拿的出手的故事，没有出众的才艺，没有开朗的性格，没有偷偷藏起的忧愁，没有肆意而为的潇洒&lt;/p&gt;
&lt;p&gt;干净得像一张白纸，过完了整个青春，我的高中，我的本科&lt;/p&gt;
&lt;p&gt;为什么不说研究生呢，因为研究生我就经常逃课了哈哈，其实不是，因为研究生不是在外地实习，就是在工位打杂，就谈不上青春什么的了&lt;/p&gt;
&lt;p&gt;如果要形容去年的话，应该是改变，我和以前有些不一样了，但也有不变的地方&lt;/p&gt;
&lt;p&gt;当然，对于上面，我并不打算细究些什么。人生在世，不断地被各种人亏欠，又不断的亏欠着别人，不断被各种人伤害着，又承受了其他人的良善与馈赠&lt;/p&gt;
&lt;p&gt;究竟谁欠谁，谁该感激谁呢？可能早已说不清&lt;/p&gt;
&lt;p&gt;一生很长，很多人不会永远在我的生活里，但会永远在我的故事里&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>远程服务器启用 Codex 插件</title><link>https://coooredump.github.io/blog/productivity-tool/codex</link><guid isPermaLink="true">https://coooredump.github.io/blog/productivity-tool/codex</guid><description>把本地的 Codex 登录信息复制到服务器，并让服务器的 Codex 流量通过 SSH 转发到本地代理，从而实现服务器远程环境下的 Codex 正常可用。</description><pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;⚠️ 提示：请严格遵守公司或组织的数据安全红线&lt;/p&gt;
&lt;p&gt;在进行下述操作前，请务必确认您的单位 允许 使用 SSH 隧道、端口转发以及代理转发等技术。 某些公司/机构明确禁止以下行为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSH 端口转发（LocalForward / RemoteForward）&lt;/li&gt;
&lt;li&gt;利用远程服务器通过本地代理访问互联网&lt;/li&gt;
&lt;li&gt;传输敏感代码、令牌的重要数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如违反安全规范，后果自负，请谨慎操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;：把本地的 Codex 登录信息复制到服务器，并让服务器的 Codex 流量通过 SSH 转发到本地代理，从而实现服务器远程环境下的 Codex 正常可用。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;为了在远程服务器（如 GPU 服务器）中正常使用 IDE（VSCode / Cursor）中的 &lt;strong&gt;Codex 插件&lt;/strong&gt;，需要将本机的 Codex 登录信息同步到服务器，并配置代理转发，使远程服务器能够通过本地代理访问 Codex 服务。以下是完整的配置步骤：&lt;/p&gt;
&lt;h2&gt;1. 上传本地 Codex 授权信息到服务器&lt;/h2&gt;
&lt;p&gt;当你在本地 IDE 成功登录 Codex 插件后，插件会在你的用户目录下生成目录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;~/.codex

$ ls -alh ~/.codex/
total 392
drwxr-xr-x  16 coooredump  staff   512B  2 26 01:42 .
drwxr-x---+ 87 coooredump  staff   2.7K  2 26 02:36 ..
-rw-r--r--@  1 coooredump  staff   2.7K  2 26 01:39 .codex-global-state.json
-rw-r--r--   1 coooredump  staff     3B  2  4 03:01 .personality_migration
-rw-------@  1 coooredump  staff   4.3K  2 26 01:37 auth.json
-rw-------   1 coooredump  staff   135B  2 26 01:42 config.toml
-rw-------@  1 coooredump  staff    85B  9 14 09:52 history.jsonl
drwxr-xr-x@  3 coooredump  staff    96B  9 14 09:52 log
-rw-r--r--@  1 coooredump  staff   165K  2 26 02:11 models_cache.json
drwxr-xr-x   4 coooredump  staff   128B  1  2 17:28 sessions
drwxr-xr-x   4 coooredump  staff   128B  2 26 01:42 shell_snapshots
drwxr-xr-x   3 coooredump  staff    96B  2 26 01:41 skills
drwxr-xr-x@  3 coooredump  staff    96B  2  6 22:54 sqlite
drwxr-xr-x   4 coooredump  staff   128B  2  6 22:54 tmp
drwxr-xr-x@  2 coooredump  staff    64B  2 26 01:37 vendor_imports
-rw-r--r--@  1 coooredump  staff    76B  9 14 09:52 version.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了在远程服务器启用 Codex，需要将这个目录复制到服务器上。&lt;/p&gt;
&lt;p&gt;1️⃣ 在本地打包 &lt;code&gt;~/.codex&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tar -cf codex.tar ~/.codex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 上传到你服务器的用户目录（&lt;code&gt;~/&lt;/code&gt;），然后在服务器上解压：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tar -xf codex.tar -C ~/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后确认目录 &lt;code&gt;~/.codex&lt;/code&gt; 是否存在即可。&lt;/p&gt;
&lt;h2&gt;2. 配置 IDE 桥接本地代理（SSH 端口转发）&lt;/h2&gt;
&lt;p&gt;由于远程服务器通常不能直接访问 Codex 服务，所以需要通过 &lt;strong&gt;本地代理（🪜）&lt;/strong&gt; 转发请求，让服务器通过 SSH 隧道访问本地代理。&lt;/p&gt;
&lt;h3&gt;2.1 在 IDE SSH 中配置端口转发&lt;/h3&gt;
&lt;p&gt;先确认本地🪜的端口（此处以 7890 为例）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260226-olDLnC.png&quot; alt=&quot;image-20260226024009236&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后在 SSH 配置（VSCode / Cursor 的 SSH Host）中，加入端口转发：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://linux.do/uploads/default/optimized/4X/c/8/3/c83f8ada1e8f87f57bfae33c68218a5f7ccc7f8c_2_254x250.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;即修改 &lt;code&gt;/Users/xxx/.ssh/config&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host 8001
    HostName xxx
    Port xxx
    User xxx
    # codex 端口转发
    RemoteForward 7890 localhost:7890
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 确保本地代理允许局域网访问&lt;/h3&gt;
&lt;p&gt;打开🪜设置，勾选：「允许来自局域网的连接」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260226-qTE32W.png&quot; alt=&quot;image-20260226024359338&quot;&gt;&lt;/p&gt;
&lt;p&gt;修改完成后，再通过 IDE 远程登录服务器。&lt;/p&gt;
&lt;h2&gt;3. 服务器代理配置&lt;/h2&gt;
&lt;p&gt;在远程服务器上，让所有 HTTP/HTTPS 请求都走刚才通过 SSH 映射的代理。&lt;/p&gt;
&lt;h3&gt;3.1 服务器代理设置&lt;/h3&gt;
&lt;p&gt;在服务器的 &lt;code&gt;~/.bashrc&lt;/code&gt; 添加以下内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export http_proxy=http://127.0.0.1:7890
export https_proxy=http://127.0.0.1:7890
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 在 IDE 中设置远程工作空间的代理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;注意，此处设置需要在远程服务器的工作空间设置，而非本地工作空间设置。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;IDE 中按下&lt;code&gt;Cmd/Ctrl + Shift + P&lt;/code&gt;，输入&lt;code&gt;Open Remote Settings&lt;/code&gt;，进入远程服务器的设置面板，搜索 &quot;proxy&quot;，将： &lt;code&gt;http-proxy&lt;/code&gt; 设置为 &lt;code&gt;http://127.0.0.1:7890&lt;/code&gt;，如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260226-oqzynq.png&quot; alt=&quot;image-20260226024539862&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后关闭并重新连接 Remote：&lt;code&gt;Cmd/Ctrl + Shift + P → Close Remote Connection&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;4. 安装并启动 Codex 插件&lt;/h2&gt;
&lt;p&gt;重新连接服务器后，在插件市场安装 Codex（如果未安装），打开 Codex 插件，此时应该能显示正常的登录界面，并且运行命令不会出现反复 &lt;code&gt;Reconnect&lt;/code&gt; 的错误。&lt;/p&gt;</content:encoded><h:img src="/_astro/20260226-hX9ebM.MLa2yTmC.jpg"/><enclosure url="/_astro/20260226-hX9ebM.MLa2yTmC.jpg"/></item><item><title>「2025 年终总结」在所有失去的人中，我最怀念我自己</title><link>https://coooredump.github.io/blog/yearly-review/2025-of-all-the-people-i-have-lost-i-miss-myself-the-most</link><guid isPermaLink="true">https://coooredump.github.io/blog/yearly-review/2025-of-all-the-people-i-have-lost-i-miss-myself-the-most</guid><description>那时西边红霞满天，我却只顾东行</description><pubDate>Sat, 07 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 2026 的开始，我并没有许下什么新年愿望，似乎每一年都没有，但每一年都会经历很多难忘的事情。&lt;/p&gt;
&lt;p&gt;这一年终究要过去了，我对 2025 的唯一感觉就是：&lt;strong&gt;年费会员快要到期了，该交钱了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;今年比往年要好很多，烟花凌空绽放，整座城市隆隆作响，极尽所能地宣告新年的到来，往年只有电视机里主持人带着职业假笑，高声呐喊这的倒计时，现在打开窗户，迎面扑来的是久违的焰火气息，让我想起遥远的过去。&lt;/p&gt;
&lt;p&gt;尤记得初春的厦门，微雨悄然而至，零星的雨滴中，傍晚树下自行车的铃声随风轻轻荡漾，街角的灯光微黄，宛如一位老友向我低语，有些东西，永远的遗留在了过去。可能是在留在了初高中，或者本科，亦或者是研究生，无从知晓。&lt;/p&gt;
&lt;p&gt;那时西边红霞满天，我却只顾东行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-FEUsos.png&quot; alt=&quot;7623766c8e19a6e0cbd1497c97a4b53e&quot;&gt;&lt;/p&gt;
&lt;h2&gt;#01 夏&lt;/h2&gt;
&lt;p&gt;暑期实习挺不顺利的，人很丧，没自信，没目标，也错过了很多机会...&lt;/p&gt;
&lt;p&gt;想回到暑期前重新开始，但没办法，回不去了，这就是人生，只有错过点什么，你才学得会珍惜&lt;/p&gt;
&lt;p&gt;就好像我这辈子也回不到大一大二的学习状态一样，或者说，人这一生，都不能走回头路&lt;/p&gt;
&lt;p&gt;很多年来，我一直持续在问自己一个问题，怎么就变成这样了呢？&lt;/p&gt;
&lt;p&gt;最近，这个问题变成了，害，那又能有什么办法呢 😮‍💨&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;屡战屡败，屡败屡战，找暑期实习哪有不疯的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-7Vu4hg.png&quot; alt=&quot;image-20260206142207815&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;暑期实习面经复盘&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-XHxwkZ.png&quot; alt=&quot;image-20260206143602814&quot;&gt;&lt;/p&gt;
&lt;p&gt;除了保研那段时间，这应该算是我第一次真正意义上高强度投递简历、参加面试了，这一年从暑期实习到秋招结束这段时间，基本都是在自我怀疑中度过的，愈发觉得自己不适合互联网。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;人生失意，莫过于此&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-WbMcST.jpg&quot; alt=&quot;IMG_8159&quot;&gt;&lt;/p&gt;
&lt;p&gt;暑期最有希望也是最想去的华子也泡死在池子里了，那个组的方向我是真感兴趣，可惜最后还是没能等来 offer。&lt;/p&gt;
&lt;p&gt;看到一个北大的哥们，比我晚入池，过几天就开奖了，HR 和我说，或许这就是北大的魅力吧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-HIgLPT.png&quot; alt=&quot;image-20250628032513786&quot;&gt;&lt;/p&gt;
&lt;p&gt;也不知道为什么，这些年，身边的人总会有种觉得我很厉害的错觉，拜托，我要是真厉害，我还用发愁这些毕业、就业的事么，这样我就不用吃生活的苦了&lt;/p&gt;
&lt;p&gt;有时候就觉得自己活得很荒谬，三天两头垂头丧气跟失恋似的&lt;/p&gt;
&lt;p&gt;朋友问我怎么情绪不对，我总不能告诉他最近暑期挂麻了，看不到未来吧&lt;/p&gt;
&lt;p&gt;我也渐渐萌发退缩的心理，想到国企或者体制内躺平（其实我也考不上），这样度过一生好像也蛮不错的。只不过没想到今年秋招的国企，反倒是历年来最黑暗的一年，往年触手可及的国家电网也因为高层换人重置考核方式，厦门所在的国企进面 baseline 骤增，亦或是 HC 骤减，亦或是停招一年。&lt;/p&gt;
&lt;p&gt;突然想明白了为什么互联网公司互相称呼同学，国企互相称呼老师，&lt;strong&gt;因为同学会毕业，早晚而已&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-1fA4KY.JPG&quot; alt=&quot;94628a820e12dc46f30efd54d788a640&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这并不是一份多么值得让人羡慕和追逐的工作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在这里，人也并没有太多的时间想以前和以后&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;甚至于为了安慰自己，还认真写过一篇博客来研究分析近几年的国企和私企。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-ldxqki.png&quot; alt=&quot;image-20260206145543603&quot;&gt;&lt;/p&gt;
&lt;p&gt;那段颓废在宿舍刷视频的时间，经常刷到蜡笔小新和海绵宝宝，不得不说在那个年代，做得是真好&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小学的时候是最应该看蜡笔小新的时候&lt;/li&gt;
&lt;li&gt;高中的时候是最应该看斗破苍穹的时候&lt;/li&gt;
&lt;li&gt;但我研究生的时候才开始沉迷这俩玩意&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过仔细一想，我现在和高中时候确实有点不一样了，比如高中时候我玩英雄联盟宁死不投，现在我看打不过了 15 分钟就上票了，不能让对面虐菜爽&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;能打过我的人，我不给他们打&lt;/strong&gt; ...&lt;/p&gt;
&lt;p&gt;人总应该是有点变化的，但我最近又有些新的理解了，成长是一件很难的事，它甚至意味着从头去修改你的一部分人生意义&lt;/p&gt;
&lt;p&gt;打个比方：你连体重都控制不了，怎么能控制自己的人生呢？&lt;/p&gt;
&lt;p&gt;朋友说没办法，现在大伙都找着了工作，体重确实不好减&lt;/p&gt;
&lt;p&gt;真不好减，最近东子、团子和阿里打外卖价格战，快给我喝出糖尿病了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-oHxYtW.jpeg&quot; alt=&quot;Screenshot 2025-06-28 at 03.48.46&quot;&gt;&lt;/p&gt;
&lt;p&gt;暑期有个好朋友跑北京实习了，还有一个去了杭州，在厦门读书时哥几个总是经常打哈哈，不过一到上班，感受又不一样了&lt;/p&gt;
&lt;p&gt;出发前几天，我说了一句“今日一别，小半年又见不到了，不过咱们的学生生涯好像也就一年多了”，他们没说话，也许是我们经历这寻常的分别太多了，本来早就习惯了，这时候突然来了个人对你说：&lt;/p&gt;
&lt;p&gt;嘿，你已经和这么多人走散了呀&lt;/p&gt;
&lt;p&gt;大多数人分开后就不会再见了，而有些人或许还有再见的机会&lt;/p&gt;
&lt;p&gt;回忆会陪你一生，即使再模糊，你还是忘不掉&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;聚散离合，此乃常态，你要习惯&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;纵观个人的命运，当然要靠自我奋斗，但也要考虑到历史的行程，但不幸的是，现在历史的进程并不乐观。&lt;/p&gt;
&lt;p&gt;在比较小的时候，大约就是初中高中吧，我对自己人生结果充满幻想和期待。但慢慢长大之后，方才意识到生活是如此复杂，我不得不学着对内心的期望放手，曾经的梦想在残酷的现实面前变得褪色。慢慢发现有些事并不是一件需要被解决的事情，而是一件需要被接受的事情，或者说，绝大多数的事情，都只是需要接受的事情，你解决不了。&lt;/p&gt;
&lt;p&gt;比如在地球 online 度过的这二十几年来看，改变的话起码要拿年来衡量，或者十年来衡量。&lt;/p&gt;
&lt;p&gt;此时，怜悯自己显得很重要，或许你可以对自己说“我也想要做得更好”以及“就让它这样吧，其实这样也好”。&lt;/p&gt;
&lt;p&gt;是的，我已经做得足够好了，因为至少现在我还爱着自己｡&lt;/p&gt;
&lt;h2&gt;#02 秋&lt;/h2&gt;
&lt;p&gt;其实，我从来没想过要如何准备才能在秋招拿到大厂的 offer，或是白菜，或是 SP、SSP&lt;/p&gt;
&lt;p&gt;我一直以来去实习，一是想顺道去别的城市看一看，二来，也是一直都想真正的写出一些好的代码，被重视的代码，听起来很理想化，但这也让我阴差阳错的实习了不少段&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在我过往的十年中，凡是我努力追逐的事，往往都无法实现，而那些真正实现的，其实都是意料之外&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以我觉得如果一件事有 100% 的把握，就没有去做的兴致了&lt;/p&gt;
&lt;p&gt;🧑‍💻面试官：这就是你笔试只做出来一半的原因吗？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;后来看到个段子，很后悔没给面试官发：&lt;/p&gt;
&lt;p&gt;“这代码我有三不写”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一：不能复制的我不写，因为别人的轮子写好了我干嘛不用&lt;/li&gt;
&lt;li&gt;第二：不让我用 ChatGPT &quot;辅助&quot;的我不写，因为基础的东西我都会，写了没意义&lt;/li&gt;
&lt;li&gt;第三：太难写的我不写，因为我还没有到那个境界，写不明白&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;现实就是这么残酷，还没来得及从暑期实习的失落中走出来，秋招就开始了，比 emo 更难受的事情是，不能 emo 太久。&lt;/p&gt;
&lt;p&gt;朋友说，当一个人过得不好时，才会怀念过去。难怪秋天刚开始，我就已经在怀念夏天了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这过日子就是问题叠着问题，我唯一能做的，就是迎接这些问题。  ——《士兵突击》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-9riioy.png&quot; alt=&quot;image-20260206165024045&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;也就投递了 187 次&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-9uMV5Q.png&quot; alt=&quot;image-20260206165322973&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;秋招 vlog&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-PMyBcV.png&quot; alt=&quot;image-20260206170133364&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;秋招面经复盘&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-plRWqV.png&quot; alt=&quot;image-20260206170105938&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;秋招，是一场无休止的 dating&lt;/p&gt;
&lt;p&gt;这种盛况，从 8 月持续到 11 月&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-xckZqA.png&quot; alt=&quot;image-20260206170923570&quot;&gt;&lt;/p&gt;
&lt;p&gt;几十年前，似乎呢群领图灵奖的巨佬总会在领奖的时候很自豪的说，我是一名程序员&lt;/p&gt;
&lt;p&gt;但现在提起程序员，我总会想到 CSDN 上那只秃头虚弱无力的猿猴趴在电脑前，用巨大的手敲打着键盘&lt;/p&gt;
&lt;p&gt;所以我很不喜欢 CSDN，当然 CSDN 也确实做的很烂，也算没有辜负我的感情&lt;/p&gt;
&lt;p&gt;我没那么喜欢互联网，互联网是一种很割裂的存在，特别是在国内，比如互联网的 OKR，很有意思的是，在互联网 OKR 本来就是要完不成的，如果有一天你完成了你的 OKR，不是说你超出预期，而是说明你的 OKR 制定的有问题&lt;/p&gt;
&lt;p&gt;我很想说这个行业病了，但其实我也改变不了啥，唯一改变的就是工作越来越难找了，同龄人之间要更卷了而已&lt;/p&gt;
&lt;p&gt;没有乌托邦，只有维也纳在等我们&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-HNb9tZ.PNG&quot; alt=&quot;Screenshot 2025-08-10 at 15.42.18&quot;&gt;&lt;/p&gt;
&lt;p&gt;但我喜欢编程，我就想工作得能开心点，从来没想过在互联网待一辈子，&lt;strong&gt;而且即使我想，我的老板可能也不想&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;面试很短，这几十钟其实是看不出来我的优点的，我每次面完都感觉没有发挥得尽兴，没有展示出来给面试官最好的一面&lt;/p&gt;
&lt;p&gt;但我的缺点还是一览无余的&lt;/p&gt;
&lt;p&gt;有一次面 HR 面的时候，HR 让我给一个词来形容自己，我说的是重感情&lt;/p&gt;
&lt;p&gt;HR 说第一次见到这样的回答，让我举个例子&lt;/p&gt;
&lt;p&gt;我想了想，说如果你司招我，那我会先考虑你司&lt;/p&gt;
&lt;p&gt;也不知道 HR 满不满意这个回答&lt;/p&gt;
&lt;p&gt;想起来本科的老学长，从闲鱼跳去了美团，换了个 base，最近经常加班，有人问是为了什么&lt;/p&gt;
&lt;p&gt;学长说是为了爱情，真好，那时候有那么一瞬间，我很羡慕，似乎这一切都和技术没有关系，与世俗的荣誉，权力都没有关系&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我喜欢纯粹的东西，面试也是&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我如果觉得一个面试官不真诚，我就不会去这家公司，如果我有选择的话&lt;/p&gt;
&lt;p&gt;以前觉得做选择时兴趣没有那么重要，但现在看来，那一点平日里微不足道的热爱，会成为在动摇时至关重要的那一份权重&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-p7Kg8W.png&quot; alt=&quot;image-20260206171454503&quot;&gt;&lt;/p&gt;
&lt;p&gt;有时候想毕业直接回到老家，又觉得辜负了自己的初心和一身技术&lt;/p&gt;
&lt;p&gt;朋友说不对，&lt;strong&gt;你现在没啥技术，谈不上辜负&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;说的也对，走一步看一步算了😄&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20251220-TkC1kV.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;不过程序员有一大悲哀就是，成为了程序员就会只把自己当成程序员，因为身在这个环境中，但其实如&lt;strong&gt;果跳出这个环境，发现还是正常生活的人多&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;计算机是严格理性与规范的学科，但我觉得也很浪漫，我们穷尽所有去提高技术，无论是设计模式，还是渐渐的模块化，微服务化，容器化，&lt;strong&gt;都是为了在未来一天，我们可以更好的被取代&lt;/strong&gt;，这个工作，在一开始，便注定了悲壮和浪漫。&lt;/p&gt;
&lt;p&gt;但有时我会想，这个行业，值不值得我投入如此的热忱，我是个没有梦想也能生存的人，高中的朋友总喜欢找一个梦想中的学校，一个梦想中追赶的人，一个梦想中的行业。但我这些都没有，朋友总觉得我是靠意志力撑过高三那段艰苦的岁月，其实不是，有没有梦想对我来说并不是那么重要。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20251220-hN13aN.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;朋友说，假设阳的挂掉一半，全国会失去大概 50% 的人口，不过很快又会恢复如初（生物小知识嗷：种群数量达到环境容纳量一半，即 K / 2 时，种群的增长速率最大），最终无数人的喜怒哀乐，生死离别，成为历史书上的轻如鸿毛的一笔，无论描写的多么惨绝人寰，也只是一笔罢了，而地球上所有惊天动地的大事，也不及宇宙中的一粒尘埃。&lt;/p&gt;
&lt;p&gt;有时候在想生命的意义是什么，我想不明白，就像高三老师问我的梦想一样，问我想要当一个什么职业，成为一个什么样的人，我说我不知道，但一定不是以后拿 20w 年薪，30w 年薪，50w 年薪或者 100w 年薪。&lt;/p&gt;
&lt;p&gt;因此不想卷了，是因为没有了卷的动力，其实最终不就是为了拿一份高薪的工作，我连对这个结果都没了兴致，又何谈过程，倒不是懒得奋斗，只不过什么时候奋斗渐渐的成了一种贬义词，我只是觉得途中的代价太大，不只是我一个人付出的，而是和我的朋友们一同付出的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;无论是什么样的结果，我都觉得难以配得上这一路走来的失去&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;人行走江湖，就是不断的相遇和失去，第一次相遇是自己的生命，最后一次失去也是，有始有终&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我已经不喜欢再和别人卷了，但是到了一定程度后，身在其中，由不得我，不过，也算是一种磨练心境吧，倒也未必不是好事。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-HLKEwp.PNG&quot; alt=&quot;Screenshot 2025-08-10 at 15.37.42&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以生命的意义是什么，我不知道，但是我会期待下一次和朋友相遇，在大学中遇到新的朋友，新的乒乓球友，我会和以前一样期待，但是不会再像以前一样严格规划我的人生了，比如某个时间点非做某件事不可，非要达到某个成就不可。&lt;/p&gt;
&lt;p&gt;在学校的时候，如果不是很忙我就喜欢到外面去吃个鸡锅，如果忙的话就推到明天去吃，明天忙就推到后天，&lt;strong&gt;人总是寄希望于后来，也习惯把这辈子没有得到的人和事寄予到下辈子，总会说我下辈子要怎样怎样。可如果这辈子就是你上辈子说的下辈子，那怎么办呢&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;所以我现在选择想吃鸡锅的时候就直接去吃😋&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-N1gZKq.png&quot; alt=&quot;image-20260206182133446&quot;&gt;&lt;/p&gt;
&lt;p&gt;那我会呆在哪里呢？HR 也问过我这个问题。&lt;/p&gt;
&lt;p&gt;我说大概是厦门吧，我的爱人、挚友都在那里，或者是在那里陪伴过我一段很长很长的时间，比我在杭州西安待的时间加起来还要长很多&lt;/p&gt;
&lt;p&gt;面试官笑得有些深意&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;几年后被末尾淘汰的那一天，一切都将作废，之前的加班作废，之前从池子中挣扎出来的惊喜作废，之前的领导同事作废，之前的薪资待遇也作废，鼠标也会作废，键盘也会作废，所有的承诺也统统作废。从那一天开始，我不再需要电脑，不再需要八股，我的人生不再有 hello world。我知道是秋招那年的那场大雪覆盖了我前半生的荒唐，覆盖了我本科的学历，我和 985 硕一起进入阿里，我仿佛从其中看到了希望，但生活在无边的大雪中又何尝不是一种荒唐，当我真正走出这片大雪后，我肚子有些饿了，此时我才意识到，原来码农烧烤才是人生的归属。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-wQPg0Y.JPG&quot; alt=&quot;IMG_8164&quot;&gt;&lt;/p&gt;
&lt;p&gt;其实我也经常黑厦门的，我很难喜欢上某个城市，比起城市的高低贵贱，电子博弈，我更在意的是我与谁一起经历过什么，到过哪里，见过哪里的风景&lt;/p&gt;
&lt;p&gt;我经常说厦门那地方，典型的工资低，物价高&lt;/p&gt;
&lt;p&gt;朋友说他不是本地人，每次买海鲜都感觉买的很贵很贵，听厦大的学长说去码头买能便宜很多&lt;/p&gt;
&lt;p&gt;我说不是，码头有一定概率买到新鲜的，而且很难买到便宜的&lt;/p&gt;
&lt;p&gt;为什么说有概率买到新鲜的，因为那海鲜，你不清楚是刚从海里拉上来的，还是昨天搁市场溜了一圈又拉回去的，反正在人看来，都是刚从船上拉下去，带着新鲜的海水和被折腾的半死不活的鱼&lt;/p&gt;
&lt;p&gt;至于价格，人们一般都会吹鼓自己的是野生的，众所周知的道理，野生的更贵，&lt;strong&gt;也没有手段证明它是不是真的野生鱼，除了价格&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;#03 厦&lt;/h2&gt;
&lt;p&gt;读了好多年书，如果能顺利毕业的话我就要毕业了，&lt;strong&gt;这话看起来没什么信息量，和我读的硕士一样&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;看树洞有人问保研和考研哪个更辛苦，我不用点开都知道评论区铁定开战了&lt;/p&gt;
&lt;p&gt;一个老哥说的很有意思，差点看破防了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;读研更辛苦&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-1lulgP.JPG&quot; alt=&quot;b1aa7170df5fcb1ed7046ec6c77874fe&quot;&gt;&lt;/p&gt;
&lt;p&gt;记得有一天发研究生补助的时候我还在和朋友聊天&lt;/p&gt;
&lt;p&gt;朋友说他感觉人生只剩下了银行卡里的一串串数字，收入和支出，感觉像是把灵魂卖给了社会，照镜子的时候感觉双眼都变得浑浊了&lt;/p&gt;
&lt;p&gt;我看了眼屏幕里那一堆减号里面的醒目的“➕600”，还是国家赏饭，我应该是肉身和灵魂都卖给了学校，还是那种 19 世纪美国南方黑哥摘棉花的价格&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-Bfd2PP.PNG&quot; alt=&quot;IMG_5800&quot;&gt;&lt;/p&gt;
&lt;p&gt;和朋友闲聊，如果能再选一次的话我一定不会读研，哈哈&lt;/p&gt;
&lt;p&gt;但怎么说呢，一切都是最好的安排，如果不是这样，我也不会遇到现在这位让我死心塌地爱着的女朋友&lt;/p&gt;
&lt;p&gt;人就是这样，总会在某个时期觉得自己曾经做的事情不正确，说不定再过几年我又觉得现在做的不正确了&lt;/p&gt;
&lt;p&gt;不过话说回来，当年觉得自己能保研可厉害了，现在想想就和网上那种看着小时候一面墙的奖状一样，叹息之墙&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;现在知道累了，当初喊我玩以为跟害我一样&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-RjHlZE.PNG&quot; alt=&quot;16f780fdab0ce49ce40c1b85fe9c3b80&quot;&gt;&lt;/p&gt;
&lt;p&gt;人还是不要美化从未走过的那条路，不然就会一直纠结和后悔，然后发现这辈子然后很快就过去了&lt;/p&gt;
&lt;p&gt;也慢慢觉得自己老了，因为回过头看，发现自己已经配不上过去的自己了&lt;/p&gt;
&lt;p&gt;如果在当年知道未来是这样，或许根本撑不过高考那段时光&lt;/p&gt;
&lt;p&gt;多年之后，我又梦到那天，画面遥远，仿佛回到高考那一天&lt;/p&gt;
&lt;p&gt;感觉，稍微有点生不逢时罢了&lt;/p&gt;
&lt;p&gt;想起来前段时间公司宣讲，说我们赶上了一个很好的时代，因为有 AI，可是 AI 并没有发挥它应有的作用，并没有让工作量减少，工作时长减少，也没有降低生活成本，房价物价，看病还是一样昂贵，大家的压力也都还是很大，甚至更焦虑了，我们并没有因此变得更幸福&lt;/p&gt;
&lt;p&gt;长期来讲，我很看好 AI，但此时此刻，并不觉得这是一个多么好的时代，即使见证了从传统 CV、NLP 到大模型的蜕变。大模型发展得太快了，还记得 22 年刚出 GPT-3.5 的时候，有人感慨涌现就像宇宙给的礼物一样神奇，现在已经没有人再发出这种感慨了，默认大模型就存在和人类差不多甚至更强的智力了，没有上古时期 NLP 用 BERT 胡言乱语的折磨&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260206-jmioKu.jpg&quot; alt=&quot;33d3a6e6a7db04a50aead8040afc78b1&quot;&gt;&lt;/p&gt;
&lt;p&gt;想起个很好笑的事，以前研究生方向不是 CV 就是 NLP，现在 CV 还有点，感觉 NLP 已经绝迹了，虽然大模型也算是一种 NLP 吧，但已经没有哪个老师招生的时候会给你说：同学，我的研究方向是 NLP 🤣&lt;/p&gt;
&lt;p&gt;现在已经到 AI Agent 时代了，国内外的 AI 产品层出不穷抢占市场，也不得不感叹国内 AI 大战的朴素，一代人有一代人的鸡蛋要领&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-dXrHsv.JPG&quot; alt=&quot;140c831cdcc83abd3e65bf054a050db3&quot;&gt;&lt;/p&gt;
&lt;p&gt;感觉我们的世界对慢节奏的人很不友好，那种田园牧歌的时代早就一去不复返了。我曾经对科研的憧憬完全来自于牛顿和苹果树，康德漫步于小径，梭罗隐居瓦尔登湖，而不是坐在电脑面前，脊柱侧弯、屁股很酸、眼睛散光，和计算机打交道，未来只有吃不完的苦。&lt;/p&gt;
&lt;p&gt;我曾经觉得我是会喜欢科研的，在我想出老师也没想出来的课题的时候，提出不一样的见解的时候，在组会分享论文的时候，做出耳目一新的 PPT 的时候。&lt;/p&gt;
&lt;p&gt;也许我确实是，但是我连打游戏都没法一天八小时持续一个月，社交不行，跑步不行，吉他也不行，唯一成功坚持过的看似也只有读书，读高中的时候，但也得考虑到咱们学六门课呢，而且真是差点就死了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我不喜欢科研，至少暂时不喜欢&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-G77E5P.JPG&quot; alt=&quot;IMG_3128&quot;&gt;&lt;/p&gt;
&lt;p&gt;人擅长什么领域，就会在心中的价值天平上给它加码，以确保自己的价值在自己心中过得去。我想高考之于我几乎所有的意义就在于此了。我确认自己的才能、价值、潜力，我知道我的头脑不会背叛我。&lt;/p&gt;
&lt;p&gt;不过我也因此发现，无论一篇论文写得多好，总能挑出来毛病，只要我想挑毛病&lt;/p&gt;
&lt;p&gt;不是我有多么丰富的知识和见解，而是我必须得挑，因为组会轮到我分享论文了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-o5PK33.jpg&quot; alt=&quot;IMG_2423&quot;&gt;&lt;/p&gt;
&lt;p&gt;我时常惊叹于我朋友的学习能力和科研水平，也惊叹于我朋友为人处事的八面玲珑。&lt;/p&gt;
&lt;p&gt;从小到大我见过的成绩好的人太多了，然而天下英雄如过江之鲫，即使你万里挑一，在这里也有 14 万个，更何况世界不止中国。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-5txGU3.PNG&quot; alt=&quot;Screenshot 2025-08-10 at 15.34.22&quot;&gt;&lt;/p&gt;
&lt;p&gt;然而幸福是一种独立于客观存在的能力，就像物债二分法，所有权的变动无效并不意味着买卖合同无效 —— 你看到什么，听到什么，感受到什么，才是最重要的，跟客观世界是什么样的、跟别人怎么看你无关。&lt;/p&gt;
&lt;p&gt;所以幸福不是那种一下被人流推到癫狂的短暂欢乐，而是一种长期平和的满足。就按一线城市人均寿命 80 岁来算，扣除最后几年没有质量的生命，再扣除已经过去的 18 年，以及 4 年本科、3 年硕士占据现有生命的份额，又能剩下多少年的人生呢？&lt;/p&gt;
&lt;p&gt;既然人生永远没有岸，你的体验难道就不重要吗？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-boB2Px.PNG&quot; alt=&quot;Screenshot 2025-08-10 at 15.40.13&quot;&gt;&lt;/p&gt;
&lt;p&gt;有时会想，到底什么样的人生是幸福的人生呢？&lt;/p&gt;
&lt;p&gt;有项研究表明，一个人在年老的时候身体和灵魂都很健康，还对自己很满意的话，他的人生就是幸福的。这里既包含了个人对自己的评估，也要考量医生和心理学家的评估。&lt;/p&gt;
&lt;p&gt;这个研究的好处是把幸福的人生具体化了。不过，研究也表明，你可能等不及到退休的时候再来评估自己的一生。&lt;/p&gt;
&lt;p&gt;所以朋友说他在犹豫交不交养老保险，他感觉活不到退休，即使活到了，按当前生育率推断，也是大概率没人给我们交养老金。&lt;/p&gt;
&lt;p&gt;总觉得人生路很长，未来还可以做很多事，我想了想&lt;strong&gt;也未必很长，人很难说什么时候会死&lt;/strong&gt;，朝生夕死也并非不可能&lt;/p&gt;
&lt;p&gt;我们总是以对未来美好的憧憬，来假释当下的苦难，只是美好一直没有到来，竞争倒是永无止息&lt;/p&gt;
&lt;p&gt;我不想这样，但有时候又没得选&lt;/p&gt;
&lt;h2&gt;#04 愛&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-lWQlfw.JPG&quot; alt=&quot;78efd7ae638ca2c22cf33454fae898ee&quot;&gt;&lt;/p&gt;
&lt;p&gt;几岁的时候，我以为，我将来会是一个很厉害的人。比如别人需要好久才能掌握的自行车，我几分钟就学会了。后来才发现我不是。走在河山林间里，就像是一块鹅卵石置于潮海之中，并没有什么不同。&lt;/p&gt;
&lt;p&gt;十几岁的时候，我意气风发。每当我爬到山顶的时候，跑向海边的时候，以为我不再是我的时候。我总以为山顶的石头不一样，升起的太阳不一样。我总以为海边吹的风不一样，尽头的那边不一样。我总觉得未来还有无限的可能。&lt;/p&gt;
&lt;p&gt;二十几岁的时候，我以为生活锤得我满是伤痕，我也会开朗乐观地面对。但是更多时候，我胆怯，我拘谨，我犹豫。我害怕认识陌生人，害怕自己没出息，害怕自己买不起房子车子，害怕遇不到双向奔赴的爱情。难过的时候，一个人难过，开心的时候，一个人开心。心情低落的时候，写些蹩脚的文字。&lt;/p&gt;
&lt;p&gt;比起一事无成，我还体会过很多糟糕的感觉，比如努力了好久的面试却被轻易挂掉了，非常期待的计划却突然落了空，曾经亲密的好友不再联系，至亲离别时的泪水。太多太多了，就像是一只耷拉着脑袋行走的小狗。&lt;/p&gt;
&lt;p&gt;庆幸的是，这些年来，我也读了很多书，去了很多地方，结交了许多挚友，写下了一些见闻，从波澜壮阔的景观，到至今馋味的各式美食。我还有着一些渺小的心愿，并一直在为它偷偷努力着，尽管可能遥遥无期。我也知道在这个世界上，有很多我穷尽一生都难以望其项背的人。我希望我的坚持，可以弥补运气和先天的不足。&lt;/p&gt;
&lt;p&gt;在大多数情况下，没人愿意聆听我的琐碎。所以，我很沉默，一直都很沉默，沉浸在自己的世界里。直到后来我慢慢遇到了一些好朋友，到如今遇到了与我同频共振的女朋友，我才真正像只刺猬一样敞开心扉，让彼此看到内心深处，那些不为人知的优雅和温柔。我想要变得有趣，变得特别，变得开朗，还想变成王小波说的那样，在这个一生的黄金时代，我有好多奢望，我想爱你，想同你一起去经历，还想在一瞬间变成天上半明半暗的云。&lt;/p&gt;
&lt;p&gt;说真的，在遇见你之前，我从没有傻到会相信一个人能在另一个人身上找到所有他期望的。&lt;/p&gt;
&lt;p&gt;但是在你身上，我找到了所有，一眼万年也有了实感。&lt;/p&gt;
&lt;p&gt;不知道这份爱意会不会让你惊诧，其实，我才是那个被惊艳到的人。&lt;/p&gt;
&lt;p&gt;你善良，温柔，知书达礼，善解人意，言语中透露的一丝稚气显得格外可爱。&lt;/p&gt;
&lt;p&gt;你太好了，好到我觉得无法用语言去形容你，词不达意。&lt;/p&gt;
&lt;p&gt;以至于我开始变得更为贪心，开始想牵你的手，想常伴在你左右。&lt;/p&gt;
&lt;p&gt;我所幻想的四季，蓝天白云，清风野草，星空斑斓，树影婆娑。&lt;/p&gt;
&lt;p&gt;我才发现我所见到的美好，身边有趣的事物，只需要有你分享就足够。&lt;/p&gt;
&lt;p&gt;原来一个人可以完成的事，其实两个人做完也不赖。&lt;/p&gt;
&lt;p&gt;原来我所憧憬的春夏秋冬，都有一个前置条件：与你。&lt;/p&gt;
&lt;p&gt;那些我曾以为需要跋山涉水，花费很多心思的快乐，有时候或许只要有了你就能轻松抵达。&lt;/p&gt;
&lt;p&gt;我喜欢你，也喜欢和你在一起的我自己。那样子的我勇敢快乐、热烈赤诚，内心一触碰都是柔软。&lt;/p&gt;
&lt;p&gt;即使生活已经磨掉了我一部分的勇气和温柔，但是你依然让我相信，失去的还会再长回来，新长出来的依旧闪闪发亮。&lt;/p&gt;
&lt;p&gt;是你让我不再害怕逃避，心有所依。&lt;/p&gt;
&lt;p&gt;是你让我明白了勇气并不是无所畏惧，而是懂得了有除了畏惧以外更重要的事。&lt;/p&gt;
&lt;p&gt;在晚风吹拂的地方，我想要趁着窗外的月光，趁着满天的繁星，拥抱你、亲吻你...&lt;/p&gt;
&lt;p&gt;当我真实地爱上你，意味着除了家人朋友，我还拥有了另一种完全不同的爱。&lt;/p&gt;
&lt;p&gt;我在某本书上曾经看到过，懂一个人是要花五百顿饭，五百瓶酒，五百个日夜，去一点点接近的，因此可以说懂一个人是要付出生命的。&lt;/p&gt;
&lt;p&gt;我想给你我所有明亮又炽热的喜欢，想和你牵手走过鼓浪屿的巷子，在潮声漫过的白城沙滩等一场日落；想和你在观音山的海岸边看烟花绽放，像星星跌进眼眸；想和你在沙坡尾的影院看一部温柔的老电影，散场后吹着夏夜的晚风慢慢回家；还想在每个黄昏和你坐在环岛路的咖啡店门口，看橘色的光一点点把云朵染成绯红，把世间的浪漫都藏进厦门的晚霞里。&lt;/p&gt;
&lt;p&gt;现在我要花我的生命去懂你爱你了。&lt;/p&gt;
&lt;p&gt;我希望你永远做自己，我来填补你的空缺。&lt;/p&gt;
&lt;p&gt;怎样过一生，这一生都会过去。&lt;/p&gt;
&lt;p&gt;这是我的选择，我愿意过这样的一生。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-i0voNJ.jpeg&quot; alt=&quot;20260110-Evc8gH&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个人可以给另一个人，最贵重的礼物，就是时间&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-pyP3Ds.JPG&quot; alt=&quot;IMG_8173&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;愛&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-KcZtOk.JPG&quot; alt=&quot;IMG_0844&quot;&gt;&lt;/p&gt;
&lt;h2&gt;#05 倒带人生&lt;/h2&gt;
&lt;p&gt;从我记事起，就记得姥姥家的院子里，种着一颗大银杏树。每年春夏，枝头总是挂着一片绿意。等秋天一到，金灿灿的叶子就在风中招摇。风将落叶带去远方，天空飘着的云很是明亮。我只是安静地看着，邻居家那只爱趴在我家屋檐上，呼呼大睡的猫咪，就能虚度一下午的时光。&lt;/p&gt;
&lt;p&gt;我这人很简单&lt;/p&gt;
&lt;p&gt;如果有星巴克和瑞幸，我一定会去喝瑞幸，因为我喝不出他们有什么区别&lt;/p&gt;
&lt;p&gt;但如果是瑞幸和库迪，我一定会去喝瑞幸，哪怕瑞幸十几块一瓶，因为我喝得出来区别&lt;/p&gt;
&lt;p&gt;我喜欢没有意义的东西，或者说纯粹的东西，比如喜剧片或悬疑片，我不喜欢对我说教的，蕴含深刻道理的喜剧，这种还不如让我去看黑马程序员网课和国考 980 系统班&lt;/p&gt;
&lt;p&gt;小时候我看了好几次东邪西毒，但当时看不懂，所以更喜欢东成西就这样纯粹的荒谬与欢笑，大话西游这样的刻骨铭心的感情&lt;/p&gt;
&lt;p&gt;在王家卫的电影哲学里，武侠、江湖皆成摆设，唯有爱与孤独才是亘古不变的精神主题&lt;/p&gt;
&lt;p&gt;父母总说长大后就能看懂，但我已经很久没有再看了，因为我觉得我还是看不懂&lt;/p&gt;
&lt;p&gt;有些东西，我并不觉得会随着阅历或者经验的增长而改变&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-ldE2z0.PNG&quot; alt=&quot;Screenshot 2025-08-10 at 15.39.55&quot;&gt;&lt;/p&gt;
&lt;p&gt;一次偶然，初中同学聊天，他说我变了，其实好多人也说过，到初中之后就开始了&lt;/p&gt;
&lt;p&gt;不过他们总会对外貌成绩一类的感兴趣，或者感到惊讶&lt;/p&gt;
&lt;p&gt;其实人真正改变的是内心，内心的改变是几乎不会表露在外的&lt;/p&gt;
&lt;p&gt;所以我一般都把他们的话当耳旁风&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因为从来没有人问过我经历过什么&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我有时会想到一个高中时很好的朋友身前，一个和我在大大小小的考试中对于名次较劲的人&lt;/p&gt;
&lt;p&gt;我曾经很想问问他，你看我有几分像从前&lt;/p&gt;
&lt;p&gt;但是已经觉得有些意兴阑珊，坚如磐石的友谊也会在岁月下磨损，最终消逝&lt;/p&gt;
&lt;p&gt;极少数人属于木桶中的美酒，岁月流逝后，反而越发醇香&lt;/p&gt;
&lt;p&gt;很明显，即使是他，也属于前者&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-NFFn6Y.PNG&quot; alt=&quot;Screenshot 2025-08-10 at 15.34.59&quot;&gt;&lt;/p&gt;
&lt;p&gt;我一直觉得，我以前和现在，本质上并没有太大的变化，直到我看到曾经自己写的日寄&lt;/p&gt;
&lt;p&gt;我需要承认，我和以前不一样了&lt;/p&gt;
&lt;p&gt;我不再敢打敢拼，不再义无反顾，我会犹豫，会权衡利弊，会害怕未来，会怀疑自己&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-1pE8cm.PNG&quot; alt=&quot;Screenshot 2025-08-10 at 15.31.15&quot;&gt;&lt;/p&gt;
&lt;p&gt;很久以前看 anlin 老师的文章，记得评论区有句话说，说是如果有一天 anlin 老师不再更新文章了，说不定不是一件坏事，因为这说明 anlin 老师在努力生活，或者过得不错，所以不再写日寄了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可我不是&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我想把这句话改改，我的日寄大多数时候都很丧，其中虽然有我个人的原因在，但也有深夜写文章的 buff 加成，我希望有一天，大家都能过得不错，这样就不用看我的文章来找一些什么精神共鸣了，但我还是希望提供一些其他东西在，比如我的失败经历、我的踩坑经验，也希望对你有些帮助&lt;/p&gt;
&lt;p&gt;有时候总在想人到底为了什么而活&lt;/p&gt;
&lt;p&gt;我问家里一个长辈，他说是为了他女儿，我觉得太俗套&lt;/p&gt;
&lt;p&gt;我问还在上高中快要高考的表妹，她说是为了自己，我觉得太热血&lt;/p&gt;
&lt;p&gt;我问了不少人，大多说法都很千篇一律&lt;/p&gt;
&lt;p&gt;我想到问一个大学时候很中二的朋友，他说我脑子是不是进水了&lt;/p&gt;
&lt;p&gt;我说我在思考人生，这是个大事&lt;/p&gt;
&lt;p&gt;他说福州有家日料自助打折了，准备找空去吃&lt;/p&gt;
&lt;p&gt;我也在看自助，但是自助的价格总是很贵&lt;/p&gt;
&lt;p&gt;但是要是能睡前买一张自助餐票，然后睡醒了去坐一段不长不短的地铁，狠狠地吃一顿&lt;/p&gt;
&lt;p&gt;这样活着也还不错&lt;/p&gt;
&lt;p&gt;但人总会告别这个世界，如果是生病的话，应该会有一个时间周期，如果真有这么一天，我不希望你遇到我时，和我说你有多舍不得我，什么没我不行，什么明明知道没有希望还鼓励我的话，什么没有你日子怎么过，你有什么事情还没有做，等你好了我们一起去怎样怎样...&lt;/p&gt;
&lt;p&gt;我更想听的是，你有多么怀念我，我这辈子已经完成了什么，告诉我接下来的日子没有我你们仍然会好好过&lt;/p&gt;
&lt;p&gt;这就足够了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-671ez6.png&quot; alt=&quot;image-20260207193109269&quot;&gt;&lt;/p&gt;
&lt;p&gt;最近贼有意思，朋友和对象吵架要分手，朋友的对象问：那我们之前的回忆算什么？&lt;/p&gt;
&lt;p&gt;TA 说这算你记性好&lt;/p&gt;
&lt;p&gt;女生又说：如果未来再相见呢？&lt;/p&gt;
&lt;p&gt;TA 说那算你运气好&lt;/p&gt;
&lt;p&gt;我的运气就挺点背的，说了再见的人基本没能再见过&lt;/p&gt;
&lt;p&gt;有些缘分就像北平的秋雨，不贪心便是落叶轻覆的思念，过分执着就成了望不穿的绵绵水帘&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一生很长，很多人不会永远在我的生活里，但会永远在我的故事里&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;朋友说酒是万能的，配什么都能喝，也没什么忌口&lt;/p&gt;
&lt;p&gt;我说也不一定，和头孢就能起反应&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-aNrkVx.PNG&quot; alt=&quot;Screenshot 2025-08-10 at 15.38.56&quot;&gt;&lt;/p&gt;
&lt;p&gt;最近快放假了，我朋友说好久没来见过我了，问我有没有空，说是想我了&lt;/p&gt;
&lt;p&gt;我发现人可以用懒这个词解释很多事情，特别是在现代社会&lt;/p&gt;
&lt;p&gt;比如懒得起了，懒得学了，懒得卷了，懒得见了&lt;/p&gt;
&lt;p&gt;摆烂这个词挺好，可以称得上是立体防御&lt;/p&gt;
&lt;p&gt;什么时候等我摆明白了，或许就看开了&lt;/p&gt;
&lt;p&gt;有时也会想在寒假去见一见多年未见的朋友，有时又觉得不值得&lt;/p&gt;
&lt;p&gt;比如舍弃一些实习的机会，科研的时间&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为了所谓的正确、成功，人们不断失去&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我是那种会因为预料到将来的离别而去珍惜与当下朋友相处时间的人&lt;/p&gt;
&lt;p&gt;但不是所有人都这样，所以大多数情况下我都是在浪费感情&lt;/p&gt;
&lt;p&gt;这是一个流行离开的世界，但是我们都不擅长告别，时间不会抚平悲伤，只会让人淡然&lt;/p&gt;
&lt;p&gt;我是个计划性比较强的人，但唯一不可能计划的便是生命的长度，我们往往在追寻其中忽视了它的宽度，忘记去创造更多的价值，我们不知道自己生命的长短，但我们现在还在这个世界上，就应该好好生活。生命本来就不长，焦虑也是一天，开心也是一天，为何不开开心心的过&lt;/p&gt;
&lt;p&gt;最近看过一句话，不知道出自哪里，说是“今天可以死，明天也可以活”，这种境界是很高的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20251220-mbucab.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;当我在写这篇年终时，正在为学习和生活上的一地鸡毛而烦恼着。&lt;/p&gt;
&lt;p&gt;不知你们会不会也像我一样，在过去或未来的某个时刻，也在怀念着那个无忧的年代。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落叶随着风一阵摆动，家乡的银杏树一直都在，可是我已经回不去了&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;#06 生活碎片&lt;/h2&gt;
&lt;p&gt;人的烦恼就是记性太好，如果可以把所有事都忘掉，以后每一天都是个新开始，你说多好&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时间改变了太多东西了&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;很荣幸能出现在别人的故事里 @zyu&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-yWbpf4.png&quot; alt=&quot;ScreenShot_2026-02-07_011954_233&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;总要做点什么自己认为正确的事&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-2cInKd.JPG&quot; alt=&quot;IMG_8178&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;依旧 Eason&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-J5FwTn.JPG&quot; alt=&quot;IMG_8174&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JayChou 今年倒是听少了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-SoaCqg.JPG&quot; alt=&quot;IMG_8176&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GitHub 2025&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-uEXdMB.jpeg&quot; alt=&quot;github-contributions_1.5x_postspark_2026-02-07_02-27-57&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;LeetCode 2025&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-0MVkGZ.jpg&quot; alt=&quot;IMG_6507&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;📷 摄影是我割舍不去的爱好&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-EO1CyL.jpg&quot; alt=&quot;6fba819e71870abe011e10ee428f6b07&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;略懂些游戏&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-WmN3TX.JPG&quot; alt=&quot;e8bd49c009317151783585ea76dba568&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-eI29Sa.jpg&quot; alt=&quot;858436270d98ea632151263fcf4daf5b&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-a51ra0.JPG&quot; alt=&quot;IMG_8172&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;和女友的 steam 家庭，有时间一定玩&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-gKeQ7g.JPG&quot; alt=&quot;02b59eb3d87d3d443a588cdb2a056363&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;应该没有女生不喜欢花，我妈妈也是&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-4OPuik.JPG&quot; alt=&quot;IMG_8182&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;翔安唯一的美，就在他的晚霞&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-fDfX2B.jpg&quot; alt=&quot;8a743a6c20cd75acedb7908bfd0d1397&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;XMU 跨年晚会&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-pHCVgK.JPEG&quot; alt=&quot;P1000439&quot;&gt;&lt;/p&gt;
&lt;p&gt;厦门夏季的天气很不错，夜晚有风，但不冷，白天的余温还没散去，风吹来只觉得凉爽&lt;/p&gt;
&lt;p&gt;很像大三刚保研完在湖边吹风的那个夜晚，意气风发&lt;/p&gt;
&lt;p&gt;不过意气风发这个词仿佛永远都不再适合我了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20260207-AfzS3a.jpg&quot; alt=&quot;2e24ee37470ef4910e30365fd81c4659&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/20260207-bfem8k.DifQqnjG.PNG"/><enclosure url="/_astro/20260207-bfem8k.DifQqnjG.PNG"/></item><item><title>2026.01.01</title><link>https://coooredump.github.io/blog/journal/2026-01-01</link><guid isPermaLink="true">https://coooredump.github.io/blog/journal/2026-01-01</guid><description>新年快乐</description><pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 2026 的开始，我并没有许下什么新年愿望，似乎每一年都没有，但每一年都会经历很多难忘的事情&lt;/p&gt;
&lt;p&gt;我的生活像支离破碎的镜面，然后我在其中寻找着相遇与感动，所幸，仍有值得留恋的地方&lt;/p&gt;
&lt;p&gt;这几年不怎么想和不少朋友见面了，可能是因为老了些，朋友和技术一样，终究会出来新的，淘汰旧的，但也终究能有留存下来的人&lt;/p&gt;
&lt;p&gt;这一年终究是过去了，我对 2025 的唯一感觉就是，年费会员快要到期了，该交钱了&lt;/p&gt;
&lt;p&gt;还是艰难地走过了这一年，也终究迎来了一些未曾有过的改变，我或许会这样走过很多年，也许不会，这大概是一个普通人的历程&lt;/p&gt;
&lt;p&gt;今年比往年要好很多，烟花凌空绽放，整座城市隆隆作响，极尽所能地宣告新年的到来，往年只有电视机里主持人带着职业假笑，高声呐喊这的倒计时，现在打开窗户，迎面扑来的是久违的焰火气息，让我想起遥远的过去&lt;/p&gt;
&lt;p&gt;怀古切勿伤今，希望我们能够在未来再见，我的朋友&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>2025.08.30 京东笔试题</title><link>https://coooredump.github.io/blog/recruitment/20250830-jd</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/20250830-jd</guid><description>京东 20250830 笔试解析</description><pubDate>Sat, 30 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;题解链接：https://mp.weixin.qq.com/s/M-YBVRdoGuKu9ZS9roUFVA&lt;/p&gt;
&lt;p&gt;测评链接：https://niumacode.com/training/184&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 定向越野&lt;/h2&gt;
&lt;p&gt;现在有赛事要在一山区举行，山区内有 $n$ 个打卡点（编号为 1~n），打卡点之间有 $m$ 条可供通行的山路，每条山路都需要花费确定的通行时间（单位：分钟）。赛事要求选手从“起点打卡点”出发，最终到达“终点打卡点”。 组委会在某两个打卡点 A 和 B 之间设置了一条特殊索道，选手可以通过该索道在这两个打卡点之间瞬间往返（不消耗时间）。 请计算选手从起点到终点的最短通行时间（保证存在可达路径）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一行两个正整数 $n, m$ ，分别表示打卡点总数和山路数量。&lt;/li&gt;
&lt;li&gt;第二行两个正整数，分别表示“起点打卡点”和“终点打卡点”的编号。&lt;/li&gt;
&lt;li&gt;第三行两个正整数，分别表示设有特殊索道的两个打卡点的编号。&lt;/li&gt;
&lt;li&gt;接下来 $m$ 行，每行三个正整数 $u, v, t$，分别表示打卡点 $u$ 和 $v$ 之间有一条山路，双向通行，通过该山路需要耗时 $t$ 分钟。&lt;/li&gt;
&lt;li&gt;$1 ≤ n ≤ 100, 1 ≤ m ≤ 2 * n$，每条道路的时间花费在 $[1, 10]$ 之间。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;一个整数表示最短通行时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输入&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;6 5
2 6
3 5
3 3 3
3 4 2
4 5 1
5 6 4
2 4 5
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;7
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;代码 1：Floyd 算法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;把“索道”视为一条权值为 0 的无向边 &lt;code&gt;(A, B)&lt;/code&gt;。 用 Floyd-Warshall 求全源最短路，直接得到所有点对最短距离。最终答案就是 &lt;code&gt;dist[s][e]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;复杂度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;时间：$O(n^3)$（n ≤ 100，可承受）&lt;/li&gt;
&lt;li&gt;空间：$O(n^2)$&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m;
    if (!(cin &gt;&gt; n &gt;&gt; m))
        return 0;
    int s, e, A, B;
    cin &gt;&gt; s &gt;&gt; e &gt;&gt; A &gt;&gt; B;

    const long long INF = (long long)1e15;
    vector&amp;#x3C;vector&amp;#x3C;long long&gt;&gt; d(n + 1, vector&amp;#x3C;long long&gt;(n + 1, INF));
    for (int i = 1; i &amp;#x3C;= n; i++)
        d[i][i] = 0;

    for (int i = 0; i &amp;#x3C; m; i++) {
        int u, v, w;
        cin &gt;&gt; u &gt;&gt; v &gt;&gt; w;
        if (w &amp;#x3C; d[u][v]) {
            d[u][v] = w;
            d[v][u] = w; // 无向边
        }
    }

    // 索道 0 费用
    d[A][B] = d[B][A] = 0;

    // Floyd
    for (int k = 1; k &amp;#x3C;= n; k++) {
        for (int i = 1; i &amp;#x3C;= n; i++) {
            if (d[i][k] == INF)
                continue;
            for (int j = 1; j &amp;#x3C;= n; j++) {
                long long v = d[i][k] + d[k][j];
                if (v &amp;#x3C; d[i][j])
                    d[i][j] = v;
            }
        }
    }

    cout &amp;#x3C;&amp;#x3C; d[s][e] &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;代码 2：堆优化版的 Dijkstra 算法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;直接使用 dijkstra 直接计算带权无向图单源最短路径，索道就是 0 权重的边。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这里使用的是「&lt;a href=&quot;https://cotenite.github.io/blog/algorithm/%E6%90%9C%E7%B4%A2%E4%B8%8E%E5%9B%BE%E8%AE%BA.html#%E5%A0%86%E4%BC%98%E5%8C%96%E7%89%88%E7%9A%84dijkstar%E7%AE%97%E6%B3%95&quot;&gt;堆优化版的 dijkstra 算法&lt;/a&gt;」&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;queue&gt;
#include &amp;#x3C;climits&gt;
using namespace std;

typedef pair&amp;#x3C;int, int&gt; pii; // (distance, node)

void dijkstra(int start, vector&amp;#x3C;vector&amp;#x3C;pii&gt;&gt;&amp;#x26; graph, vector&amp;#x3C;int&gt;&amp;#x26; dist) {
    int n = graph.size();
    dist.assign(n, INT_MAX);
    priority_queue&amp;#x3C;pii, vector&amp;#x3C;pii&gt;, greater&amp;#x3C;pii&gt;&gt; pq;
    dist[start] = 0;
    pq.push({0, start});
    while (!pq.empty()) {
        int u = pq.top().second;
        int d = pq.top().first;
        pq.pop();
        if (d != dist[u]) continue;
        for (auto&amp;#x26; edge : graph[u]) {
            int v = edge.first;
            int w = edge.second;
            if (dist[u] + w &amp;#x3C; dist[v]) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
}

int main() {
    int n, m;
    cin &gt;&gt; n &gt;&gt; m;
    int S, T;
    cin &gt;&gt; S &gt;&gt; T;
    int A, B;
    cin &gt;&gt; A &gt;&gt; B;
    
    vector&amp;#x3C;vector&amp;#x3C;pii&gt;&gt; graph(n+1);
    for (int i = 0; i &amp;#x3C; m; i++) {
        int u, v, t;
        cin &gt;&gt; u &gt;&gt; v &gt;&gt; t;
        graph[u].push_back({v, t});
        graph[v].push_back({u, t});
    }
    
    // 添加特殊索道：A和B之间双向0成本
    graph[A].push_back({B, 0});
    graph[B].push_back({A, 0});
    
    vector&amp;#x3C;int&gt; dist;
    dijkstra(S, graph, dist);
    
    cout &amp;#x3C;&amp;#x3C; dist[T] &amp;#x3C;&amp;#x3C; endl;
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 上升子序列&lt;/h2&gt;
&lt;p&gt;给定一个长为 $n$ 的排列 ${a}$，排列是指 ${a}$ 中包含 $1～n$ 的所有正整数恰好一次。&lt;/p&gt;
&lt;p&gt;回顾一下， ${a}$ 的一个子序列是从 ${a}$ 中若干个元素（不一定连续）按取出顺序不改变相对位置形成的序列。&lt;/p&gt;
&lt;p&gt;对于 ${a}$ 的一个子序列 ${b}$，如果 ${b}$ 中的元素单调递增，就称 ${b}$ 是 ${a}$ 的一个&lt;strong&gt;上升子序列&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于 ${a}$ 的一个 ${c}$上升子序列 ，如果找不到比 ${c}$ 元素个数更多的上升子序列了，就称 ${c}$ 是 ${a}$ 的一个&lt;strong&gt;最长上升子序列&lt;/strong&gt;。显然，${a}$ 可以有多个最长上升子序列。&lt;/p&gt;
&lt;p&gt;请你对每个 $i∈[1,n]$ ，求出有多少个不同的 ${a}$ 的最长上升子序列包含 $a_i$。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一行一个正整数 $n$。&lt;/li&gt;
&lt;li&gt;第二行 $n$ 个正整数，以空格分隔，表示 $a_i$。&lt;/li&gt;
&lt;li&gt;保证 ${a}$ 是一个 $1～n$ 的排列。&lt;/li&gt;
&lt;li&gt;$1≤n≤2*10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;输出 $n$ 行，每一行一个非负整数，第 $i$ 行的输出表示包含 $a_i$ 的最长上升子序列个数。&lt;/li&gt;
&lt;li&gt;因为答案可能很大，所以你只需要输出答案对 $998244352$ 取模的结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输入&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;5
3 1 4 2 5
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;1
2
2
1
3
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;解释&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于输入的排列，其最长上升子序列长度为 ，所有不同的最长上升子序列如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$1,2,5$&lt;/li&gt;
&lt;li&gt;$1,4,5$&lt;/li&gt;
&lt;li&gt;$3,4,5$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以 $a_4=2$ 为例，包含了 $a_4=2$ 的最长上升子序列有 1 个，故答案为 1。 包含了 $a_2=1$ 的最长上升子序列有 2 个，故答案为 2。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;代码 1：涉及最长上升子序列（LIS）长度与计数的组合、前向/后向 DP、线段树（或树状数组）维护 &lt;code&gt;&amp;#x3C;最长长度, 计数&gt;&lt;/code&gt; 的区间合并规则（长度优先、同长计数相加）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个问题要求我们求出每个元素 $a_i$ 在所有最长上升子序列中出现的次数。题目核心是通过&lt;strong&gt;动态规划与线段树结合&lt;/strong&gt;来解决，具体思路如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算正向 DP（从左到右）
&lt;ul&gt;
&lt;li&gt;使用 Fenwick 树来高效计算以每个位置结尾的 LIS 长度和数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[i]&lt;/code&gt;：以 &lt;code&gt;a[i]&lt;/code&gt; 结尾的 LIS 长度。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cnt[i]&lt;/code&gt;：以 &lt;code&gt;a[i]&lt;/code&gt; 结尾且长度为 &lt;code&gt;dp[i]&lt;/code&gt; 的 LIS 数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;计算反向 DP（从右到左）
&lt;ul&gt;
&lt;li&gt;将数组反转并映射值（&lt;code&gt;a[i] = n - a[i] + 1&lt;/code&gt;），以便计算从右向左的 LIS（实际上是最长下降子序列的逆）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp_rev[i]&lt;/code&gt;：从 &lt;code&gt;a[i]&lt;/code&gt; 开始（到末尾）的最长下降子序列的长度（实际上是从右向左的 LIS）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cnt_rev[i]&lt;/code&gt;：相应的数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;判断每个元素是否在 LIS 中
&lt;ul&gt;
&lt;li&gt;整个序列的 LIS 长度为 L。&lt;/li&gt;
&lt;li&gt;对于元素 &lt;code&gt;a[i]&lt;/code&gt;，如果 &lt;code&gt;dp[i] + dp_rev[i] - 1 == L&lt;/code&gt;，则它出现在某个 LIS 中。&lt;/li&gt;
&lt;li&gt;此时，包含 &lt;code&gt;a[i]&lt;/code&gt; 的 LIS 数量为 &lt;code&gt;cnt[i] \* cnt_rev[i] % MOD&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
using ll = long long;
const int MOD = 998244353;
const int N = 200005;

int n;
int a[N];
int dp[N], dp_rev[N];
ll cnt[N], cnt_rev[N];

struct Fenw {
    int len;
    vector&amp;#x3C;int&gt; max_val;
    vector&amp;#x3C;ll&gt; cnt_val;
    
    Fenw(int n) : len(n), max_val(n + 1, 0), cnt_val(n + 1, 0) {}
    
    void update(int i, int val, ll c) {
        for (; i &amp;#x3C;= len; i += i &amp;#x26; -i) {
            if (val &gt; max_val[i]) {
                max_val[i] = val;
                cnt_val[i] = c;
            } else if (val == max_val[i]) {
                cnt_val[i] = (cnt_val[i] + c) % MOD;
            }
        }
    }
    
    pair&amp;#x3C;int, ll&gt; query(int i) {
        int res_val = 0;
        ll res_cnt = 0;
        for (; i &gt; 0; i -= i &amp;#x26; -i) {
            if (max_val[i] &gt; res_val) {
                res_val = max_val[i];
                res_cnt = cnt_val[i];
            } else if (max_val[i] == res_val) {
                res_cnt = (res_cnt + cnt_val[i]) % MOD;
            }
        }
        return {res_val, res_cnt};
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin &gt;&gt; n;
    for (int i = 1; i &amp;#x3C;= n; i++) cin &gt;&gt; a[i];
    
    Fenw fenw(n);
    for (int i = 1; i &amp;#x3C;= n; i++) {
        auto [val, c] = fenw.query(a[i] - 1);
        dp[i] = val + 1;
        cnt[i] = max(1LL, c);
        fenw.update(a[i], dp[i], cnt[i]);
    }
    
    int LIS = 0;
    for (int i = 1; i &amp;#x3C;= n; i++) LIS = max(LIS, dp[i]);
    
    reverse(a + 1, a + n + 1);
    for (int i = 1; i &amp;#x3C;= n; i++) a[i] = n - a[i] + 1;
    
    Fenw fenw_rev(n);
    for (int i = 1; i &amp;#x3C;= n; i++) {
        auto [val, c] = fenw_rev.query(a[i] - 1);
        dp_rev[i] = val + 1;
        cnt_rev[i] = max(1LL, c);
        fenw_rev.update(a[i], dp_rev[i], cnt_rev[i]);
    }
    
    reverse(dp_rev + 1, dp_rev + n + 1);
    reverse(cnt_rev + 1, cnt_rev + n + 1);
    
    vector&amp;#x3C;ll&gt; ans(n + 1, 0);
    for (int i = 1; i &amp;#x3C;= n; i++) {
        if (dp[i] + dp_rev[i] - 1 == LIS) {
            ans[i] = cnt[i] * cnt_rev[i] % MOD;
        }
    }
    
    for (int i = 1; i &amp;#x3C;= n; i++) {
        cout &amp;#x3C;&amp;#x3C; ans[i] &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    }
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/20250831-FnzK8P.CP5ZVE2I.png"/><enclosure url="/_astro/20250831-FnzK8P.CP5ZVE2I.png"/></item><item><title>秋招的终局之战：意向书、offer 选择与三方</title><link>https://coooredump.github.io/blog/recruitment/salary-negotiation</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/salary-negotiation</guid><description>整理一下互联网秋招后期，主要是拿到 offer 之后的攻略</description><pubDate>Mon, 25 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;关于校招，我发现目前网上可参考的资料一般都是面试注意事项和面试经验，关于面试通过之后要怎么做的经验特别少。比方说面完 hr 面我该干嘛？谈薪的时候是怎么谈的？收到意向书是不是就可以什么都不管了？有这么多 offer 我该怎么选？我要拒绝 offer 最好什么时候说？怎么说？有没有模板？等等。&lt;/p&gt;
&lt;p&gt;对于大多数人来说，我们都是第一次拿到 offer，甚至是第一次参加面试，对于这些相关的流程一窍不通。但是用人单位呢，他们大多已经举办了多届招聘，对于应届生的心态了如指掌，关键时刻打电话给你这么一催，你就容易上头，本来还在犹豫的 offer 也不等了。这就造成同学们前期不知道进行到哪一步了，下一步要做什么该做什么，整天检查邮箱生怕到手的 offer 因为没完成某个步骤而丢掉了，后期在面对多个 offer 的时候不知道该用什么标准来选择，被 hr 忽悠了去，错失了 argue 的机会或者跳到坑里去。&lt;/p&gt;
&lt;p&gt;所以我写这篇文章的目的很简单，就是整理一下互联网秋招后期，主要是拿到 offer 之后的攻略，为大家的 offer 锦上添花。&lt;/p&gt;
&lt;h2&gt;HR 面结束后的全流程&lt;/h2&gt;
&lt;p&gt;这里只列举秋招面完hr面之后的流程，从投递到面试的过程不考虑在这篇文章之内，并且不同公司和不同学校的要求可能有所不同。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hr 面结束&lt;/li&gt;
&lt;li&gt;做测评（百度华为等才会有）：测评主要是测试你的性格和一些行测题目，认真做就行，只要不是非常离谱都会过的，测评很少有卡人的。收到测评一般来说就意味着你面试通过了。&lt;/li&gt;
&lt;li&gt;hr 打电话问你愿不愿意去实习（百度、快手等）：如果加了 hr 微信可能会在微信上问。比较缺人的组就会有这样子问，实习一般都不是必须，不过能去实习的话用人单位会比较喜欢，对于你上手工作也比较快，有的公司会将这一段实习时间算在晋升考核时间里，也就是晋升会更快。&lt;/li&gt;
&lt;li&gt;hr 打电话发&lt;strong&gt;口头 offer&lt;/strong&gt;（也就是 oc）：口头 offer 就是你通过了所有面试，这个口头 offer 约束力没那么强，也不会透露你的薪资，但是有 hr 会问你期望薪资是多少（比如 shopee），这个时候建议做好准备，&lt;strong&gt;给 hr 报一个年薪下限&lt;/strong&gt;。我的经验是去找小伙伴，找到同样投这个公司的小伙伴的 QQ 群或者微信群，大家信息互通一下（不是说让你交流薪资啊！高压线），看看大家对于期望薪资是怎么报的，参考一下，然后也可以互相交流一下。
&lt;ul&gt;
&lt;li&gt;找这些群的渠道有：在牛客网上这个公司的话题里面找帖子，或者发帖求组团；问身边已经加入群组的人拉你进去。&lt;/li&gt;
&lt;li&gt;第二个是看看 offershow，看往年相同职位的薪资，然后参考今年的整体情况报一下，&lt;strong&gt;给 hr 报的时候不能报具体数字，也不要报上限，建议就报一个下限，比如说我希望公司能给我不低于 30 万的年薪&lt;/strong&gt;。但这个薪资不是最终的，这个时候除非是特别不想去的公司，一般建议都接下口头 offfer（或者说养鱼）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;座谈会（只有 TP-LINK 有）：座谈会就是发一封邮件约定某个时间参与一个线上会议，同一批次很多个候选人一起参加，主要是 hr 宣讲公司情况，福利待遇，和接下来的流程。座谈会上不会透露薪资情况，因为通常这个时候当年的薪酬方案还没做出来（薪酬方案每年都要重新做，因为市场薪酬价格一直在变）。就是了解一下公司的，只需要听就行，不听也行。
云证信息（只有腾讯有）。会发一封邮件让你验证身份，就是扫码通过微信小程序做一下人脸识别，不需要准备其他手续材料。&lt;/li&gt;
&lt;li&gt;邮件意向书：&lt;strong&gt;一般邮件意向书会在 oc 之后，你接受 oc 之后才有&lt;/strong&gt;。注意，意向书也不能和 offer 划等号，这个意向书只是说我们公司承诺给你这个 offer，并且意向书上通常不会有薪资情况。&lt;strong&gt;有的公司会要求回复意向书，因为这个意向书可以视作两方协议，关于两方协议，相关工作里面有介绍，没有法律约束力，可以随意毁两方，并且通常是没有违约金的&lt;/strong&gt;。注意看意向书邮件的末尾是否说了需要回复，或者点击链接确认，以及有效期，否则没有及时回复或确认可能会错失 offer！通常来说，收到意向书就 99% 稳了，一般很少有毁意向书的。这个时候除非特别不想去或者说明确说明两方违约要交违约金，一般建议接下意向书（或者说养鱼）。&lt;/li&gt;
&lt;li&gt;谈薪：也就是俗称的“&lt;strong&gt;开奖&lt;/strong&gt;”，通常形式是 hr 打电话过来跟你聊你的薪资情况。有几点需要注意的：
&lt;ul&gt;
&lt;li&gt;第一，hr 会问你手头都有什么 offer，薪资分别是多少，这个时候如果有其他的，就报一下，&lt;strong&gt;hr 可能会让你发意向书邮件截图或者带薪 offer 的薪资截图&lt;/strong&gt;，如果你的其他 offer 确实很好，hr 会考虑给你重新确定薪酬方案（因为薪酬方案是已经确定的，要改的话要重新定），但一般不会因为你只有他一家 offer 立马 pua 压价（薪酬方案一般做出来了就不会临时改动）。&lt;/li&gt;
&lt;li&gt;第二，有很多公司虽然说是谈薪，其实只是通知你一下你的薪酬方案是多少，&lt;strong&gt;不要以为 hr 会像面试一样花半个小时跟你讨论你的薪资该怎么分配&lt;/strong&gt;。他把台词读完了之后会问你对 offer 有什么问题吗，要接 offer 吗，&lt;strong&gt;这时候如果你有更好的 offer 要 argue，就要在这个时候提出你的要求，比如不能比某某公司的薪酬低，总包不能低于多少多少等等&lt;/strong&gt;，记住不要说确定的数字比如我要总包 42W，一定要说一个范围或者下限。如果你觉得这个 offer 不 ok，直接说很抱歉我不能接受这个薪酬或者我已经有更好的 offer 了。&lt;/li&gt;
&lt;li&gt;第三，谈薪时间是不会提前约定的，&lt;strong&gt;谈薪电话通常会毫无征兆的打过来&lt;/strong&gt;，如果漏接了也没关系，hr 会过几个小时再打一次，如果一直打不通，会给你发邮件的（亲测 TP-LINK 在多次打不通我的电话之后给我发了一封邮件约谈薪时间），如果你有加等开奖群，你会发现群里会一个接一个的有人开奖，如果开奖的跟你是同一个部门的，那你就要做好接电话的准备了。&lt;strong&gt;并且一个谈薪电话通常很短，没有 argue 环节的话通常不超过 5 分钟&lt;/strong&gt;，hr 语速拉满，仿佛台词烫嘴，这个时候如果听不清，最好的方法是开启通话录音。并且如果公司 hr 收到拒 offer 反馈太多了还会二次谈薪（比如 2020 年秋招的百度、shopee 等等，第一次谈薪后又涨了一波，给所有人又打电话谈了一次薪），甚至你都已经接了还会再谈薪一次。如果有些公司催你接 offer 而另一些公司还没有到谈薪的地步，那你要拖一下，另外用这个已经谈薪的 offer 去催那个没谈薪的，一般你跟 hr 说一下有效期，对方会很快给你谈薪的。&lt;/li&gt;
&lt;li&gt;签三方：这是最重要的一步！&lt;strong&gt;三方的形式有网签三方和纸质三方&lt;/strong&gt;，根据发起方不同有学校发起和公司发起，这个要看学校，对，这是学校定的而不是公司规定的。比如广东的高校采用的就是网签三方的形式，且无需公司发起，通过小程序“广东大学生就业创业”来完成。&lt;/li&gt;
&lt;li&gt;带薪 offer（大部分银行没有带薪 offer）：很多公司是在收到三方之后才会发放带薪 offer。带薪 offer 这个就是最终的 offer 啦，这个带薪 offer 会详细写明你的职位，福利和薪资，offer 上面数据可能会和三方不同或者跟 hr 跟你讲的不同，比方说签字费总包房补啥的，数字都需要好好看一下。这个带薪 offer 一般都是不需要确认的，但是为了稳妥起见，建议通读 offer 邮件全文。&lt;/li&gt;
&lt;li&gt;填入职信息（比如腾讯）和申请实习：有的公司在带薪 offer 里面会提供入职信息填写的链接，这个时候就要填你的入职信息了，通常需要准备工牌照、英文名、毕业时间、入职时间等等。有些信息确实太早没法确定，可以问一下师兄师姐往年情况或者 hr 这种不确定的情况怎么处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;选择 offer 考虑因素&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;更多参考链接：&lt;a href=&quot;https://www.nowcoder.com/discuss/353154033839972352&quot;&gt;分享 offer 选择&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个东西非常主观的其实，因人而异，因为每个人考虑问题的时候最关心的点不同，比如有的人面向薪资编程，有的人家里不缺钱就想找个稳定的工作融入社会，有的人想跟 npy 待的近些，有的人非大厂不去，等等。我在这里结合自身心路历程和相关资料总结列举一下可能需要考虑的因素，具体每个因素怎么看待要看同学们了。其实适合自己的自己喜欢就行，没有必要掂量来掂量去的，比别人好也不见得是永远都好，以下各条因素之间并无排序，并不表示某一个比较重要，看个人偏好。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;薪资&lt;/strong&gt;（offershow）：一般来说这个具体看自己得到的薪酬跟同年同岗位应届生相比是什么水平（这个可以在 offershow 小程序里面看得到），跟往年同岗位相比什么水平，以及跟自己同实验室或者水平相当的同学相比什么水平，如果相差的比较大，先考虑能不能 argue，不能 argue 基本可以确定是公司开给你的劝退价，别去了。还要看薪资的构成，总包具体包括哪些？分几年给？加班费另外算还是算在了月薪里面？是不是把房补餐补也算在里面了？年终有多大的比例能拿满？通常来说薪资=base+签字费+股票（+房补+餐补+加班费+人才补贴），base 就是基本工资，通常是 M×N 的形式，以 32×16 为例，这个 base 的意思就是每个月月薪 32000，16 是一年发 16 个月，因为一年只有 12 个月，那么也就是说年终奖金是 16-12=4 个月的月薪，也就是 128000 元的年终。签字费也在很多公司里面有，意思就是你在劳动合同上签字我就给你这笔钱，一次性的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;base 地&lt;/strong&gt;：base 地就是你将在哪个城市工作。base 地我们要考虑什么呢，通常来说同学们都会选择离家比较近的，或者当地有人才补贴的，落户比较方便的，房价比较低的，跟好朋友或者 npy 比较近的，这个 base 是不是总部，这个城市互联网产业发展好不好，机会多不多，气候怎么样。通常来说在总部比较好一些，年会什么的在总部举办的活动要大一些，总部的设施要完善一些，leader 一般都在总部。如果你打算在哪里工作哪里买房定居，那么户口和房价应该好好考虑一下，当然如果打算干两年回老家就不用了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公司规模&lt;/strong&gt;：看公司的平台大不大，对于应届生来说，第一份工作最好选平台比较大的，大公司流程比较规范，以后跳槽的时候简历上有一条比较加分的工作经历。并且大公司一般来说不会出现不良行为。在牛客文章「&lt;a href=&quot;https://www.nowcoder.com/discuss/353154033839972352&quot;&gt;分享 offer 选择&lt;/a&gt;」中就有比较小公司之间的 offer 该怎么选和大公司之间的 offer 该怎么选的建议，在这里就不多说了，我觉得这篇文章总结的就很好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;是不是核心业务&lt;/strong&gt;：每个大公司都有自己主打的业务，或者公司的营收主要靠某项业务支撑，那么最好了解一下自己的 team 是不是这项业务的。如果自己的业务是核心业务的话，能得到公司的重视，做的事情都不是小儿科，能够得到极大的锻炼机会，并且核心业务通常赚钱多，相应的绩效和年终也会很多。不过最近技术中台的职位也是比较受欢迎的，这种职位一般都是为各个业务提供接口，做业务的通常就是 ToC 的，技术中台则通常是 ToB 的，并且一般 ToB 的没有 ToC 的赚钱多，但是也没有 ToC 这么累。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公司文化&lt;/strong&gt;（脉脉）：看个人对公司文化的喜好吧，可以问问身边一些已经在公司入职的学长学姐们，毕竟坊间的一些传闻多是添油加醋的，分分钟让你感觉像火坑，但是实际上感觉如何还是看个人的。具体就是你要去的那个组好不好了，可以让已经在公司工作的人内部打听一下，也可以到脉脉上看看（btw 脉脉上的东西建议不要过于迷信）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;薪资组成结构&lt;/h2&gt;
&lt;p&gt;年包：base (基本工资 + 绩效 + 年终奖) + 期权/股票 + 签字费&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 互联网没有绩效，华为有绩效&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;薪资结构：薪资通常由&lt;strong&gt;基本工资 + 绩效工资&lt;/strong&gt;构成。有些公司会将绩效工资设的较高，这虽然可能赚的更多，但也意味着更大的不确定性，往往比较卷。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;五险一金：尤其关注社保和公积金的缴纳基数与比例。多数私企不会按全额月薪缴纳，但缴纳基数和比例越高对个人越有利。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;年终奖：了解年终奖的评估标准，如是否固定发放、基于个人或公司绩效等，并询问普通员工的一般绩效水平。年终奖通常以 15 薪、16 薪等形式体现，表现突出者可获得更高奖金。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;补贴：确认是否有餐补、交通补助、季节性补贴、住房补贴及生日福利等额外补贴，这些都是收入的一部分。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;福利：健身房（建议考察其设施规模）、食堂、茶水间零食饮料、夜宵以及员工折扣等福利也挺重要的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他福利：留意是否有签字费、安家费或政府人才补贴等一次性或阶段性奖励。比如杭州就有人才补贴，相当于变相提高了收入。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;如何谈薪？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;参考链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1iS4y1h7m3/?spm_id_from=333.337.search-card.all.click&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;如何谈薪&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1N14y1r7tJ/?spm_id_from=333.788.videopod.sections&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;HR 常见压价话术 + 谈薪公式&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1Gux9eoEwz/?spm_id_from=333.1387.search.video_card.click&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;2025 届秋招谈薪急救指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nowcoder.com/discuss/353157137239056384&quot;&gt;如何跟 hr 聊薪资，argue 薪资的技巧全在这里&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;谈薪时间及基本常识&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;谈薪时间&lt;/strong&gt;（HR 面后还需要确定是否给你发放 offer，发放 offer 后一段时间才会统一约谈薪资）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互联网企业大致在 10 月底 / 11 月初高峰统一谈薪，逼签三方&lt;/li&gt;
&lt;li&gt;如果是体制内 / 国企，一般是直接面试完现场谈薪，而且这个薪资基本无法谈（argue）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;⚠️ 互联网没有绩效（月薪 = base），华为才有绩效（月薪 = base + 绩效）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;期望薪资：顾名思义就是你自己期望在这份工作中得到的薪资，通常有两种说法，不同公司 hr 方式不一样&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一种是月薪，比如 30k&lt;/li&gt;
&lt;li&gt;还有一种就是总包，比如年薪 50w&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;签字费：签字费就是签订了三方入职之后，公司会直接发到你账户一笔费用，这也是近些年公司们对人才争夺的一种方式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;白菜价（校招中最基础的 offer）、sp (special offer)、ssp (super special offer)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;谈薪谈的是什么？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;薪资中的固定部分：基本月薪如何发放？有没有拆分？如果有，不算绩效、补贴、提成的话，写在合同里的基本月薪是多少？&lt;/li&gt;
&lt;li&gt;薪资中的浮动部分：绩效奖金、提成的发放标准是什么？计算逻辑是怎样的？公司有百分之几的人能够拿到全额奖金？有没有机会拿到超额奖金？&lt;/li&gt;
&lt;li&gt;几险几金？缴纳基数是多少？缴纳比例是多少？有没有额外的商业保险？&lt;/li&gt;
&lt;li&gt;其他福利待遇：有没有餐补、房补、交通补、出差补、医疗补、生日福利、节日福利等等？&lt;/li&gt;
&lt;li&gt;公司的考勤制度是怎样的？每天工作几个小时？是不是双休？有没有全勤奖金？请假扣不扣钱？有没有带薪休假？年假不休的话能否折算成奖金发放？&lt;/li&gt;
&lt;li&gt;有无签字费？签字费怎么发放？合约期是多少？发生违约该怎么办？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;谈薪避坑&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;非常不建议说“自己找工作主要是积累经验，更看重机会，薪资不是那么重要”。这点非常致命，这很明显给了 HR 后续压你薪资的借口，属于是自己给自己画饼了&lt;/li&gt;
&lt;li&gt;不要直接给出你的心理预期薪资，可以先反问对方“请问公司给咱们这个岗位的预算是多少”或“咱们的薪资结构大概是怎样一个构成呢”
&lt;ul&gt;
&lt;li&gt;月 base 多少？发多少个月？&lt;/li&gt;
&lt;li&gt;多少个月是年终奖？&lt;/li&gt;
&lt;li&gt;年终奖的计算逻辑是怎样的？&lt;/li&gt;
&lt;li&gt;有没有绩效津贴？&lt;/li&gt;
&lt;li&gt;薪酬的支付周期和具体到账时间？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不要用城市生活成本高、压力大之类的话术要求更高工资，因为公司给你更高薪资取决于你的能力而非生活成本&lt;/li&gt;
&lt;li&gt;不要直接说出自己期望薪资的底线，往往会造成薪资低于这个数。可以先不说具体数字，先和 HR 沟通公司的薪资结构，福利待遇等，最后稍加思考后，适当抬高你的期望薪资&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;谈薪必问合集&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;薪资结构一定要了解清楚&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;固薪发几个月？几月发？&lt;/li&gt;
&lt;li&gt;年终奖范围？几月发？&lt;/li&gt;
&lt;li&gt;月薪包含绩效吗？&lt;/li&gt;
&lt;li&gt;社保公积金缴纳比例？&lt;/li&gt;
&lt;li&gt;工资以外的福利？&lt;/li&gt;
&lt;li&gt;公司加班是发加班费还是调休呢？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;谈薪模板&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ 万能报价语句：&lt;strong&gt;准确报数字 + 缓和气氛 + 综合考虑&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;🔥 参考薪资公式：&lt;strong&gt;目标薪资 + 15% 报价&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;HR 一定会压价，所以需要还价&lt;/p&gt;
&lt;p&gt;✅ 万能还价语句：&lt;strong&gt;理解 + 阐述优势 + 加入意愿 + 再次报价 + 请求协助&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如说&lt;strong&gt;你的底线薪资是 20K，市场平均薪资是 25K，你的目标薪资是 27K&lt;/strong&gt;，就直接说：&lt;/p&gt;
&lt;p&gt;1️⃣ 我的期望薪资是月薪 31K，我之前做了一些市场调查，还有我在这个岗位师兄师姐进行了解和反馈，这个薪资我觉得是一个市场的平均值；&lt;/p&gt;
&lt;p&gt;2️⃣ 回归到胜任能力上，同时在前面几轮的面试中，我相信面试官你们也是充分了解了我的能力；而这个岗位所需要的 XX、XX 能力，在我过往的经历中是可以体现的，因此这个岗位我是完全能够胜任的；&lt;/p&gt;
&lt;p&gt;3️⃣ 另外我手上也有其它公司的面试机会 / offer，但在此前面试中我也感受到贵公司的企业氛围/福利/文化是我比较喜欢的，所以在同等情况前提下我更愿意来贵公司；&lt;/p&gt;
&lt;p&gt;4️⃣ 同时因为我从学校出来要考虑到租房、出行等费用，综合考虑下我觉得这个薪资是比较合理的。&lt;/p&gt;
&lt;p&gt;5️⃣ 希望 HR 您能帮我争取一下！&lt;/p&gt;
&lt;h3&gt;如何 argue&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;寒暄环节：客套一下，感谢公司发 offer，快速切入正题；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心模板：先表达你来该公司的强烈意愿，然后表示你拿了什么什么厂的什么价位的 offer，白菜就别说了，如果 sp 可以说，然后表示如果能在这个价位基础之上加多少钱就更合适了，自己未来还是想来这边发展的，希望这边可以帮忙申请一下。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;最后等待结果，并再次感谢对方给与这次机会。&lt;/li&gt;
&lt;li&gt;一般来说，如果第一次没有 argue 下来，又特别想试试，可以二次 argue，但不建议事过三。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;当然以上的方式有点强硬，对于没有高薪 offer 的同学也想得到稍微比较高的薪资的 offer，态度可以换一种。&lt;/p&gt;
&lt;p&gt;沟通的时候依旧去表达一下自己的期望薪资，也许期望薪资会在 hr 给你开出具体薪资之后才说，并且这两者之间有一定的出入，那么可以尝试性的沟通一下，我这边对薪资也有了一定的了解，结合我个人能力来评估，希望能够你在开出的薪资上面提升一些，跟我的预期更符合一些，同时我也有一些其他的 offer 的在手上，他们有些开了薪资，有些会在之后谈薪资，但是如果贵公司能够达到我的期望值，我会立马下定决心去这里。&lt;/p&gt;
&lt;p&gt;当然特别需要注意自己的语气和行为方式，所谓知己知彼方能百战不殆，之前也出现过 argue offer 将 offer 弄没了的，也有些公司如果需要 argue sp offer 需要进行加面，不成功也许 offer 就没了，去年身边有同学就在加面中挂了，血的教训所以对于那些你比较想去的公司，需要合理的去评估自己的实力。现在大家都会有一种心态，就是比较。在没拿到公司的 offer 之前，特别想通过面试，拿到 offer，拿到 offer 之后，发现身边的同学薪资都比较高，心理多少会出现不平衡的状态，然后可能就会导致在和 hr 谈薪资的过程中会犯错误，没有合理的去评估实力就进行 argue，如果只是 argue 失败，保持一开始的薪资还好，如果被收回 offer 就追悔莫及。&lt;/p&gt;
&lt;p&gt;另外，脱离公司、岗位、部门比较薪资本身就是一种错误的行为，例如近两年火热的算法岗，一部分公司区别对待，算法和开发工资有点差别，有些公司就是无差别对待。&lt;/p&gt;
&lt;p&gt;另外要注意 offer 提供的地点。举个例子，小米在北京和武汉开出的薪资就是不一致的，可千万别看见 offershow 北京的工资去和公司谈武汉的薪资，因为一线城市和二线城市消费水平本身就有一定的差距，二线城市对于许多人来说，离家近，所以也一定程度上牺牲了薪资。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;argue 薪资有可能需要出示证明，所以没有备用 offer 原则上不建议编造&lt;/li&gt;
&lt;li&gt;通过 offershow 提前知道公司开出的 offer 薪资大致的区间，同时也给自己制定一个期望薪资&lt;/li&gt;
&lt;li&gt;需要有同级别公司的 offer（白菜、sp、ssp）才能去 argue 大公司的 offer。例如百度的签字费去年是针对拿到字节、阿里、腾讯的人可以 argue&lt;/li&gt;
&lt;li&gt;有些公司 sp、白菜是在面试的时候就已经确定了的，这个是根据面试表现来给，argue 的话更多的是要签字费&lt;/li&gt;
&lt;li&gt;阿里（杭州）第一年对于应届生有 10w 补贴，所以在拿阿里 offer 去和别的公司 argue，自己也可以算上&lt;/li&gt;
&lt;li&gt;对于不那么想去的公司，argue 可以强硬一点，对于特别想去的公司，如果薪资还满意的话，符合自己的实力，不建议盲目的上去 argue&lt;/li&gt;
&lt;li&gt;部分公司可以在谈完薪资之后继续谈，如果不成，大不了不去&lt;/li&gt;
&lt;li&gt;如果想留多个 offer 进行比较，还是那句话，argue 要注意分寸，不要把原来的 offer 给 argue 没了&lt;/li&gt;
&lt;li&gt;有些公司薪资是可以谈的（比如美团），有些公司的薪资对于大部分的人来说谈不了（比如字节），所以了解清楚你要去谈薪资的公司是否可以谈&lt;/li&gt;
&lt;li&gt;argue 有风险、合理评估实力再决定&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;毁约或拒绝 offer 话术&lt;/h2&gt;
&lt;p&gt;什么时候放弃 offer，一般来说有两个阶段（意向书阶段 or 谈薪阶段）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个是公司发下来意向书的时候，在意向书的末尾都需要点击链接确认或者放弃意向书，这个时候点击放弃，或者等待意向书有效期过自动放弃，又或者要求回复该意向书邮件，这时候回复邮件里就表明放弃就可以了。不过这样的话无论你是点击放弃还是过了有效期自动放弃，hr 都会打一个电话过来向你了解情况，防止你忘了或者误操作之类的。这个时候一般都会问你为啥放弃这个 offer 呢，你就说你的理由就可以了，不过需要注意的是这个时候一般还没有谈薪，你不知道你将要放弃的公司会开给你多少钱，或者你的 dream offer 会开给你多少钱。&lt;/li&gt;
&lt;li&gt;第二个阶段就是 hr 打电话来谈薪的时候，谈完薪资，就基本上一切都确定了，你再考虑是不是要放弃，这个时候可以直接在电话里头跟 hr 说你要拒绝。当然还有一种委婉的方式是在 hr 给你的考虑期限内，给 hr 发一封邮件拒绝。 如果拒绝的很早，也是可以的，但是一般不建议在口头 offer 或者意向书之后就拒绝，因为你还没有薪资信息给你参考，这个时候就拒绝的话难免有考虑不周的可能。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;开头感谢： 对方的机会、认可和时间付出&lt;/p&gt;
&lt;p&gt;表明态度： 说明无法接受 offer，态度诚恳且明确&lt;/p&gt;
&lt;p&gt;简述原因： 原因简洁，避免负面评价（如职业规划调整、家庭原因等）&lt;/p&gt;
&lt;p&gt;表达歉意： 对浪费的时间与资源表示歉意&lt;/p&gt;
&lt;p&gt;表达期望： 祝福对方，并表达未来合作的可能性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;电话/微信/短信&lt;/h3&gt;
&lt;p&gt;如果是打电话或者发微信，发短信的话，你可以说:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“很感谢你们给我的宝贵机会，我已经有更好的 offer 了，所以暂时不考虑其他 offer，希望将来有机会再合作，非常抱歉给贵公司的招聘工作带来不便，祝你们招聘顺利！”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;或者：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“非常感谢你们对我的认可，但是我现在已经拿到了更合适我的 offer，综合考虑之后，我认为这个 offer 跟我的职业规划并不是十分匹配（适用于不喜欢这个岗位，这个 base 的情况）/我认为贵公司的薪资不符合我的预期（适用于薪资太低的情况），因此我决定放弃这个 offer，很抱歉给贵公司的招聘工作带来的不便，祝你们招聘顺利！希望将来有机会再合作！”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;或者：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“面试官您好，了解完贵团队的相关业务之后，我觉得我还是不太适合这个方面的开发工作，所以我打算放弃这个机会，感谢贵公司给我的宝贵机会（这个是我在面完某公司二面之后，面试官觉得这个职位跟我的背景不是很符合，加微信让我考虑考虑，之后我给面试官发的微信消息）”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;或者：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“面试官您好，我已经慎重考虑过了，综合 XX 的价值观和 base 薪资等因素，我觉得目前来说 XX 公司不是很适合我。非常感谢 XX 公司对我的认可，希望以后有机会再合作。”（这是我面试时加了 XX 公司面试官的微信，谈完薪后在微信上把决定告诉面试官，由面试官传达 hr 的）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;或者：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“面试官您好，很抱歉这么晚来打扰您。我很认真的思考过了，因为我目前已经拿到了十分合适我的 offer 啦，因此并不打算考虑其他机会，也不想浪费面试官们宝贵的时间。XXXX 我有一点了解，您的部门有很光明的前景和潜力，但是我个人觉得并不是很适合这个方向，所以还是不打算继续接下来的流程啦。”（这是我已经拿到 Y 公司的意向书后，XX 公司的面试官加我微信邀请我面试，我给面试官发的拒绝消息，仅供不打算继续面试流程的同学们参考）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;邮件&lt;/h3&gt;
&lt;p&gt;而如果你是发邮件拒绝的话，可以这么说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;尊敬的 TT 公司 hr 您好！我是 YYYY 年校园招聘提前批中获得贵公司 offer 的候选人 XXX。
非常感谢 TT 公司给我的 offer，由于本人获得了另外更适合我的 offer，根据自己的职业期望与规划，思考良久，认为目前 TT 公司不是十分符合自己的专业方向与职业规划，故决定放弃贵公司的录用邀请。祝愿贵公司能找到更加适合这个职位的人选，我很抱歉给贵公司的工作带来的不便，若今后有机会也非常希望能再次合作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;三方与毁三方流程&lt;/h2&gt;
&lt;p&gt;三方实际就是毕业生就业协议。其实是全国普通高等学校就业协议书，哪三方呢，有&lt;strong&gt;毕业生，用人单位和学校&lt;/strong&gt;。能够解决应届毕业生的一些福利待遇等，具体根据双方谈的条件来定，正常来讲会有五险一金，有的公司涉及档案，户籍等等，等到正式入职之后这个三方就自动终止了，说白了对我们就没有用了。&lt;/p&gt;
&lt;p&gt;毁三方的流程：如果不是因为特别的原因一般不要毁三方，因为一个是时间会拖的很长，一个是需要违约金，因为我也没有毁过三方，所以这里引用牛客帖子「&lt;a href=&quot;https://www.nowcoder.com/discuss/144015&quot;&gt;offer、三方、两方、毁约，这些你需要知道的事&lt;/a&gt;」中关于毁三方的详细流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;与原单位协商，向原单位接收违约，按照三方协议规定，交纳违约金（有些单位不收违约金），从原单位开出退函。&lt;/li&gt;
&lt;li&gt;从新单位获取接收函。&lt;/li&gt;
&lt;li&gt;拿着原单位退函和新单位接收函到就业指导中心领新三方（有时也不需要接收函）。&lt;/li&gt;
&lt;li&gt;拿新三方与新单位签约。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个过程中，关键在于第一步：如何与原单位协商，拿到退函。具体的情况，不同单位不一样，有的单位可能会拖很久。所以，如果新单位的签约时间很紧，而原单位又不会很快给你开退函的话，那结果很可能是你两家单位都签不了。&lt;/p&gt;</content:encoded><h:img src="/_astro/20250823-ma1Nen.CQmUQFOx.png"/><enclosure url="/_astro/20250823-ma1Nen.CQmUQFOx.png"/></item><item><title>技术面反问环节与 HR 面常见套路与反问</title><link>https://coooredump.github.io/blog/recruitment/reverse-interview</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/reverse-interview</guid><description>对于招聘方而言，清晰、有条理的提问不仅会提高你在面试人员心中的好感度，也能让面试方觉得你对他们感兴趣。如果什么也不问，等于你就错失了一次通过内部渠道熟悉公司的好机会，还可能会跳进一些不知道的坑。</description><pubDate>Sun, 24 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文主要罗列一下&lt;strong&gt;技术面/主管面的反问环节参考问题&lt;/strong&gt;，以及 &lt;strong&gt;HR 面的常见问题&lt;/strong&gt;和&lt;strong&gt;反问参考&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先，作为求职者而言，要清楚面试是一个双向选择的过程，刚刚被面试官一通提问过后，权力的棒子就交回你手中了。所以在这个环节，要基于你自身实际的需求，充分挖掘这家公司你在意的点。&lt;strong&gt;如果什么也不问，等于你就错失了一次通过内部渠道熟悉公司的好机会，还可能会跳进一些不知道的坑&lt;/strong&gt;。对于招聘方而言，清晰、有条理的提问不仅会提高你在面试人员心中的好感度，也能让面试方觉得你对他们感兴趣、是真的想来他们公司，而不是把公司当做自己池子里的鱼。&lt;/p&gt;
&lt;p&gt;下面我将按照不同轮次的面试去分类，分别介绍一下反问环节的技巧。注意面试之前要还是要通过各种信息途径对公司去有一个基本的了解，所谓知己知彼者，百战不殆。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;预期使用方式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;检查一下哪些问题你感兴趣&lt;/li&gt;
&lt;li&gt;检查一下哪些是你可以自己在网上找到答案&lt;/li&gt;
&lt;li&gt;找不到的话就向面试官提问&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;绝对不要想把这个列表里的每个问题都问一遍（尊重面试官的时间）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 注意：无论哪一轮的面试，一定要做到不卑不亢，不能怯场，特别是 HR 面。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;HR 面一定要做到：&lt;strong&gt;自信！自信！还是 tmd 自信&lt;/strong&gt;！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HR 不懂技术，这时候可以狠狠地吹。&lt;/p&gt;
&lt;p&gt;聊天时言语中可以带有真诚的语气，但回答的内容不要过于老实。&lt;/p&gt;
&lt;p&gt;正所谓真诚是必杀技，但是只有真诚那是杀必。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;个人推荐的问题 🙋&lt;/h2&gt;
&lt;h3&gt;技术面 / 主管面&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;组内的业务与技术栈？&lt;/li&gt;
&lt;li&gt;可以再详细介绍一下这个岗位的具体职责吗？&lt;/li&gt;
&lt;li&gt;组内的 Code Review 是怎么做的？&lt;/li&gt;
&lt;li&gt;公司/部门对于实习生/校招生有没有技术栈上的要求？计算机基础、项目经历和编程能力怎么考量？&lt;/li&gt;
&lt;li&gt;您能介绍一下你们部门在整个业务环节中扮演的角色吗？（&lt;strong&gt;用于试探是不是边缘部门&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;工作节奏是怎样的呢/是否加班？（&lt;strong&gt;这个问题我觉得问一线员工比问 HR 靠谱&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;你们部门的工作是预研还是解决现有需求？&lt;/li&gt;
&lt;li&gt;对接人/乙方/客户主要是谁？&lt;/li&gt;
&lt;li&gt;生产环境发生事故了怎么办？&lt;/li&gt;
&lt;li&gt;新员工入职的培训流程/培养机制？&lt;/li&gt;
&lt;li&gt;团队总共有多少人？&lt;/li&gt;
&lt;li&gt;部门未来会逐渐拓展新的业务吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;HR 面&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;按以下 3 个模块顺序问，因为薪资在后续还会再提及，如果 hr 没问预期薪资，则不需要/不该（在 hr 面）主动咨询具体给到的薪资范围，但是一定要问问薪资结构的组成、福利等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;个人发展&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;培养机制？晋升渠道？涨薪途径？&lt;/li&gt;
&lt;li&gt;每年几月份调薪？调薪幅度是？&lt;/li&gt;
&lt;li&gt;试用期多久？转正要求是什么？试用期薪资打折吗？&lt;/li&gt;
&lt;li&gt;岗位稳定性如何、是否会裁员？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;休假&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工作节奏如何？午休多久？双休吗？&lt;/li&gt;
&lt;li&gt;年假有多久？带薪休假时间有多久？&lt;/li&gt;
&lt;li&gt;病假和事假是分开的还是一起算？&lt;/li&gt;
&lt;li&gt;加班是调休还是双倍薪资呢？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;薪资福利&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;薪资结构是怎样的？&lt;/li&gt;
&lt;li&gt;年终奖情况与绩效评定？&lt;/li&gt;
&lt;li&gt;有五险一金或者其他退休养老金等福利吗？&lt;/li&gt;
&lt;li&gt;公积金按多少比例缴纳？&lt;/li&gt;
&lt;li&gt;公司是否有食堂，是否提供餐补/房补/交通补贴？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;技术/主管面【反问环节】&lt;/h2&gt;
&lt;p&gt;该轮面试的特点是面试方对具体的工作业务流程都很熟悉，所以可以问以下内容：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;工作内容：&lt;strong&gt;主要针对技术面&lt;/strong&gt;，包括不限于岗位职责、具体工作形式等，可结合自己对部门的了解去问，越详细越好，最好能接着面试官的话题去问，他们会认为你对这份工作有着深入的理解，也方便你判断自己对岗位是否感兴趣、契合度高不高。常见提问形式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以再详细介绍一下这个岗位的具体职责吗？&lt;/li&gt;
&lt;li&gt;我们部门的工作是预研还是解决现有需求？&lt;/li&gt;
&lt;li&gt;对接人/乙方/客户主要是谁？&lt;/li&gt;
&lt;li&gt;工作时间如何/是否加班？（&lt;strong&gt;这个问题我觉得问一线员工比问 HR 靠谱&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;公司层面：&lt;strong&gt;主要针对主管面&lt;/strong&gt;，相比于基层员工，主管对于业务和整个公司战略的层面有更深入的理解。常见提问形式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;您能介绍一下我们部门在整个业务环节中扮演的角色吗？（&lt;strong&gt;用于试探是不是边缘部门&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;公司/部门的氛围如何？工作节奏怎么样？&lt;/li&gt;
&lt;li&gt;新员工入职的培训流程/培养机制？&lt;/li&gt;
&lt;li&gt;团队总共有多少人？&lt;/li&gt;
&lt;li&gt;会逐渐拓展新的业务吗？&lt;/li&gt;
&lt;li&gt;行业未来前景如何？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;其它也可以接着技术面觉得没问充分的点继续深入，下列是所有反问合集，仅供参考。&lt;/p&gt;
&lt;h3&gt;职责&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;On-call （电话值班）的计划或者规定是什么？值班或者遇到问题加班时候有加班费吗？&lt;/li&gt;
&lt;li&gt;我的日常工作是什么？&lt;/li&gt;
&lt;li&gt;有给我设定的特定目标吗？&lt;/li&gt;
&lt;li&gt;团队里面初级和高级工程师的比例是多少？（有计划改变吗）&lt;/li&gt;
&lt;li&gt;入职培训 (onboarding) 会是什么样的？&lt;/li&gt;
&lt;li&gt;每个开发者有多大的自由来做出决定？&lt;/li&gt;
&lt;li&gt;在你看来，这个工作做到什么程度算成功？&lt;/li&gt;
&lt;li&gt;你期望我在最初的一个月 / 三个月能够完成什么？&lt;/li&gt;
&lt;li&gt;试用期结束的时候，你会怎么样衡量我的绩效？&lt;/li&gt;
&lt;li&gt;自己单独的开发活动和按部就班工作的比例大概是怎样的？&lt;/li&gt;
&lt;li&gt;一个典型的一天或者一周的工作是怎样安排的？&lt;/li&gt;
&lt;li&gt;对我的申请你有什么疑虑么？&lt;/li&gt;
&lt;li&gt;在这份工作上，我将会和谁紧密合作？&lt;/li&gt;
&lt;li&gt;我的直接上级他们的上级都是什么样的管理风格？（事无巨细还是着眼宏观）&lt;/li&gt;
&lt;li&gt;我在这个岗位上应该如何发展？会有哪些机会？&lt;/li&gt;
&lt;li&gt;每天预期 / 核心工作时间是多少小时？&lt;/li&gt;
&lt;li&gt;我入职的岗位是新增还是接替之前离职的同事？（是否有技术债需要还）？&lt;/li&gt;
&lt;li&gt;入职之后在哪个项目组，项目是新成立还是已有的？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;技术&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;公司常用的技术栈是什么？&lt;/li&gt;
&lt;li&gt;你们怎么使用源码控制系统？&lt;/li&gt;
&lt;li&gt;你们怎么测试代码？&lt;/li&gt;
&lt;li&gt;你们怎么追踪 bug？&lt;/li&gt;
&lt;li&gt;你们怎样监控项目？&lt;/li&gt;
&lt;li&gt;你们怎么集成和部署代码改动？是使用持续集成和持续部署吗 (CI/CD)？&lt;/li&gt;
&lt;li&gt;你们的基础设施搭建在版本管理系统里吗？或者是代码化的吗？&lt;/li&gt;
&lt;li&gt;从计划到完成一项任务的工作流是什么样的？&lt;/li&gt;
&lt;li&gt;你们如何准备故障恢复？&lt;/li&gt;
&lt;li&gt;有标准的开发环境吗？是强制的吗？&lt;/li&gt;
&lt;li&gt;你们需要花费多长时间来给产品搭建一个本地测试环境？（分钟 / 小时 / 天）&lt;/li&gt;
&lt;li&gt;你们需要花费多长时间来响应代码或者依赖中的安全问题？&lt;/li&gt;
&lt;li&gt;所有的开发者都可以使用他们电脑的本地管理员权限吗？&lt;/li&gt;
&lt;li&gt;介绍一下你们的技术原则或者展望。&lt;/li&gt;
&lt;li&gt;你们的代码有开发文档吗？有没有单独的供消费者阅读的文档？&lt;/li&gt;
&lt;li&gt;你们有更高层次的文档吗？比如说 ER 图，数据库范式&lt;/li&gt;
&lt;li&gt;你们使用静态代码分析吗？&lt;/li&gt;
&lt;li&gt;你们如何管理内部和外部的数字资产？&lt;/li&gt;
&lt;li&gt;你们如何管理依赖？&lt;/li&gt;
&lt;li&gt;公司是否有技术分享交流活动？有的话，多久一次呢？&lt;/li&gt;
&lt;li&gt;你们的数据库是怎么进行版本控制的？&lt;/li&gt;
&lt;li&gt;业务需求有没有文档记录？是如何记录的？&lt;/li&gt;
&lt;li&gt;你们是如何面对和解决技术债的？是否有专门的时间或者预算用于重构？&lt;/li&gt;
&lt;li&gt;你们如何进行单元测试呢，是否都有单元测试的习惯？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;团队&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;工作是怎么组织的？&lt;/li&gt;
&lt;li&gt;团队内 / 团队间的交流通常是怎样的？&lt;/li&gt;
&lt;li&gt;你们使用什么工具来做项目组织？你的实际体会是什么？&lt;/li&gt;
&lt;li&gt;如果遇到不同的意见怎样处理？&lt;/li&gt;
&lt;li&gt;谁来设定优先级 / 计划？&lt;/li&gt;
&lt;li&gt;如果团队没能赶上预期发布日期怎么办？&lt;/li&gt;
&lt;li&gt;每周都会开什么类型的会议？&lt;/li&gt;
&lt;li&gt;会有定期的和上级的一对一谈话吗？&lt;/li&gt;
&lt;li&gt;产品 / 服务的规划是什么样的？（n 周一发布 / 持续部署 / 多个发布流 / ...）&lt;/li&gt;
&lt;li&gt;生产环境发生事故了怎么办？是否有不批评人而分析问题的文化？&lt;/li&gt;
&lt;li&gt;有没有一些团队正在经历还尚待解决的挑战？&lt;/li&gt;
&lt;li&gt;你们如何跟踪进度？&lt;/li&gt;
&lt;li&gt;预期和目标是如何设定的？谁来设定？&lt;/li&gt;
&lt;li&gt;Code Review 如何实施？&lt;/li&gt;
&lt;li&gt;给我介绍下团队里一个典型的 sprint&lt;/li&gt;
&lt;li&gt;你们如何平衡技术和商业目标？&lt;/li&gt;
&lt;li&gt;你们如何共享知识？&lt;/li&gt;
&lt;li&gt;团队有多大？&lt;/li&gt;
&lt;li&gt;公司技术团队的架构和人员组成？&lt;/li&gt;
&lt;li&gt;团队内开发、产品、运营哪一方是需求的主要提出方？哪一方更强势？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;问未来的同事&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;开发者倾向于从哪里学习？&lt;/li&gt;
&lt;li&gt;你对在这里工作最满意的地方是？&lt;/li&gt;
&lt;li&gt;最不满意的呢？&lt;/li&gt;
&lt;li&gt;如果可以的话，你想改变哪里？&lt;/li&gt;
&lt;li&gt;团队最老的成员在这里多久了？&lt;/li&gt;
&lt;li&gt;在小团队中，有没有出现成员性格互相冲突的情况？最后是如何解决的？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;公司&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;公司为什么在招人？（产品发展 / 新产品 / 波动...）&lt;/li&gt;
&lt;li&gt;有没有会议 / 旅行预算？使用的规定是什么？&lt;/li&gt;
&lt;li&gt;晋升流程是怎样的？要求 / 预期是怎样沟通的？&lt;/li&gt;
&lt;li&gt;绩效评估流程是怎样的？&lt;/li&gt;
&lt;li&gt;技术和管理两条职业路径是分开的吗？&lt;/li&gt;
&lt;li&gt;对于多元化招聘的现状或者观点是什么？&lt;/li&gt;
&lt;li&gt;有公司级别的学习资源吗？比如电子书订阅或者在线课程？&lt;/li&gt;
&lt;li&gt;有获取证书的预算吗？&lt;/li&gt;
&lt;li&gt;公司的成熟度如何？（早期寻找方向 / 有内容的工作 / 维护中 / ...）&lt;/li&gt;
&lt;li&gt;我可以为开源项目做贡献吗？是否需要审批？&lt;/li&gt;
&lt;li&gt;你认为公司未来五年或者十年会发展成什么样子？&lt;/li&gt;
&lt;li&gt;公司的大多数员工是如何看待整洁代码的？&lt;/li&gt;
&lt;li&gt;你上次注意到有人成长是什么时候？他们在哪方面成长了？&lt;/li&gt;
&lt;li&gt;在这里成功的定义是什么？如何衡量成功？&lt;/li&gt;
&lt;li&gt;有体育活动或者团建么？&lt;/li&gt;
&lt;li&gt;有内部的黑客马拉松活动吗？&lt;/li&gt;
&lt;li&gt;公司支持开源项目吗？&lt;/li&gt;
&lt;li&gt;有竞业限制或者保密协议需要签吗？&lt;/li&gt;
&lt;li&gt;你们认为公司文化中的空白是什么？&lt;/li&gt;
&lt;li&gt;能够跟我说一公司处于不良情况，以及如何处理的故事吗？&lt;/li&gt;
&lt;li&gt;您在这工作了多久了？您觉得体验如何？&lt;/li&gt;
&lt;li&gt;大家为什么会喜欢这里？&lt;/li&gt;
&lt;li&gt;公司的调薪制度是如何的？&lt;/li&gt;
&lt;li&gt;公司有没有申请调岗的制度？&lt;/li&gt;
&lt;li&gt;公司对于员工的心理健康和福祉有什么具体措施？&lt;/li&gt;
&lt;li&gt;你对在这里工作最满意的地方是？你为什么留在这家公司？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;社会问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;你们关于多元化招聘什么看法？&lt;/li&gt;
&lt;li&gt;你们的公司文化如何？你认为有什么空白么？&lt;/li&gt;
&lt;li&gt;这里的工作生活平衡地怎么样？&lt;/li&gt;
&lt;li&gt;公司对气候变化有什么态度吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;冲突&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;不同的意见如何处理？&lt;/li&gt;
&lt;li&gt;如果被退回了会怎样？（“这个在预计的时间内做不完”）&lt;/li&gt;
&lt;li&gt;当团队有压力并且在超负荷工作的时候怎么处理？&lt;/li&gt;
&lt;li&gt;如果有人注意到了在流程或者技术等其他方面又改进的地方，怎么办？&lt;/li&gt;
&lt;li&gt;当管理层的预期和工程师的绩效之间有差距的时候如何处理？&lt;/li&gt;
&lt;li&gt;能给我讲一个公司深处有毒环境以及如何处理的故事吗？&lt;/li&gt;
&lt;li&gt;如果在公司内你的同事因侵犯他人而被调查，请问你会如何处理？&lt;/li&gt;
&lt;li&gt;假设我自己很不幸是在公司内的受害者，在公司内部有没有争取合法权益的渠道？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;商业&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;你们现在盈利吗？&lt;/li&gt;
&lt;li&gt;如果没有的话，还需要多久？&lt;/li&gt;
&lt;li&gt;如果有的话，年度营业额是大概有多少？（我现在的公司年度营业额是 5 亿）&lt;/li&gt;
&lt;li&gt;公司的资金来源是什么？谁影响或者制定高层计划或方向？&lt;/li&gt;
&lt;li&gt;你们如何挣钱？&lt;/li&gt;
&lt;li&gt;什么阻止了你们挣更多的钱？&lt;/li&gt;
&lt;li&gt;公司未来一年的增长计划怎样？五年呢？&lt;/li&gt;
&lt;li&gt;你们认为什么是你们的竞争优势？&lt;/li&gt;
&lt;li&gt;你们的竞争优势是什么？&lt;/li&gt;
&lt;li&gt;公司未来的商业规划是怎样的？有上市的计划吗？&lt;/li&gt;
&lt;li&gt;都在做副业吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;远程工作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;远程工作和办公室工作的比例是多少？&lt;/li&gt;
&lt;li&gt;公司提供硬件吗？更新计划如何？&lt;/li&gt;
&lt;li&gt;使用自己的硬件办公可以吗？现在有政策吗？&lt;/li&gt;
&lt;li&gt;额外的附件和家具可以通过公司购买吗？这方面是否有预算？&lt;/li&gt;
&lt;li&gt;有共享办公或者上网的预算吗？&lt;/li&gt;
&lt;li&gt;多久需要去一次办公室？&lt;/li&gt;
&lt;li&gt;公司的会议室是否一直是视频会议就绪的？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;办公室布局&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;办公室的布局如何？（开放的 / 小隔间 / 独立办公室）&lt;/li&gt;
&lt;li&gt;有没有支持 / 市场 / 或者其他需要大量打电话的团队在我的团队旁边办公？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;终极问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;该职位为何会空缺？&lt;/li&gt;
&lt;li&gt;公司如何保证人才不流失？&lt;/li&gt;
&lt;li&gt;这份工作 / 团队 / 公司最好和最坏的方面是？&lt;/li&gt;
&lt;li&gt;你最开始为什么选择了这家公司？&lt;/li&gt;
&lt;li&gt;你为什么留在这家公司？&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HR 面【常见问题】&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;HR 面之前一定要背调公司以及相关部门，对公司有一个大概的了解。&lt;/p&gt;
&lt;p&gt;还有个大伙常见的疑问：公司 HR 面会刷人吗？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先很多大厂的 HR 权限很大，拥有一票否决权，真的会刷人&lt;/li&gt;
&lt;li&gt;小厂的 HR 来说权限不大，一般不太会刷人&lt;/li&gt;
&lt;li&gt;一般来说，只要 HR 面你表现得像个正常人/人一样，然后自信一点，对公司价值观认可点，问题都不大，很多时候你挂了不一定是 HR 面挂了，可能是 HR 面之后的排序挂了&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1. 你为什么选择我们公司？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;想和优秀的人（前几轮面试官）做有挑战的事（业务与未来发展契合）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;🔥 回答这个问题的前提：提前了解了公司的主营业务、核心产品、行业地位、文化价值观（如官网、新闻稿、社交媒体），仔细阅读 JD，提炼岗位关键词，针对性准备案例。&lt;/p&gt;
&lt;p&gt;高分逻辑：体现对企业的深度了解 + 个人职业规划的契合（有理有据即可，体现你对公司的向往和理解）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;【示例】我注意到贵司近年来在大模型领域的技术投入（如 xxx 项目），这与我的专业背景和长期关注的行业方向高度匹配。同时，我也想和优秀的人做有挑战的事，通过前几轮面试，我感受到了贵公司技术有深度，氛围好，让我坚信在这里能快速成长。&lt;/p&gt;
&lt;h3&gt;2. 职业规划/你对未来有什么规划？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;不要假大空，说做几年然后升架构师...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我的未来规划是，我觉得人应该两条腿走路，一条腿是技术，一条腿是业务。技术是为业务服务的，你脱离了业务，技术就没有意义了；脱离了技术，业务就没法实现了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;业务层面：我了解咱们公司，（举个例子）咱们部门是搞本地生活的，本地生活涉及到了人的衣食住行，至关重要，有一天可能短视频/游戏/xxx 业务都没了，但是人的衣食住行一定是存在的，所以我觉得公司的业务是很有价值的，我希望未来在公司中深挖业务，能够提升自己对整个行业的理解，我之前实习/工作的时候，有个 xxx 需求没解决/报错了，但是半个月都没人发现，为什么，因为当时我们做的是伪需求，我们解决的问题不是痛点问题，我希望我的技术能够为业务赋能，去挖掘用户痛点，解决用户的痛点问题，以此创造价值。&lt;/li&gt;
&lt;li&gt;技术层面：我希望深入了解公司的技术，陪着公司一起成长，...（了解公司的技术的话可以吹一波）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;希望我后面无论是在技术方面还是在业务方面，都能做到在公司独当一面。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这样回答，既有技术又有业务，还有你对公司行业的思考，中间甚至吹了一波公司，比那些假大空的回答好多了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. 你怎么看待加班 / 996，是否接受加班？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;不卑不亢，尽显男儿本色 [doge]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我可以接受加班，尤其是在紧急需求下，工作分内之事我肯定义不容辞，但是这一定是要建立在高效的工作之上，而不是为了加班而加班，所以我会提高自己的工作效率，避免不必要的加班。&lt;/p&gt;
&lt;p&gt;因人而异地追问：想问下贵公司一般都是什么原因导致加班呢？其他小伙伴加班频率怎样？加班有没有调休制度呢？方便我提前做准备。&lt;/p&gt;
&lt;h3&gt;4. 你有什么优点/特点？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 HR 问你有什么特点/优点，请记住一定要说出一个词“靠谱”。&lt;/p&gt;
&lt;p&gt;为什么要一面、二面、HR面，往宽泛了说就是，往往学历越高的人都是越有能力的，面试表现得越好就越有能力，越有能力的人越能干成事儿，能干成事的人他就靠谱，就能放心的把事情交给你。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;参考词汇：靠谱、目标感强、有规划、主动性强、不拖沓、有责任心&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可根据以上几个词举实际场景佐证。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;5. 你有什么缺点？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;选择与岗位无关且可改善的点，避免暴露致命短板。&lt;/p&gt;
&lt;p&gt;⚠️ 别和优点矛盾&lt;/p&gt;
&lt;p&gt;下列举几个例子&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1️⃣ 我有时候会对细节过于苛求，希望把事情做得尽善尽美，这可能导致在项目初期花费较多时间进行打磨。但我已经意识到，在追求质量的同时也需要兼顾效率，完成比完美来得重要。后来我会在项目开始前设定清晰的时间节点和优先级标准，确保在关键细节上投入主要精力，同时不影响整体的进度。这个方法让我既能保证工作输出质量，又能按时完成任务。&lt;/p&gt;
&lt;p&gt;2️⃣ 我过去在大型会议或面对不熟悉的群体做 presentation 时容易紧张，不过我们实验室每周会固定开组会分享论文，我都会主动在组会上争取更多的分享机会来锻炼自己，挑战自己的软肋，因为我清楚这对于职业发展很重要。导师也会带我们出去各种大型会议参加活动和做 presentation，现在我已经能更自信、有条理地表达自己的观点了，虽然还存在不足，但是已经有所进步了。&lt;/p&gt;
&lt;p&gt;3️⃣ 在读研的期间，我对很多新任务都充满热情，所以有时会不自觉地承接过多任务，导致自己的核心项目受到影响。后来我学会了更高效地进行优先级管理，并且定期和我的导师对齐工作重点，确保我的时间精力都投入在最重要的事情上。这不仅提高了我的个人效率，也保证了主线和所有支线的平稳推进。&lt;/p&gt;
&lt;p&gt;4️⃣ 我的缺点是缺乏自信，...&lt;/p&gt;
&lt;p&gt;5️⃣ 我是个急性子，有时候没有特别想清楚就开始干了，可能会导致过程中和团队同事有一些往复的讨论和确认，耽误时间，造成团队同事的困惑。&lt;/p&gt;
&lt;h3&gt;6. 目前手上有没有其他 offer？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;无论什么面试，一定要扬长避短，当 HR 询问你是否手头有其他 offer 时，回答这个问题需要既展示你的市场竞争力，也要表现出对当前面试公司的高度兴趣。&lt;/p&gt;
&lt;p&gt;你有其他公司的 offer 说明你能力强、价值高、比较抢手，但同时也说明了你没那么稳定，你有可能去其他公司。候选人的稳定性是非常重要的，你有其他 offer，价值上去了，稳定性就下来了。&lt;/p&gt;
&lt;p&gt;⚠️ 如果没有 offer，HR 面可以说有 offer，但是谈薪阶段不能编造 offer，有些需要提供证明才能 argue。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;个人战略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于「小厂」就说没有 offer 或者提及几个体量差不多的公司；&lt;/li&gt;
&lt;li&gt;对于大厂则必须说有 offer，用于 argue 和体现自己价值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ &lt;strong&gt;有 offer 的话术&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1️⃣ 现在 xxx 公司给我发了 offer，但是我觉得你们公司很不错，业务前景很好，（此处省略一段，这里可以吹一波公司的业务），但是他那边已经发了 offer，如果说你们这边能够尽快发 offer 的话，我收到 offer 之后我就把那边拒了。&lt;/li&gt;
&lt;li&gt;2️⃣ 目前我确实收到了几个公司的 offer。这些公司虽然各有千秋，但我发现它们与我的职业发展规划并不完全吻合。相比之下，贵公司的职位更符合我的长远职业目标。我对贵公司的发展潜能非常感兴趣，因此我非常期待能有机会加入您的团队。&lt;/li&gt;
&lt;li&gt;3️⃣ 是的，我已经拿到了两个 offer，这些公司与贵公司在行业定位上有不少相似之处。然而，通过今天的了解，无论是从公司的发展前景、职业成长空间，还是面试过程中体验到的公司氛围来看，贵公司都给我留下了更为深刻的印象。我会在您做出决定之后，认真考虑是否接受其他 offer。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;核心是表达出&lt;strong&gt;我有后手有退路，但只要你们给，一定是选你们&lt;/strong&gt;。而且别搞得太生硬，整些什么仰慕公司文化之类的就太假太尬了（除非一些企业文化印象深刻 / 广为流传，比如鹅厂），尽量给一些&lt;strong&gt;软理由&lt;/strong&gt;（信服力比较强）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;xx 公司路程远，贵公司的路程比较方便；&lt;/li&gt;
&lt;li&gt;我男/女朋友要去你们那里发展/拿了那边公司的 offer，我不想异地恋；&lt;/li&gt;
&lt;li&gt;我有个 xx 亲戚在你们那 xx 城市，可以给我点帮助，这样我不用租房；&lt;/li&gt;
&lt;li&gt;看中 xx 城市户口，希望给未来自己孩子 xx 条件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⚠️ 但是如果两家公司差距过大，就不要提了，比如你在面一个小公司，你告诉 HR 说你拿到了字节的 offer，但是我更想去你们这个小公司，别说 HR 不信了，你自己都不相信这是你说的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ &lt;strong&gt;没 offer 的话术&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1️⃣ 我近期才开始积极寻找工作机会，之前参与过两家公司的面试，并且已经进入最后阶段。尽管还未收到正式 offer，但我对贵公司的职位特别感兴趣。我认为贵公司的岗位职责和未来的发展潜力与我的职业规划高度契合。因此，我非常希望有机会加入贵公司的团队。&lt;/li&gt;
&lt;li&gt;2️⃣ 我最近才开始面试，对于求职过程持谨慎态度，并没有广泛投递简历，而是选择性地申请了几家我特别感兴趣的公司。目前有两家公司正在进行中，但尚未接到具体 offer。我对贵公司所在行业的前景感到非常乐观，并认为这是一个能够促进我职业发展的绝佳机会。因此，我非常倾向于加入贵公司。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7. 你的期望薪资是多少？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 并非每一场 HR 面都会问你期望薪资，一般在 10~11 月谈薪。&lt;/p&gt;
&lt;p&gt;参考视频：&lt;a href=&quot;https://www.bilibili.com/video/BV1Gux9eoEwz/?spm_id_from=333.1387.search.video_card.click&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;2025 届秋招谈薪急救指南&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;不要直接给出你的心理预期薪资，可以先反问对方“请问公司给咱们这个岗位的预算是多少”或“咱们的薪资结构大概是怎样一个构成呢”
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;月 base 多少？发多少个月（几薪）？&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;月薪有没有包含绩效？&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;多少个月是年终奖？&lt;/li&gt;
&lt;li&gt;年终奖的计算逻辑是怎样的？&lt;/li&gt;
&lt;li&gt;薪酬的支付周期和具体到账时间？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;非常不建议说“自己找工作主要是积累经验，更看重机会，薪资不是那么重要”，这点非常致命，这很明显给了 HR 后续压你薪资的借口，属于是自己给自己画饼了&lt;/li&gt;
&lt;li&gt;HR 直接问候选人期望薪资是多少，一般有「月薪」或者「总包」两种说法，可以先咨询下 hr 问的是月薪还是年包（如果说都可以，那还是说月 base 吧，年包容易被坑）&lt;/li&gt;
&lt;li&gt;参考往年和今年的整体情况报一下，&lt;strong&gt;给 hr 报的时候不能报具体数字，也不要报上限，建议就报一个下限，比如说我希望公司能给我不低于 50 万的年薪&lt;/strong&gt;，但这个薪资不是最终的&lt;/li&gt;
&lt;li&gt;至于该公司往年市场薪资如何，可以根据「&lt;strong&gt;OfferShow&lt;/strong&gt;」提供的数据，让你对各公司各岗位（各 bg 下）的市场价位有一个大致的了解&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ 回答模板（以月薪为例）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如说&lt;strong&gt;你的底线薪资是 23k，市场平均薪资是 25k，你的目标薪资是 27k&lt;/strong&gt;，就直接说&lt;strong&gt;我的期望薪资是 29k&lt;/strong&gt;。原因如下：&lt;/p&gt;
&lt;p&gt;1️⃣ 我之前做了一些市场调查，还有我在这个岗位师兄师姐进行了解和反馈，这个薪资我觉得是一个市场的平均值；&lt;/p&gt;
&lt;p&gt;2️⃣ 回归到胜任能力上，同时在前面几轮的面试中，我相信面试官你们也是充分了解了我的能力；而这个岗位所需要的 XX、XX 能力，在我过往的经历中是可以体现的，因此这个岗位我是完全能够胜任的；&lt;/p&gt;
&lt;p&gt;3️⃣ 另外我手上也有其它公司的面试机会，但在此前面试中我也感受到贵公司的企业氛围/福利/文化是我比较喜欢的，所以在同等情况前提下我更愿意来贵公司；&lt;/p&gt;
&lt;p&gt;4️⃣ 同时因为我从学校出来要考虑到租房、出行等费用，综合考虑下我觉得这个薪资是比较合理的。&lt;/p&gt;
&lt;p&gt;5️⃣ 希望 HR 您能帮我争取一下！&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;具体谈薪时间（并非 HR 面，而是接收 offer 后（一段时间 / 立刻）到谈薪环节）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;互联网企业大致在 10 月底 / 11 月初高峰统一谈薪，逼签三方&lt;/li&gt;
&lt;li&gt;如果是体制内 / 国企，一般是直接面试完现场谈薪，而且这个薪资基本无法谈（argue）&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8. 你有没有女/男朋友？家庭情况如何？&lt;/h3&gt;
&lt;p&gt;主要是确定你的&lt;strong&gt;稳定性&lt;/strong&gt;，你到底会不会来，知道 HR 的目的后，你就可以根据自己的情况灵活作答。&lt;/p&gt;
&lt;h3&gt;9. 敬请期待&lt;/h3&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;HR 面【反问环节】&lt;/h2&gt;
&lt;p&gt;该轮面试的重要性不用我多说了吧，求职者最关注的薪资待遇等关键信息都会在这轮中体现。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;薪资待遇&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工资及工资构成？&lt;/li&gt;
&lt;li&gt;五险一金基数分别是多少？&lt;/li&gt;
&lt;li&gt;年终奖情况与绩效评定？&lt;/li&gt;
&lt;li&gt;年假？&lt;/li&gt;
&lt;li&gt;食宿/房补餐补？&lt;/li&gt;
&lt;li&gt;落户政策？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;工作方面&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工作时间？工作地点？通勤是否方便？办公环境如何？&lt;/li&gt;
&lt;li&gt;加班 and 出差情况？&lt;/li&gt;
&lt;li&gt;试用期多久？转正要求是什么？试用期工资/年终奖情况？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;个人发展方面&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;培养机制？晋升渠道？涨薪途径？&lt;/li&gt;
&lt;li&gt;往年公司营收如何？&lt;/li&gt;
&lt;li&gt;岗位稳定性如何、是否会裁员？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;🧑🏻‍💻 鉴于裁员是咱应届生都比较关注的一点，直接问会不会裁员可能得到一个模棱两可的答复，可以像这样问：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在网上看到一些关于贵司裁员的消息，请问这些消息是否属实？如果属实，方便告知一下是什么原因吗？毕竟我更希望和贵司成为长期的合作伙伴（这样 HR 就能感受到你的真诚，会给予更多对你有用的信息）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🧑🏻‍💻 另外，对于手头有多个 offer 的大佬，面的这家 HR 对你也很感兴趣的情况下，出于怕你是海投的考虑一般会问为什么没有选择之前的 offer？可以这么回答，得体又能给自己加分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我觉得找工作是人生的一件大事，所以会深思熟虑，如果随便就签约一个不满足自己期望的公司导致后面又走违约流程，这样对公司对自己都是极其不负责任的。遇到合适的机会再签约，也是对招聘方的尊重。如果您觉得我满足您这边的招聘要求，麻烦您给我一些考虑的时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;下列是所有反问合集，仅供参考。&lt;/p&gt;
&lt;h3&gt;待遇&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;如果有奖金计划的话，奖金如何分配？&lt;/li&gt;
&lt;li&gt;如果有奖金计划的话，过去的几年里通常会发百分之多少的奖金？&lt;/li&gt;
&lt;li&gt;有五险一金或者其他退休养老金等福利吗？&lt;/li&gt;
&lt;li&gt;五险一金中，补充公积金一般交多少比例？我可以自己选择这一比例吗？&lt;/li&gt;
&lt;li&gt;有什么医疗保险吗？如果有的话何时开始？&lt;/li&gt;
&lt;li&gt;有额外商业保险吗？例如人寿保险和额外的养老/医疗保险？&lt;/li&gt;
&lt;li&gt;商业保险可以给家人办理吗？成年人/未成年人？&lt;/li&gt;
&lt;li&gt;更换工作地点，公司付费吗？&lt;/li&gt;
&lt;li&gt;是否可以申请更换工作地点？&lt;/li&gt;
&lt;li&gt;是否愿意协助海外应聘者申请工作签证？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;休假&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;带薪休假时间有多久？&lt;/li&gt;
&lt;li&gt;病假和事假是分开的还是一起算？&lt;/li&gt;
&lt;li&gt;我可以提前使用假期时间吗？也就是说应休假期是负的？&lt;/li&gt;
&lt;li&gt;假期的更新策略是什么样的？也就是说未休的假期能否滚入下一周期？&lt;/li&gt;
&lt;li&gt;照顾小孩的政策如何？&lt;/li&gt;
&lt;li&gt;无薪休假政策是什么样的？&lt;/li&gt;
&lt;li&gt;学术性休假政策是怎么样的？&lt;/li&gt;
&lt;li&gt;孕产假政策具体是怎样的？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;福利&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;公司提供 Mac 开发吗？&lt;/li&gt;
&lt;li&gt;使用自带电脑有补贴吗？&lt;/li&gt;
&lt;li&gt;公积金多少比例缴纳？&lt;/li&gt;
&lt;li&gt;公司是否有食堂，是否有餐饮福利补贴？&lt;/li&gt;
&lt;li&gt;是否提供租房补贴？&lt;/li&gt;
&lt;li&gt;是否提供话费补贴？&lt;/li&gt;
&lt;li&gt;是否有交通补贴？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;人才培养&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;升职加薪条件是否量化？&lt;/li&gt;
&lt;li&gt;每年给团队安排多少费用用于学习培训？&lt;/li&gt;
&lt;li&gt;每年组织多少次关于技术能力提升的讲座/论坛？&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/20250810-qYVoqU.B6VfE7pf.png"/><enclosure url="/_astro/20250810-qYVoqU.B6VfE7pf.png"/></item><item><title>八股文 @ 智力题</title><link>https://coooredump.github.io/blog/recruitment/2025-brainteaser</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/2025-brainteaser</guid><description>记录面经高频智力题，实时更新中...</description><pubDate>Sat, 23 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;其实互联网招聘中，有一类型考察是考察你的临场反应速度，比如脑筋急转弯这种智力题或者情景题，比如很知名的&lt;strong&gt;腾讯赛马问题&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;1. 三人三鬼过桥&lt;/h2&gt;
&lt;p&gt;有三个人跟三个鬼要过河，河上没桥只有条小船，然后船一次只能渡一个人和一个鬼，或者两个鬼或者两个人，无论在哪边岸上，只有是人比鬼少的情况下（如两鬼一人，三鬼两人，三鬼一人）人会被鬼吃，然而船又一定需要人或鬼操作才能航行（要有人或鬼划船），问如何安全的把三人三鬼渡过河对岸?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;参考回答&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;先两鬼过去。在一鬼回来。对面有一鬼。这边有三人两鬼。&lt;/li&gt;
&lt;li&gt;再两鬼过去。在一鬼回来。对面有两鬼。这边有三人一鬼。&lt;/li&gt;
&lt;li&gt;再两人过去。一人一鬼回来。对面一人一鬼。这边两人两鬼。&lt;/li&gt;
&lt;li&gt;最后两人过去。一鬼回来。对面三人。这边三鬼。&lt;/li&gt;
&lt;li&gt;剩下的就三个鬼二个过去一个回来在接另外个就OK了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 赛马找最快的马匹（Tencent）&lt;/h2&gt;
&lt;p&gt;一般有这么几种问法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;25 匹马 5 条跑道找最快的 3 匹马，需要跑几次？参考回答：7 次&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;64 匹马 8 条跑道找最快的 4 匹马，需要跑几次？参考回答：11 次&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;25 匹马 5 条跑道找最快的 5 匹马，需要跑几次？参考回答：最少 8 次，最多 9 次&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;建议画图表来看，将问题简单化一点，将大问题化成小问题即可，同时 &lt;a href=&quot;https://www.bilibili.com/video/BV1KJ411g78y&quot;&gt;B 站有个讲解视频&lt;/a&gt;还不错。&lt;/p&gt;
&lt;h3&gt;Q1：25 匹马 5 条跑道找最快的 3 匹马，需要跑几次？&lt;/h3&gt;
&lt;p&gt;将 25 匹马分成 ABCDE 共 5 组，假设每组的排名就是 A1&gt;A2&gt;A3&gt;A4&gt;A5,用边相连，这里比赛 5 次&lt;/p&gt;
&lt;p&gt;第 6 次，每组的第一名进行比赛，可以找出最快的马，这里假设 A1&gt;B1&gt;C1&gt;D1&gt;E1&lt;/p&gt;
&lt;p&gt;D1，E1 肯定进不了前 3，直接排除掉&lt;/p&gt;
&lt;p&gt;第 7 次，B1 C1 A2 B2 A3 比赛，可以找出第二，第三名&lt;/p&gt;
&lt;p&gt;所以最少比赛需要 7 次&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250811-RkfF5q.png&quot; alt=&quot;image-20250811001055887&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Q2：64 匹马 8 条跑道找最快的 4 匹马，需要跑几次？&lt;/h3&gt;
&lt;p&gt;第一步：全部马分为 8 组，每组 8 匹，每组各跑一次，然后淘汰掉每组的后四名，如下图（需要比赛 8 场）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250811-Q8wILt.png&quot; alt=&quot;image-20250811001823846&quot;&gt;&lt;/p&gt;
&lt;p&gt;第二步：取每组第一名进行一次比赛，然后淘汰最后四名所在组的所有马，如下图（需要比赛 1 场）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250811-O9IuUE.png&quot; alt=&quot;image-20250811001947522&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个时候总冠军已经诞生，它就是 A1（它不需要比赛了）。&lt;/p&gt;
&lt;p&gt;而其他可能跑得最快的三匹马只可能是下图中的黄域了（A2，A3，A4，B1，B2，B3，C1，C2，D1，共 9 匹马）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250811-COJIst.png&quot; alt=&quot;image-20250811002042719&quot;&gt;&lt;/p&gt;
&lt;p&gt;第三步：只要从上面的 9 匹马中找出跑得最快的三匹马就可以了，但是现在只要 8 个跑道，那就随机选出 8 匹马进行一次比赛吧（需要比赛一场）&lt;/p&gt;
&lt;p&gt;第四步：上面比赛完，选出了前三名，但是 9 匹马中还有一匹马没跑呢，它可能是一个潜力股啊，那就和前三名比一比吧，这四匹马比一场，选出前三名。最后加上总冠军，跑得最快的四匹马诞生了！&lt;/p&gt;
&lt;p&gt;最后，一共需要比赛的场次：8 + 1 + 1 + 1 = 11 场&lt;/p&gt;
&lt;h3&gt;Q3：25 匹马 5 条跑道找最快的 5 匹马，需要跑几次？&lt;/h3&gt;
&lt;p&gt;通过前 6 场决出第一名的方式不变，第 7 场才是关键，能否同时决出第 2、3 名次的马。&lt;/p&gt;
&lt;p&gt;在上面的方法中，第 7 场比赛 [A2、B1、C1、D1、E1] 是为了决定第 2 名的马。但是在第 6 场比赛中我们已经得到 (B1&gt;C1&gt;D1&gt;E1)，试问？有 B1 在的比赛，C1、D1、E1 还有可能争夺第 2 名吗？ 当然不可能，也就是说第 2 名只能在 A2、B1 中出现。实际上只需要 2 条跑道就可以决出第 2 名，剩下 C1、D1、E1 的 3 条跑道都只能用来凑热闹的吗？&lt;/p&gt;
&lt;p&gt;能够优化的关键出来了，我们是否能够通过剩下的 3 个跑道来决出第 3 名呢？当然可以，我们来进一步分析第 3 名的情况？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 A2&gt;B1 (即第 2 名为 A2)，那么根据第 6 场比赛中的 (B1&gt;C1&gt;D1&gt;E1)。 可以断定第 3 名只能在 A3 和 B1 中产生。&lt;/li&gt;
&lt;li&gt;如果 B1&gt;A2 (即第 2 名为 B1)，那么可以断定的第 3 名只能在 A2, B2, C1 中产生。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;好了，结论也出来了，只要我们把 [A2、B1、A3、B2、C1] 作为第 7 场比赛的马，那么这场比赛的第 2，3 名一定是整个 25 匹马中的第 2，3 名。&lt;/p&gt;
&lt;p&gt;我们在这里列举出第 7 场的 2，3 名次的所有可能情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;① 第 2 名=A2，第 3 名=A3&lt;/li&gt;
&lt;li&gt;② 第 2 名=A2，第 3 名=B1&lt;/li&gt;
&lt;li&gt;③ 第 2 名=B1，第 3 名=A2&lt;/li&gt;
&lt;li&gt;④ 第 2 名=B1，第 3 名=B2&lt;/li&gt;
&lt;li&gt;⑤ 第 2 名=B1，第 3 名=C1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第 8 场比赛很复杂，我们要根据第 7 场的所有可能的比赛情况进行分析。&lt;/p&gt;
&lt;p&gt;① 第 2 名=A2，第 3 名=A3。那么此种情况下第 4 名只能在 A4 和 B1 中产生。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果第 4 名=A4，那么第 5 名只能在 A5、B1 中产生。&lt;/li&gt;
&lt;li&gt;如果第 4 名=B1，那么第 5 名只能在 A4、B2、C1 中产生。&lt;/li&gt;
&lt;li&gt;不管结果如何，此种情况下，第 4、5 名都可以在第 8 场比赛中决出。其中比赛马匹为 [A4、A5、B1、B2、C1]。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;② 第 2 名=A2，第 3 名=B1。那么此种情况下第 4 名只能在 A3、B2、C1 中产生。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果第 4 名=A3，那么第 5 名只能在 A4、B2、C1 中产生。&lt;/li&gt;
&lt;li&gt;如果第 4 名=B2，那么第 5 名只能在 A3、B3、C1 中产生。&lt;/li&gt;
&lt;li&gt;如果第 4 名=C1，那么第 5 名只能在 A3、B2、C2、D1 中产生。&lt;/li&gt;
&lt;li&gt;那么，第 4、5 名需要在马匹 [A3、B2、B3、C1、A4、C2、D1] 七匹马中产生，则必须比赛两场才行，也就是到第 9 场角逐出全部的前 5 名。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;③ 第 2 名=B1，第 3 名=A2。那么此种情况下第 4 名只能在 A3、B2、C1 中产生。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;情况和 ② 一样，必须角逐第 9 场&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;④ 第 2 名=B1，第 3 名=B2。 那么此种情况下第 4 名只能在 A2、B3、C1 中产生。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果第 4 名=A2，那么第 5 名只能在 A3、B3、C1 中产生。&lt;/li&gt;
&lt;li&gt;如果第 4 名=B3，那么第 5 名只能在 A2、B4、C1 中产生。&lt;/li&gt;
&lt;li&gt;如果第 4 名=C1，那么第 5 名只能在 A2、B3、C2、D1 中产生。&lt;/li&gt;
&lt;li&gt;那么，第 4、5 名需要在马匹 [A2、B3、B4、C1、A3、C2、D1] 七匹马中产 生，则必须比赛两场才行，也就是到第 9 场角逐出全部的前 5 名。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⑤ 第 2 名=B1，第 3 名=C1。那么此种情况下第 4 名只能在 A2、B2、C2、D1 中产生。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果第 4 名=A2，那么第 5 名只能在 A3、B2、C2、D1 中产生。&lt;/li&gt;
&lt;li&gt;如果第 4 名=B2，那么第 5 名只能在 A2、B3、C2、D1 中产生。&lt;/li&gt;
&lt;li&gt;如果第 4 名=C2，那么第 5 名只能在 A2、B2、C3、D1 中产生。&lt;/li&gt;
&lt;li&gt;如果第 4 名=D1，那么第 5 名只能在 A2、B2、C2、D2、E2 中产生。&lt;/li&gt;
&lt;li&gt;那么，第 4、5 名需要在马匹 [A2、B2、C2、D1、A3、B3、C3、D2、E1] 九匹马中产 生，因此也必须比赛两场，也就是到第 9 长决出胜负。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结：最好情况可以在第 8 场角逐出前 5 名，最差也可以在第 9 场搞定。&lt;/p&gt;
&lt;h2&gt;3. 给定随机函数，生成别的随机数（Tencent）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;基本都是小生成大&lt;/strong&gt;：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;小生成大：给定生成 1 到 7 的随机数 &lt;code&gt;Rand7()&lt;/code&gt;，如何得到生成 1 到 10 的随机数函数 &lt;code&gt;Rand10()&lt;/code&gt;？&lt;/li&gt;
&lt;li&gt;大生成小：给定生成 1 到 7 的随机数 &lt;code&gt;Rand7()&lt;/code&gt;，如何得到生成 1 到 5 的随机数函数 &lt;code&gt;Rand5()&lt;/code&gt;？&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;1️⃣ 如果是大生成小的就比较容易，比如 &lt;code&gt;Rand7()&lt;/code&gt; 生成 &lt;code&gt;Rand5()&lt;/code&gt;，直接取前 5 个值即可。&lt;/p&gt;
&lt;p&gt;2️⃣ 如果是小生成大的，我们可以先构造一个大于 7 的随机数生成函数。记住以下式子：
$$
RandNN=(RandN()-1)*N+RandN()
$$
以上式子是「&lt;strong&gt;等概率&lt;/strong&gt;」生成 $1$ 到 $N^2$ 之间的随机数，等概率很重要。&lt;/p&gt;
&lt;p&gt;式子以看作是在数轴上撒豆子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$N$ 是跨度/步长，是 &lt;code&gt;RandN()&lt;/code&gt; 生成的数的范围长度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RandN() - 1&lt;/code&gt; 的目的是生成 $0$ 到 $N-1$ 的数，是跳数&lt;/li&gt;
&lt;li&gt;后面 + &lt;code&gt;RandN()&lt;/code&gt; 的目的是&lt;strong&gt;填满中间的空隙&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如 &lt;code&gt;Rand49 = (Rand7() - 1) * 7 + Rand7()&lt;/code&gt; 可以等概率生成 1～49 之间的随机数，然后大生成小的话只需要取 1～40 (4 * 10) 之间的数字。代码如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;LeetCode 原题：&lt;a href=&quot;https://leetcode.cn/problems/implement-rand10-using-rand7/&quot;&gt;470. 用 Rand7() 实现 Rand10()&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// The rand7() API is already defined for you.
// int rand7();
// @return a random integer in the range 1 to 7

class Solution {
public:
    int rand10() {
        while (true) {
            int v = (rand7() - 1) * 7 + rand7(); // equal prob [1 ~ 49]
            if (v &gt;= 1 &amp;#x26;&amp;#x26; v &amp;#x3C;= 40)
                return v % 10 + 1;
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 砝码称轻重，找出最轻的&lt;/h2&gt;
&lt;p&gt;其实这都是一类题，这里列举几个经典的：&lt;/p&gt;
&lt;p&gt;Q1：有一个天平，九个砝码，其中一个砝码比另八个要轻一些，问至少要用天平称几次才能将轻的那个找出来？&lt;/p&gt;
&lt;p&gt;A1：至少 2 次。第一次，一边 3 个，哪边轻就在哪边，一样重就是剩余的 3 个； 第二次，一边 1 个，哪边轻就是哪个，一样重就是剩余的那个；至少称 2 次．&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Q2：十组砝码每组十个，每个砝码都是 10g 重，但是现在其中有一组砝码每个都只有 9g 重，现有一个能显示克数的秤，最少称几次能找到轻的那组？&lt;/p&gt;
&lt;p&gt;A2：至少 1 次。将砝码分组 1~10，第一组拿一个，第二组拿两个以此类推。。第十组拿十个放到秤上称出克数 x，则 y = 550 - x，第 y 组就是轻的那组。&lt;/p&gt;
&lt;h2&gt;5. 利用空瓶换饮料，最多喝几瓶&lt;/h2&gt;
&lt;p&gt;Q：1000 瓶饮料，3 个空瓶子能够换 1 瓶饮料，问最多能喝几瓶？&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;思路 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;拿走 3 瓶，换回 1 瓶，相当于减少 2 瓶。&lt;/p&gt;
&lt;p&gt;但是最后剩下 4 瓶的时候例外，这时只能换 1 瓶！&lt;/p&gt;
&lt;p&gt;所以我们计算 1000 减 2 能减多少次，直到剩下 4（1000-4=996，996/2=498），所以 1000 减 2 能减 498 次直到剩下 4 瓶，最后剩下的 4 瓶还可以换一瓶。&lt;/p&gt;
&lt;p&gt;所以总共是 1000+498+1=1499 瓶。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;思路 2 —— 动态规划&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;3 个瓶子时将发生一次交换，因此前 3 个视为特殊情况&lt;/li&gt;
&lt;li&gt;之后每增加 2 个瓶子又可以再换 1 瓶&lt;/li&gt;
&lt;li&gt;即 &lt;code&gt;dp[i] = dp[i - 2] + 2 + 1&lt;/code&gt;：增加 2 瓶饮料可以再换 1 瓶饮料&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int dp(int n) {
    vector&amp;#x3C;int&gt; f(n + 1);
    f[0] = 0;
    f[1] = 1;
    f[2] = 2;
    for(int i = 3; i &amp;#x3C;= n; i++) {
        f[i] = f[i - 2] + 2 + 1;
    }
    return f[n];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 毒药毒白鼠，找出哪个瓶子中是毒药&lt;/h2&gt;
&lt;p&gt;有 1000 个一模一样的瓶子，其中有 999 瓶是普通的水，有 1 瓶是毒药。任何喝下毒药的生命都会在一星期之后死亡。现在你只有 10 只小白鼠和 1 个星期的时间，如何检验出哪个瓶子有毒药？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;参考答案&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;涉及&lt;strong&gt;位运算&lt;/strong&gt;思想，首先一共有 1000 瓶，2 的 10 次方是 1024，刚好大于 1000，也就是说，&lt;strong&gt;1000 瓶药品可以使用 10 位二进制数就可以表示&lt;/strong&gt;。从第一个开始：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 1 瓶： 00 0000 0001&lt;/li&gt;
&lt;li&gt;第 2 瓶： 00 0000 0010&lt;/li&gt;
&lt;li&gt;第 3 瓶： 00 0000 0011&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;li&gt;第 999 瓶： 11 1111 0010&lt;/li&gt;
&lt;li&gt;第 1000 瓶： 11 1111 0011&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要十只老鼠，如果按顺序编号，ABCDEFGHIJ 分别代表&lt;strong&gt;从低位到高位&lt;/strong&gt;每一个位。 每只老鼠对应一个二进制位，如果该位上的数字为 1，则给老鼠喝瓶里的药。&lt;/p&gt;
&lt;p&gt;观察，若死亡的老鼠编号为：ACFGJ，一共死去五只老鼠，则对应的编号为 10 0110 0101，则有毒的药品为该编号的药品，转为十进制数为：613 号。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;类似问题还有：8 瓶酒一瓶有毒，用小老鼠测试。每次测试结果 8 小时后才会得出，而你只有 8 个小时的时间。最少需要（ ）老鼠测试？
A、2
B、3
C、4
D、6&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;答案：$8 = 2^3$，所以选 B&lt;/p&gt;
&lt;h2&gt;7. 利用烧绳子计算时间&lt;/h2&gt;
&lt;p&gt;现有若干不均匀的绳子，烧完这根绳子需要一个小时，问如何准确计时 15 分钟，30 分钟，45 分钟，75 分钟 ...&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;15 分钟：对折之后两头烧（如果不能对折，那就参考 45 分钟的解法）&lt;/p&gt;
&lt;p&gt;30 分钟：两头烧&lt;/p&gt;
&lt;p&gt;45 分钟：准备两根绳子，一根两头烧，一根一头烧，同时进行，两头烧完过了 30 分钟，立即将另一根的另一头点燃，等烧完又过了 15 分钟，加起来 45 分钟&lt;/p&gt;
&lt;p&gt;75 分钟：30 + 45&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;8. 在 24 小时内时针、分针、秒针可以重合几次&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 前提是模拟最真实的时钟走法（2 次），而非只会停留在整数（22 次）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;❌ 错误答案：24 小时中时针走 2 圈，而分针走 24 圈，时针和分针重合 24-2=22 次，而只要时针和分针重合，秒针一定有机会重合，所以总共重合 22 次。&lt;/p&gt;
&lt;p&gt;✅ 正确答案：在 24 小时内（不包含 24 点），也就只有&lt;strong&gt;两次重合&lt;/strong&gt;，分别为 0 点和 12 点。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;时针和分针重合的时候，秒针根本就不在重合的地方，而是在其他地方，以下是数学推导：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250813-gFk71z.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;9. 100 个囚犯猜帽子颜色&lt;/h2&gt;
&lt;p&gt;一百个囚犯站成一纵列，每人头上随机带上黑色或白色的帽子，每个人都不知道自己帽子的颜色，但是能看见自己前面所有人帽子的颜色． 然后从最后一个囚犯开始，每人只能用同一种声调和音量说一个字：&quot;黑&quot;或&quot;白&quot;， 如果说中了自己帽子的颜色，就存活，说错了就拉出去斩了，说的参考回答所有囚犯都能听见。是否说对，其他囚犯不知道。在这之前，所有囚犯可以聚在一起商量策略，问如果囚犯都足够聪明而且反应足够快，100 个人最大存活人数是多少？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果增加限制条件，每个囚犯只能看见前面一个人帽子颜色：那么方法 (3) 就失效了，只能用方法 (2)，即存活 50 人。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3&gt;(1) 所有人凭运气乱猜 [50]&lt;/h3&gt;
&lt;p&gt;最坏情况下所有人都猜错，平均有 50 人猜对。&lt;/p&gt;
&lt;h3&gt;(2) 一半人凭运气 [75]&lt;/h3&gt;
&lt;p&gt;很显然，&lt;strong&gt;坐在最后面的囚犯是不可能保证自己猜对的，他猜黑猜白都只有一半的几率猜对，似乎没什么区别&lt;/strong&gt;；但囚犯可以事先约定好一种暗号，即最后一个囚犯猜黑表示什么意思，猜白表示什么意思。比如，&lt;strong&gt;最后一个囚犯可以猜测和他前面的囚犯的帽子一样的颜色&lt;/strong&gt;，这就相当于用他的猜测告诉了他前面那个囚犯该猜什么，于是坐倒数第二的囚犯可以保证被释放；此时，坐在倒数第三个位置上的囚犯面对与刚才坐最后的囚犯相同的处境，他同样可以用他的猜测提示出他前面那个人的帽子颜色。&lt;/p&gt;
&lt;p&gt;相当于 50 人（偶数索引位置）给前一个报点，保证活 50 个（奇数索引位置），这样可以保证至少 50 个人猜对，&lt;strong&gt;平均情况则有 75 个人猜对&lt;/strong&gt;。但这不是最佳的策略。&lt;/p&gt;
&lt;h3&gt;(3) 最佳策略 [99]&lt;/h3&gt;
&lt;p&gt;最佳策略可以保证，除了坐在最后面的囚犯以外，其余 99 个囚犯都能猜对。&lt;/p&gt;
&lt;p&gt;最后的囚犯他完全可以透露出与全局相关的一些信息，因此以后所有的人都可以用这条信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如，他可以数一数他前面 99 个人一共有多少顶白帽子，并约定他猜“黑”表示他前面共有偶数顶白帽，他猜“白”表示他前面共有奇数顶白帽。&lt;/li&gt;
&lt;li&gt;坐倒数第二的那个人也数一数他前面 98 个人的白帽子个数：如果他数出来的个数与先前透露出的个数一奇一偶，则他自己肯定戴的是白帽子；如果他数出来的和先前透露的结果奇偶性相同，则他自己戴的肯定是黑帽子。&lt;/li&gt;
&lt;li&gt;这样，坐倒数第二的保证可以猜对了。那接下来咋办呢？不要忘了，其他囚犯能听到刚才那人猜的是什么，并且知道他的猜测保证是对的。这相当于每个人：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不仅能看到坐他前面的所有人的帽子颜色&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;还知道他背后那些人的帽子颜色&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结合最初的那个奇偶性信息&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;接下来的每一个人都可以猜出自己脑袋上的帽子颜色。这样下去，至少 99 个囚犯可以保证被释放。这种策略显然是最佳的，不可能再有什么策略能保证所有人都被释放，因为至少坐最后的那个人不可能保证自己猜对。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结：等价于最后一个囚犯报全局信息，往前每一个囚犯根据全局信息、自己数得到的信息、背后那些囚犯的信息，这三条信息可以保证自己一定猜对。&lt;/p&gt;
&lt;h2&gt;10. 小猴子搬香蕉&lt;/h2&gt;
&lt;p&gt;一个小猴子边上有 100 根香蕉，它要走过 50 米才能到家，每次它最多搬 50 根香蕉，多了就被压死了，它每走 1 米就要吃掉一根，请问它最多能把多少根香蕉搬到家里？&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：他可以把香蕉放下往返的走，但是必须保证它每走一米都能有香蕉吃。也可以走到 n 米时，放下一些香蕉，拿着 n 根香蕉走回去重新搬 50 根。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;参考回答：这种试题通常有一个迷惑点，让人看不懂题目的意图。此题迷惑点在于&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;走一米吃一根香蕉，一共走 50 米，那不是把 50 根香蕉吃完了吗？&lt;/li&gt;
&lt;li&gt;如果要回去搬另外 50 根香蕉，则往回走的时候也要吃香蕉，这样每走一米需要吃掉三根香蕉，走 50 米岂不是需要 150 根香蕉？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实不然，本题关键点在于：猴子搬箱子的过程其实分为两个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一阶段：来回搬，当香蕉数目大于 50 根时，猴子每搬一米需要吃掉三根香蕉。&lt;/li&gt;
&lt;li&gt;第二阶段：香蕉数 ≤ 50，直接搬回去。每走一米吃掉 1 根。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们分析第一阶段：假如把 100 根香蕉分为两箱。一箱 50 根。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一步，把 A 箱搬一米，吃一根。&lt;/li&gt;
&lt;li&gt;第二步，往回走一米，吃一根。&lt;/li&gt;
&lt;li&gt;第三步，把 B 箱搬一米，吃一根。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样，把所有香蕉搬走一米需要吃掉三根香蕉。&lt;/p&gt;
&lt;p&gt;这样走到第几米的时候，香蕉数刚好小于 50 呢？&lt;/p&gt;
&lt;p&gt;$100-(n*3) &amp;#x3C; 50\ &amp;#x26;&amp;#x26;\ 100-((n-1)*3)&gt;50$，n 取整数只能是 17。&lt;/p&gt;
&lt;p&gt;走到 16 米的时候，吃掉 48 根香蕉，剩 52 根香蕉。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;16 这步很有意思，它可以直接搬 50 往前走：50 - (50 -16) = 16 根&lt;/p&gt;
&lt;p&gt;也可以再来回搬一次（即 17 这种），结果都是一样的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;到 17 米的时候，猴子还有 49 根香蕉。这时猴子就轻松啦，直接背着 49 根走就行，把剩下的 50 - 17 = 33 米走完，还剩 49 - 33 = 16 根香蕉。&lt;/p&gt;
&lt;h2&gt;11. 高楼扔鸡蛋（经典）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;有 2 个鸡蛋&lt;/strong&gt;，从 100 层楼上往下扔，以此来测试鸡蛋的硬度。比如鸡蛋在第 9 层没有摔碎，在第 10 层摔碎了，那么鸡蛋不会摔碎的临界点就是 9 层。&lt;/p&gt;
&lt;p&gt;问：&lt;strong&gt;如何用最少的尝试次数&lt;/strong&gt;，测试出鸡蛋不会摔碎的临界点？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;首先要说明的是这道题你要是一上来就说出正确参考回答，那说明你的智商不是超过 160 就是你做过这题。&lt;/p&gt;
&lt;p&gt;所以建议你循序渐进的回答，一上来就说最优解可能结果不会让面试官满意。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;(1) 暴力法&lt;/h3&gt;
&lt;p&gt;按楼层顺序逐层扔，但是在最坏情况下，这个方法需要扔 100 次。&lt;/p&gt;
&lt;h3&gt;(2) 二分法&lt;/h3&gt;
&lt;p&gt;类似于二分查找的方法，这个方法在最坏情况下，需要尝试 50 次。&lt;/p&gt;
&lt;h3&gt;(3) 均匀法&lt;/h3&gt;
&lt;p&gt;如何让第一枚鸡蛋和第二枚鸡蛋的尝试次数尽可能均衡呢？&lt;/p&gt;
&lt;p&gt;只需对 100 做一个平方根运算，$\sqrt{100}=10$。&lt;/p&gt;
&lt;p&gt;因此，尝试每 10 层扔一次：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次从第 10 层扔&lt;/li&gt;
&lt;li&gt;第二次从第 20 层扔&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;第九次从第 90 层扔&lt;/li&gt;
&lt;li&gt;第十次从第 100 层扔&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样最好的情况是第 10 层碎掉，尝试次数为 1 + 9 = 10 次；&lt;/p&gt;
&lt;p&gt;最坏的情况是在第 100 层碎掉，尝试次数为 10 + 9 = 19 次。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;优化点&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以从 15 层开始扔，接下来是 25、35、...、95，这样最坏情况是在第 95 层碎掉，尝试次数为 9 + 9 = 18 次。&lt;/p&gt;
&lt;h3&gt;(4) 最优解法&lt;/h3&gt;
&lt;p&gt;我们需要一种策略，使得&lt;strong&gt;无论临界楼层在哪，最坏情况下的尝试次数都相同&lt;/strong&gt;。这意味着我们需要&lt;strong&gt;平衡&lt;/strong&gt;每次扔鸡蛋后可能的后续尝试次数。&lt;/p&gt;
&lt;p&gt;最优解法是反向思考的经典：如果最优解法在最坏情况下需要扔 X 次，那第一次在第几层扔最好呢？&lt;/p&gt;
&lt;p&gt;参考回答是：从 X 层扔。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;反向证明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设最优的尝试次数的 x 次，为什么第一次扔就要选择第 x 层呢？&lt;/li&gt;
&lt;li&gt;假设第一次扔在第 x+1 层：如果第一个鸡蛋碎了，那么第二个鸡蛋只能从第 1 层开始一层一层扔，一直扔到第 x 层。这样一来，我们总共尝试了 x+1 次，和假设尝试 x 次相悖。由此可见，第一次扔的楼层必须小于 x+1 层。&lt;/li&gt;
&lt;li&gt;假设第一次扔在第 x-1 层：如果第一个鸡蛋碎了，那么第二个鸡蛋只能从第 1 层开始一层一层扔，一直扔到第 x-2 层。这样一来，我们总共尝试了 x-2+1 = x-1 次，虽然没有超出假设次数，但似乎有些过于保守。&lt;/li&gt;
&lt;li&gt;假设第一次扔在第 x 层：如果第一个鸡蛋碎了，那么第二个鸡蛋只能从第 1 层开始一层一层扔，一直扔到第 x-1 层。这样一来，我们总共尝试了 x-1+1 = x 次，刚刚好没有超出假设次数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此，要想尽量楼层跨度大一些，又要保证不超过假设的尝试次数 x，那么第一次扔鸡蛋的最优选择就是第 x 层。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如何求 x ？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;设最优策略下，第一次在第 x 层扔鸡蛋：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果碎了，用第二个鸡蛋从第 1 层到第 x-1 层逐层测试，最多需要 x 次（第一次 + (x-1)次）。&lt;/li&gt;
&lt;li&gt;如果没碎，下一步从第 x + (x-1)层扔（&lt;strong&gt;因为已经用了一次尝试，所以下一步减少一层来保持次数平衡&lt;/strong&gt;）。
&lt;ul&gt;
&lt;li&gt;如果这次碎了，用第二个鸡蛋从第 x+1 层到第 x + (x-1) - 1 层逐层测试，最多需要 2 + (x-2) = x 次。&lt;/li&gt;
&lt;li&gt;如果没碎，下一步从第 x + (x-1) + (x-2)层扔，依此类推。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样，我们需要找到一个 x，使得 $x + (x-1) + (x-2) + ... + 1 ≥ 100$。即，$x(x + 1)/2 ≥ 100$。&lt;/p&gt;
&lt;p&gt;解这个不等式：$x² + x - 200 ≥ 0$&lt;/p&gt;
&lt;p&gt;使用求根公式：$x = \frac{-1 ± \sqrt{1 + 800}}{2} = \frac{-1 ± \sqrt{801}}{2} ≈ \frac{-1 ± 28.3}{2}$&lt;/p&gt;
&lt;p&gt;正根约为 13.65，所以 x 至少为 $14$。&lt;/p&gt;
&lt;p&gt;因此，最优解在最坏情况的尝试次数是 14 次，第一次扔鸡蛋的楼层也是 14 层。&lt;/p&gt;
&lt;p&gt;最后，让我们把第一个鸡蛋没碎的情况下，所尝试的楼层数完整列举出来：14，27， 39， 50， 60， 69， 77， 84， 90， 95， 99， 100。&lt;/p&gt;
&lt;h2&gt;12. N 只蚂蚁走树枝，问总距离或者总时间&lt;/h2&gt;
&lt;p&gt;问题：放 N 只蚂蚁在一条长度为 M 树枝上，蚂蚁与蚂蚁之间碰到就各自往反方向走，问所有蚂蚁离开树枝的总时间是多少？&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;这个问题看起来复杂，但有一个非常巧妙的&lt;strong&gt;等价转换&lt;/strong&gt; —— &lt;strong&gt;蚂蚁相遇掉头的等效性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当两只蚂蚁相遇并掉头时，可以认为它们&lt;strong&gt;互相穿过&lt;/strong&gt;而不改变方向。&lt;/li&gt;
&lt;li&gt;这是因为：
&lt;ul&gt;
&lt;li&gt;从蚂蚁个体的角度看，掉头后继续走的方向和直接穿过是一样的。&lt;/li&gt;
&lt;li&gt;从整体角度看，蚂蚁的位置和离开树枝的时间不会改变。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;所以可以忽略碰到往反方向走这个条件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以答案就很简单了，就是离开树枝时间最长的那只蚂蚁所需的时间：
$$
T=max(max(x_i),max(M-x_i))
$$&lt;/p&gt;
&lt;h2&gt;13. N 个强盗分配 M 个金币，求方案使得自己分配最多&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;海盗分金博弈 🏴‍☠️&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;5 个海盗抢到了 100 枚金币，每一颗都一样的大小和价值。 他们决定这么分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;抽签决定自己的号码（1，2，3，4，5）&lt;/li&gt;
&lt;li&gt;首先，由 1 号提出分配方案，然后大家 5 人进行表决，当半数以上的人同意时（不包括半数，这是重点），按照他的提案进行分配，否则将被扔入大海喂鲨鱼。&lt;/li&gt;
&lt;li&gt;如果 1 号死后，再由 2 号提出分配方案，然后大家 4 人进行表决，当且仅当半超过半数的人同意时，按照他的提案进行分配，否则将被扔入大海喂鲨鱼。&lt;/li&gt;
&lt;li&gt;依次类推......&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;假设每一位海盗都足够聪明，并且利益至上，能多分一枚金币绝不少分，那么 1 号海盗该怎么分金币才能使自己分到最多的金币呢？&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;思路&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从后向前推，如果 1 至 3 号强盗都喂了鲨鱼，只剩 4 号和 5 号的话，5 号一定投反对票让 4 号喂鲨鱼，以独吞全部金币。所以，4 号惟有支持 3 号才能保命。&lt;/p&gt;
&lt;p&gt;3 号知道这一点，就会提出“100，0，0”的分配方案，对 4 号、5 号一毛不拔而将全部金币归为已有，因为他知道 4 号一无所获但还是会投赞成票，再加上自己一票，他的方案即可通过。&lt;/p&gt;
&lt;p&gt;不过，2 号推知 3 号的方案，就会提出“98，0，1，1”的方案，即放弃 3 号，而给予 4 号和 5 号各一枚金币。由于该方案对于 4 号和 5 号来说比在 3 号分配时更为有利，他们将支持他而不希望他出局而由 3 号来分配。这样，2 号将拿走 98 枚金币。&lt;/p&gt;
&lt;p&gt;同样，2 号的方案也会被 1 号所洞悉，1 号并将提出（97，0，1，2，0）或（97，0，1，0，2）的方案，即放弃 2 号，而给 3 号一枚金币，同时给 4 号（或 5 号）2 枚金币。由于 1 号的这一方案对于 3 号和 4 号（或 5 号）来说，相比 2 号分配时更优，他们将投 1 号的赞成票，再加上 1 号自己的票，1 号的方案可获通过，97 枚金币可轻松落入囊中。这无疑是 1 号能够获取最大收益的方案了！&lt;/p&gt;
&lt;p&gt;✅ 参考回答是：1 号强盗分给 3 号 1 枚金币，分给 4 号或 5 号强盗 2 枚，自己独得 97 枚。分配方案可写成（97，0，1，2，0）或（97，0，1，0，2）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;此题还有变种：就是只需要一半人同意即可，不需要一半人以上同意方案就可以通过，在其他条件不变的情况下，1 号该怎么分配才能获得最多的金币？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;参考回答：类似的推理过程&lt;/p&gt;
&lt;p&gt;4 号：4 号提出的方案的时候肯定是最终方案，因为不管 5 号同意不同意都能通过，所以 4 号 5 号不必担心自己被投入大海。那此时 5 号获得的金币为 0，4 号获得的金币为 100。&lt;/p&gt;
&lt;p&gt;5 号：因为 4 号提方案的时候 ，自己获取的金币为 0 。所以只要 4 号之前的人分配给自己的金币大于 0 就同意该方案。&lt;/p&gt;
&lt;p&gt;4 号：如果 3 号提的方案一定能获得通过（原因：3 号给 5 号的金币大于 0， 5 号就同意 因此就能通过），那自己获得的金币就为 0，所以只要 2 号让自己获得的金币大于 0 就会同意。&lt;/p&gt;
&lt;p&gt;3 号：因为到了自己提方案的时候可以给 5 号一金币，自己的方案就能通过，但考虑到 2 号提方案的时候给 4 号一个金币，2 号的方案就会通过，那自己获得的金币就为 0。所以只要 1 号让自己获得的金币大于 0 就会同意。&lt;/p&gt;
&lt;p&gt;2 号：因为到了自己提方案的时候只要给 4 号一金币，就能获得通过，根本就不用顾及 3 号 5 号同意不同意，所以不管 1 号怎么提都不会同意。&lt;/p&gt;
&lt;p&gt;1 号：2 号肯定不会同意。但只要给 3 号一块金币，5 号一块金币（因为 5 号如果不同意，那么 4 号分配的时候，他什么都拿不到）就能获得通过。&lt;/p&gt;
&lt;p&gt;所以参考回答是 98，0，1，0，1。&lt;/p&gt;
&lt;p&gt;类似的问题也可用类似的推理即可。&lt;/p&gt;
&lt;h2&gt;14. 火枪手决斗，谁活下来等概率大&lt;/h2&gt;
&lt;p&gt;彼此痛恨的甲、乙、丙三个枪手准备决斗。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;甲枪法最好，十发八中；&lt;/li&gt;
&lt;li&gt;乙枪法次之，十发六中；&lt;/li&gt;
&lt;li&gt;丙枪法最差，十发四中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果三人同时开枪，并且每人每轮只发一枪；那么枪战后，谁活下来的机会大一些？&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;一般人认为甲的枪法好，活下来的可能性大一些。但合乎推理的结论是，枪法最糟糕的丙活下来的几率最大。那么我们先来分析一下各个枪手的策略。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如同田忌赛马一般，枪手甲一定要对枪手乙先。因为乙对甲的威胁要比丙对甲的威胁更大，甲应该首先干掉乙，这是甲的最佳策略。同样的道理，枪手乙的最佳策略是第一枪瞄准甲。乙一旦将甲干掉，乙和丙进行对决，乙胜算的概率自然大很多。枪手丙的最佳策略也是先对甲。乙的枪法毕竟比甲差一些，丙先把甲干掉再与乙进行对决，丙的存活概率还是要高一些。&lt;/li&gt;
&lt;li&gt;我们根据分析来计算一下三个枪手在上述情况下的存活几率：
&lt;ul&gt;
&lt;li&gt;第一轮：甲射乙，乙射甲，丙射甲。
&lt;ul&gt;
&lt;li&gt;甲的活率为24%（40% X 60%）&lt;/li&gt;
&lt;li&gt;乙的活率为20%（100% - 80%)&lt;/li&gt;
&lt;li&gt;丙的活率为100%（无人射丙）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;由于丙 100％ 存活率，因此根据上轮甲乙存活的情况来计算三人第二轮的存活几率：
&lt;ul&gt;
&lt;li&gt;情况 1：甲活乙死（24% X 80% = 19.2%）
&lt;ul&gt;
&lt;li&gt;甲射丙，丙射甲：甲的活率为 60%，丙的活率为 20%&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;情况 2：乙活甲死（20% X 76% = 15.2%）
&lt;ul&gt;
&lt;li&gt;乙射丙，丙射乙：乙的活率为 60%，丙的活率为 40%&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;情况 3：甲乙同活（24% X 20% = 4.8%）
&lt;ul&gt;
&lt;li&gt;重复第一轮&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;情况 4：甲乙同死（76% X 80% = 60.8%）
&lt;ul&gt;
&lt;li&gt;枪战结束&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;据此来计算三人活率：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;甲的活率为 (19.2% X 60%) + (4.8% X 24%) = 12.672%&lt;/li&gt;
&lt;li&gt;乙的活率为 (15.2% X 60%) + (4.8% X 20%) = 10.08%&lt;/li&gt;
&lt;li&gt;丙的活率为 (19.2% X 20%) + (15.2% X 40%) + (4.8% X 100%) + (60.8% X 100%) = 75.52%&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过对两轮枪战的详细概率计算，我们发现枪法最差的丙存活的几率最大，枪法较好的甲和乙的存活几率却远低于丙的存活几率。&lt;/p&gt;
&lt;h2&gt;15. 先手必胜问题&lt;/h2&gt;
&lt;p&gt;100 本书，每次能够拿 1～5 本，怎么拿能保证最后一次是你拿？&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;寻找每个回合固定的拿取模式，最后一次是我拿，那么上个回合最少剩下 $6(5_{max} + 1_{min})$ 本。那么只要保持每个回合结束后都剩下 6 的倍数，并且在这个回合中我拿的和对方拿的加起来为 6（这样这个回合结束后剩下的还是 6 的倍数），就必胜。&lt;/p&gt;
&lt;p&gt;关键是第一次我必须先手拿（100 % 6 = 4）本（这不算在第一回合里面），剩下 96 本对方先手，只需我每次都维持当前回合共取 6 本即可，经过 16 回合即可获胜。&lt;/p&gt;
&lt;h2&gt;16. 掰巧克力问题或者参加辩论赛&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;掰巧克力问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Q：我们有一块由 N×M 个小方块组成的巧克力。每次操作，可以选择一块当前的巧克力，然后沿着一行或一列将其掰开（即水平或垂直切割）。最少需要多少次操作，才能将所有巧克力掰成 1×1 的小块？&lt;/p&gt;
&lt;p&gt;A：模拟题，即 $N - 1 + N * (M - 1) = (M*N - 1) 次$&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;辩论赛问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Q：1000 个人参加辩论赛，1V1，输了就退出，需要安排多少场比赛？&lt;/p&gt;
&lt;p&gt;A：999 场。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每场比赛有 2 人对决，1 人胜出，1 人被淘汰。因此，每场比赛都会淘汰 1 人。&lt;/li&gt;
&lt;li&gt;最终要淘汰 999 人。因此，需要 999 场比赛，因为每场比赛淘汰 1 人。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/20250823-WDMwTU.4cwKrhNz.png"/><enclosure url="/_astro/20250823-WDMwTU.4cwKrhNz.png"/></item><item><title>八股文 @ 面试手撕</title><link>https://coooredump.github.io/blog/recruitment/2025-interview-hand-tear</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/2025-interview-hand-tear</guid><description>记录面经高频手撕题，实时更新中...</description><pubDate>Sat, 23 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;415. 大数相加&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 WXG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/add-strings/&quot;&gt;415. 字符串相加&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给定两个字符串形式的非负整数 &lt;code&gt;num1&lt;/code&gt; 和 &lt;code&gt;num2&lt;/code&gt; ，计算它们的和并同样以字符串形式返回。&lt;/p&gt;
&lt;p&gt;你不能使用任何內建的用于处理大整数的库（比如 &lt;code&gt;BigInteger&lt;/code&gt;）， 也不能直接将输入的字符串转换为整数形式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：num1 = &quot;11&quot;, num2 = &quot;123&quot;
输出：&quot;134&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：num1 = &quot;456&quot;, num2 = &quot;77&quot;
输出：&quot;533&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1️⃣ 解法一：对位数较短的数字进行了补零操作（预处理）：self-AC&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    string addStrings(string num1, string num2) {
        int m = num1.length(), n = num2.length();
        int len = abs(m - n);
        string zero(len, &apos;0&apos;);
        if (m &amp;#x3C; n)
            num1 = zero + num1;
        else if (m &gt; n)
            num2 = zero + num2;
        int mx = max(m, n);
        int remain = 0;
        string ans;
        // cout &amp;#x3C;&amp;#x3C; num1 &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; num2 &amp;#x3C;&amp;#x3C; endl;
        for (int i = mx - 1; i &gt;= 0 || remain; i--) {
            if (i &amp;#x3C; 0 &amp;#x26;&amp;#x26; remain) {
                ans.push_back(&apos;1&apos;);
                break;
            }
            char ch = num1[i] + (num2[i] - &apos;0&apos;) + remain;
            if (ch &gt; &apos;9&apos;) {
                ch = ch - 10;
                remain = 1;
            } else {
                remain = 0;
            }
            ans.push_back(ch);
        }
        reverse(ans.begin(), ans.end());
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 解法二：去除预处理的过程，直接模拟&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    string addStrings(string num1, string num2) {
        int i = num1.length() - 1, j = num2.length() - 1, add = 0;
        string ans = &quot;&quot;;
        while (i &gt;= 0 || j &gt;= 0 || add) {
            int x = i &gt;= 0 ? num1[i] - &apos;0&apos; : 0;
            int y = j &gt;= 0 ? num2[j] - &apos;0&apos; : 0;
            int result = x + y + add;
            ans.push_back(&apos;0&apos; + result % 10);
            add = result / 10;
            i--;
            j--;
        }
        reverse(ans.begin(), ans.end());
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;43. 大数相乘&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;小鹏汽车智驾一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/multiply-strings/&quot;&gt;43. 字符串相乘&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给定两个以字符串形式表示的非负整数 &lt;code&gt;num1&lt;/code&gt; 和 &lt;code&gt;num2&lt;/code&gt;，返回 &lt;code&gt;num1&lt;/code&gt; 和 &lt;code&gt;num2&lt;/code&gt; 的乘积，它们的乘积也表示为字符串形式。&lt;/p&gt;
&lt;p&gt;**注意：**不能使用任何内置的 BigInteger 库或直接将输入转换为整数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入: num1 = &quot;2&quot;, num2 = &quot;3&quot;
输出: &quot;6&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入: num1 = &quot;123&quot;, num2 = &quot;456&quot;
输出: &quot;56088&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1️⃣ 解法一思路（竖式相加）：建立在「大数相加」的基础上，因为多个数之间需要累加（这段代码自己 AC 的，容易理解）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202502282147715.png&quot; alt=&quot;fig1&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    // 大数相加
    string addStrings(string num1, string num2) {
        int i = num1.length() - 1, j = num2.length() - 1, add = 0;
        string ans = &quot;&quot;;
        while (i &gt;= 0 || j &gt;= 0 || add) {
            int x = i &gt;= 0 ? num1[i] - &apos;0&apos; : 0;
            int y = j &gt;= 0 ? num2[j] - &apos;0&apos; : 0;
            int result = x + y + add;
            add = result / 10;
            ans.push_back(&apos;0&apos; + result % 10);
            i--;
            j--;
        }
        reverse(ans.begin(), ans.end());
        return ans;
    }

    // 大数相乘
    string multiply(string num1, string num2) {
        if (num1 == &quot;0&quot; || num2 == &quot;0&quot;)
            return &quot;0&quot;;
        int multiply = 0;
        int m = num1.length(), n = num2.length();
        string ans = &quot;0&quot;;
        for (int i = m - 1; i &gt;= 0; i--) {
            int x = num1[i] - &apos;0&apos;;
            string num;
            int add = 0;
            for (int j = n - 1; j &gt;= 0 || add; j--) {
                if (x == 0) {
                    num = &quot;0&quot;;
                    break;
                }
                if (j &amp;#x3C; 0) {
                    num.push_back(&apos;0&apos; + add);
                    break;
                }
                int y = num2[j] - &apos;0&apos;;
                int result = x * y + add;
                add = result / 10;
                num.push_back(&apos;0&apos; + result % 10);
            }
            reverse(num.begin(), num.end());
            if (num != &quot;0&quot;)
                ans = addStrings(ans, num + string(multiply, &apos;0&apos;));
            multiply++;
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 解法二：直接做乘法，长度分别为 &lt;code&gt;m&lt;/code&gt; 和 &lt;code&gt;n&lt;/code&gt; 的数字相乘，值长度不超过 &lt;code&gt;m + n&lt;/code&gt;，&lt;code&gt;vector&amp;#x3C;int&gt; ansArr(m + n)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202502282211768.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**	 	E x a m p l e
 *
 *               9   9   9
 *         ×     6   7   8
 *  ----------------------
 *              72  72  72
 *          63  63  63
 *      54  54  54
 *  ----------------------
 *      54 117 189 135  72
 *  ----------------------
 *      54 117 189 142   2
 *  -----------------------
 *      54 117 203   2   2
 *  -----------------------
 *      54 137   3   2   2
 *  -----------------------
 *      67   7   3   2   2
 *  -----------------------
 *   6   7   7   3   2   2
 */
class Solution {
public:
    string multiply(string num1, string num2) {
        if (num1 == &quot;0&quot; || num2 == &quot;0&quot;) {
            return &quot;0&quot;;
        }
        int m = num1.length(), n = num2.length();
        vector&amp;#x3C;int&gt; ansArr(m + n);
        for (int i = m - 1; i &gt;= 0; i--) {
            int x = num1[i] - &apos;0&apos;;
            for (int j = n - 1; j &gt;= 0; j--) {
                int y = num2[j] - &apos;0&apos;;
                ansArr[i + j + 1] += x * y;
            }
        }
        for (int i = m + n - 1; i &gt; 0; i--) {
            ansArr[i - 1] += ansArr[i] / 10;
            ansArr[i] %= 10;
        }
        int idx = ansArr[0] == 0 ? 1 : 0;
        string ans;
        while (idx &amp;#x3C; m + n) {
            ans.push_back(&apos;0&apos; + ansArr[idx]);
            idx++;
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;239. 滑动窗口最大值&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 WXG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/sliding-window-maximum/&quot;&gt;239. 滑动窗口最大值&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt;，有一个大小为 &lt;code&gt;k&lt;/code&gt; 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 &lt;code&gt;k&lt;/code&gt; 个数字。滑动窗口每次只向右移动一位。&lt;/p&gt;
&lt;p&gt;返回 &lt;em&gt;滑动窗口中的最大值&lt;/em&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [1,3,-1,-3,5,3,6,7], k = 3
输出：[3,3,5,5,6,7]
解释：
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1️⃣ 优先队列 &lt;code&gt;priority_queue&lt;/code&gt;（记录最大值） + 哈希表（记录删除元素）：self-AC&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    vector&amp;#x3C;int&gt; maxSlidingWindow(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
        priority_queue&amp;#x3C;int, vector&amp;#x3C;int&gt;, less&amp;#x3C;int&gt;&gt; pq;
        int n = nums.size();
        unordered_map&amp;#x3C;int, int&gt; cnt;
        for (int i = 0; i &amp;#x3C; k; i++)
            pq.push(nums[i]);
        vector&amp;#x3C;int&gt; ans{pq.top()};
        for (int i = k; i &amp;#x3C; n; i++) {
            cnt[nums[i - k]]++;
            pq.push(nums[i]);
            while (pq.size() &gt; k &amp;#x26;&amp;#x26; cnt[pq.top()] &gt; 0) {
                cnt[pq.top()]--;
                pq.pop();
            }
            ans.push_back(pq.top());
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 优先队列 &lt;code&gt;priority_queue&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt;&lt;/code&gt;，通过记录索引值判断 &lt;code&gt;pq.top()&lt;/code&gt; 元素是否在定长窗口内&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    vector&amp;#x3C;int&gt; maxSlidingWindow(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
        priority_queue&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; pq;
        for (int i = 0; i &amp;#x3C; k; i++) {
            pq.emplace(nums[i], i);
        }
        vector&amp;#x3C;int&gt; ans{pq.top().first};
        for (int i = k; i &amp;#x3C; nums.size(); i++) {
            pq.emplace(nums[i], i);
            while (pq.top().second &amp;#x3C; i - k + 1) {
                pq.pop();
            }
            ans.push_back(pq.top().first);
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;206. 反转链表&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 WXG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list/&quot;&gt;206. 反转链表&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;相关例题 —— &lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list-ii/&quot;&gt;92. 反转链表 II&lt;/a&gt;：反转部分区间，找到区间 &lt;code&gt;leftNode&lt;/code&gt; 与 &lt;code&gt;rightNode&lt;/code&gt;，以及 &lt;code&gt;leftNode&lt;/code&gt; 左节点 &lt;code&gt;pre&lt;/code&gt; 与 &lt;code&gt;rightNode&lt;/code&gt; 右节点 &lt;code&gt;nxt&lt;/code&gt;，独立区间（断开连接）后反转再接回。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;给你单链表的头节点 &lt;code&gt;head&lt;/code&gt; ，请你反转链表，并返回反转后的链表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://assets.leetcode.com/uploads/2021/02/19/rev1ex1.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1,2,3,4,5]
输出：[5,4,3,2,1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1️⃣ 解法一：递归&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 手写链表（LeetCode 已经定义，题目若需要则自己定义）
struct ListNode {
    int val;
    ListNode* next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode* next) : val(x), next(next) {};
};

class Solution {
public:
    // 递归
    ListNode* reverseList(ListNode* head) {
        if (!head || !head-&gt;next) {
            return head;
        }
        ListNode* new_head = reverseList(head-&gt;next);
        head-&gt;next-&gt;next = head;
        head-&gt;next = nullptr;
        return new_head;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 解法二：三指针迭代（&lt;code&gt;pre = nullptr, cur = head, nxt = cur-&gt;next&lt;/code&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        while (cur) {
            ListNode* nxt = cur-&gt;next;
            cur-&gt;next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;146. LRU 缓存&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 WXG 一面｜腾讯 CSIG 一面｜腾讯 PCG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;据说是所有面试中出现概率的 No.1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/lru-cache/&quot;&gt;146. LRU 缓存&lt;/a&gt;、&lt;a href=&quot;https://leetcode.cn/problems/lru-cache-lcci/&quot;&gt;面试题 16.25. LRU 缓存&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;请你设计并实现一个满足 &lt;a href=&quot;https://baike.baidu.com/item/LRU&quot;&gt;LRU (最近最少使用) 缓存&lt;/a&gt; 约束的数据结构。&lt;/p&gt;
&lt;p&gt;实现 &lt;code&gt;LRUCache&lt;/code&gt; 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LRUCache(int capacity)&lt;/code&gt; 以 &lt;strong&gt;正整数&lt;/strong&gt; 作为容量 &lt;code&gt;capacity&lt;/code&gt; 初始化 LRU 缓存&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int get(int key)&lt;/code&gt; 如果关键字 &lt;code&gt;key&lt;/code&gt; 存在于缓存中，则返回关键字的值，否则返回 &lt;code&gt;-1&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void put(int key, int value)&lt;/code&gt; 如果关键字 &lt;code&gt;key&lt;/code&gt; 已经存在，则变更其数据值 &lt;code&gt;value&lt;/code&gt; ；如果不存在，则向缓存中插入该组 &lt;code&gt;key-value&lt;/code&gt; 。如果插入操作导致关键字数量超过 &lt;code&gt;capacity&lt;/code&gt; ，则应该 &lt;strong&gt;逐出&lt;/strong&gt; 最久未使用的关键字。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;函数 &lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;put&lt;/code&gt; 必须以 &lt;code&gt;O(1)&lt;/code&gt; 的平均时间复杂度运行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入
[&quot;LRUCache&quot;, &quot;put&quot;, &quot;put&quot;, &quot;get&quot;, &quot;put&quot;, &quot;get&quot;, &quot;put&quot;, &quot;get&quot;, &quot;get&quot;, &quot;get&quot;]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废，缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废，缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LRUCache：循环双向链表 + 哈希表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;循环：方便获取末尾节点进行 LRU 逐出/删除&lt;/li&gt;
&lt;li&gt;双向链表&lt;/li&gt;
&lt;li&gt;哈希表：快速找到 key 对应的节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503010621556.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// LRUCache = 循环双向链表 + 哈希表
class Node {
public:
    int key;
    int value;
    Node* prev;
    Node* next;
    Node(int k = 0, int v = 0) : key(k), value(v) {}
};

class LRUCache {
private:
    int capacity;
    Node* dummy;
    unordered_map&amp;#x3C;int, Node*&gt; key_to_node;

    // 删除一个节点
    void remove(Node* x) {
        x-&gt;prev-&gt;next = x-&gt;next;
        x-&gt;next-&gt;prev = x-&gt;prev;
    }

    // 在链表头添加一个节点
    void push_front(Node* x) {
        x-&gt;prev = dummy;
        x-&gt;next = dummy-&gt;next;
        x-&gt;prev-&gt;next = x;
        x-&gt;next-&gt;prev = x;
    }

    // 获取 key 对应的节点, 同时把该节点移到链表头部
    Node* get_node(int key) {
        auto it = key_to_node.find(key);
        if (it == key_to_node.end()) {
            return nullptr;
        }
        Node* node = it-&gt;second;
        remove(node);
        push_front(node);
        return node;
    }

public:
    LRUCache(int capacity) : capacity(capacity), dummy(new Node()) {
        // 循环双向链表: 方便取末尾值进行删除
        dummy-&gt;prev = dummy;
        dummy-&gt;next = dummy;
    }

    int get(int key) {
        Node* node = get_node(key);
        return node ? node-&gt;value : -1;
    }

    void put(int key, int value) {
        Node* node = get_node(key);
        if (node) {
            node-&gt;value = value;
            return;
        }
        node = new Node(key, value);
        key_to_node[key] = node;
        push_front(node);
        if (key_to_node.size() &gt; capacity) {
            Node* back_node = dummy-&gt;prev;
            key_to_node.erase(back_node-&gt;key);
            remove(back_node);
            delete back_node;
        }
    }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj-&gt;get(key);
 * obj-&gt;put(key,value);
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;460. LFU 缓存&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;华为机考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/lfu-cache/&quot;&gt;460. LFU 缓存&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;相关例题：&lt;a href=&quot;https://leetcode.cn/problems/lru-cache/&quot;&gt;146. LRU 缓存&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;请你为 &lt;a href=&quot;https://baike.baidu.com/item/%E7%BC%93%E5%AD%98%E7%AE%97%E6%B3%95&quot;&gt;最不经常使用（LFU）&lt;/a&gt;缓存算法设计并实现数据结构。&lt;/p&gt;
&lt;p&gt;实现 &lt;code&gt;LFUCache&lt;/code&gt; 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LFUCache(int capacity)&lt;/code&gt; - 用数据结构的容量 &lt;code&gt;capacity&lt;/code&gt; 初始化对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int get(int key)&lt;/code&gt; - 如果键 &lt;code&gt;key&lt;/code&gt; 存在于缓存中，则获取键的值，否则返回 &lt;code&gt;-1&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void put(int key, int value)&lt;/code&gt; - 如果键 &lt;code&gt;key&lt;/code&gt; 已存在，则变更其值；如果键不存在，请插入键值对。当缓存达到其容量 &lt;code&gt;capacity&lt;/code&gt; 时，则应该在插入新项之前，移除最不经常使用的项。在此问题中，当存在平局（即两个或更多个键具有相同使用频率）时，应该去除 &lt;strong&gt;最久未使用&lt;/strong&gt; 的键。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了确定最不常使用的键，可以为缓存中的每个键维护一个 &lt;strong&gt;使用计数器&lt;/strong&gt; 。使用计数最小的键是最久未使用的键。&lt;/p&gt;
&lt;p&gt;当一个键首次插入到缓存中时，它的使用计数器被设置为 &lt;code&gt;1&lt;/code&gt; (由于 put 操作)。对缓存中的键执行 &lt;code&gt;get&lt;/code&gt; 或 &lt;code&gt;put&lt;/code&gt; 操作，使用计数器的值将会递增。&lt;/p&gt;
&lt;p&gt;函数 &lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;put&lt;/code&gt; 必须以 &lt;code&gt;O(1)&lt;/code&gt; 的平均时间复杂度运行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：
[&quot;LFUCache&quot;, &quot;put&quot;, &quot;put&quot;, &quot;get&quot;, &quot;put&quot;, &quot;get&quot;, &quot;get&quot;, &quot;put&quot;, &quot;get&quot;, &quot;get&quot;, &quot;get&quot;]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]
输出：
[null, null, null, 1, null, -1, 3, null, -1, 3, 4]

解释：
// cnt(x) = 键 x 的使用计数
// cache=[] 将显示最后一次使用的顺序（最左边的元素是最近的）
LFUCache lfu = new LFUCache(2);
lfu.put(1, 1);   // cache=[1,_], cnt(1)=1
lfu.put(2, 2);   // cache=[2,1], cnt(2)=1, cnt(1)=1
lfu.get(1);      // 返回 1
                 // cache=[1,2], cnt(2)=1, cnt(1)=2
lfu.put(3, 3);   // 去除键 2 ，因为 cnt(2)=1 ，使用计数最小
                 // cache=[3,1], cnt(3)=1, cnt(1)=2
lfu.get(2);      // 返回 -1（未找到）
lfu.get(3);      // 返回 3
                 // cache=[3,1], cnt(3)=2, cnt(1)=2
lfu.put(4, 4);   // 去除键 1 ，1 和 3 的 cnt 相同，但 1 最久未使用
                 // cache=[4,3], cnt(4)=1, cnt(3)=2
lfu.get(1);      // 返回 -1（未找到）
lfu.get(3);      // 返回 3
                 // cache=[3,4], cnt(4)=1, cnt(3)=3
lfu.get(4);      // 返回 4
                 // cache=[3,4], cnt(4)=2, cnt(3)=3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LFU 缓存 = 循环双向链表 + &lt;code&gt;key_to_node&lt;/code&gt; 哈希表 + &lt;code&gt;freq_to_dummy&lt;/code&gt; 哈希表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503011758025.png&quot; alt=&quot;460-2-c.png&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Node {
public:
    int key;
    int value;
    int freq = 1; // default
    Node* prev;
    Node* next;

    Node(int k = 0, int v = 0) : key(k), value(v) {}
};

class LFUCache {
private:
    int min_freq;
    int capacity;
    unordered_map&amp;#x3C;int, Node*&gt; key_to_node;
    unordered_map&amp;#x3C;int, Node*&gt; freq_to_dummy; // 每个 freq 代表一个链表的头节点

    // 创新一个新的双向链表
    Node* new_list() {
        Node* dummy = new Node(); // 哨兵节点
        dummy-&gt;prev = dummy;
        dummy-&gt;next = dummy;
        return dummy;
    }

    void remove(Node* node) {
        node-&gt;prev-&gt;next = node-&gt;next;
        node-&gt;next-&gt;prev = node-&gt;prev;
    }

    void push_front(int freq, Node* node) {
        auto it = freq_to_dummy.find(freq);
        if (it == freq_to_dummy.end()) {
            // pair&amp;#x3C;iterator, bool&gt; emplace()
            it = freq_to_dummy.emplace(freq, new_list()).first;
        }
        Node* dummy = it-&gt;second;
        node-&gt;prev = dummy;
        node-&gt;next = dummy-&gt;next;
        node-&gt;prev-&gt;next = node;
        node-&gt;next-&gt;prev = node;
    }

    Node* get_node(int key) {
        auto it = key_to_node.find(key);
        if (it == key_to_node.end()) {
            return nullptr;
        }
        Node* node = it-&gt;second;
        remove(node);
        Node* dummy = freq_to_dummy[node-&gt;freq];
        if (dummy-&gt;prev == dummy) { // 如果该 freq 对应的链表移除 node 后为空时
            freq_to_dummy.erase(node-&gt;freq);
            delete dummy;
            if (min_freq == node-&gt;freq) { // node-&gt;freq 为最小记数
                min_freq++;
            }
        }
        push_front(++node-&gt;freq, node);
        return node;
    }

public:
    LFUCache(int capacity) : capacity(capacity) {}

    int get(int key) {
        Node* node = get_node(key);
        return node ? node-&gt;value : -1;
    }

    void put(int key, int value) {
        Node* node = get_node(key);
        if (node) {
            node-&gt;value = value;
            return;
        }
        if (key_to_node.size() == capacity) {
            Node* dummy = freq_to_dummy[min_freq];
            Node* last_node = dummy-&gt;prev;
            key_to_node.erase(last_node-&gt;key);
            remove(last_node);
            delete last_node;
            if (dummy-&gt;prev == dummy) {
                freq_to_dummy.erase(min_freq);
                delete dummy;
            }
        }
        node = new Node(key, value);
        key_to_node[key] = node;
        push_front(1, node);
        min_freq = 1;
    }
};

/**
 * Your LFUCache object will be instantiated and called as such:
 * LFUCache* obj = new LFUCache(capacity);
 * int param_1 = obj-&gt;get(key);
 * obj-&gt;put(key,value);
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;560. 和为 k 的子数组&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Momenta 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/subarray-sum-equals-k/&quot;&gt;560. 和为 K 的子数组&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个整数 &lt;code&gt;k&lt;/code&gt; ，请你统计并返回该数组中和为 &lt;code&gt;k&lt;/code&gt; 的子数组的个数 。&lt;/p&gt;
&lt;p&gt;子数组是数组中元素的连续非空序列。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [1,1,1], k = 2
输出：2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [1,2,3], k = 3
输出：2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1️⃣ 前缀和 + 哈希表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int subarraySum(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
        int n = nums.size();
        vector&amp;#x3C;int&gt; preSum(n + 1);
        for(int i = 1; i &amp;#x3C;= n; i++)
            preSum[i] = nums[i - 1] + preSum[i - 1];
        int ans = 0;
        unordered_map&amp;#x3C;int, int&gt; cnt;
        for(int num : preSum) {
            ans += cnt.contains(num - k) ? cnt[num - k] : 0;
            cnt[num]++;
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;93. 复原 IP 地址&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 PCG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/restore-ip-addresses/&quot;&gt;93. 复原 IP 地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有效 IP 地址&lt;/strong&gt; 正好由四个整数（每个整数位于 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;255&lt;/code&gt; 之间组成，且不能含有前导 &lt;code&gt;0&lt;/code&gt;），整数之间用 &lt;code&gt;&apos;.&apos;&lt;/code&gt; 分隔。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;例如：&lt;code&gt;&quot;0.1.2.201&quot;&lt;/code&gt; 和&lt;code&gt; &quot;192.168.1.1&quot;&lt;/code&gt; 是 &lt;strong&gt;有效&lt;/strong&gt; IP 地址，但是 &lt;code&gt;&quot;0.011.255.245&quot;&lt;/code&gt;、&lt;code&gt;&quot;192.168.1.312&quot;&lt;/code&gt; 和 &lt;code&gt;&quot;192.168@1.1&quot;&lt;/code&gt; 是 &lt;strong&gt;无效&lt;/strong&gt; IP 地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;给定一个只包含数字的字符串 &lt;code&gt;s&lt;/code&gt; ，用以表示一个 IP 地址，返回所有可能的&lt;strong&gt;有效 IP 地址&lt;/strong&gt;，这些地址可以通过在 &lt;code&gt;s&lt;/code&gt; 中插入 &lt;code&gt;&apos;.&apos;&lt;/code&gt; 来形成。你 &lt;strong&gt;不能&lt;/strong&gt; 重新排序或删除 &lt;code&gt;s&lt;/code&gt; 中的任何数字。你可以按 &lt;strong&gt;任何&lt;/strong&gt; 顺序返回答案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：s = &quot;25525511135&quot;
输出：[&quot;255.255.11.135&quot;,&quot;255.255.111.35&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：s = &quot;0000&quot;
输出：[&quot;0.0.0.0&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1️⃣ 解法一：四层循环迭代，简单易懂暴力&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    vector&amp;#x3C;string&gt; restoreIpAddresses(string s) {
        int n = s.length();
        vector&amp;#x3C;string&gt; ans;
        for (int a = 1; a &amp;#x3C;= 3; a++) {
            for (int b = 1; b &amp;#x3C;= 3; b++) {
                for (int c = 1; c &amp;#x3C;= 3; c++) {
                    for (int d = 1; d &amp;#x3C;= 3; d++) {
                        if (a + b + c + d == n) {
                            int numA = stoi(s.substr(0, a));
                            int numB = stoi(s.substr(a, b));
                            int numC = stoi(s.substr(a + b, c));
                            int numD = stoi(s.substr(a + b + c, d));
                            if (numA &amp;#x3C;= 255 &amp;#x26;&amp;#x26; numB &amp;#x3C;= 255 &amp;#x26;&amp;#x26; numC &amp;#x3C;= 255 &amp;#x26;&amp;#x26; numD &amp;#x3C;= 255) {
                                string ip = to_string(numA) + &quot;.&quot; +
                                            to_string(numB) + &quot;.&quot; +
                                            to_string(numC) + &quot;.&quot; +
                                            to_string(numD);
                                if (ip.length() == n + 3) {
                                    ans.push_back(ip);
                                }
                            }
                        }
                    }
                }
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 回溯法｜递归&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 代码1
class Solution {
public:
    vector&amp;#x3C;string&gt; ans;
    string ip;

    void backtracking(string s, int i, int segment) {
        if (i == s.length() &amp;#x26;&amp;#x26; segment == 4) {
            ip = ip.substr(0, ip.length() - 1);
            ans.push_back(ip);
            return;
        }
        if (segment &gt; 4) {
            return;
        }
        for (int j = 1; j &amp;#x3C;= 3 &amp;#x26;&amp;#x26; i + j - 1 &amp;#x3C; s.length(); j++) {
            if (j &gt; 1 &amp;#x26;&amp;#x26; s[i] == &apos;0&apos;) {
                return;
            }
            string subIP = s.substr(i, j);
            int numIP = stoi(subIP);
            if (numIP &gt; 255)
                break;
            int len = ip.length();
            ip = ip + subIP + &apos;.&apos;;
            backtracking(s, i + j, segment + 1);
            ip = ip.substr(0, len);
        }
    }

    vector&amp;#x3C;string&gt; restoreIpAddresses(string s) {
        int n = s.length();
        if (n &amp;#x3C; 4) {
            return {};
        }
        backtracking(s, 0, 0);
        return ans;
    }
};

// 代码2
class Solution {
public:
    vector&amp;#x3C;string&gt; ans;
    vector&amp;#x3C;string&gt; path;

    vector&amp;#x3C;string&gt; restoreIpAddresses(string s) {
        int n = s.length();
        if (n &amp;#x3C; 4 || n &gt; 12) {
            return {};
        }

        function&amp;#x3C;void(int)&gt; dfs = [&amp;#x26;](int i) {
            if (i == n &amp;#x26;&amp;#x26; path.size() == 4) {
                string ip = path[0] + &quot;.&quot; + path[1] + &quot;.&quot; + path[2] + &quot;.&quot; + path[3];
                ans.push_back(ip);
                return;
            }
            for (int j = 1; j &amp;#x3C;= 3 &amp;#x26;&amp;#x26; i + j - 1 &amp;#x3C; n; j++) {
                string sub = s.substr(i, j);
                if ((j &gt; 1 &amp;#x26;&amp;#x26; s[i] == &apos;0&apos;) || stoi(sub) &gt; 255)
                    break;
                path.push_back(sub);
                dfs(i + j);
                path.pop_back();
            }
        };

        dfs(0);

        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;25. K 个一组反转链表&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 WXG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/reverse-nodes-in-k-group/&quot;&gt;25. K 个一组翻转链表&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;相关例题 1：&lt;a href=&quot;https://leetcode.cn/problems/swap-nodes-in-pairs/&quot;&gt;24. 两两交换链表中的节点&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/swap-nodes-in-pairs/submissions/604519840/&quot;&gt;迭代｜四指针&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/swap-nodes-in-pairs/submissions/604520846/&quot;&gt;递归｜三指针&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相关例题 2：&lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list/&quot;&gt;206. 反转链表&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;相关例题 3：&lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list-ii/&quot;&gt;92. 反转链表 II&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;给你链表的头节点 &lt;code&gt;head&lt;/code&gt; ，每 &lt;code&gt;k&lt;/code&gt; 个节点一组进行翻转，请你返回修改后的链表。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;k&lt;/code&gt; 是一个正整数，它的值小于或等于链表的长度。如果节点总数不是 &lt;code&gt;k&lt;/code&gt; 的整数倍，那么请将最后剩余的节点保持原有顺序。&lt;/p&gt;
&lt;p&gt;你不能只是单纯的改变节点内部的值，而是需要实际进行节点交换。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503010320428.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1,2,3,4,5], k = 2
输出：[2,1,4,3,5]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1️⃣ 解法一：利用「206. 反转链表」+「92. 反转链表 II」完成&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    // 206. 反转链表
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        while (cur) {
            ListNode* nxt = cur-&gt;next;
            cur-&gt;next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }

    // 92. 反转链表 II
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        ListNode dummy(0, head);
        ListNode* pre = &amp;#x26;dummy;
        for (int i = 0; i &amp;#x3C; left - 1; i++) {
            pre = pre-&gt;next;
        }
        ListNode* leftNode = pre-&gt;next;
        ListNode* rightNode = leftNode;
        for (int i = left; i &amp;#x3C; right; i++) {
            rightNode = rightNode-&gt;next;
        }
        ListNode* nxt = rightNode-&gt;next;
        rightNode-&gt;next = nullptr;
        reverseList(leftNode);
        pre-&gt;next = rightNode;
        leftNode-&gt;next = nxt;
        return dummy.next;
    }

    ListNode* reverseKGroup(ListNode* head, int k) {
        int n = 0;
        ListNode* cur = head;
        while (cur) {
            n++;
            cur = cur-&gt;next;
        }
        if (n &amp;#x3C; k) {
            return head;
        }
        ListNode* new_head = head;
        int times = n / k;
        for (int i = 0; times--; i += k) {
            ListNode* node = reverseBetween(new_head, i + 1, i + k);
            if (i == 0) {
                new_head = node;
            }
        }
        return new_head;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 解法二｜&lt;code&gt;0x3f&lt;/code&gt;：从反转链表直接到「K 个一组翻转链表」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic.leetcode.cn/1669456133-wgksZa-006.jpg&quot; alt=&quot;006.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;反转过程同「反转链表」代码，从 &lt;code&gt;while (cur)&lt;/code&gt; 变成 &lt;code&gt;for (int i = 0; i &amp;#x3C; k; i++)&lt;/code&gt;；其次每处理 k 个一组后，节点之间需要切换（🌟）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        // 统计节点个数
        int n = 0;
        for (ListNode* cur = head; cur; cur = cur-&gt;next) {
            n++;
        }
        ListNode dummy(0, head);
        ListNode* p0 = &amp;#x26;dummy;
        ListNode* pre = nullptr;
        ListNode* cur = head;
        // k 个一组处理
        for (; n &gt;= k; n -= k) {
            // 同 [206. 反转链表]
            for (int i = 0; i &amp;#x3C; k; i++) {
                ListNode* nxt = cur-&gt;next;
                cur-&gt;next = pre;
                pre = cur;
                cur = nxt;
            }
            ListNode* nxt = p0-&gt;next;
            p0-&gt;next-&gt;next = cur;
            p0-&gt;next = pre;
            p0 = nxt;
        }
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;234. 回文链表&lt;/h2&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/palindrome-linked-list/&quot;&gt;234. 回文链表&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;相关例题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/middle-of-the-linked-list/&quot;&gt;876. 链表的中间结点&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list/&quot;&gt;206. 反转链表&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;给你一个单链表的头节点 &lt;code&gt;head&lt;/code&gt; ，请你判断该链表是否为回文链表。如果是，返回 &lt;code&gt;true&lt;/code&gt; ；否则，返回 &lt;code&gt;false&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://assets.leetcode.com/uploads/2021/03/03/pal1linked-list.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1,2,2,1]
输出：true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先找到中间节点（类 876 题使用快慢指针），然后反转后半段的链表（206 题反转链表），之后逐个比较即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    // 876. 链表的中间结点
    ListNode* middleNode(ListNode* head) {
        ListNode *slow = head, *fast = head;
        while (fast &amp;#x26;&amp;#x26; fast-&gt;next) {
            slow = slow-&gt;next;
            fast = fast-&gt;next-&gt;next;
        }
        return slow;
    }

    // 206. 反转链表｜迭代
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        while (cur) {
            ListNode* nxt = cur-&gt;next;
            cur-&gt;next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }

    // 206. 反转链表｜递归
    ListNode* recursion_reverseList(ListNode* head) {
        if (!head || !head-&gt;next) {
            return head;
        }
        ListNode* new_head = recursion_reverseList(head-&gt;next);
        head-&gt;next-&gt;next = head;
        head-&gt;next = nullptr;
        return new_head;
    }

    bool isPalindrome(ListNode* head) {
        // 中间节点 (偶数则为后一个节点)
        ListNode* middle = middleNode(head);
        ListNode* node = reverseList(middle);
        while (node) {
            if (head-&gt;val != node-&gt;val) {
                return false;
            }
            head = head-&gt;next;
            node = node-&gt;next;
        }
        return true;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;23. 合并 K 个升序链表&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;快手搜广推一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/merge-k-sorted-lists/&quot;&gt;23. 合并 K 个升序链表&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给你一个链表数组，每个链表都已经按升序排列。&lt;/p&gt;
&lt;p&gt;请你将所有链表合并到一个升序链表中，返回合并后的链表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：lists = [[1,4,5],[1,3,4],[2,6]]
输出：[1,1,2,3,4,4,5,6]
解释：链表数组如下：
[
  1-&gt;4-&gt;5,
  1-&gt;3-&gt;4,
  2-&gt;6
]
将它们合并到一个有序链表中得到。
1-&gt;1-&gt;2-&gt;3-&gt;4-&gt;4-&gt;5-&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1️⃣ 最小堆&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeKLists(vector&amp;#x3C;ListNode*&gt;&amp;#x26; lists) {
        auto cmp = [](const ListNode* a, const ListNode* b) {
            return a-&gt;val &gt; b-&gt;val;
        };
        // decltype 推断表达式类型
        priority_queue&amp;#x3C;ListNode*, vector&amp;#x3C;ListNode*&gt;, decltype(cmp)&gt; pq;
        for (auto head : lists) {
            if (head) {
                pq.push(head);
            }
        }
        ListNode dummy{};
        auto cur = &amp;#x26;dummy;
        while (!pq.empty()) {
            ListNode* nxt = pq.top();
            pq.pop();
            cur-&gt;next = nxt;
            cur = cur-&gt;next;
            if (nxt-&gt;next) {
                pq.push(nxt-&gt;next);
            }
        }
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 分治法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode dummy{};
        ListNode* cur = &amp;#x26;dummy;
        while (list1 &amp;#x26;&amp;#x26; list2) {
            if (list1-&gt;val &amp;#x3C; list2-&gt;val) {
                cur-&gt;next = list1;
                list1 = list1-&gt;next;
            } else {
                cur-&gt;next = list2;
                list2 = list2-&gt;next;
            }
            cur = cur-&gt;next;
        }
        cur-&gt;next = list1 ? list1 : list2;
        return dummy.next;
    }

    ListNode* mergeKLists(vector&amp;#x3C;ListNode*&gt;&amp;#x26; lists, int l, int r) {
        if (l == r)
            return lists[l];
        if (l &gt; r)
            return nullptr;
        int m = (l + r) &gt;&gt; 1;
        auto left = mergeKLists(lists, l, m);
        auto right = mergeKLists(lists, m + 1, r);
        return mergeTwoLists(left, right);
    }

    ListNode* mergeKLists(vector&amp;#x3C;ListNode*&gt;&amp;#x26; lists) {
        return mergeKLists(lists, 0, lists.size() - 1);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;445. 两数相加 II&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 CDG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/add-two-numbers-ii/&quot;&gt;445. 两数相加 II&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;前置题目：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/add-two-numbers/&quot;&gt;2. 两数相加&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list/&quot;&gt;206. 反转链表&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;给你两个 &lt;strong&gt;非空&lt;/strong&gt; 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。&lt;/p&gt;
&lt;p&gt;你可以假设除了数字 0 之外，这两个数字都不会以零开头。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503280909618.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：l1 = [7,2,4,3], l2 = [5,6,4]
输出：[7,8,0,7]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反转链表 + 两数相加 = 秒杀&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
    ListNode* reverseList(ListNode* head) {
        if (head == nullptr || head-&gt;next == nullptr) {
            return head;
        }
        auto new_head = reverseList(head-&gt;next);
        head-&gt;next-&gt;next = head; // 把下一个节点指向自己
        head-&gt;next = nullptr; // 断开指向下一个节点的连接，保证最终链表的末尾节点的 next 是空节点
        return new_head;
    }

    // l1 和 l2 为当前遍历的节点，carry 为进位
    ListNode* addTwo(ListNode* l1, ListNode* l2, int carry = 0) {
        if (l1 == nullptr &amp;#x26;&amp;#x26; l2 == nullptr) { // 递归边界：l1 和 l2 都是空节点
            return carry ? new ListNode(carry) : nullptr; // 如果进位了，就额外创建一个节点
        }
        if (l1 == nullptr) { // 如果 l1 是空的，那么此时 l2 一定不是空节点
            swap(l1, l2); // 交换 l1 与 l2，保证 l1 非空，从而简化代码
        }
        carry += l1-&gt;val + (l2 ? l2-&gt;val : 0); // 节点值和进位加在一起
        l1-&gt;val = carry % 10; // 每个节点保存一个数位
        l1-&gt;next = addTwo(l1-&gt;next, (l2 ? l2-&gt;next : nullptr), carry / 10); // 进位
        return l1;
    }

public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        l1 = reverseList(l1);
        l2 = reverseList(l2); // l1 和 l2 反转后，就变成【2. 两数相加】了
        auto l3 = addTwo(l1, l2);
        return reverseList(l3);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;24. 两两交换链表中的节点&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 CSIG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/swap-nodes-in-pairs/&quot;&gt;24. 两两交换链表中的节点&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给你一个链表，两两交换其中相邻的节点，并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题（即，只能进行节点交换）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503280915893.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1,2,3,4]
输出：[2,1,4,3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;四个指针秒了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if (!head)
            return head;
        ListNode dummy(0, head);
        ListNode* node0 = &amp;#x26;dummy;
        ListNode* node1 = head;
        while (node1 &amp;#x26;&amp;#x26; node1-&gt;next) {
            ListNode* node2 = node1-&gt;next;
            ListNode* node3 = node2-&gt;next;
            node2-&gt;next = node1;
            node1-&gt;next = node3;
            node0-&gt;next = node2;
            node0 = node1;
            node1 = node3;
        }
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;72. 编辑距离&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 CDG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/edit-distance/&quot;&gt;72. 编辑距离&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给你两个单词 &lt;code&gt;word1&lt;/code&gt; 和 &lt;code&gt;word2&lt;/code&gt;， &lt;em&gt;请返回将 &lt;code&gt;word1&lt;/code&gt; 转换成 &lt;code&gt;word2&lt;/code&gt; 所使用的最少操作数&lt;/em&gt; 。&lt;/p&gt;
&lt;p&gt;你可以对一个单词进行如下三种操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入一个字符&lt;/li&gt;
&lt;li&gt;删除一个字符&lt;/li&gt;
&lt;li&gt;替换一个字符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：word1 = &quot;horse&quot;, word2 = &quot;ros&quot;
输出：3
解释：
horse -&gt; rorse (将 &apos;h&apos; 替换为 &apos;r&apos;)
rorse -&gt; rose (删除 &apos;r&apos;)
rose -&gt; ros (删除 &apos;e&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;递推｜注意边界初始化值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.length(), n = word2.length();
        vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; f(m + 1, vector&amp;#x3C;int&gt;(n + 1));
        for (int i = 0; i &amp;#x3C;= m; i++)
            f[i][0] = i;
        for (int j = 0; j &amp;#x3C;= n; j++)
            f[0][j] = j;
        for (int i = 0; i &amp;#x3C; m; i++) {
            for (int j = 0; j &amp;#x3C; n; j++) {
                f[i + 1][j + 1] = word1[i] == word2[j] ? f[i][j] : min({f[i][j], f[i + 1][j], f[i][j + 1]}) + 1;
            }
        }
        return f[m][n];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 寻找两个正序数组的中位数&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;字节 AML 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode: &lt;a href=&quot;https://leetcode.cn/problems/median-of-two-sorted-arrays/&quot;&gt;4. 寻找两个正序数组的中位数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给定两个大小分别为 &lt;code&gt;m&lt;/code&gt; 和 &lt;code&gt;n&lt;/code&gt; 的正序（从小到大）数组 &lt;code&gt;nums1&lt;/code&gt; 和 &lt;code&gt;nums2&lt;/code&gt;。请你找出并返回这两个正序数组的 &lt;strong&gt;中位数&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;算法的时间复杂度应该为 &lt;code&gt;O(log (m+n))&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums1 = [1,3], nums2 = [2]
输出：2.00000
解释：合并数组 = [1,2,3] ，中位数 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums1 = [1,2], nums2 = [3,4]
输出：2.50000
解释：合并数组 = [1,2,3,4] ，中位数 (2 + 3) / 2 = 2.5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;🧐 分析&lt;/h3&gt;
&lt;p&gt;本质上，我们需要在两个有序数组中，查找第 k 小的数，其中 k =  (m + n) / 2 &lt;strong&gt;取上整&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 m+n 是奇数，返回第 k 小的数。&lt;/li&gt;
&lt;li&gt;如果 m+n 是偶数，返回第 k 小的数和第 k+1 小的数的平均值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先从最暴力的「排序」做法开始，然后讲解「双指针」做法，最后过渡到「二分查找」做法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503300510187.png&quot; alt=&quot;lc4-1-c.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503300507332.png&quot; alt=&quot;lc4-4-c2.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503300506015.png&quot; alt=&quot;image-20250330050623809&quot;&gt;&lt;/p&gt;
&lt;h3&gt;🙋 答疑&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503300507578.png&quot; alt=&quot;image-20250330050705467&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1️⃣ 相向双指针｜均匀分组｜当条件「第一组最大值 ≤ 第二组最小值」满足&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    // 相向双指针｜均匀分组｜当条件「第一组最大值 &amp;#x3C;= 第二组最小值」满足
    // Hot 100 中最难的一题
    double findMedianSortedArrays(vector&amp;#x3C;int&gt;&amp;#x26; a, vector&amp;#x3C;int&gt;&amp;#x26; b) {
        if (a.size() &gt; b.size())
            swap(a, b);
        int m = a.size(), n = b.size();
        a.insert(a.begin(), INT_MIN);
        b.insert(b.begin(), INT_MIN);
        a.push_back(INT_MAX);
        b.push_back(INT_MAX);

        int i = 0, j = (m + n + 1) / 2;
        while (true) {
            if (a[i] &amp;#x3C;= b[j + 1] &amp;#x26;&amp;#x26; b[j] &amp;#x3C;= a[i + 1]) {
                int max1 = max(a[i], b[j]);         // 第一组最大值
                int min2 = min(a[i + 1], b[j + 1]); // 第二组最小值
                return (m + n) % 2 ? max1 : (max1 + min2) / 2.0;
            }
            i++;
            j--;
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 用二分查找优化： 由于满足点只有一个, 所以判断条件为 $a[i] ≤ b[j + 1]$&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    // 二分优化, 由于满足点只有一个, 所以判断条件为 a[i] &amp;#x3C;= b[j + 1]
    double findMedianSortedArrays(vector&amp;#x3C;int&gt;&amp;#x26; a, vector&amp;#x3C;int&gt;&amp;#x26; b) {
        if (a.size() &gt; b.size())
            swap(a, b);
        int m = a.size(), n = b.size();
        a.insert(a.begin(), INT_MIN);
        b.insert(b.begin(), INT_MIN);
        a.push_back(INT_MAX);
        b.push_back(INT_MAX);

        int left = 0, right = m + 1;
        while (left + 1 &amp;#x3C; right) {
            int i = (left + right) / 2;
            int j = (m + n + 1) / 2 - i;
            if (a[i] &amp;#x3C;= b[j + 1]) {
                left = i;
            } else {
                right = i;
            }
        }

        // left == right - 1
        int i = left;
        int j = (m + n + 1) / 2 - i;
        int max1 = max(a[i], b[j]);
        int min2 = min(a[i + 1], b[j + 1]);
        return (m + n) % 2 ? max1 : (max1 + min2) / 2.0;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;230. 二叉搜索树中第 K 小的元素&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 WXG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode：&lt;a href=&quot;https://leetcode.cn/problems/kth-smallest-element-in-a-bst/&quot;&gt;230. 二叉搜索树中第 K 小的元素&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给定一个二叉搜索树的根节点 &lt;code&gt;root&lt;/code&gt; ，和一个整数 &lt;code&gt;k&lt;/code&gt; ，请你设计一个算法查找其中第 &lt;code&gt;k&lt;/code&gt; 小的元素（从 1 开始计数）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504112354266.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：root = [3,1,4,null,2], k = 1
输出：1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1️⃣ 中序遍历：在中序遍历，即「左-根-右」的过程中，每次递归完左子树，就把 k 减少 1，表示我们按照中序遍历访问到了一个节点。如果减一后 k 变成 0，那么答案就是当前节点的值，用一个外部变量 ans 记录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int kthSmallest(TreeNode* root, int k) {
        int ans;
        auto dfs = [&amp;#x26;](this auto&amp;#x26;&amp;#x26; dfs, TreeNode* node) -&gt; void {
            if (node == nullptr) {
                return;
            }
            dfs(node-&gt;left); // 左
            if (--k == 0) {
                ans = node-&gt;val; // 根
            }
            dfs(node-&gt;right); // 右
        };
        dfs(root);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 中序遍历：直接将所有答案记录到 vector 数组中，返回对应索引值即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    vector&amp;#x3C;int&gt; ans;

    void preOrder(TreeNode* node) {
        if (node == nullptr)
            return;
        preOrder(node-&gt;left);
        ans.push_back(node-&gt;val);
        preOrder(node-&gt;right);
    }

    int kthSmallest(TreeNode* root, int k) {
        preOrder(root);
        return ans[k - 1];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;354. 俄罗斯套娃信封问题&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 WXG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ LeetCode：&lt;a href=&quot;https://leetcode.cn/problems/russian-doll-envelopes/&quot;&gt;354. 俄罗斯套娃信封问题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给你一个二维整数数组 &lt;code&gt;envelopes&lt;/code&gt; ，其中 &lt;code&gt;envelopes[i] = [wi, hi]&lt;/code&gt; ，表示第 &lt;code&gt;i&lt;/code&gt; 个信封的宽度和高度。&lt;/p&gt;
&lt;p&gt;当另一个信封的宽度和高度都比这个信封大的时候，这个信封就可以放进另一个信封里，如同俄罗斯套娃一样。&lt;/p&gt;
&lt;p&gt;请计算 &lt;strong&gt;最多能有多少个&lt;/strong&gt; 信封能组成一组“俄罗斯套娃”信封（即可以把一个信封放到另一个信封里面）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：不允许旋转信封。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出：3
解释：最多信封的个数为 3, 组合为: [2,3] =&gt; [5,4] =&gt; [6,7]。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：envelopes = [[1,1],[1,1],[1,1]]
输出：1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;0️⃣ DP 会超时，只能用二分查找。&lt;/p&gt;
&lt;p&gt;1️⃣ 贪心 + 二分查找：先排序，再按照 LIS 二分贪心模板求最长递增子序列。因为二者都必须是递增的，&lt;strong&gt;所以第二维度需要逆序排序&lt;/strong&gt;，使得第一维度相同的多个数，最后一个插入的一定是最小值，这样能嵌套的信封最多。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int maxEnvelopes(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; envelopes) {
        sort(envelopes.begin(), envelopes.end(), [](const auto&amp;#x26; a, const auto&amp;#x26; b) {
            return a[0] &amp;#x3C; b[0] || (a[0] == b[0] &amp;#x26;&amp;#x26; a[1] &gt; b[1]);
        });
        vector&amp;#x3C;int&gt; g;
        for (auto&amp;#x26; e : envelopes) {
            auto it = lower_bound(g.begin(), g.end(), e[1]);
            if (it == g.end()) {
                g.push_back(e[1]);
            } else {
                *it = e[1];
            }
        }
        return g.size();
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/20250823-TTIoIL.CDWv99Zo.png"/><enclosure url="/_astro/20250823-TTIoIL.CDWv99Zo.png"/></item><item><title>八股文 @ 计算机网络</title><link>https://coooredump.github.io/blog/recruitment/2025-network</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/2025-network</guid><description>记录面经高频计算机网络题，实时更新中...</description><pubDate>Sat, 23 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 键入网址到网页显示，期间发生了什么？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;超参数一面、腾讯 WXG 测开一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;回答模板&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当您在浏览器中输入网址并按下回车后，整个过程涉及 TCP/IP 协议栈的每一层协作，具体过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应用层：浏览器解析 URL，生成 HTTP 请求报文（包含请求方法、路径、头部字段等）。随后触发 DNS 查询（通过 UDP 协议），将域名解析为 IP 地址。若访问 HTTPS 站点，还会触发 TLS 握手协商加密参数。&lt;/li&gt;
&lt;li&gt;传输层：获取目标 IP 后，操作系统通过 TCP 协议与服务器建立连接（三次握手：SYN → SYN-ACK → ACK）。TCP 为 HTTP 数据提供分段、序列号、确认应答和重传机制，确保可靠传输。连接建立后，HTTP 请求被封装为 TCP 数据段发送。&lt;/li&gt;
&lt;li&gt;网络层：TCP 数据段交给 IP 协议处理，添加源/目标 IP 地址构成 IP 数据包，通过路由选择算法决定转发路径。可能经过多个路由器（跳数递增、TTL递减），最终抵达目标服务器。&lt;/li&gt;
&lt;li&gt;网络接口层：IP 数据包被封装为帧（如以太网帧），添加 MAC 地址头部。通过 ARP 协议查询下一跳路由器或目标服务器的 MAC 地址，经物理网络（如交换机、光纤）传输至下一节点。&lt;/li&gt;
&lt;li&gt;服务器端处理：服务器反向解封装帧→IP 包→TCP 段→HTTP 请求，处理后生成 HTTP 响应（状态码、响应头、HTML 等内容），再沿协议栈封装返回。&lt;/li&gt;
&lt;li&gt;客户端解析与渲染：浏览器接收响应后，解析 HTML 构建 DOM 树，加载 CSS/JS 等子资源（可能触发多次 TCP 连接复用或并发），最终完成页面渲染。&lt;/li&gt;
&lt;li&gt;TCP 连接释放：数据传输完成后，通过 TCP 四次挥手（FIN-ACK）安全关闭连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;详细的过程如下&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;想必不少小伙伴面试过程中，会遇到「当键入网址后，到网页显示，其间发生了什么」的面试题。&lt;/p&gt;
&lt;p&gt;这问题真挺常问的，好几家公司问了这个问题。&lt;/p&gt;
&lt;p&gt;接下来以下图较简单的网络拓扑模型作为例子，探究探究期间发生了什么？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250815-WtyQdc.jpg&quot; alt=&quot;简单的网络模型&quot;&gt;&lt;/p&gt;
&lt;h3&gt;(1) HTTP&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;浏览器做的第一步工作就是解析 URL&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先浏览器做的第一步工作就是要对 URL 进行解析，从而生成发送给 Web 服务器的请求信息。&lt;/p&gt;
&lt;p&gt;让我们看看一条长长的 URL 里的各个元素的代表什么，见下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-cMNV13.jpg&quot; alt=&quot;URL 解析&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以图中的长长的 URL 实际上是请求服务器里的文件资源。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;要是上图中的蓝色部分 URL 元素都省略了，那应该是请求哪个文件呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当没有路径名时，就代表访问根目录下事先设置的默认文件，也就是 &lt;code&gt;/index.html &lt;/code&gt;或者 &lt;code&gt;/default.html&lt;/code&gt; 这些文件，这样就不会发生混乱了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;生产 HTTP 请求信息&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对 URL 进行解析之后，浏览器确定了 Web 服务器和文件名，接下来就是根据这些信息来生成 HTTP 请求消息了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-pP9kk7.jpg&quot; alt=&quot;HTTP 的消息格式&quot;&gt;&lt;/p&gt;
&lt;h3&gt;(2) DNS&lt;/h3&gt;
&lt;p&gt;通过浏览器解析 URL 并生成 HTTP 消息后，需要委托操作系统将消息发送给 Web 服务器。&lt;/p&gt;
&lt;p&gt;但在发送之前，还有一项工作需要完成，那就是查询服务器域名对应的 IP 地址，因为委托操作系统发送消息时，必须提供通信对象的 IP 地址。&lt;/p&gt;
&lt;p&gt;有一种服务器就专门保存了 Web 服务器域名与 IP 的对应关系，它就是 DNS 服务器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;域名的层级关系&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;DNS 中的域名都是用句点来分隔的，比如 &lt;code&gt;www.server.com&lt;/code&gt;，这里的句点代表了不同层次之间的界限。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在域名中，越靠右的位置表示其层级越高。&lt;/li&gt;
&lt;li&gt;毕竟域名是外国人发明，所以思维和中国人相反，比如说一个城市地点的时候，外国喜欢从小到大的方式顺序说起（如 XX 街道 XX 区 XX 市 XX 省），而中国则喜欢从大到小的顺序（如 XX 省 XX 市 XX 区 XX 街道）。&lt;/li&gt;
&lt;li&gt;实际上域名最后还有一个点，比如 &lt;code&gt;www.server.com.&lt;/code&gt;，这个最后的一个点代表根域名。&lt;/li&gt;
&lt;li&gt;也就是，&lt;code&gt;.&lt;/code&gt; 根域是在最顶层，它的下一层就是 &lt;code&gt;.com&lt;/code&gt; 顶级域，再下面是 &lt;code&gt;server.com&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以域名的层级关系类似一个树状结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根 DNS 服务器（.）&lt;/li&gt;
&lt;li&gt;顶级域 DNS 服务器（.com）&lt;/li&gt;
&lt;li&gt;权威 DNS 服务器（server.com）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-1ysw7d.jpg&quot; alt=&quot;DNS 树状结构&quot;&gt;&lt;/p&gt;
&lt;p&gt;根域的 DNS 服务器信息保存在互联网中所有的 DNS 服务器中。&lt;/p&gt;
&lt;p&gt;这样一来，任何 DNS 服务器就都可以找到并访问根域 DNS 服务器了。&lt;/p&gt;
&lt;p&gt;因此，客户端只要能够找到任意一台 DNS 服务器，就可以通过它找到根域 DNS 服务器，然后再一路顺藤摸瓜找到位于下层的某台目标 DNS 服务器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;域名解析的工作流程&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;客户端首先会发出一个 DNS 请求，问 &lt;code&gt;www.server.com&lt;/code&gt; 的 IP 是啥，并发给本地 DNS 服务器（也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址）。&lt;/li&gt;
&lt;li&gt;本地域名服务器收到客户端的请求后，如果缓存里的表格能找到 &lt;code&gt;www.server.com&lt;/code&gt;，则它直接返回 IP 地址。如果没有，本地 DNS 会去问它的根域名服务器：“能告诉我 &lt;code&gt;www.server.com&lt;/code&gt; 的 IP 地址吗？” 根域名服务器是最高层次的，它不直接用于域名解析，但能指明一条道路。&lt;/li&gt;
&lt;li&gt;根 DNS 收到来自本地 DNS 的请求后，发现后置是 &lt;code&gt;.com&lt;/code&gt;，说“&lt;code&gt;www.server.com&lt;/code&gt; 这个域名归 &lt;code&gt;.com&lt;/code&gt; 区域管理”，我给你 &lt;code&gt;.com&lt;/code&gt; 顶级域名服务器地址给你，你去问问它吧。”&lt;/li&gt;
&lt;li&gt;本地 DNS 收到顶级域名服务器的地址后，发起请求问“你能告诉我 &lt;code&gt;www.server.com&lt;/code&gt; 的 IP 地址吗？”&lt;/li&gt;
&lt;li&gt;顶级域名服务器说：“我给你负责 &lt;code&gt;www.server.com&lt;/code&gt; 区域的权威 DNS 服务器的地址，你去问它应该能问到”。&lt;/li&gt;
&lt;li&gt;本地 DNS 于是转向问权威 DNS 服务器：“&lt;code&gt;www.server.com&lt;/code&gt; 对应的 IP 是啥呀？” &lt;code&gt;server.com&lt;/code&gt; 的权威 DNS 服务器，它是域名解析结果的原出处。&lt;/li&gt;
&lt;li&gt;权威 DNS 服务器查询后将对应的 IP 地址 &lt;code&gt;X.X.X.X&lt;/code&gt; 告诉本地 DNS。&lt;/li&gt;
&lt;li&gt;本地 DNS 再将 IP 地址返回客户端，客户端和目标建立连接。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至此，我们完成了 DNS 的解析过程。现在总结一下，整个过程我画成了一个图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-ygg9cE.jpg&quot; alt=&quot;域名解析的工作流程&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;那是不是每次解析域名都要经过那么多的步骤呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当然不是了，还有缓存这个东西的嘛。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;浏览器会先看自身有没有对这个域名的缓存&lt;/strong&gt;，如果有，就直接返回；&lt;/li&gt;
&lt;li&gt;如果没有，就去问操作系统，操作系统也会去看自己的缓存，如果有，就直接返回；&lt;/li&gt;
&lt;li&gt;如果没有，再去 &lt;code&gt;hosts&lt;/code&gt; 文件看；&lt;/li&gt;
&lt;li&gt;也没有，才会去问「本地 DNS 服务器」&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(3) 协议栈&lt;/h3&gt;
&lt;p&gt;通过 DNS 获取到 IP 后，就可以把 HTTP 的传输工作交给操作系统中的&lt;strong&gt;协议栈&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;协议栈的内部分为几个部分，分别承担不同的工作。上下关系是有一定的规则的，上面的部分会向下面的部分委托工作，下面的部分收到委托的工作并执行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-KdjTna.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;应用程序（浏览器）通过调用 Socket 库，来委托协议栈工作。&lt;/p&gt;
&lt;p&gt;协议栈的上半部分有两块，分别是负责收发数据的 TCP 和 UDP 协议，这两个传输协议会接受应用层的委托执行收发数据的操作。&lt;/p&gt;
&lt;p&gt;协议栈的下面一半是用 IP 协议控制网络包收发操作，在互联网上传数据时，数据会被切分成一块块的网络包，而将网络包发送给对方的操作就是由 IP 负责的。&lt;/p&gt;
&lt;p&gt;此外 IP 中还包括 &lt;code&gt;ICMP&lt;/code&gt; 协议和 &lt;code&gt;ARP&lt;/code&gt; 协议。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ICMP&lt;/code&gt; 用于告知网络包传送过程中产生的错误以及各种控制信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ARP&lt;/code&gt; 用于根据 IP 地址查询相应的以太网 MAC 地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;IP 下面的网卡驱动程序负责控制网卡硬件，而最下面的网卡则负责完成实际的收发操作，也就是对网线中的信号执行发送和接收操作。&lt;/p&gt;
&lt;h3&gt;(4) TCP —— 可靠传输&lt;/h3&gt;
&lt;p&gt;HTTP 是基于 TCP 协议传输的，所以在这我们先了解下 TCP 协议。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TCP 包头格式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们先看看 TCP 报文头部的格式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-fMeS8t.jpg&quot; alt=&quot;TCP 包头格式&quot;&gt;&lt;/p&gt;
&lt;p&gt;首先，&lt;strong&gt;源端口号&lt;/strong&gt;和&lt;strong&gt;目标端口号&lt;/strong&gt;是不可少的，如果没有这两个端口号，数据就不知道应该发给哪个应用。&lt;/p&gt;
&lt;p&gt;接下来有包的&lt;strong&gt;序号&lt;/strong&gt;，这个是为了解决包乱序的问题。&lt;/p&gt;
&lt;p&gt;还有应该有的是&lt;strong&gt;确认号&lt;/strong&gt;，目的是确认发出去对方是否有收到。如果没有收到就应该重新发送，直到送达，这个是为了解决丢包的问题。&lt;/p&gt;
&lt;p&gt;接下来还有一些&lt;strong&gt;状态位&lt;/strong&gt;。例如 &lt;code&gt;SYN&lt;/code&gt; 是发起一个连接，&lt;code&gt;ACK&lt;/code&gt; 是回复，&lt;code&gt;RST&lt;/code&gt; 是重新连接，&lt;code&gt;FIN&lt;/code&gt; 是结束连接等。TCP 是面向连接的，因而双方要维护连接的状态，这些带状态位的包的发送，会引起双方的状态变更。&lt;/p&gt;
&lt;p&gt;还有一个重要的就是&lt;strong&gt;窗口大小&lt;/strong&gt;。TCP 要做&lt;strong&gt;流量控制&lt;/strong&gt;，通信双方各声明一个窗口（缓存大小），标识自己当前能够的处理能力，别发送的太快，撑死我，也别发的太慢，饿死我。&lt;/p&gt;
&lt;p&gt;除了做流量控制以外，TCP 还会做&lt;strong&gt;拥塞控制&lt;/strong&gt;，对于真正的通路堵车不堵车，它无能为力，唯一能做的就是控制自己，也即控制发送的速度。不能改变世界，就改变自己嘛。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TCP 传输数据之前，要先三次握手建立连接&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 HTTP 传输数据之前，首先需要 TCP 建立连接，TCP 连接的建立，通常称为&lt;strong&gt;三次握手&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个所谓的「连接」，只是双方计算机里维护一个状态机，在连接建立的过程中，双方的状态变化时序图就像这样。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-Cj3KhF.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一开始，客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口，处于 LISTEN 状态。&lt;/li&gt;
&lt;li&gt;然后客户端主动发起连接 SYN，之后处于 SYN-SENT 状态。&lt;/li&gt;
&lt;li&gt;服务端收到发起的连接，返回 SYN，并且 ACK 客户端的 SYN，之后处于 SYN-RCVD 状态。&lt;/li&gt;
&lt;li&gt;客户端收到服务端发送的 SYN 和 ACK 之后，发送对 SYN 确认的 ACK，之后处于 ESTABLISHED 状态，因为它一发一收成功了。&lt;/li&gt;
&lt;li&gt;服务端收到 ACK 的 ACK 之后，处于 ESTABLISHED 状态，因为它也一发一收了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;所以三次握手目的是保证双方都有发送和接收的能力&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如何查看 TCP 的连接状态？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TCP 的连接状态查看，在 Linux 可以通过 &lt;code&gt;netstat -napt&lt;/code&gt; 命令查看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-isvy0h.jpg&quot; alt=&quot;TCP 连接状态查看&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TCP 分割数据&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果 HTTP 请求消息比较长，超过了 MSS 的长度，这时 TCP 就需要把 HTTP 的数据拆解成一块块的数据发送，而不是一次性发送所有数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-lMIBw4.jpg&quot; alt=&quot;MTU 与 MSS&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MTU&lt;/code&gt;：一个网络包的最大长度，以太网中一般为 1500 字节。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MSS&lt;/code&gt;：除去 IP 和 TCP 头部之后，一个网络包所能容纳的 TCP 数据的最大长度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据会被以 MSS 的长度为单位进行拆分，拆分出来的每一块数据都会被放进单独的网络包中。也就是在每个被拆分的数据加上 TCP 头信息，然后交给 IP 模块来发送数据。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：HTTP 头部和消息体是作为一个整体被 TCP 分割的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-JDZHuh.jpg&quot; alt=&quot;数据包分割&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TCP 报文生成&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TCP 协议里面会有两个端口，一个是浏览器监听的端口（通常是随机生成的），一个是 Web 服务器监听的端口（HTTP 默认端口号是 80， HTTPS 默认端口号是 443）。&lt;/p&gt;
&lt;p&gt;在双方建立了连接后，TCP 报文中的数据部分就是存放 HTTP 头部 + 数据，组装好 TCP 报文之后，就需交给下面的网络层处理。&lt;/p&gt;
&lt;p&gt;至此，网络包的报文如下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-xwlGUX.jpg&quot; alt=&quot;TCP 层报文&quot;&gt;&lt;/p&gt;
&lt;h3&gt;(5) IP —— 远程定位&lt;/h3&gt;
&lt;p&gt;TCP 模块在执行连接、收发、断开等各阶段操作时，都需要委托 IP 模块将数据封装成网络包发送给通信对象。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;IP 包头格式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们先看看 IP 报文头部的格式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-gbOCmK.jpg&quot; alt=&quot;IP 包头格式&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 IP 协议里面需要有&lt;strong&gt;源地址 IP&lt;/strong&gt; 和&lt;strong&gt;目标地址 IP&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源地址 IP，即是客户端输出的 IP 地址&lt;/li&gt;
&lt;li&gt;目标地址，即通过 DNS 域名解析得到的 Web 服务器 IP&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为 HTTP 是经过 TCP 传输的，所以在 IP 包头的协议号，要填写为 &lt;code&gt;06&lt;/code&gt;（十六进制），表示协议为 TCP。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;假设客户端有多个网卡，就会有多个 IP 地址，那 IP 头部的源地址应该选择哪个 IP 呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当存在多个网卡时，在填写源地址 IP 时，就需要判断到底应该填写哪个地址。这个判断相当于在多块网卡中判断应该使用哪个一块网卡来发送包。&lt;/p&gt;
&lt;p&gt;这个时候就需要根据路由表规则，来判断哪一个网卡作为源地址 IP。&lt;/p&gt;
&lt;p&gt;在 Linux 操作系统，我们可以使用 &lt;code&gt;route -n&lt;/code&gt; 命令查看当前系统的路由表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-HdpYnJ.jpg&quot; alt=&quot;路由表&quot;&gt;&lt;/p&gt;
&lt;p&gt;举个例子，根据上面的路由表，我们假设 Web 服务器的目标地址是 &lt;code&gt;192.168.10.200&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-GPRirF.jpg&quot; alt=&quot;路由规则判断&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先先和第一条目的子网掩码（Genmask）进行 与运算，得到结果为 &lt;code&gt;192.168.10.0&lt;/code&gt;，但是第一个条目的 Destination 是 &lt;code&gt;192.168.3.0&lt;/code&gt;，两者不一致所以匹配失败。&lt;/li&gt;
&lt;li&gt;再与第二条目的子网掩码进行 与运算，得到的结果为 &lt;code&gt;192.168.10.0&lt;/code&gt;，与第二条目的 Destination &lt;code&gt;192.168.10.0&lt;/code&gt; 匹配成功，所以将使用 &lt;code&gt;eth1&lt;/code&gt; 网卡的 IP 地址作为 IP 包头的源地址。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么假设 Web 服务器的目标地址是 &lt;code&gt;10.100.20.100&lt;/code&gt;，那么依然依照上面的路由表规则判断，判断后的结果是和第三条目匹配。&lt;/p&gt;
&lt;p&gt;第三条目比较特殊，它目标地址和子网掩码都是 &lt;code&gt;0.0.0.0&lt;/code&gt;，这表示&lt;strong&gt;默认网关&lt;/strong&gt;，如果其他所有条目都无法匹配，就会自动匹配这一行。并且后续就把包发给路由器，Gateway 即是路由器的 IP 地址。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;IP 报文生成&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;至此，网络包的报文如下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-3qv23L.jpg&quot; alt=&quot;IP 层报文&quot;&gt;&lt;/p&gt;
&lt;h3&gt;(6) MAC —— 两点传输&lt;/h3&gt;
&lt;p&gt;生成了 IP 头部之后，接下来网络包还需要在 IP 头部的前面加上 MAC 头部。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MAC 包头格式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;MAC 头部是以太网使用的头部，它包含了接收方和发送方的 MAC 地址等信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-HtJv4C.jpg&quot; alt=&quot;MAC 包头格式&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 MAC 包头里需要&lt;strong&gt;发送方 MAC 地址&lt;/strong&gt;和&lt;strong&gt;接收方目标 MAC 地址&lt;/strong&gt;，&lt;strong&gt;用于两点之间的传输&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一般在 TCP/IP 通信里，MAC 包头的协议类型只使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0800&lt;/code&gt; ： IP 协议&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0806&lt;/code&gt; ： ARP 协议&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;MAC 发送方和接收方如何确认?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;发送方的 MAC 地址获取就比较简单了，MAC 地址是在网卡生产时写入到 ROM 里的，只要将这个值读取出来写入到 MAC 头部就可以了。&lt;/p&gt;
&lt;p&gt;接收方的 MAC 地址就有点复杂了，只要告诉以太网对方的 MAC 的地址，以太网就会帮我们把包发送过去，那么很显然这里应该填写对方的 MAC 地址。&lt;/p&gt;
&lt;p&gt;所以先得搞清楚应该把包发给谁，这个只要查一下路由表就知道了。在路由表中找到相匹配的条目，然后把包发给 Gateway 列中的 IP 地址就可以了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;既然知道要发给谁，按如何获取对方的 MAC 地址呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不知道对方 MAC 地址？不知道就喊呗。&lt;/p&gt;
&lt;p&gt;此时就需要 ARP 协议帮我们找到路由器的 MAC 地址。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-DoZffa.jpg&quot; alt=&quot;ARP 广播&quot;&gt;&lt;/p&gt;
&lt;p&gt;ARP 协议会在以太网中以&lt;strong&gt;广播&lt;/strong&gt;的形式，对以太网所有的设备喊出：“这个 IP 地址是谁的？请把你的 MAC 地址告诉我”。&lt;/p&gt;
&lt;p&gt;然后就会有人回答：“这个 IP 地址是我的，我的 MAC 地址是 XXXX”。&lt;/p&gt;
&lt;p&gt;如果对方和自己处于同一个子网中，那么通过上面的操作就可以得到对方的 MAC 地址。然后，我们将这个 MAC 地址写入 MAC 头部，MAC 头部就完成了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;好像每次都要广播获取，这不是很麻烦吗？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;放心，在后续操作系统会把本次查询结果放到一块叫做 ARP 缓存的内存空间留着以后用，不过缓存的时间就几分钟。&lt;/p&gt;
&lt;p&gt;也就是说，在发包时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先查询 ARP 缓存，如果其中已经保存了对方的 MAC 地址，就不需要发送 ARP 查询，直接使用 ARP 缓存中的地址。&lt;/li&gt;
&lt;li&gt;而当 ARP 缓存中不存在对方 MAC 地址时，则发送 ARP 广播查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;查看 ARP 缓存内容&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 Linux 系统中，我们可以使用 &lt;code&gt;arp -a&lt;/code&gt; 命令来查看 ARP 缓存的内容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-NEnHPb.jpg&quot; alt=&quot;ARP 缓存内容&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MAC 报文生成&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;至此，网络包的报文如下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-8nBe2S.jpg&quot; alt=&quot;MAC 层报文&quot;&gt;&lt;/p&gt;
&lt;h3&gt;(7) 网卡&lt;/h3&gt;
&lt;p&gt;网络包只是存放在内存中的一串二进制数字信息，没有办法直接发送给对方。因此，我们&lt;strong&gt;需要将数字信息转换为电信号，才能在网线上传输&lt;/strong&gt;，也就是说，这才是真正的数据发送过程。&lt;/p&gt;
&lt;p&gt;负责执行这一操作的是&lt;strong&gt;网卡&lt;/strong&gt;，&lt;strong&gt;要控制网卡还需要靠网卡驱动程序&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;网卡驱动获取网络包之后，会将其复制到网卡内的缓存区中，接着会在其开头加上报头和起始帧分界符，在末尾加上用于检测错误的帧校验序列&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-ReahOu.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;起始帧分界符是一个用来表示包起始位置的标记&lt;/li&gt;
&lt;li&gt;末尾的 FCS（帧校验序列）用来检查包传输过程是否有损坏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后网卡会将包转为电信号，通过网线发送出去。&lt;/p&gt;
&lt;h3&gt;(8) 交换机&lt;/h3&gt;
&lt;p&gt;下面来看一下包是如何通过交换机的。交换机的设计是将网络包原样转发到目的地。交换机工作在 MAC 层，也称为二层网络设备。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;交换机的包接收操作&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先，电信号到达网线接口，交换机里的模块进行接收，接下来交换机里的模块将电信号转换为数字信号。&lt;/p&gt;
&lt;p&gt;然后通过包末尾的 &lt;code&gt;FCS&lt;/code&gt; 校验错误，如果没问题则放到缓冲区。这部分操作基本和计算机的网卡相同，但交换机的工作方式和网卡不同。&lt;/p&gt;
&lt;p&gt;计算机的网卡本身具有 MAC 地址，并通过核对收到的包的接收方 MAC 地址判断是不是发给自己的，如果不是发给自己的则丢弃；相对地，&lt;strong&gt;交换机的端口不核对接收方 MAC 地址，而是直接接收所有的包并存放到缓冲区中&lt;/strong&gt;。因此，&lt;strong&gt;和网卡不同，交换机的端口不具有 MAC 地址&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;将包存入缓冲区后，接下来需要查询一下这个包的接收方 MAC 地址是否已经在 MAC 地址表中有记录了。&lt;/p&gt;
&lt;p&gt;交换机的 MAC 地址表主要包含两个信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个是设备的 MAC 地址，&lt;/li&gt;
&lt;li&gt;另一个是该设备连接在交换机的哪个端口上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-hWVoGr.jpg&quot; alt=&quot;交换机的 MAC 地址表&quot;&gt;&lt;/p&gt;
&lt;p&gt;举个例子，如果收到的包的接收方 MAC 地址为 &lt;code&gt;00-02-B3-1C-9C-F9&lt;/code&gt;，则与图中表中的第 3 行匹配，根据端口列的信息，可知这个地址位于 3 号端口上，然后就可以通过交换电路将包发送到相应的端口了。&lt;/p&gt;
&lt;p&gt;所以，&lt;strong&gt;交换机根据 MAC 地址表查找 MAC 地址，然后将信号发送到相应的端口&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当 MAC 地址表找不到指定的 MAC 地址会怎么样？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;地址表中找不到指定的 MAC 地址。这可能是因为具有该地址的设备还没有向交换机发送过包，或者这个设备一段时间没有工作导致地址被从地址表中删除了。&lt;/p&gt;
&lt;p&gt;这种情况下，交换机无法判断应该把包转发到哪个端口，只能将包转发到除了源端口之外的所有端口上，无论该设备连接在哪个端口上都能收到这个包。&lt;/p&gt;
&lt;p&gt;这样做不会产生什么问题，因为以太网的设计本来就是将包发送到整个网络的，然后&lt;strong&gt;只有相应的接收者才接收包，而其他设备则会忽略这个包&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;有人会说：“这样做会发送多余的包，会不会造成网络拥塞呢？”&lt;/p&gt;
&lt;p&gt;其实完全不用过于担心，因为发送了包之后目标设备会作出响应，只要返回了响应包，交换机就可以将它的地址写入 MAC 地址表，下次也就不需要把包发到所有端口了。&lt;/p&gt;
&lt;p&gt;局域网中每秒可以传输上千个包，多出一两个包并无大碍。&lt;/p&gt;
&lt;p&gt;此外，如果接收方 MAC 地址是一个广播地址，那么交换机会将包发送到除源端口之外的所有端口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;以下两个属于广播地址&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MAC 地址中的 &lt;code&gt;FF:FF:FF:FF:FF:FF&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IP 地址中的 &lt;code&gt;255.255.255.255&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(9) 路由器 —— 出境大门&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;路由器与交换机的区别&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;网络包经过交换机之后，现在到达了路由器，并在此被转发到下一个路由器或目标设备。&lt;/p&gt;
&lt;p&gt;这一步转发的工作原理和交换机类似，也是通过查表判断包转发的目标。&lt;/p&gt;
&lt;p&gt;不过在具体的操作过程上，路由器和交换机是有区别的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;因为路由器是基于 &lt;code&gt;IP&lt;/code&gt; 设计的&lt;/strong&gt;，俗称&lt;strong&gt;三层网络设备&lt;/strong&gt;（物理层 + 链路层 + 网络层），路由器的各个端口都具有 MAC 地址和 IP 地址；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;而交换机是基于以太网设计的&lt;/strong&gt;，俗称&lt;strong&gt;二层网络设备&lt;/strong&gt;（物理层 + 链路层），交换机的端口不具有 MAC 地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;路由器基本原理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;路由器的端口具有 MAC 地址，因此它就能够成为以太网的发送方和接收方；同时还具有 IP 地址，从这个意义上来说，它和计算机的网卡是一样的。&lt;/p&gt;
&lt;p&gt;当转发包时，首先路由器端口会接收发给自己的以太网包，然后路由表查询转发目标，再由相应的端口作为发送方将以太网包发送出去。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;路由器的包接收操作&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先，电信号到达网线接口部分，路由器中的模块会将电信号转成数字信号，然后通过包末尾的 FCS 进行错误校验。&lt;/p&gt;
&lt;p&gt;如果没问题则检查 MAC 头部中的接收方 MAC 地址，看看是不是发给自己的包，如果是就放到接收缓冲区中，否则就丢弃这个包。&lt;/p&gt;
&lt;p&gt;总的来说，路由器的端口都具有 MAC 地址，只接收与自身地址匹配的包，遇到不匹配的包则直接丢弃。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;查询路由表确定输出端口&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;完成包接收操作之后，路由器就会去掉包开头的 MAC 头部。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MAC 头部的作用就是将包送达路由器&lt;/strong&gt;，其中的接收方 MAC 地址就是路由器端口的 MAC 地址。因此，当包到达路由器之后，MAC 头部的任务就完成了，于是 MAC 头部就会被丢弃。&lt;/p&gt;
&lt;p&gt;接下来，路由器会根据 MAC 头部后方的 IP 头部中的内容进行包的转发操作。&lt;/p&gt;
&lt;p&gt;转发操作分为几个阶段，首先是查询&lt;strong&gt;路由表&lt;/strong&gt;判断转发目标。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-r03Gqu.jpg&quot; alt=&quot;路由器转发&quot;&gt;&lt;/p&gt;
&lt;p&gt;具体的工作流程根据上图，举个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设地址为 1&lt;code&gt;0.10.1.101&lt;/code&gt; 的计算机要向地址为 &lt;code&gt;192.168.1.100&lt;/code&gt; 的服务器发送一个包，这个包先到达图中的路由器。&lt;/li&gt;
&lt;li&gt;判断转发目标的第一步，就是根据包的接收方 IP 地址查询路由表中的目标地址栏，以找到相匹配的记录。&lt;/li&gt;
&lt;li&gt;路由匹配和前面讲的一样，每个条目的子网掩码和 &lt;code&gt;192.168.1.100&lt;/code&gt; IP 做 &lt;code&gt;&amp;#x26;&lt;/code&gt; 与运算后，得到的结果与对应条目的目标地址进行匹配，如果匹配就会作为候选转发目标，如果不匹配就继续与下个条目进行路由匹配。&lt;/li&gt;
&lt;li&gt;如第二条目的子网掩码 &lt;code&gt;255.255.255.0&lt;/code&gt; 与 &lt;code&gt;192.168.1.100&lt;/code&gt; IP 做 &lt;code&gt;&amp;#x26;&lt;/code&gt; 与运算后，得到结果是 &lt;code&gt;192.168.1.0&lt;/code&gt;，这与第二条目的目标地址 &lt;code&gt;192.168.1.0&lt;/code&gt; 匹配，该第二条目记录就会被作为转发目标。&lt;/li&gt;
&lt;li&gt;实在找不到匹配路由时，就会选择默认路由，路由表中子网掩码为 &lt;code&gt;0.0.0.0&lt;/code&gt; 的记录表示「&lt;strong&gt;默认路由&lt;/strong&gt;」。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;路由器的发送操作&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来就会进入包的发送操作。&lt;/p&gt;
&lt;p&gt;首先，我们需要根据路由表的网关列判断对方的地址。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果网关是一个 IP 地址，则这个IP 地址就是我们要转发到的目标地址，还未抵达终点，还需继续需要路由器转发。&lt;/li&gt;
&lt;li&gt;如果网关为空，则 IP 头部中的接收方 IP 地址就是要转发到的目标地址，也是就终于找到 IP 包头里的目标地址了，说明已抵达终点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;知道对方的 IP 地址之后，接下来需要通过 ARP 协议根据 IP 地址查询 MAC 地址，并将查询的结果作为接收方 MAC 地址。&lt;/p&gt;
&lt;p&gt;路由器也有 ARP 缓存，因此首先会在 ARP 缓存中查询，如果找不到则发送 ARP 查询请求。&lt;/p&gt;
&lt;p&gt;接下来是发送方 MAC 地址字段，这里填写输出端口的 MAC 地址。还有一个以太类型字段，填写 &lt;code&gt;0800&lt;/code&gt; （十六进制）表示 IP 协议。&lt;/p&gt;
&lt;p&gt;网络包完成后，接下来会将其转换成电信号并通过端口发送出去。这一步的工作过程和计算机也是相同的。&lt;/p&gt;
&lt;p&gt;发送出去的网络包会通过&lt;strong&gt;交换机&lt;/strong&gt;到达下一个路由器。由于接收方 MAC 地址就是下一个路由器的地址，所以交换机会根据这一地址将包传输到下一个路由器。&lt;/p&gt;
&lt;p&gt;接下来，下一个路由器会将包转发给再下一个路由器，经过层层转发之后，网络包就到达了最终的目的地。&lt;/p&gt;
&lt;p&gt;不知你发现了没有，在网络包传输的过程中，&lt;strong&gt;源 IP 和目标 IP 始终是不会变的，一直变化的是 MAC 地址，因为需要 MAC 地址在以太网内进行两个设备之间的包传输&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;(10) 服务器与客户端&lt;/h3&gt;
&lt;p&gt;数据包抵达了服务器，于是服务器开始扒数据包的皮！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-HH645z.jpg&quot; alt=&quot;网络分层模型&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据包抵达服务器后，服务器会先扒开数据包的 MAC 头部，查看是否和服务器自己的 MAC 地址符合，符合就将包收起来。&lt;/li&gt;
&lt;li&gt;接着继续扒开数据包的 IP 头，发现 IP 地址符合，根据 IP 头中协议项，知道自己上层是 TCP 协议。&lt;/li&gt;
&lt;li&gt;于是，扒开 TCP 的头，里面有序列号，需要看一看这个序列包是不是我想要的，如果是就放入缓存中然后返回一个 ACK，如果不是就丢弃。&lt;/li&gt;
&lt;li&gt;TCP 头部里面还有端口号， HTTP 的服务器正在监听这个端口号。于是，服务器自然就知道是 HTTP 进程想要这个包，于是就将包发给 HTTP 进程。&lt;/li&gt;
&lt;li&gt;服务器的 HTTP 进程看到，原来这个请求是要访问一个页面，于是就把这个网页封装在 HTTP 响应报文里。&lt;/li&gt;
&lt;li&gt;HTTP 响应报文也需要穿上 TCP、IP、MAC 头部，不过这次是源地址是服务器 IP 地址，目的地址是客户端 IP 地址。&lt;/li&gt;
&lt;li&gt;穿好头部衣服后，从网卡出去，交由交换机转发到出城的路由器，路由器就把响应数据包发到了下一个路由器，就这样跳啊跳。&lt;/li&gt;
&lt;li&gt;最后跳到了客户端的城门把守的路由器，路由器扒开 IP 头部发现是要找城内的人，于是又把包发给了城内的交换机，再由交换机转发到客户端。&lt;/li&gt;
&lt;li&gt;客户端收到了服务器的响应数据包后，开始扒皮，把收到的数据包的皮扒剩 HTTP 响应报文后，交给浏览器去渲染页面，一份特别的数据包快递，就这样显示出来了！&lt;/li&gt;
&lt;li&gt;最后，客户端要离开了，向服务器发起了 TCP 四次挥手，至此双方的连接就断开了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;TCP/IP 网络模型共有 4 层，分别是应用层、传输层、网络层和网络接口层，每一层负责的职能如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应用层，负责向用户提供一组应用程序，比如 HTTP、DNS、FTP 等;&lt;/li&gt;
&lt;li&gt;传输层，负责端到端的通信，比如 TCP、UDP 等；&lt;/li&gt;
&lt;li&gt;网络层，负责网络包的封装、分片、路由、转发，比如 IP、ICMP 等；&lt;/li&gt;
&lt;li&gt;网络接口层，负责网络包在物理网络中的传输，比如网络包的封帧、 MAC 寻址、差错检测，以及通过网卡传输网络帧等；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250819-g5GjZC.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;网络通信是分层进行的，每一层都只与另一端的对等层进行对话。下层为上层提供服务。这个过程就像寄信：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;应用层&lt;/strong&gt; (HTTP)：你写好信的内容（HTTP 请求/响应）。这包括了 HTTP 头部（如 Content-Type: text/html) 和 消息体（如 HTML 代码）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传输层&lt;/strong&gt; (TCP)：TCP 层拿到这封完整的“信”（HTTP 数据），但它发现这封信太长了，一个信封装不下。于是它把信拆分成几个小份，每一份都塞进一个 TCP 信封里。每个 TCP 信封上都写着信息（TCP 头部），比如“这是第几份”、“总共有几份”、“发送方和接收方的端口号”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络层&lt;/strong&gt; (IP)：IP 层不管 TCP 拆成了几份，它只管拿来一个 TCP 信封，就把它塞进一个更大的 IP 信封里。IP 信封上写着更大的地址信息（IP 头部），比如发送方和接收方的 IP 地址，用于在整个网络上路由。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据链路层&lt;/strong&gt; (MAC)：最后，MAC 层把 IP 信封再塞进一个帧信封里，这个信封上写着在当前局域网内下一站设备的地址（MAC 头部），比如下一个路由器或者交换机的 MAC 地址。这个帧信封的大小受 &lt;code&gt;MTU&lt;/code&gt; 限制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 如果 URL 请求的网页响应很慢，可能在哪个环节出现问题？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 WXG 测开一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当 URL 请求的网页响应很慢时，问题可能出现在多个环节，包括客户端、网络传输、服务器端以及内容本身。以下是可能的问题点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在客户端层面&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;浏览器缓存过多或配置错误可能拖慢页面加载&lt;/li&gt;
&lt;li&gt;操作系统网络栈异常（如 TCP 连接数限制或缓冲区设置不当）也可能导致连接建立缓慢&lt;/li&gt;
&lt;li&gt;DNS 客户端缓存污染或硬件资源（如 CPU、内存）不足会进一步加剧延迟&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS 解析环节&lt;/strong&gt;可能出现问题
&lt;ul&gt;
&lt;li&gt;例如本地 DNS 服务器响应缓慢、递归查询超时，或域名解析记录未正确缓存。如果 DNS 服务器故障或域名配置错误，会导致域名到 IP 地址的转换耗时过长&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络传输过程&lt;/strong&gt;中
&lt;ul&gt;
&lt;li&gt;路由路径不佳可能导致数据包经过过多跳数，从而增加延迟&lt;/li&gt;
&lt;li&gt;带宽瓶颈在用户本地网络或服务器接入端可能成为限制因素，尤其是在传输大文件时&lt;/li&gt;
&lt;li&gt;数据包丢失或网络抖动会触发 TCP 重传，降低有效吞吐量，而防火墙或中间设备策略也可能意外丢弃合法流量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务器端问题&lt;/strong&gt;包括
&lt;ul&gt;
&lt;li&gt;负载过高，例如 CPU、内存或 I/O 资源饱和，导致无法及时处理请求&lt;/li&gt;
&lt;li&gt;应用逻辑性能差，如低效的数据库查询、代码执行缓慢或缓存未命中，会直接拖慢响应速度&lt;/li&gt;
&lt;li&gt;后端依赖服务（如第三方 API 或数据库）延迟也可能阻塞整体处理流程&lt;/li&gt;
&lt;li&gt;Web 服务器配置不当（如进程数不足或保持连接超时设置不合理）会影响请求处理效率&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议与连接方面&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;TCP 三次握手或 TLS 协商在高延迟网络中可能显著增加连接建立时间&lt;/li&gt;
&lt;li&gt;HTTP/1.1 的队头阻塞问题（未启用并行连接时）会导致单个请求延迟影响后续资源加载&lt;/li&gt;
&lt;li&gt;CDN 或代理服务器配置错误可能造成请求转发效率低下或缓存失效&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容本身&lt;/strong&gt;的问题也不容忽视，例如未压缩的大资源（如图片、视频）会增加传输时间，过多阻塞渲染的 JavaScript 或同步加载策略会延迟页面呈现，而重复请求或未优化的资源链接受限於浏览器并发限制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. UDP 通信：如果 client 端 sendto 一段 1024 字节的 buf，server 端循环调用 &lt;code&gt;recvfrom(fd,buf,64,0)&lt;/code&gt;，能否收完？能的话需要调用几次？不能收完原因是什么？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯面试题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 UDP 通信中，&lt;code&gt;sendto()&lt;/code&gt; 发送的每一次数据都是一个完整的独立报文，接收端使用 &lt;code&gt;recvfrom(fd, buf, 64, 0)&lt;/code&gt; 时，每次只能接收一个完整报文的最多 64 字节，如果报文长度超过了缓冲区大小（如发送端发送了 1024 字节），则接收端只会接收到前 64 字节，超出部分会被系统直接丢弃，无法通过多次 &lt;code&gt;recvfrom()&lt;/code&gt; 调用将同一个报文拆分接收，因此在这种情况下接收端无法收完整个报文。&lt;/p&gt;
&lt;h2&gt;4. tcp 通信：client 端循环调用 &lt;code&gt;send(fd,buf,1)&lt;/code&gt; 1024 次发给 server，从 server 端捉包，客户端总共发了几个包过来？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯面试题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 TCP 通信中，尽管客户端调用了 1024 次 &lt;code&gt;send(fd, buf, 1)&lt;/code&gt; 每次仅发送 1 字节的数据，但由于 TCP 是面向字节流的协议，发送的数据会被内核缓冲区聚合后再发送，而不会直接对应为 1024 个网络包。特别是在默认启用 Nagle 算法的情况下，TCP 会将多次小的数据发送请求进行合并，直到缓冲区满或收到 ACK 才会实际发送出去，因此从服务端抓包来看，最终接收到的数据通常会被合并成更少数量的 TCP 包，远少于 1024 个。这种行为由操作系统的 TCP 堆积机制和网络状况共同决定，即使应用层调用了 1024 次 &lt;code&gt;send()&lt;/code&gt;，网络中实际传输的包数可能只有几十个或几百个。&lt;/p&gt;
&lt;h2&gt;5. 应用层有哪些协议？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;HTTP：超文本传输协议&lt;/li&gt;
&lt;li&gt;HTTPS：HTTP 的安全版本，在 HTTP 下加入 SSL/TLS 层，用于对通信进行加密、认证，确保数据的安全性和完整性&lt;/li&gt;
&lt;li&gt;FTP：文件传输协议，用于在客户端和服务器之间进行双向文件传输（上传和下载）。&lt;/li&gt;
&lt;li&gt;SMTP：简单邮件传输协议，用于发送邮件以及将邮件从发送人的邮件服务器转发到接收人的邮件服务器&lt;/li&gt;
&lt;li&gt;POP3：邮局协议第 3 版，用于从邮件服务器下载邮件到本地计算机，通常下载后会删除服务器上的邮件&lt;/li&gt;
&lt;li&gt;IMAP：互联网消息访问协议，更高级的邮件接收协议，允许用户在本地管理服务器上的邮件（如创建、删除、移动邮箱文件夹），邮件始终保留在服务器上&lt;/li&gt;
&lt;li&gt;SSH：远程连接协议，用于通过加密的连接安全地远程登录到另一台计算机，并执行命令，是 Telnet 的安全替代品&lt;/li&gt;
&lt;li&gt;DNS：域名系统协议，它不是直接为用户服务的，而是为其他应用层协议服务的。它将人类可读的域名（如 &lt;code&gt;www.google.com&lt;/code&gt;）转换为机器可读的 IP 地址（如 &lt;code&gt;142.251.42.206&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;DHCP：动态主机配置协议，自动为网络中的设备分配 IP 地址、子网掩码、默认网关和 DNS 服务器地址，即“插网线就能上网”的基础&lt;/li&gt;
&lt;li&gt;SIP：会话发起协议，用于创建、修改和终止包含视频、语音、即时消息等在内的多媒体会话，是很多 VOIP（网络电话）系统的基础&lt;/li&gt;
&lt;li&gt;RTP：实时传输协议，通常与 SIP 等协议配合使用，负责实际传输音频和视频流数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. TCP 是什么，怎么保证可靠性的？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 TEG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TCP（传输控制协议）是一种面向连接的、可靠的、基于字节流的传输层通信协议。&lt;/p&gt;
&lt;p&gt;它主要通过以下核心机制来保证可靠性的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;确认和重传机制&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;确认 (ACK)&lt;/strong&gt;：接收方在成功收到数据包后，会向发送方返回一个确认报文（ACK）。ACK 中包含了期望收到的下一个字节的序列号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时重传&lt;/strong&gt;：发送方发送一个数据包后会启动一个定时器。如果在定时器超时前没有收到对应的 ACK，发送方就认为该数据包丢失，会重新发送它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快速重传&lt;/strong&gt;：如果发送方连续收到 3 个相同的 ACK（意味着接收方收到了乱序的包，一直在重复索要某个丢失的包），发送方会立即重传那个被认为丢失的数据包，而不必等待超时。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;序列号和确认号&lt;/strong&gt;：每个字节的数据都被赋予一个唯一的序列号。确认号告诉发送方“我已经成功收到了确认号之前的所有数据，期望下一个收到的数据序列号是这个确认号”。这解决了&lt;strong&gt;数据包乱序&lt;/strong&gt;和&lt;strong&gt;确认丢失&lt;/strong&gt;的问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校验和&lt;/strong&gt;：TCP头部和数据部分都包含一个校验和。接收方会计算校验和，如果与报文中的校验和不匹配，则丢弃该数据包。发送方会因为收不到 ACK 而触发重传，从而解决了&lt;strong&gt;数据错误&lt;/strong&gt;的问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流量控制&lt;/strong&gt;：使用&lt;strong&gt;滑动窗口协议&lt;/strong&gt;来实现。接收方通过 TCP 头部的“窗口大小”字段告诉发送方自己还有多少缓冲区可以接收数据。这防止了发送方发送数据过快，导致接收方缓冲区溢出，从而解决了&lt;strong&gt;数据淹没&lt;/strong&gt;的问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拥塞控制&lt;/strong&gt;：通过一套复杂的算法（如慢启动、拥塞避免、快速恢复）来探测网络当前的承载能力。当发现网络出现拥塞（如丢包）时，会主动降低发送速率，从而减轻网络负担，避免整个网络崩溃。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;： TCP 通过&lt;strong&gt;序列号/确认号&lt;/strong&gt;确保数据有序、不丢；通过&lt;strong&gt;校验和&lt;/strong&gt;确保数据正确；通过&lt;strong&gt;流量控制&lt;/strong&gt;保护接收方；通过&lt;strong&gt;拥塞控制&lt;/strong&gt;保护网络。其可靠性是这一整套机制协同工作的结果。&lt;/p&gt;
&lt;h2&gt;7. 三次握手如果改成两次握手会怎样？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 TEG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;核心问题： &lt;strong&gt;无法防止已失效的连接请求报文突然又传送到服务器，从而导致错误和资源浪费&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;TCP 三次握手改为两次握手会导致可靠性严重下降，主要引发两个问题：&lt;strong&gt;已失效的连接请求&lt;/strong&gt;和&lt;strong&gt;无法同步初始序列号&lt;/strong&gt;。若只有两次握手，当客户端发送的 SYN 报文因网络延迟而滞留，客户端会重发新 SYN 并建立连接；但延迟的旧 SYN 之后到达服务端时，服务端会误以为是新的连接请求并直接响应，导致服务端资源被无效占用（半连接状态）。此外，两次握手无法确保双方对初始序列号的确认达成一致：服务端无法确认客户端是否收到自己的 SYN-ACK 响应，若该响应丢失，客户端无法感知连接已建立，而服务端却认为连接有效，导致数据单向传输失败。因此，第三次握手的 ACK 是防止历史连接混乱和确保序列号同步的关键。&lt;/p&gt;
&lt;h2&gt;8. 如果网络条件特别好，能不能两次握手？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 TEG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;即使在网络条件特别好的环境下，TCP 也不能改为两次握手，原因在于两次握手无法解决&lt;strong&gt;历史连接&lt;/strong&gt;和&lt;strong&gt;初始序列号同步确认&lt;/strong&gt;这两个根本性问题。&lt;/p&gt;
&lt;p&gt;首先，&lt;strong&gt;历史连接问题&lt;/strong&gt;无法避免。假设客户端发送了一个 SYN 报文后由于某种原因（如应用层重启）决定放弃连接，但该 SYN 报文因网络延迟稍后才到达服务器。在两次握手模型下，服务器收到后会立即回复 SYN-ACK 并认为连接已建立，分配资源并等待数据传输。然而客户端早已放弃这个连接，不会发送数据，导致服务器资源被长期无效占用（直到超时）。三次握手中的第三次 ACK 是关键：客户端通过它来确认本次连接的有效性，若连接已失效（如收到旧的 SYN-ACK），客户端会发送 RST 复位报文来终止服务端的无效连接。&lt;/p&gt;
&lt;p&gt;其次，&lt;strong&gt;序列号同步的可靠性&lt;/strong&gt;无法保证。TCP 依赖序列号来保证数据有序性和可靠性，而初始序列号（ISN）的同步需要双方确认。在两次握手中，服务器无法确认客户端是否成功接收到了自己发送的 SYN-ACK（包含服务器的 ISN）。如果这个 SYN-ACK 丢失，客户端根本不知道服务器已准备就绪，而服务器却认为连接已建立并开始等待数据，此时双方状态不一致。第三次握手的 ACK 正式确认了双方对初始序列号的认可，确保了连接状态的双向同步。&lt;/p&gt;
&lt;p&gt;因此，即使网络完美无缺，两次握手在协议设计层面仍存在本质缺陷，无法保证连接的可靠性和一致性。TCP 的三次握手是基于逻辑必要性而非网络质量的设计。第三次握手的开销（一个 ACK 包）极小，与它所带来的连接可靠性保障相比，是完全可以接受的。为了节省这微不足道的开销而引入巨大的连接混乱风险，是绝对不值得的。&lt;/p&gt;
&lt;h2&gt;9. UPD 传输的数据是不是一个完整报文&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;网易游戏一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;UDP 是面向报文的传输协议，它以报文（Datagram）为单位进行传输，不分割也不合并。&lt;/li&gt;
&lt;li&gt;应用层交给 UDP 一个报文，UDP 添加头部后交给 IP 层发送。接收端收到的数据也一定是一个完整的报文（不会被分片成多个）。&lt;/li&gt;
&lt;li&gt;如果报文超过网络传输路径的最大传输单元（MTU），则 IP 层（非 UDP）会对报文分片传输，并在接收端自动重组后再交给 UDP。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;10. UDP 如何保证传输效率又不丢包&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;网易游戏一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;UDP 本身不保证可靠性，也就是说它不会主动处理丢包或重传。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果应用层需要在 UDP 基础上实现高效可靠传输，需要自己实现：
&lt;ul&gt;
&lt;li&gt;ACK 应答机制（确认重传）&lt;/li&gt;
&lt;li&gt;序列号机制（识别丢包、乱序）&lt;/li&gt;
&lt;li&gt;流量控制、拥塞控制&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实践中，比如实时音视频通话、游戏等场景，更倾向于使用 UDP，偶尔丢包可接受，但延迟必须低，&lt;strong&gt;可靠性可以通过应用层算法进行补偿（如丢帧插值、快速重传策略）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此 UDP 本身&lt;strong&gt;仅保证效率&lt;/strong&gt;，可靠性需要&lt;strong&gt;应用层自己设计补偿机制&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;11. TCP 和 UDP 粘包分包，怎么解决？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;网易游戏一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;粘包&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2021/png/92791/1609848617470-e3c6dc94-17e6-429d-abad-aa9ce71c84f1.png#crop=0&amp;#x26;crop=0&amp;#x26;crop=1&amp;#x26;crop=1&amp;#x26;height=262&amp;#x26;id=DJZpo&amp;#x26;margin=%5Bobject%20Object%5D&amp;#x26;name=image.png&amp;#x26;originHeight=524&amp;#x26;originWidth=1118&amp;#x26;originalType=binary&amp;#x26;ratio=1&amp;#x26;rotation=0&amp;#x26;showTitle=false&amp;#x26;size=37355&amp;#x26;status=done&amp;#x26;style=none&amp;#x26;title=&amp;#x26;width=559&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;分包/半包&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2021/png/92791/1609848676317-81754963-e040-4216-82a6-9ffa4d4eb84b.png#crop=0&amp;#x26;crop=0&amp;#x26;crop=1&amp;#x26;crop=1&amp;#x26;height=261&amp;#x26;id=Hd5O0&amp;#x26;margin=%5Bobject%20Object%5D&amp;#x26;name=image.png&amp;#x26;originHeight=522&amp;#x26;originWidth=1102&amp;#x26;originalType=binary&amp;#x26;ratio=1&amp;#x26;rotation=0&amp;#x26;showTitle=false&amp;#x26;size=35199&amp;#x26;status=done&amp;#x26;style=none&amp;#x26;title=&amp;#x26;width=551&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;粘包、分包问题的原因&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TCP 是面向&lt;strong&gt;流&lt;/strong&gt;的协议，没有报文边界的概念，数据会合并、分割。&lt;/li&gt;
&lt;li&gt;UDP 是面向&lt;strong&gt;报文&lt;/strong&gt;的协议，有天然的报文边界，不存在粘包问题，但仍可能因报文超过 MTU 而发生分片（IP 层分片重组）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;TCP 解决粘包分包的常见方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;长度字段法&lt;/strong&gt;：报文头部增加一个字段表示当前报文长度，接收方按长度拆分数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分隔符法&lt;/strong&gt;：在报文末尾增加特殊分隔符标记报文结束（如 HTTP 协议中 &lt;code&gt;\r\n&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;固定长度法&lt;/strong&gt;：每个消息长度固定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议约定&lt;/strong&gt;：使用标准协议（如 HTTP、Protobuf）明确消息结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;UDP 是否有粘包问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UDP 天然不存在粘包问题，但应保证数据小于 MTU，避免 IP 分片。&lt;/li&gt;
&lt;li&gt;即使数据报被 IP 层分片，在 UDP 层也会被重组好再交给应用层。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;这是一个非常经典的网络编程问题，接下来我会用清晰易懂的方式解释 TCP 粘包和分包。&lt;/p&gt;
&lt;p&gt;首先要理解最关键的一点：&lt;strong&gt;TCP 是面向字节流的协议&lt;/strong&gt;，它不关心应用层数据的边界。&lt;/p&gt;
&lt;p&gt;想象一下 TCP 连接就像一根水管，发送方不断地往里面倒水（数据），接收方从另一端接水。TCP 保证水的顺序是对的，并且不丢失，但它不保证你每次接起来的一瓢水，正好是发送方每次倒进去的那一壶。你可能一次接到两壶水（粘包），也可能只接到半壶水（分包）。&lt;/p&gt;
&lt;h3&gt;(1) TCP 粘包&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;：指发送方发送的多个数据包，在接收方接收时被“粘”在了一起，变成一个大的数据包。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通俗比喻&lt;/strong&gt;：发送方分别发送了“Hello”和“World”两个包。接收方一次读取可能收到的是“HelloWorld”，两个包粘在了一起。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;产生原因&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Nagle 算法&lt;/strong&gt;：为了优化网络效率，TCP 默认会使用 Nagle 算法。该算法会将多个小的、发送间隔短的数据包合并成一个大的数据包再发送，从而减少网络上的小包数量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接收方缓冲区积累&lt;/strong&gt;：发送方发送数据包的速度快于接收方应用层读取数据的速度，导致多个数据包在接收方的缓冲区中堆积在一起，当应用层一次读取足够大的缓冲区时，就把多个包读出来了。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;发送方：
[Packet A] [Packet B] [Packet C]
      |          |          |
      v          v          v
TCP 发送流： |--A--|--B--|--C--|
                    |
                    v
接收方缓冲区： |-----A+B+C-----|  &amp;#x3C;- 应用一次读取到了合并的数据
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;(2) TCP 分包&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;：指发送方发送的一个数据包，在接收方接收时被拆分成多个数据包接收。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通俗比喻&lt;/strong&gt;：发送方发送了一个“HelloWorld”包。接收方可能第一次读取到“Hel”，第二次读取到“loWo”，第三次读取到“rld”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;产生原因&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;MSS 限制&lt;/strong&gt;：TCP 有一个最大报文段长度。如果应用层下发的数据包大小超过了 MSS，TCP 层在发送前必须对这个数据包进行拆分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MTU 限制&lt;/strong&gt;：在数据链路层，网络接口有一个最大传输单元。如果 IP 数据包（包含 TCP 头和数据）超过了 MTU，它也会被分片传输。虽然这对 TCP 是透明的，但本质也是一种分包。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接收方缓冲区不足&lt;/strong&gt;：当接收方的 TCP 缓冲区大小小于来的数据包时，TCP 只会将缓冲区满的部分数据交付给应用层，剩下的数据要等应用层读取腾出空间后再继续交付。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;发送方：
[Packet A (很大)]
      |
      v
TCP 发送流： |--A1--|--A2--|--A3--|
                    |
                    v
接收方缓冲区： |--A1--| &amp;#x3C;- 应用第一次读取
然后： |--A2--| &amp;#x3C;- 应用第二次读取
然后： |--A3--| &amp;#x3C;- 应用第三次读取
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;(3) 为什么 UDP 没有粘包和分包问题？&lt;/h3&gt;
&lt;p&gt;作为对比，&lt;strong&gt;UDP 是面向数据报的协议&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🔥&lt;strong&gt;保护消息边界&lt;/strong&gt;：UDP 每次发送都是一个完整的、独立的数据报。接收方每次接收也必须以一个完整的 UDP 数据报为单位。即使数据报被 IP 层分片，在 UDP 层也会被重组好再交给应用层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要么收全，要么丢包&lt;/strong&gt;：如果接收方的缓冲区放不下一个完整的 UDP 数据报，这个数据报就会被直接丢弃。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，对于 UDP：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发送多少次 &lt;code&gt;sendto&lt;/code&gt;，接收方就需要多少次 &lt;code&gt;recvfrom&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;每次 &lt;code&gt;recvfrom&lt;/code&gt; 得到的数据，必然是且仅是一次 &lt;code&gt;sendto&lt;/code&gt; 发送的完整数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(4) 如何解决粘包和分包问题？&lt;/h3&gt;
&lt;p&gt;既然 TCP 本身不维护边界，就需要&lt;strong&gt;应用层自己来定义通信协议&lt;/strong&gt;，以便能从字节流中正确地拆分出原始的数据包。&lt;/p&gt;
&lt;p&gt;常见的解决方案有：&lt;/p&gt;
&lt;h4&gt;1. 定长消息&lt;/h4&gt;
&lt;p&gt;每个数据包都固定长度。例如，规定每个包都是 100 字节。如果发送的数据不足 100 字节，就用空格或 &lt;code&gt;0x00&lt;/code&gt; 填充。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：处理简单。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：浪费带宽，不灵活。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 分隔符&lt;/h4&gt;
&lt;p&gt;在每个数据包的结尾加上一个特殊的分隔符，例如换行符 &lt;code&gt;\n&lt;/code&gt;。接收方通过这个分隔符来切分数据流。HTTP 协议的头部分就是用 &lt;code&gt;\r\n&lt;/code&gt; 来分隔的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：简单直观。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：如果消息体本身包含分隔符，需要转义处理，增加复杂性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 长度前缀（最常用、最标准的方法）&lt;/h4&gt;
&lt;p&gt;在数据包的首部加上一个固定长度的字段，用来表示包体的长度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;处理流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;应用层协议定义一个头部，比如前 4 个字节 (&lt;code&gt;uint32_t&lt;/code&gt;) 表示包体长度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发送时，先计算包体长度，写入头部，再写入包体数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接收时：&lt;/p&gt;
&lt;p&gt;a. 先读取固定长度的头部（例如 4 字节）。&lt;/p&gt;
&lt;p&gt;b. 解析头部，得到后面包体的真实长度 N。&lt;/p&gt;
&lt;p&gt;c. 继续从缓冲区读取至少 N 个字节，这样就得到了一个完整的数据包。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;应用层数据包结构：
[ 4字节长度头 (0x00000005) ] [ 5字节包体 (&quot;Hello&quot;) ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;(5) 为什么要解决 TCP 粘包和分包/半包问题？&lt;/h3&gt;
&lt;p&gt;解决粘包和分包问题&lt;strong&gt;根本目的是为了能让接收方正确地、无歧义地还原出发送方发出的原始业务消息&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果不解决，会导致通信完全无法正常进行。&lt;/p&gt;
&lt;h4&gt;1. 业务逻辑的完整性被破坏&lt;/h4&gt;
&lt;p&gt;应用程序是基于“消息”或“请求”来工作的。每个数据包通常代表一个完整的业务指令。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;例子&lt;/strong&gt;：一个在线游戏，客户端发送了两个指令：“攻击A”和“使用药水”。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;粘包后果&lt;/strong&gt;：服务器可能一次性收到“攻击A使用药水”，它无法理解这个合并的字符串是什么指令，导致逻辑错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分包后果&lt;/strong&gt;：服务器先收到“攻击”，它不知道要攻击谁，等下一个包“A”到来时，逻辑已经混乱。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;不解决的后果&lt;/strong&gt;：应用程序无法理解对方在说什么，业务逻辑无法执行。&lt;/p&gt;
&lt;h4&gt;2. 数据解析失败&lt;/h4&gt;
&lt;p&gt;很多标准的数据格式（如 JSON, XML）和序列化协议（如 Protobuf）要求一个完整的数据块才能被正确解析。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;例子&lt;/strong&gt;：客户端发送了一个 JSON 字符串 &lt;code&gt;{&quot;action&quot;: &quot;login&quot;, &quot;user&quot;: &quot;alice&quot;}&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;粘包后果&lt;/strong&gt;：接收方可能收到 &lt;code&gt;{&quot;action&quot;: &quot;login&quot;, &quot;user&quot;: &quot;alice&quot;}{&quot;action&quot;: &quot;logout&quot;, &quot;user&quot;: &quot;bob&quot;}&lt;/code&gt;，这根本不是合法的 JSON，解析器会直接报错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分包后果&lt;/strong&gt;：接收方第一次只收到 &lt;code&gt;{&quot;action&quot;: &quot;login&quot;&lt;/code&gt;，这同样是一个残缺的、非法的 JSON，解析也会失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;不解决的后果&lt;/strong&gt;：数据反序列化失败，程序可能崩溃或抛出异常。&lt;/p&gt;
&lt;h4&gt;3. 状态同步混乱&lt;/h4&gt;
&lt;p&gt;在客户端-服务器模型中，通信双方都维护着一定的状态。消息的顺序和完整性对状态同步至关重要。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;例子&lt;/strong&gt;：金融交易系统，服务器依次发送两条消息：“余额=100元” 和 “扣款20元”。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;粘包后果&lt;/strong&gt;：客户端收到“余额=100元扣款20元”，无法解析，不知道当前余额是多少。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分包后果&lt;/strong&gt;：客户端先收到“余额=100”，然后卡住了，它以为余额就是100，但随后收到的“元扣款20元”又无法理解，导致UI显示错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;不解决的后果&lt;/strong&gt;：客户端和服务器状态不一致，用户体验受损，甚至造成数据错误。&lt;/p&gt;
&lt;h4&gt;4. 协议本身无法工作&lt;/h4&gt;
&lt;p&gt;很多标准的应用层协议（如 HTTP、FTP、Redis 协议）都内置了解决粘包/分包的机制。如果你自己设计的协议不处理这个问题，那就等同于这个协议是无效的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;以 HTTP 为例&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;它的“长度前缀”就是 &lt;code&gt;Content-Length&lt;/code&gt; 头&lt;/strong&gt;。浏览器读取到 HTTP 头部，发现 &lt;code&gt;Content-Length: 1024&lt;/code&gt;，就知道在头部之后，还需要读取 1024 字节才能得到一个完整的响应体。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;它的“分隔符”就是 &lt;code&gt;\r\n&lt;/code&gt;&lt;/strong&gt;。通过读取头部的 &lt;code&gt;\r\n\r\n&lt;/code&gt; 来分隔头部和主体。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果 HTTP 不解决粘包/分包，网页将永远无法正常加载。&lt;/p&gt;
&lt;h3&gt;(6) 总结&lt;/h3&gt;
&lt;p&gt;| 特性         | TCP（流式协议）                  | UDP（数据报协议）                      |
| :----------- | :------------------------------- | :------------------------------------- |
| &lt;strong&gt;数据边界&lt;/strong&gt; | &lt;strong&gt;不保护&lt;/strong&gt;，可能导致粘包和分包   | &lt;strong&gt;保护&lt;/strong&gt;，每次接收都是一个完整的数据报 |
| &lt;strong&gt;解决方案&lt;/strong&gt; | &lt;strong&gt;应用层协议解决&lt;/strong&gt;，如长度前缀法 | 无需额外处理                           |&lt;/p&gt;
&lt;p&gt;理解 TCP 粘包和分包是进行可靠网络编程的基础。&lt;strong&gt;在实践中，几乎总是使用“长度前缀”的方法来设计应用层协议&lt;/strong&gt;，例如 Google 的 Protocol Buffers 和许多消息队列的通信协议都采用这种方式。&lt;/p&gt;
&lt;h2&gt;12. TCP 为什么是四次挥手&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;网易游戏一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TCP 四次挥手的原因：TCP 连接是全双工通信，数据在两个方向上可以独立传输，需要分别关闭。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;四次挥手的过程&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;主动方                被动方
  |------- FIN ------&gt;|
  |                   |
  |&amp;#x3C;------ ACK -------|
  |                   |
  |                   |（被动方可能仍有数据要发送）
  |&amp;#x3C;------ FIN -------|
  |                   |
  |------- ACK ------&gt;|
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一次挥手&lt;/strong&gt;：主动关闭方发出 &lt;code&gt;FIN&lt;/code&gt;，表示我方数据发送完毕。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二次挥手&lt;/strong&gt;：被动方发送 &lt;code&gt;ACK&lt;/code&gt;，确认收到对方的关闭请求，但此时被动方可能还有数据未发送完。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三次挥手&lt;/strong&gt;：被动方发完数据后再发送 &lt;code&gt;FIN&lt;/code&gt;，通知主动方自己数据也发送完毕。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第四次挥手&lt;/strong&gt;：主动方再发送 &lt;code&gt;ACK&lt;/code&gt;，确认收到被动方的关闭请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;13. 在浏览器中访问一个 http 服务器，这里面会经过哪些协议？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;快手一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当你在浏览器里输入一个 &lt;code&gt;http://...&lt;/code&gt; 地址访问服务器时，整个过程会涉及到一系列分层的协议，每一层负责不同的功能。整体遵循的是 &lt;strong&gt;TCP/IP 协议栈&lt;/strong&gt;，从应用层到物理层逐层完成通信。可以这样理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在最顶层，浏览器使用 &lt;strong&gt;HTTP 协议&lt;/strong&gt; 发出请求。HTTP 是应用层协议，规定了请求报文和响应报文的格式，例如 &lt;code&gt;GET /index.html HTTP/1.1&lt;/code&gt;。如果用的是 HTTPS，那么在 HTTP 之前会先进行 TLS/SSL 握手，加密后才传输数据。&lt;/li&gt;
&lt;li&gt;HTTP 请求需要依赖 &lt;strong&gt;TCP 协议&lt;/strong&gt; 提供可靠的字节流传输。浏览器和服务器之间首先通过三次握手建立 TCP 连接，之后 HTTP 报文就会作为 TCP 的数据部分发送。TCP 协议保证了数据的有序性和完整性，丢包时会自动重传。&lt;/li&gt;
&lt;li&gt;TCP 连接又要通过 &lt;strong&gt;IP 协议&lt;/strong&gt; 来实现跨主机的数据包传递。IP 协议定义了地址和路由机制，确保数据从浏览器所在的主机能够正确送达目标服务器的 IP 地址。如果访问的是一个域名，浏览器会先用 &lt;strong&gt;DNS 协议&lt;/strong&gt; 把域名解析成 IP 地址，这一步也是不可或缺的。&lt;/li&gt;
&lt;li&gt;在链路上传输时，IP 报文会被封装在 &lt;strong&gt;以太网协议（或 Wi-Fi 协议）&lt;/strong&gt; 的帧里，通过物理层的比特流传输。为了更高效地在局域网找到目标设备，还会用到 &lt;strong&gt;ARP 协议&lt;/strong&gt; 来把 IP 地址解析为 MAC 地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以整体过程是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DNS&lt;/strong&gt;：把域名解析为服务器 IP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ARP&lt;/strong&gt;（如果需要）：在局域网内解析 IP 到 MAC。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP&lt;/strong&gt;：通过三次握手建立连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP&lt;/strong&gt;：浏览器发出请求，服务器返回响应。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP/以太网/物理层&lt;/strong&gt;：底层负责把数据包真正送到服务器。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果是 &lt;code&gt;https://&lt;/code&gt;，还要加上 &lt;strong&gt;TLS/SSL&lt;/strong&gt; 层，建立加密通道后才开始传输 HTTP 数据。&lt;/p&gt;
&lt;h2&gt;14. 为什么不直接用 tcp 协议，还需要用 http 协议？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;快手一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是个非常经典的问题。我们完全可以用 TCP 在两台机器之间收发字节流，但光靠 TCP，还缺少很多“应用层语义”。HTTP 就是为了解决这些问题而存在的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;首先，TCP 只是传输层协议&lt;/strong&gt;。它提供的功能是：建立可靠的、双向的字节流通道，保证数据顺序不乱、不丢、不重。它并不知道字节流里是什么内容，也没有规定数据要如何划分、如何解释。你完全可以在 TCP 上自己定义一套格式来交流，但如果没有标准，不同的应用之间就无法互通。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HTTP 是应用层协议&lt;/strong&gt;，它定义了浏览器和服务器之间通信的统一格式。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何表示“我要获取某个资源”（GET 请求）；&lt;/li&gt;
&lt;li&gt;如何附带参数、发送表单或文件（POST 请求）；&lt;/li&gt;
&lt;li&gt;如何在请求头里写出数据类型、压缩方式、认证信息（Header）；&lt;/li&gt;
&lt;li&gt;如何让服务器告诉客户端返回的状态（200 OK、404 Not Found）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有了这些约定，不同厂商的浏览器、服务器都能在一个共同的语义上交流，而不仅仅是传输字节。&lt;/p&gt;
&lt;p&gt;如果我们直接用 TCP，理论上也能实现浏览器和服务器交互，但必须自己再设计一整套协议，比如：如何表示一个请求的开始和结束，怎么传递参数，怎么返回错误码，怎么处理长连接。这些 HTTP 已经帮我们定义好了，相当于是建立在 TCP 之上的一个通用“语言”。&lt;/p&gt;
&lt;p&gt;再进一步看，HTTP 还解决了 &lt;strong&gt;扩展性和兼容性&lt;/strong&gt; 的问题。随着需求变化，它不断引入新特性：从 HTTP/1.0 的简单请求，到 HTTP/1.1 的持久连接、管道化，再到 HTTP/2 的多路复用、头部压缩，HTTP/3 甚至换到了 QUIC（基于 UDP）。如果只用裸 TCP，这些机制都需要自己重新造轮子。&lt;/p&gt;
&lt;h2&gt;15. 如何查看 HTTP 丢包的情况？&lt;/h2&gt;
&lt;p&gt;HTTP 基于 TCP，因此 HTTP 丢包实质是网络层或传输层的问题。排查步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;使用 &lt;code&gt;ping&lt;/code&gt; 命令&lt;/strong&gt;：检测到目标服务器的基本连通性和网络延迟，看是否有包丢失（packet loss）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用 &lt;code&gt;traceroute&lt;/code&gt;（Windows 是 &lt;code&gt;tracert&lt;/code&gt;）&lt;/strong&gt;：追踪数据包经过的路径，定位在哪个网络节点出现丢包或高延迟&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用 &lt;code&gt;curl -v&lt;/code&gt; 或 &lt;code&gt;wget&lt;/code&gt;&lt;/strong&gt;：详细输出 HTTP 请求/响应过程，观察连接建立、SSL握手、数据传输各阶段是否超时或失败&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;专业工具&lt;/strong&gt;：使用 &lt;code&gt;tcpdump&lt;/code&gt; 抓包，然后用 Wireshark 分析，可以精确看到 TCP 重传（Retransmission），这是判断丢包的最直接证据&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;16. GET 请求和 POST 请求二者有什么区别？分别什么场景下使用？&lt;/h2&gt;
&lt;p&gt;根本区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GET 是&lt;strong&gt;幂等&lt;/strong&gt;的（多次执行效果相同）且&lt;strong&gt;安全的&lt;/strong&gt;（不修改服务器数据），主要设计用于获取资源。&lt;/li&gt;
&lt;li&gt;POST 是&lt;strong&gt;非幂等&lt;/strong&gt;的，主要设计用于提交数据并可能修改服务器状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体差异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数位置&lt;/strong&gt;：GET 参数在 URL 查询字符串中，POST 在请求体内。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;/strong&gt;：GET 参数暴露在 URL 和浏览器历史中，不适合传敏感信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据长度&lt;/strong&gt;：GET 受 URL 长度限制（通常2KB-8KB），POST 可传输更大数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存与书签&lt;/strong&gt;：GET 可被缓存、可收藏为书签，POST 一般不会。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GET 用于搜索、筛选、跳转页面等获取数据的操作。&lt;/li&gt;
&lt;li&gt;POST 用于登录、注册、下单、支付等修改数据的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;17. 在淘宝下浏览，cookie 是在淘宝域这边，那如果从淘宝跳到支付宝，怎么处理中间这些过程呢？&lt;/h2&gt;
&lt;p&gt;非常好、非常核心的一个问题！这个问题涉及到单点登录（SSO, Single Sign-On）的核心流程。下面我以一个清晰、分步骤的方式为你解释整个处理过程。&lt;/p&gt;
&lt;h3&gt;核心问题：为什么淘宝的Cookie不能直接用于支付宝？&lt;/h3&gt;
&lt;p&gt;简单回答：&lt;strong&gt;浏览器基于安全原因实施的“同源策略”（Same-origin policy）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;淘宝的域名可能是&lt;/strong&gt; &lt;code&gt;taobao.com&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支付宝的域名是&lt;/strong&gt; &lt;code&gt;alipay.com&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它们的域名不同，因此浏览器在向支付宝的服务器发送请求时，&lt;strong&gt;绝不会&lt;/strong&gt;携带淘宝域名下的Cookie。这是至关重要的安全机制，防止恶意网站窃取你在其他网站的登录状态。&lt;/p&gt;
&lt;h3&gt;解决方案：基于信任的“授权”流程&lt;/h3&gt;
&lt;p&gt;既然Cookie不能共享，淘宝就需要用一种“信物”告诉支付宝：“这个用户是我这儿来的，他已经在我这儿登录了，你也要让他登录。” 这个流程的核心是&lt;strong&gt;令牌（Token）&lt;/strong&gt; 的传递和验证。&lt;/p&gt;
&lt;p&gt;整个过程可以概括为以下几个关键步骤：&lt;/p&gt;
&lt;h4&gt;第1步：用户点击支付，发起跳转&lt;/h4&gt;
&lt;p&gt;用户在淘宝选好商品，点击“付款”按钮。此时，淘宝后端会生成一个&lt;strong&gt;临时的、一次性的令牌&lt;/strong&gt;，我们通常称之为 &lt;strong&gt;授权码（Auth Code）&lt;/strong&gt;。然后，淘宝会将用户浏览器重定向到支付宝的登录/授权页面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;跳转的URL看起来会像这样：&lt;/strong&gt;
&lt;code&gt;https://auth.alipay.com/authorize?client_id=淘宝在支付宝注册的ID&amp;#x26;redirect_uri=淘宝的回调地址&amp;#x26;state=一个随机字符串（防CSRF攻击）&amp;#x26;auth_code=刚刚生成的临时令牌&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键点：&lt;/strong&gt; 这个 &lt;code&gt;auth_code&lt;/code&gt; 就是淘宝给支付宝的“介绍信”。&lt;/p&gt;
&lt;h4&gt;第2步：浏览器跳转到支付宝&lt;/h4&gt;
&lt;p&gt;用户的浏览器根据上一步的URL，跳转到了支付宝的域名下。此时，浏览器会携带支付宝自己的Cookie（如果用户之前登录过支付宝且Cookie未过期，那么他已经是登录状态）。&lt;/p&gt;
&lt;h4&gt;第3步：支付宝验证授权码&lt;/h4&gt;
&lt;p&gt;支付宝的服务器接收到这个请求后，会做几件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;验证用户登录状态&lt;/strong&gt;：检查浏览器请求中携带的支付宝Cookie。如果用户已登录，支付宝就知道当前用户是谁。如果未登录，会先引导用户输入账号密码登录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;联系淘宝进行验证&lt;/strong&gt;：支付宝的服务器会&lt;strong&gt;在后台（服务器到服务器）&lt;/strong&gt;，通过一个安全的API接口，联系淘宝的服务器。它会说：“我收到了一个授权码 &lt;code&gt;auth_code=xxx&lt;/code&gt;，请问这个码是有效的吗？它对应的是哪个用户？”&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;第4步：淘宝确认授权码有效性&lt;/h4&gt;
&lt;p&gt;淘宝的服务器在后台收到支付宝的查询请求后，会：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;验证这个 &lt;code&gt;auth_code&lt;/code&gt; 是否是自己刚生成的、是否在有效期内、是否已经被使用过。&lt;/li&gt;
&lt;li&gt;如果一切有效，淘宝会告诉支付宝：“这个码有效，它对应的用户ID是 &lt;code&gt;TB123456&lt;/code&gt;。”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;关键点：&lt;/strong&gt; 这个服务器对服务器的通信是绝对安全的，因为它们之间有预先约定好的身份验证方式（例如使用App Secret），避免了中间人伪造请求。&lt;/p&gt;
&lt;h4&gt;第5步：支付宝建立本地会话并重定向回淘宝&lt;/h4&gt;
&lt;p&gt;支付宝确认了用户身份后：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在自己的系统中，为这个用户（TB123456对应的支付宝用户）创建一个&lt;strong&gt;支付宝域的登录会话（Session）&lt;/strong&gt;，并生成一个&lt;strong&gt;支付宝的令牌（比如叫 &lt;code&gt;alipay_token&lt;/code&gt;）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;通常，它会通过Set-Cookie头部，将这个会话信息设置为支付宝域名下的Cookie。&lt;/li&gt;
&lt;li&gt;然后，支付宝将浏览器重定向回第一步中 &lt;code&gt;redirect_uri&lt;/code&gt; 指定的淘宝回调地址，并附上成功的信号和这个 &lt;code&gt;alipay_token&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;回跳的URL像这样：&lt;/strong&gt;
&lt;code&gt;https://www.taobao.com/callback?from=alipay&amp;#x26;result=success&amp;#x26;alipay_token=支付宝生成的令牌&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;第6步：淘宝确认最终状态，完成支付流程&lt;/h4&gt;
&lt;p&gt;用户的浏览器跳回淘宝的页面。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;淘宝的后端收到 &lt;code&gt;alipay_token&lt;/code&gt; 后，会再次在后台向支付宝验证这个令牌的有效性（同样是安全的服务器间通信）。&lt;/li&gt;
&lt;li&gt;验证通过后，淘宝就知道：“好了，用户已经在支付宝那边登录成功了，支付环节可以继续了。”&lt;/li&gt;
&lt;li&gt;随后，淘宝会展示支付页面，用户输入密码完成支付。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;技术总结与关键点&lt;/h3&gt;
&lt;p&gt;| 步骤               | 发生地点 | 关键动作                                         | 通信方向                                   |
| :----------------- | :------- | :----------------------------------------------- | :----------------------------------------- |
| 1. 准备跳转        | 淘宝     | 生成临时 &lt;code&gt;auth_code&lt;/code&gt;，拼接跳转URL                | 用户浏览器 -&gt; 淘宝 -&gt; &lt;strong&gt;重定向&lt;/strong&gt; -&gt; 支付宝 |
| 2. 跳转            | 浏览器   | 携带&lt;strong&gt;支付宝Cookie&lt;/strong&gt;访问支付宝                   | 用户浏览器 -&gt; 支付宝                       |
| 3. 验证授权码      | &lt;strong&gt;后台&lt;/strong&gt; | 支付宝服务器用 &lt;code&gt;auth_code&lt;/code&gt; 询问淘宝服务器        | &lt;strong&gt;支付宝服务器 &amp;#x3C; - &gt; 淘宝服务器&lt;/strong&gt;            |
| 4. 确认身份        | &lt;strong&gt;后台&lt;/strong&gt; | 淘宝服务器告知支付宝用户身份                     | &lt;strong&gt;淘宝服务器 -&gt; 支付宝服务器&lt;/strong&gt;             |
| 5. 建立会话 &amp;#x26; 回跳 | 支付宝   | 支付宝设置自身Cookie，带 &lt;code&gt;alipay_token&lt;/code&gt; 跳回淘宝 | 用户浏览器 -&gt; 支付宝 -&gt; &lt;strong&gt;重定向&lt;/strong&gt; -&gt; 淘宝 |
| 6. 最终确认        | &lt;strong&gt;后台&lt;/strong&gt; | 淘宝用 &lt;code&gt;alipay_token&lt;/code&gt; 向支付宝确认               | &lt;strong&gt;淘宝服务器 &amp;#x3C; - &gt; 支付宝服务器&lt;/strong&gt;            |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心思想：&lt;/strong&gt;
&lt;strong&gt;通过浏览器重定向传递一次性临时令牌（Auth Code），再通过后端服务器之间的安全通信来验证用户真实身份，最终在目标网站（支付宝）上建立独立的本地会话。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这种模式是OAuth 2.0等标准授权协议的精髓。对于用户来说，如果他已经登录了支付宝，整个过程几乎是无感的，点击“付款”后直接就到了支付界面，实现了流畅的单点登录体验。&lt;/p&gt;
&lt;h2&gt;18. TCP 为什么“四次挥手”，三次不行？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：TCP 全双工，两条方向&lt;strong&gt;独立关闭&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;步骤：A 发 &lt;strong&gt;FIN&lt;/strong&gt; → B 回 &lt;strong&gt;ACK&lt;/strong&gt;（A→B 方向关闭）；B 还有数据要发，等发完再发 &lt;strong&gt;FIN&lt;/strong&gt; → A 回 &lt;strong&gt;ACK&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;若“三次”把 B 的 ACK 和 FIN 合并，&lt;strong&gt;无法覆盖“半关闭继续发送”的常见场景&lt;/strong&gt;（B 可能还有尾包）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TIME_WAIT&lt;/strong&gt;（最后 ACK 的一方进入）：防止旧包影响后续连接，确保对端能重传 FIN 时仍可应答。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/20250823-mxoA5u.BsXTbImn.png"/><enclosure url="/_astro/20250823-mxoA5u.BsXTbImn.png"/></item><item><title>八股文 @ 场景题</title><link>https://coooredump.github.io/blog/recruitment/2025-scenario-questions</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/2025-scenario-questions</guid><description>记录面经高频场景题，实时更新中...</description><pubDate>Sat, 23 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;记录高频场景题，部分内容自行搜索&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;快排存在的问题，如何优化？&lt;/h2&gt;
&lt;p&gt;3 种快排基准选择方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;随机（rand 函数）&lt;/li&gt;
&lt;li&gt;固定（队首、队尾）&lt;/li&gt;
&lt;li&gt;三数取中（队首、队中和队尾的中间数）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;4 种优化方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优化 1：当待排序序列的长度分割到一定大小后，使用插入排序&lt;/li&gt;
&lt;li&gt;优化 2：在一次分割结束后，可以把与 key 相等的元素聚在一起，继续下次分割时，不用再对与 key 相等元素分割&lt;/li&gt;
&lt;li&gt;优化 3：优化递归操作&lt;/li&gt;
&lt;li&gt;优化 4：使用并行或多线程处理子序列&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;写三个线程交替打印 ABC&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;thread&gt;
#include &amp;#x3C;mutex&gt;
#include &amp;#x3C;condition_variable&gt;

std::mutex mtx;
std::condition_variable cv;
int turn = 0;

void print(char c, int id) {
    for (int i = 0; i &amp;#x3C; 5; i++) {
        std::unique_lock&amp;#x3C;std::mutex&gt; lock(mtx);
        cv.wait(lock, [id] { return turn == id; });
        std::cout &amp;#x3C;&amp;#x3C; c;
        turn = (turn + 1) % 3;
        cv.notify_all();
    }
}

int main() {
    std::thread A(print, &apos;A&apos;, 0);
    std::thread B(print, &apos;B&apos;, 1);
    std::thread C(print, &apos;C&apos;, 2);
    
    A.join(); B.join(); C.join();
    std::cout &amp;#x3C;&amp;#x3C; std::endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不使用临时变量实现 swap 函数&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void swap_xor(int a, int b) {
    a ^= b;
    b ^= a;
    a ^= b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;✍️ Top K 问题（可以采取的方法有哪些，各自优点）&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;✍️ 8G 的 int 型数据，计算机的内存只有 2G，怎么对它进行排序？&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;✍️ 手撕线程安全的单例模式&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;手撕 shared_ptr（线程安全）&lt;/h2&gt;
&lt;h3&gt;非线程安全的简单实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;memory&gt;

template&amp;#x3C;typename T&gt;
class smartPtr {
private:
    T *_ptr;
    size_t* _count;

public:
    smartPtr(T *ptr = nullptr):_ptr(ptr) {
        if (_ptr) {
            _count = new size_t(1);
        } else {
            _count = new size_t(0);
        }
    }

    smartPtr(const smartPtr &amp;#x26;ptr) {
        if (this != &amp;#x26;ptr) {
            this-&gt;_ptr = ptr._ptr;
            this-&gt;_count = ptr._count;
            ++(*this-&gt;_count)   ;
        }
    }

    smartPtr&amp;#x26; operator=(const smartPtr &amp;#x26;ptr) {
        if (this-&gt;_ptr == ptr._ptr)
            return *this;

        if (this-&gt;_ptr) {
            --(*this-&gt;_count);
            if (this-&gt;_count == 0) {
                delete this-&gt;_ptr;
                delete this-&gt;_count;
            }
        }

        this-&gt;_ptr = ptr._ptr;
        this-&gt;_count = ptr._count;
        ++(*this-&gt;_count);

        return *this;
    }

    ~smartPtr() {
        --(*this-&gt;_count);
        if (0 == *this-&gt;_count) {
            delete this-&gt;_ptr;
            delete this-&gt;_count;
        }
    }

    size_t use_count() {
        return *this-&gt;_count;
    }

    T&amp;#x26; operator*() {
        assert(this-&gt;_ptr == nullptr);
        return *(this-&gt;_ptr);
    }

    T* operator-&gt;() {
        assert(this-&gt;_ptr == nullptr);
        return this-&gt;_ptr;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基于原子操作的线程安全实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#pragma once

#include &amp;#x3C;atomic&gt;  // 引入原子操作

template &amp;#x3C;typename T&gt;
class shared_ptr {
private:
  T* ptr;                               // 指向管理的对象
  std::atomic&amp;#x3C;std::size_t&gt;* ref_count;  // 原子引用计数

  // 释放资源
  void release() {
    // P.S. 这里使用 std::memory_order_acq_rel 内存序，保证释放资源的同步
    if (ref_count &amp;#x26;&amp;#x26; ref_count-&gt;fetch_sub(1, std::memory_order_acq_rel) == 1) {
      delete ptr;
      delete ref_count;
    }
  }

public:
  // 默认构造函数
  shared_ptr() : ptr(nullptr), ref_count(nullptr) {}

  // 构造函数
  // P.S. 这里使用 explicit 关键字，防止隐式类型转换
  // shared_ptr&amp;#x3C;int&gt; ptr1 = new int(10);  不允许出现
  explicit shared_ptr(T* p) : ptr(p), ref_count(p ? new std::atomic&amp;#x3C;std::size_t&gt;(1) : nullptr) {}

  // 析构函数
  ~shared_ptr() { release(); }

  // 拷贝构造函数
  shared_ptr(const shared_ptr&amp;#x3C;T&gt;&amp;#x26; other) : ptr(other.ptr), ref_count(other.ref_count) {
    if (ref_count) {
      ref_count-&gt;fetch_add(1, std::memory_order_relaxed);  // 引用计数增加，不需要强内存序
    }
  }

  // 拷贝赋值运算符
  shared_ptr&amp;#x3C;T&gt;&amp;#x26; operator=(const shared_ptr&amp;#x3C;T&gt;&amp;#x26; other) {
    if (this != &amp;#x26;other) {
      release();  // 释放当前资源
      ptr = other.ptr;
      ref_count = other.ref_count;
      if (ref_count) {
        ref_count-&gt;fetch_add(1, std::memory_order_relaxed);  // 引用计数增加
      }
    }
    return *this;
  }

  // 移动构造函数
  // P.S. noexcept 关键字表示该函数不会抛出异常。
  // 标准库中的某些操作（如 std::swap）要求移动操作是 noexcept 的，以确保异常安全。
  // noexcept 可以帮助编译器生成更高效的代码，因为它不需要为异常处理生成额外的代码。
  shared_ptr(shared_ptr&amp;#x3C;T&gt;&amp;#x26;&amp;#x26; other) noexcept : ptr(other.ptr), ref_count(other.ref_count) {
    other.ptr = nullptr;
    other.ref_count = nullptr;
  }

  // 移动赋值运算符
  shared_ptr&amp;#x3C;T&gt;&amp;#x26; operator=(shared_ptr&amp;#x3C;T&gt;&amp;#x26;&amp;#x26; other) noexcept {
    if (this != &amp;#x26;other) {
      release();  // 释放当前资源
      ptr = other.ptr;
      ref_count = other.ref_count;
      other.ptr = nullptr;
      other.ref_count = nullptr;
    }
    return *this;
  }

  // 解引用运算符
  // P.S. const 关键字表示该函数不会修改对象的状态。
  T&amp;#x26; operator*() const { return *ptr; }

  // 箭头运算符
  T* operator-&gt;() const { return ptr; }

  // 获取引用计数
  std::size_t use_count() const { return ref_count ? ref_count-&gt;load(std::memory_order_acquire) : 0; }

  // 获取原始指针
  T* get() const { return ptr; }

  // 重置指针
  void reset(T* p = nullptr) {
    release();
    ptr = p;
    ref_count = p ? new std::atomic&amp;#x3C;std::size_t&gt;(1) : nullptr;
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;✍️ 手撕线程池&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;✍️ 手撕 string&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;✍️ 手撕 ringbuffer&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;✍️ 实现一个线程安全的带过期时间的 FIFO Cache&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;✍️ 实现一个线程安全的带过期时间的 LRU&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;✍️ 一致性哈希&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;✍️ 海量数据的 bitmap 使用原理&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;✍️ 布隆过滤器原理与优点&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;高并发情况下对 LRU 的优化方案&lt;/h2&gt;
&lt;h3&gt;痛点&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;全局锁/链表热点&lt;/strong&gt;：经典 LRU 的 head/tail 在高并发下极易形成热点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每次访问都要“提到头”&lt;/strong&gt;：更新双向链表成本高，频繁 CAS/锁冲突。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;淘汰路径阻塞读写&lt;/strong&gt;：命中/写入与淘汰耦合，尾部扫描占用临界区。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;冷热抖动/缓存污染&lt;/strong&gt;：短瞬热键把冷数据顶走。&lt;/p&gt;
&lt;h3&gt;优化思路（从易到难逐级上车）&lt;/h3&gt;
&lt;h4&gt;1) 分片化 + 本地 LRU&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;做法&lt;/strong&gt;：按 key 的 hash 做分片（shard），每片一个独立 LRU（或近似 LRU），容量按权重均分或动态调。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;锁策略&lt;/strong&gt;：每片用细粒度锁/读写锁；读命中尽量无锁，写与晋升仅锁所在分片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：消除全局热点，线性提升吞吐；工程复杂度低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要点&lt;/strong&gt;：分片数 ≈ 2–4×CPU 核数；用 &lt;strong&gt;striped LongAdder&lt;/strong&gt; 做指标计数，减少原子热点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2) 近似 LRU，降低更新成本&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CLOCK/CLOCK-Pro&lt;/strong&gt;：用环形指针+访问位替代双向链表，晋升成本 O(1)，冲突小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2Q/SLRU&lt;/strong&gt;：新到/短热进 A 队列；稳定热留在 B 队列（或 Probation/Protected），抑制一次性流量污染。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：命中率接近 LRU，且更抗“瞬时热键”；实现简单，晋升轻。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3) Admission + TinyLFU（高命中率方案，参考 Caffeine 思路）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;做法&lt;/strong&gt;：把淘汰决策拆成两步：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Admission&lt;/strong&gt;：用 Count-Min Sketch 统计最近频次，只有当新键的估计频次 ≥ 牺牲者时才接纳（W-TinyLFU）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分层缓存&lt;/strong&gt;：Window（短热）+ Protected（稳定热）+ Probation（候选）三层滑动窗口。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：在长尾流量、穿透流量下显著提高命中率；减少无效写入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代价&lt;/strong&gt;：实现复杂一点，需要周期性衰减 Sketch。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4) 访问路径“无锁化”，把晋升/淘汰异步化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心手法&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;读路径只做 &lt;strong&gt;O(1) 查表&lt;/strong&gt;（ConcurrentHashMap/lock-free map），把“命中事件”写入每分片的 &lt;strong&gt;MPSC ring buffer&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;独立 &lt;strong&gt;单线程维护者（per-shard maintainer）&lt;/strong&gt; 从队列批量消费事件，顺序更新近似 LRU 的元数据与淘汰。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果&lt;/strong&gt;：读路径彻底避开链表/锁；写放大被批处理吸收，减少抖动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现提示&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;ring buffer 长度按峰值 QPS×延迟上限估算；丢弃策略用 coalesce（只保留最近一次命中标记）。&lt;/li&gt;
&lt;li&gt;淘汰与值回收放在维护线程，避免在读线程做内存管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5) 批量淘汰 &amp;#x26; 背景回收&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;做法&lt;/strong&gt;：容量接近阈值时，维护线程一次淘汰 N 个（例如 1–2% 容量）而不是逐个淘汰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;好处&lt;/strong&gt;：减少频繁进入临界区；尾部扫描可顺序内存访问，CPU 友好。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;6) 多级/多层缓存&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本地 L1（进程内） + 远端 L2（Redis/MC）&lt;/strong&gt;：L1 用近似 LRU + Admission，L2 用简单 LRU/TTL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨核/跨机&lt;/strong&gt;：本地每核分片 + 机内共享 L2，减少跨 NUMA 抖动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨机一致性&lt;/strong&gt;：允许短暂不一致，配合 &lt;strong&gt;stale-while-revalidate&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;7) 数据结构与内存优化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;索引&lt;/strong&gt;：map 索引 + 紧凑元数据（指针压缩、索引 offset）；避免频繁分配（对象池/arena/slab）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计数器&lt;/strong&gt;：热点计数用 &lt;strong&gt;LongAdder/striped&lt;/strong&gt;；Sketch 用 uint32 环形衰减。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Java 场景&lt;/strong&gt;：尽量 &lt;strong&gt;off-heap&lt;/strong&gt; 存元数据/大 value；避免大对象导致频繁 GC。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回收&lt;/strong&gt;：RCU/epoch-based 回收，减少停顿；避免 ABA 可用带版本的指针或 tag。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;8) 防击穿/雪崩/穿透的配套&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;singleflight&lt;/strong&gt;：同 key 的 miss 合并成一次回源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自适应 TTL + 负缓存&lt;/strong&gt;：短暂缓存“未命中/404”，降低穿透。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;退避/限流&lt;/strong&gt;：上游慢时降低 admission、缩短 window；必要时降级只读旧值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;高德地图导航网络架构设计&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;场景题：现在导航功能越来越丰富了，现在对云端的依赖越来越强，在这个背景下，我们有大量的网络业务要去做相关的业务请求，但是我们测试的车在路上跑，他的网络性能带宽相对来说不稳定性会比较强，基于费用的考虑这个整体的流量也是一个需要考虑的事情，所以整体网络上大模块的设计要考虑几个因素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个就是并发量的问题，如果并发量过大，性能负载非常高，但是车机系统给我们导航的性能负载是有限的，所以会存在瓶颈的问题&lt;/li&gt;
&lt;li&gt;第二个就是网络的好坏差异也会影响成功率&lt;/li&gt;
&lt;li&gt;第三个就是我们有那么多的业务去做变化，然后流量这方面也是一个负担，那如何去进一步降低流量这方面的消耗&lt;/li&gt;
&lt;li&gt;基于以上几点，你要怎么去设计一个网络并发请求管理的模块，使得整体达到一个符合业务需要的效果，你会从几个方面进行切入设计呢？&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;好的，这是一个非常经典且实际的工程问题。结合您提出的车载导航系统场景，我将从一个资深架构师的角度，系统地阐述如何设计这样一个网络并发请求管理模块。&lt;/p&gt;
&lt;p&gt;这个设计的核心目标是：&lt;strong&gt;在有限的车机资源和不稳定的网络环境下，通过智能的请求管理，确保高优先级业务的成功率和实时性，同时最大限度地降低流量消耗和系统负载。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们可以从以下几个核心方面进行切入设计：&lt;/p&gt;
&lt;h3&gt;一、核心架构：分层与队列管理&lt;/h3&gt;
&lt;p&gt;首先，模块不应该让业务方直接发起请求，而应该通过一个中心化的管理器。这使我们能够实施统一的策略。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 请求池/队列管理器&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分级队列&lt;/strong&gt;：将业务请求按照&lt;strong&gt;优先级&lt;/strong&gt;进行分类。例如：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实时关键队列：&lt;/strong&gt; 路线计算、交通事件、实时路况更新。这些请求必须低延迟、高成功率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;普通队列：&lt;/strong&gt; 沿途搜索、地址详情查询。可以容忍一定延迟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批量/延迟队列：&lt;/strong&gt; 地图瓦片预加载、日志上报、非关键数据同步。可以等待网络良好时批量发送。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发控制器&lt;/strong&gt;：设置一个全局的并发请求数上限（例如，根据车机性能定为 4-6 个）。管理器从高优先级队列中取出请求执行，只有当高优先级队列为空时，才处理低优先级队列。这直接解决了&lt;strong&gt;并发量过大导致的性能瓶颈&lt;/strong&gt;问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 智能调度器&lt;/strong&gt;：调度器是大脑，它根据当前系统状态决定何时发送何请求。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;网络状态感知：&lt;/strong&gt; 模块需要实时监听网络类型（5G/4G/Wi-Fi）和信号强度。可以定义几个状态：&lt;code&gt;EXCELLENT&lt;/code&gt;, &lt;code&gt;GOOD&lt;/code&gt;, &lt;code&gt;POOR&lt;/code&gt;, &lt;code&gt;DISCONNECTED&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态策略调整：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;网络 &lt;code&gt;GOOD&lt;/code&gt; 以上：正常处理所有队列。&lt;/li&gt;
&lt;li&gt;网络 &lt;code&gt;POOR&lt;/code&gt;：自动降级。例如，只发送实时关键队列的请求；降低实时路况更新的频率；暂停批量队列。&lt;/li&gt;
&lt;li&gt;网络 &lt;code&gt;DISCONNECTED&lt;/code&gt;：缓存所有可缓存的请求，并提示用户。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二、流量优化：减少不必要的数据传输&lt;/h3&gt;
&lt;p&gt;这是降低流量负担的关键。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 请求去重与合并&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;去重：&lt;/strong&gt; 在短时间内，如果有多个完全相同的请求（例如，同一路段的实时路况），只发送一个，返回的结果共享给所有请求者。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合并：&lt;/strong&gt; 对于批量队列中的小请求（例如，多个POI的详情查询），可以将它们在客户端打包成一个批量请求发送到云端，云端处理后返回一个合并的响应。这大大减少了HTTP头等冗余数据的传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 数据压缩与差分更新&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;压缩：&lt;/strong&gt; 请求和响应体都使用高效的压缩算法（如 GZIP, Brotli）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;差分更新：&lt;/strong&gt; 这是流量优化的“大杀器”。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理：&lt;/strong&gt; 客户端本地缓存已有数据（如旧版地图数据、之前的路线信息）。当需要更新时，客户端将当前数据的版本号或哈希值发给服务端。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务端：&lt;/strong&gt; 计算新旧数据之间的“差异”（Delta），只将这个很小的差异包返回给客户端。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端：&lt;/strong&gt; 根据差异包和本地数据，合成最新数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用场景：&lt;/strong&gt; 路线变更、地图增量更新、交通信息流。这能减少高达90%的流量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 缓存策略&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多级缓存：&lt;/strong&gt; 实现内存缓存 + 磁盘缓存。
&lt;ul&gt;
&lt;li&gt;对于静态或半静态数据（如地图瓦片、城市基础信息），设置很长的过期时间。&lt;/li&gt;
&lt;li&gt;对于动态数据（如实时路况），设置较短的过期时间（如30秒）。在过期时间内，直接使用缓存，不发请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存有效性：&lt;/strong&gt; 通过服务端返回的 &lt;code&gt;ETag&lt;/code&gt; 或 &lt;code&gt;Last-Modified&lt;/code&gt; 头，在请求时进行验证，如果数据未变，则返回304 Not Modified，节省响应体流量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;三、容错与稳定性：提升弱网下的成功率&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 重试与退避机制&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;智能重试：&lt;/strong&gt; 不是所有失败都重试。仅对网络错误、5xx服务器错误等进行重试，而对4xx客户端错误则不重试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指数退避：&lt;/strong&gt; 重试的时间间隔逐渐延长（如1s, 2s, 4s, 8s...），避免在服务器临时故障时加剧其压力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先级关联：&lt;/strong&gt; 高优先级请求的重试次数可以更多，间隔更短；低优先级请求则相反。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 请求超时与熔断&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;动态超时：&lt;/strong&gt; 根据网络状况和请求优先级设置不同的超时时间。弱网环境下，适当延长超时时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;熔断器模式：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;当某个服务端接口失败率超过阈值时，熔断器会“跳闸”。&lt;/li&gt;
&lt;li&gt;在接下来的一段时间内，所有对该接口的请求会立即失败，而不再真正发出网络请求。&lt;/li&gt;
&lt;li&gt;经过一个冷却时间后，熔断器进入“半开”状态，试探性地放一个请求过去，如果成功则关闭熔断器，恢复流量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用：&lt;/strong&gt; 防止因某个后端服务故障而导致客户端资源（线程、连接）被耗尽和流量浪费。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/20250823-vpZNIK.BDwBbDP1.png"/><enclosure url="/_astro/20250823-vpZNIK.BDwBbDP1.png"/></item><item><title>八股文 @ 操作系统</title><link>https://coooredump.github.io/blog/recruitment/2025-os</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/2025-os</guid><description>记录面经高频操作系统题，实时更新中...</description><pubDate>Sat, 23 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 进程、线程、协程的区别？&lt;/h2&gt;
&lt;p&gt;进程提供强隔离但开销大，线程平衡了资源共享和调度效率，协程则以极低开销实现高并发但局限于单线程内。&lt;/p&gt;
&lt;p&gt;实际应用中可根据任务需求（计算密集型 vs. I/O 密集型）、隔离性要求和性能目标选择合适机制。&lt;/p&gt;
&lt;h3&gt;进程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;进程&lt;/strong&gt;是操作系统进行资源分配和保护的基本单位。&lt;/p&gt;
&lt;p&gt;每个进程拥有独立的虚拟地址空间、文件描述符、系统资源（如打开的文件和信号处理程序）以及安全上下文（例如用户 ID 和权限）。&lt;/p&gt;
&lt;p&gt;进程之间的内存空间是隔离的，一个进程的崩溃通常不会直接影响其他进程，这种隔离性提高了系统的稳定性和安全性。&lt;/p&gt;
&lt;p&gt;进程间的通信（IPC）需要借助操作系统提供的机制，如管道、消息队列、共享内存或套接字，这些方式通常涉及较高的开销。&lt;/p&gt;
&lt;p&gt;进程的创建和销毁需要分配或回收大量资源（如页表和文件描述符表），上下文切换时需保存和恢复完整的 CPU 状态（包括寄存器、内存映射等），因此效率较低。&lt;/p&gt;
&lt;p&gt;🔥【场景】进程适用于需要强隔离性的任务，例如运行独立的应用程序或服务。&lt;/p&gt;
&lt;h3&gt;线程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;线程&lt;/strong&gt;是进程内的执行单元，一个进程可以包含多个线程。&lt;/p&gt;
&lt;p&gt;所有线程共享同一进程的地址空间和系统资源（如全局变量、打开的文件和堆内存），但每个线程拥有独立的栈空间、寄存器状态和线程局部存储。&lt;/p&gt;
&lt;p&gt;由于共享内存，线程间可以直接读写同一数据，但这也引入了竞态条件和数据一致性问题，因此必须使用同步机制（如互斥锁、信号量或条件变量）来协调访问。&lt;/p&gt;
&lt;p&gt;线程的创建和上下文切换由操作系统内核调度器管理，切换时只需保存和恢复线程独有的状态（如栈指针和寄存器），开销比进程小，但仍需在用户态和内核态之间切换。&lt;/p&gt;
&lt;p&gt;🔥【场景】线程适合用于需要共享数据的并发任务，例如图形界面应用中的后台计算或 I/O 操作。&lt;/p&gt;
&lt;h3&gt;协程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;协程&lt;/strong&gt;是一种用户态的轻量级线程，其调度完全由程序控制（而非操作系统内核）。&lt;/p&gt;
&lt;p&gt;协程在同一个线程内执行，共享该线程的所有资源，但拥有独立的栈上下文（通常大小可自定义且远小于线程栈）。&lt;/p&gt;
&lt;p&gt;协程的切换无需陷入内核，而是通过代码中的显式让步（yield）或事件循环来触发，仅需保存和恢复少量寄存器状态（如程序计数器和栈指针），因此开销极低，每秒可支持百万次切换。&lt;/p&gt;
&lt;p&gt;然而，协程无法利用多核 CPU 的并行能力，若需跨核执行仍需结合多线程或多进程。&lt;/p&gt;
&lt;p&gt;🔥【场景】协程适用于高并发的 I/O 密集型任务（如网络服务或异步编程），通过非阻塞操作和协作式调度最大化 CPU 利用率。&lt;/p&gt;
&lt;h2&gt;2. 进程通信的方式是什么？&lt;/h2&gt;
&lt;p&gt;进程间通信（IPC）用于在不同进程之间传递数据和信号，常见的 IPC 方式包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;管道（匿名/命名）&lt;/li&gt;
&lt;li&gt;消息队列&lt;/li&gt;
&lt;li&gt;共享内存&lt;/li&gt;
&lt;li&gt;信号量&lt;/li&gt;
&lt;li&gt;信号&lt;/li&gt;
&lt;li&gt;Socket 套接字&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;匿名管道是一种半双工的通信方式，数据只能单向流动，通常用于具有亲缘关系的进程之间，例如父子进程。它可以看成是一种特殊的文件，但只存在于内存中。&lt;/p&gt;
&lt;p&gt;命名管道与管道类似，但它提供了一个路径名与之关联，允许无亲缘关系的进程之间进行通信。&lt;/p&gt;
&lt;p&gt;消息队列是消息的链表，存放在内核中并由消息队列标识符标识。它允许一个或多个进程向队列中写入消息，其他进程从队列中读取消息，克服了管道只能承载无格式字节流以及缓冲区大小受限等缺点。&lt;/p&gt;
&lt;p&gt;共享内存允许多个进程访问同一块内存空间，这是最快的一种进程通信方式，因为数据不需要在进程之间复制。但需要配合信号量等同步机制来避免多个进程同时读写时造成的冲突。&lt;/p&gt;
&lt;p&gt;信号量是一个计数器，用于控制多个进程对共享资源的访问，通常作为一种锁机制来防止多个进程同时访问一个共享资源。&lt;/p&gt;
&lt;p&gt;信号是一种比较复杂的通信方式，用于通知接收进程某个事件已经发生，比如中断处理。&lt;/p&gt;
&lt;p&gt;套接字是一种通用的进程间通信机制，不仅可用于同一台机器上的进程通信，也可用于不同机器之间的网络通信。它支持多种协议，如 TCP 和 UDP，提供了灵活而强大的通信能力。&lt;/p&gt;
&lt;h2&gt;3. 线程通信/同步的方式是什么？&lt;/h2&gt;
&lt;p&gt;Linux 系统提供了五种用于线程通信的方式：&lt;strong&gt;互斥锁&lt;/strong&gt;、&lt;strong&gt;读写锁&lt;/strong&gt;、&lt;strong&gt;条件变量&lt;/strong&gt;、&lt;strong&gt;自旋锁&lt;/strong&gt;和&lt;strong&gt;信号量&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;互斥锁&lt;/strong&gt;（Mutex）：互斥量从本质上说是一把锁，在访问共享资源前对互斥量进行加锁，在访问完成后释放互斥量上的锁。对互斥量进行加锁以后，任何其他试图再次对互斥锁加锁的线程将会阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞，所有在该互斥锁上的阻塞线程都会变成可运行状态，第一个变为运行状态的线程可以对互斥锁加锁，其他线程将会看到互斥锁依然被锁住，只能回去再次等待它重新变为可用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件变量&lt;/strong&gt;（Condition Variables）：条件变量是在多线程程序中用来&lt;strong&gt;实现 &quot;等待--&gt;唤醒&quot;&lt;/strong&gt; 逻辑常用的方法。条件变量利用线程间共享的全局变量进行同步的一种机制，主要包括两个动作：一个线程等待&quot;条件变量的条件成立&quot;而挂起；另一个线程使“条件成立”。&lt;strong&gt;为了防止竞争，条件变量的使用总是和一个互斥锁结合在一起&lt;/strong&gt;。线程在改变条件状态前必须首先锁住互斥量，函数 pthread_cond_wait 把自己放到等待条件的线程列表上，然后对互斥锁解锁（这两个操作是原子操作）。在函数返回时，互斥量再次被锁住。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自旋锁&lt;/strong&gt;（Spinlock）：自旋锁通过 CPU 提供的 CAS 函数（&lt;code&gt;Compare And Swap&lt;/code&gt;），在「用户态」完成加锁和解锁操作，不会主动产生线程上下文切换，所以相比互斥锁来说，会快一些，开销也小一些。一般加锁的过程，包含两个步骤：第一步，查看锁的状态，如果锁是空闲的，则执行第二步；第二步，将锁设置为当前线程持有。使用自旋锁的时候，当发生多线程竞争锁的情况，加锁失败的线程会「&lt;strong&gt;忙等待&lt;/strong&gt;」，直到它拿到锁。CAS 函数就把这两个步骤合并成一条硬件级指令，形成原子指令，这样就保证了这两个步骤是不可分割的，要么一次性执行完两个步骤，要么两个步骤都不执行。这里的「忙等待」可以用 &lt;code&gt;while&lt;/code&gt; 循环等待实现，不过最好是使用 CPU 提供的 &lt;code&gt;PAUSE&lt;/code&gt; 指令来实现「忙等待」，因为可以减少循环等待时的耗电量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;信号量&lt;/strong&gt;（Semaphores）：信号量可以是命名的（有名信号量）或无名的（仅限于当前进程内的线程），用于控制对资源的访问次数。通常信号量表示资源的数量，对应的变量是一个整型（sem）变量。另外，还有两个原子操作的系统调用函数来控制信号量的，分别是：P 操作：将 sem 减 1，相减后，如果 sem &amp;#x3C; 0，则进程/线程进入阻塞等待，否则继续，表明 P 操作可能会阻塞；V 操作：将 sem 加 1，相加后，如果 sem ≤ 0，唤醒一个等待中的进程/线程，表明 V 操作不会阻塞；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读写锁&lt;/strong&gt;（Read-Write Locks）：读写锁从字面意思我们也可以知道，它由「读锁」和「写锁」两部分构成，如果只读取共享资源用「读锁」加锁，如果要修改共享资源则用「写锁」加锁。所以，&lt;strong&gt;读写锁适用于能明确区分读操作和写操作的场景&lt;/strong&gt;。读写锁的工作原理是：当「写锁」没有被线程持有时，多个线程能够并发地持有读锁，这大大提高了共享资源的访问效率，因为「读锁」是用于读取共享资源的场景，所以多个线程同时持有读锁也不会破坏共享资源的数据。但是，一旦「写锁」被线程持有后，读线程的获取读锁的操作会被阻塞，而且其他写线程的获取写锁的操作也会被阻塞。所以说，&lt;strong&gt;写锁是独占锁&lt;/strong&gt;，因为任何时刻只能有一个线程持有写锁，类似互斥锁和自旋锁，&lt;strong&gt;而读锁是共享锁&lt;/strong&gt;，因为读锁可以被多个线程同时持有。知道了读写锁的工作原理后，我们可以发现，&lt;strong&gt;读写锁在读多写少的场景，能发挥出优势&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;屏障&lt;/strong&gt;（Barrier）：同步多个线程的执行阶段，所有线程到达屏障点后才继续执行（如 &lt;code&gt;pthread_barrier_wait&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;总结，线程通信方式主要依赖于共享内存下的各种同步原语和消息传递机制，这些机制协同工作保证了线程间数据的正确性和程序的并发性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;4. 进程上下文切换会发生什么？进程上下文切换场景有哪些？&lt;/h2&gt;
&lt;p&gt;发生进程切换时操作系统会：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;保存当前进程上下文&lt;/strong&gt;：操作系统将当前运行进程的 CPU 状态（包括程序计数器、寄存器内容、栈指针、内存映射表等）保存到其进程控制块（PCB）中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度新进程&lt;/strong&gt;：调度器从就绪队列中选择下一个要运行的进程，并加载其 PCB 中存储的上下文信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复新进程上下文&lt;/strong&gt;：将新进程的寄存器值、程序计数器、栈指针等重新载入 CPU，并切换内存地址空间（更新页表寄存器或 TLB）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;切换内核栈&lt;/strong&gt;：将内核栈指针指向新进程的内核栈，确保系统调用和中断处理能正确执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新系统状态&lt;/strong&gt;：刷新进程调度相关数据（如时间戳、优先级），并可能处理信号或待处理中断。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;进程上下文切换有哪些场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时间片耗尽&lt;/strong&gt;：当前进程用完调度器分配的时间片（如 CFS 调度器），被迫让出 CPU。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更高优先级进程就绪&lt;/strong&gt;：高优先级进程进入可运行状态（如实时任务），抢占当前低优先级进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主动阻塞&lt;/strong&gt;：进程因等待资源（如 I/O 操作完成、互斥锁释放、信号量申请）主动放弃 CPU。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中断处理&lt;/strong&gt;：硬件中断（如磁盘读写完成、网络包到达）触发中断服务程序，可能唤醒其他高优先级进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统调用返回&lt;/strong&gt;：某些系统调用（如阻塞式 read）返回时，内核可能调度其他进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常事件&lt;/strong&gt;：进程触发异常（如缺页故障），需等待操作系统处理完毕后再恢复执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 平时会看哪些系统指标？&lt;/h2&gt;
&lt;p&gt;常看这些核心系统指标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU 相关指标&lt;/strong&gt;：CPU 使用率（包括用户态、内核态、空闲时间）、负载平均值（load average）反映系统整体负载压力，上下文切换次数（context switches）和中断频率（interrupts）可帮助判断调度开销和硬件活动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存相关指标&lt;/strong&gt;：内存使用率、可用内存（available memory）、交换分区使用情况（swap usage）以及页错误率（page faults）和内存换入/换出（swap in/out）能揭示内存压力与潜在瓶颈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁盘 I/O 指标&lt;/strong&gt;：磁盘利用率、读写吞吐量（throughput）、IOPS（每秒 I/O 操作数）以及响应时间（latency）和队列深度（queue depth）可用于评估存储性能是否达标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络相关指标&lt;/strong&gt;：网络接口的带宽使用率、数据包发送/接收速率、错误包和丢包率（packet loss）、TCP 连接数及重传率能反映网络连通质量和负载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程与线程指标&lt;/strong&gt;：运行中进程数量、线程数量、僵尸进程（zombie processes）以及关键进程的 CPU 和内存占用情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统整体指标&lt;/strong&gt;：启动时间、系统调用频率（system calls）、文件描述符使用量（file descriptors）以及温度与电源状态（适用于物理机）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可通过 &lt;code&gt;top&lt;/code&gt;、&lt;code&gt;vmstat&lt;/code&gt;、&lt;code&gt;iostat&lt;/code&gt;、&lt;code&gt;netstat&lt;/code&gt;、&lt;code&gt;dstat&lt;/code&gt;、&lt;code&gt;htop&lt;/code&gt;、&lt;code&gt;pidstat&lt;/code&gt;、&lt;code&gt;sar&lt;/code&gt; 等工具查看。&lt;/p&gt;
&lt;h2&gt;6. CPU 中断数量怎么看？如果 CPU 中断数量分配不均匀怎么办？&lt;/h2&gt;
&lt;h3&gt;一、如何查看中断数量？&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat /proc/interrupts
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;每一行是一个中断源（如网卡、定时器、磁盘等）&lt;/li&gt;
&lt;li&gt;每列是一个 CPU 核心&lt;/li&gt;
&lt;li&gt;数字表示该中断在某 CPU 上被处理的次数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二、什么是“中断不均匀”？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;某个中断（特别是网卡）&lt;strong&gt;集中在某个 CPU 上处理&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;导致该核负载高、其他核闲置&lt;/li&gt;
&lt;li&gt;常见于 &lt;strong&gt;网卡中断（eth0）偏向 CPU 0&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;三、怎么解决中断不均匀？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;一句话：可以绑中断到线程还是进程，然后再绑核。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;方法一：&lt;strong&gt;中断绑定（IRQ Affinity）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;绑定中断到特定 CPU 核，或进行负载均衡：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo 2 &gt; /proc/irq/XX/smp_affinity  # 绑定到 CPU1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;2&lt;/code&gt; 表示 CPU1，&lt;code&gt;1&lt;/code&gt; 表示 CPU0，&lt;code&gt;3&lt;/code&gt; 表示 CPU0+CPU1（按位）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以用脚本自动平衡多个中断源到多个核上。&lt;/p&gt;
&lt;p&gt;方法二：&lt;strong&gt;使用 RPS / RFS（软中断调度）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RPS：Receive Packet Steering，用于软中断在多核间分散&lt;/li&gt;
&lt;li&gt;配置 &lt;code&gt;/sys/class/net/eth0/queues/rx-*/rps_cpus&lt;/code&gt; 指定核&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法三：&lt;strong&gt;开启多队列网卡（RSS）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RSS（Receive Side Scaling）：硬件级多队列分发中断&lt;/li&gt;
&lt;li&gt;需网卡和驱动支持，查看：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ethtool -l eth0    # 查看队列
ethtool -x eth0    # 查看中断队列分配
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CPU 中断不均衡会导致某核过载，通过 &lt;strong&gt;IRQ 绑定、RPS/RFS、RSS&lt;/strong&gt; 技术可将中断负载均衡到多个 CPU，提高性能。&lt;/p&gt;
&lt;h2&gt;7. 用户态和内核态的区别？&lt;/h2&gt;
&lt;p&gt;内核态和用户态是操作系统中的两种运行模式。它们的主要区别在于权限和可执行的操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在用户态下，进程仅能执行非特权指令，无法直接访问硬件设备或敏感的内核资源（如内存管理单元、I/O 端口），若需执行特权操作（如文件读写、网络通信）必须通过系统调用接口向内核发起请求，由内核代理完成。这种模式限制了用户程序的行为，防止其意外破坏系统或其他进程。&lt;/li&gt;
&lt;li&gt;在内核态下，代码可执行所有CPU指令（包括特权指令），直接操作硬件资源（如内存分配、设备驱动管理），并完全控制系统关键数据结构（如进程调度表、文件系统缓存）。操作系统内核及部分底层驱动运行于此模式，确保核心任务的完整性和高效性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;内核态的底层操作主要包括：内存管理、进程管理、设备驱动程序控制、系统调用等。这些操作涉及到操作系统的核心功能，需要较高的权限来执行。&lt;/p&gt;
&lt;p&gt;两种模式的切换通过硬件陷阱机制（如软中断、系统调用）触发：当用户程序发起系统调用时，CPU 自动从用户态切换到内核态，内核完成请求后返回结果并切换回用户态。这种设计既隔离了用户程序的错误影响，又保证了系统服务的可控提供。&lt;/p&gt;
&lt;p&gt;分为内核态和用户态的原因主要有以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;/strong&gt;：通过对权限的划分，用户程序无法直接访问硬件资源，从而避免了恶意程序对系统资源的破坏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;稳定性&lt;/strong&gt;：用户态程序出现问题时，不会影响到整个系统，避免了程序故障导致系统崩溃的风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隔离性&lt;/strong&gt;：内核态和用户态的划分使得操作系统内核与用户程序之间有了明确的边界，有利于系统的模块化和维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8. 你说到进程是分配资源的基本单位，那么这个资源指的是什么？&lt;/h2&gt;
&lt;p&gt;虚拟内存、CPU 时间片、文件句柄、信号量等资源。&lt;/p&gt;
&lt;h2&gt;9. 进程切换和线程切换的区别？&lt;/h2&gt;
&lt;p&gt;进程切换涉及完整的上下文切换，包括保存和恢复进程的私有资源，如&lt;strong&gt;虚拟内存地址空间&lt;/strong&gt;（需切换页表或刷新 TLB）、&lt;strong&gt;寄存器状态&lt;/strong&gt;、&lt;strong&gt;内核栈&lt;/strong&gt;、&lt;strong&gt;文件描述符表&lt;/strong&gt;以及&lt;strong&gt;信号处理&lt;/strong&gt;设置等。因为进程拥有独立的地址空间，切换后需要更新内存管理单元（MMU）的映射关系，这一操作通常需要较长时间且可能因 TLB 失效导致性能下降。此外，进程切换必然需要从用户态陷入内核态，由操作系统调度器完成。&lt;/p&gt;
&lt;p&gt;线程切换则发生在同一进程内部，因此无需切换虚拟地址空间（页表保持不变）、文件描述符表等进程级资源。只需保存和恢复线程的私有上下文，如&lt;strong&gt;寄存器值&lt;/strong&gt;、&lt;strong&gt;栈指针&lt;/strong&gt;以及&lt;strong&gt;线程局部存储&lt;/strong&gt;。由于资源共享，线程切换的开销显著低于进程切换，且在某些情况下（如用户级线程）甚至无需陷入内核，由用户空间的线程库即可完成调度。&lt;/p&gt;
&lt;h2&gt;10. 进程的五种状态如何切换（进程状态图）？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;一个完整的进程状态的变迁如下图：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504301906192.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;再来详细说明一下进程的状态变迁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NULL -&gt; 创建状态：一个新进程被创建时的第一个状态；&lt;/li&gt;
&lt;li&gt;创建状态 -&gt; 就绪状态：当进程被创建完成并初始化后，一切就绪准备运行时，变为就绪状态，这个过程是很快的；&lt;/li&gt;
&lt;li&gt;就绪态 -&gt; 运行状态：处于就绪状态的进程被操作系统的进程调度器选中后，就分配给 CPU 正式运行该进程；&lt;/li&gt;
&lt;li&gt;运行状态 -&gt; 结束状态：当进程已经运行完成或出错时，会被操作系统作结束状态处理；&lt;/li&gt;
&lt;li&gt;运行状态 -&gt; 就绪状态：处于运行状态的进程在运行过程中，由于分配给它的运行时间片用完，操作系统会把该进程变为就绪态，接着从就绪态选中另外一个进程运行；&lt;/li&gt;
&lt;li&gt;运行状态 -&gt; 阻塞状态：当进程请求某个事件且必须等待时，例如请求 I/O 事件；&lt;/li&gt;
&lt;li&gt;阻塞状态 -&gt; 就绪状态：当进程要等待的事件完成时，它从阻塞状态变到就绪状态；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;11. 64 bits 的 Linux 默认栈大小？&lt;/h2&gt;
&lt;p&gt;在 64 位的 Linux 系统上，默认的线程栈大小通常是 &lt;code&gt;8 MB&lt;/code&gt;。这个值并不是由内核直接规定的，而是取决于具体的 &lt;code&gt;pthread&lt;/code&gt; 线程库的实现（比如 glibc），大多数主流发行版都将其默认设置为 8 MB。&lt;/p&gt;
&lt;p&gt;你可以通过命令 &lt;code&gt;ulimit -s&lt;/code&gt; 来查看当前 shell 环境下线程栈的默认大小（以 KB 为单位），通常它显示为 8192。需要注意的是，这个 ulimit 设置是针对进程的主线程以及由当前 shell 启动的程序的新线程的默认值，而在程序中通过 &lt;code&gt;pthread_create&lt;/code&gt; 创建新线程时，如果不显式指定栈大小，也会默认使用这个 8 MB 的值。&lt;/p&gt;
&lt;h2&gt;12. 进程调度算法有哪些？&lt;/h2&gt;
&lt;h3&gt;1️⃣ 先来先服务调度算法&lt;/h3&gt;
&lt;p&gt;最简单的一个调度算法，就是非抢占式的先来先服务算法。每次从就绪队列选择最先进入队列的进程，然后一直运行，直到进程退出或被阻塞，才会继续从队列中选择第一个进程接着运行。这似乎很公平，但是当一个长作业先运行了，那么后面的短作业等待的时间就会很长，不利于短作业。 FCFS 对长作业有利，适用于 CPU 繁忙型作业的系统，而不适用于 I/O 繁忙型作业的系统。&lt;/p&gt;
&lt;h3&gt;2️⃣ 最短任务优先调度算法&lt;/h3&gt;
&lt;p&gt;最短作业优先调度算法同样也是顾名思义，它会优先选择运行时间最短的进程来运行，这有助于提高系统的吞吐量。这显然对长作业不利，很容易造成一种极端现象。比如，一个长作业在就绪队列等待运行，而这个就绪队列有非常多的短作业，那么就会使得长作业不断的往后推，周转时间变长，致使长作业长期不会被运行（饥饿）。&lt;/p&gt;
&lt;h3&gt;3️⃣ 高响应比优先调度算法&lt;/h3&gt;
&lt;p&gt;前面的「先来先服务调度算法」和「最短作业优先调度算法」都没有很好的权衡短作业和长作业。&lt;/p&gt;
&lt;p&gt;而高响应比优先调度算法主要是权衡了短作业和长作业。&lt;/p&gt;
&lt;p&gt;每次进行进程调度时，先计算「响应比优先级」，然后把「响应比优先级」最高的进程投入运行，「响应比优先级」的计算公式：
$$
优先权=\frac{等待时间+要求服务时间}{要求服务时间}
$$
从上面的公式，可以发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果两个进程的「等待时间」相同时，「要求的服务时间」越短，「响应比」就越高，这样短作业的进程容易被选中运行；&lt;/li&gt;
&lt;li&gt;如果两个进程「要求的服务时间」相同时，「等待时间」越长，「响应比」就越高，这就兼顾到了长作业进程，因为进程的响应比可以随时间等待的增加而提高，当其等待时间足够长时，其响应比便可以升到很高，从而获得运行的机会。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4️⃣ 时间片轮转调度算法&lt;/h3&gt;
&lt;p&gt;最古老、最简单、最公平且使用最广的算法就是时间片轮转调度算法。&lt;/p&gt;
&lt;p&gt;每个进程被分配一个时间段，称为时间片（Quantum)，即允许该进程在该时间段中运行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果时间片用完，进程还在运行，那么将会把此进程从 CPU 释放出来，并把 CPU 分配另外一个进程；&lt;/li&gt;
&lt;li&gt;如果该进程在时间片结束前阻塞或结束，则 CPU 立即进行切换；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，时间片的长度就是一个很关键的点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果时间片设得太短会导致过多的进程上下文切换，降低了 CPU 效率；&lt;/li&gt;
&lt;li&gt;如果设得太长又可能引起对短作业进程的响应时间变长。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通常时间片设为 20ms~50ms 通常是一个比较合理的折中值。&lt;/p&gt;
&lt;h3&gt;5️⃣ 最高优先级调度算法&lt;/h3&gt;
&lt;p&gt;前面的「时间片轮转算法」做了个假设，即让所有的进程同等重要，也不偏袒谁，大家的运行时间都一样。&lt;/p&gt;
&lt;p&gt;但是，对于多用户计算机系统就有不同的看法了，它们希望调度是有优先级的，即希望调度程序能从就绪队列中选择最高优先级的进程进行运行，这称为最高优先级调度算法。 进程的优先级可以分为，静态优先级或动态优先级：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态优先级：创建进程时候，就已经确定了优先级了，然后整个运行时间优先级都不会变化；&lt;/li&gt;
&lt;li&gt;动态优先级：根据进程的动态变化调整优先级，比如如果进程运行时间增加，则降低其优先级，如果进程等待时间（就绪队列的等待时间）增加，则升高其优先级，也就是随着时间的推移增加等待进程的优先级。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该算法也有两种处理优先级高的方法，非抢占式和抢占式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非抢占式：当就绪队列中出现优先级高的进程，运行完当前进程，再选择优先级高的进程。&lt;/li&gt;
&lt;li&gt;抢占式：当就绪队列中出现优先级高的进程，当前进程挂起，调度优先级高的进程运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是依然有缺点，可能会导致低优先级的进程永远不会运行。&lt;/p&gt;
&lt;h3&gt;6️⃣ 多级反馈队列调度算法&lt;/h3&gt;
&lt;p&gt;多级反馈队列调度算法是「时间片轮转算法」和「最高优先级算法」的综合和发展。&lt;/p&gt;
&lt;p&gt;顾名思义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;「多级」表示有多个队列，每个队列优先级从高到低，同时优先级越高时间片越短&lt;/li&gt;
&lt;li&gt;「反馈」表示如果有新的进程加入优先级高的队列时，立刻停止当前正在运行的进程，转而去运行优先级高的队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504301939468.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;来看看，它是如何工作的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置了多个队列，赋予每个队列不同的优先级，每个队列优先级从高到低，同时优先级越高时间片越短；&lt;/li&gt;
&lt;li&gt;新的进程会被放入到第一级队列的末尾，按先来先服务的原则排队等待被调度，如果在第一级队列规定的时间片没运行完成，则将其转入到第二级队列的末尾，以此类推，直至完成；&lt;/li&gt;
&lt;li&gt;当较高优先级的队列为空，才调度较低优先级的队列中的进程运行。如果进程运行时，有新进程进入较高优先级的队列，则停止当前运行的进程并将其移入到原队列末尾，接着让较高优先级的进程运行；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以发现，对于短作业可能可以在第一级队列很快被处理完。&lt;/p&gt;
&lt;p&gt;对于长作业，如果在第一级队列处理不完，可以移入下次队列等待被执行，虽然等待的时间变长了，但是运行时间也会更长了，&lt;strong&gt;所以该算法很好的兼顾了长短作业，同时有较好的响应时间&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;13. 除了互斥锁你还知道什么锁？分别应用于什么场景？&lt;/h2&gt;
&lt;p&gt;还有读写锁、自旋锁、条件变量、信号量。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读写锁：读写锁允许多个线程同时读取共享资源，但只允许一个线程进行写操作。适用于&lt;strong&gt;读操作频繁、写操作较少&lt;/strong&gt;的场景，可以提高并发性能。&lt;/li&gt;
&lt;li&gt;自旋锁：自旋锁是一种忙等待锁，线程在获取锁时不会进入阻塞状态，而是循环忙等待直到获取到锁。&lt;strong&gt;适用于临界区很小且锁的持有时间很短的场景&lt;/strong&gt;，避免线程频繁切换带来的开销。&lt;/li&gt;
&lt;li&gt;条件变量：条件变量用于线程间的同步和通信。它通常与互斥锁一起使用，&lt;strong&gt;线程可以通过条件变量等待某个条件满足&lt;/strong&gt;，当条件满足时，其他线程可以通过条件变量发送信号通知等待线程。&lt;/li&gt;
&lt;li&gt;信号量：信号量是一种计数器，用于控制对共享资源的访问。&lt;strong&gt;它可以用来限制同时访问资源的线程数量&lt;/strong&gt;，或者用语线程间的同步。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;14. 死锁发生条件是什么？&lt;/h2&gt;
&lt;p&gt;死锁只有同时满足以下四个条件才会发生：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;互斥条件&lt;/strong&gt;：互斥条件是指多个线程不能同时使用同一个资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持有并等待条件&lt;/strong&gt;：持有并等待条件是指，当线程 A 已经持有了资源 1，又想申请资源 2，而资源 2 已经被线程 C 持有了，所以线程 A 就会处于等待状态，但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不可剥夺条件&lt;/strong&gt;：不可剥夺条件是指，当线程已经持有了资源 ，在自己使用完之前不能被其他线程获取，线程 B 如果也想使用此资源，则只能在线程 A 使用完并释放后才能获取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;环路等待条件&lt;/strong&gt;：环路等待条件指的是，在死锁发生的时候，两个线程获取资源的顺序构成了环形链。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;15. 如何避免死锁？&lt;/h2&gt;
&lt;p&gt;避免死锁问题就只需要破环其中一个条件就可以，最常见的并且可行的就是&lt;strong&gt;使用资源有序分配法，来破环环路等待条件&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;那什么是资源有序分配法呢？线程 A 和 线程 B 获取资源的顺序要一样，当线程 A 是先尝试获取资源 A，然后尝试获取资源 B 的时候，线程 B 同样也是先尝试获取资源 A，然后尝试获取资源 B。也就是说，线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504302052155.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;16. 聊一聊银行家算法&lt;/h2&gt;
&lt;p&gt;系统发生死锁是很正常的，我们需要主动去预防死锁，即进行有序的资源分配，使用银行家算法。&lt;/p&gt;
&lt;p&gt;银行家算法是最有代表性的避免死锁的算法。&lt;/p&gt;
&lt;p&gt;为什么叫银行家算法呢？就是这个算法的逻辑很像银行放贷的逻辑，也就是尽可能避免坏账的出现。&lt;/p&gt;
&lt;p&gt;银行家算法的业务逻辑如下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不负荷执行：一个进程的最大需求量不超过系统拥有的总资源数，才会被接纳执行。&lt;/li&gt;
&lt;li&gt;可分期：一个进程可以分期请求资源，但总请求书不可超过最大需求量。&lt;/li&gt;
&lt;li&gt;推迟分配：当系统现有资源数小于进程需求时，对进程的需求可以延迟分配，但总让进程在有限时间内获取资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;听起来有点绕，我们还是举个例子来说明：&lt;strong&gt;假如系统中有三类互斥资源 R1、R2、R3，可用资源数分别是 9、8、5&lt;/strong&gt;，在指定时刻有 P1、P2、P3、P4 和 P5 这五个进程，这些进程的对三类互斥资源的最大需求量和已分配资源数如下表所示，那么系统如何先后运行这五个进程，不会发生死锁问题？&lt;/p&gt;
&lt;p&gt;| 进程 | 最大需求量（R1、R2、R3） | 已分配资源数（R1、R2、R3） |
| ---- | ------------------------ | -------------------------- |
| P1   | 6 5 2                    | 1 2 1                      |
| P2   | 2 2 1                    | 2 1 1                      |
| P3   | 8 1 1                    | 2 1 0                      |
| P4   | 1 2 1                    | 1 2 0                      |
| P5   | 3 4 4                    | 1 1 3                      |&lt;/p&gt;
&lt;p&gt;首先分析首次需求的资源，&lt;strong&gt;系统剩余可用资源数分别是 2、1、0&lt;/strong&gt;，各进程需要的资源数如下表所示。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资源 R1 的剩余可用资源数 = 9 - 1 - 2 - 2 - 1 - 1 = 2。&lt;/li&gt;
&lt;li&gt;资源 R2 的剩余可用资源数 = 8 - 2 - 1 - 1 - 2 - 1 = 1。&lt;/li&gt;
&lt;li&gt;资源 R3 的剩余可用资源数 = 5 - 1 - 1 - 0 - 0 - 3 = 0。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 进程 | 最大需求量 | 已分配资源数 | 首次分配需要的资源数（前者-后者） |
| ---- | ---------- | ------------ | --------------------------------- |
| P1   | 6 5 2      | 1 2 1        | 5 3 1                             |
| P2   | 2 2 1      | 2 1 1        | 0 1 0                             |
| P3   | 8 1 1      | 2 1 0        | 6 0 1                             |
| P4   | 1 2 1      | 1 2 0        | 0 0 1                             |
| P5   | 3 4 4      | 1 1 3        | 2 3 1                             |&lt;/p&gt;
&lt;p&gt;根据银行家算法不负荷原则【一个进程的最大需求量不超过系统拥有的总资源数，才会被接纳执行】，优先给进程 P2 执行，因为剩余的 0 1 0 资源够让 P2 执行。&lt;/p&gt;
&lt;p&gt;经过一系列分析和资源试分配后... 得到安全执行顺序为 &lt;code&gt;p2 =&gt; p4 =&gt; p5 =&gt; p1 =&gt; p3&lt;/code&gt; 或 &lt;code&gt;p2 =&gt; p4 =&gt; p5 =&gt; p3 =&gt; p1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;银行家算法的核心思想，就是在分配给进程资源前，首先判断这个进程的安全性，也就是预执行，判断分配后是否产生死锁现象。如果系统当前资源能满足其执行，则尝试分配，如果不满足则让该进程等待。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通过不断检查剩余可用资源是否满足某个进程的最大需求，如果可以则加入安全序列，并把该进程当前持有的资源回收；不断重复这个过程，看最后能否实现让所有进程都加入安全序列&lt;/strong&gt;。安全序列一定不会发生死锁，但没有死锁不一定是安全序列。&lt;/p&gt;
&lt;h2&gt;17. 乐观锁和悲观锁有什么区别？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;乐观锁&lt;/strong&gt;（不加锁、匹配版本号或时间戳、适用读多写少、无锁编程）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基本思想：乐观锁假设多个事务之间很少发生冲突，因此在读取数据时不会加锁，而是在更新数据时检查数据的版本（如使用版本号或时间戳），如果版本匹配则执行更新操作，否则认为发生了冲突。&lt;/li&gt;
&lt;li&gt;使用场景：乐观锁适用于读多写少的场景，可以减少锁的竞争，提高并发性能。例如，数据库中的乐观锁机制可以用于处理并发更新同一行数据的情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;悲观锁&lt;/strong&gt;（加锁、适用写多）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基本思想：悲观锁假设多个事务之间会频繁发生冲突，因此在读取数据时会加锁，防止其他事务对数据进行修改，直到当前事务完成操作后才释放锁。&lt;/li&gt;
&lt;li&gt;使用场景：悲观锁适用于写多的场景，通过加锁保证数据的一致性。例如，数据库中的行级锁机制可以用于处理并发更新同一行数据的情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;乐观锁适用于读多写少的场景，通过版本控制来处理冲突；而悲观锁适用于写多的场景，通过加锁来避免冲突。&lt;/p&gt;
&lt;p&gt;互斥锁、自旋锁、读写锁，都是属于悲观锁。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里举一个场景例子：在线文档。&lt;/p&gt;
&lt;p&gt;我们都知道在线文档可以同时多人编辑的，如果使用了悲观锁，那么只要有一个用户正在编辑文档，此时其他用户就无法打开相同的文档了，这用户体验当然不好了。&lt;/p&gt;
&lt;p&gt;那实现多人同时编辑，实际上是用了乐观锁，它允许多个用户打开同一个文档进行编辑，编辑完提交之后才验证修改的内容是否有冲突。&lt;/p&gt;
&lt;p&gt;怎么样才算发生冲突？这里举个例子，比如用户 A 先在浏览器编辑文档，之后用户 B 在浏览器也打开了相同的文档进行编辑，但是用户 B 比用户 A 提交早，这一过程用户 A 是不知道的，当 A 提交修改完的内容时，那么 A 和 B 之间并行修改的地方就会发生冲突。&lt;/p&gt;
&lt;p&gt;服务端要怎么验证是否冲突了呢？通常方案如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于发生冲突的概率比较低，所以先让用户编辑文档，但是浏览器在下载文档时会记录下服务端返回的文档版本号；&lt;/li&gt;
&lt;li&gt;当用户提交修改时，发给服务端的请求会带上原始文档版本号，服务器收到后将它与当前版本号进行比较，如果版本号不一致则提交失败，如果版本号一致则修改成功，然后服务端版本号更新到最新的版本号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际上，我们常见的 SVN 和 Git 也是用了乐观锁的思想，先让用户编辑代码，然后提交的时候，通过版本号来判断是否产生了冲突，发生了冲突的地方，需要我们自己修改后，再重新提交。&lt;/p&gt;
&lt;p&gt;乐观锁虽然去除了加锁解锁的操作，但是一旦发生冲突，重试的成本非常高，所以只有在冲突概率非常低，且加锁成本非常高的场景时，才考虑使用乐观锁。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;18. 虚拟内存？虚拟地址空间？使用虚拟内存的优点？&lt;/h2&gt;
&lt;p&gt;虚拟内存是一种内存管理技术，它会使程序自己认为自己拥有一块很大且连续的内存，而实际上这些内存可能分布在物理内存和磁盘上，在需要时进行数据交换。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过分页和交换技术，将暂时不用的内存页暂存到磁盘，可以弥补物理内存大小的不足；&lt;/li&gt;
&lt;li&gt;保证进程之间的隔离性和安全性（检测并防止内存访问越界等）；&lt;/li&gt;
&lt;li&gt;虚拟内存空间连续，简化了编程和内存管理，无需关心物理内存的实际分布情况；&lt;/li&gt;
&lt;li&gt;支持共享内存，不同进程的虚拟地址可映射到相同的物理内存页，实现高效的数据共享。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当系统物理内存不足时，操作系统需要将部分页面写入磁盘（换出）并从磁盘中加载需要的页面（换入），这会带来较高的磁盘 I/O 开销，严重时会导致“抖动”，使系统响应速度下降；&lt;/li&gt;
&lt;li&gt;虚拟内存还需要维护页表来管理虚拟地址与物理地址之间的映射，这也会占用一定的内存和 CPU 资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虚拟地址空间是针对于每个单一进程所能访问的内存地址范围，通常被划分为代码段、数据段、堆、栈和内存映射区域等部分。&lt;/p&gt;
&lt;h2&gt;19. 介绍一下操作系统内存管理&lt;/h2&gt;
&lt;p&gt;操作系统设计了虚拟内存，每个进程都有自己的独立的虚拟内存，我们所写的程序不会直接与物理内打交道。Linux 是通过对内存分页的方式来管理内存，分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小，每一页的大小为 4KB，虚拟地址与物理地址之间通过页表来映射，页表是存储在内存里的，内存管理单元 （MMU）就做将虚拟内存地址转换成物理地址的工作。而当进程访问的虚拟地址在页表中查不到时，系统会产生一个缺页异常，进入系统内核空间分配物理内存、更新进程页表，最后再返回用户空间，恢复进程的运行。&lt;/p&gt;
&lt;h2&gt;20. 程序的内存布局是怎样的？&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010011266.png&quot; alt=&quot;image-20240725233029022&quot;&gt;&lt;/p&gt;
&lt;p&gt;通过这张图你可以看到，用户空间内存，从低到高分别是 6 种不同的内存段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;代码段：包括二进制可执行代码，通常只读，还可共享（多个进程运行同一程序时只存一份）；&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;静态存储区：数据段 + BSS 段&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据段：包括已初始化的静态常量和全局变量；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BSS 段：包括未初始化的静态变量和全局变量；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;堆（比如 &lt;code&gt;malloc/new&lt;/code&gt;）：包括动态分配的内存，从低地址开始向上增长，程序员负责管理，容易泄露；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件映射区（比如 &lt;code&gt;mmap&lt;/code&gt;）：包括共享库、文件映射、匿名映射、共享内存等；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;栈：包括局部变量和函数调用的上下文（参数、返回地址）等，函数调用或返回会自动分配与释放后。栈的大小是固定的，一般是 8 MB。当然系统也提供了参数，以便我们自定义大小；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内核空间（对用户程序不可见）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上图中的内存布局可以看到，代码段下面还有一段内存空间的（灰色部分），这一块区域是「保留区」，之所以要有保留区这是因为在大多数的系统里，我们认为比较小数值的地址不是一个合法地址，例如，我们通常在 C 的代码里会将无效的指针赋值为 NULL。因此，这里会出现一段不可访问的内存保留区，防止程序因为出现 bug，导致读或写了一些小内存地址的数据，而使得程序跑飞。&lt;/p&gt;
&lt;p&gt;🔥 在这 7 个内存段中，堆和文件映射段的内存是动态分配的。比如说，使用 C 标准库的 &lt;code&gt;malloc()&lt;/code&gt; 或者 &lt;code&gt;mmap()&lt;/code&gt;，就可以分别在堆和文件映射段动态分配内存。&lt;/p&gt;
&lt;h2&gt;21. 堆与栈的区别，以及各自优缺点？&lt;/h2&gt;
&lt;p&gt;栈由编译器自动管理，内存分配和释放通过指针的移动完成，速度极快。它用于存储局部变量、函数参数和返回地址，数据遵循后进先出的顺序，生命周期与函数调用周期一致，函数结束时自动释放。栈的大小通常有限，过度使用可能导致栈溢出。&lt;/p&gt;
&lt;p&gt;堆由程序员手动管理，内存分配和释放需要显式操作，速度相对较慢。它用于动态分配的内存，生命周期由程序员控制，数据无需遵循特定顺序，但管理不当可能导致内存泄漏或碎片化。堆的大小受系统虚拟内存限制，容量远大于栈。&lt;/p&gt;
&lt;p&gt;栈的优点是高效且无碎片，但容量有限且灵活性低；堆的优点是容量大且灵活，但管理复杂且容易引发错误。&lt;/p&gt;
&lt;h2&gt;22. &lt;code&gt;fork()&lt;/code&gt; 会复制哪些东西&lt;/h2&gt;
&lt;p&gt;当调用 &lt;code&gt;fork()&lt;/code&gt; 创建子进程时，操作系统会复制父进程的整个&lt;strong&gt;虚拟地址空间&lt;/strong&gt;（包括代码、数据、堆和栈）、执行上下文（如寄存器状态）、打开的文件描述符表、信号处理设置、进程权限属性以及资源限制配置，形成一份几乎完全相同的副本。不过，子进程会拥有独立的进程 ID 和父进程 ID，且不会继承父进程的未决信号、定时器或文件锁。&lt;/p&gt;
&lt;p&gt;现代系统通过写时复制 COW 技术优化性能：初始时父子进程共享物理内存页，仅当任一进程尝试修改某页时，内核才为该页创建实际副本，从而避免不必要的内存复制开销。&lt;/p&gt;
&lt;h2&gt;23. 写时复制是什么？节省了哪些资源？&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010024167.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;写时复制（Copy-on-Write, COW）是一种高效的内存管理优化技术，其核心思想是允许多个进程或线程共享同一份物理内存数据，直到其中某个进程试图修改这些数据时，系统才会为该进程分配新的物理内存并复制原始数据，从而避免不必要的提前复制开销。&lt;/p&gt;
&lt;p&gt;在具体实现中，当父进程调用 &lt;code&gt;fork()&lt;/code&gt; 创建子进程时，操作系统并不会立即复制父进程的整个地址空间到新的物理内存中，而是让子进程共享父进程的物理内存页，同时将这些页标记为写时复制状态。此时，父子进程的页表项均指向相同的物理内存页，但权限被设置为只读。当任一进程（父进程或子进程）尝试对共享页进行写操作时，会触发页错误异常（page fault），内核中的页错误处理程序会识别这是由写时复制引起的，随后为执行写操作的进程分配一个新的物理页，复制原始数据内容，并更新该进程的页表以指向新页且恢复可写权限，最后重新执行导致异常的写指令。&lt;/p&gt;
&lt;p&gt;写时复制技术显著减少了进程创建（如 &lt;code&gt;fork()&lt;/code&gt;）和内存复制（如 &lt;code&gt;mmap()&lt;/code&gt; 私有映射）的开销，尤其在大内存进程中效果明显，因为它延迟了实际复制操作到真正需要时才发生，节省了时间和物理内存资源。典型的应用场景包括快速进程创建、内存快照、虚拟机管理及某些数据结构（如字符串实现中的共享缓冲区）。&lt;/p&gt;
&lt;h2&gt;24. malloc 1KB 和 1MB 有什么区别？&lt;/h2&gt;
&lt;p&gt;1KB（小内存）：通常通过 glibc 的 &lt;code&gt;ptmalloc&lt;/code&gt; 分配器从&lt;strong&gt;线程的本地缓存（fast bins 或 small bins）&lt;/strong&gt; 中直接分配，这些缓存来源于预先从堆（heap）中申请的内存块（chunk）。分配速度快，无需直接与内核交互。&lt;/p&gt;
&lt;p&gt;1MB（大内存）：可能超过 &lt;code&gt;mmap&lt;/code&gt; 阈值（默认一般为 128KB），分配器会直接使用 &lt;code&gt;mmap&lt;/code&gt; 系统调用从操作系统中申请独立的内存映射区域，而非从堆区分配。释放时同样通过 &lt;code&gt;munmap&lt;/code&gt; 直接返还操作系统。&lt;/p&gt;
&lt;h2&gt;25. free 释放内存会归还给操作系统吗？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;free()&lt;/code&gt; 的行为并不是由 C 语言标准直接规定的，而是由底层的内存管理器（通常是 &lt;code&gt;glibc&lt;/code&gt; 的 &lt;code&gt;malloc&lt;/code&gt; 实现，即 &lt;code&gt;ptmalloc2&lt;/code&gt;）决定的。它的核心目标是平衡性能（减少系统调用和锁竞争）和内存效率。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当我们调用 &lt;code&gt;free&lt;/code&gt; 函数释放一块动态分配的内存时，这个过程并非简单直接地将内存交还给操作系统，而是由底层的内存管理器（如 glibc 中的 &lt;code&gt;ptmalloc&lt;/code&gt;）负责处理。具体来说，内存管理器会将被释放的内存块标记为空闲状态，并保留在进程的堆空间中，&lt;code&gt;ptmalloc&lt;/code&gt; 将其加入到自己的空闲内存双向链表中。这样做的目的是为了优化性能：如果程序后续再次申请类似大小的内存，管理器可以迅速从空闲链表中分配一块，避免了频繁向操作系统申请内存的系统调用开销，同时 &lt;code&gt;ptmalloc&lt;/code&gt; 也会尝试对小块内存进行合并，避免过多的内存碎片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只有在某些特定条件下，内存管理器才会将内存真正归还给操作系统&lt;/strong&gt;。例如，当释放的内存块非常大（比如超过 &lt;code&gt;128KB&lt;/code&gt;，这类大块内存通常由 &lt;code&gt;mmap&lt;/code&gt; 分配而非堆分配）时，管理器可能会直接使用 &lt;code&gt;munmap&lt;/code&gt; 系统调用将其释放回操作系统。此外，如果堆顶（即堆空间的末端）存在大量连续的空闲内存，管理器也可能通过 &lt;code&gt;sbrk&lt;/code&gt; 系统调用来降低堆顶指针，从而缩减进程的堆空间，将这部分空闲内存返还给系统。但需要注意的是，由于内存碎片的存在，堆中部即使有空闲内存也难以被归还，因为操作系统要求归还的内存必须是地址连续的。因此，&lt;code&gt;free&lt;/code&gt; 的行为是内存管理器在性能和系统资源占用之间的一种权衡策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;26. malloc 是如何分配内存的（介绍一下 brk 和 mmap）？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;malloc&lt;/code&gt; 是 C 标准库中用于动态分配内存的函数，其具体实现依赖于底层的内存分配器（如 glibc 的 &lt;code&gt;ptmalloc&lt;/code&gt;）。它的分配通过多级缓存和堆/&lt;code&gt;mmap&lt;/code&gt; 混合策略优化不同尺寸的内存分配，在用户态管理内存池以减少系统调用次数，同时尝试平衡性能与碎片化问题。&lt;/p&gt;
&lt;p&gt;以下是其核心分配机制：&lt;/p&gt;
&lt;h3&gt;1. 小内存分配（通常 ≤ 128KB）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;线程本地缓存（Tcache）&lt;/strong&gt;：首先检查线程本地缓存（每个线程独享的无锁结构），从中快速获取所需大小的内存块。若命中则直接返回，避免全局锁竞争。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fast Bins / Small Bins&lt;/strong&gt;：若 Tcache 未命中，则根据请求大小从对应的 Fast Bins（小内存单链表，LIFO）或 Small Bins（固定大小链表）中查找空闲块。Fast Bins 通常不合并碎片以提升分配速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆区分配&lt;/strong&gt;：若以上缓存均无可用块，分配器会通过 &lt;code&gt;brk()&lt;/code&gt; 系统调用扩展进程的堆空间，从堆顶申请一大块内存，并将其分割为所需尺寸返回给用户。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 大内存分配（通常 &gt; 128KB）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;直接使用 &lt;code&gt;mmap()&lt;/code&gt; 系统调用：分配器会绕过堆管理，直接通过 &lt;code&gt;mmap&lt;/code&gt; 从操作系统申请独立的内存映射区域。此类内存释放时通过 &lt;code&gt;munmap&lt;/code&gt; 立即归还系统，避免堆碎片化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 分配器的优化策略&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内存块（chunk）对齐&lt;/strong&gt;：返回的内存块通常按 16 字节对齐（64 位系统），满足硬件和算法效率需求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;碎片管理&lt;/strong&gt;：释放后的内存块会根据邻居是否空闲进行合并，减少内存碎片。但 Fast Bins 中的块暂不合并以加速小内存分配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多级缓存&lt;/strong&gt;：通过 Tcache、Fast Bins、Unsorted Bins、Small Bins、Large Bins 等多级链表缓存不同尺寸的空闲块，适配不同分配模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. brk 与 mmap&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;brk()&lt;/code&gt;：用于扩展或收缩堆空间，频繁调用可能引发内存碎片。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mmap()&lt;/code&gt;：用于大内存或匿名映射，每次调用涉及内核态切换，开销较大但隔离性好。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;brk&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010031653.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;mmap&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010032024.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;27. 操作系统内存不足的时候会发生什么（如何避免预读失效和缓存污染）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;触发内核回收机制&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页面回收（Page Reclaim）&lt;/strong&gt;：内核优先回收&lt;strong&gt;干净页&lt;/strong&gt;（Clean Page，如文件读缓存）和&lt;strong&gt;空闲页&lt;/strong&gt;，直接丢弃或重新关联。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;脏页回写（Writeback Dirty Pages）&lt;/strong&gt;：将&lt;strong&gt;脏页&lt;/strong&gt;（Dirty Page，被修改过的缓存）异步写入磁盘，然后回收为空闲页。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交换（Swapping）&lt;/strong&gt;：将不活跃的匿名页（进程堆栈数据）换出到磁盘交换分区（Swap），释放物理内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OOM Killer（Out-of-Memory Killer）&lt;/strong&gt;：若回收后仍不足，内核根据进程的 &lt;code&gt;oom_score&lt;/code&gt;（基于内存占用、优先级等）强制终止某些进程，释放内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;应用程序通过 &lt;code&gt;malloc&lt;/code&gt; 函数申请内存的时候，实际上申请的是虚拟内存，此时并不会分配物理内存。&lt;/p&gt;
&lt;p&gt;当应用程序读写了这块虚拟内存，CPU 就会去访问这个虚拟内存， 这时会发现这个虚拟内存没有映射到物理内存， CPU 就会产生缺页中断，进程会从用户态切换到内核态，并将缺页中断交给内核的 Page Fault Handler（缺页中断函数）处理。&lt;/p&gt;
&lt;p&gt;缺页中断处理函数会看是否有空闲的物理内存，如果有，就直接分配物理内存，并建立虚拟内存与物理内存之间的映射关系。&lt;/p&gt;
&lt;p&gt;如果没有空闲的物理内存，那么内核就会开始进行回收内存的工作，回收的方式主要是两种：直接内存回收和后台内存回收。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后台内存回收（kswapd）：在物理内存紧张的时候，会唤醒 kswapd 内核线程来回收内存，这个回收内存的过程异步的，不会阻塞进程的执行。&lt;/li&gt;
&lt;li&gt;直接内存回收（direct reclaim）：如果后台异步回收跟不上进程内存申请的速度，就会开始直接回收，这个回收内存的过程是同步的，会阻塞进程的执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果直接内存回收后，空闲的物理内存仍然无法满足此次物理内存的申请，那么内核就会放最后的大招了 —— 触发 OOM（Out of Memory）机制。&lt;/p&gt;
&lt;p&gt;OOM Killer 机制会根据算法选择一个占用物理内存较高的进程，然后将其杀死，以便释放内存资源，如果物理内存依然不足，OOM Killer 会继续杀死占用物理内存较高的进程，直到释放足够的内存位置。&lt;/p&gt;
&lt;p&gt;申请物理内存的过程如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010044860.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;系统内存紧张的时候，就会进行回收内存的工作，那具体哪些内存是可以被回收的呢？&lt;/p&gt;
&lt;p&gt;🔥 主要有两类内存可以被回收，而且它们的回收方式也不同。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件页（File-backed Page）：内核缓存的磁盘数据（Buffer）和内核缓存的文件数据（Cache）都叫作文件页。大部分文件页，都可以直接释放内存，以后有需要时，再从磁盘重新读取就可以了。而那些被应用程序修改过，并且暂时还没写入磁盘的数据（也就是脏页），就得先写入磁盘，然后才能进行内存释放。所以，回收干净页的方式是直接释放内存，回收脏页的方式是先写回磁盘后再释放内存。&lt;/li&gt;
&lt;li&gt;匿名页（Anonymous Page）：这部分内存没有实际载体，不像文件缓存有硬盘文件这样一个载体，比如&lt;strong&gt;堆&lt;/strong&gt;、&lt;strong&gt;栈&lt;/strong&gt;数据等。这部分内存很可能还要再次被访问，所以不能直接释放内存，它们回收的方式是通过 Linux 的 Swap 机制，Swap 会把不常访问的内存先写到磁盘中，然后释放这些内存，给其他更需要的进程使用。再次访问这些内存时，重新从磁盘读入内存就可以了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;文件页和匿名页的回收都是基于 LRU 算法，也就是优先回收不常访问的内存。LRU 回收算法，实际上维护着 &lt;code&gt;active&lt;/code&gt; 和 &lt;code&gt;inactive&lt;/code&gt; 两个双向链表&lt;/strong&gt;，其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;active_list 活跃内存页链表，这里存放的是最近被访问过（活跃）的内存页；&lt;/li&gt;
&lt;li&gt;inactive_list 不活跃内存页链表，这里存放的是很少被访问（非活跃）的内存页（&lt;strong&gt;预取页&lt;/strong&gt;）；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有了这两个 LRU 链表后，预读页就只需要加入到 inactive_list 区域的头部，当页被真正访问的时候，才将页插入 active_list 的头部，同时将 active_list 尾部页淘汰到 inactive_list 头部中；如果预读的页一直没有被访问，就会从 inactive_list 移除，这样就不会影响 active_list 中的热点数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505092142975.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;越接近链表尾部，就表示内存页越不常访问。这样，在回收内存时，系统就可以根据活跃程度，优先回收不活跃的内存。&lt;/p&gt;
&lt;p&gt;但是如果还是使用「只要数据被访问一次，就将数据加入到活跃 LRU 链表头部」这种方式的话，那么还存在&lt;strong&gt;缓存污染&lt;/strong&gt;的问题：当我们在批量读取数据的时候，由于数据被访问了一次，这些大量数据都会被加入到「活跃 LRU 链表」里，然后之前缓存在活跃 LRU 链表里的热点数据全部都被淘汰了，如果这些大量的数据在很长一段时间都不会被访问的话，那么整个活跃 LRU 链表就被污染了。&lt;/p&gt;
&lt;p&gt;前面的 LRU 算法只要数据被访问一次，就将数据加入活跃 LRU 链表，这种 LRU 算法进入活跃 LRU 链表的门槛太低了！正是因为门槛太低，才导致在发生缓存污染的时候，很容易就将原本在活跃 LRU 链表里的热点数据淘汰了。所以，只要我们提高进入到活跃 LRU 链表的门槛，就能有效地保证活跃 LRU 链表里的热点数据不会被轻易替换掉。&lt;/p&gt;
&lt;p&gt;Linux 操作系统是这样提高门槛的：在内存页被访问第二次的时候，才将页从 inactive_list 升级到 active_list 里。&lt;/p&gt;
&lt;p&gt;提高了进入活跃 LRU 链表的门槛后，就很好了避免缓存污染带来的影响。在批量读取数据时候，如果这些大量数据只会被访问一次，那么它们就不会进入到活跃 LRU 链表，也就不会把热点数据淘汰，只会待在非活跃 LRU 链表中，后续很快也会被淘汰。&lt;/p&gt;
&lt;h2&gt;28. 页面置换算法有哪些？&lt;/h2&gt;
&lt;p&gt;页面置换算法的功能是，当出现缺页异常，需调入新页面而内存已满时，选择被置换的物理页面，也就是说选择一个物理页面换出到磁盘，然后把需要访问的页面换入到物理页。&lt;/p&gt;
&lt;p&gt;那其算法目标则是尽可能减少页面的换入换出的次数，常见的页面置换算法有如下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最佳页面置换算法（OPT）&lt;/li&gt;
&lt;li&gt;先进先出置换算法（FIFO）&lt;/li&gt;
&lt;li&gt;最近最久未使用的置换算法（LRU）&lt;/li&gt;
&lt;li&gt;时钟页面置换算法（Clock）&lt;/li&gt;
&lt;li&gt;最不常用置换算法（LFU）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1️⃣ 最佳页面置换算法（OPT）&lt;/h3&gt;
&lt;p&gt;最佳页面置换算法基本思路是，置换在「未来」最长时间不访问的页面。&lt;/p&gt;
&lt;p&gt;所以，该算法实现需要计算内存中每个逻辑页面的「下一次」访问时间，然后比较，选择未来最长时间不访问的页面。&lt;/p&gt;
&lt;p&gt;我们举个例子，假设一开始有 3 个空闲的物理页，然后有请求的页面序列，那它的置换过程如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010059018.png&quot; alt=&quot;最佳页面置换算法&quot;&gt;&lt;/p&gt;
&lt;p&gt;在这个请求的页面序列中，缺页共发生了 7 次（空闲页换入 3 次 + 最优页面置换 4 次），页面置换共发生了 4 次。&lt;strong&gt;这很理想，但是实际系统中无法实现，因为程序访问页面时是动态的，我们是无法预知每个页面在「下一次」访问前的等待时间&lt;/strong&gt;。所以，最佳页面置换算法作用是为了衡量你的算法的效率，你的算法效率越接近该算法的效率，那么说明你的算法是高效的。&lt;/p&gt;
&lt;h3&gt;2️⃣ 先进先出置换算法（FIFO）&lt;/h3&gt;
&lt;p&gt;既然我们无法预知页面在下一次访问前所需的等待时间，那我们可以选择在内存驻留时间很长的页面进行中置换，这个就是「先进先出置换」算法的思想。还是以前面的请求的页面序列作为例子，假设使用先进先出置换算法，则过程如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010100501.png&quot; alt=&quot;先进先出置换算法&quot;&gt;&lt;/p&gt;
&lt;p&gt;在这个请求的页面序列中，缺页共发生了 10 次（页面置换共发生了 7 次），跟最佳页面置换算法比较起来，性能明显差了很多。&lt;/p&gt;
&lt;h3&gt;3️⃣ 最近最久未使用的置换算法（LRU）&lt;/h3&gt;
&lt;p&gt;最近最久未使用（LRU）的置换算法的基本思路是，发生缺页时，选择最长时间没有被访问的页面进行置换，也就是说，该算法假设已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。&lt;/p&gt;
&lt;p&gt;这种算法近似最优置换算法，最优置换算法是通过「未来」的使用情况来推测要淘汰的页面，而 LRU 则是通过「历史」的使用情况来推测要淘汰的页面。还是以前面的请求的页面序列作为例子，假设使用最近最久未使用的置换算法，则过程如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010102960.png&quot; alt=&quot;最近最久未使用的置换算法&quot;&gt;&lt;/p&gt;
&lt;p&gt;在这个请求的页面序列中，缺页共发生了 9 次（页面置换共发生了 6 次），跟先进先出置换算法比较起来，性能提高了一些。&lt;/p&gt;
&lt;p&gt;虽然 LRU 在理论上是可以实现的，但代价很高。为了完全实现 LRU，需要在内存中维护一个所有页面的链表，最近最多使用的页面在表头，最近最少使用的页面在表尾。困难的是，在每次访问内存时都必须要更新「整个链表」。在链表中找到一个页面，删除它，然后把它移动到表头是一个非常费时的操作。所以，LRU 虽然看上去不错，但是由于开销比较大，实际应用中比较少使用。&lt;/p&gt;
&lt;h3&gt;4️⃣ 时钟页面置换算法（Clock）&lt;/h3&gt;
&lt;p&gt;那有没有一种即能优化置换的次数，也能方便实现的算法呢？&lt;/p&gt;
&lt;p&gt;时钟页面置换算法就可以两者兼得，它跟 LRU 近似，又是对 FIFO 的一种改进。&lt;/p&gt;
&lt;p&gt;该算法的思路是，把所有的页面都保存在一个类似钟面的「环形链表」中，一个表针指向最老的页面。&lt;/p&gt;
&lt;p&gt;当发生缺页中断时，算法首先检查表针指向的页面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果它的访问位位是 0 就淘汰该页面，并把新的页面插入这个位置，然后把表针前移一个位置；&lt;/li&gt;
&lt;li&gt;如果访问位是 1 就清除访问位，并把表针前移一个位置，重复这个过程直到找到了一个访问位为 0 的页面为止；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;时钟页面置换算法的工作流程图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010103584.png&quot; alt=&quot;时钟页面置换算法&quot;&gt;&lt;/p&gt;
&lt;h3&gt;5️⃣ 最不常用置换算法（LFU）&lt;/h3&gt;
&lt;p&gt;最不常用（LFU）算法，这名字听起来很调皮，但是它的意思不是指这个算法不常用，而是当发生缺页中断时，选择「访问次数」最少的那个页面，并将其淘汰。&lt;/p&gt;
&lt;p&gt;它的实现方式是，对每个页面设置一个「访问计数器」，每当一个页面被访问时，该页面的访问计数器就累加 1。在发生缺页中断时，淘汰计数器值最小的那个页面。&lt;/p&gt;
&lt;p&gt;看起来很简单，每个页面加一个计数器就可以实现了，但是在操作系统中实现的时候，我们需要考虑效率和硬件成本的。&lt;/p&gt;
&lt;p&gt;要增加一个计数器来实现，这个硬件成本是比较高的，另外如果要对这个计数器查找哪个页面访问次数最小，查找链表本身，如果链表长度很大，是非常耗时的，效率不高。&lt;/p&gt;
&lt;p&gt;但还有个问题，LFU 算法只考虑了频率问题，没考虑时间的问题，比如有些页面在过去时间里访问的频率很高，但是现在已经没有访问了，而当前频繁访问的页面由于没有这些页面访问的次数高，在发生缺页中断时，就会可能会误伤当前刚开始频繁访问，但访问次数还不高的页面。&lt;/p&gt;
&lt;p&gt;那这个问题的解决的办法还是有的，可以定期减少访问的次数，比如当发生时间中断时，把过去时间访问的页面的访问次数除以 2，也就说，随着时间的流失，以前的高访问次数的页面会慢慢减少，相当于加大了被置换的概率。&lt;/p&gt;
&lt;h2&gt;29. 什么是中断？&lt;/h2&gt;
&lt;p&gt;中断是计算机系统中一种重要的事件处理机制，当硬件设备或软件程序需要处理器立即处理某个事件时，会向 CPU 发送一个信号，强制暂停当前正在执行的程序，转而去执行与该事件相关的特定处理程序（称为中断处理程序或中断服务例程），待该程序执行完毕后再恢复之前被暂停的任务。&lt;/p&gt;
&lt;p&gt;中断的核心目的是提高处理器的效率，使其不必持续轮询设备状态，而是由外设在需要时主动通知 CPU，从而实现异步事件处理和并发执行。中断可分为两类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;硬件中断&lt;/strong&gt;：由外部设备（如键盘、鼠标、磁盘控制器、网卡）通过物理信号线触发，例如用户按下键盘按键或网卡接收到数据包时，会立即向 CPU 发送中断请求（IRQ），CPU 根据中断编号查找并执行对应的驱动处理程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;软件中断&lt;/strong&gt;：由程序执行特定指令（如系统调用、陷阱或异常）触发，例如应用程序请求操作系统服务（如读写文件）时通过软中断陷入内核，或发生除零错误等异常时强制切换处理流程。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;中断处理过程涉及保存当前执行上下文（如寄存器状态）、跳转到中断向量表指定的地址、执行处理程序，最后恢复上下文并返回原任务。这一机制是现代操作系统实现多任务、设备驱动和实时响应的基础。&lt;/p&gt;
&lt;h2&gt;30. 讲讲中断的流程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;中断触发：硬件设备（如键盘、网卡）或软件（通过 int 指令）发出中断信号。硬件中断通过中断控制器（如 APIC）汇总后发送给 CPU。&lt;/li&gt;
&lt;li&gt;中断响应：CPU 在每个指令执行结束后检查是否有中断请求。若有且未被屏蔽（可屏蔽中断），则暂停当前程序，保存当前执行上下文（包括程序计数器 PC、寄存器等状态到内核栈），并关闭中断（防止嵌套中断干扰现场保存）。&lt;/li&gt;
&lt;li&gt;中断路由：CPU 根据中断号查询中断描述符表（IDT），找到对应的中断服务程序（ISR） 的入口地址。&lt;/li&gt;
&lt;li&gt;执行中断处理程序：跳转到 ISR 执行具体的中断处理逻辑（如从键盘缓冲区读取按键值、处理网络数据包）。此时可能分为两部分：
&lt;ul&gt;
&lt;li&gt;上半部：在中断关闭状态下执行紧急任务（如响应硬件、拷贝数据），要求快速完成。&lt;/li&gt;
&lt;li&gt;下半部：通过软中断、任务队列或工作队列等机制延迟处理非紧急任务（如数据处理），此时会重新开启中断。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;恢复现场：ISR 执行完毕后，从内核栈恢复之前保存的上下文（寄存器等状态）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;31. 中断的类型有哪些？&lt;/h2&gt;
&lt;p&gt;中断可以根据其来源和触发方式分为以下几类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;硬件中断：由外部硬件设备触发，通过中断请求线（IRQ）向 CPU 发送信号。例如键盘输入、鼠标移动、磁盘 I/O 完成或网络数据包到达。硬件中断可进一步分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可屏蔽中断：可通过设置 CPU 标志位（如 IF 位）临时屏蔽，例如大多数外设中断。&lt;/li&gt;
&lt;li&gt;非可屏蔽中断：用于处理硬件紧急事件（如内存错误、电源故障），无法通过软件屏蔽。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;软件中断：由程序执行特定指令主动触发，用于实现系统调用或异常处理。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统调用：用户程序通过 &lt;code&gt;int 0x80&lt;/code&gt; 或 &lt;code&gt;syscall&lt;/code&gt; 指令陷入内核，请求操作系统服务。&lt;/li&gt;
&lt;li&gt;陷阱：用于调试（如断点中断 &lt;code&gt;int 3&lt;/code&gt;）或功能调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;异常：由 CPU 在执行指令时检测到错误或特殊条件时自动触发，属于同步中断。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;故障：可修复的错误（如缺页异常），处理后可重新执行指令。&lt;/li&gt;
&lt;li&gt;陷阱：执行后继续下一条指令（如调试断点）。&lt;/li&gt;
&lt;li&gt;中止：严重错误（如硬件故障），导致进程终止。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;伪中断：由硬件错误或信号干扰导致的无效中断请求，中断控制器需过滤此类信号。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;32. 你了解过哪些 I/O 模型？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;阻塞 I/O：这是最基础的模型。当应用程序发起一个 I/O 操作（如 read 系统调用）时，线程会被挂起（阻塞），直到操作系统内核将数据完全准备好并复制到用户空间缓冲区后，线程才被唤醒继续执行。在此期间，该线程无法执行任何其他任务。模型简单，但并发性能差，需要为每个连接创建大量线程。&lt;/li&gt;
&lt;li&gt;非阻塞 I/O：应用程序发起 I/O 操作后，如果数据尚未就绪，内核会立即返回一个错误（如 EWOULDBLOCK），而不是阻塞线程。应用程序需要不断地轮询（polling）内核，询问数据是否准备就绪。这种方式避免了线程阻塞，但轮询会消耗大量 CPU 资源，效率低下。&lt;/li&gt;
&lt;li&gt;I/O 多路复用：这是目前高并发网络应用中最主流的模型。应用程序通过调用 &lt;code&gt;select&lt;/code&gt;, &lt;code&gt;poll&lt;/code&gt;, 或 &lt;code&gt;epoll&lt;/code&gt; 等系统函数，将一个或多个文件描述符（socket）的监听委托给内核。内核会监视这些描述符，当其中任何一个有数据就绪时，就通知应用程序。应用程序收到通知后再进行实际的 I/O 操作（如 recv）。这使得一个线程可以同时管理多个 I/O 连接，极大地提高了系统的并发能力。它常被称为 Reactor 模式。&lt;/li&gt;
&lt;li&gt;信号驱动 I/O：应用程序通过 fcntl 系统调用为一个文件描述符开启信号驱动模式，并指定一个信号（如 SIGIO）。当内核数据就绪时，它会向应用程序发送一个信号。应用程序在信号处理函数中进行 I/O 操作。这种方式避免了轮询，但信号处理本身比较复杂，且在大流量场景中信号队列可能溢出，因此并不常用。&lt;/li&gt;
&lt;li&gt;异步 I/O：这是真正的异步模型。应用程序发起一个 I/O 操作（如 aio_read）后立即返回，内核会负责完成包括数据准备和从内核空间拷贝到用户空间在内的所有工作。整个操作完成后，内核会通过信号或回调函数通知应用程序。这与信号驱动 I/O 的关键区别在于：信号驱动 I/O 是内核通知我们“何时可以开始”进行 I/O 操作，而异步 I/O 是内核通知我们“I/O 操作已经完成”。Linux 原生 AIO 支持有限，更多使用像 &lt;code&gt;io_uring&lt;/code&gt; 这样的新一代异步接口。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;33. 讲一讲 I/O 多路复用，以及 select、poll、epoll 的区别？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;wxg 一面&lt;/p&gt;
&lt;p&gt;视频链接：&lt;a href=&quot;https://www.bilibili.com/video/BV1gN411e7gd/?spm_id_from=333.337.search-card.all.click&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;&lt;strong&gt;腾讯面试:请描述 select、poll、epoll 这三种 I/O 多路复用技术的执行原理&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;推荐阅读：&lt;a href=&quot;https://mp.weixin.qq.com/s/OmRdUgO1guMX76EdZn11UQ&quot;&gt;&lt;strong&gt;图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的！&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I/O 多路复用是一种 I/O 的处理方式，指的是&lt;strong&gt;复用一个线程处理多个 socket 中的事件&lt;/strong&gt;。能够复用资源，防止创建过多线程导致的上下文切换的开销。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010129252.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们熟悉的 select/poll/epoll 内核提供给用户态的&lt;strong&gt;多路复用系统调用&lt;/strong&gt;，进程可以通过一个系统调用函数从内核中获取多个事件。&lt;/p&gt;
&lt;p&gt;select/poll/epoll 是如何获取网络事件的呢？在获取事件时，先把所有连接（文件描述符）传给内核，再由内核返回产生了事件的连接，然后在用户态中再处理这些连接对应的请求即可。&lt;/p&gt;
&lt;h3&gt;select、poll&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ select 图解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010215740.png&quot; alt=&quot;image-20250501021504644&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ poll 图解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010216153.png&quot; alt=&quot;image-20250501021646782&quot;&gt;&lt;/p&gt;
&lt;p&gt;select 实现多路复用的方式是，将已连接的 Socket 都放到一个文件描述符集合，然后调用 select 函数将文件描述符集合拷贝到内核里，让内核来检查是否有网络事件产生，检查的方式很粗暴，就是通过遍历文件描述符集合的方式，当检查到有事件产生后，将此 Socket 标记为可读或可写， 接着再把整个文件描述符集合拷贝回用户态里，然后用户态还需要再通过遍历的方法找到可读或可写的 Socket，然后再对其处理。&lt;/p&gt;
&lt;p&gt;所以，对于 select 这种方式，需要进行 2 次「遍历」文件描述符集合，一次是在内核态里，一个次是在用户态里 ，而且还会发生 2 次「拷贝」文件描述符集合，先从用户空间传入内核空间，由内核修改后，再传出到用户空间中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;select&lt;/code&gt; 使用固定长度的 BitsMap，表示文件描述符集合，而且所支持的文件描述符的个数是有限制的，在 Linux 系统中，由内核中的 FD_SETSIZE 限制， 默认最大值为 1024，只能监听 0~1023 的文件描述符。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;poll&lt;/code&gt; 不再用 BitsMap 来存储所关注的文件描述符，取而代之用动态数组，以链表形式来组织，突破了 select 的文件描述符个数限制，当然还会受到系统文件描述符限制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是 poll 和 select 并没有太大的本质区别，都是使用「线性结构」存储进程关注的 Socket 集合&lt;/strong&gt;，因此都需要遍历文件描述符集合来找到可读或可写的 Socket，时间复杂度为 O(n)，而且也需要在用户态与内核态之间拷贝文件描述符集合，这种方式随着并发数上来，性能的损耗会呈指数级增长。&lt;/p&gt;
&lt;h3&gt;epoll&lt;/h3&gt;
&lt;p&gt;Linux 2.6 版本诞生了 epoll 模型，彻底解决了 select/poll 性能不足的问题&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ epoll 图解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010232627.png&quot; alt=&quot;image-20250501023238509&quot;&gt;&lt;/p&gt;
&lt;p&gt;先复习下 &lt;code&gt;epoll&lt;/code&gt; 的用法。如下的代码中，先用 &lt;code&gt;epoll_create&lt;/code&gt; 创建一个 epoll 对象 &lt;code&gt;epoll_fd&lt;/code&gt;，再通过 &lt;code&gt;epoll_ctl&lt;/code&gt; 将需要监视的 socket 添加到 &lt;code&gt;epoll_fd&lt;/code&gt; 中，最后调用 &lt;code&gt;epoll_wait&lt;/code&gt; 等待数据。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...);

// epoll_fd
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中

while(1) {
    int n = epoll_wait(...);
    for(接收到数据的socket){
        //处理
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;epoll 通过两个方面，很好解决了 select/poll 的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一点，&lt;strong&gt;epoll 在内核里使用「红黑树」来跟踪进程所有待检测的文件描述字&lt;/strong&gt;，把需要监控的 socket 通过 &lt;code&gt;epoll_ctl()&lt;/code&gt; 函数加入内核中的红黑树里，红黑树是个高效的数据结构，增删改一般时间复杂度是 $O(logn)$。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构，所以 select/poll 每次操作时都传入整个 socket 集合给内核，而 epoll 因为在内核维护了红黑树，可以保存所有待检测的 socket ，所以只需要传入一个待检测的 socket，减少了内核和用户空间大量的数据拷贝和内存分配。&lt;/li&gt;
&lt;li&gt;第二点，&lt;strong&gt;epoll 使用事件驱动的机制&lt;/strong&gt;，内核里维护了一个链表来记录就绪事件，当某个 socket 有事件发生时，内核通过&lt;strong&gt;回调函数&lt;/strong&gt;将其加入到这个就绪事件列表中，当用户调用 &lt;code&gt;epoll_wait()&lt;/code&gt; 函数时，只会返回有事件发生的文件描述符的个数，不需要像 select/poll 那样轮询扫描整个 socket 集合，大大提高了检测的效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从下图你可以看到 epoll 相关的接口作用：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010200863.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;epoll 的方式即使监听的 Socket 数量越多的时候，效率不会大幅度降低，能够同时监听的 Socket 的数目也非常的多了，上限就为系统定义的进程打开的最大文件描述符个数。因而，epoll 被称为解决 C10K 问题（服务器同时处理10,000个客户端连接的挑战）的利器。&lt;/p&gt;
&lt;h2&gt;34. epoll 的边缘触发和水平触发有什么区别？&lt;/h2&gt;
&lt;p&gt;epoll 支持两种事件触发模式，分别是边缘触发（edge-triggered，ET）和水平触发（level-triggered，LT）。&lt;/p&gt;
&lt;p&gt;这两个术语还挺抽象的，其实它们的区别还是很好理解的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用边缘触发模式时，当被监控的 Socket 描述符上有可读事件发生时，&lt;strong&gt;服务器端只会从 &lt;code&gt;epoll_wait&lt;/code&gt; 中苏醒一次，即使进程没有调用 &lt;code&gt;read&lt;/code&gt; 函数从内核读取数据，也依然只苏醒一次，因此我们程序要保证一次性将内核缓冲区的数据读取完&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;使用水平触发模式时，当被监控的 Socket 上有可读事件发生时，&lt;strong&gt;服务器端不断地从 &lt;code&gt;epoll_wait&lt;/code&gt; 中苏醒，直到内核缓冲区数据被 &lt;code&gt;read&lt;/code&gt; 函数读完才结束&lt;/strong&gt;，目的是告诉我们有数据需要读取；&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;举个例子，你的快递被放到了一个快递箱里，如果快递箱只会通过短信通知你一次，即使你一直没有去取，它也不会再发送第二条短信提醒你，这个方式就是边缘触发；如果快递箱发现你的快递没有被取出，它就会不停地发短信通知你，直到你取出了快递，它才消停，这个就是水平触发的方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这就是两者的区别，边缘触发的意思是只有第一次满足条件的时候才触发，之后就不会再传递同样的事件了；水平触发的意思是只要满足事件的条件，比如内核中有数据需要读，就一直不断地把这个事件传递给用户。&lt;/p&gt;
&lt;p&gt;如果使用边缘触发模式，I/O 事件发生时只会通知一次，而且我们不知道到底能读写多少数据，所以在收到通知后应尽可能地读写数据，以免错失读写的机会。因此，我们会循环从文件描述符读写数据，那么如果文件描述符是阻塞的，没有数据可读写时，进程会阻塞在读写函数那里，程序就没办法继续往下执行。所以，边缘触发模式一般和非阻塞 I/O 搭配使用，程序会一直执行 I/O 操作，直到系统调用（如 read 和 write）返回错误，错误类型为 EAGAIN 或 EWOULDBLOCK。&lt;/p&gt;
&lt;p&gt;如果使用水平触发模式，当内核通知文件描述符可读写时，接下来还可以继续去检测它的状态，看它是否依然可读或可写。所以在收到通知后，没必要一次执行尽可能多的读写操作。&lt;/p&gt;
&lt;p&gt;一般来说，边缘触发的效率比水平触发的效率要高，因为边缘触发可以减少 epoll_wait 的系统调用次数，系统调用也是有一定的开销的的，毕竟也存在上下文的切换。&lt;/p&gt;
&lt;h2&gt;35. coredump 是什么，什么时候触发 coredump&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;coredump&lt;/code&gt; 是程序由于异常或者 bug 在运行时异常退出或者终止，在一定的条件下生成的一个叫做 core 的文件，这个 core 文件会记录程序在运行时的内存，寄存器状态，内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。&lt;/p&gt;
&lt;p&gt;coredump 产生的条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;shell 资源控制限制，使用 ulimit -c 命令查看 shell 执行程序时的资源 ，如果为 0，则不会产生 coredump。可以用 ulimit -c unlimited 设置为不限大小。&lt;/li&gt;
&lt;li&gt;读写越界，包括：数组访问越界，指针指向错误的内存，字符串读写越界&lt;/li&gt;
&lt;li&gt;使用了线程不安全的函数，读写未加锁保护&lt;/li&gt;
&lt;li&gt;错误使用指针转换&lt;/li&gt;
&lt;li&gt;堆栈溢出&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;36. 什么是零拷贝？&lt;/h2&gt;
&lt;p&gt;传统 IO 的工作方式，从硬盘读取数据，然后再通过网卡向外发送，我们需要进行 4 上下文切换，和 4 次数据拷贝，其中 2 次数据拷贝发生在内存里的缓冲区和对应的硬件设备之间，这个是由 DMA 完成，另外 2 次则发生在内核态和用户态之间，这个数据搬移工作是由 CPU 完成的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010246557.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;为了提高文件传输的性能，于是就出现了零拷贝技术，它通过一次系统调用（&lt;code&gt;sendfile&lt;/code&gt; 方法）合并了磁盘读取与网络发送两个操作，降低了上下文切换次数。另外，拷贝数据都是发生在内核中的，天然就降低了数据拷贝的次数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010248223.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;零拷贝技术的文件传输方式相比传统文件传输的方式，减少了 2 次上下文切换和数据拷贝次数，只需要 2 次上下文切换和数据拷贝次数，就可以完成文件的传输，而且 2 次的数据拷贝过程，都不需要通过 CPU，2 次都是由 DMA 来搬运。&lt;/p&gt;
&lt;p&gt;总体来看，零拷贝技术可以把文件传输的性能提高至少一倍以上。&lt;/p&gt;
&lt;h2&gt;37. Linux I/O 栈&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;参考链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/435406445&quot;&gt;Linux 系统中 I/O 操作的数据读写流程介绍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_39802680/article/details/117707098&quot;&gt;Linux I/O 栈｜读写流程&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503290106690.png&quot; alt=&quot;Linux 的IO栈&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;程序的内存分布，其中包括内核空间（在内存中），联想一下即可理解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503290041746.png&quot; alt=&quot;image-20240725233029022&quot;&gt;&lt;/p&gt;
&lt;p&gt;linux I/O 存储栈分为 7 层:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VFS 虚拟文件层: 在各个具体的文件系统上建立一个抽象层，屏蔽不同文件系统的差异。&lt;/li&gt;
&lt;li&gt;PageCache 层: 为了缓解内核与磁盘速度的巨大差异。&lt;/li&gt;
&lt;li&gt;映射层 Mapping Layer: 内核必须从块设备上读取数据，Mapping layer 要确定在物理设备上的位置。&lt;/li&gt;
&lt;li&gt;通用块设备层: 通用块层处理来自系统其他组件发出的块设备请求，包含了块设备操作的一些通用函数和数据结构。&lt;/li&gt;
&lt;li&gt;I/O 调度层： IO 调度层主要是为了减少磁盘 IO 的次数，增大磁盘整体的吞吐量，队列中多个 bio 进行排序和合并。&lt;/li&gt;
&lt;li&gt;块设备驱动层: 每一类设备都有其驱动程序，负责设备的读写。&lt;/li&gt;
&lt;li&gt;物理设备层: 物理设备层有 HDD、SSD、Nvme 等磁盘设备。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250820-VwrRqM.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;38. 一次完整的 I/O 读请求处理链路&lt;/h2&gt;
&lt;p&gt;🔥 下面是一个完整的、详细的 I/O 请求处理链路（以一次文件读取操作为例）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;应用调用 &lt;code&gt;read()&lt;/code&gt; → 触发系统调用进入内核。&lt;/li&gt;
&lt;li&gt;VFS 根据文件类型调用文件系统的 &lt;code&gt;read_iter&lt;/code&gt; 方法。&lt;/li&gt;
&lt;li&gt;文件系统检查 Page Cache：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命中&lt;/strong&gt;：直接拷贝数据到用户缓冲区。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;未命中&lt;/strong&gt;：创建 &lt;code&gt;bio&lt;/code&gt; 请求，提交到块层。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I/O 调度器合并/排序请求后，下发到 SCSI 中层。&lt;/li&gt;
&lt;li&gt;设备驱动将请求转换为硬件命令，提交给控制器。&lt;/li&gt;
&lt;li&gt;设备通过 DMA 读取数据到内存，触发中断通知完成。&lt;/li&gt;
&lt;li&gt;中断处理程序唤醒原始请求，数据被填入 Page Cache 并返回用户态。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1. 系统调用接口（VFS 层）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：为用户空间（如 &lt;code&gt;libc&lt;/code&gt; 库）提供统一的系统调用接口（如 &lt;code&gt;read()&lt;/code&gt;, &lt;code&gt;write()&lt;/code&gt;, &lt;code&gt;io_uring_enter&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键组件&lt;/strong&gt;：&lt;strong&gt;虚拟文件系统（VFS）&lt;/strong&gt;。它抽象了所有文件系统和设备的操作，为上层提供统一的 &lt;code&gt;file_operations&lt;/code&gt; 函数指针接口（如 &lt;code&gt;read_iter&lt;/code&gt;, &lt;code&gt;write_iter&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 文件系统层&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：处理与特定文件系统（如 ext4、XFS、Btrfs）相关的逻辑，如路径解析、权限检查、元数据管理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键步骤&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将文件的偏移量和大小转换为&lt;strong&gt;块设备&lt;/strong&gt;上的逻辑块地址（LBA）。&lt;/li&gt;
&lt;li&gt;通过 &lt;strong&gt;Page Cache&lt;/strong&gt; 缓存文件和目录数据，减少直接磁盘访问。&lt;/li&gt;
&lt;li&gt;若请求的数据已在 Page Cache 中（缓存命中），则直接返回；否则触发缺页异常，发起 I/O 请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 块 I/O 层&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：管理 I/O 调度、合并请求，并转换为统一的块 I/O 请求（&lt;code&gt;struct bio&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键组件&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Page Cache&lt;/strong&gt;：通过 &lt;code&gt;address_space&lt;/code&gt; 操作与文件系统交互。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I/O 调度器&lt;/strong&gt;（如 mq-deadline、Kyber、BFQ）：对 I/O 请求进行排序、合并（Merge）、重排（Sort），以减少磁盘寻道时间并保证公平性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;块设备映射&lt;/strong&gt;：处理分区、软件 RAID（如 mdadm）或逻辑卷（LVM）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. SCSI/设备映射层&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：将块 I/O 请求转换为特定设备驱动可理解的格式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键组件&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SCSI 子系统&lt;/strong&gt;：为 SATA、SAS、NVMe 等设备提供统一的中层抽象（即使是非 SCSI 设备也通常接入此框架）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备映射器&lt;/strong&gt;（Device Mapper）：支持加密（dm-crypt）、快照（dm-snapshot）、多路径（dm-multipath）等高级功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 设备驱动层&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：直接与物理硬件控制器（如 NVMe、SATA AHCI）交互，通过 DMA 将数据从内存传输到设备。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键步骤&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将 I/O 请求转换为设备特定的命令描述符。&lt;/li&gt;
&lt;li&gt;通过写入控制器的寄存器或门铃（Doorbell）来提交命令。&lt;/li&gt;
&lt;li&gt;处理设备完成中断，并向上层通知 I/O 完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. 硬件层&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;物理设备&lt;/strong&gt;：如 NVMe SSD、SATA HDD、网络存储（通过 iSCSI 等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据传输&lt;/strong&gt;：通常通过 DMA（直接内存访问）在设备和内存之间传输数据，无需 CPU 参与。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;新范式：io_uring&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：提供高性能异步 I/O 接口，&lt;strong&gt;绕过传统内核路径的部分开销&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心机制&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;通过一对&lt;strong&gt;环形队列&lt;/strong&gt;（提交队列 SQ 和完成队列 CQ）在用户态和内核态之间传递请求和结果。&lt;/li&gt;
&lt;li&gt;支持轮询模式（Polling），避免中断开销，进一步降低延迟。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;39. write 怎么保证写入的持久性？&lt;/h2&gt;
&lt;p&gt;在 Linux 中，一个简单的 &lt;code&gt;write()&lt;/code&gt; 系统调用并不能保证数据真正持久化到物理存储设备上。要确保写入的持久性，必须理解内核的缓存机制并显式地采取额外措施。&lt;/p&gt;
&lt;h3&gt;1. 为什么 &lt;code&gt;write()&lt;/code&gt; 不能保证持久性？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Page Cache 缓冲：&lt;code&gt;write()&lt;/code&gt; 系统调用默认会将数据写入内核的 Page Cache 后就立即返回成功。这意味着数据并未立刻写入磁盘，而是停留在易失性内存中。&lt;/li&gt;
&lt;li&gt;延迟写入 Write Back：内核通过“延迟写入”策略来优化性能：定期（由 &lt;code&gt;dirty_writeback_centisecs&lt;/code&gt; 控制）或根据内存压力，将脏页异步刷回磁盘。在此期间若系统崩溃（断电、内核恐慌），这些数据会丢失。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. 如何保证写入持久性？&lt;/h3&gt;
&lt;h4&gt;方法 1：同步写入（O_SYNC）&lt;/h4&gt;
&lt;p&gt;在打开文件时使用 &lt;code&gt;O_SYNC&lt;/code&gt; 标志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;fd = open(&quot;file.txt&quot;, O_WRONLY | O_SYNC);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;作用：每次 &lt;code&gt;write()&lt;/code&gt; 都会阻塞调用者，直到数据及其元数据（如 inode 大小、修改时间）&lt;strong&gt;完全写入物理磁盘&lt;/strong&gt;后才返回。&lt;/li&gt;
&lt;li&gt;缺点：性能极差，每次写入都需等待磁盘 I/O 完成（延迟通常为毫秒级）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;方法 2：显式刷盘（fsync() / fdatasync()）&lt;/h4&gt;
&lt;p&gt;在 &lt;code&gt;write()&lt;/code&gt; 后调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;write(fd, data, size);
fsync(fd);  // 或 fdatasync(fd);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fsync()&lt;/code&gt;&lt;/strong&gt;：确保文件数据和元数据都持久化到磁盘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fdatasync()&lt;/code&gt;&lt;/strong&gt;：仅保证数据持久化，元数据（如访问时间）可能不立即刷盘，性能稍好。&lt;/li&gt;
&lt;li&gt;优点：可批量写入后一次性刷盘，比 &lt;code&gt;O_SYNC&lt;/code&gt; 性能更好。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;方法 3：直接 I/O（O_DIRECT）&lt;/h4&gt;
&lt;p&gt;在打开文件时使用 &lt;code&gt;O_DIRECT&lt;/code&gt; 标志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;fd = open(&quot;file.txt&quot;, O_WRONLY | O_DIRECT);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;作用：绕过 Page Cache，直接与磁盘交换数据。但需用户自行处理对齐限制（缓冲区地址、大小需对齐磁盘扇区，通常 512B/4KB）。&lt;/li&gt;
&lt;li&gt;注意：&lt;code&gt;O_DIRECT&lt;/code&gt; 仅避免缓存，但写入成功返回仅表示数据已提交到磁盘驱动器的缓存（可能仍在易失性缓存中）。若要真正持久化，仍需结合 &lt;code&gt;fsync()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;40. Linux 内存布局&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Linux 进程内存分布&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 Linux 操作系统中，虚拟地址空间的内部又被分为内核空间和用户空间两部分，不同位数的系统，地址空间的范围也不同。比如最常见的 32 位和 64 位系统，如下所示。通过这里可以看出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;32 位系统的内核空间占用 1G，位于最高处，剩下的 3G 是用户空间；&lt;/li&gt;
&lt;li&gt;64 位系统的内核空间和用户空间都是 128T，分别占据整个内存空间的最高和最低处，剩下的中间部分是未定义的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505011505174.jpeg&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;虽然每个进程都各自有独立的虚拟内存，&lt;strong&gt;但是每个虚拟内存中的内核地址，其实关联的都是相同的物理内存&lt;/strong&gt;。这样，进程切换到内核态后，就可以很方便地访问内核空间内存。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505011506844.jpeg&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来，进一步了解虚拟空间的划分情况，用户空间和内核空间划分的方式是不同的，内核空间的分布情况就不多说了。我们看看用户空间分布的情况，以 32 位系统为例，用一张图来表示它们的关系：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505011506976.png&quot; alt=&quot;虚拟内存空间划分&quot;&gt;&lt;/p&gt;
&lt;p&gt;通过这张图你可以看到，用户空间内存&lt;strong&gt;从低到高&lt;/strong&gt;分别是 6 种不同的内存段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码段，包括二进制可执行代码；&lt;/li&gt;
&lt;li&gt;数据段，包括已初始化的静态常量和全局变量；&lt;/li&gt;
&lt;li&gt;BSS 段，包括未初始化的静态变量和全局变量；&lt;/li&gt;
&lt;li&gt;堆段，包括动态分配的内存，从低地址开始向上增长；&lt;/li&gt;
&lt;li&gt;文件映射段，包括动态库、共享内存等，从低地址开始向上增长（跟硬件和内核版本有关 (opens new window)）；&lt;/li&gt;
&lt;li&gt;栈段，包括局部变量和函数调用的上下文等。栈的大小是固定的，一般是 8 MB。当然系统也提供了参数，以便我们自定义大小；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上图中的内存布局可以看到，代码段下面还有一段内存空间的（灰色部分），这一块区域是「保留区」，之所以要有保留区这是因为在大多数的系统里，我们认为比较小数值的地址不是一个合法地址，例如，我们通常在 C 的代码里会将无效的指针赋值为 NULL。因此，这里会出现一段不可访问的内存保留区，防止程序因为出现 bug，导致读或写了一些小内存地址的数据，而使得程序跑飞。&lt;/p&gt;
&lt;p&gt;在这 7 个内存段中，&lt;strong&gt;堆和文件映射段的内存是动态分配的&lt;/strong&gt;。比如说，&lt;strong&gt;使用 C 标准库的 &lt;code&gt;malloc()&lt;/code&gt; 或者 &lt;code&gt;mmap()&lt;/code&gt; ，就可以分别在堆和文件映射段动态分配内存&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;41. malloc 分配的是物理内存吗？&lt;/h2&gt;
&lt;p&gt;不是的，&lt;strong&gt;&lt;code&gt;malloc()&lt;/code&gt; 分配的是虚拟内存&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果分配后的虚拟内存没有被访问的话，虚拟内存是不会映射到物理内存的，这样就不会占用物理内存了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;只有在访问已分配的虚拟地址空间的时候，操作系统通过查找页表，发现虚拟内存对应的页没有在物理内存中，就会触发缺页中断，然后操作系统会建立虚拟内存和物理内存之间的映射关系。&lt;/p&gt;
&lt;h2&gt;42. 在 4GB 物理内存的机器上，申请 8GB 内存会发生什么？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 CSIG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 32位/64位 操作系统环境下，申请的虚拟内存超过物理内存后会怎么样？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 32 位操作系统，因为进程最大只能申请 3 GB 大小的虚拟内存，所以直接申请 8G 内存，会申请失败。&lt;/li&gt;
&lt;li&gt;在 64 位操作系统，因为进程最大只能申请 128 TB 大小的虚拟内存，即使物理内存只有 4GB，申请 8G 内存也是没问题，因为申请的内存是虚拟内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;程序申请的虚拟内存，如果没有被使用，它是不会占用物理空间的。当访问这块虚拟内存后，操作系统才会进行物理内存分配。&lt;/p&gt;
&lt;p&gt;如果申请物理内存大小超过了空闲物理内存大小，就要看操作系统有没有开启 &lt;em&gt;Swap&lt;/em&gt; 机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果没有开启 &lt;strong&gt;Swap&lt;/strong&gt; 机制，程序就会直接 &lt;strong&gt;OOM&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;如果有开启 Swap 机制，程序可以正常运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505091537400.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;43. 生产者消费者问题&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 WXG 一面&lt;/p&gt;
&lt;p&gt;下面讨论的是「单生产者单消费者」问题，那如果是「多生产者多消费者」问题呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生产者之间的竞争（互斥）：多个生产者可能同时尝试向缓冲区写入数据。&lt;/li&gt;
&lt;li&gt;消费者之间的竞争（互斥）：多个消费者可能同时尝试从缓冲区读取数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100014972.jpg&quot; alt=&quot;生产者-消费者模型&quot;&gt;&lt;/p&gt;
&lt;p&gt;生产者-消费者问题描述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生产者在生成数据后，放在一个缓冲区中；&lt;/li&gt;
&lt;li&gt;消费者从缓冲区取出数据处理；&lt;/li&gt;
&lt;li&gt;任何时刻，只能有一个生产者或消费者可以访问缓冲区；&lt;/li&gt;
&lt;li&gt;缓冲区空时，消费者必须等待生产者生成数据；&lt;/li&gt;
&lt;li&gt;缓冲区满时，生产者必须等待消费者取出数据。说明生产者和消费者需要同步。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么我们需要三个信号量，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥信号量 &lt;code&gt;mutex&lt;/code&gt;：用于互斥访问缓冲区，初始化值为 1；&lt;/li&gt;
&lt;li&gt;资源信号量 &lt;code&gt;fullBuffers&lt;/code&gt;：用于消费者询问缓冲区是否有数据，有数据则读取数据，初始化值为 0（表明缓冲区一开始为空）；&lt;/li&gt;
&lt;li&gt;资源信号量 &lt;code&gt;emptyBuffers&lt;/code&gt;：用于生产者询问缓冲区是否有空位，有空位则生成数据，初始化值为 n （缓冲区大小）；&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100017799.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;44. 哲学家就餐问题&lt;/h2&gt;
&lt;h3&gt;方案一&lt;/h3&gt;
&lt;p&gt;只要有一个哲学家进入了「临界区」，也就是准备要拿叉子时，其他哲学家都不能动，只有这位哲学家用完叉子了，才能轮到下一个哲学家进餐。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100030243.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;方案二&lt;/h3&gt;
&lt;p&gt;让偶数编号的哲学家「先拿左边的叉子后拿右边的叉子」，奇数编号的哲学家「先拿右边的叉子后拿左边的叉子」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100039373.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;45. 读者-写者问题&lt;/h2&gt;
&lt;p&gt;前面的「哲学家进餐问题」对于互斥访问有限的竞争问题（如 I/O 设备）一类的建模过程十分有用。&lt;/p&gt;
&lt;p&gt;另外，还有个著名的问题是「读者-写者」，它为数据库访问建立了一个模型。&lt;/p&gt;
&lt;p&gt;读者-写者的问题描述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;「读-读」允许：同一时刻，允许多个读者同时读&lt;/li&gt;
&lt;li&gt;「读-写」互斥：没有写者时读者才能读，没有读者时写者才能写&lt;/li&gt;
&lt;li&gt;「写-写」互斥：没有其他写者时，写者才能写&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;读者优先&lt;/h3&gt;
&lt;p&gt;使用信号量的方式来尝试解决：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信号量 wMutex：控制写操作的互斥信号量，初始值为 1；&lt;/li&gt;
&lt;li&gt;读者计数 rCount：正在进行读操作的读者个数，初始化为 0；&lt;/li&gt;
&lt;li&gt;信号量 rCountMutex：控制对 rCount 读者计数器的互斥修改，初始值为 1；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100100726.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;上面的这种实现，是读者优先的策略，因为只要有读者正在读的状态，后来的读者都可以直接进入，如果读者持续不断进入，则写者会处于饥饿状态。&lt;/p&gt;
&lt;h3&gt;写者优先&lt;/h3&gt;
&lt;p&gt;那既然有读者优先策略，自然也有写者优先策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只要有写者准备要写入，写者应尽快执行写操作，后来的读者就必须阻塞；&lt;/li&gt;
&lt;li&gt;如果有写者持续不断写入，则读者就处于饥饿；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在方案一的基础上新增如下变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信号量 rMutex：控制读者进入的互斥信号量，初始值为 1；&lt;/li&gt;
&lt;li&gt;信号量 wDataMutex：控制写者写操作的互斥信号量，初始值为 1；&lt;/li&gt;
&lt;li&gt;写者计数 wCount：记录写者数量，初始值为 0；&lt;/li&gt;
&lt;li&gt;信号量 wCountMutex：控制 wCount 互斥修改，初始值为 1；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100118027.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;46. 磁盘调度算法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100133104.jpg&quot; alt=&quot;磁盘的结构&quot;&gt;&lt;/p&gt;
&lt;p&gt;每个扇区是 &lt;code&gt;512&lt;/code&gt; 字节，多个具有相同编号的磁道形成一个圆柱，称之为磁盘的柱面。&lt;/p&gt;
&lt;p&gt;磁盘调度算法的目的很简单，就是为了提高磁盘的访问性能，一般是通过优化磁盘的访问请求顺序来做到的。&lt;/p&gt;
&lt;p&gt;寻道的时间是磁盘访问最耗时的部分，如果请求顺序优化的得当，必然可以节省一些不必要的寻道时间，从而提高磁盘的访问性能。&lt;/p&gt;
&lt;p&gt;假设有下面一个请求序列，每个数字代表磁道的位置：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;98，183，37，122，14，124，65，67&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;初始磁头当前的位置是在第 53 磁道。&lt;/p&gt;
&lt;p&gt;接下来，分别对以上的序列，作为每个调度算法的例子，那常见的磁盘调度算法有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先来先服务算法&lt;/li&gt;
&lt;li&gt;最短寻道时间优先算法&lt;/li&gt;
&lt;li&gt;扫描算法&lt;/li&gt;
&lt;li&gt;循环扫描算法&lt;/li&gt;
&lt;li&gt;LOOK 与 C-LOOK 算法&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1️⃣ 先来先服务&lt;/h3&gt;
&lt;p&gt;先来先服务（First-Come，First-Served，FCFS），顾名思义，先到来的请求，先被服务。&lt;/p&gt;
&lt;p&gt;请求顺序：&lt;code&gt;98，183，37，122，14，124，65，67&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;先来先服务算法总共移动了 640 个磁道的距离，这么一看这种算法，比较简单粗暴，但是如果大量进程竞争使用磁盘，请求访问的磁道可能会很分散，那先来先服务算法在性能上就会显得很差，因为寻道时间过长。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100139091.png&quot; alt=&quot;先来先服务&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2️⃣ 最短寻道时间优先&lt;/h3&gt;
&lt;p&gt;最短寻道时间优先（Shortest Seek First，SSF）算法的工作方式是，优先选择从当前磁头位置所需寻道时间最短的请求&lt;/p&gt;
&lt;p&gt;请求顺序：&lt;code&gt;65，67，37，14，98，122，124，183&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100140052.png&quot; alt=&quot;最短寻道时间优先&quot;&gt;&lt;/p&gt;
&lt;p&gt;磁头移动的总距离是 236 磁道，相比先来先服务性能提高了不少。&lt;/p&gt;
&lt;p&gt;但这个算法可能存在某些请求的饥饿，因为本次例子我们是静态的序列，看不出问题，假设是一个动态的请求，如果后续来的请求都是小于 183 磁道的，那么 183 磁道可能永远不会被响应，于是就产生了饥饿现象，这里产生饥饿的原因是磁头在一小块区域来回移动。&lt;/p&gt;
&lt;h3&gt;3️⃣ 扫描算法&lt;/h3&gt;
&lt;p&gt;最短寻道时间优先算法会产生饥饿的原因在于：磁头有可能再一个小区域内来回得移动。&lt;/p&gt;
&lt;p&gt;为了防止这个问题，可以规定：磁头在一个方向上移动，访问所有未完成的请求，直到磁头到达该方向上的最后的磁道，才调换方向，这就是扫描（Scan）算法。&lt;/p&gt;
&lt;p&gt;这种算法也叫做&lt;strong&gt;电梯调度算法&lt;/strong&gt;，比如电梯保持按一个方向移动，直到在那个方向上没有请求为止，然后改变方向。&lt;/p&gt;
&lt;p&gt;那么，假设扫描调度算先朝磁道号减少的方向移动，具体请求则会是下列从左到右的顺序：&lt;code&gt;37，14，0，65，67，98，122，124，183&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100142620.png&quot; alt=&quot;扫描算法&quot;&gt;&lt;/p&gt;
&lt;p&gt;磁头先响应左边的请求，直到到达最左端（0 磁道）后，才开始反向移动，响应右边的请求。&lt;/p&gt;
&lt;p&gt;扫描调度算法性能较好，不会产生饥饿现象，但是存在这样的问题，中间部分的磁道会比较占便宜，中间部分相比其他部分响应的频率会比较多，也就是说每个磁道的响应频率存在差异。&lt;/p&gt;
&lt;h3&gt;4️⃣ 循环扫描算法&lt;/h3&gt;
&lt;p&gt;扫描算法使得每个磁道响应的频率存在差异，那么要优化这个问题的话，可以总是按相同的方向进行扫描，使得每个磁道的响应频率基本一致。&lt;/p&gt;
&lt;p&gt;循环扫描（Circular Scan, CSCAN ）规定：只有磁头朝某个特定方向移动时，才处理磁道访问请求，而&lt;strong&gt;返回时直接快速移动至最靠边缘的磁道，也就是复位磁头，这个过程是很快的，并且返回中途不处理任何请求&lt;/strong&gt;，该算法的特点，就是磁道只响应一个方向上的请求。&lt;/p&gt;
&lt;p&gt;请求顺序：&lt;code&gt;65，67，98，122，124，183，199，0，14，37&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100146962.png&quot; alt=&quot;循环扫描算法&quot;&gt;&lt;/p&gt;
&lt;p&gt;磁头先响应了右边的请求，直到碰到了最右端的磁道 199，就立即回到磁盘的开始处（磁道 0），但这个返回的途中是不响应任何请求的，直到到达最开始的磁道后，才继续顺序响应右边的请求。&lt;/p&gt;
&lt;p&gt;循环扫描算法相比于扫描算法，对于各个位置磁道响应频率相对比较平均。&lt;/p&gt;
&lt;h3&gt;5️⃣ LOOK 与 C-LOOK 算法&lt;/h3&gt;
&lt;p&gt;我们前面说到的扫描算法和循环扫描算法，都是磁头移动到磁盘「最始端或最末端」才开始调换方向。&lt;/p&gt;
&lt;p&gt;那这其实是可以优化的，优化的思路就是磁头在移动到「最远的请求」位置，然后立即反向移动。&lt;/p&gt;
&lt;p&gt;那&lt;strong&gt;针对 SCAN 算法的优化则叫 LOOK 算法&lt;/strong&gt;，它的工作方式，磁头在每个方向上仅仅移动到最远的请求位置，然后立即反向移动，而不需要移动到磁盘的最始端或最末端，反向移动的途中会响应请求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100148456.png&quot; alt=&quot;LOOK 算法&quot;&gt;&lt;/p&gt;
&lt;p&gt;而&lt;strong&gt;针对 C-SCAN 算法的优化则叫 C-LOOK&lt;/strong&gt;，它的工作方式，磁头在每个方向上仅仅移动到最远的请求位置，然后立即反向移动，而不需要移动到磁盘的最始端或最末端，反向移动的途中不会响应请求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505100148106.png&quot; alt=&quot;C-LOOK 算法&quot;&gt;&lt;/p&gt;
&lt;h2&gt;47. 内存对齐？内存对齐发生在哪一层？我可以不对齐吗？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;字节跳动 · 基础架构一面&lt;/p&gt;
&lt;p&gt;腾讯 TEG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;内存对齐的本质目的是：为了让 CPU 访问内存时更加高效，避免跨越多个存储单元（比如 cache line 或总线周期），减少访问次数和性能开销。&lt;/p&gt;
&lt;p&gt;不对齐会导致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读取数据跨多个内存块或 cache line；&lt;/li&gt;
&lt;li&gt;可能需要多次访存、数据拼接；&lt;/li&gt;
&lt;li&gt;某些平台（如 ARM）甚至直接报错。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么内存对齐能减少访存次数？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现代 CPU 是以“块”单位读取数据的，比如 64 字节的 cache line。&lt;/p&gt;
&lt;p&gt;如果数据起始地址对齐，就能在一个 cache line 内读完，&lt;strong&gt;一次访问就搞定&lt;/strong&gt;；但如果不对齐，数据可能跨两个块，就会导致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU 需要 &lt;strong&gt;两次访问&lt;/strong&gt; 才拿到完整数据；&lt;/li&gt;
&lt;li&gt;多占用一次 cache、总线带宽；&lt;/li&gt;
&lt;li&gt;增加延迟、降低效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以对齐能保证数据落在一个块内，提高命中率，减少访存。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;内存对齐是在哪一存储层次对齐的？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;主要是在这两个层次【🔥&lt;strong&gt;字节面试官说是在 L1 Cache，主要还是为了在同一个 cacheline 中访问&lt;/strong&gt;】：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内存 → Cache（尤其是 L1 Cache）之间&lt;/strong&gt;：为了避免数据跨多个 cache line（通常是 64 字节）导致多次 cache 访问&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;总线传输层（memory bus）&lt;/strong&gt;：CPU 和内存之间的数据传输有对齐要求，总线通常按 4 字节、8 字节或更大位宽传输数据，不对齐可能增加传输次数或触发异常&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不是针对寄存器本身，寄存器只是最终使用数据的地方。对齐主要作用在&lt;strong&gt;数据加载阶段（load from memory）&lt;/strong&gt;，不是计算阶段。&lt;/p&gt;
&lt;h2&gt;48. CPU 读取数据的工作流？&lt;/h2&gt;
&lt;p&gt;CPU 读取数据时，首先在高速缓存（L1/L2/L3）中查找，若命中则直接返回；若未命中，则通过内存管理单元（MMU）将虚拟地址转换为物理地址，并访问主存（DRAM）加载数据块（缓存行），同时将其填入缓存。&lt;/p&gt;
&lt;p&gt;若数据不在主存，会触发缺页异常，由操作系统从磁盘交换空间调入所需内存页，再重新执行访问。&lt;/p&gt;
&lt;h2&gt;49. 计算机存储层次结构各层的典型访问粒度和时延&lt;/h2&gt;
&lt;p&gt;| 存储层次        | 访问粒度          | 访问时延    | 备注                                |
| :-------------- | :---------------- | :---------- | :---------------------------------- |
| &lt;strong&gt;CPU 寄存器&lt;/strong&gt;  | 4-8 字节          | ~0.3-0.5 ns | 直接与ALU交互，速度最快             |
| &lt;strong&gt;L1 缓存&lt;/strong&gt;     | 64 字节（缓存行） | ~0.5-1 ns   | 分指令与数据缓存，核心独享          |
| &lt;strong&gt;L2 缓存&lt;/strong&gt;     | 64 字节（缓存行） | ~3-10 ns    | 通常为核心独享或共享                |
| &lt;strong&gt;L3 缓存&lt;/strong&gt;     | 64 字节（缓存行） | ~10-20 ns   | 多核心共享，容量更大                |
| &lt;strong&gt;主存 (DRAM)&lt;/strong&gt; | 4KB (页) / 64字节 | ~50-100 ns  | 通过内存总线访问，需MMU转换虚拟地址 |
| &lt;strong&gt;SSD (NVMe)&lt;/strong&gt;  | 4KB (页)          | ~10-100 μs  | 需通过PCIe总线，涉及驱动和中断处理  |
| &lt;strong&gt;HDD (磁盘)&lt;/strong&gt;  | 512B-4KB (扇区)   | ~5-10 ms    | 含寻道时间和旋转延迟，机械操作慢    |
| &lt;strong&gt;网络存储&lt;/strong&gt;    | 可变 (通常≥1KB)   | ~1-100 ms   | 受网络延迟和协议开销影响极大        |&lt;/p&gt;
&lt;h2&gt;50. fork 16 GB 会发生什么？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;小红书 搜广推 C++ 一面&lt;/p&gt;
&lt;p&gt;主要考察写时复制&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当进程调用 fork 时，操作系统会创建原进程的完整副本，包括 16 GB 内存空间，但由于写时复制（Copy-on-Write）机制，实际物理内存不会立即翻倍，而是父子进程共享同一物理内存，仅当任一进程尝试修改内存页时，才会为该页创建独立副本，因此 fork 后初始内存占用几乎不变，但后续可能因写入操作导致内存逐渐增加。&lt;/p&gt;
&lt;h2&gt;51. 进程启动流程&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;高德 C++ 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;进程启动流程是操作系统将磁盘上的可执行文件加载到内存并开始执行的完整过程，主要包括以下步骤：&lt;strong&gt;Shell 解析命令后调用 &lt;code&gt;fork&lt;/code&gt; 创建新进程，接着通过 &lt;code&gt;exec&lt;/code&gt; 系列函数加载可执行文件，操作系统读取文件头获取代码段、数据段等信息，分配内存空间并建立虚拟内存映射，将程序段加载到指定内存地址，初始化 BSS 段为零，设置程序计数器指向入口点（如 C 语言的 main 函数），最后从用户态跳转到程序入口开始执行&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1. 完整的进程启动流程&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 以执行 ./myprogram arg1 arg2 为例的完整流程

用户输入命令
    ↓
Shell解析命令和参数
    ↓  
fork() 创建子进程
    ↓
子进程调用 execve() 系统调用
    ↓
操作系统加载器工作：
   - 验证可执行文件格式
   - 读取程序头表
   - 分配虚拟地址空间
   - 建立内存映射
   - 加载代码段、数据段
   - 初始化BSS段为0
    ↓
设置进程上下文：
   - 初始化寄存器
   - 设置栈指针
   - 传递环境变量和参数
    ↓
动态链接（如果需要）：
   - 加载共享库
   - 重定位符号地址
    ↓
跳转到程序入口 _start
    ↓
调用 __libc_start_main
    ↓  
调用 main() 函数
    ↓
程序开始执行用户代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 关键阶段详细说明&lt;/h3&gt;
&lt;h4&gt;阶段1：创建进程 - fork()&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;unistd.h&gt;
#include &amp;#x3C;iostream&gt;

// Shell 中执行的大致等价代码
void shell_execute(const char* program, char* const argv[]) {
    pid_t pid = fork();  // 创建子进程副本
    
    if (pid == 0) {
        // 子进程
        execve(program, argv, environ);  // 加载新程序
        // 如果execve成功，不会返回
        std::cerr &amp;#x3C;&amp;#x3C; &quot;执行失败&quot; &amp;#x3C;&amp;#x3C; std::endl;
        exit(1);
    } else {
        // 父进程 (Shell) 等待子进程
        waitpid(pid, nullptr, 0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;阶段2：加载程序 - execve()&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// execve 系统调用的核心工作：
// 1. 验证文件格式（ELF、Mach-O等）
// 2. 替换当前进程的地址空间
// 3. 设置新的内存映射
// 4. 初始化堆栈段

// 内存映射建立过程：
// +---------------------+
// |       栈段          | ← 包含 argc, argv, envp
// +---------------------+
// |       堆段          | ← 初始为空
// +---------------------+
// |       BSS段         | ← 未初始化数据（清零）
// +---------------------+
// |      数据段         | ← 已初始化全局变量
// +---------------------+
// |      代码段         | ← 程序指令
// +---------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;阶段3：程序初始化 - _start 到 main&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 实际的启动顺序（由链接器安排）：
// _start (汇编入口) → __libc_start_main → main

// 典型的 _start 实现（简化）：
void _start() {
    // 初始化基础环境
    long argc = *((long*)((char**)&amp;#x26;argc - 1));
    char** argv = (char**)((char**)&amp;#x26;argc + 1);
    char** envp = argv + argc + 1;
    
    // 调用libc初始化
    __libc_start_main(main, argc, argv, 
                      __libc_csu_init, 
                      __libc_csu_fini, 
                      NULL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 实际代码示例&lt;/h3&gt;
&lt;h4&gt;完整的进程创建示例&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;unistd.h&gt;
#include &amp;#x3C;sys/wait.h&gt;

int main(int argc, char* argv[], char* envp[]) {
    std::cout &amp;#x3C;&amp;#x3C; &quot;父进程 PID: &quot; &amp;#x3C;&amp;#x3C; getpid() &amp;#x3C;&amp;#x3C; std::endl;
    
    pid_t pid = fork();
    
    if (pid == -1) {
        std::cerr &amp;#x3C;&amp;#x3C; &quot;fork 失败&quot; &amp;#x3C;&amp;#x3C; std::endl;
        return 1;
    } else if (pid == 0) {
        // 子进程
        std::cout &amp;#x3C;&amp;#x3C; &quot;子进程 PID: &quot; &amp;#x3C;&amp;#x3C; getpid() &amp;#x3C;&amp;#x3C; std::endl;
        
        // 准备新程序的参数
        char* new_argv[] = {&quot;/bin/ls&quot;, &quot;-l&quot;, nullptr};
        char* new_envp[] = {&quot;PATH=/usr/bin&quot;, nullptr};
        
        // 执行新程序（替换当前进程映像）
        execve(&quot;/bin/ls&quot;, new_argv, new_envp);
        
        // 只有execve失败才会执行到这里
        std::cerr &amp;#x3C;&amp;#x3C; &quot;execve 失败&quot; &amp;#x3C;&amp;#x3C; std::endl;
        return 1;
    } else {
        // 父进程
        std::cout &amp;#x3C;&amp;#x3C; &quot;父进程等待子进程 &quot; &amp;#x3C;&amp;#x3C; pid &amp;#x3C;&amp;#x3C; std::endl;
        waitpid(pid, nullptr, 0);
        std::cout &amp;#x3C;&amp;#x3C; &quot;子进程执行完毕&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;理解程序参数传递&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

// 观察启动时的参数和环境变量
int main(int argc, char* argv[], char* envp[]) {
    std::cout &amp;#x3C;&amp;#x3C; &quot;参数个数: &quot; &amp;#x3C;&amp;#x3C; argc &amp;#x3C;&amp;#x3C; std::endl;
    
    // 打印所有参数
    for (int i = 0; i &amp;#x3C; argc; ++i) {
        std::cout &amp;#x3C;&amp;#x3C; &quot;argv[&quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot;]: &quot; &amp;#x3C;&amp;#x3C; argv[i] &amp;#x3C;&amp;#x3C; std::endl;
    }
    
    // 打印环境变量（前10个）
    std::cout &amp;#x3C;&amp;#x3C; &quot;\n环境变量:&quot; &amp;#x3C;&amp;#x3C; std::endl;
    for (int i = 0; envp[i] != nullptr &amp;#x26;&amp;#x26; i &amp;#x3C; 10; ++i) {
        std::cout &amp;#x3C;&amp;#x3C; &quot;envp[&quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot;]: &quot; &amp;#x3C;&amp;#x3C; envp[i] &amp;#x3C;&amp;#x3C; std::endl;
    }
    
    return 0;
}

// 编译后运行：./program arg1 arg2
// 输出：
// 参数个数: 3
// argv[0]: ./program
// argv[1]: arg1  
// argv[2]: arg2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 动态链接过程&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 对于动态链接的程序，启动还包括：

// 1. 加载动态链接器 (ld-linux.so)
// 2. 解析程序依赖的共享库
// 3. 重定位符号地址
// 4. 执行共享库的初始化代码

// 可以使用 ldd 查看依赖：
// $ ldd /bin/ls
//     linux-vdso.so.1
//     libselinux.so.1 =&gt; /lib/x86_64-linux-gnu/libselinux.so.1
//     libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 内存布局初始化&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 进程启动后的典型内存布局：

#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;cstdlib&gt;

int global_var = 100;                    // 数据段
const int const_global = 200;            // 只读数据段  
int uninit_global;                       // BSS段

int main() {
    int local_var = 300;                 // 栈
    static int static_local = 400;       // 数据段
    int* heap_var = new int(500);        // 堆
    
    std::cout &amp;#x3C;&amp;#x3C; &quot;代码段: &quot; &amp;#x3C;&amp;#x3C; (void*)main &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;数据段: &quot; &amp;#x3C;&amp;#x3C; &amp;#x26;global_var &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;只读数据: &quot; &amp;#x3C;&amp;#x3C; &amp;#x26;const_global &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;BSS段: &quot; &amp;#x3C;&amp;#x3C; &amp;#x26;uninit_global &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;栈: &quot; &amp;#x3C;&amp;#x3C; &amp;#x26;local_var &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;堆: &quot; &amp;#x3C;&amp;#x3C; heap_var &amp;#x3C;&amp;#x3C; std::endl;
    
    delete heap_var;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 关键系统调用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 进程启动涉及的主要系统调用：

// 1. fork() - 创建进程副本
// 2. execve() - 加载新程序
// 3. mmap() - 内存映射（文件、堆等）
// 4. brk()/sbrk() - 调整堆大小
// 5. mprotect() - 设置内存保护

// 可以使用 strace 跟踪：
// $ strace -f ./myprogram
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结要点&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;进程启动的核心步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;进程创建&lt;/strong&gt; - fork() 复制当前进程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序加载&lt;/strong&gt; - execve() 替换地址空间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存初始化&lt;/strong&gt; - 建立代码段、数据段、堆栈段&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态链接&lt;/strong&gt; - 加载共享库并重定位&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;环境设置&lt;/strong&gt; - 传递参数和环境变量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行跳转&lt;/strong&gt; - 从 _start 到 main&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;面试回答关键点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;理解 fork + execve 的分工&lt;/li&gt;
&lt;li&gt;知道内存各段的初始化顺序&lt;/li&gt;
&lt;li&gt;了解参数和环境变量的传递机制&lt;/li&gt;
&lt;li&gt;清楚动态链接的基本过程&lt;/li&gt;
&lt;li&gt;明白用户态到程序执行的转换&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;52. 动态库文件结构&lt;/h2&gt;
&lt;p&gt;动态库（共享库）的文件结构是基于标准格式（如ELF on Linux, Mach-O on macOS, PE on Windows）组织的，它包含&lt;strong&gt;代码段、数据段、符号表、重定位表、动态段和依赖信息&lt;/strong&gt;等部分，使得库可以在运行时被多个进程共享加载。其核心特点是代码位置无关（PIC），通过过程链接表（PLT）和全局偏移表（GOT）实现延迟绑定，从而支持动态链接和内存共享。&lt;/p&gt;
&lt;h2&gt;53. 动态链接过程&lt;/h2&gt;
&lt;p&gt;动态链接流程是程序运行时将可执行文件与其依赖的共享库进行连接的过程，主要分为&lt;strong&gt;加载时链接&lt;/strong&gt;和&lt;strong&gt;运行时链接&lt;/strong&gt;两个阶段：首先动态链接器加载程序依赖的所有共享库并解析符号引用，通过全局偏移表（GOT）和过程链接表（PLT）实现延迟绑定，在首次调用函数时进行符号解析和重定位，最终建立完整的执行环境。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;程序启动
    ↓
内核加载可执行文件
    ↓
检查 .interp 段找到动态链接器 (ld-linux.so)
    ↓
将控制权交给动态链接器
    ↓
链接器自举初始化
    ↓
加载程序本身的动态段 (.dynamic)
    ↓
递归加载所有依赖的共享库
    ↓
符号解析和重定位：
   - 建立符号哈希表
   - 解析未定义符号
   - 重定位 GOT/PLT 条目
    ↓
执行共享库的初始化函数 (.init)
    ↓
将控制权交回程序入口 (_start)
    ↓
程序开始执行，使用延迟绑定：
   - 首次调用函数 → PLT → 动态链接器解析
   - 解析结果填入 GOT
   - 后续调用直接跳转
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;54. 进程 vs 线程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;地址空间&lt;/strong&gt;：进程有&lt;strong&gt;独立虚拟地址空间&lt;/strong&gt;；同一进程内的线程&lt;strong&gt;共享地址空间&lt;/strong&gt;与大部分资源（堆、静态区、文件描述符等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源/隔离&lt;/strong&gt;：进程隔离强、崩溃不互相影响；线程隔离弱、一个线程越界可能拖垮整个进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度/开销&lt;/strong&gt;：创建/切换进程开销普遍高于线程；线程更轻量，适合密集并发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通信&lt;/strong&gt;：进程需 IPC；线程直接读写共享内存但必须同步（锁/原子/条件变量等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;55. 进程间通信（IPC）&amp;#x26; 常用的&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;管道/命名管道（pipe/FIFO）&lt;/strong&gt;：简单、单主机、字节流；父子/同机进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UNIX 域套接字&lt;/strong&gt;：同机、支持&lt;strong&gt;数据+FD 传递&lt;/strong&gt;、语义和 TCP 类似；服务化常用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP/UDP 套接字&lt;/strong&gt;：跨机器通信；TCP 有序可靠，UDP 低延迟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享内存（POSIX shm/System V）+ 同步原语（信号量/互斥量/futex）&lt;/strong&gt;：最高吞吐，但&lt;strong&gt;自己保证并发正确性&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消息队列（POSIX/System V）&lt;/strong&gt;：内核队列，结构化消息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mmap/内存映射文件&lt;/strong&gt;：文件即内存，适合大文件、零拷贝。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更上层&lt;/strong&gt;：gRPC/Thrift、DBus 等做序列化与服务发现。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;工程里最常用：UNIX 域套接字、共享内存+锁、TCP、mmap、gRPC。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;56. 操作系统分配给进程的资源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;虚拟地址空间（代码/数据/堆/栈、mmap 区域）、页表&lt;/li&gt;
&lt;li&gt;打开的文件描述符/套接字、工作目录、umask&lt;/li&gt;
&lt;li&gt;进程号/父子关系、凭据（uid/gid/capabilities）&lt;/li&gt;
&lt;li&gt;信号处置表、定时器、线程与它们的栈&lt;/li&gt;
&lt;li&gt;IPC 对象（共享内存、信号量、消息队列）&lt;/li&gt;
&lt;li&gt;调度份额/优先级、cgroup/命名空间限制（在容器场景）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;57. 为什么采用分页而不是分段？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：现代通用 OS 以&lt;strong&gt;分页&lt;/strong&gt;为主，段仅用于极少量兼容/逻辑标记。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分页（固定大小，如 4KiB）优势&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无外部碎片&lt;/strong&gt;（只在最后一页可能有内部碎片）。&lt;/li&gt;
&lt;li&gt;硬件简单：页表 + TLB；易做&lt;strong&gt;按需分配/缺页换入/写时复制&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;保护与共享粒度统一（页为单位映射/权限）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分段（可变长）劣势&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;易产生&lt;strong&gt;外部碎片&lt;/strong&gt;，需要紧缩/搬移。&lt;/li&gt;
&lt;li&gt;硬件复杂（段表 + 段间跨越处理），与换页/缓存局部性冲突。&lt;/li&gt;
&lt;li&gt;x86-64 实际上把大多数段功能&lt;strong&gt;弱化/屏蔽&lt;/strong&gt;，保护依赖分页。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;折中&lt;/strong&gt;：逻辑上“分段”（代码/数据/堆/栈），实现上“分页”。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;追问：大页（2MiB/1GiB）？——仍是分页，只是页更大以降低 TLB 压力；映射策略而非分段回潮。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;58. 断电下如何保证数据完整性（非原子操作）&lt;/h2&gt;
&lt;p&gt;按层分：&lt;strong&gt;应用&lt;/strong&gt; → &lt;strong&gt;文件系统&lt;/strong&gt; → &lt;strong&gt;块设备/存储&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;应用层模式&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;写临时文件 + fsync + rename 原子替换&lt;/strong&gt;：&lt;code&gt;write(tmp) → fsync(tmp) → rename(tmp, real)&lt;/code&gt;；POSIX 保证 &lt;code&gt;rename&lt;/code&gt; 原子。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双写/版本号&lt;/strong&gt;：旧版本保留，新版本落盘并写入更高版本号，恢复时选最大有效版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件系统层&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Journaling（ext4/XFS/NTFS）&lt;/strong&gt;：先把&lt;strong&gt;元数据变化&lt;/strong&gt;（可选含数据）写入&lt;strong&gt;日志区&lt;/strong&gt;并 &lt;code&gt;fsync&lt;/code&gt;，再应用到主区；崩溃后&lt;strong&gt;重放日志&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;ext4 模式：&lt;code&gt;journal&lt;/code&gt;（数据+元数据）、&lt;code&gt;ordered&lt;/code&gt;（默认：元数据入日志，数据先落盘）、&lt;code&gt;writeback&lt;/code&gt;（只记元数据，吞吐高、风险大）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;COW（btrfs/ZFS）&lt;/strong&gt;：写新位置、最后&lt;strong&gt;原子切换根指针&lt;/strong&gt;，天生避免部分写撕裂；配合&lt;strong&gt;校验和&lt;/strong&gt;端到端检测损坏。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储层&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;写屏障/缓存刷写&lt;/strong&gt;：强制控制顺序（FUA/flush），避免控制器缓存丢电。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;带断电保护的设备&lt;/strong&gt;（电容/电池）避免 cache 丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;误区：只 &lt;code&gt;fsync(fd)&lt;/code&gt; 不 &lt;code&gt;fsync(dir)&lt;/code&gt;。——创建/rename 后应 &lt;code&gt;fsync&lt;/code&gt; 其所在目录，确保目录项持久。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;59. 写前日志（WAL）详细机制（ARIES 思想）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：崩溃后保证持久化一致性，支持 Steal/No-Force（页可被偷刷，提交不强制写页）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心规则（WAL rule）&lt;/strong&gt;：&lt;strong&gt;日志先于数据落盘&lt;/strong&gt;。任何页写出前，相关日志记录（含 LSN）必须已稳定落盘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志记录&lt;/strong&gt;（物理/生理混合）：事务 id、页 id、偏移、旧值/新值、&lt;code&gt;LSN&lt;/code&gt;；每个数据页保存 &lt;code&gt;pageLSN&lt;/code&gt;（最新应用日志的 LSN）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提交路径&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;事务产生修改 → 生成日志（append-only）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fsync(log)&lt;/code&gt; 成功即&lt;strong&gt;提交完成&lt;/strong&gt;（No-Force），数据页稍后异步刷。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checkpoint&lt;/strong&gt;（“模糊”快照）：记录&lt;strong&gt;脏页表（DPT）&lt;strong&gt;与&lt;/strong&gt;活跃事务表&lt;/strong&gt;，无需停世界。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;崩溃恢复三阶段&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Analysis&lt;/strong&gt;：扫描日志，重建 DPT/活跃事务集。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redo（重复历史）&lt;/strong&gt;：从最早需要的 LSN 开始，只对 &lt;code&gt;pageLSN &amp;#x3C; record.LSN&lt;/code&gt; 的页重做，保证持久化到崩溃点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Undo&lt;/strong&gt;：对未提交事务按时间倒序回滚，写**补偿日志（CLR）**以实现幂等/可重入回放。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页写出判定&lt;/strong&gt;：写页时带上其 &lt;code&gt;pageLSN&lt;/code&gt;；重做阶段据此判“是否需要重放”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化&lt;/strong&gt;：组提交（group commit）、对齐日志块、校验和、日志切段/归档。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;常见坑：未严格保证“日志先行”导致&lt;strong&gt;撕裂页不可重做&lt;/strong&gt;；把日志与数据放同一易丢电缓存而&lt;strong&gt;缺少写屏障&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="/_astro/20250823-2j2LWn.oeDkM9lS.png"/><enclosure url="/_astro/20250823-2j2LWn.oeDkM9lS.png"/></item><item><title>2025 互联网秋招进行时指南</title><link>https://coooredump.github.io/blog/recruitment/2025-autumn-recruitment</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/2025-autumn-recruitment</guid><description>比较是偷走幸福的贼</description><pubDate>Fri, 22 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;面试篇&lt;/h2&gt;
&lt;h3&gt;精简化回答&lt;/h3&gt;
&lt;p&gt;一般面试主要由以下几个环节组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自我介绍&lt;/li&gt;
&lt;li&gt;个人项目&lt;/li&gt;
&lt;li&gt;项目介绍&lt;/li&gt;
&lt;li&gt;重点难点新颖点&lt;/li&gt;
&lt;li&gt;知识点深挖&lt;/li&gt;
&lt;li&gt;八股文问答&lt;/li&gt;
&lt;li&gt;手撕算法题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并不是每一场面试都会按照这几个环节来，不同的公司可能各自的侧重点不同，比如腾讯会把重点放在八股文问答环节，考察你对基础知识的掌握。&lt;/p&gt;
&lt;p&gt;但不管怎样，我们在面试前都可以按照这个流程来去准备，并且在这个过程中，我们一定要养成一个能力：&lt;strong&gt;用最精练的语言回答问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这句话很好理解，例如自我介绍时，要尽可能在 1 分钟左右将自己的背景、荣誉、论文、奖项等有助于突出个人优势的经历展现出来，这点相信大家都有所准备。但是对于个人项目介绍环节，有些人可能会因为项目体量稍大、涉及知识点稍多等原因开始进行长篇大论式的介绍，这是非常忌讳的，过多的阐述反而会让人找不到重点，大部分面试官也都没有耐心听你讲很多，所以建议大家私下准备时可以手动对项目做个精简的总结，比较推荐的做法是「&lt;strong&gt;仿照论文&lt;/strong&gt;」的写法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;XX 是一个关于 XX 方面的项目，主要实现了 XX（挑最核心的）能力，该项目实现过程中遇到的重点难点为 XX，为了解决该问题，本文采用了 XX 的方法，对比业内现有方案，本项目的优势主要在于 XX&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然简历上项目的写法也可以按照这个格式，不过纵使项目涉及的点很多也不要写过多内容，挑最核心的就可以，其余内容在面试官深入挖掘该项目时再进行补充即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;精简化回答&lt;/strong&gt;的能力体现出的是个人思维的敏捷性以及言语表达能力，面试官也尤为在意，因此面试者应该日常注重培养该能力。&lt;/p&gt;
&lt;h3&gt;记录并总结每一场面试&lt;/h3&gt;
&lt;p&gt;中国的学生们最擅长的就是考试，每一场考试后我们会总结出错题和遗漏的知识点来查漏补缺，同时还会购买一些辅助资料来学习专家为我们总结的知识点，又或是买一些真题卷来为考试做模拟演练，不管怎样，我们的目的只有一个，那就是&lt;strong&gt;在下一场考试做得更好&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;面试也是同样的道理，在秋招中我们可能会经历数十场面试，在每一次面试结束后我们都应该回顾这场面试的主要内容并记录下答得不好的地方，面试后再通过搜集资料对这些问题进行修正，在之后的每一场面试前我们都应该重新回顾这些内容。&lt;/p&gt;
&lt;p&gt;除开从亲身参与的面试中获取经验外，我们还可以在牛客等平台去看别人分享的面试记录，如果发现有自己不懂的地方也应该及时记录下来，并且在空余的时间，我们可以发散思维，从自己记录的内容里引申出一些面试可能会涉及的别的认识点，这样可以逐步扩充我们的知识库。&lt;/p&gt;
&lt;p&gt;上述记录并总结的过程推荐借助 AI 来高效完成。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;总结&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507072335927.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;offer 选择篇&lt;/h2&gt;
&lt;h3&gt;具体岗位方向的决定（技术➕业务）&lt;/h3&gt;
&lt;p&gt;虽然我们在面试前已经确定了未来就业的一个大致方向，例如是算法岗还是研发岗，但对于具体细致的方向很多人没有一个明确的想法，实际上岗位投递时我们能选择某个岗位但至于具体做哪方面的内容也是不确定的，这样就会导致大家在收到多个 offer 后会很纠结到底该怎么选择。&lt;/p&gt;
&lt;p&gt;但不管怎么样，任何岗位的职责都可以被定义为&lt;strong&gt;业务➕技术&lt;/strong&gt;的组合，这里可以拿抖音举例，大公司一般都会建设一套庞大的研发体系，&lt;strong&gt;上层重业务，下层重技术&lt;/strong&gt;，其对应的发展路线也是截然不同的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于基础架构岗位比如数据库内核分布式存储等，业务与技术是高度重合的，你需要 follow 业内最先进的技术并想办法应用到你的工作中，因此掌握并应用技术就是你的业务。&lt;/li&gt;
&lt;li&gt;对于业务研发岗位技术与业务重合度要小很多，你的工作中可能不会涉及到很高大上的技术（代码主要涉及 crud），但你的重心需要 focus 在业务模型中，对于重业务的岗位其业务模型是相当复杂的，你在工作中的成长主要是对整个业务模型有更深入的了解。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507072334670.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;关于二者孰好孰坏很难给个结论，有人觉得做纯技术岗完全是给业务打工，毕竟技术是要服务于业务的，但也有很多人觉得业务岗纯 crud 岗，没什么技术含量，当然实际情况是复杂的，我这里主要是想给大家做一个更细致的说明，至于具体的抉择大家可以参考下图给出的三个关注点，另外我们拿到 offer 后最好让 hr 安排一个员工跟我们对接了解一下具体工作内容，而不是直接在网上求助，网友只能给一个模糊的答案。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507072335934.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;当然，我觉得校招方向的选择也不用太过于纠结，职业生涯才刚开始，未来还有很多机会，所以放心大胆的做决定吧。&lt;/p&gt;
&lt;h3&gt;薪资 argue&lt;/h3&gt;
&lt;p&gt;关于薪资的等级普遍被划分为「白菜、SP、SSP」三个等级，那如何拿到高等级呢？本人特别优异是一方面，学会与 hr 博弈是另一方面，hr 的绩效就是用尽可能少的钱招到优秀的人力，如果你别无选择，哪怕你真的很优秀 hr 也会压低你的薪资，因此建议 offer 能多拿就多拿。&lt;/p&gt;
&lt;h2&gt;致 2026 年毕业正在秋招的你&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;下文链接：&lt;a href=&quot;https://www.nowcoder.com/feed/main/detail/6fd7f15714a6494c96f4fa5b2cd6d462&quot;&gt;&lt;strong&gt;timeErrors&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;秋招并非完全和它的名字一样，秋招并非是只有秋天才招聘，近些年来夏天也会招聘&lt;/p&gt;
&lt;p&gt;我是 25 年毕业的，当年拿过一些 ssp，比如阿里云啥的，当然是比较久远的事了，现在进厂了，虽然才入职一个来月，但和当年很不一样了，时间就很紧张，一些准备上的东西我就不说了，比如什么时候投递，简历怎么写，怎么准备笔试，八股等等，牛客上都有很好的经验贴之类，我挑一些我认为很重要的点来讲吧，下面是个人感受和观点，并不一定正确，仅供参考。&lt;/p&gt;
&lt;p&gt;简单来讲就是：&lt;strong&gt;想好目的，降低预期，接受现实&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;最重要的就是心态，不要过度提高自己的预期，也不要因为一两次失利而气馁。&lt;/p&gt;
&lt;p&gt;于秋招而言，特别是从业于互联网，根据我院我熟知的周围起码二三十人去大厂的人来看，几乎没有人可以最终选出来一个满意的 offer，大部分人都是选了个差不多可以接受的，尽管大家基本都是 5、6 个 offer 往上，且基本都有 ssp，而且非常普遍的一个现象就是来互联网的大家都很累，只是谁更累而已，据我观察，十点左右走是一个比较符合大家从公司跑路的时间，不乏有干到 1、2 点甚至 3、4 点的人。&lt;/p&gt;
&lt;p&gt;所以去互联网，只有钱多累，和钱少累两种，而且要么是心累，要么身体累，更有甚者两者都累，两者都累建议尽早跑。&lt;/p&gt;
&lt;p&gt;于工作而言，因为去哪里都很累，所以其他因素就不得不考虑，这些年进厂的感受，应该着重考虑下面几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;心累 &gt; 身体累，即组里的氛围是很需要看中的，如果你的老板或者同事天天 pua 你，总是告诉你你什么都干不好，那我想干起来也是相当难受的。但恰巧这一点是最难判断的，你问老板组里氛围怎么样，就和问买水果的老板这瓜甜不甜一样，老板肯定说没有不甜的瓜。身体累顾名思义，就是加班，这里建议问问 HR，加班是调休还是双倍薪资，给自己卖一个合适的预期，比如有人就喜欢双倍薪资，有人就喜欢调休，当然绝大多数人都不喜欢加班，但这不是没办法吗&lt;/li&gt;
&lt;li&gt;业务稳定/上升，业务稳定，意味着你要做的工作不会特别赶，简称你可以早点下班，少加点班，因为需求不会那么多和那么着急，反正业务用了很多年，也不差这一会，但这也有可能意味着你的晋升或者跳槽认可度略低，这点很容易理解，晋升是因为前面可能有很多老兵排着，跳槽和工作量某种程度上挂钩，产出少了自然不好包装自己。业务上升又是另一个极端，产品迭代快，需求多，需求高强度的加班。但在互联网，并不是你干的多就升的快，只是业务稳定/上升代表着这个部门突然消失的概率要小些，也意味着你不容易试用期就被裁掉（如果业务稳定的部门突然招了很多人，需要警惕下，老板估计是要整什么大活了）&lt;/li&gt;
&lt;li&gt;拥抱变化，互联网总是很喜欢拥抱变化，即你曾经或许实习的组氛围什么都不错，但你真正入职可能就变了，一年在互联网里是很长的一段时间，一年前京东还不搞外卖，淘宝还没有闪购，deepseek 还没有出世，百度还在讲闭源模型一定比开源牛逼，gpt 还是稳压 gemini。身边有不少人，要么入职的时候组没了，要么那个组多发了 hc，结果 ta 入职到别的组去了等等。当然这种事情也可以和老板 HR 确认，只是说怎么确认都会有不确定性，做好拥抱变化的准备，随遇而安吧&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;offer 选择&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果决定来互联网，有大厂选大厂，这意味着你的机动性会更高些，谁也不知道入职后会不会踩坑，大厂会比中小厂在跳槽的时候容错会更高些，如果都是大厂，除了上面几点外，&lt;strong&gt;其余就是向钱看了，至于什么大厂间不同的 title，toc/tob，核不核心，其实没那么重要，自己干的舒服是最重要的，舒服这个定义现在很廉价，就是一个正常的老板，正常的业务，正常的同事而已，但实现这一点很难&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以我建议，一定要想好自己为什么要来互联网，你的目标是什么，这一点很重要，&lt;strong&gt;人还是需要一点理想主义的&lt;/strong&gt;，如果为了狠狠赚一笔来，其实会在工作中怀疑自己：我真的需要这么多钱吗？因为大多数人是没有时间花掉这些钱的，至于白菜，sp，ssp 的分级，其实在扣完一大堆税到手后，并没有想象中差距那么大。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;✍️ 比较是偷走幸福的贼~&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以及脉脉等平台上的消息需要甄别下，很多老哥喜欢反串，很多消息保真，但更多消息是不保真的，工作是没有十全十美的，现在想要找一个晋升快，氛围好，不累，没杂活的组，几乎是不存在的。就是大家最终选的 offer 一定是或多或少有坑的，只是看最终能接受什么样的坑。此外针对于大部门的好坏评价基本上和自己无关，大部门可以理解为 +3  及以上这种，这种体量下，不可能有任何一个大部门永远保持无差评，但自己的入职后体验基本上只取决于你的 +1 和 mt 怎么样。&lt;/p&gt;
&lt;p&gt;一年前的时候面百度，当时刚从阿里出来，面试管说，别因为某些人/事把自己对技术的一份热爱磨灭了，那样生活就失去了很多乐趣，不好的地方可以尽早换，不管是在福报厂还是宇宙厂还是兄弟厂，&lt;strong&gt;人活的自在开心有价值是最重要的&lt;/strong&gt;，否则都是浮云了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;个人的命运，当然要靠自我奋斗，但也要考虑到历史的行程，但不幸的是，现在历史的进程并不乐观&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我引用一段话作为结尾吧：在比较小的时候，大约就是初中高中吧，我对自己人生结果充满幻想和期待。但慢慢长大之后，方才意识到生活是如此复杂，我不得不学着对内心的期望放手，曾经的梦想在残酷的现实面前变得褪色。此时，怜悯自己显得很重要，或许你可以对自己说“我也想要做得更好”以及“就让它这样吧，其实这样也好”。&lt;/p&gt;
&lt;p&gt;是的，我已经做得足够好了，因为至少现在我还爱着自己｡&lt;/p&gt;</content:encoded><h:img src="/_astro/202507072320845.CfUBludo.png"/><enclosure url="/_astro/202507072320845.CfUBludo.png"/></item><item><title>八股文 @ C++</title><link>https://coooredump.github.io/blog/recruitment/2025-cpp</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/2025-cpp</guid><description>记录面经高频 C++ 题，实时更新中...</description><pubDate>Fri, 22 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;✍️ C++ 11 新特性大全：https://zhuanlan.zhihu.com/p/139515439&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. GCC 编译流程｜编译与汇编的区别&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;参考链接：https://developer.aliyun.com/article/1650283&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一段高级语言代码经过四个阶段的处理形成可执行的目标二进制代码。&lt;/p&gt;
&lt;p&gt;预处理器→编译器→汇编器→链接器：最难理解的是&lt;strong&gt;编译与汇编的区别&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这里采用《深入理解计算机系统》的说法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;预处理阶段&lt;/strong&gt;：预处理阶段主要处理 &lt;code&gt;#include&lt;/code&gt; 指令、宏替换、条件编译等，生成 &lt;code&gt;.i&lt;/code&gt; 文件。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;展开头文件：将 &lt;code&gt;#include&lt;/code&gt; 指定的文件插入到源代码中&lt;/li&gt;
&lt;li&gt;宏替换：替换所有 &lt;code&gt;#define&lt;/code&gt; 定义的宏&lt;/li&gt;
&lt;li&gt;条件编译：根据预处理指令（如 &lt;code&gt;#ifdef&lt;/code&gt;）选择性地编译代码&lt;/li&gt;
&lt;li&gt;去除注释：删除源代码中的注释内容&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;写好的高级语言的程序文本比如 &lt;code&gt;hello.c&lt;/code&gt;，预处理器根据 &lt;code&gt;#&lt;/code&gt; 开头的命令，修改原始的程序，如
&lt;code&gt;#include&amp;#x3C;stdio.h&gt;&lt;/code&gt; 将把系统中的头文件插入到程序文本中，通常是以 &lt;code&gt;.i&lt;/code&gt; 结尾的文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -E source.c -o source.i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;编译阶段&lt;/strong&gt;：编译阶段对源代码进行语法语义检查，生成汇编代码，产生 &lt;code&gt;.s&lt;/code&gt; 文件。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;编译器将 &lt;code&gt;hello.i&lt;/code&gt; 文件翻译成汇编语言程序 &lt;code&gt;hello.s&lt;/code&gt;，不同的高级语言翻译的汇编语言相同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -S source.i -o source.s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;汇编阶段&lt;/strong&gt;：汇编阶段将汇编代码翻译成机器码（机器可识别的目标代码），生成 &lt;code&gt;.o&lt;/code&gt; 目标文件。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;汇编器将汇编代码 &lt;code&gt;hello.s&lt;/code&gt; 翻译成机器语言指令。&lt;strong&gt;把这些指令打包成可重定位目标程序&lt;/strong&gt;，即 &lt;code&gt;.o&lt;/code&gt; 文件。&lt;code&gt;hello.o&lt;/code&gt; 是一个二进制文件，&lt;strong&gt;它的字节码是机器语言指令&lt;/strong&gt;，不再是字符，前面两个阶段都还有字符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -c source.s -o source.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;链接阶段&lt;/strong&gt;： 链接阶段将多个目标文件和库文件链接在一起，生成最终的可执行文件，链接过程还可能会调用外部的动态或静态库。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;比如 &lt;code&gt;hello&lt;/code&gt; 程序调用 &lt;code&gt;printf&lt;/code&gt; 程序，它是每个 C 编译器都会提供的标准库 C 的函数。这个函数存在于一个名叫 &lt;code&gt;printf.o&lt;/code&gt; 的单独编译好的目标文件中，这个文件将以某种方式合并到 &lt;code&gt;hello.o&lt;/code&gt; 中。链接器就负责这种合并，得到的是可执行目标文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc source.o -o executable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关于编译优化&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GCC 和 G++ 提供了多种优化选项，开发者可以根据项目需求选择合适的优化级别&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;| 优化级别 | 描述                                       |
| -------- | ------------------------------------------ |
| &lt;code&gt;-O0&lt;/code&gt;    | 无优化（默认）                             |
| &lt;code&gt;-O1&lt;/code&gt;    | 基本优化                                   |
| &lt;code&gt;-O2&lt;/code&gt;    | 在不显著增加编译时间的前提下进行进一步优化 |
| &lt;code&gt;-O3&lt;/code&gt;    | 启用所有优化选项，可能导致代码体积增加     |
| &lt;code&gt;-Os&lt;/code&gt;    | 优化代码体积，适用于存储受限的设备         |&lt;/p&gt;
&lt;h2&gt;2. C 和 C++ 区别（函数/类/struct/class）&lt;/h2&gt;
&lt;p&gt;首先，C 和 C++ 在基本语句上没有过大的区别。&lt;/p&gt;
&lt;p&gt;C++ 有&lt;strong&gt;新增的语法和关键字&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;语法的区别有&lt;strong&gt;头文件的不同&lt;/strong&gt;和&lt;strong&gt;命名空间的不同&lt;/strong&gt;，C++ 允许我们自己定义自己的空间，C 中不可以。&lt;/li&gt;
&lt;li&gt;关键字方面比如 C++ 与 C 动态管理内存的方式不同，C++ 中在 &lt;code&gt;malloc&lt;/code&gt; 和 &lt;code&gt;free&lt;/code&gt; 的基础上增加了 &lt;code&gt;new&lt;/code&gt;和 &lt;code&gt;delete&lt;/code&gt;，而且 C++ 中在指针的基础上增加了引用的概念，关键字例如 C++中还增加了 &lt;code&gt;auto&lt;/code&gt;，&lt;code&gt;explicit&lt;/code&gt; 体现显示转换和隐式转换上的概念要求，还有 &lt;code&gt;dynamic_cast&lt;/code&gt; 增加类型安全方面的内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;函数方面 C++ 中有&lt;strong&gt;重载&lt;/strong&gt;和&lt;strong&gt;虚函数&lt;/strong&gt;的概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C++ 支持函数重载而 C 不支持&lt;/strong&gt;，是因为 C++ 函数的名字修饰与 C 不同，C++ 函数名字的修饰会将参数加在后面，例如，&lt;code&gt;int func(int, double)&lt;/code&gt; 经过名字修饰之后会变成 &lt;code&gt;_func_int_double&lt;/code&gt;，而 C 中则会变成&lt;code&gt;_func&lt;/code&gt;，所以 C++ 中会支持不同参数调用不同函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C++ 还有虚函数概念，用以实现多态&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类方面，C 的 &lt;code&gt;struct&lt;/code&gt; 和 C++ 的&lt;code&gt;类&lt;/code&gt;也有很大不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C++ 中的 struct 不仅可以有成员变量还可以成员函数，而且对于 struct 增加了权限访问的概念，&lt;strong&gt;struct 的默认成员访问权限和默认继承权限都是 &lt;code&gt;public&lt;/code&gt;&lt;/strong&gt;，C++ 中除了 &lt;code&gt;struct&lt;/code&gt; 还有 &lt;code&gt;class&lt;/code&gt; 表示类，struct 和 class 还有一点不同在于 &lt;strong&gt;class 的默认成员访问权限和默认继承权限都是 &lt;code&gt;private&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++ 中增加了&lt;strong&gt;模板&lt;/strong&gt;来重用代码，提供了更加强大的 &lt;strong&gt;STL 标准库&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;最后补充一点就是 &lt;strong&gt;C 是一种结构化的语言，重点在于算法和数据结构&lt;/strong&gt;。C 程序的设计首先考虑的是如何通过一个代码，一个过程对输入进行运算处理输出。&lt;strong&gt;而 C++ 首先考虑的是如何构造一个对象模型&lt;/strong&gt;，让这个模型能够契合与之对应的问题领域，这样就能通过获取对象的状态信息得到输出。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;C 的 struct 更适合看成是一个数据结构的实现体，而 C++ 的 class 更适合看成是一个对象的实现体。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3. C++ 和 JAVA 区别（语言特性、垃圾回收、应用场景等）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本人 C++ 和 Java 都有染指。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;指针&lt;/strong&gt;：Java 让程序员没法找到指针来直接访问内存，没有指针的概念，并且有自动内存管理功能，从而有效地防止了 C++ 语言中的指针操作失误的影响。但并非 Java 中没有指针，Java 虚拟机内部中还是使用了指针，保证了 Java 程序的安全性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多重继承&lt;/strong&gt;：C++ 支持多重继承但 Java 不支持，但支持一个类继承多个接口，实现 C++ 中多重继承的功能，又避免了 C++ 的多重继承带来的不便。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据类型和类&lt;/strong&gt;：Java 是完全面向对象的语言，所有的函数和变量必须是类的一部分。除了基本数据类型之外，其余的都作为类对象，对象将数据和方法结合起来，把他们封装在类中，这样每个对象都可以实现自己的特点和行为。Java 中取消了 C++ 中的 &lt;code&gt;struct&lt;/code&gt; 和 &lt;code&gt;union&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自动内存管理&lt;/strong&gt;：Java 程序中所有对象都是用 &lt;code&gt;new&lt;/code&gt; 操作符建立在内存堆栈上，Java 自动进行无用内存回收操作，不需要程序员进行手动删除。而 C++ 中必须由程序员释放内存资源，增加了程序设计者的负担。Java 中当一个对象不再被使用时，无用内存回收器将给他们加上标签，Java 里无用内存回收程序是以线程方式在后台运行，利用空闲时间工作来删除。&lt;/p&gt;
&lt;p&gt;Java 不支持操作符重载，操作符重载是 C++ 的突出特性。&lt;/p&gt;
&lt;p&gt;Java 不支持预处理功能，C++ 在编译过程中都有一个预编译阶段，Java 没有预处理器，但它提供了 &lt;code&gt;import&lt;/code&gt;，与 C++ 预处理器具有类似功能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类型转换&lt;/strong&gt;：C++ 中有数据类型隐含转换的机制，Java 中需要显式强制类型转换。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;字符串&lt;/strong&gt;：C++ 中字符串是以 NULL 终止符代表字符串的结束，而 Java 的字符串是用类对象（&lt;code&gt;string&lt;/code&gt; 和 &lt;code&gt;stringBuffer&lt;/code&gt;）来实现的。&lt;/p&gt;
&lt;p&gt;Java 中不提供 goto 语句，虽然指定 goto 为关键字，但不支持使用它。&lt;/p&gt;
&lt;p&gt;Java 的异常机制用于捕获例外事件，增强系统容错能力。&lt;/p&gt;
&lt;h2&gt;4. C++ 中 const 和 static 关键字的作用&lt;/h2&gt;
&lt;h3&gt;static&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;static&lt;/code&gt; 作用：控制变量的存储方式和&lt;strong&gt;可见性&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;1️⃣ 局部静态变量&lt;/h4&gt;
&lt;p&gt;一般情况下，对于&lt;strong&gt;局部变量&lt;/strong&gt;在程序中是存放在栈区的，并且局部的生命周期在包含语句块执行结束时便也结束了。但是如果用 static 关键字修饰，该变量会存放在&lt;strong&gt;静态数据区&lt;/strong&gt;，作用域仍为局部作用域，但是当局部静态变量离开作用域后，并没有销毁，而是仍然驻留在内存当中，只不过我们不能再对它进行访问，直到该函数再次被调用，并且值不变。&lt;/p&gt;
&lt;h4&gt;2️⃣ 全局静态变量&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;即 static 限制了全局变量的作用域（本文件）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于一个全局变量，它既可以在本文件中被访问到，也可以在同一个工程其它源文件被访问（添加 &lt;code&gt;extern&lt;/code&gt; 进行声明即可）；而使用 &lt;code&gt;static&lt;/code&gt; 对全局变量进行修饰改变了其作用域范围，&lt;strong&gt;由原来整个工程可见变成了本文件可见&lt;/strong&gt;，同时也是存放在静态数据区，在整个程序运行期间一直存在。&lt;/p&gt;
&lt;h4&gt;3️⃣ 静态函数&lt;/h4&gt;
&lt;p&gt;函数的定义和声明在&lt;strong&gt;默认&lt;/strong&gt;情况下都是 &lt;code&gt;extern&lt;/code&gt; 的，但静态函数只是在声明它的文件当中可见（与全局静态变量类似）&lt;/p&gt;
&lt;h4&gt;4️⃣ 类的静态成员/函数&lt;/h4&gt;
&lt;p&gt;在类中，静态成员可以实现多个对象之间的数据共享，并且使用静态数据成员还不会破坏隐藏的原则，即保证了安全性。因此静态成员/函数是类中所有对象共享的成员/函数，而不是某个对象的成员/函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在模块内的 static 全局变量可以被模块内所有函数访问，&lt;strong&gt;但不能被模块外其它函数访问&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;在类中的 static 成员变量属于整个类所拥有，对类的所有对象只有一份拷⻉；&lt;/li&gt;
&lt;li&gt;在类中的 static 成员函数属于整个类所拥有，&lt;strong&gt;这个函数不接收 this 指针&lt;/strong&gt;，因而只能访问类的 static 成员变量；&lt;/li&gt;
&lt;li&gt;static 类对象必须要在类外进行初始化，&lt;strong&gt;static 修饰的变量先于对象存在&lt;/strong&gt;，所以 static 修饰的变量要在类外初始化；&lt;/li&gt;
&lt;li&gt;由于 static 修饰的类成员属于类，不属于对象，因此 static 类成员函数是没有 &lt;code&gt;this&lt;/code&gt; 指针，this 指针是指向本对象的指针，正因为没有 this 指针，所以 static 类成员函数不能访问非 static 的类成员，只能访问 static 修饰的类成员；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;static 成员函数不能被 &lt;code&gt;virtual&lt;/code&gt; 修饰&lt;/strong&gt;，static 成员不属于任何对象或实例，所以加上 virtual 没有任何实际意义；静态成员函数没有 this 指针，虚函数的实现是为每一个对象分配一个 vptr 指针，而 vptr 是通过 this 指针调用的，所以不能为 virtual；虚函数的调用关系，this-&gt;vptr-&gt;ctable-&gt;virtual function。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;const&lt;/h3&gt;
&lt;h4&gt;1️⃣ const 修饰基本数据类型&lt;/h4&gt;
&lt;p&gt;修饰符 const 可以用在类型说明符前，也可以在类型说明符后，结果都是一样的，使用这些常量时，只要不改变这些常量的值即可。&lt;/p&gt;
&lt;h4&gt;2️⃣ const 修饰指针变量和引用变量&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;引用同理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果 const 位于 &lt;code&gt;const T*&lt;/code&gt; 左侧，则 const 就是用来修饰指针所指向的变量，即指针指向为常量。&lt;/p&gt;
&lt;p&gt;如果 const 位于 &lt;code&gt;T* const&lt;/code&gt; 右侧，则 const 就是修饰指针本身，即指针本身是常量。&lt;/p&gt;
&lt;h4&gt;3️⃣ const 应用到函数中&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;作为参数的 const 修饰符：调用函数时，用相应的变量初始化 const 常量，则在函数体中，按照 const 所修饰的部分进行常量化，保护了原对象的属性。&lt;/li&gt;
&lt;li&gt;作为函数返回值的 const 修饰符：声明了返回值后，它意味着这个返回值是一个常量，不能被修改。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意：参数 const 通常用于参数为指针或引用的情况。&lt;/p&gt;
&lt;h4&gt;4️⃣ const 在类中的用法&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;const 成员变量：只在某个对象生命周期内是常量，而对于整个类而言是可以改变的（因为类可以创建多个对象，不同对象其 const 数据成员值可以不同，&lt;strong&gt;所以不能在类的声明中初始化 const 数据成员&lt;/strong&gt;，因为类对象在没有创建的时候，编译器不知道 const 数据成员的值是什么，&lt;strong&gt;const 数据成员的初始化只能在类的构造函数初始化列表中进行&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;const 成员函数：&lt;strong&gt;防止成员函数修改对象的内容&lt;/strong&gt;，要注意，&lt;strong&gt;const 和 static 对于成员函数来说是不能同时使用的&lt;/strong&gt;，因为 static 关键字修饰静态成员函数不含有 this 指针，即不能实例化，const 成员函数又必须具体到某一个函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;补充：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;const 成员函数如果实在想修改某个变量，可以使用 &lt;code&gt;mutable&lt;/code&gt; 进行修饰；&lt;/li&gt;
&lt;li&gt;成员变量中如果想建立在整个类中都恒定的常量，应该用类中的&lt;code&gt;枚举常量&lt;/code&gt;来实现或者 &lt;code&gt;static const&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5️⃣ const 修饰类对象、定义常量函数&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;const 常量对象只能调用 const 常量函数&lt;/strong&gt;，非 const 成员函数都不能调用。&lt;/p&gt;
&lt;p&gt;原因：对象调用成员函数时，在形参列表的最前面加一个形参 this，但这是隐式的。this 指针是默认指向调用函数的当前对象的，所以很自然，this 是一个常量指针 &lt;code&gt;test * const&lt;/code&gt;，因为不可以修改 this 指针代表的地址。但当成员函数的参数列表后加了 const 关键字（&lt;code&gt;void print() const;&lt;/code&gt;），此成员函数为常量成员函数，此时它的隐式 this 形参为 &lt;code&gt;const test * const&lt;/code&gt;，&lt;strong&gt;表示指向常量对象的常量指针&lt;/strong&gt;，即不可以通过 this 指针来改变指向对象的值。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;非常量对象可以调用类中的 const 成员函数，也可以调用非 const 成员函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

class Test {
public:
    Test(int val) : value(val) {}

    // 常量成员函数
    void print() const {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Value: &quot; &amp;#x3C;&amp;#x3C; value &amp;#x3C;&amp;#x3C; std::endl;
    }

    // 非常量成员函数
    void setValue(int val) {
        value = val;
    }

private:
    int value;
};

int main() {
    const Test obj(10);
    obj.print(); // OK，调用常量成员函数
    // obj.setValue(20); // 错误，`const`对象不能调用非常量成员函数
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ 注意区别 &lt;code&gt;int print() const;&lt;/code&gt; 和 &lt;code&gt;const int print();&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前者为常量成员函数，&lt;code&gt;const&lt;/code&gt; 位于函数声明的末尾，只能由 const 常量对象来调用该 const 常量函数。&lt;/li&gt;
&lt;li&gt;后者为普通成员函数，但是返回值为 &lt;code&gt;const int&lt;/code&gt;（注意不能用 const 修饰 void，即 &lt;code&gt;const void print()&lt;/code&gt; 会编译错误，所以这里用了 &lt;code&gt;int&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 说一说 C++ 中四种 cast 转换&lt;/h2&gt;
&lt;p&gt;C++ 中四种类型转换是：&lt;code&gt;static_cast&lt;/code&gt;、&lt;code&gt;dynamic_cast&lt;/code&gt;、&lt;code&gt;const_cast&lt;/code&gt;、&lt;code&gt;reinterpret_cast&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;const_cast&lt;/h3&gt;
&lt;p&gt;用于将 const 变量转为非 const 变量：常量指针转换为⾮常量指针，并且仍然指向原来的对象；常量引⽤被转换为⾮常量引⽤，并且仍然指向原来的对象。&lt;code&gt;const_cast&lt;/code&gt; 去掉类型的 &lt;code&gt;const&lt;/code&gt; 或 &lt;code&gt;volatile&lt;/code&gt; 属性。&lt;/p&gt;
&lt;h3&gt;static_cast&lt;/h3&gt;
&lt;p&gt;用于各种隐式转换，但是没有运行时类型检查来保证转换的安全性。&lt;/p&gt;
&lt;p&gt;比如非 const 转 const，void* 转指针等，static_cast 还可以用于&lt;strong&gt;多态向上转换&lt;/strong&gt;（如 Derived 转 Base，即子类转基类）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进行向上转换（把派生类指针或引用转换为基类）是安全的&lt;/li&gt;
&lt;li&gt;进行向下转换（把基类指针或引用转换为派生类），由于没有运行时类型检查，所以是不安全的&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Base 是 Derived 的基类/父类
int main() {
    Derived* d;
    Base* base = static_cast&amp;#x3C;Base*&gt;(d);	// 向上类型转换
    base-&gt;show();
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;dynamic_cast&lt;/h3&gt;
&lt;p&gt;在进行向下转换时，&lt;code&gt;dynamic_cast&lt;/code&gt; 具有类型检查（信息在虚函数中）的功能，比 &lt;code&gt;static_cast&lt;/code&gt; 更安全。&lt;/p&gt;
&lt;p&gt;只能用于含有虚函数的类，用于类层次间的向上和向下转换（基类转子类），只能转指针或引用，向下转换时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于指针，转换失败则返回 &lt;code&gt;nullptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对于引用，转换失败则抛异常&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() {
    Base* base = new Derived; // 不使用 static_cast 也可以隐式向上转换
    Derived* derive = dynamic_cast&amp;#x3C;Derived*&gt;(base);	// 向下类型转换，使用 dynamic_cast
    if (derive) {
        derive-&gt;show();
    } else {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Conversion failed!&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }

    delete base;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;reinterpret_cast&lt;/h3&gt;
&lt;p&gt;几乎什么都可以转，比如将 int 转指针，可能会出问题，尽量少用。&lt;/p&gt;
&lt;p&gt;WARNING：reinterpret_cast 本质上依赖于机器，要想安全地使用 reinterpret_cast 必须对涉及的类型和编译器实现转换的过程都非常了解。&lt;/p&gt;
&lt;h3&gt;为什么不使用 C 的强制转换？&lt;/h3&gt;
&lt;p&gt;C 的强制转换表面上看起来功能强大什么都能转，但是转化不够明确，不能进行错误检查，容易出错。&lt;/p&gt;
&lt;h3&gt;static_cast 与 dynamic_cast 之间的区别？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;dynamic_cast&lt;/code&gt; 和 &lt;code&gt;static_cast&lt;/code&gt; 的主要区别在于类型检查的时间点和安全性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类型检查时间点&lt;/strong&gt;：&lt;code&gt;static_cast&lt;/code&gt;在编译时进行类型检查，而&lt;code&gt;dynamic_cast&lt;/code&gt;在运行时进行类型检查。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;/strong&gt;：&lt;code&gt;static_cast&lt;/code&gt;不执行运行时类型检查，因此如果在类层次结构中进行不安全的向下转换，可能导致未定义行为。相反，&lt;code&gt;dynamic_cast&lt;/code&gt; 会在运行时检查转换的安全性，如果转换不安全，则返回&lt;code&gt;nullptr&lt;/code&gt;或抛出异常，提供更高的安全性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. C/C++ 的四大内存分区和常量的存储位置&lt;/h2&gt;
&lt;p&gt;四大内存分区：栈、堆、静态存储区（全局变量 + 静态变量 + 常量）和代码区。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504070536367.png&quot; alt=&quot;image-20250407053610463&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1️⃣ 栈区&lt;/h3&gt;
&lt;p&gt;由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行，系统自动释放栈区内存，不需要用户管理，整个程序的栈区大小可以在编译器中由用户自行设定，VS 中默认的栈区大小为 1M，可以通过 VS 手动更改栈的大小。64 bits 的 Linux 默认栈大小为 10MB，可通过 &lt;code&gt;ulimit -s&lt;/code&gt; 临时修改，可通过 &lt;code&gt;ulimit -a&lt;/code&gt; 查看。&lt;/p&gt;
&lt;h3&gt;2️⃣ 堆区&lt;/h3&gt;
&lt;p&gt;由程序员手动申请，手动释放，若不手动释放，程序结束后由系统回收，生命周期是整个程序运行期间。使用 &lt;code&gt;malloc&lt;/code&gt; 或者 &lt;code&gt;new&lt;/code&gt; 进行堆的申请，&lt;strong&gt;堆的总大小为机器的虚拟内存的大小&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;说明：&lt;code&gt;new&lt;/code&gt; 操作符本质上是使用了 &lt;code&gt;malloc&lt;/code&gt; 进行内存的申请，&lt;code&gt;new&lt;/code&gt; 和 &lt;code&gt;malloc&lt;/code&gt; 的区别如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;malloc&lt;/code&gt; 是 C 语言中的函数，而 &lt;code&gt;new&lt;/code&gt; 是 C++ 中的操作符。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;malloc&lt;/code&gt; 申请之后返回的类型是 &lt;code&gt;void*&lt;/code&gt;，而 &lt;code&gt;new&lt;/code&gt; 返回的指针带有类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;malloc&lt;/code&gt; 只负责内存的分配而不会调用类的构造函数，而 &lt;code&gt;new&lt;/code&gt; 不仅会分配内存，而且会自动调用类的构造函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;堆和栈的区别&lt;/h4&gt;
&lt;p&gt;申请方式不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈是系统自动分配&lt;/li&gt;
&lt;li&gt;堆是自己申请和释放的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;申请大小限制不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈空间默认 10 MB；栈顶和栈底是之前预设好的，栈是向栈底扩展，大小固定，可以通过 &lt;code&gt;ulimit -a&lt;/code&gt; 查看，由 &lt;code&gt;ulimit -s&lt;/code&gt; 修改&lt;/li&gt;
&lt;li&gt;堆区一般是 1G~4G；堆向高地址扩展，是不连续的内存区域，大小可以灵活调整&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;申请效率不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈由系统分配，速度快，不会有碎片&lt;/li&gt;
&lt;li&gt;堆由程序员分配，速度慢，且会有碎片&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;栈快还是堆快？&lt;/h4&gt;
&lt;p&gt;毫无疑问是栈快一点。&lt;/p&gt;
&lt;p&gt;因为操作系统会在底层对栈提供支持，会分配专门的寄存器存放栈的地址，栈的入栈出栈操作也十分简单，并且有专门的指令执行，所以栈的效率比较高也比较快。&lt;/p&gt;
&lt;p&gt;而堆的操作是由 C/C++ 函数库提供的，在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问，第一次访问指针，第二次根据指针保存的地址访问内存，因此堆比较慢。&lt;/p&gt;
&lt;h3&gt;3️⃣ 静态存储区&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;静态存储区 = 全局数据区 + 常量区&lt;/p&gt;
&lt;p&gt;全局数据区：全局变量 + 静态变量，该区域会被自动初始化&lt;/p&gt;
&lt;p&gt;常量区：存放常量，不允许修改&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;静态存储区内的变量在程序编译阶段已经分配好内存空间并初始化。这块内存在程序的整个运行期间都存在，它主要存放 &lt;strong&gt;static 静态变量&lt;/strong&gt;、&lt;strong&gt;全局变量&lt;/strong&gt;和 &lt;strong&gt;const 常量&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;区分：&lt;code&gt;static&lt;/code&gt; 修饰「局部变量」在静态存储区中；&lt;code&gt;const&lt;/code&gt; 修饰「局部变量」则是在栈区中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这里不区分初始化和未初始化的数据区，是因为静态存储区内的变量若不显示初始化，&lt;strong&gt;则编译器会自动以默认的方式进行初始化，即静态存储区内不存在未初始化的变量&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;静态存储区内的常量分为&lt;strong&gt;常变量&lt;/strong&gt;和&lt;strong&gt;字符串常量&lt;/strong&gt;，一经初始化，不可修改。静态存储内的常变量是全局变量，与局部常变量不同，&lt;strong&gt;区别在于局部常变量存放于栈&lt;/strong&gt;，实际可间接通过指针或者引用进行修改，而全局常变量存放于静态常量区则不可以间接修改。&lt;/li&gt;
&lt;li&gt;字符串常量存储在静态存储区的常量区，字符串常量的名称即为它本身，属于常变量。&lt;/li&gt;
&lt;li&gt;数据区的具体划分，有利于我们对于变量类型的理解。不同类型的变量存放的区域不同。后面将以实例代码说明这四种数据区中具体对应的变量。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4️⃣ 代码区&lt;/h3&gt;
&lt;p&gt;存放程序体的二进制代码，比如我们写的函数都是在代码区。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int a = 0;//静态全局变量区
char *p1; //编译器默认初始化为NULL
void main()
{
    int b; //栈
    char s[] = &quot;abc&quot;;//栈
    char *p2 = &quot;123456&quot;;//123456在字符串常量区，p2在栈上
    static int c =0; //c在静态变量区，0为文字常量，在代码区
    const int d=0; //栈
    static const int d;//静态常量区
    p1 = (char *)malloc(10);//分配得来得10字节在堆区。
    strcpy(p1, &quot;123456&quot;); //123456放在字符串常量区，编译器可能会将它与p2所指向的&quot;123456&quot;优化成一个地方
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7a. C++ 中 class 的大小由哪些因素决定？&lt;/h2&gt;
&lt;p&gt;在 C++ 中，类的大小由多个因素决定，主要包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;普通成员变量&lt;/strong&gt;：类中定义的非静态成员变量会直接影响类的大小。每个成员变量都会占用相应的内存空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚函数&lt;/strong&gt;：如果类包含虚函数，编译器会为该类添加一个虚函数表（vtable），并在每个对象中添加一个指向该表的指针（vptr），这会增加每个对象的大小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;继承&lt;/strong&gt;：类的继承关系也会影响其大小。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单一继承&lt;/strong&gt;：派生类会继承基类的成员变量和成员函数，但不会直接增加对象的大小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多重继承&lt;/strong&gt;：派生类继承多个基类时，可能会导致对象中包含多个基类的子对象，从而增加对象的大小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟继承&lt;/strong&gt;：为了解决菱形继承问题，编译器可能会在派生类中引入虚拟基类指针，增加对象的大小。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存对齐&lt;/strong&gt;：&lt;strong&gt;编译器通常会对类的成员变量进行内存对齐&lt;/strong&gt;，以提高访问效率。这可能导致类的实际大小大于成员变量总和。
&lt;ul&gt;
&lt;li&gt;分配内存的顺序是按照声明的顺序。&lt;/li&gt;
&lt;li&gt;每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍，不是整数倍空出内存，直到偏移量是整数倍为止。&lt;/li&gt;
&lt;li&gt;最后整个结构体的大小必须是里面变量类型最大值的整数倍。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;⚠️ 需要注意的是，类的构造函数、析构函数、静态成员变量、静态成员函数和普通成员函数不会直接影响类的大小。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构造函数和析构函数：
&lt;ul&gt;
&lt;li&gt;构造函数和析构函数是特殊的成员函数，用于对象的初始化和销毁。&lt;/li&gt;
&lt;li&gt;它们的存在不会增加类的实例大小，因为它们在对象创建和销毁时被调用，但并不占用对象的内存空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态成员变量：
&lt;ul&gt;
&lt;li&gt;静态成员变量属于类本身，而不是类的实例。&lt;/li&gt;
&lt;li&gt;它们在类的所有实例之间共享，只有一份存储空间（&lt;strong&gt;静态存储区&lt;/strong&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态成员函数：
&lt;ul&gt;
&lt;li&gt;静态成员函数也属于类本身，而不是类的实例。&lt;/li&gt;
&lt;li&gt;它们在类的所有实例之间共享，只有一份存储空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;普通成员函数：
&lt;ul&gt;
&lt;li&gt;普通成员函数是类的成员，&lt;strong&gt;但普通成员函数的代码通常存储在程序的代码段中&lt;/strong&gt;，而不是对象的内存中。&lt;/li&gt;
&lt;li&gt;因此，普通成员函数不会影响类的实例大小。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7b. [7a 类似问题] C++ 的对象存储空间是怎么安排的？&lt;/h2&gt;
&lt;p&gt;C++ 中对象的存储取决于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象的类型（普通对象、继承对象、虚函数表等）&lt;/li&gt;
&lt;li&gt;存储方式（栈、堆、静态存储区）&lt;/li&gt;
&lt;li&gt;对齐方式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体来说：&lt;/p&gt;
&lt;p&gt;1️⃣ 普通对象&lt;/p&gt;
&lt;p&gt;（1）&lt;strong&gt;非静态成员变量&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通对象的非静态成员变量按照 &lt;strong&gt;声明顺序&lt;/strong&gt; 在内存中存储。&lt;/li&gt;
&lt;li&gt;编译器会根据 CPU 架构和优化需求进行 &lt;strong&gt;内存对齐&lt;/strong&gt;（alignment），可能会插入填充字节（padding）。&lt;/li&gt;
&lt;li&gt;类的大小通常是 &lt;strong&gt;最大成员类型的对齐倍数&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

struct A {
    char c;   // 1 字节
    int i;    // 4 字节
};

int main() {
    std::cout &amp;#x3C;&amp;#x3C; sizeof(A) &amp;#x3C;&amp;#x3C; std::endl;  // 输出可能是 8（对齐）
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;内存布局（假设 4 字节对齐）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-less&quot;&gt;| c (1B) | padding (3B) | i (4B) |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（2）&lt;strong&gt;静态成员变量&lt;/strong&gt;：不属于对象本身，放在静态存储区，在程序启动时分配。&lt;/p&gt;
&lt;p&gt;2️⃣ 继承&lt;/p&gt;
&lt;p&gt;（1）非虚继承：没有 &lt;code&gt;virtual&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;派生类对象包括基类的成员变量&lt;/strong&gt;，存储顺序是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;基类子成员&lt;/li&gt;
&lt;li&gt;派生类新增成员&lt;/li&gt;
&lt;li&gt;对齐填充&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Base {
    int a;
};

struct Derived : public Base {
    char b;
};

int main() {
    std::cout &amp;#x3C;&amp;#x3C; sizeof(Derived) &amp;#x3C;&amp;#x3C; std::endl;  // 可能是 8（对齐）
}

内存分布：| Base::a (4B) | Derived::b (1B) | padding (3B) |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（2）虚继承：基类含有 &lt;code&gt;virtual&lt;/code&gt; 方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**&lt;a href=&quot;https://zhuanlan.zhihu.com/p/342271992&quot;&gt;虚基类&lt;/a&gt;**存储方式不同，编译器会创建虚基类指针 &lt;code&gt;vptr&lt;/code&gt; 以及虚基类表 &lt;code&gt;vtable&lt;/code&gt; 来管理它&lt;/li&gt;
&lt;li&gt;可能会多一个指向虚基类表的&lt;strong&gt;指针&lt;/strong&gt;，因此对象的大小会变大&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Base {
    int a;
    virtual void func() {}  // 引入虚表
};

struct Derived : public Base {
    char b;
};

int main() {
    std::cout &amp;#x3C;&amp;#x3C; sizeof(Derived) &amp;#x3C;&amp;#x3C; std::endl;  // 可能是 16（虚表指针 + 对齐）
}

// 假设指针 8 字节
| vptr (8B) | Base::a (4B) | padding (3B) | Derived::b (1B) |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3️⃣ 多重继承&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;非虚多重继承&lt;/strong&gt;：派生类按继承顺序依次存储多个基类的成员变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚多重继承&lt;/strong&gt;：对象中会存储多个虚表指针，可能引入 &lt;strong&gt;虚基类偏移表&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct A {
    int a;
};

struct B {
    double b;
};

struct C : public A, public B {
    char c;
};

int main() {
    std::cout &amp;#x3C;&amp;#x3C; sizeof(C) &amp;#x3C;&amp;#x3C; std::endl;  // 可能是 24（对齐 + 多继承）
}

| A::a (4B) | padding (4B) | B::b (8B) | C::c (1B) | padding (7B) |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4️⃣ 对象存储方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈上对象：&lt;strong&gt;普通局部对象&lt;/strong&gt;，生命周期受到作用域控制&lt;/li&gt;
&lt;li&gt;堆上对象：使用 &lt;code&gt;new&lt;/code&gt; 关键词分配的对象存储在堆区，需要手动 &lt;code&gt;delete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;静态存储区：&lt;code&gt;static&lt;/code&gt; 变量存储在静态存储区&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;5️⃣ 虚函数和 vtable 虚表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果类中有 &lt;strong&gt;虚函数&lt;/strong&gt;，编译器会为该类生成 &lt;strong&gt;虚表（vtable）&lt;/strong&gt;，并在对象中存储 &lt;strong&gt;虚指针（vptr）&lt;/strong&gt;，指向该虚表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚表存储在静态区&lt;/strong&gt;，而 &lt;strong&gt;vptr 存储在对象头部&lt;/strong&gt;（通常是对象的第一个成员）。&lt;/li&gt;
&lt;li&gt;vptr 使得多态调用能够动态绑定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⚠️ 虚指针存储在对象头部；&lt;strong&gt;虚表存储在静态存储区&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;8. new/delete 和 malloc/free 有什么区别和联系？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;更多内容（讲得很好）：&lt;a href=&quot;https://jacktang816.github.io/post/cppnewdelete/&quot;&gt;C++ 种内存管理之 new/delete&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;联系&lt;/strong&gt;：都可以用来在堆上分配和回收空间，&lt;code&gt;new&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt; 是操作符，&lt;code&gt;malloc&lt;/code&gt;/&lt;code&gt;free&lt;/code&gt; 是库函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;执行 new 实际上执行两个过程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用 &lt;code&gt;malloc&lt;/code&gt; 分配未初始化的内存空间&lt;/li&gt;
&lt;li&gt;使用对象的构造函数对空间进行初始化，并返回空间的首地址&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;执行 delete 实际上也有两个过程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用析构函数对对象进行析构&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;free&lt;/code&gt; 释放指针所指向空间的内存&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;二者区别&lt;/strong&gt;：&lt;code&gt;new&lt;/code&gt; 得到的是经过初始化的空间，而 &lt;code&gt;malloc&lt;/code&gt; 得到的是未初始化的空间，所以 new 是 new 一个类型，而 malloc 则是 malloc 一个字节长度的空间。&lt;code&gt;delete&lt;/code&gt; 和 &lt;code&gt;free&lt;/code&gt; 同理，delete 不仅释放空间还析构对象，delete 一个类型，free 一个字节长度的空间。&lt;/p&gt;
&lt;h3&gt;对象的自动删除&lt;/h3&gt;
&lt;p&gt;通过之前的分析我们知道，&lt;code&gt;new&lt;/code&gt;关键字创建对象并非一步完成，而是通过先分配未初始化内存和调用构造函数初始化两步实现的。那么在这个过程中如果是第一步出错，那么内存分配失败不会调用构造函数，这是没有问题的。但是如果第一步已经完成在堆中已经成功分配了内存之后，在第二步调用构造函数时异常导致创建对象失败（抛出 &lt;code&gt;std::bad_alloc&lt;/code&gt;），那么就应该将第一步中申请的内存释放。C++中规定，如果一个对象无法完全构造，那么这个对象就是一个无效对象，也不会调用析构函数。因此为了保证对象的完整性，当通过 new 分配的堆内存对象在构造函数执行过程中出现异常时，就会停止构造函数的执行并且自动调用对应的 &lt;code&gt;delete&lt;/code&gt; 运算符来对已经分配好的对内存执行销毁处理，即对象的自动删除技术。&lt;/p&gt;
&lt;h3&gt;🔥 为什么有了 malloc/free 还需要 new/delete&lt;/h3&gt;
&lt;p&gt;因为对于非内部数据类型而言，光用 malloc/free 无法满足动态对象的要求。对象在创建的同时需要自动执行构造函数，对象在消亡以前要自动执行析构函数。由于 malloc/free 是库函数而不是操作符，不在编译器控制权限之内，不能够把执行的构造函数和析构函数的任务强加于 malloc/free，所以在 C++ 中需要一个能完成动态内存分配和初始化工作的运算符 new，以及一个能完成清理和释放内存工作的运算符 delete。而且在对非基本数据类型的对象使用的时候，对象创建的时候还需要执行构造函数，销毁的时候要执行析构函数。而 malloc/free 是库函数，&lt;strong&gt;是已经编译的代码&lt;/strong&gt;，所以不能把构造函数和析构函数的功能强加给 malloc/free，所以 new/delete 是必不可少的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;既然 new/delete 的功能完全覆盖了 malloc/free，为什么 C++ 不把 malloc/free 淘汰出局呢&lt;/strong&gt;？这是因为 C++ 程序经常要调用 C 函数，而 C 程序只能用 malloc/free 管理动态内存。&lt;/p&gt;
&lt;h3&gt;🔥 malloc 与 free 的实现原理（&lt;code&gt;brk()&lt;/code&gt;、&lt;code&gt;mmap()&lt;/code&gt;）&lt;/h3&gt;
&lt;p&gt;1、在标准 C 库中，提供了 &lt;code&gt;malloc/free&lt;/code&gt; 函数分配释放内存，这两个函数底层是由 &lt;code&gt;brk&lt;/code&gt;、&lt;code&gt;mmap&lt;/code&gt;、&lt;code&gt;munmap&lt;/code&gt; 这些系统调用实现的;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;brk&lt;/code&gt; 是将「堆顶」指针向高地址移动，获得新的内存空间；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mmap&lt;/code&gt; 是在进程的虚拟地址空间中（堆和栈中间，称为文件映射区域的地方）找一块空闲的虚拟内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;这两种方式分配的都是虚拟内存，没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候，发生缺页中断，操作系统负责分配物理内存，然后建立虚拟内存和物理内存之间的映射关系&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;2、malloc 分配阈值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;malloc 小于 128k 的内存，使用 &lt;code&gt;brk&lt;/code&gt; 分配内存，将「堆顶」指针往高地址推；&lt;/li&gt;
&lt;li&gt;malloc 大于 128k 的内存，使用 &lt;code&gt;mmap&lt;/code&gt; 分配内存，在堆和栈之间找一块空闲内存分配；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;brk 分配的内存需要等到高地址内存释放以后才能释放，而 mmap 分配的内存可以单独释放&lt;/strong&gt;。当最高地址空间的空闲内存超过 128K（可由 &lt;code&gt;M_TRIM_THRESHOLD&lt;/code&gt; 选项调节）时，执行内存紧缩操作（&lt;code&gt;trim&lt;/code&gt;）。在上一个步骤 free 的时候，发现最高地址空闲内存超过 128K，于是内存紧缩。&lt;/p&gt;
&lt;p&gt;3、空闲地址链表：malloc 是从堆里面申请内存，也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时，就会遍历该链表，然后就寻找第一个空间大于所申请空间的堆结点，然后就将该结点从空闲结点链表中删除，并将该结点的空间分配给程序。&lt;/p&gt;
&lt;h3&gt;🔥 被 free 回收的内存是立即返回给操作系统吗？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;更详细的内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/0voice/kernel_memory_management/blob/main/%E2%9C%8D%20%E6%96%87%E7%AB%A0/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%20glibc%20malloc%EF%BC%9A%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E5%99%A8%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86.md#4-chunk&quot;&gt;深入理解 glibc malloc: 内存分配器实现原理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://xiaolincoding.com/os/3_memory/malloc.html#free-%E9%87%8A%E6%94%BE%E5%86%85%E5%AD%98-%E4%BC%9A%E5%BD%92%E8%BF%98%E7%BB%99%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%90%97&quot;&gt;xiaolincoding&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;不一定。被 &lt;code&gt;free&lt;/code&gt; 的内存&lt;strong&gt;不一定会立刻返回给操作系统&lt;/strong&gt;，具体行为取决于操作系统的内存管理机制以及 C 语言运行时库（如 glibc）的实现方式。&lt;/p&gt;
&lt;p&gt;对于 「malloc 申请的内存，free 释放内存会归还给操作系统吗？」这个问题，我们可以做个总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;malloc 通过 &lt;code&gt;brk()&lt;/code&gt; 方式申请的内存，free 释放内存的时候，并不会把内存归还给操作系统，而是缓存在 malloc 的内存池中，待下次使用；&lt;/li&gt;
&lt;li&gt;malloc 通过 &lt;code&gt;mmap()&lt;/code&gt; 方式申请的内存，free 释放内存的时候，会把内存归还给操作系统，内存得到真正的释放。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;什么场景下 malloc() 会通过 brk() 分配内存？又是什么场景下通过 mmap() 分配内存？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;malloc() 源码里默认定义了一个阈值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果用户分配的内存小于 128 KB，则通过 brk() 申请内存；&lt;/li&gt;
&lt;li&gt;如果用户分配的内存大于 128 KB，则通过 mmap() 申请内存；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意，不同的 glibc 版本定义的阈值也是不同的。&lt;/p&gt;
&lt;h4&gt;1. 内存释放流程&lt;/h4&gt;
&lt;p&gt;当你在 C/C++ 中使用 &lt;code&gt;free(ptr)&lt;/code&gt; 释放一块内存时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内存被标记为“空闲”&lt;/strong&gt;，表示这块内存可以被后续的 &lt;code&gt;malloc&lt;/code&gt; 或 &lt;code&gt;calloc&lt;/code&gt; 重用。&lt;/li&gt;
&lt;li&gt;但它&lt;strong&gt;通常不会立即归还给操作系统&lt;/strong&gt;，而是由内存分配器（如 glibc 的 &lt;code&gt;ptmalloc&lt;/code&gt;）保留在用户进程中，用于后续分配。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 什么时候会真正返回给操作系统？&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果释放的是&lt;strong&gt;堆顶的内存块&lt;/strong&gt;（即堆的末端），且满足一定条件，glibc 可能会调用 &lt;code&gt;brk&lt;/code&gt; 或 &lt;code&gt;mmap&lt;/code&gt; 对应的释放机制（如 &lt;code&gt;munmap&lt;/code&gt;）来将这部分内存返回给操作系统。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;mmap&lt;/code&gt; 分配的大块内存（通常大于一定阈值，比如 128KB），在被 &lt;code&gt;free&lt;/code&gt; 时通常会直接使用 &lt;code&gt;munmap&lt;/code&gt; 归还给操作系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. glibc 的行为（以 Linux 为例）&lt;/h4&gt;
&lt;p&gt;glibc 的 &lt;code&gt;malloc&lt;/code&gt; 有一套复杂的内存池机制，常见策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小块内存来自内部的 &lt;strong&gt;arena&lt;/strong&gt;，&lt;code&gt;free&lt;/code&gt; 后不会归还操作系统，而是缓存起来以便重用。&lt;/li&gt;
&lt;li&gt;大块内存通过 &lt;code&gt;mmap&lt;/code&gt; 分配，&lt;code&gt;free&lt;/code&gt; 后可能会立即调用 &lt;code&gt;munmap&lt;/code&gt; 释放给系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 查看内存是否释放&lt;/h4&gt;
&lt;p&gt;可以使用工具如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;top&lt;/code&gt; 或 &lt;code&gt;htop&lt;/code&gt; 查看内存使用趋势&lt;/li&gt;
&lt;li&gt;&lt;code&gt;valgrind&lt;/code&gt; 检查内存泄漏&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pmap&lt;/code&gt; 查看进程的内存映射情况&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mallinfo()&lt;/code&gt;（旧）或 &lt;code&gt;malloc_info()&lt;/code&gt;（新）来观察 glibc 的内存使用状况&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;🔥 malloc、realloc、calloc 的区别？&lt;/h3&gt;
&lt;p&gt;1️⃣ &lt;code&gt;malloc&lt;/code&gt; 函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void* malloc(unsigned int num_size);

int *p = malloc(20*sizeof(int)); // 申请 20 个 int 类型的空间；
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ &lt;code&gt;calloc&lt;/code&gt; 函数：省去了人为空间计算；malloc 申请的空间的值是随机初始化的，calloc 申请的空间的值是初始化为 0 的；&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void* calloc(size_t n,size_t size);

int *p = calloc(20, sizeof(int));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3️⃣ &lt;code&gt;realloc&lt;/code&gt; 函数：给动态分配的空间分配额外的空间，用于扩充容量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void realloc(void *p, size_t new_size);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;9. 异常/错误处理有几种方法，为什么有些场合要禁用？&lt;/h2&gt;
&lt;p&gt;C++ 提供了多种错误处理机制，主要包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回码：函数通过返回值指示成功或失败，调用者需要检查返回值以确定操作结果。&lt;/li&gt;
&lt;li&gt;错误码：使用全局或静态变量存储错误码，调用者需要在每个步骤后检查错误码。&lt;/li&gt;
&lt;li&gt;异常处理：使用 &lt;code&gt;try&lt;/code&gt;、&lt;code&gt;catch&lt;/code&gt; 和 &lt;code&gt;throw&lt;/code&gt; 关键字捕获和处理异常，提供结构化的错误处理方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在某些场合，可能需要禁用异常处理，原因包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;性能要求高的场合：异常处理可能引入性能开销，影响程序的执行效率。&lt;/li&gt;
&lt;li&gt;嵌入式系统：资源有限，可能不支持异常处理。&lt;/li&gt;
&lt;li&gt;编译器不支持：某些编译器可能不支持异常处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;禁用异常处理可以通过编译器选项实现，例如在 Sun Studio 中使用 &lt;code&gt;-features=no%except&lt;/code&gt; 来禁用异常处理。&lt;/p&gt;
&lt;h2&gt;10. C 相关的问题，什么是野指针，有哪些野指针？&lt;/h2&gt;
&lt;p&gt;野指针是指向「&lt;strong&gt;未初始化&lt;/strong&gt;」或「&lt;strong&gt;已释放内存&lt;/strong&gt;」的指针，使用野指针会导致未定义行为，常见野指针：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;未初始化的指针：指针声明后未被初始化，默认值不确定，可能指向任意内存地址&lt;/li&gt;
&lt;li&gt;悬垂指针：指向已释放内存的指针，释放内存后未将指针置为 NULL，导致指针仍指向已回收的内存地址&lt;/li&gt;
&lt;li&gt;空指针：指针被初始化为 NULL，但在后续使用前未被赋予有效地址，导致解引用时发生错误&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为避免野指针，应该在声明指针时进行初始化，并在释放内存后将指针置为 NULL。&lt;/p&gt;
&lt;p&gt;在更多结构化的解决方案中，一种流行的避免悬垂指针的技术是使用&lt;strong&gt;智能指针&lt;/strong&gt;，一个智能指针通常使用引用技术来收回对象。还有些技术包括 tombstones 方法和 locks-and-keys 方法。另一个方法是使用 Boehm 垃圾收集器，一种保守的垃圾收集器，取代 C 和 C++ 中的标准内存分配函数。此法通过禁止内存释放函数来完全消除悬垂指针引发的错误，通过收集垃圾来回收对象。&lt;/p&gt;
&lt;h2&gt;11. 你平常怎么调试代码，你能想到多少方法？&lt;/h2&gt;
&lt;p&gt;调试代码是开发过程中非常重要的一部分，尤其是当出现问题时。调试的方式有很多种，下面是我能想到的常见调试方法：&lt;/p&gt;
&lt;h3&gt;使用调试器 (Debugger)&lt;/h3&gt;
&lt;p&gt;调试器是一种强大的工具，可以让你在程序运行时暂停执行，检查变量的值、调用堆栈等信息，逐行执行代码来找出错误。常见的调试器包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GDB (GNU Debugger)&lt;/strong&gt;：适用于 C/C++ 等语言，通过命令行进行调试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visual Studio Debugger&lt;/strong&gt;：适用于 Windows 上的 C++ 和 .NET 程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLDB&lt;/strong&gt;：用于 macOS 或 Linux 的调试器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Xcode Debugger&lt;/strong&gt;：适用于 macOS 和 iOS 应用的调试器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用调试器，你可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;设置断点&lt;/strong&gt;：暂停程序执行，以检查变量状态和函数调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逐步执行代码&lt;/strong&gt;：逐行执行，查看每一行代码的效果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查栈信息和变量的值&lt;/strong&gt;：实时查看变量的值、函数调用栈、内存内容等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，使用 GDB 调试 C++ 代码时，可以使用以下命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gdb ./your_program&lt;/code&gt; 启动调试器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;break main&lt;/code&gt; 在 &lt;code&gt;main()&lt;/code&gt; 函数处设置断点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run&lt;/code&gt; 启动程序执行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;step&lt;/code&gt; 或 &lt;code&gt;next&lt;/code&gt; 逐步执行代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;插入日志输出 (Logging)&lt;/h3&gt;
&lt;p&gt;在代码中添加日志输出是调试程序的常见方法。你可以在代码中插入 &lt;code&gt;printf&lt;/code&gt;、&lt;code&gt;std::cout&lt;/code&gt; 或日志库（如 &lt;code&gt;log4cpp&lt;/code&gt;, &lt;code&gt;spdlog&lt;/code&gt;, &lt;code&gt;glog&lt;/code&gt; 等）来输出变量值、函数执行状态和程序流程。&lt;/p&gt;
&lt;p&gt;常见做法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;输出函数进入与退出的日志。&lt;/li&gt;
&lt;li&gt;打印变量值、数据结构的内容。&lt;/li&gt;
&lt;li&gt;打印程序的状态和执行的分支。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::cout &amp;#x3C;&amp;#x3C; &quot;Value of x: &quot; &amp;#x3C;&amp;#x3C; x &amp;#x3C;&amp;#x3C; std::endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非常直接和简单。&lt;/li&gt;
&lt;li&gt;可以在生产环境中使用（例如在开发版和发布版中配置不同的日志级别）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可能会遗漏某些地方，导致调试信息不够全面。&lt;/li&gt;
&lt;li&gt;需要在最终代码中删除或关闭冗余的日志输出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;单元测试 (Unit Testing)&lt;/h3&gt;
&lt;p&gt;单元测试是一种自动化的方式，可以帮助你验证代码的正确性。使用框架如 Google Test（C++）、JUnit（Java）、pytest（Python）等，可以编写测试用例，自动运行测试，并在代码发生变化时及时捕捉错误。&lt;/p&gt;
&lt;p&gt;单元测试的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确保代码的每个模块都按预期工作。&lt;/li&gt;
&lt;li&gt;能够提前发现潜在问题，特别是在修改代码时。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试用例需要编写和维护，可能需要额外的时间。&lt;/li&gt;
&lt;li&gt;需要有较好的测试覆盖率，才能检测到更多的错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;静态分析工具 (Static Analysis)&lt;/h3&gt;
&lt;p&gt;静态分析工具可以在代码运行之前，扫描代码并检查潜在的错误、内存泄漏、资源管理问题等。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Clang Static Analyzer&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CppCheck&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SonarQube&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coverity&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;静态分析工具能够检测到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;未初始化的变量。&lt;/li&gt;
&lt;li&gt;内存泄漏。&lt;/li&gt;
&lt;li&gt;潜在的并发问题。&lt;/li&gt;
&lt;li&gt;错误的代码模式等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;代码审查 (Code Review)&lt;/h3&gt;
&lt;p&gt;代码审查是与团队成员或同事一起查看和讨论代码的过程。其他开发者可以帮助你发现代码中的潜在问题或逻辑错误。&lt;/p&gt;
&lt;p&gt;代码审查的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多人的视角能够发现更多问题。&lt;/li&gt;
&lt;li&gt;通过讨论，能够提升代码质量和团队合作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;集成测试 (Integration Testing)&lt;/h3&gt;
&lt;p&gt;集成测试是测试多个组件（或模块）一起工作时的行为。在多个模块组合工作时，问题可能不是单独模块内部，而是它们之间的交互。集成测试帮助你检查模块之间的接口和数据流。&lt;/p&gt;
&lt;p&gt;集成测试通常用来发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模块之间的兼容性问题。&lt;/li&gt;
&lt;li&gt;数据格式错误。&lt;/li&gt;
&lt;li&gt;不正确的模块交互等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;内存泄漏检测工具&lt;/h3&gt;
&lt;p&gt;如果你的程序存在内存泄漏问题，可以使用专门的工具来检测内存的分配和释放：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🔥 &lt;strong&gt;Valgrind&lt;/strong&gt;：广泛用于检测内存泄漏、内存错误等问题，适用于 C/C++ 程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AddressSanitizer&lt;/strong&gt;：现代编译器（如 Clang、GCC）提供的工具，可以检测内存相关的错误，包括越界访问、内存泄漏等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些工具帮助你找出内存泄漏和错误的内存访问问题，并给出详细的报告。&lt;/p&gt;
&lt;h3&gt;运行时分析工具&lt;/h3&gt;
&lt;p&gt;运行时分析工具通过收集程序运行时的信息来进行调试和优化。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;gprof&lt;/strong&gt;：用于性能分析，查看程序中哪些函数占用了最多的时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;perf&lt;/strong&gt;：Linux 下的性能分析工具，帮助查看程序在系统层面的性能瓶颈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VisualVM&lt;/strong&gt;：Java 应用程序的性能分析工具，能够分析内存、CPU 和线程使用情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;条件断点和日志断点&lt;/h3&gt;
&lt;p&gt;在调试过程中，有时你希望仅在满足特定条件时暂停程序。这时可以使用&lt;strong&gt;条件断点&lt;/strong&gt;或&lt;strong&gt;日志断点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;条件断点&lt;/strong&gt;：只有当某个条件成立时，调试器才会停止程序执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志断点&lt;/strong&gt;：调试器在不停止程序执行的情况下，记录断点信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;回滚与分支 (Git Bisect)&lt;/h3&gt;
&lt;p&gt;如果你无法确定错误是在哪次提交中引入的，使用 Git 提供的 &lt;code&gt;git bisect&lt;/code&gt; 命令来回滚到历史提交并逐步测试，可以帮助定位问题的来源。&lt;/p&gt;
&lt;p&gt;通过二分查找算法，&lt;code&gt;git bisect&lt;/code&gt; 可以帮助你快速定位到错误引入的那一行代码。&lt;/p&gt;
&lt;h3&gt;故障注入 (Fault Injection)&lt;/h3&gt;
&lt;p&gt;故障注入是故意在程序中引入故障，以测试程序在面对错误时的反应。例如，可以通过随机生成异常、模拟网络延迟或中断等方式，检查系统的健壮性和错误处理能力。&lt;/p&gt;
&lt;h3&gt;动态分析与跟踪 (Dynamic Analysis)&lt;/h3&gt;
&lt;p&gt;使用跟踪工具（如 &lt;code&gt;strace&lt;/code&gt;, &lt;code&gt;ltrace&lt;/code&gt;, &lt;code&gt;dtrace&lt;/code&gt; 等）来实时观察程序执行过程中的系统调用和函数调用。这种方式帮助你了解程序在运行时的行为，找出性能瓶颈或其他问题。&lt;/p&gt;
&lt;h2&gt;12. 什么是 C++ 多态？&lt;/h2&gt;
&lt;p&gt;C++ 多态即使用基类指针或引用来调用子类的重写方法，从而使得同一接口表现不同的行为。&lt;/p&gt;
&lt;p&gt;多态优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码复用：通过基类指针或引用，可以操作不同类型的派生类对象，实现代码复用&lt;/li&gt;
&lt;li&gt;扩展性：新增派生类时，不需要修改依赖于基类的代码，只需要确保新类正确重写了虚函数&lt;/li&gt;
&lt;li&gt;解耦：多态允许程序更加模块化，降低类之间的耦合度&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;🔥 面试一定要回答「静态多态」+「动态多态」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;多态一般就是指&lt;strong&gt;继承 + 虚函数实现的多态&lt;/strong&gt;，对于重载来说，实际原理是编译器为函数生成符号表时的不同规则，重载只是一种语言特性，与多态无关，与面向对象无关，所以如果非要说重载算是多态的一种，那 C++ 中多态可以分为「静态多态」和「动态多态」两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态多态：在编译时期就决定了调用哪个函数，根据参数列表来决定，主要通过&lt;strong&gt;函数重载&lt;/strong&gt;和&lt;strong&gt;模板&lt;/strong&gt;实现&lt;/li&gt;
&lt;li&gt;动态多态：通过子类重写父类的虚函数来实现，是运行期间决定调用的函数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;动态多态的实现与虚函数表（V-Table），虚函数指针（V-Ptr）相关&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚函数表（V-Table）：C++ 运行时使用虚函数表来实现多态，每个包含虚函数的类都有一个虚函数表，表中存储了指向类中所有虚函数的指针。&lt;/li&gt;
&lt;li&gt;虚函数指针（V-Ptr）：对象中包含一个指向该类虚函数表的指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;扩展&lt;/strong&gt;：子类是否要重写父类的虚函数？子类继承父类时，父类的纯虚函数必须重写，否则子类也是一个虚类不可实例化。定义纯虚函数是为了实现一个接口，起到一个规范的作用，规范继承这个类的程序员必须实现这个函数。&lt;/p&gt;
&lt;h2&gt;13. 什么是虚函数与虚函数指针，C++ 虚函数的实现原理？&lt;/h2&gt;
&lt;p&gt;首先说一下 C++ 中多态的表象：在基类的函数前加上 &lt;code&gt;virtual&lt;/code&gt; 关键字，在派生类中重写该函数，运行时将会根据对象的实际类型来调用相应的函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果对象类型是派生类，就调用派生类的函数&lt;/li&gt;
&lt;li&gt;如果是基类，就调用基类的函数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;虚函数 &lt;code&gt;vtable&lt;/code&gt; 与虚函数指针 &lt;code&gt;vptr&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;实际上，当一个类中包含虚函数 &lt;code&gt;virtual&lt;/code&gt; 时，编译器就会为该类生成一个虚函数表 &lt;code&gt;vtable&lt;/code&gt;，保存该类中虚函数的地址。同样，派生类继承基类，派生类中自然一定有虚函数，所以编译器也会为派生类生成自己的虚函数表 &lt;code&gt;vtable&lt;/code&gt;。当我们定义一个派生类对象时，编译器检测到该类型有虚函数，就会为这个派生类对象生成一个虚函数指针 &lt;code&gt;vptr&lt;/code&gt;，指向该类型的虚函数表 &lt;code&gt;vtable&lt;/code&gt;，&lt;strong&gt;虚函数指针 &lt;code&gt;vptr&lt;/code&gt; 的初始化是在构造函数中完成的&lt;/strong&gt;。后续如果有一个基类类型的指针指向派生类，那么当调用虚函数时，就会根据所指真正对象的虚函数表指针 &lt;code&gt;vptr&lt;/code&gt; 去寻找虚函数的地址，也就可以调用派生类的虚函数表中虚函数以此实现多态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;补充&lt;/strong&gt;：如果基类中没有定义成 &lt;code&gt;virtual&lt;/code&gt;（只有继承），那么在这种情况调用的则是 &lt;code&gt;Base&lt;/code&gt; 中的 &lt;code&gt;func()&lt;/code&gt;。&lt;strong&gt;因为如果基类和派生类中都没有虚函数 &lt;code&gt;virtual&lt;/code&gt; 的定义，那么编译器就会认为不用留给动态多态的机会&lt;/strong&gt;，就事先进行函数地址的绑定（早绑定 —— 静态绑定），具体过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义了派生类对象，首先构造基类的空间，然后构造派生类的自身内容，形成一个派生类对象&lt;/li&gt;
&lt;li&gt;进行类型转换时，直接截取基类的部分内存，编译器认为类型就是基类，那么函数符号表（不同于虚函数表）绑定的函数地址也就是基类中的函数地址，执行的就是基类函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 🌟只有 virtual 存在，编译器才会认为存在「多态」
class Base {
public:
    // virtual 不存在则只调用 ~Base()
    virtual ~Base() { // 虚析构函数
        // 释放 Base 的资源
        cout &amp;#x3C;&amp;#x3C; &quot;释放 Base 的资源&quot; &amp;#x3C;&amp;#x3C; endl;
    }

    // virtual 不存在则只调用 Base func()
    virtual void func() {
        cout &amp;#x3C;&amp;#x3C; &quot;Base_func()&quot; &amp;#x3C;&amp;#x3C; endl;
    }
};

class Derived : public Base {
public:
    // override 可加可不加，有助于编译器检查
    ~Derived() override {
        // 释放 Derived 的资源
        cout &amp;#x3C;&amp;#x3C; &quot;释放 Derived 的资源&quot; &amp;#x3C;&amp;#x3C; endl;
    }

    // override 可加可不加，有助于编译器检查
    void func() override {
        cout &amp;#x3C;&amp;#x3C; &quot;Derived_func()&quot; &amp;#x3C;&amp;#x3C; endl;
    }
};

int main() {
    Base* ptr = new Derived;
    ptr-&gt;func();
    delete ptr;  // 调用时，先执行 Derived::~Derived()，再执行 Base::~Base()
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Derived_func()
释放 Derived 的资源
释放 Base 的资源
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++ 虚函数的内存分布 &amp;#x26; 实现原理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;以上简要介绍了「虚函数」相关内容（简要介绍了原理），接下来详细阐述实现原理&lt;/p&gt;
&lt;p&gt;更多信息：&lt;a href=&quot;https://jacktang816.github.io/post/virtualfunction/&quot;&gt;C++ 虚函数的实现基本原理&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class A {
  public:
    virtual void v_a(){}
    virtual ~A(){}
    int64_t _m_a;
};

int main(){
    A* a = new A();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如以上代码所示，在 C++ 中定义一个对象 A，那么在内存中的分布大概是如下图这个样子。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先在主函数的栈帧上有一个 A 类型的指针指向堆里面分配好的对象 A 实例。&lt;/li&gt;
&lt;li&gt;对象 A 实例的&lt;strong&gt;头部&lt;/strong&gt;是一个 &lt;code&gt;vtable&lt;/code&gt; 指针，紧接着是 &lt;strong&gt;A 对象按照声明顺序排列的成员变量&lt;/strong&gt;（当我们创建一个对象时，便可以通过实例对象的地址，得到该实例的虚函数表，从而获取其函数指针）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vptr&lt;/code&gt; 指针指向的是代码段中的 A 类型的&lt;strong&gt;虚函数表中的第一个虚函数起始地址&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;虚函数表 &lt;code&gt;vtable&lt;/code&gt; 的结构其实是有一个头部的，叫做 &lt;code&gt;vtable_prefix&lt;/code&gt; ，紧接着是按照声明顺序排列的虚函数。&lt;/li&gt;
&lt;li&gt;注意到这里有两个虚析构函数，因为对象有两种构造方式，&lt;strong&gt;栈构造&lt;/strong&gt;和&lt;strong&gt;堆构造&lt;/strong&gt;，所以对应的，对象会有两种析构方式，其中堆上对象的析构和栈上对象的析构不同之处在于，栈内存的析构不需要执行 &lt;code&gt;delete&lt;/code&gt; 函数，会自动被回收。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;typeinfo&lt;/code&gt; 存储着 A 的类基础信息，包括父类与类名称，C++关键字 &lt;code&gt;typeid&lt;/code&gt; 返回的就是这个对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;typeinfo&lt;/code&gt; 也是一个类，对于没有父类的 A 来说，当前 tinfo 是 &lt;code&gt;class_type_info&lt;/code&gt; 类型的，从虚函数指针指向的 vtable 起始位置可以看出。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503021850782.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;1️⃣ &lt;code&gt;Example-1&lt;/code&gt;｜如果是多继承情况下，编译器如下处理虚函数表｜&lt;strong&gt;虚函数的实现原理&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拷贝基类的虚函数表，多继承则拷贝每个虚函数基类的虚函数表&lt;/li&gt;
&lt;li&gt;多继承会存在一个基类虚函数表和派生类自身虚函数表合并共用，该基类称为派生类的主基类&lt;/li&gt;
&lt;li&gt;派生类重写基类虚函数，则替换重写后的虚函数地址&lt;/li&gt;
&lt;li&gt;如果有自身虚函数，则追加自身虚函数到自身的虚函数表&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;其中 D 对象 &lt;code&gt;vptr1&lt;/code&gt; 指向的虚函数表合并了「某个基类虚函数表」和「派生类自身虚函数表」，&lt;code&gt;vptr2&lt;/code&gt; 则指向另一个基类的虚函数表&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503021812761.png&quot; alt=&quot;image-20250302181234528&quot;&gt;&lt;/p&gt;
&lt;p&gt;2️⃣ &lt;code&gt;Example-2&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class A{
private:
    uint64_t a;
public:
    virtual void A_a(){std::cout &amp;#x3C;&amp;#x3C; __func__;}
};
class C{
private:
    uint64_t c;
public:
    virtual void C_a(){std::cout &amp;#x3C;&amp;#x3C; __func__;}
};

class D : public A,public C{
private:
    uint64_t d;
public:
    virtual void D_a(){std::cout &amp;#x3C;&amp;#x3C; __func__;}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;class D 的虚函数表&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503021849625.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;14. 析构函数可以是虚函数吗？什么情况下析构函数必须是虚函数？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;🪞镜像问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么需要虚析构？虚析构实现原理？&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;析构函数一般写成虚函数的原因？&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;析构函数可以是虚函数。将析构函数声明为 &lt;code&gt;virtual&lt;/code&gt; 虚函数，确保在删除基类指针指向的派生类对象时，能够正确调用派生类的析构函数，&lt;strong&gt;避免内存泄漏&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;举例来说，一个基类的指针指向一个派生类的对象，在使用完毕准备销毁时，如果基类的析构函数没有定义成 &lt;code&gt;virtual&lt;/code&gt; 虚函数，那么编译器根据指针类型就会认为当前对象类型是基类，&lt;strong&gt;仅调用基类的析构函数&lt;/strong&gt;（该对象的析构函数的函数地址早就被绑定为基类的析构函数——静态绑定 / 早绑定），派生类的自身内容将无法被析构，造成内存泄漏。如果基类的析构函数定义为虚函数，那么编译器就可以根据实际对象，执行派生类的析构函数，再执行基类的析构函数，成功释放内存。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注释助于理解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 🌟只有 virtual 存在，编译器才会认为存在「多态」
class Base {
public:
    // virtual 不存在则只调用 ~Base()
    virtual ~Base() { // 虚析构函数
        // 释放 Base 的资源
        cout &amp;#x3C;&amp;#x3C; &quot;释放 Base 的资源&quot; &amp;#x3C;&amp;#x3C; endl;
    }

    // virtual 不存在则只调用 Base func()
    virtual void func() {
        cout &amp;#x3C;&amp;#x3C; &quot;Base_func()&quot; &amp;#x3C;&amp;#x3C; endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        // 释放 Derived 的资源
        cout &amp;#x3C;&amp;#x3C; &quot;释放 Derived 的资源&quot; &amp;#x3C;&amp;#x3C; endl;
    }

    void func() {
        cout &amp;#x3C;&amp;#x3C; &quot;Derived_func()&quot; &amp;#x3C;&amp;#x3C; endl;
    }
};

int main() {
    Base* ptr = new Derived;
    ptr-&gt;func();
    delete ptr;  // 调用时，先执行 Derived::~Derived()，再执行 Base::~Base()
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;Derived_func()		// func() 没定义 virtual 则输出 Base_func()
释放 Derived 的资源	 // ~Base() 没定义 virtual 则不输出
释放 Base 的资源
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ &lt;strong&gt;C++ 默认的析构函数不是虚函数，是因为虚函数需要额外的虚函数表和虚表指针，占用额外的内存&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当类被设计为「基类」，并且可能被继承时，析构函数应当声明为虚函数&lt;/strong&gt;。如果类不会被继承，则析构函数可以不声明为虚函数。然而，为了代码的健壮性和可维护性，&lt;strong&gt;通常建议将基类的析构函数声明为虚函数&lt;/strong&gt;，即使该类当前不会被继承。&lt;/p&gt;
&lt;h2&gt;15. 构造函数为什么一般不定义为虚函数&lt;/h2&gt;
&lt;p&gt;1️⃣ 虚函数调用只需要知道“部分信息”，即只需要知道函数接口，而不需要知道对象的具体类型。但是创建对象时，是需要知道对象的完整信息，特别是需要知道创建对象的确切类型，因此构造函数不应该被定义为虚函数。&lt;/p&gt;
&lt;p&gt;2️⃣ 从编译器实现虚函数进行多态的方式来看，虚函数调用时通过实例化之后对象的虚函数表指针 &lt;code&gt;vptr&lt;/code&gt; 来找到虚函数地址进行调用的，如果说构造函数是虚的，那么虚函数表指针则不存在（&lt;strong&gt;因为虚函数指针 &lt;code&gt;vptr&lt;/code&gt; 的初始化是在构造函数中完成的&lt;/strong&gt;），无法找到对应的虚函数表 &lt;code&gt;vtable&lt;/code&gt; 来调用虚函数，&lt;strong&gt;那么这个调用实际上也是违反了先实例化后调用的准则&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;16. 构造函数的执行顺序？析构函数的执行顺序？&lt;/h2&gt;
&lt;h3&gt;1️⃣ 构造函数顺序&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;基类构造函数：如果有多个基类，则构造函数调用顺序是某类在「类派生列表」中出现的顺序，而不是它们在成员初始化表中的顺序&lt;/li&gt;
&lt;li&gt;成员类对象构造函数：如果有多个成员类对象，则构造函数的调用顺序是对象在类中被声明的顺序，而不是它们出现在成员初始化表中的顺序&lt;/li&gt;
&lt;li&gt;派生类构造函数&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;类派生列表&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class 派生类名:类派生列表 {
	成员列表
}

class Derived : public Base1, public Base2 {
	成员列表
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 析构函数顺序&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;调用派生类的析构函数&lt;/li&gt;
&lt;li&gt;调用成员类对象的析构函数&lt;/li&gt;
&lt;li&gt;调用基类的析构函数&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;17. 静态绑定和动态绑定&lt;/h2&gt;
&lt;p&gt;我们首先要知道静态类型和动态类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态类型：在程序中被声明时所采用的类型，&lt;strong&gt;在编译期间确定&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;动态类型：目前所指对象的实际类型，&lt;strong&gt;在运行期间确定&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于静态绑定和动态绑定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态绑定，又称早绑定，绑定的是静态类型，所对应的函数或属性依赖于对象的静态类型，发生在编译期间。&lt;/li&gt;
&lt;li&gt;动态绑定，又称晚绑定，绑定的是动态类型，所对应的函数或属性依赖于动态类型，发生在运行期间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如说，&lt;code&gt;virtual&lt;/code&gt; 函数是动态绑定的，非虚函数是静态绑定的，缺省参数值也是静态绑定的。&lt;/p&gt;
&lt;p&gt;⚠️ 注意，我们不应该重新定义继承而来的缺省参数，因为即使我们重定义了，也不会起到效果。因为一个基类的指针指向一个派生类对象，在派生类的对象中针对虚函数的参数缺省值进行了重定义， 但是缺省参数值是静态绑定的，静态绑定绑定的是静态类型相关的内容。&lt;/p&gt;
&lt;h2&gt;18. 纯虚函数&lt;/h2&gt;
&lt;p&gt;纯虚函数是在基类中「声明但不实现」的虚函数，其声明方式是在函数声明的结尾处添加 &lt;code&gt;= 0&lt;/code&gt;，类中如果至少包含一个纯虚函数，则该类称为抽象类，&lt;strong&gt;抽象类是不能实例化对象的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;纯虚函数的主要作用是定义接口规范，强制要求派生类必须实现这些函数，从而实现借口的统一和标准化。派生类中必须实现继承于基类的纯虚函数，否则含有纯虚函数的类又会是抽象类，无法实例化。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Shape {
public:
    virtual void draw() = 0; // 纯虚函数
};

class Circle : public Shape {
public:
    // 必须实现，否则该派生类为抽象类，不能实例化
    void draw() override {
        cout &amp;#x3C;&amp;#x3C; &quot;Drawing a circle&quot; &amp;#x3C;&amp;#x3C; endl;
    }
};

int main() {
    Shape* shape = new Circle();
    shape-&gt;draw(); // 输出：Drawing a circle
    delete shape;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;19. 深拷贝和浅拷贝的区别（举例说明深拷贝的安全性）&lt;/h2&gt;
&lt;p&gt;1️⃣ &lt;strong&gt;浅拷贝&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当出现类的等号 &lt;code&gt;=&lt;/code&gt; 赋值时，会调用拷贝构造函数，在未显式定义拷贝构造函数的情况下，系统会调用默认的拷贝函数 —— 即浅拷贝，它能够完成成员的复制，当数据成员中没有指针时，浅拷贝是可行的；&lt;/li&gt;
&lt;li&gt;但当数据成员中有&lt;strong&gt;指针&lt;/strong&gt;时，如果采用简单的浅拷贝，则两个类中的两个指针指向同一个地址，当对象快要结束时，&lt;strong&gt;会调用两次析构函数，从而导致野指针的问题&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class ShallowCopy {
private:
    int* data;
public:
    ShallowCopy(int d) : data(new int(d)) {}
    // 而且在对象结束时，会调用两次析构函数，从而导致野指针问题
    ~ShallowCopy() { delete data; }
    void setData(int d) { *data = d; }
    int getData() const { return *data; }

    // 默认拷贝构造函数（浅拷贝）
    ShallowCopy(const ShallowCopy&amp;#x26; source) : data(source.data) {}
};

int main() {
    ShallowCopy obj1(10);
    ShallowCopy obj2 = obj1; // 使用默认拷贝构造函数

    cout &amp;#x3C;&amp;#x3C; &quot;obj1 data: &quot; &amp;#x3C;&amp;#x3C; obj1.getData() &amp;#x3C;&amp;#x3C; endl;
    cout &amp;#x3C;&amp;#x3C; &quot;obj2 data: &quot; &amp;#x3C;&amp;#x3C; obj2.getData() &amp;#x3C;&amp;#x3C; endl;

    obj1.setData(20); // 修改 obj1 的数据

    cout &amp;#x3C;&amp;#x3C; &quot;After modifying obj1&quot; &amp;#x3C;&amp;#x3C; endl;
    cout &amp;#x3C;&amp;#x3C; &quot;obj1 data: &quot; &amp;#x3C;&amp;#x3C; obj1.getData() &amp;#x3C;&amp;#x3C; endl;
    cout &amp;#x3C;&amp;#x3C; &quot;obj2 data: &quot; &amp;#x3C;&amp;#x3C; obj2.getData() &amp;#x3C;&amp;#x3C; endl; // obj2 数据也被修改了

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ &lt;strong&gt;深拷贝&lt;/strong&gt;：在数据成员含有指针时，必须采用深拷贝（自定义拷贝构造函数），在拷贝构造函数中创建一个全新对象，与原对象完全独立。深拷⻉与浅拷⻉之间的区别就在于，&lt;strong&gt;深拷⻉会在堆内存中另外申请空间来存储数据&lt;/strong&gt;，从而解决来野指针的问题。简而言之，当数据成员中有指针时，必需要用深拷⻉更加安全。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class DeepCopy {
private:
    int *data;
public:
    DeepCopy(int d) : data(new int(d)) {}
    ~DeepCopy() { delete data; }
    void setData(int d) { *data = d; }
    int getData() const { return *data; }

    // 自定义拷贝构造函数（深拷贝）
    DeepCopy(const DeepCopy &amp;#x26;source) : data(new int(*source.data)) {}
};

int main() {
    DeepCopy obj1(10);
    DeepCopy obj2 = obj1; // 使用自定义拷贝构造函数

    cout &amp;#x3C;&amp;#x3C; &quot;obj1 data: &quot; &amp;#x3C;&amp;#x3C; obj1.getData() &amp;#x3C;&amp;#x3C; endl;
    cout &amp;#x3C;&amp;#x3C; &quot;obj2 data: &quot; &amp;#x3C;&amp;#x3C; obj2.getData() &amp;#x3C;&amp;#x3C; endl;

    obj1.setData(20); // 修改 obj1 的数据

    cout &amp;#x3C;&amp;#x3C; &quot;After modifying obj1&quot; &amp;#x3C;&amp;#x3C; endl;
    cout &amp;#x3C;&amp;#x3C; &quot;obj1 data: &quot; &amp;#x3C;&amp;#x3C; obj1.getData() &amp;#x3C;&amp;#x3C; endl;
    cout &amp;#x3C;&amp;#x3C; &quot;obj2 data: &quot; &amp;#x3C;&amp;#x3C; obj2.getData() &amp;#x3C;&amp;#x3C; endl; // obj2 数据没有变化

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;20. 说一下你理解的 C++ 四种智能指针｜shared_ptr 的简易实现&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;更多信息：&lt;a href=&quot;https://jacktang816.github.io/post/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88/&quot;&gt;C++ 智能指针&lt;/a&gt;、&lt;a href=&quot;https://zhuanlan.zhihu.com/p/137958974&quot;&gt;知乎 C++ 智能指针&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;看这两篇，取取交集&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在使用 C++ 开发过程中，最容易也是最麻烦的问题便是内存泄漏。相较于 Java、Python 或者 Go 语言都拥有垃圾回收机制，在对象没有引用时就会被系统自动回收而且基本上没有指针的概念，但是 C++ 则要求程序员自己管理内存，这一方面让程序员有更大的自由度但是也会很大影响程序员的开发效率。因此 C++11 标准中新推出了 &lt;code&gt;shared_ptr&lt;/code&gt;、&lt;code&gt;unique_ptr&lt;/code&gt; 和 &lt;code&gt;weak_ptr&lt;/code&gt; 三个智能指针来帮助管理内存。&lt;/p&gt;
&lt;p&gt;智能指针就是一个类，当超出了类的作用域时，类会自动调用析构函数，析构函数会自动释放资源，所以智能指针的作用原理就是在函数结束时自动释放内存空间，不需要手动释放。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;T* get();
T&amp;#x26; operator*();
T* operator-&gt;();
T&amp;#x26; operator=(const T&amp;#x26; val);
T* release();
void reset (T* ptr = nullptr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常用接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T&lt;/code&gt; 是模板参数，即传入的类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get()&lt;/code&gt; 用来获取 &lt;code&gt;auto_ptr&lt;/code&gt; 封装在内部的指针，也就是获取原生指针&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operator*()&lt;/code&gt; 重载 &lt;code&gt;*&lt;/code&gt;，&lt;code&gt;operator-&gt;()&lt;/code&gt; 重载 &lt;code&gt;-&gt;&lt;/code&gt;，&lt;code&gt;operator=()&lt;/code&gt; 重载 &lt;code&gt;=&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;release()&lt;/code&gt; 将 &lt;code&gt;auto_ptr&lt;/code&gt; 封装在内部的指针置为 &lt;code&gt;nullptr&lt;/code&gt;，但不会破坏指针所指向的内容，函数返回的是内部指针置空之前的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reset()&lt;/code&gt; 直接释放封装的内部指针所指向的内存，如果指定了 &lt;code&gt;ptr&lt;/code&gt; 的值，则将内部指针初始化为该值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来说说哪四种智能指针：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;auto_ptr&lt;/code&gt; 为 C++98 的方案，C++11 已抛弃&lt;/li&gt;
&lt;li&gt;C++11 引入
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;std::shared_ptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::weak_ptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::unique_ptr&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;0️⃣ auto_ptr&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;C++98 方案，C++11 已抛弃&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto_ptr&amp;#x3C;std::string&gt; p1(new string(&quot;string&quot;));
auto_ptr&amp;#x3C;std::string&gt; p2;
p2 = p1;	// auto_ptr 不会报错
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;p2 剥夺了 p1 的所有权，但是当程序运行时访问 p1 将会报错，所以 &lt;code&gt;auto_ptr&lt;/code&gt; 缺点就是存在潜在的内存崩溃问题。&lt;/p&gt;
&lt;h3&gt;1️⃣ shared_ptr 共享式智能指针&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;彻底理解：&lt;code&gt;shared_ptr&lt;/code&gt; 是&lt;strong&gt;有两层析构&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;shared_ptr 本身析构会使得指向的共享对象的引用数 -1，当共享对象引用数为 0 时，则调用共享对象本身的析构函数&lt;/li&gt;
&lt;li&gt;这样就可以理解循环引用了：共享对象引用还是 1，未调用共享对象本身的析构函数，其中成员 shared_ptr 的析构函数也不会被调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;shared_ptr&lt;/code&gt; 能够自动记录共享对象的引用次数，并且在引用计数降至零时自动删除对象，从而防止内存泄漏。每个 &lt;code&gt;shared_ptr&lt;/code&gt; 的拷贝都指向相同的内存，在最后一个 &lt;code&gt;shared_ptr&lt;/code&gt; 析构的时候其指向的内存资源才会被释放。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;shared_ptr&lt;/code&gt; 初始化方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构造函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::make_shared()&lt;/code&gt; 辅助函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reset()&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::shared_ptr&amp;#x3C;int&gt; p(new int(1));
std::shared_ptr&amp;#x3C;int&gt; p2 = p;
std::shared_ptr&amp;#x3C;A&gt; ap = std::make_shared&amp;#x3C;A&gt;();

std::shared_ptr&amp;#x3C;int&gt; ptr;
ptr.reset(new int(1));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不能将一个原始指针直接赋值给一个智能指针&lt;/strong&gt;，如：&lt;code&gt;std::shared_ptr&amp;#x3C;int&gt; p = new int(1)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于一个未初始化的智能指针，可以通过调用 &lt;code&gt;reset&lt;/code&gt; 方法初始化，当智能指针中有值的时候，调用 &lt;code&gt;reset&lt;/code&gt; 方法会使引用计数减 1。当需要获取原指针的时候可以通过 &lt;code&gt;get&lt;/code&gt; 方法返回原始指针：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::shared_ptr&amp;#x3C;int&gt; p(new int(1));
int *ptr = p.get();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;智能指针初始化时也可以指定删除器，当其引用计数为 0 时将自动调用删除器来释放对象，删除器可以是一个函数对象。&lt;strong&gt;如当使用 &lt;code&gt;shared_ptr&lt;/code&gt; 管理动态数组时，需要指定删除器，因为 &lt;code&gt;shared_ptr&lt;/code&gt; 默认删除器不支持数组对象&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// lambda 表达式作为删除器
std::shared_ptr&amp;#x3C;int&gt; p(new int[10], [](int *p) { delete []p; })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 &lt;code&gt;shared_ptr&lt;/code&gt; 的注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不要用一个裸指针初始化多个 &lt;code&gt;shared_ptr&lt;/code&gt;&lt;/strong&gt;，会出现 &lt;em&gt;&lt;strong&gt;double_free&lt;/strong&gt;&lt;/em&gt; 导致程序崩溃&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;shared_from_this()&lt;/code&gt; 返回 this 指针，不要把 this 指针作为 &lt;code&gt;shared_ptr&lt;/code&gt; 返回出来，因为 &lt;code&gt;this&lt;/code&gt; 指针本质就是裸指针，通过 this 返回可能会导致重复析构，&lt;strong&gt;不能把 this 指针交给智能指针管理&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class A {
  	shared_ptr&amp;#x3C;A&gt; GetSelf() {
    	return shared_from_this();
  		// return shared_ptr&amp;#x3C;A&gt;(this); 错误，会导致 double free
	}  
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;尽量使用 &lt;code&gt;std::make_shared&amp;#x3C;T&gt;()&lt;/code&gt;，少用 &lt;code&gt;new&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要 &lt;code&gt;delete&lt;/code&gt; &lt;code&gt;get()&lt;/code&gt; 返回的裸指针&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不是 &lt;code&gt;new&lt;/code&gt; 出来的空间要自定义删除器&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要避免循环引用&lt;/strong&gt;，循环引用导致内存永远不会被释放，造成内存泄漏&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class A;
class B;

class A {
public:
    std::shared_ptr&amp;#x3C;B&gt; b;
};

class B {
public:
    std::shared_ptr&amp;#x3C;A&gt; a;
};

int main() {
    std::shared_ptr&amp;#x3C;A&gt; ap = std::make_shared&amp;#x3C;A&gt;();
    std::shared_ptr&amp;#x3C;B&gt; bp = std::make_shared&amp;#x3C;B&gt;();
    ap-&gt;b = bp;
    bp-&gt;a = ap;
    // 此时，a 和 b 相互持有对方的 shared_ptr，形成循环引用
    // 程序结束时，a 和 b 的引用计数都不会降为零，导致内存泄漏
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🌟 &lt;strong&gt;解释说明循环引用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先循环引用导致 shared_ptr 指向的共享对象 A 和 B 的引用计数都是 2；&lt;/li&gt;
&lt;li&gt;在离开作用域后，根据栈后进先出的特点，首先 &lt;code&gt;shared_ptr&amp;#x3C;B&gt; bp&lt;/code&gt; 析构时只减少 B 的引用次数为 1（这里是对象 shared_ptr 析构而非对象 B 析构），由于此时对象 B 的引用次数仍为 1（减为 0 的 B 才会被释放），所以不会调用（对象 B）内部智能指针 &lt;code&gt;a&lt;/code&gt; 的析构函数来减少引用，所以也就无法减少 A 的引用次数了。&lt;/li&gt;
&lt;li&gt;接着 &lt;code&gt;ap&lt;/code&gt; 析构时减少 A 的引用次数为 1，此时 A 的引用仍为 1 不会被析构，所以无法析构其成员对象 &lt;code&gt;b&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;最终导致指针永远不会析构，产生了内存泄漏（解决方案就是使用 &lt;code&gt;weak_ptr&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2️⃣ weak_ptr 弱引用智能指针&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;weak_ptr&lt;/code&gt; 是一种不控制对象生命周期的智能指针，它指向一个 shared_ptr 管理的对象，它不管理 &lt;code&gt;shared_ptr&lt;/code&gt; 内部指针，进行该对象的内存管理的是那个强引用的 shared_ptr。&lt;/p&gt;
&lt;p&gt;weak_ptr 只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作，纯粹是作为一个旁观者监视 &lt;code&gt;shared_ptr&lt;/code&gt; 中管理的资源是否存在，&lt;strong&gt;它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造&lt;/strong&gt;，&lt;strong&gt;它的构造和析构不会引起引用记数的增加或减少&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;weak_ptr 是用来解决 shared_ptr 相互引用时的死锁问题，如果说两个 shared_ptr 相互引用，那么这两个指针的引用计数永远不可能下降为 0，也就是资源永远不会释放。它是对对象的一种弱引用，不会增加对象的引用计数，&lt;strong&gt;和 shared_ptr 之间可以相互转化&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;shared_ptr 可以直接赋值给它&lt;/li&gt;
&lt;li&gt;它也可以通过调用 lock 函数来获得 shared_ptr&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;循环引用是当两个智能指针都是 shared_ptr 类型的时候，析构时两个资源引用计数会减 1，但是两者引用计数还是为 1，导致跳出函数时资源没有被释放（&lt;strong&gt;析构函数没有被调用&lt;/strong&gt;），解决办法就是把其中一个改为 &lt;code&gt;weak_ptr&lt;/code&gt; 就可以。&lt;/p&gt;
&lt;p&gt;总之 weak_ptr &lt;strong&gt;可以用来返回 this 指针和解决循环引用问题&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;作用 1：返回 this 指针，上面介绍的 &lt;code&gt;shared_from_this()&lt;/code&gt; 其实就是通过 &lt;code&gt;weak_ptr&lt;/code&gt; 返回的 this 指针&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Q：&lt;code&gt;shared_from_this()&lt;/code&gt; 是如何实现的？&lt;/p&gt;
&lt;p&gt;A：使用 &lt;code&gt;shared_from_this()&lt;/code&gt; 的类需要继承 &lt;code&gt;enable_shared_from_this&lt;/code&gt; 类，&lt;code&gt;enable_shared_from_this&lt;/code&gt; 类中持有一个类型为 &lt;code&gt;weak_ptr&lt;/code&gt; 的成员 &lt;code&gt;_M_weak_this&lt;/code&gt;，调用 &lt;code&gt;shared_from_this()&lt;/code&gt; 就是将内部持有的 &lt;code&gt;weak_ptr&lt;/code&gt; 转成了 &lt;code&gt;shared_ptr&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class enable_shared_from_this
{
    shared_ptr&amp;#x3C;const _Tp&gt; shared_from_this() const
    {
        return shared_ptr&amp;#x3C;const _Tp&gt;(this-&gt;_M_weak_this);
    }

    mutable weak_ptr&amp;#x3C;_Tp&gt; _M_weak_this;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;作用 2：解决循环引用问题&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class A {
    std::shared_ptr&amp;#x3C;B&gt; bptr;
    
    ~A() {
        cout &amp;#x3C;&amp;#x3C; &quot;A delete&quot; &amp;#x3C;&amp;#x3C; endl;
    }
    
    void Print() {
        cout &amp;#x3C;&amp;#x3C; &quot;A&quot; &amp;#x3C;&amp;#x3C; endl;
    }
};

class B {
    std::weak_ptr&amp;#x3C;A&gt; aptr; 		// 这里改成 weak_ptr
    
    // B 对象销毁时才调用（即引用计数为 0 时）
    ~B() {
        cout &amp;#x3C;&amp;#x3C; &quot;B delete&quot; &amp;#x3C;&amp;#x3C; endl;
    }
    
    void PrintA() {
        if (!aptr.expired()) { 	// 监视 shared_ptr 的生命周期
            auto ptr = aptr.lock();
            ptr-&gt;Print();
        }
    }
};

int main() {
    auto aaptr = std::make_shared&amp;#x3C;A&gt;();
    auto bbptr = std::make_shared&amp;#x3C;B&gt;();
    aaptr-&gt;bptr = bbptr;
    bbptr-&gt;aptr = aaptr;
    bbptr-&gt;PrintA();
    return 0;
}

// 输出：
// A
// A delete
// B delete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🔥 代码解释：尽管局部变量的析构顺序是按照后进先出的原则，但&lt;strong&gt;关键在于“对象的销毁时机”是由引用计数决定的，而不是直接由局部变量析构的顺序决定的&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;局部变量析构顺序&lt;/strong&gt;：在 main 函数中，aaptr 先创建、bbptr 后创建，因此在退出作用域时，bbptr 会先析构，随后 aaptr 析构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;引用计数的影响&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;创建时，aaptr 持有 A 对象，bbptr 持有 B 对象。&lt;/li&gt;
&lt;li&gt;A 对象内部的成员变量 bptr 又持有 B 对象的 shared_ptr，因此 B 对象的引用计数为 2。&lt;/li&gt;
&lt;li&gt;B 对象内部的 weak_ptr 不会影响 A 对象的引用计数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;析构过程&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;当 bbptr 析构时，仅仅减少了 B 对象的引用计数，从 2 变为 1，但 B 对象并没有被销毁，因为 &lt;code&gt;aaptr-&gt;bptr&lt;/code&gt; 仍然持有它。&lt;/li&gt;
&lt;li&gt;随后 aaptr 析构，导致 A 对象的引用计数从 1 变为 0，从而触发 A 的析构函数，输出 “A delete”。&lt;/li&gt;
&lt;li&gt;在 A 的析构过程中，其成员变量 bptr 被析构，从而使 B 对象的引用计数从 1 减为 0。此时，B 对象的析构函数被调用，输出 “B delete”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3️⃣ unique_ptr 独占式智能指针（替换 &lt;code&gt;auto_ptr&lt;/code&gt;）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;unique_ptr&lt;/code&gt; 是一个独占型的智能指针，它不允许其他的智能指针共享其内部的指针：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不允许通过赋值将一个 &lt;code&gt;unique_ptr&lt;/code&gt; 拷贝/赋值给另外一个 &lt;code&gt;unique_ptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;但是允许通过函数返回给其他的 &lt;code&gt;unique_ptr&lt;/code&gt; 或者通过 &lt;code&gt;std::move&lt;/code&gt; 来转移到其他的 &lt;code&gt;unique_ptr&lt;/code&gt;，&lt;strong&gt;这样的话它本身就不再拥有原指针的所有权了&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与 &lt;code&gt;shared_ptr&lt;/code&gt; 相比，&lt;code&gt;unique_ptr&lt;/code&gt; 除了独占性的特点外，还能够指向一个数组：&lt;code&gt;std::unique_ptr&amp;#x3C;int []&gt; p(new int[10]);&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;shared_ptr&lt;/code&gt; 与 &lt;code&gt;unique_ptr&lt;/code&gt; 的使用需要根据场景决定，如果希望&lt;strong&gt;只有一个智能指针管理资源&lt;/strong&gt;或者&lt;strong&gt;管理数组&lt;/strong&gt;就使用 &lt;code&gt;unique_ptr&lt;/code&gt;，如果希望使用多个智能指针管理同一个资源就使用 &lt;code&gt;shared_ptr&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;🔥 实现简易的 &lt;code&gt;shared_ptr&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;memory&gt;

template&amp;#x3C;typename T&gt;
class smartPtr {
private:
    T *_ptr;
    size_t* _count;

public:
    smartPtr(T *ptr = nullptr):_ptr(ptr) {
        if (_ptr) {
            _count = new size_t(1);
        } else {
            _count = new size_t(0);
        }
    }

    smartPtr(const smartPtr &amp;#x26;ptr) {
        if (this != &amp;#x26;ptr) {
            this-&gt;_ptr = ptr._ptr;
            this-&gt;_count = ptr._count;
            ++(*this-&gt;_count)   ;
        }
    }

    smartPtr&amp;#x26; operator=(const smartPtr &amp;#x26;ptr) {
        if (this-&gt;_ptr == ptr._ptr)
            return *this;

        if (this-&gt;_ptr) {
            --(*this-&gt;_count);
            if (this-&gt;_count == 0) {
                delete this-&gt;_ptr;
                delete this-&gt;_count;
            }
        }

        this-&gt;_ptr = ptr._ptr;
        this-&gt;_count = ptr._count;
        ++(*this-&gt;_count);

        return *this;
    }

    ~smartPtr() {
        --(*this-&gt;_count);
        if (0 == *this-&gt;_count) {
            delete this-&gt;_ptr;
            delete this-&gt;_count;
        }
    }

    size_t use_count() {
        return *this-&gt;_count;
    }

    T&amp;#x26; operator*() {
        assert(this-&gt;_ptr == nullptr);
        return *(this-&gt;_ptr);
    }

    T* operator-&gt;() {
        assert(this-&gt;_ptr == nullptr);
        return this-&gt;_ptr;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;21. shared_ptr 的实现，shared_ptr 一定不会导致内存泄漏吗？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;std::shared_ptr&lt;/code&gt; 的实现基于引用计数，每个 &lt;code&gt;shared_ptr&lt;/code&gt; 实例持有一个指向控制块的指针，控制块中包含引用计数和所管理对象的指针。 当 &lt;code&gt;shared_ptr&lt;/code&gt; 的引用计数降为零时，控制块会删除所管理的对象。 然而，&lt;code&gt;shared_ptr&lt;/code&gt; 并非在所有情况下都能防止内存泄漏。 当存在&lt;strong&gt;循环引用&lt;/strong&gt;时，&lt;code&gt;shared_ptr&lt;/code&gt; 的引用计数永远不会降为零，导致内存无法被释放，从而引发内存泄漏。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;memory&gt;

class A;
class B;

class A {
public:
    std::shared_ptr&amp;#x3C;B&gt; b;
};

class B {
public:
    std::shared_ptr&amp;#x3C;A&gt; a;
};

int main() {
    std::shared_ptr&amp;#x3C;A&gt; a = std::make_shared&amp;#x3C;A&gt;();
    std::shared_ptr&amp;#x3C;B&gt; b = std::make_shared&amp;#x3C;B&gt;();
    a-&gt;b = b;
    b-&gt;a = a;
    // 此时，a 和 b 相互持有对方的 shared_ptr，形成循环引用
    // 程序结束时，a 和 b 的引用计数都不会降为零，导致内存泄漏
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了解决循环引用问题，可以使用 &lt;code&gt;std::weak_ptr&lt;/code&gt;，它是一种不增加引用计数的智能指针。 &lt;code&gt;std::weak_ptr&lt;/code&gt; 用于打破循环引用，避免内存泄漏。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;memory&gt;

class A;
class B;

class A {
public:
    std::weak_ptr&amp;#x3C;B&gt; b; // 使用 weak_ptr 打破循环引用
};

class B {
public:
    std::shared_ptr&amp;#x3C;A&gt; a;
};

int main() {
    std::shared_ptr&amp;#x3C;A&gt; a = std::make_shared&amp;#x3C;A&gt;();
    std::shared_ptr&amp;#x3C;B&gt; b = std::make_shared&amp;#x3C;B&gt;();
    a-&gt;b = b;
    b-&gt;a = a;
    // 此时，a 和 b 之间的循环引用被 weak_ptr 打破
    // 程序结束时，a 和 b 的引用计数会降为零，内存会被正确释放
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者如果在类之间的引用是单向的（即不会形成循环引用），可以考虑使用 &lt;code&gt;std::unique_ptr&lt;/code&gt;。&lt;code&gt;std::unique_ptr&lt;/code&gt; 不会引起引用计数问题，因为它是独占的，每个对象只有一个拥有者。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;memory&gt;

class A;
class B;

class A {
public:
    std::unique_ptr&amp;#x3C;B&gt; b; // 改为 unique_ptr
};

class B {
public:
    std::shared_ptr&amp;#x3C;A&gt; a;
};

int main() {
    std::shared_ptr&amp;#x3C;A&gt; a = std::make_shared&amp;#x3C;A&gt;();
    std::shared_ptr&amp;#x3C;B&gt; b = std::make_shared&amp;#x3C;B&gt;();
    a-&gt;b = std::move(b); // 转移所有权
    // 使用 unique_ptr 的情况下，没有循环引用问题
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;22. STL 中 vector、list、map 的底层原理实现和适用场景？&lt;/h2&gt;
&lt;p&gt;关于 STL 库中所有的结构的底层实现原理：https://zhuanlan.zhihu.com/p/542115773&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;顺带了解了 set、map、unordered_map、unordered_set 之间区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;set&lt;/code&gt;、&lt;code&gt;map&lt;/code&gt;：底层使用红黑树实现，有序，插入、查找、删除的时间复杂度为 $O(logn)$
&lt;ul&gt;
&lt;li&gt;优点：有序性，内部实现红黑树使得很多操作都在 $O(logn)$ 时间复杂度下完成&lt;/li&gt;
&lt;li&gt;缺点：空间占用率高，需要额外保存父节点、孩子节点和红/黑性质&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unordered_set&lt;/code&gt;、&lt;code&gt;unordered_map&lt;/code&gt;：底层使用哈希表实现，无序，查找的时间复杂度为 $O(1)$
&lt;ul&gt;
&lt;li&gt;优点：因为内部实现了哈希表，因此其查找速度非常的快&lt;/li&gt;
&lt;li&gt;缺点：哈希表的建立比较费时&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;1️⃣ vector 动态数组&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vector&lt;/code&gt; 底层是动态数组，元素连续存储在堆上&lt;/li&gt;
&lt;li&gt;自动扩容机制：
&lt;ul&gt;
&lt;li&gt;vector 采用几何增长策略（通常是 2 倍扩容）&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;size() == capacity()&lt;/code&gt; 时，会申请更大的内存空间，然后拷贝旧数据到新空间&lt;/li&gt;
&lt;li&gt;由于 realloc 可能导致数据搬移，&lt;code&gt;push_back()&lt;/code&gt; 的均摊时间复杂度为 $O(1)$，但最坏情况 $O(n)$（扩容时）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;❓所以有可能 vector 的&lt;strong&gt;插入操作可能导致迭代器失效&lt;/strong&gt;：因为 vector 动态增加大小时，并不是在原空间后增加新空间，而是以原大小两倍在开辟另外一片较大空间，然后将内容拷贝过来，并释放原有空间，所以迭代器失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;适用场景：&lt;/p&gt;
&lt;p&gt;✅ 高效的随机访问（&lt;code&gt;O(1)&lt;/code&gt;）。
✅ 批量尾部插入/删除（&lt;code&gt;push_back()&lt;/code&gt;）。
❌ 不适合频繁插入/删除中间元素（&lt;code&gt;O(n)&lt;/code&gt;）。
❌ 扩容会导致数据搬移（不适合超大数据集）。&lt;/p&gt;
&lt;p&gt;2️⃣ list 双向链表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;list&lt;/code&gt; 底层是双向链表，每个节点存储数据和两个指针&lt;/li&gt;
&lt;li&gt;插入和删除操作非常高效，不影响其他元素&lt;/li&gt;
&lt;li&gt;不支持随机访问，必须顺序遍历才能找到某个元素 $O(n)$&lt;/li&gt;
&lt;li&gt;不会发生扩容问题，适合频繁插入/删除的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;适用场景：&lt;/p&gt;
&lt;p&gt;✅ 高效插入/删除（&lt;code&gt;O(1)&lt;/code&gt;，特别是中间位置）。
✅ 不关心随机访问，仅需遍历。
❌ 不适合频繁随机访问（&lt;code&gt;O(n)&lt;/code&gt;）。
❌ 额外的指针开销（内存占用比 &lt;code&gt;vector&lt;/code&gt; 高）。&lt;/p&gt;
&lt;p&gt;3️⃣ map 红黑树&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt; 底层实现是红黑树（Red-Black Tree），一种自平衡二叉搜索树&lt;/li&gt;
&lt;li&gt;key 是有序的&lt;/li&gt;
&lt;li&gt;插入、删除、查找 $O(logn)$，因为树的高度是 $O(logn)$&lt;/li&gt;
&lt;li&gt;迭代遍历按照 key 顺序进行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 操作            | 时间复杂度 | 说明                   |
| --------------- | ---------- | ---------------------- |
| 插入 &lt;code&gt;insert()&lt;/code&gt; | $O(log n)$ | 需要维护红黑树平衡     |
| 删除 &lt;code&gt;erase()&lt;/code&gt;  | $O(log n)$ | 删除节点后可能需要旋转 |
| 查找 &lt;code&gt;find()&lt;/code&gt;   | $O(log n)$ | 通过 BST 进行搜索      |&lt;/p&gt;
&lt;p&gt;适用场景：&lt;/p&gt;
&lt;p&gt;✅ 需要有序存储的数据结构（默认按照 key 递增）。
✅ 需要高效查找、插入、删除（&lt;code&gt;O(log n)&lt;/code&gt;）。
❌ 不适合频繁变更 key（因为 key 作为 BST 节点的一部分）。
❌ 遍历效率比 &lt;code&gt;unordered_map&lt;/code&gt; 低（有序存储开销大）。&lt;/p&gt;
&lt;h2&gt;23. 菱形继承会出现二义性问题，C++ 中如何解决这个问题？&lt;/h2&gt;
&lt;p&gt;❓镜像问题：一个派生类继承两个父类，这两个父类同时有一个共同基类，如果你去调用两个父类的基类对象函数，会有问题吗？怎么解决？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：在 Java 中，&lt;strong&gt;由于 Java 不支持多重继承，所以菱形继承问题也不存在&lt;/strong&gt;。 Java 使用接口来替代多重继承，接口只定义了一些抽象的方法，而没有具体的实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是 C++ 多重继承造成的菱形继承问题，如果一个派生类继承了两个拥有相同基类的父类，&lt;strong&gt;那么基类的成员会被继承两次，这会导致 “二义性问题” 和 “冗余存储”&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503070316671.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;❌ 编译错误！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

class Base {
public:
    void show() { std::cout &amp;#x3C;&amp;#x3C; &quot;Base::show()&quot; &amp;#x3C;&amp;#x3C; std::endl; }
};

class Parent1 : public Base {};  // 继承自 Base
class Parent2 : public Base {};  // 继承自 Base

// 多重继承
class Derived : public Parent1, public Parent2 {};

int main() {
    Derived d;
    d.show();  // ⚠️ 编译错误：二义性
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1️⃣ 解决方案一：使用作用域解析符&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;code&gt;Derived&lt;/code&gt; 仍然包含 &lt;strong&gt;两个 &lt;code&gt;Base&lt;/code&gt; 实例&lt;/strong&gt;，&lt;strong&gt;数据冗余&lt;/strong&gt;，而且每次调用 &lt;code&gt;show()&lt;/code&gt; 需要手动指定作用域，不优雅。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() {
    Derived d;
    d.Parent1::show();  // 访问 Parent1 继承的 Base
    d.Parent2::show();  // 访问 Parent2 继承的 Base
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 使用虚继承｜最佳方案 ✅&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;虚继承是为了让某个类做出声明，承诺愿意共享它的基类，这个被共享的基类就是虚基类&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;多继承除了造成命名冲突，还有数据冗余等问题，为了解决这些问题，C++ 引进了「&lt;strong&gt;虚继承&lt;/strong&gt;」&lt;/p&gt;
&lt;p&gt;这样能够保证 &lt;code&gt;Derived&lt;/code&gt; 只含有一个唯一的 Base 实例。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

class Base {
public:
    void show() { std::cout &amp;#x3C;&amp;#x3C; &quot;Base::show()&quot; &amp;#x3C;&amp;#x3C; std::endl; }
};

// 让 Parent1 和 Parent2 进行虚继承
class Parent1 : virtual public Base {};
class Parent2 : virtual public Base {};

// 继承 Parent1 和 Parent2
class Derived : public Parent1, public Parent2 {};

int main() {
    Derived d;
    d.show();  // ✅ 现在可以直接调用，不会有二义性
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不使用 &lt;code&gt;virtual&lt;/code&gt; 时&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Derived&lt;/code&gt; &lt;strong&gt;会有两个 &lt;code&gt;Base&lt;/code&gt; 对象&lt;/strong&gt;，导致二义性问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存浪费&lt;/strong&gt;（两个 &lt;code&gt;Base&lt;/code&gt; 子对象的冗余）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用 &lt;code&gt;virtual&lt;/code&gt; 继承&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Parent1&lt;/code&gt; 和 &lt;code&gt;Parent2&lt;/code&gt; &lt;strong&gt;不会各自包含 &lt;code&gt;Base&lt;/code&gt; 的副本&lt;/strong&gt;，而是&lt;strong&gt;共享同一个 &lt;code&gt;Base&lt;/code&gt; 实例&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Derived&lt;/code&gt; 只会有一个 &lt;code&gt;Base&lt;/code&gt; 实例&lt;/strong&gt;，所以调用 &lt;code&gt;show()&lt;/code&gt; 时不会有二义性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🔥虚继承是为了让某个类做出声明，承诺愿意共享它的基类，这个被共享的基类就是虚基类！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用虚继承解决菱形继承中的命名冲突问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503070316430.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🔥 虚继承在 C++ 标准库中的实际应用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503070317150.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;再看个虚继承的例子，彻底明白虚继承：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;

class Base0 {
public:
	int var0;
	void fun0() { cout &amp;#x3C;&amp;#x3C; &quot;Member of Base0&quot; &amp;#x3C;&amp;#x3C; endl; }
};

class Base1 : virtual public Base0 {
public:
	int var1;
};

class Base2 : virtual public Base0 {
public:
	int var2;
};

class Derived : public Base1, public Base2 {
	//定义派生类Derived 
public:
	int var;
	void fun() {
		cout &amp;#x3C;&amp;#x3C; &quot;Member of Derived&quot; &amp;#x3C;&amp;#x3C; endl;
	}
};

int main() {
	Derived d;
	d.var0 = 2; //直接访问虚基类的数据成员
	d.fun0();   //直接访问虚基类的函数成员
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⁉️将 Base0 类作为它的直接派生类 Base1 和 Base2 的&lt;strong&gt;虚基类&lt;/strong&gt;，即 Base1 虚继承 Base0，Base2 虚继承 Base0。之后 Derived 再继承 Base1 和 Base2，&lt;strong&gt;在 Derived 对象里面就不会存在 Base0 类的双份的成员&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Derived 对象包含着从 Base1 继承的成员和从 Base2 继承的成员，&lt;strong&gt;但是从 Base1 继承的 Base0 成员实际上这个地方放了一个指针，这个指针指向真正的 Base0 成员，Base2 的也是&lt;/strong&gt;。所以实质上从最远的基类继承过来的成员，在最远派生类中只有一份。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503070325788.png&quot; alt=&quot;image-20250307024809735&quot;&gt;&lt;/p&gt;
&lt;h2&gt;24. 动态编译 vs 静态编译，动态链接 vs 静态链接？&lt;/h2&gt;
&lt;p&gt;在编译和链接过程中，我们可以分为以下几个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编译（Compilation）：将源代码 &lt;code&gt;.cpp&lt;/code&gt; 转换为目标文件 &lt;code&gt;.o&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;链接（Linking）：将多个目标文件和库组合成一个可执行文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;(1) 静态编译 vs 动态编译&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态编译（Static Compilation）：所有代码都在 &lt;strong&gt;编译时&lt;/strong&gt; 确定，并编译成完整的 &lt;strong&gt;可执行文件&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;动态编译（Dynamic Compilation）：代码可以在 &lt;strong&gt;运行时动态生成或加载&lt;/strong&gt;，例如 JIT（Just-In-Time）编译。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;(2) 静态链接 vs 动态链接&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态链接（Static Linking）：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编译时&lt;/strong&gt; 将所有 &lt;strong&gt;库的代码&lt;/strong&gt; 直接复制到可执行文件中。&lt;/li&gt;
&lt;li&gt;生成的可执行文件 &lt;strong&gt;较大&lt;/strong&gt;，但不依赖外部动态库。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;动态链接（Dynamic Linking）：
&lt;ul&gt;
&lt;li&gt;运行时&lt;strong&gt;按需加载&lt;/strong&gt;动态库（&lt;code&gt;.so&lt;/code&gt;/&lt;code&gt;.dll&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;可执行文件 &lt;strong&gt;更小&lt;/strong&gt;，可以&lt;strong&gt;更新动态库&lt;/strong&gt;而无需重新编译整个程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;25. 拷贝构造函数与 &lt;code&gt;operator=()&lt;/code&gt; 的区别？&lt;/h2&gt;
&lt;p&gt;在 C++ 中，&lt;strong&gt;拷贝构造函数&lt;/strong&gt; 和 &lt;strong&gt;赋值运算符 (&lt;code&gt;operator=&lt;/code&gt;)&lt;/strong&gt; 主要区别在于 &lt;strong&gt;调用时机和行为&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(1) 拷贝构造函数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;作用：用于创建新对象时，用已有对象进行初始化。&lt;/li&gt;
&lt;li&gt;调用时机：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用已有对象初始化新对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数按值传递参数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数返回对象（优化前的 NRVO）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class MyClass {
public:
    int data;
    MyClass(int d) : data(d) {}
    
    // 拷贝构造函数
    MyClass(const MyClass&amp;#x26; other) {
        data = other.data;
        std::cout &amp;#x3C;&amp;#x3C; &quot;Copy Constructor\n&quot;;
    }
};

int main() {
    MyClass obj1(10);
    MyClass obj2 = obj1;  // 拷贝构造
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;CopyEdit
Copy Constructor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;(2) 赋值运算符 &lt;code&gt;operator=&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;作用：用于 &lt;strong&gt;已有对象之间赋值&lt;/strong&gt;，即一个对象的内容 &lt;strong&gt;被另一个对象替换&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;调用时机：
&lt;ul&gt;
&lt;li&gt;两个已存在对象进行赋值时&lt;/li&gt;
&lt;li&gt;🔥 &lt;code&gt;a = b;&lt;/code&gt; 而不是 &lt;code&gt;MyClass a = b;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class MyClass {
public:
    int data;
    MyClass(int d) : data(d) {}

    // 赋值运算符
    MyClass&amp;#x26; operator=(const MyClass&amp;#x26; other) {
        if (this == &amp;#x26;other) return *this;  // 防止自赋值
        data = other.data;
        std::cout &amp;#x3C;&amp;#x3C; &quot;Assignment Operator\n&quot;;
        return *this;
    }
};

int main() {
    MyClass obj1(10);
    MyClass obj2(20);
    obj2 = obj1;  // 赋值运算符调用
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Assignment Operator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;(3) 主要区别&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;| &lt;strong&gt;对比项&lt;/strong&gt;         | &lt;strong&gt;拷贝构造函数&lt;/strong&gt; | &lt;strong&gt;赋值运算符 (&lt;code&gt;operator=&lt;/code&gt;)&lt;/strong&gt; |
| ------------------ | ---------------- | ---------------------------- |
| &lt;strong&gt;作用&lt;/strong&gt;           | 初始化新对象     | 赋值给已有对象               |
| &lt;strong&gt;调用时机&lt;/strong&gt;       | &lt;code&gt;MyClass a = b;&lt;/code&gt; | &lt;code&gt;a = b;&lt;/code&gt;                     |
| &lt;strong&gt;是否创建新对象&lt;/strong&gt; | ✅ 是             | ❌ 否                         |
| &lt;strong&gt;默认行为&lt;/strong&gt;       | 成员逐一拷贝     | 成员逐一赋值                 |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(4) 特殊情况&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;避免自赋值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if (this == &amp;#x26;other) return *this;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;支持链式赋值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;MyClass&amp;#x26; operator=(const MyClass&amp;#x26; other) {
    this-&gt;data = other.data;
    return *this;
}

obj1 = obj2 = obj3;  // 链式赋值
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;26. 右值引用的主要用途？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;等价于问题：什么情况下会用到右值引用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;右值引用是 C++11 引入的新特性，用于实现移动语义和完美转发：&lt;/p&gt;
&lt;h3&gt;1️⃣ 实现移动语义&lt;/h3&gt;
&lt;p&gt;在传统 C++ 中，对象的赋值和传递通常涉及深拷贝，这会带来性能开销，通过右值引用，可以触发移动构造函数将资源所有权从一个对象转移到另一个对象（将资源从临时对象移动到新对象），无需深拷贝，&lt;strong&gt;避免了不必要的复制和销毁操作&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当一个临时对象或不再使用的资源，需要被高效地“移动”而不是拷贝时，就用到右值引用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::vector&amp;#x3C;int&gt; v1 = {1,2,3};
std::vector&amp;#x3C;int&gt; v2 = std::move(v1); // 此时v1内容转移给v2，避免深拷贝
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 完美转发&lt;/h3&gt;
&lt;p&gt;用于函数模板的完美转发，将参数以原始的形式传递给下一个函数，避免了不必要的复制和类型转换。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;模板中利用万能引用（forwarding reference）配合&lt;code&gt;std::forward&lt;/code&gt;实现任意类型参数的原始性质传递&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template&amp;#x3C;typename T&gt;
void wrapper(T&amp;#x26;&amp;#x26; arg) {
    func(std::forward&amp;#x3C;T&gt;(arg));  // 原样传递arg（左值传左值，右值传右值）
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;针对「完美转发」，请看如下例子&lt;/h3&gt;
&lt;p&gt;假设我们有两个重载的函数 &lt;code&gt;process&lt;/code&gt;，一个接收左值引用，另一个接收右值引用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void process(int&amp;#x26; i) {
    std::cout &amp;#x3C;&amp;#x3C; &quot;左值引用处理: &quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; std::endl;
}

void process(int&amp;#x26;&amp;#x26; i) {
    std::cout &amp;#x3C;&amp;#x3C; &quot;右值引用处理: &quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们希望编写一个模板函数 &lt;code&gt;forwarding&lt;/code&gt;，它能够将传入的参数完美地转发给 &lt;code&gt;process&lt;/code&gt;，即保持参数的左值或右值属性不变。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不使用完美转发的情况&lt;/strong&gt;：如果我们直接在模板函数中调用 &lt;code&gt;process(param)&lt;/code&gt;，无论传入的是左值还是右值，&lt;code&gt;param&lt;/code&gt; 在函数内部都是一个左值，这会导致总是调用接收左值引用的 &lt;code&gt;process&lt;/code&gt; 函数：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template &amp;#x3C;typename T&gt;
// void forwarding(T param) 也是如此，即右值无法传递进去导致参数不匹配
void forwarding(T&amp;#x26;&amp;#x26; param) {
    process(param); // param 被视为左值，即右值无法传递进去导致参数不匹配
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用完美转发的情况&lt;/strong&gt;：为了实现完美转发，我们需要：
&lt;ul&gt;
&lt;li&gt;使用万能引用：在模板参数中使用 &lt;code&gt;T&amp;#x26;&amp;#x26;&lt;/code&gt;，使得函数能够同时接收左值和右值。&lt;/li&gt;
&lt;li&gt;为了解决这个问题，引入了 &lt;code&gt;std::forward&lt;/code&gt;, 将模板函数改成如下形式就可以了, &lt;code&gt;forward&lt;/code&gt; 被称为完美转发，根据参数的类型（左值或右值）进行条件转发，保持其原有的值类别。语义上：&lt;code&gt;数据是左值就转发成左值，右值就转发成右值，哪怕在万能引用中也是如此&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;utility&gt; // std::forward

template &amp;#x3C;typename T&gt;
void forwarding(T&amp;#x26;&amp;#x26; param) {
    process(std::forward&amp;#x3C;T&gt;(param));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试代码：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() {
    int a = 10;
    forwarding(a);        // 传入左值
    forwarding(20);       // 传入右值
    forwarding(std::move(a)); // 将左值转换为右值
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;输出结果：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;左值引用处理: 10
右值引用处理: 20
右值引用处理: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;补充：左值引用（&amp;#x26;）与右值引用（&amp;#x26;&amp;#x26;）&lt;/h3&gt;
&lt;p&gt;在 C++11 中提出了右值引用，作用是为了和左值引用区分开来，其作用是: &lt;code&gt;右值引用限制了其只能接收右值，可以利用这个特性从而提供重载&lt;/code&gt;，这是右值引用有且唯一的特性，限制了接收参数必为右值, 这点常用在 move construct 中，告诉别人这是一个即将消失的对象的引用，可以瓜分我的对象东西，除此之外，右值引用就没有别的特性了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Base{
public:
      Base(const Base&amp;#x26; b){...} //copy construct 
      Base(Base&amp;#x26;&amp;#x26; b){...}      //move construct
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，一个右值引用变量在使用上就变成了左值，已经不再携带其是右引用这样的信息，只是一个左值，这就是引用在c++中特殊而且复杂的一点，&lt;code&gt;引用在 c++ 中是一个特别的类型，因为它的值类型和变量类型不一样, 左值/右值引用变量的值类型都是左值, 而不是左值引用或者右值引用&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int val = 0;
int&amp;#x26; val_left_ref = val;      
int&amp;#x26;&amp;#x26; val_right_ref = 0;

// 引用必须在初始化时绑定到一个有效的对象，且绑定后无法更改
val_left_ref = 0;      // val_left_ref 此时是 int，而不是 int&amp;#x26;
val_right_ref = 0;     // val_right_ref 此时是 int， 而不是 int&amp;#x26;&amp;#x26;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;🔥 补充：万能引用（T&amp;#x26;&amp;#x26;）&lt;/h3&gt;
&lt;p&gt;模板中的 &lt;code&gt;T&amp;#x26;&amp;#x26;&lt;/code&gt; 不同于普通的右值引用，而是万能引用，其既能接收左值又能接收右值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template&amp;#x3C;typename T&gt;
void emplace_back(T&amp;#x26;&amp;#x26; arg) {
}

Class Base {
};

int main() {
    Base a;
    emplace_back(a);      // ok
    emplace_back(Base()); // also ok
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种特性常用在容器元素的增加上，利用传参是左值还是右值进而在生成元素的时候调用 copy construct 还是 move construct，比如说 &lt;code&gt;vector&lt;/code&gt; 的 &lt;code&gt;emplace_back&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;所以为什么需要 &lt;code&gt;std::forwad&lt;/code&gt;？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力，但是引用类型的唯一作用就是限制了接收的类型，&lt;strong&gt;后续使用中都退化成了左值&lt;/strong&gt;，我们希望能够在传递过程中保持它的左值或者右值的属性, 如果不使用 &lt;code&gt;forward&lt;/code&gt;，直接按照下面的方式写就会导致问题。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template &amp;#x3C;typename T&gt;
// void forwarding(T param) 也是如此，即右值无法传递进去导致参数不匹配
void forwarding(T&amp;#x26;&amp;#x26; param) {
    process(param); // param 被视为左值，即右值无法传递进去导致参数不匹配
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以为了解决这个问题引入了 &lt;code&gt;std::forward&lt;/code&gt;，将模板函数改成如下形式，即可实现完美转发：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template &amp;#x3C;typename T&gt;
void forwarding(T&amp;#x26;&amp;#x26; param) {
    process(std::forward&amp;#x3C;T&gt;(param));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;27. C++ 中有哪些锁？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;更多参考：&lt;a href=&quot;https://zhuanlan.zhihu.com/p/641510924&quot;&gt;如何避免死锁&lt;/a&gt;、&lt;a href=&quot;https://interviewguide.cn/notes/03-hunting_job/02-interview/01-01-07-basic.html&quot;&gt;介绍几种经典的锁&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;从种类上分：普通锁、读写锁、递归锁&lt;/li&gt;
&lt;li&gt;从实现上分：互斥锁、自旋锁、信号量、条件变量&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;互斥锁（Mutex）&lt;/h3&gt;
&lt;p&gt;🌟互斥锁是在抢锁失败的情况下&lt;strong&gt;主动放弃 CPU 进入睡眠状态&lt;/strong&gt;直到锁的状态改变时再唤醒，而操作系统负责线程调度，为了实现锁的状态发生改变时唤醒阻塞的线程或者进程，需要把锁交给操作系统管理，所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的，加锁的时间大概 100ns 左右，而实际上互斥锁的一种可能的实现是先自旋一段时间，当自旋的时间超过阀值之后再将线程投入睡眠中，因此在并发运算中使用互斥锁（每次占用锁的时间很短）的效果可能不亚于使用自旋锁。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥锁（Mutex）：用于保护共享资源，确保任一时刻只有一个线程访问资源。&lt;/li&gt;
&lt;li&gt;信号量（Semaphore）：一种特殊的计数器，可以同时允许多个线程访问有限的共享资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;互斥锁相当于信号量初值为 1 的特殊情况；信号量允许多个线程并发访问资源（初值 &gt; 1）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::mutex mtx;

void foo() {
    std::lock_guard&amp;#x3C;std::mutex&gt; lock(mtx);
    // 临界区操作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保护关键资源（如共享变量）&lt;/li&gt;
&lt;li&gt;控制资源的访问量&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;条件锁/条件变量（Condition Variable）&lt;/h3&gt;
&lt;p&gt;🌟互斥锁一个明显的缺点是他只有两种状态：锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足，他常和互斥锁一起使用，以免出现竞态条件。当条件不满足时，线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量，他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。&lt;strong&gt;总的来说互斥锁是线程间互斥的机制，条件变量则是同步机制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;条件变量用于线程间通信，当某个条件满足后再唤醒等待线程。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;mutex&gt;
#include &amp;#x3C;condition_variable&gt;

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void wait_thread() {
    std::unique_lock&amp;#x3C;std::mutex&gt; lock(mtx);
    cv.wait(lock, [](){ return ready; });  // 等待条件满足
    // 执行后续任务
}

void signal_thread() {
    {
        std::lock_guard&amp;#x3C;std::mutex&gt; lock(mtx);
        ready = true;  // 修改条件
    }
    cv.notify_one();  // 通知等待线程
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生产者-消费者模型&lt;/li&gt;
&lt;li&gt;线程等待某条件满足才能执行&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;自旋锁（Spin Lock）&lt;/h3&gt;
&lt;p&gt;🌟如果线程无法取得锁，线程不会立刻放弃 CPU 时间片，而是一直循环尝试获取锁，直到获取为止。如果别的线程长时期占有锁那么自旋就是在浪费 CPU 做无用功，但是自旋锁一般应用于加锁时间很短的场景，这个时候效率比较高。&lt;/p&gt;
&lt;p&gt;线程在等待资源时不会挂起或睡眠，而是不断循环检测锁状态（&lt;strong&gt;忙等待&lt;/strong&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;atomic&gt;

class SpinLock {
    std::atomic_flag lock_ = ATOMIC_FLAG_INIT;
public:
    void lock() {
        while (lock_.test_and_set(std::memory_order_acquire));
    }
    void unlock() {
        lock_.clear(std::memory_order_release);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;临界区非常短小&lt;/li&gt;
&lt;li&gt;多核 CPU、短暂等待资源的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;读写锁（Read-Write Lock）&lt;/h3&gt;
&lt;p&gt;允许多个线程同时进行读操作，但写操作必须独占访问。&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读锁共享：多个读线程并发执行&lt;/li&gt;
&lt;li&gt;写锁独占：写线程执行时不能有其他读、写线程存在&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// C++17 的 shared_mutex
#include &amp;#x3C;shared_mutex&gt;

std::shared_mutex rw_mutex;
int shared_data = 0;

void reader() {
    std::shared_lock&amp;#x3C;std::shared_mutex&gt; lock(rw_mutex);
    // 读取shared_data
}

void writer() {
    std::unique_lock&amp;#x3C;std::shared_mutex&gt; lock(rw_mutex);
    shared_data++;  // 写操作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应用场景：大量读、少量写的场景（如配置文件读取，缓存数据等）&lt;/p&gt;
&lt;h3&gt;递归锁（Recursive Mutex）&lt;/h3&gt;
&lt;p&gt;同一线程可以多次获取同一个锁，但必须释放相同次数后才完全解锁。&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;避免了同一线程递归调用中因反复加锁而引起的死锁问题&lt;/li&gt;
&lt;li&gt;相比普通锁，多了一些额外开销&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;mutex&gt;

std::recursive_mutex r_mutex;

void recursive_function(int n) {
    std::lock_guard&amp;#x3C;std::recursive_mutex&gt; lock(r_mutex);
    if (n &gt; 0) {
        recursive_function(n - 1); // 递归调用
    }
    // 临界区操作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应用场景：函数递归调用或函数间的相互调用都可能再次尝试获取同一锁&lt;/p&gt;
&lt;h2&gt;28. 如何用 C++ 实现一个读写锁&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;mutex&gt;
#include &amp;#x3C;condition_variable&gt;

class RWLock {
private:
    std::mutex mtx_;
    std::condition_variable cv_;
    int readers_;          // 正在读取的线程数量
    int writers_waiting_;  // 等待写入的线程数量
    bool writing_;         // 当前是否有写线程

public:
    RWLock() : readers_(0), writers_waiting_(0), writing_(false) {}

    // 读锁定
    void lock_read() {
        std::unique_lock&amp;#x3C;std::mutex&gt; lock(mtx_);
        // 当有写操作进行中或等待中的写操作时等待
        cv_.wait(lock, [this]() {
            return !writing_ &amp;#x26;&amp;#x26; writers_waiting_ == 0;
        });
        ++readers_;
    }

    // 读解锁
    void unlock_read() {
        std::unique_lock&amp;#x3C;std::mutex&gt; lock(mtx_);
        if (--readers_ == 0) {
            cv_.notify_all();
        }
    }

    // 写锁定
    void lock_write() {
        std::unique_lock&amp;#x3C;std::mutex&gt; lock(mtx_);
        ++writers_waiting_;
        cv_.wait(lock, [this]() { return !writing_ &amp;#x26;&amp;#x26; readers_ == 0; });
        --writers_waiting_;
        writing_ = true;
    }

    // 写解锁
    void unlock_write() {
        std::unique_lock&amp;#x3C;std::mutex&gt; lock(mtx_);
        writing_ = false;
        cv_.notify_all();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用实例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;thread&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;chrono&gt;

RWLock rwlock;
int shared_data = 0;

void reader(int id) {
    rwlock.lock_read();
    std::cout &amp;#x3C;&amp;#x3C; &quot;Reader &quot; &amp;#x3C;&amp;#x3C; id &amp;#x3C;&amp;#x3C; &quot; reads value: &quot; &amp;#x3C;&amp;#x3C; shared_data &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    rwlock.unlock_read();
}

void writer(int id) {
    rwlock.lock_write();
    ++shared_data;
    std::cout &amp;#x3C;&amp;#x3C; &quot;Writer &quot; &amp;#x3C;&amp;#x3C; id &amp;#x3C;&amp;#x3C; &quot; updated value to: &quot; &amp;#x3C;&amp;#x3C; shared_data &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    std::this_thread::sleep_for(std::chrono::milliseconds(150));
    rwlock.unlock_write();
}

int main() {
    std::vector&amp;#x3C;std::thread&gt; threads;

    // 启动读线程
    for (int i = 0; i &amp;#x3C; 5; ++i) {
        threads.emplace_back(reader, i);
    }

    // 启动写线程
    for (int i = 0; i &amp;#x3C; 3; ++i) {
        threads.emplace_back(writer, i);
    }

    for (auto &amp;#x26;t : threads) {
        t.join();
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;以上实现倾向于&lt;strong&gt;写优先&lt;/strong&gt;（有写操作等待时，不允许新的读操作）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以通过修改逻辑实现读优先或公平性策略，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;去除 &lt;code&gt;writers_waiting_ == 0&lt;/code&gt; 的约束实现读优先&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;更复杂的公平策略则需要额外的数据结构管理等待顺序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实际应用中，推荐使用现有的成熟实现，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C++17 起的标准库提供的 &lt;code&gt;std::shared_mutex&lt;/code&gt;（标准的读写锁实现）：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;shared_mutex&gt;

std::shared_mutex rw_mutex;

void reader() {
    std::shared_lock lock(rw_mutex); // 读锁
    // 读取数据
}

void writer() {
    std::unique_lock lock(rw_mutex); // 写锁
    // 修改数据
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;29. 引用和指针的区别，是否能加 &lt;code&gt;const&lt;/code&gt;，作用是什么？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;指针&lt;/strong&gt;：存储变量的内存地址，可以为空（&lt;code&gt;nullptr&lt;/code&gt;），需要通过解引用操作符&lt;code&gt;*&lt;/code&gt;访问指针指向的值。指针可以在运行时重新指向不同的对象。指针可以有多级。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;引用&lt;/strong&gt;：是变量的别名，必须在初始化时绑定到一个有效的对象，且绑定后无法更改。引用不能为空，始终指向初始化时绑定的对象。引用只有一级。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const&lt;/code&gt;修饰：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指针&lt;/strong&gt;：&lt;code&gt;const&lt;/code&gt;可以修饰指针本身或指针指向的对象。
&lt;ul&gt;
&lt;li&gt;指向常量的指针：&lt;code&gt;const int* ptr&lt;/code&gt; / &lt;code&gt;int const* ptr&lt;/code&gt; 表示指针指向的值是常量，不能通过该指针修改值，但可以改变指针本身的指向。&lt;/li&gt;
&lt;li&gt;常量指针：&lt;code&gt;int* const ptr&lt;/code&gt;表示指针本身是常量，不能改变指针的指向，但可以通过指针修改指向的值。&lt;/li&gt;
&lt;li&gt;指向常量的常量指针：&lt;code&gt;const int* const ptr&lt;/code&gt;表示指针本身和指针指向的值都是常量，既不能修改指针的指向，也不能修改指向的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;引用&lt;/strong&gt;：&lt;strong&gt;引用本身不能是常量&lt;/strong&gt;，但可以引用一个常量对象。
&lt;ul&gt;
&lt;li&gt;指向常量的引用：&lt;code&gt;const int&amp;#x26; ref&lt;/code&gt;表示引用绑定到一个常量值，不能通过该引用修改值。常量引用常用于函数参数，允许函数接受常量或非常量实参而不进行拷贝。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;30. 哈希桶满了怎么办？&lt;/h2&gt;
&lt;p&gt;哈希表（如 &lt;code&gt;unordered_map&lt;/code&gt;）在插入元素后，如果负载因子（&lt;code&gt;load_factor&lt;/code&gt;，即元素个数/桶数量）超过阈值（通常是1.0左右），将触发&lt;strong&gt;扩容（rehash）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重新分配更多的 bucket（一般是原来容量的2倍或更多）。&lt;/li&gt;
&lt;li&gt;重新计算元素位置（rehash），将原有元素重新插入新的 bucket 中。&lt;/li&gt;
&lt;li&gt;扩容时性能开销较大 $O(n)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，为了减少扩容次数，可以提前使用 &lt;code&gt;reserve&lt;/code&gt; 或 &lt;code&gt;rehash&lt;/code&gt; 提高效率。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;unordered_map&amp;#x3C;int, int&gt; umap;
umap.reserve(1000); // 提前预留空间，避免频繁扩容
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;31. AVL vs. 红黑树&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;AVL 树&lt;/strong&gt;（严格平衡二叉树）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;左右子树高度差&lt;strong&gt;绝对不能超过1&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;插入删除频繁时，旋转调整成本较高（严格的平衡限制）。&lt;/li&gt;
&lt;li&gt;查询效率略优于红黑树（更平衡），但插入删除的开销稍高。&lt;/li&gt;
&lt;li&gt;适用于对查询操作要求极高，但修改频率较低的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;红黑树&lt;/strong&gt;（弱平衡二叉树）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;平衡规则相对宽松，允许一定的高度差异。&lt;/li&gt;
&lt;li&gt;插入删除操作旋转调整较少，综合效率更高。&lt;/li&gt;
&lt;li&gt;广泛应用于 C++ 中的 STL &lt;code&gt;map&lt;/code&gt;、&lt;code&gt;set&lt;/code&gt; 等数据结构中。&lt;/li&gt;
&lt;li&gt;更适用于&lt;strong&gt;插入删除较频繁的场景&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果场景&lt;strong&gt;读多写少&lt;/strong&gt;，要求非常严格的平衡，AVL 树适合。&lt;/p&gt;
&lt;p&gt;如果场景&lt;strong&gt;写操作频繁&lt;/strong&gt;，对读写整体性能要求更均衡，红黑树更合适。&lt;/p&gt;
&lt;h2&gt;32. &lt;code&gt;move()&lt;/code&gt; 底层原理&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;std::move()&lt;/code&gt; 的底层原理实际上非常简单，它本身&lt;strong&gt;并不真正执行移动&lt;/strong&gt;，而是一个类型转换工具，用来将&lt;strong&gt;左值（lvalue）强制转换为右值引用（rvalue reference）&lt;/strong&gt;，从而允许移动语义发生。&lt;/p&gt;
&lt;h3&gt;一、源码分析（典型实现）&lt;/h3&gt;
&lt;p&gt;在C++标准库中，&lt;code&gt;std::move()&lt;/code&gt; 一般可实现为如下模板函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template &amp;#x3C;typename T&gt;
constexpr std::remove_reference_t&amp;#x3C;T&gt;&amp;#x26;&amp;#x26; move(T&amp;#x26;&amp;#x26; arg) noexcept {
    return static_cast&amp;#x3C;std::remove_reference_t&amp;#x3C;T&gt;&amp;#x26;&amp;#x26;&gt;(arg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码可以解析为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T&amp;#x26;&amp;#x26; arg&lt;/code&gt;：这是一个&lt;strong&gt;万能引用&lt;/strong&gt;（forwarding reference），能够绑定到左值或右值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remove_reference_t&amp;#x3C;T&gt;&lt;/code&gt;：移除模板参数 &lt;code&gt;T&lt;/code&gt; 可能带有的引用限定符，保证返回的确实是一个右值引用类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static_cast&amp;#x3C;remove_reference_t&amp;#x3C;T&gt;&amp;#x26;&amp;#x26;&gt;&lt;/code&gt;：进行强制类型转换，将传入参数从左值转换为右值引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二、原理分析&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;std::move()&lt;/code&gt; 本身没有发生移动动作，它只是一个&lt;strong&gt;类型转换工具&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;转换前&lt;/strong&gt;：变量（对象）本身是&lt;strong&gt;左值&lt;/strong&gt;，只能调用拷贝构造函数或拷贝赋值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;转换后&lt;/strong&gt;：变量变为&lt;strong&gt;右值引用&lt;/strong&gt;，具备调用移动构造函数或移动赋值的资格。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本质是告诉编译器：“&lt;strong&gt;这里的对象我不再需要了，可以放心进行资源的移动操作。&lt;/strong&gt;”&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::string str1 = &quot;Hello&quot;;
std::string str2 = std::move(str1);  
// str1 的内容被“窃取”，str2 可能直接接管内部缓冲区，而非复制
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三、实际的“移动”如何发生？&lt;/h3&gt;
&lt;p&gt;实际的移动（资源转移）是通过被调用对象的&lt;strong&gt;移动构造函数&lt;/strong&gt;或&lt;strong&gt;移动赋值运算符&lt;/strong&gt;实现的，而不是通过&lt;code&gt;std::move()&lt;/code&gt;实现：&lt;/p&gt;
&lt;p&gt;例如，&lt;code&gt;std::string&lt;/code&gt; 的移动构造函数的伪代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 移动构造函数示意
string(string&amp;#x26;&amp;#x26; other) noexcept {
    data_ = other.data_;
    size_ = other.size_;
    other.data_ = nullptr;  // 原对象失去所有权
    other.size_ = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;std::move()&lt;/code&gt; 提供右值引用，而真正资源转移的逻辑，由类的移动构造或移动赋值完成。&lt;/p&gt;
&lt;h3&gt;四、注意事项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::move()&lt;/code&gt;不会清空对象&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;调用&lt;code&gt;std::move()&lt;/code&gt;后的对象处于有效但&lt;strong&gt;未指定状态&lt;/strong&gt;（valid but unspecified state），通常对象变为空或默认状态。&lt;/li&gt;
&lt;li&gt;你可以继续赋值或析构，但不应该继续访问对象原先的资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;移动语义要求类本身支持移动构造或移动赋值&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;若类本身未定义移动构造或移动赋值，调用&lt;code&gt;std::move()&lt;/code&gt; 仍然可能降级成拷贝。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 问题                       | 结论                             |
| -------------------------- | -------------------------------- |
| &lt;code&gt;std::move()&lt;/code&gt;本质是什么？  | 类型转换函数，从左值转为右值引用 |
| 真正的移动操作在哪里发生？ | 类的移动构造函数或移动赋值运算符 |
| 调用后原对象的状态？       | 有效但未指定                     |&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::move()&lt;/code&gt; 本身几乎没有开销，它只是一个编译期的类型转换工具，真正的开销和行为由类型本身的移动构造和赋值函数决定。&lt;/p&gt;
&lt;h2&gt;33. 可执行文件加载到内存里，其内存布局是怎样的？&lt;/h2&gt;
&lt;p&gt;当可执行文件（如Linux ELF格式）加载到内存中运行时，其典型内存布局为：&lt;/p&gt;
&lt;p&gt;从低地址到高地址顺序：&lt;/p&gt;
&lt;p&gt;| 内存段                     | 功能说明                               |
| -------------------------- | -------------------------------------- |
| &lt;strong&gt;代码段（text segment）&lt;/strong&gt; | 存放程序的机器指令（只读、可执行）     |
| &lt;strong&gt;数据段（data segment）&lt;/strong&gt; | 已初始化的全局变量和静态变量           |
| &lt;strong&gt;BSS段（bss segment）&lt;/strong&gt;   | 未初始化或初值为零的全局变量和静态变量 |
| &lt;strong&gt;堆（Heap）&lt;/strong&gt;             | 动态分配的内存（由低地址向高地址增长） |
| ↕️                          | （动态增长空间）                       |
| &lt;strong&gt;栈（Stack）&lt;/strong&gt;            | 函数调用栈帧（由高地址向低地址增长）   |&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503260012924.png&quot; alt=&quot;虚拟内存空间划分&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;代码段&lt;/strong&gt;：函数指令&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据段&lt;/strong&gt;：全局或静态变量（初值不为0）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BSS段&lt;/strong&gt;：全局或静态变量（初值为0或未初始化）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆段&lt;/strong&gt;：动态内存（malloc/new）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;栈段&lt;/strong&gt;：函数调用的局部变量、调用返回地址、临时变量等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件映射段&lt;/strong&gt;：包括动态库、共享内存等，从低地址开始向上增长（跟硬件和内核版本有关）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;34. 宏定义与函数的区别？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;宏在预处理阶段完成替换&lt;/strong&gt;，之后被替换的文本参与编译，相当于直接插入了代码，运行时不存在函数调用，执行起来更快；函数调用在运行时需要跳转到具体调用函数。&lt;/li&gt;
&lt;li&gt;宏定义属于在结构中插入代码，没有返回值；函数调用具有返回值。&lt;/li&gt;
&lt;li&gt;宏定义参数没有类型，不进行类型检查；函数参数具有类型，需要检查类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;35. 宏定义 &lt;code&gt;define&lt;/code&gt; 与 &lt;code&gt;typedef&lt;/code&gt; 的区别？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;宏主要用于定义常量及书写复杂的内容；typedef 主要用于定义类型别名。&lt;/li&gt;
&lt;li&gt;宏替换发生在编译阶段之前（&lt;strong&gt;预处理阶段&lt;/strong&gt;），属于文本插入替换；typedef 是&lt;strong&gt;编译&lt;/strong&gt;的一部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;宏不检查类型；typedef 会检查数据类型&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;注意对指针的操作，&lt;code&gt;typedef char * p_char&lt;/code&gt; 和 &lt;code&gt;#define p_char char *&lt;/code&gt; 区别巨大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;36. 变量声明与定义的区别？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;声明&lt;/strong&gt;仅仅是把变量的声明的位置及类型提供给编译器，并不分配内存空间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;要在定义的地方为其分配存储空间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相同变量可以在多处声明（外部变量 &lt;code&gt;extern&lt;/code&gt;），但只能在一处定义。&lt;/p&gt;
&lt;h2&gt;37. strlen 和 sizeof 的区别？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;sizeof 参数可以是任何数据的类型或者数据（sizeof 参数不退化）&lt;/li&gt;
&lt;li&gt;strlen 参数只能是字符指针且结尾是&apos;\0&apos;的字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() {
    const char* str = &quot;Hello World&quot;;
    cout &amp;#x3C;&amp;#x3C; sizeof(str) &amp;#x3C;&amp;#x3C; endl;    // 指针字节：8
    cout &amp;#x3C;&amp;#x3C; strlen(str) &amp;#x3C;&amp;#x3C; endl;    // 字符串长度(不包含&apos;\0&apos;)：11
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;38. final 和 override&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;override&lt;/code&gt;：指定了子类的这个虚函数是重写的父类的，如果你名字不小心打错了的话，编译器是不会编译通过的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final&lt;/code&gt;：当某个类不希望被继承，或者某个虚函数不希望被重写，那么可以在类名和虚函数后添加 &lt;code&gt;final&lt;/code&gt; 关键字，添加 final 关键字后被继承或重写，编译器会报错&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Base {
    virtual void foo();
};
 
class A : public Base {
    void foo() final; // foo 被 override 并且是最后一个 override，在其子类中不可以重写
};

// 指明 B 是不可以被继承的
class B final : public A {
    void foo() override; // Error: 在 A 中已经被 final 了
};

// Error: B is final
class C : public B {
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;39. C 与 C++ 的类型安全&lt;/h2&gt;
&lt;p&gt;类型安全很大程度上可以等价于内存安全，类型安全的代码不会试图访问自己没被授权的内存区域。&lt;strong&gt;“类型安全”常被用来形容编程语言，其根据在于该门编程语言是否提供保障类型安全的机制&lt;/strong&gt;；有的时候也用“类型安全”形容某个程序，判别的标准在于该程序是否隐含类型错误。&lt;/p&gt;
&lt;p&gt;类型安全的编程语言与类型安全的程序之间，没有必然联系。好的程序员可以使用类型不那么安全的语言写出类型相当安全的程序，相反的，差一点儿的程序员可能使用类型相当安全的语言写出类型不太安全的程序。绝对类型安全的编程语言暂时还没有。&lt;/p&gt;
&lt;h3&gt;C 的类型安全&lt;/h3&gt;
&lt;p&gt;C 只在局部上下文中表现出类型安全，比如试图从一种结构体的指针转换成另一种结构体的指针时，编译器将会报告错误，除非使用显式类型转换。然而，C 中相当多的操作是不安全的。以下是两个十分常见的例子：&lt;/p&gt;
&lt;p&gt;1️⃣ printf 格式输出：下述代码中，使用 &lt;code&gt;%d&lt;/code&gt; 控制整型数字的输出，没有问题，但是改成 &lt;code&gt;%f&lt;/code&gt; 时，明显输出错误，再改成 &lt;code&gt;%s&lt;/code&gt; 时，运行直接报 &lt;strong&gt;segmentation fault 错误&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
int main() {
    printf(&quot;%d\n&quot;, 10);	// 10
    printf(&quot;%f\n&quot;, 10);	// 0.00
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ malloc 函数返回值：&lt;code&gt;malloc&lt;/code&gt; 是 C 中进行内存分配的函数，它的返回类型是 &lt;code&gt;void*&lt;/code&gt; 即空类型指针，常常有这样的用法 &lt;code&gt;char* pStr = (char*)malloc(100 * sizeof(char))&lt;/code&gt;，&lt;strong&gt;这里明显做了显式的类型转换&lt;/strong&gt;。类型匹配尚且没有问题，但是一旦出现 &lt;code&gt;int* pInt = (int*)malloc(100 * sizeof(char))&lt;/code&gt; 就很可能带来一些问题，而这样的转换 C 并不会提示错误。&lt;/p&gt;
&lt;h3&gt;C++ 类型安全&lt;/h3&gt;
&lt;p&gt;如果 C++ 使用得当，它将远比 C 更有类型安全性。相比于 C 语言，C++ 提供了一些新的机制保障类型安全：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作符 new 返回的指针类型严格与对象匹配，而不是 &lt;code&gt;void*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;C 中很多以 &lt;code&gt;void*&lt;/code&gt; 为参数的函数可以改写为 C++ 模板函数，而模板是支持类型检查的&lt;/li&gt;
&lt;li&gt;引入 &lt;code&gt;const&lt;/code&gt; 关键字代替 &lt;code&gt;#define constants&lt;/code&gt;，它是有类型、有作用域的，而 &lt;code&gt;#define constants&lt;/code&gt; 只是简单的文本替换&lt;/li&gt;
&lt;li&gt;一些 &lt;code&gt;#define&lt;/code&gt; 宏可被改写为 &lt;code&gt;inline&lt;/code&gt; 函数，结合函数的重载，可在类型安全的前提下支持多种类型，当然改写为模板也能保证类型安全&lt;/li&gt;
&lt;li&gt;C++ 提供了 &lt;code&gt;dynamic_cast&lt;/code&gt; 关键字，使得转换过程更加安全，因为 &lt;code&gt;dynamic_cast&lt;/code&gt; 比 &lt;code&gt;static_cast&lt;/code&gt; 涉及更多具体的类型检查&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;40. 内联函数 &lt;code&gt;inline&lt;/code&gt; 和宏定义 &lt;code&gt;define&lt;/code&gt; 的区别？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在使用时，宏只做简单字符串替换（预处理，即编译前）；而内联函数可以进行参数类型检查（编译时），且具有返回值&lt;/li&gt;
&lt;li&gt;内联函数在编译时直接将函数代码嵌入到目标代码中，&lt;strong&gt;省去函数调用的开销来提高执行效率&lt;/strong&gt;，并且进行参数类型检查，具有返回值，可以实现重载&lt;/li&gt;
&lt;li&gt;宏定义时要注意书写（参数要括起来）否则容易出现歧义，内联函数不会产生歧义&lt;/li&gt;
&lt;li&gt;内联函数有类型检测、语法判断等功能，而宏没有&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;内联函数适用场景:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用宏定义的地方都可以使用 inline 函数&lt;/li&gt;
&lt;li&gt;作为类成员接口函数来读写类的私有成员或者保护成员，会提高效率&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;41. 什么是大小端存储，以及如何用代码判断大小端？&lt;/h2&gt;
&lt;p&gt;大端存储：字数据的高字节存储在低地址中&lt;/p&gt;
&lt;p&gt;小端存储：字数据的低字节存储在低地址中&lt;/p&gt;
&lt;p&gt;例如：32bit 的数字 0x12345678&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以在 Socket 编程中，往往需要将操作系统所用的小端存储的 IP 地址转换为大端存储，这样才能进行网络传输&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;小端模式中的存储方式为&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503260131145.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;大端模式中的存储方式为&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503260132626.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;了解了大小端存储的方式，如何在代码中进行判断呢？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;
int main()
{
    int a = 0x1234;
    //由于 int 和 char 的长度不同，借助 int 型转换成 char 型，只会留下低地址的部分
    char c = (char)(a);
    if (c == 0x12)
        cout &amp;#x3C;&amp;#x3C; &quot;big endian&quot; &amp;#x3C;&amp;#x3C; endl;
    else if(c == 0x34)
        cout &amp;#x3C;&amp;#x3C; &quot;little endian&quot; &amp;#x3C;&amp;#x3C; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;42. C++ 中有几种类型的 &lt;code&gt;new&lt;/code&gt;？&lt;/h2&gt;
&lt;h3&gt;(1) plain new&lt;/h3&gt;
&lt;p&gt;言下之意就是普通的new，就是我们常用的new，在C++中定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void *) throw();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此 &lt;strong&gt;plain new&lt;/strong&gt; 在空间分配失败的情况下，抛出异常 &lt;strong&gt;std::bad_alloc&lt;/strong&gt; 而不是返回 NULL，因此通过判断返回值是否为 NULL 是徒劳的。&lt;/p&gt;
&lt;h3&gt;(2) nothrow new&lt;/h3&gt;
&lt;p&gt;nothrow new 在空间分配失败的情况下是不抛出异常，而是返回 NULL，定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void * operator new(std::size_t, const std::nothrow_t&amp;#x26;) throw();
void operator delete(void*) throw();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;(3) placement new&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;字节校招问题：&lt;strong&gt;placement new&lt;/strong&gt; 是什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一般来说，使用 new 申请空间时，是从系统的“堆”中分配空间。申请所得的空间的位置是根据当时的内存的实际使用情况决定的。但是，在某些特殊情况下，&lt;strong&gt;可能需要在已分配的特定内存创建对象&lt;/strong&gt;，这就是所谓的 “定位放置 new” （&lt;code&gt;placement new&lt;/code&gt;）操作。 定位放置 new 操作的语法形式不同于普通的 new 操作。例如，一般都用如下语句 &lt;code&gt;A* p = new A;&lt;/code&gt; 申请空间，而 placement new 操作则使用如下语句 &lt;code&gt;A* p = new (ptr)A;&lt;/code&gt; 申请空间，其中 &lt;code&gt;ptr&lt;/code&gt; 就是程序员指定的&lt;strong&gt;内存首地址&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;用定位放置 new 操作，既可以在栈（stack）上生成对象，也可以在堆（heap）上生成对象，如本例就是在栈上生成一个对象。&lt;/li&gt;
&lt;li&gt;优势：&lt;strong&gt;复用已有内存空间&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;场景题：如果有这样一个场景，我们需要大量的申请一块类似的内存空间，然后又释放掉，比如在一个 Server 中对于客户端的请求，每个客户端的每一次上行数据我们都需要为此申请一块内存，当我们处理完请求给客户端下行回复时释放掉该内存，表面上看者符合 C++ 的内存管理要求，没有什么错误，但是仔细想想很不合理，&lt;strong&gt;为什么我们每个请求都要重新申请一块内存呢，要知道每一次内存的申请，系统都要在内存中找到一块合适大小的连续的内存空间，这个过程是很慢的（相对而言)，极端情况下，如果当前系统中有大量的内存碎片，并且我们申请的空间很大，甚至有可能失败。为什么我们不能共用一块我们事先准备好的内存呢？可以的，我们可以使用 &lt;code&gt;placement new&lt;/code&gt; 来构造对象，那么就会在我们指定的内存空间中构造对象。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;这种 new 允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new 不用担心内存分配失败，因为它根本不分配内存，它做的唯一一件事情就是调用对象的构造函数。定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void* operator new(size_t, void*);
void operator delete(void*, void*);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;placement new&lt;/code&gt; 需要注意两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;palcement new 的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组&lt;/li&gt;
&lt;li&gt;placement new 构造起来的对象数组，要显式的调用他们的析构函数来销毁（析构函数并不释放对象的内存），千万不要使用 &lt;code&gt;delete&lt;/code&gt;，这是因为 placement new 构造起来的对象或数组大小并不一定等于原来分配的内存大小，使用 delete 会造成内存泄漏或者之后释放内存时出现运行时错误&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;
using namespace std;

class ADT {
	int i;
	int j;
public:
	ADT() {
		i = 10;
		j = 100;
		cout &amp;#x3C;&amp;#x3C; &quot;ADT construct i=&quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot; j=&quot;&amp;#x3C;&amp;#x3C;j &amp;#x3C;&amp;#x3C;endl;
	}
	~ADT() {
		cout &amp;#x3C;&amp;#x3C; &quot;ADT destruct&quot; &amp;#x3C;&amp;#x3C; endl;
	}
};

int main() {
	char *p = new(nothrow) char[sizeof ADT + 1];
	if (p == NULL) {
		cout &amp;#x3C;&amp;#x3C; &quot;alloc failed&quot; &amp;#x3C;&amp;#x3C; endl;
	}
	ADT *q = new(p) ADT;  //placement new:不必担心失败，只要p所指对象的的空间足够ADT创建即可
	//delete q;//错误!不能在此处调用delete q;
	q-&gt;ADT::~ADT();//显示调用析构函数
	delete[] p;
	return 0;
}

//ADT construct i=10 j=100
//ADT destruct
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;43. C++ 11 新特性有哪些？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自动类型推导（&lt;code&gt;auto&lt;/code&gt; 关键字）&lt;/strong&gt;：编译器可根据变量初始化表达式自动推导其类型，简化代码编写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;decltype&lt;/code&gt; 关键字&lt;/strong&gt;：用于获取表达式的类型，常与 &lt;code&gt;auto&lt;/code&gt; 结合使用，以推导复杂类型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;右值引用和移动语义、&lt;code&gt;move&lt;/code&gt; 函数&lt;/strong&gt;：通过右值引用（&lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt;）支持移动构造和移动赋值，提高资源管理和程序性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;初始化列表&lt;/strong&gt;：引入统一的列表初始化语法，允许使用花括号对变量进行初始化，增强初始化的灵活性和可读性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;nullptr&lt;/code&gt; 关键字&lt;/strong&gt;：引入新的空指针常量，替代原有的 &lt;code&gt;NULL&lt;/code&gt;，提高类型安全性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强类型枚举（&lt;code&gt;enum class&lt;/code&gt;）&lt;/strong&gt;：提供作用域限定的枚举类型，避免与其他标识符冲突，并增强类型安全性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;constexpr&lt;/code&gt; 关键字&lt;/strong&gt;：允许在编译期计算常量表达式，提高程序效率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lambda 表达式&lt;/strong&gt;：支持匿名函数，方便定义内联的回调或操作，简化代码结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;范围 &lt;code&gt;for&lt;/code&gt; 循环&lt;/strong&gt;：引入基于范围的 &lt;code&gt;for&lt;/code&gt; 循环，简化对容器或数组的遍历操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;智能指针&lt;/strong&gt;：新增 &lt;code&gt;std::unique_ptr&lt;/code&gt; 和改进的 &lt;code&gt;std::shared_ptr&lt;/code&gt;，提供安全的资源管理机制，减少内存泄漏风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程支持库&lt;/strong&gt;：标准库中加入多线程支持，包括线程管理、互斥量、条件变量等，方便进行并发编程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::tuple&lt;/code&gt;&lt;/strong&gt;：提供固定大小的多元组，允许存储多个不同类型的值，增强数据结构的表达能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;正则表达式库&lt;/strong&gt;：标准库新增正则表达式支持，方便进行字符串匹配和处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::array&lt;/code&gt;&lt;/strong&gt;：提供固定大小的数组封装，结合了数组的性能和容器的功能性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::unordered_map&lt;/code&gt; 和 &lt;code&gt;std::unordered_set&lt;/code&gt;&lt;/strong&gt;：新增无序关联容器，基于哈希表实现，提供平均常数时间的查找和插入性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::chrono&lt;/code&gt; 时间库&lt;/strong&gt;：引入时间处理库，提供时钟、时间点、时间间隔等功能，方便进行时间相关的操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;static_assert&lt;/code&gt;&lt;/strong&gt;：在编译期进行断言检查，确保代码满足特定条件，提高代码的可靠性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::function&lt;/code&gt; 和 &lt;code&gt;std::bind&lt;/code&gt;&lt;/strong&gt;：提供通用的函数包装器和绑定器，支持函数对象、成员函数和自由函数的统一调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户定义字面量&lt;/strong&gt;：允许为标准类型和自定义类型定义字面量后缀，增强代码的可读性和表达能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;alignas&lt;/code&gt; 和 &lt;code&gt;alignof&lt;/code&gt; 关键字&lt;/strong&gt;：提供对齐控制和查询功能，确保数据在内存中的对齐方式符合特定要求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;explicit&lt;/code&gt; 关键字&lt;/strong&gt;：体现显示转换和隐式转换上的概念要求&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::atomic&amp;#x3C;T&gt;&lt;/code&gt; 是 C++11 引入的原子类型，用于在多线程中安全地读写变量&lt;/li&gt;
&lt;li&gt;还有虚函数 &lt;code&gt;override&lt;/code&gt;、容器非成员函数 &lt;code&gt;swap&lt;/code&gt;、新的 &lt;code&gt;bitset&lt;/code&gt; 位运算...&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;44. C++ class 与 C struct 的区别？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;C 语言不支持继承和多态&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1️⃣ 默认访问权限不同&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C &lt;code&gt;struct&lt;/code&gt; 默认权限为 public&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C++ &lt;code&gt;class&lt;/code&gt; 默认权限为 private&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;C++ &lt;code&gt;struct&lt;/code&gt; 默认权限 public&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2️⃣ 成员函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C++ 中的 &lt;code&gt;struct&lt;/code&gt; 和 &lt;code&gt;class&lt;/code&gt; 都可包含成员函数&lt;/li&gt;
&lt;li&gt;C 中的 &lt;code&gt;struct&lt;/code&gt; 只能包含数据，&lt;strong&gt;不能包含成员函数&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++ 的 &lt;code&gt;class&lt;/code&gt; 与 &lt;code&gt;struct&lt;/code&gt; 都支持模板、虚函数、多态、构造函数、析构函数、重载操作符等高级特性，这些都是 C 中 &lt;code&gt;struct&lt;/code&gt; 不具备的功能。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;以上谈论的是 C struct 和 C++ class 的区别，接下来聊一聊 C++ struct 和 C++ class 的区别。&lt;/p&gt;
&lt;p&gt;在 &lt;strong&gt;C++ 中&lt;/strong&gt;，&lt;code&gt;class&lt;/code&gt; 和 &lt;code&gt;struct&lt;/code&gt; 的功能几乎是等价的（除了默认访问权限不同），继承时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;struct 的继承默认是 public 继承&lt;/li&gt;
&lt;li&gt;class 的继承默认是 private 继承&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通常情况下，如果类主要用于表示&lt;strong&gt;数据结构&lt;/strong&gt;，不需要封装和访问控制，且所有成员均为 &lt;code&gt;public&lt;/code&gt;，则常用 &lt;code&gt;struct&lt;/code&gt;；如果强调&lt;strong&gt;封装、访问控制&lt;/strong&gt;，需要私有或受保护成员时，则倾向于用 &lt;code&gt;class&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;45. 怎么优化系统性能&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;合理使用缓存机制，如内存缓存、Redis 等&lt;/li&gt;
&lt;li&gt;利用多线程或多进程技术，让更多的处理器核心参与计算，提升吞吐量&lt;/li&gt;
&lt;li&gt;选择高效的算法和数据结构可以显著提升系统性能&lt;/li&gt;
&lt;li&gt;编写高质量的代码，避免冗余计算，减少函数调用和内存分配，合理使用同步和异步操作&lt;/li&gt;
&lt;li&gt;采用集群等高可用架构，避免单点故障，确保系统在高负载下仍能稳定运行&lt;/li&gt;
&lt;li&gt;负载均衡，通过将请求分配到多台服务器上，避免单一服务器的性能瓶颈&lt;/li&gt;
&lt;li&gt;使用消息队列实现高并发下的异步处理，削峰填谷，缓解系统压力&lt;/li&gt;
&lt;li&gt;&lt;code&gt;perf&lt;/code&gt; 工具查看系统性能瓶颈&lt;/li&gt;
&lt;li&gt;开启编译优化 &lt;code&gt;-O2&lt;/code&gt;、&lt;code&gt;-O3&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以下展开介绍几个主要的优化点。&lt;/p&gt;
&lt;h3&gt;内存管理优化&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;减少内存分配与释放次数：&lt;/strong&gt; 频繁的堆内存分配和释放会严重拖慢程序，甚至导致内存碎片。应尽量重用对象、使用内存池等技术来降低分配开销。例如，在 C++ 中可以实现&lt;strong&gt;对象池&lt;/strong&gt;，预先分配一定数量的对象，在需要时复用它们而不是每次 &lt;code&gt;new&lt;/code&gt; 和 &lt;code&gt;delete&lt;/code&gt;。对于生命周期较短且数量巨大的对象，尽可能分配在栈上而非堆上，因为栈上的分配/回收开销远小于堆（注意栈有大小限制，过大的对象还是要放在堆上）。在 Java/Python 等有垃圾回收的语言中，无法手动管理内存，但可以通过减少不必要的临时对象创建来减轻 GC 压力。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;避免不必要的数据拷贝&lt;/strong&gt;：数据拷贝不仅耗费 CPU 时间，还增加内存占用。在C/C++中，尽量通过指针、引用传递大对象，或使用移动语义（&lt;code&gt;std::move&lt;/code&gt;）来避免昂贵的深拷贝。例如，将函数参数改为 &lt;code&gt;const std::vector&amp;#x3C;T&gt;&amp;#x26;&lt;/code&gt; 引用而不是传值，可以省去一遍拷贝的成本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;提高内存访问局部性：&lt;/strong&gt; 尽量使用连续内存的数据结构，有利于 CPU 缓存命中率。例如，相比链表，数组或动态数组（如 &lt;code&gt;std::vector&lt;/code&gt;）在遍历时连续访问内存，对缓存更友好。访问内存时，如果数据分散，CPU缓存无法有效预取，性能会下降。因此，应尽量使常用的数据在内存中连续存放。对于需要处理大批量数据的场景，可以考虑将“数组的结构”转变为“结构的数组”以提高向量化和缓存性能。这种优化在需要对大量对象的某个字段进行批量操作时特别有效，因为连续的内存布局可以充分利用 SIMD 指令和缓存行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;控制内存使用与回收：&lt;/strong&gt; 注意避免&lt;strong&gt;内存泄漏&lt;/strong&gt;和不必要的内存占用。未释放的内存不仅浪费资源，还可能导致系统频繁进行垃圾回收或交换，从而严重影响性能。应使用恰当的数据结构来节省内存，例如在需要存储大量布尔值时使用位图/位集而不是布尔数组。&lt;/p&gt;
&lt;h3&gt;I/O 优化&lt;/h3&gt;
&lt;p&gt;尽量减少 I/O 调用次数： 外部I/O（磁盘读写、网络通信）往往比内存操作慢几个数量级。优化I/O的一个基本原则是减少系统调用频次。例如，与其逐字节写入文件，不如积累一定数据后一次写入（批处理）；读文件时尽量使用批量读取或流式读取来降低调用开销。将零散的小I/O操作合并为较少的几次大操作，可以大幅降低每次调用的固定成本，提高总体吞吐量。&lt;/p&gt;
&lt;p&gt;使用缓冲和缓存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓冲是在内存中暂存数据，凑够一定量再进行 I/O&lt;/li&gt;
&lt;li&gt;缓存则是将经常访问的数据暂存内存，以避免重复从慢速存储获取&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;异步和并行 I/O： 传统同步I/O会阻塞执行线程，等待操作完成。通过异步 I/O，程序可以在等待I/O的同时去处理其他任务，从而提高整体效率和响应性。另外，对于磁盘 I/O 密集型任务，合理利用操作系统的内存映射文件（mmap）也能提升效率，因为操作系统会自动预读和缓存文件内容，且内存映射减少了用户态/内核态的数据拷贝。&lt;/p&gt;
&lt;h3&gt;性能分析与瓶颈定位&lt;/h3&gt;
&lt;p&gt;在展开具体优化工作之前，&lt;strong&gt;识别性能瓶颈&lt;/strong&gt;是关键的一步。盲目优化往往事倍功半，甚至优化了非瓶颈部分而徒增代码复杂度。因此建议利用各种分析工具（Profiler）来定位程序中的“热区”和问题点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU Profiling&lt;/strong&gt;：常用 GNU gprof 工具对应用程序进行采样分析，生成函数级别的耗时报告。在Linux上可以使用 &lt;code&gt;perf&lt;/code&gt; 工具对程序采集更底层的性能数据（如CPU周期、缓存未命中等）。跨平台的工具如 Intel VTune, AMD uProf 提供更高级的性能分析（包括线程并发、微架构瓶颈）。另外，Valgrind 的 Callgrind 模块也能分析代码热点和调用关系，并可借助KCachegrind等可视化工具查看分析结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存和资源分析&lt;/strong&gt;：使用 Valgrind 的 Memcheck 工具可以检测内存泄漏和非法内存访问，这有助于消除由于内存问题导致的异常行为和性能下降。Massif 是 Valgrind 的堆分析器，可以跟踪程序堆内存使用随时间的变化，找出高峰时占用大的代码路径。对于更复杂的内存分析，可以借助 Google Perf Tools（gperftools）中的 heap profiler 或 Dr. Memory 等工具。在需要分析缓存行为时，Valgrind 的 Cachegrind 模块可以模拟CPU缓存，报告缓存命中率，帮助调整数据结构以提高缓存友好度。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;46. 说说移动构造函数与拷贝构造函数&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;我们用对象 a 初始化对象 b，之后对象 a 我们就不再使用了，但是对象 a 的空间还在（在析构之前），既然拷贝构造函数实际上就是把 a 对象的内容复制一份到 b 中，那么为什么我们不能直接使用 a 的空间呢？这样就避免了新的空间的分配，大大降低了构造的成本。这就是移动构造函数设计的初衷。&lt;/li&gt;
&lt;li&gt;‼️&lt;strong&gt;拷贝构造函数中，对于指针，我们一定要采用深拷贝&lt;/strong&gt;；而&lt;strong&gt;移动构造函数中，对于指针，我们采用浅拷贝&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;移动构造函数的参数 &lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt; 和拷贝构造函数 &lt;code&gt;&amp;#x26;&lt;/code&gt; 不同：拷贝构造函数的参数是一个左值引用，但是移动构造函数的初值是一个右值引用。意味着，移动构造函数的参数是一个右值或者将亡值的引用。也就是说，只用一个&lt;strong&gt;右值&lt;/strong&gt;或者&lt;strong&gt;将亡值&lt;/strong&gt;初始化另一个对象的时候，才会调用移动构造函数。&lt;strong&gt;而那个 &lt;code&gt;move()&lt;/code&gt; 语句，就是将一个左值变成一个将亡值&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;

class MyString {
public:
    // 构造函数
    MyString() : str(nullptr), len(0) {}

    // 构造函数
    MyString(const char* s) : str(nullptr), len(0) {
        if (s != nullptr) {
            len = strlen(s);
            str = new char[len + 1];
            strcpy(str, s);
        }
    }

    // 拷贝构造函数: 有指针则采用深拷贝
    MyString(const MyString&amp;#x26; other) : str(nullptr), len(0) {
        if (other.str != nullptr) {
            len = other.len;
            str = new char[len + 1];
            strcpy(str, other.str);
        }
    }

    // 移动构造函数: 采用浅拷贝
    MyString(MyString&amp;#x26;&amp;#x26; other) noexcept {
        str = other.str;
        len = other.len;
        other.str = nullptr;
        other.len = 0;
    }

    // 析构函数
    ~MyString() {
        if (str != nullptr) {
            delete[] str;
            str = nullptr;
            len = 0;
        }
    }

private:
    char* str;
    size_t len;
};

int main() {
    MyString s1(&quot;Hello&quot;);  		 // 调用构造函数
    MyString s2(s1);      		 // 调用拷贝构造函数
    MyString s3(std::move(s1));  // 调用移动构造函数
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;47. C++ 中指针参数传递和引用参数传递有什么区别？底层原理是什么？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;恍然大悟&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;(1) 指针参数传递本质上是值传递，它所传递的是一个地址值&lt;/h3&gt;
&lt;p&gt;值传递过程中，被调函数的形式参数作为被调函数的局部变量处理，会在栈中开辟内存空间以存放由主调函数传递进来的实参值，从而形成了实参的一个副本（替身）。&lt;/p&gt;
&lt;p&gt;值传递的特点是，被调函数对形式参数的任何操作都是作为局部变量进行的，不会影响主调函数的实参变量的值（&lt;strong&gt;即使是形参指针地址变了，实参指针地址都不会变&lt;/strong&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;

void changePointer(int* ptr) {
    int b = 20;
    ptr = &amp;#x26;b;  // 仅改变了形参指针的指向，实参指针不变
}

int main() {
    int a = 10;
    int* p = &amp;#x26;a;

    cout &amp;#x3C;&amp;#x3C; &quot;Before function call: &quot; &amp;#x3C;&amp;#x3C; *p &amp;#x3C;&amp;#x3C; endl;  // 输出 10
    changePointer(p);
    cout &amp;#x3C;&amp;#x3C; &quot;After function call: &quot; &amp;#x3C;&amp;#x3C; *p &amp;#x3C;&amp;#x3C; endl;   // 仍然输出 10，不是 20

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;(2) 引用参数传递过程中，被调函数的形式参数也作为局部变量在栈中开辟了内存空间，但是这时存放的是由主调函数放进来的实参变量的地址&lt;/h3&gt;
&lt;p&gt;被调函数对形参（本体）的任何操作都被处理成间接寻址，即通过栈中存放的地址访问主调函数中的实参变量（根据&lt;strong&gt;别名&lt;/strong&gt;找到主调函数中的本体）。&lt;/p&gt;
&lt;p&gt;因此，&lt;strong&gt;被调函数对形参的任何操作都会影响主调函数中的实参变量&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;二者区别&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1)&lt;/strong&gt; 引用传递和指针传递是不同的，虽然他们都是在被调函数栈空间上的一个局部变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;但是任何对于&lt;strong&gt;引用参数&lt;/strong&gt;的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。&lt;/li&gt;
&lt;li&gt;而对于&lt;strong&gt;指针传递的参数&lt;/strong&gt;，如果改变被调函数中的指针地址，它将应用不到主调函数的相关变量。🔥 如果想通过指针参数传递来改变主调函数中的相关变量（地址），那就得使用&lt;strong&gt;指向指针的指针&lt;/strong&gt;或者&lt;strong&gt;指针引用&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2)&lt;/strong&gt; 从编译的角度来讲，程序在编译时分别将指针和引用添加到符号表上，符号表中记录的是变量名及变量所对应地址。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指针变量在符号表上对应的地址值为指针变量的地址值，而引用在符号表上对应的地址值为引用对象的地址值（与实参名字不同，地址相同）。&lt;/li&gt;
&lt;li&gt;符号表生成之后就不会再改，因此指针可以改变其指向的对象（指针变量中的值可以改），而引用对象则不能修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;48. C++ 中类成员的访问权限和继承权限问题&lt;/h2&gt;
&lt;h3&gt;访问权限&lt;/h3&gt;
&lt;p&gt;① &lt;code&gt;public&lt;/code&gt;: 用该关键字修饰的成员表示公有成员，该成员不仅可以在类内可以被访问，在类外也是可以被访问的，是类对外提供的可访问接口；&lt;/p&gt;
&lt;p&gt;② &lt;code&gt;private&lt;/code&gt;: 用该关键字修饰的成员表示私有成员，该成员仅在类内可以被访问，在类体外是隐藏状态；&lt;/p&gt;
&lt;p&gt;③ &lt;code&gt;protected&lt;/code&gt;: 用该关键字修饰的成员表示保护成员，保护成员在类体外同样是隐藏状态，但是对于该类的派生类来说，相当于公有成员，在派生类中可以被访问。&lt;/p&gt;
&lt;h3&gt;继承方式&lt;/h3&gt;
&lt;p&gt;① 若继承方式是 &lt;code&gt;public&lt;/code&gt;，&lt;strong&gt;基类成员在派生类中的访问权限保持不变&lt;/strong&gt;，也就是说，基类中的成员访问权限，在派生类中仍然保持原来的访问权限；&lt;/p&gt;
&lt;p&gt;② 若继承方式是 &lt;code&gt;private&lt;/code&gt;，基类所有成员在派生类中的访问权限都会变为私有 (private) 权限；&lt;/p&gt;
&lt;p&gt;③ 若继承方式是 &lt;code&gt;protected&lt;/code&gt;，基类的共有成员 &lt;code&gt;public&lt;/code&gt; 和保护成员 &lt;code&gt;protected&lt;/code&gt; 在派生类中的访问权限都会变为保护 (protected) 权限，私有成员在派生类中的访问权限仍然是私有 (private) 权限。&lt;/p&gt;
&lt;h2&gt;49. 定义与声明的区别&lt;/h2&gt;
&lt;p&gt;如果是指「变量」的声明和定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从编译原理上来说，&lt;strong&gt;声明&lt;/strong&gt;是仅仅告诉编译器，有个某类型的变量会被使用，但是编译器并不会为它分配任何内存。&lt;/li&gt;
&lt;li&gt;而&lt;strong&gt;定义&lt;/strong&gt;就是分配了内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果是指「函数」的声明和定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;声明：一般在头文件里，对编译器说我有一个函数叫 &lt;code&gt;function()&lt;/code&gt; 让编译器知道这个函数的存在。&lt;/li&gt;
&lt;li&gt;定义：一般在源文件里，具体就是函数的实现过程写明函数体。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;50. 你知道 strcpy 与 memcpy 的区别吗&lt;/h2&gt;
&lt;p&gt;1、复制的内容不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;strcpy&lt;/code&gt; 只能复制字符串&lt;/li&gt;
&lt;li&gt;而 &lt;code&gt;memcpy&lt;/code&gt; 可以复制任意内容，例如字符数组、整型、结构体、类等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2、复制的方法不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;strcpy&lt;/code&gt; 不需要指定长度，它遇到被复制字符的串结束符 &lt;code&gt;&quot;\0&quot;&lt;/code&gt; 才结束，所以容易溢出&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memcpy&lt;/code&gt; 则是根据其第 3 个参数决定复制的长度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3、用途不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通常在复制字符串时用 &lt;code&gt;strcpy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;而需要复制其他类型数据时则一般用 &lt;code&gt;memcpy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;51. volatile 关键字的作用&lt;/h2&gt;
&lt;h3&gt;面试回答&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;volatile&lt;/code&gt; 的意思是“脆弱的”，表明它修饰的变量的值十分容易被改变，所以编译器就不会对这个变量进行优化（CPU 的优化是让该变量存放到 CPU 寄存器而不是内存），进而提供稳定的访问。每次读取 &lt;code&gt;volatile&lt;/code&gt; 的变量时，系统总是会从内存中读取这个变量，并且将它的值立刻保存。&lt;/p&gt;
&lt;h3&gt;解释&lt;/h3&gt;
&lt;p&gt;C/C++ 中的 volatile 关键字和 const 对应，用来修饰变量，通常用于建立语言级别的 memory barrier。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;volatile&lt;/code&gt; 关键字是一种类型修饰符，用它声明的类型变量&lt;strong&gt;表示可以被某些编译器未知的因素更改&lt;/strong&gt;，比如：操作系统、硬件或者其它线程等。遇到这个关键字声明的变量，编译器对访问该变量的代码就不再进行优化，从而可以提供对特殊地址的稳定访问。声明时语法：&lt;code&gt;int volatile vInt&lt;/code&gt;; 当要求使用 volatile 声明的变量的值的时候，&lt;strong&gt;系统总是重新从它所在的内存读取数据&lt;/strong&gt;，即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;stdio.h&gt;
 
void main()
{
    volatile int i = 10;
    int a = i;
 
    printf(&quot;i = %d&quot;, a);
    __asm {
        mov dword ptr [ebp-4], 20h
    }
 
    int b = i;
    printf(&quot;i = %d&quot;, b);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;i = 10
i = 32	// 如果没有 volatile 关键字修饰则该值为 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ &lt;code&gt;volatile&lt;/code&gt; 用在如下的几个地方：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;中断服务程序&lt;/strong&gt;中修改的供其它程序检测的变量需要加 volatile。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多任务环境&lt;/strong&gt;下各任务间共享的标志应该加 volatile：当两个线程都要用到某一个变量且该变量的值会被改变时，应该用 volatile 声明，该关键字的作用是防止优化编译器把变量从内存装入 CPU 寄存器中。如果变量被装入寄存器，那么两个线程有可能一个使用内存中的变量，一个使用寄存器中的变量，这会造成程序的错误执行。volatile 的意思是让编译器每次操作该变量时一定要从内存中真正取出，而不是使用已经存在寄存器中的值。&lt;/li&gt;
&lt;li&gt;存储器映射的硬件寄存器通常也要加 volatile 说明，因为每次对它的读写都可能有不同意义。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;52. 如果有一个空类，它会默认存在哪些函数？&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Empty(); // 缺省构造函数 //
Empty( const Empty&amp;#x26; ); // 拷贝构造函数 //
~Empty(); // 析构函数 //
Empty&amp;#x26; operator=( const Empty&amp;#x26; ); // 赋值运算符 //
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;53. const char* 与 string 之间的区别&lt;/h2&gt;
&lt;p&gt;string 是 C++ 标准库里面其中一个，封装了对字符串的操作，实际操作过程我们可以用 &lt;code&gt;const char*&lt;/code&gt; 给 &lt;code&gt;string&lt;/code&gt; 类初始化。&lt;/p&gt;
&lt;p&gt;三者之间的转化关系如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 1. string 转 const char*
string s = “abc”;
const char* c_s = s.c_str();

// 2. const char* 转 string, 直接赋值即可
const char* c_s = “abc”;
string s(c_s);

// 3. string 转 char* 
string s = “abc”;
const int len = s.length();
char* c;
c = new char[len + 1];
strcpy(c, s.c_str());

// 4. char* 转 string, 直接赋值即可
char* c = “abc”;
string s(c);

// 5. const char* 转 char*
const char* cpc = “abc”;
char* pc = new char[strlen(cpc) + 1];
strcpy(pc, cpc);

// 6. char* 转 const char*, 直接赋值即可
char* pc = “abc”;
const char* cpc = pc;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;54. static_cast 比 C 语言中的转换好在哪里？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;更加安全；&lt;/li&gt;
&lt;li&gt;更直接明显，能够一眼看出是什么类型转换为什么类型，容易找出程序中的错误；可清楚地辨别代码中每个显式的强制转；可读性更好，能体现程序员的意图。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;55. delete 和 delete[] 区别？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;delete&lt;/code&gt; 只会调用一次析构函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete[]&lt;/code&gt; 会调用数组中每个元素的析构函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;56. 为什么不把所有函数写成内联函数？&lt;/h2&gt;
&lt;p&gt;内联函数以代码复杂为代价，它以省去函数调用的开销来提高执行效率。所以一方面如果内联函数体内代码执行时间相比函数调用开销较大，则没有太大的意义；另一方面每一处内联函数的调用都要复制代码，消耗更多的内存空间，因此以下情况不宜使用内联函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数体内的代码比较长，将导致内存消耗代价&lt;/li&gt;
&lt;li&gt;函数体内有循环，函数执行时间要比函数调用开销大&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;57. 哪些函数不能是虚函数？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;构造函数：构造函数初始化对象，派生类必须知道基类函数干了什么，才能进行构造；当有虚函数时，每一个类有一个虚表，每一个对象有一个虚表指针，虚表指针在构造函数中初始化。&lt;/li&gt;
&lt;li&gt;内联函数：内联函数表示在编译阶段进行函数体的替换操作，而虚函数意味着在运行期间进行类型确定，所以内联函数不能是虚函数。&lt;/li&gt;
&lt;li&gt;静态函数：静态函数不属于对象属于类，静态成员函数没有 this 指针，因此静态函数设置为虚函数没有任何意义。&lt;/li&gt;
&lt;li&gt;友元函数：友元函数不属于类的成员函数，不能被继承。对于没有继承特性的函数没有虚函数的说法。&lt;/li&gt;
&lt;li&gt;普通函数：普通函数不属于类的成员函数，不具有继承特性，因此普通函数没有虚函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;58. 什么原因造成内存泄露，你怎么避免/解决内存泄露？&lt;/h2&gt;
&lt;h3&gt;1️⃣ 什么是内存泄露？&lt;/h3&gt;
&lt;p&gt;在程序运行过程中不再使用的对象没有被正确释放，从而导致程序使用的内存不断增加，最终导致程序异常退出或内存分配失败。&lt;/p&gt;
&lt;h3&gt;2️⃣ 什么原因造成内存泄露？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;忘记释放内存：分配了内存但没有释放&lt;/li&gt;
&lt;li&gt;异常 / 逻辑处理不当：写了内存释放代码，但最后未执行到&lt;/li&gt;
&lt;li&gt;循环引用：使用智能指针 shared_ptr 造成内存泄露&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3️⃣ 如何避免/解决内存泄露 ‼️&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;内存泄露一般是因为&lt;strong&gt;分配了内存但没有释放&lt;/strong&gt;，要解决这个问题，我通常从以下几个层面入手：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;我会用 &lt;strong&gt;RAII 机制管理资源&lt;/strong&gt;（构造时分配，析构时释放）&lt;/li&gt;
&lt;li&gt;能用&lt;strong&gt;智能指针&lt;/strong&gt;（&lt;code&gt;unique_ptr&lt;/code&gt;, &lt;code&gt;shared_ptr&lt;/code&gt;）的地方绝不手动 &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt;，同时要注意&lt;strong&gt;避免循环引用&lt;/strong&gt;（使用弱引用）&lt;/li&gt;
&lt;li&gt;对于资源管理比较复杂的类，我会写好析构函数，&lt;strong&gt;并考虑拷贝/移动语义，防止资源重复释放或泄露&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;正确捕获处理异常 / 回滚式编程：&lt;strong&gt;编写异常安全的代码非常困难&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决内存泄露本质上就是：该释放的要释放，生命周期清楚，用好工具，写好代码。我平时更倾向于用智能指针来管理资源，基本上能从根上避免大部分内存泄露问题。&lt;/p&gt;
&lt;h3&gt;4️⃣ 如何定位内存泄露&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;🔗参考链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/dWbqNIA4pLWs4pd53Gaq8A&quot;&gt;Linux内存泄露定位1：valgrind篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=MzU4NjY0NTExNA==&amp;#x26;mid=2247486485&amp;#x26;idx=1&amp;#x26;sn=a4ff43bbf0f25700369fd433ac66613a&amp;#x26;chksm=fdf96700ca8eee1615ef071da78d32ae68986610d69c44915dd6f6fdf9aa87f659889ef33eac&amp;#x26;scene=178&amp;#x26;cur_album_id=2238988581711282176&amp;#x26;search_click_id=#rd&quot;&gt;Linux内存泄露定位2：mtrace篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/JX2NVI35ze02k7LwCodhLA&quot;&gt;Linux内存泄露定位3：hook+backtrace篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/gb3hcwgoFXTiZhmWxyf-bg&quot;&gt;Linux内存泄露定位4：eBPF+uprobes篇&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;静态检测工具：检查代码中是否出现内存泄露，
&lt;ul&gt;
&lt;li&gt;cppcheck&lt;/li&gt;
&lt;li&gt;clang-tidy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;valgrind&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;需要调试信息 &lt;code&gt;-g&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;valgrind --leak-check=full&lt;/code&gt; 可执行程序&lt;/li&gt;
&lt;li&gt;可视 valgrind 为虚拟机，将可执行程序当作文件来处理，读取二进制文件的内容，进行指令解析并执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hook&lt;/code&gt; + &lt;code&gt;backtrace&lt;/code&gt;：侵入式（可能会引起程序异常）
&lt;ul&gt;
&lt;li&gt;hook 住内存分配和释放接口&lt;/li&gt;
&lt;li&gt;每次申请内存都记录一下，每次释放时也记录一下，然后再把这两种记录进行一个对比，把相同的去掉，剩下就是&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eBPF&lt;/code&gt; + &lt;code&gt;uprobes&lt;/code&gt;：非侵入式（内核中进行统计，不会影响程序）
&lt;ul&gt;
&lt;li&gt;不需要调试信息&lt;/li&gt;
&lt;li&gt;原理与上一种相同，但是不是侵入式，运行在内核&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;59. C++ 写了析构函数，系统会帮我们生成默认移动构造函数这些吗（介绍 C++ 六个特殊成员函数）&lt;/h2&gt;
&lt;p&gt;写了析构函数，系统可能不再自动生成“移动构造”和“移动赋值”函数了，但拷贝构造和拷贝赋值通常还是会生成的。&lt;/p&gt;
&lt;p&gt;在 C++ 里，有六个所谓的“特殊成员函数”：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;默认构造函数 &lt;code&gt;MyClass()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;析构函数 &lt;code&gt;~MyClass()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;拷贝构造函数 &lt;code&gt;MyClass(const MyClass&amp;#x26; other)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;拷贝赋值函数 &lt;code&gt;MyClass&amp;#x26; operator=(const MyClass&amp;#x26; other)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;移动构造函数（C++11 起）&lt;code&gt;MyClass(MyClass&amp;#x26;&amp;#x26; other) noexcept&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;移动赋值函数（C++11 起）&lt;code&gt;MyClass&amp;#x26; operator=(MyClass&amp;#x26;&amp;#x26; other) noexcept&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你自己写了一个&lt;strong&gt;析构函数&lt;/strong&gt;，那编译器就认为你要自己管理资源了。所以出于安全考虑，它&lt;strong&gt;不会再自动生成移动构造函数和移动赋值函数了&lt;/strong&gt;，你得自己写，或者用 &lt;code&gt;= default&lt;/code&gt; 显式声明。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;MyClass(MyClass&amp;#x26;&amp;#x26;) = default;
MyClass&amp;#x26; operator=(MyClass&amp;#x26;&amp;#x26;) = default;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;60. C++ 右值引用和移动拷贝(赋值)函数的作用&lt;/h2&gt;
&lt;p&gt;右值引用和移动语义是在 C++11 之后引入的，目的是&lt;strong&gt;优化性能，避免不必要的资源拷贝&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;以前在 C++98 里，如果你把一个对象传给另一个对象，哪怕那个对象马上就要销毁了，编译器也只能做拷贝，哪怕里面的数据非常大，比如堆上几百 MB 的数组，也得老老实实拷贝一份，非常浪费性能。&lt;/p&gt;
&lt;p&gt;而右值引用的出现，&lt;strong&gt;让编译器能识别出“这是一个临时对象”，你可以放心地把它的资源“抢过来”用，而不是复制一份。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;移动构造函数 &lt;code&gt;MyClass(MyClass&amp;#x26;&amp;#x26; a) noexcept&lt;/code&gt; 和移动赋值操作符 &lt;code&gt;MyClass&amp;#x26; operator=(MyClass&amp;#x26;&amp;#x26; a) noexcept&lt;/code&gt; 的作用就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;移动构造&lt;/strong&gt;：当一个临时对象要变成另一个对象时，直接“接管”它的内部资源，比如把指针地址拿过来，然后把原对象的指针清空，这样就不需要重新分配和复制内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;移动赋值&lt;/strong&gt;：和移动构造类似，不过是用于对象已经存在的情况下，把另一个临时对象的资源拿过来，原来的资源先释放，然后再接管。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;61. C++ 线程 thread_local 的作用是什么？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;它是线程单独拥有的资源，没办法作为共享资源&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;thread_local&lt;/code&gt; 是 C++11 引入的存储类型说明符，用于&lt;strong&gt;为每个线程创建独立的变量副本&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个线程都需要使用一个自己的变量（如缓存、计数器等），避免同步。&lt;/li&gt;
&lt;li&gt;类似于线程的“全局变量”，但互不干扰。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;thread_local int counter = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ &lt;strong&gt;示例场景&lt;/strong&gt;：日志系统中用 &lt;code&gt;thread_local&lt;/code&gt; 缓存上下文&lt;/p&gt;
&lt;p&gt;在多线程程序中，很多系统会&lt;strong&gt;给每个线程维护一份独立的日志信息&lt;/strong&gt;，比如线程 ID、调用栈、临时日志缓存等。如果所有线程都用一个共享变量，会导致锁竞争、效率低下。&lt;/p&gt;
&lt;p&gt;这时候就可以用 &lt;code&gt;thread_local&lt;/code&gt; 给每个线程&lt;strong&gt;一份独立副本&lt;/strong&gt;！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;thread&gt;
#include &amp;#x3C;string&gt;

class Logger {
public:
    static void log(const std::string&amp;#x26; message) {
        log_context += message + &quot;\n&quot;;
    }

    static void flush() {
        std::cout &amp;#x3C;&amp;#x3C; &quot;[Thread &quot; &amp;#x3C;&amp;#x3C; std::this_thread::get_id() &amp;#x3C;&amp;#x3C; &quot;]\n&quot;;
        std::cout &amp;#x3C;&amp;#x3C; log_context &amp;#x3C;&amp;#x3C; std::endl;
        log_context.clear();
    }

private:
    static thread_local std::string log_context; // 每个线程一份
};

thread_local std::string Logger::log_context;

void thread_task(int id) {
    Logger::log(&quot;Start work in thread &quot; + std::to_string(id));
    Logger::log(&quot;Doing some work...&quot;);
    Logger::log(&quot;Finish work in thread &quot; + std::to_string(id));
    Logger::flush();
}

int main() {
    std::thread t1(thread_task, 1);
    std::thread t2(thread_task, 2);
    t1.join();
    t2.join();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;62. 如何定义一个只能在堆上（栈上）生成对象的类？&lt;/h2&gt;
&lt;h3&gt;只能在堆上&lt;/h3&gt;
&lt;p&gt;方法：将析构函数设置为私有&lt;/p&gt;
&lt;p&gt;原因：C++ 是静态绑定语言，编译器管理栈上对象的生命周期，编译器在为类对象分配栈空间时，会先检查类的析构函数的访问性。若析构函数不可访问，则不能在栈上创建对象。&lt;/p&gt;
&lt;h3&gt;只能在栈上&lt;/h3&gt;
&lt;p&gt;方法：将 new 和 delete 重载为私有&lt;/p&gt;
&lt;p&gt;原因：在堆上生成对象，使用 new 关键词操作，其过程分为两阶段：第一阶段，使用 new 在堆上寻找可用内存，分配给对象；第二阶段，调用构造函数生成对象。将 new 操作设置为私有，那么第一阶段就无法完成，就不能够在堆上生成对象。&lt;/p&gt;
&lt;h2&gt;63. 递归过深会造成什么问题，OOM 吗？&lt;/h2&gt;
&lt;p&gt;递归过深确实可能引发一些严重问题，但不一定是 OOM（Out of Memory）。更常见的问题是栈溢出（Stack Overflow）。&lt;/p&gt;
&lt;p&gt;✅ 栈溢出（Stack Overflow）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次函数调用都会在调用栈上分配一块内存来保存函数的执行上下文（如局部变量、返回地址等）。&lt;/li&gt;
&lt;li&gt;如果递归层级太深，调用栈不断增长，最终会超过系统分配给程序的栈空间上限（跟默认「线程栈」大小相关）。&lt;/li&gt;
&lt;li&gt;此时程序会抛出 StackOverflowError（Java） 或 Segmentation Fault（C/C++），或者 RecursionError（Python）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;❌ 而不是 OOM（Out of Memory）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OOM 通常是指 &lt;strong&gt;堆内存&lt;/strong&gt; 耗尽，例如大量创建对象、数组或内存泄漏。&lt;/li&gt;
&lt;li&gt;除非每层递归都分配了大量堆内存（比如在每层递归里 new 很大的对象），否则递归本身并不会直接造成 OOM。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;递归过深导致的栈溢出，和线程的栈大小直接相关：每个线程在启动时，操作系统会为它分配一块固定大小的栈内存（线程栈），专门用于保存函数调用帧，如果递归太深，每层调用都占用一点栈空间，栈就会被用完，最终导致栈溢出。线程栈大小是有限的，不同语言/平台有不同的默认值如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java 一般是 1MB（可通过 &lt;code&gt;-Xss&lt;/code&gt; 参数设置）&lt;/li&gt;
&lt;li&gt;Linux 上的原生线程（如 &lt;code&gt;pthread&lt;/code&gt;）默认栈大小通常是 8MB&lt;/li&gt;
&lt;li&gt;Python 受限于解释器的默认递归深度（&lt;code&gt;sys.getrecursionlimit()&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;64. 如何获取前 K 个最大元素？&lt;/h2&gt;
&lt;p&gt;这个问题是数据结构与算法中的经典题目之一，常用于考察排序、堆、优先队列的应用。以下是几种常见的解法及其时间复杂度分析：&lt;/p&gt;
&lt;h3&gt;解法一：排序法（适合数据量不大）&lt;/h3&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接对数组进行排序&lt;/li&gt;
&lt;li&gt;取前 K 个元素&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;vector&gt;
#include &amp;#x3C;algorithm&gt;

std::vector&amp;#x3C;int&gt; topKSort(std::vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
    std::sort(nums.begin(), nums.end(), std::greater&amp;#x3C;int&gt;());
    return std::vector&amp;#x3C;int&gt;(nums.begin(), nums.begin() + k);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;排序时间复杂度：&lt;code&gt;O(n log n)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;空间复杂度：&lt;code&gt;O(1)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;解法二：最小堆（推荐，适合大数据）&lt;/h3&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用大小为 K 的最小堆来保存当前最大的 K 个元素&lt;/li&gt;
&lt;li&gt;遍历整个数组，若当前元素比堆顶大，则替换堆顶&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;vector&gt;
#include &amp;#x3C;queue&gt;

std::vector&amp;#x3C;int&gt; topKHeap(const std::vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
    if (k == 0)
        return {};

    std::priority_queue&amp;#x3C;int, std::vector&amp;#x3C;int&gt;, std::greater&amp;#x3C;int&gt;&gt; minHeap;

    for (int num : nums) {
        if (minHeap.size() &amp;#x3C; k) {
            minHeap.push(num);
        } else if (num &gt; minHeap.top()) {
            minHeap.pop();
            minHeap.push(num);
        }
    }

    std::vector&amp;#x3C;int&gt; result;
    while (!minHeap.empty()) {
        result.push_back(minHeap.top());
        minHeap.pop();
    }

    std::sort(result.rbegin(), result.rend()); // 可选：从大到小排序
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：&lt;code&gt;O(n log k)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;构建堆：&lt;code&gt;O(k)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;遍历其余 $n - k$ 个元素，每次 &lt;code&gt;O(log k)&lt;/code&gt;：总共 &lt;code&gt;O((n-k) log k)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;空间复杂度：&lt;code&gt;O(k)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;🔥 解法三：快排的思想（Top-K 问题，适合不要求完整排序）&lt;/h3&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类似快速排序中的分区（partition），选定一个“枢轴”，将大于 pivot 的放左边，小于的放右边&lt;/li&gt;
&lt;li&gt;不断递归，直到找到第 K 个最大的数为止&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;vector&gt;
#include &amp;#x3C;cstdlib&gt; // for rand()

int partition(std::vector&amp;#x3C;int&gt;&amp;#x26; nums, int left, int right) {
    int pivot = nums[right], i = left;
    for (int j = left; j &amp;#x3C; right; ++j) {
        if (nums[j] &gt;= pivot) {
            std::swap(nums[i], nums[j]);
            ++i;
        }
    }
    std::swap(nums[i], nums[right]);
    return i;
}

void quickSelect(std::vector&amp;#x3C;int&gt;&amp;#x26; nums, int left, int right, int k) {
    if (left &gt;= right)
        return;
    int pivotIndex = partition(nums, left, right);
    if (pivotIndex == k)
        return;
    else if (pivotIndex &amp;#x3C; k)
        quickSelect(nums, pivotIndex + 1, right, k);
    else
        quickSelect(nums, left, pivotIndex - 1, k);
}

std::vector&amp;#x3C;int&gt; topKQuickSelect(std::vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
    quickSelect(nums, 0, nums.size() - 1, k);
    return std::vector&amp;#x3C;int&gt;(nums.begin(), nums.begin() + k);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;平均：&lt;code&gt;O(n)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;最坏（退化成链表）：&lt;code&gt;O(n^2)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;空间复杂度：&lt;code&gt;O(1)&lt;/code&gt;（递归栈不计）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;✅ 总结对比&lt;/h3&gt;
&lt;p&gt;| 方法     | 时间复杂度 | 空间复杂度 | 适用场景                |
| -------- | ---------- | ---------- | ----------------------- |
| 排序法   | O(n log n) | O(1)       | 数据量小，代码简单      |
| 最小堆   | O(n log k) | O(k)       | 数据量大，k 远小于 n    |
| 快速选择 | 平均 O(n)  | O(1)       | 不关心顺序，只要前 K 大 |&lt;/p&gt;
&lt;h2&gt;65. 堆和栈在操作系统底层的实现？&lt;/h2&gt;
&lt;h3&gt;堆与栈、进程虚拟内存空间分布&lt;/h3&gt;
&lt;p&gt;【速度、安全】栈是用于管理函数调用、局部变量等的高效内存区域，由操作系统自动管理&lt;/p&gt;
&lt;p&gt;【动态、灵活】堆是用于动态分配的内存区域，由程序员手动管理（或者通过垃圾回收机制管理 — java）&lt;/p&gt;
&lt;p&gt;⚙️ 在 32 位机器上，进程虚拟内存空间分布如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504281608151.png&quot; alt=&quot;image-20250428160810384&quot;&gt;&lt;/p&gt;
&lt;p&gt;栈为什么适合函数调用：严格的顺序性，嵌套调用不会乱。&lt;/p&gt;
&lt;p&gt;递归调用过深，栈会发生什么：栈溢出、触发 SIGSEGV 信号，程序崩溃。&lt;/p&gt;
&lt;h3&gt;🔥 栈在操作系统底层的实现&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;线程创建时，操作系统为其创建栈空间（&lt;strong&gt;默认 8 MB&lt;/strong&gt;）—— 分成进程栈与线程栈
&lt;ol&gt;
&lt;li&gt;进程栈（主线程栈）在进程启动时创建&lt;/li&gt;
&lt;li&gt;线程栈通过 &lt;code&gt;pthread_create&lt;/code&gt;、&lt;code&gt;clone&lt;/code&gt; 系统调用创建&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;栈的使用：只需要移动栈指针&lt;/li&gt;
&lt;li&gt;硬件支持：寄存器 x86 下
&lt;ol&gt;
&lt;li&gt;栈顶指针 rsp&lt;/li&gt;
&lt;li&gt;栈底指针 rbp&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;操作系统管理：
&lt;ol&gt;
&lt;li&gt;函数调用时，先将参数压栈&lt;/li&gt;
&lt;li&gt;执行 call 指令，将返回地址（返回上一级函数的下一条指令）压栈&lt;/li&gt;
&lt;li&gt;创建栈帧，保存旧的 rbp，设置新的 rbp&lt;/li&gt;
&lt;li&gt;可能为局部变量分配空间（清理局部变量等的栈空间）&lt;/li&gt;
&lt;li&gt;恢复之前的 rbp 和 rsp，ret 指令弹出返回地址&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504281617109.png&quot; alt=&quot;image-20250428161750930&quot;&gt;&lt;/p&gt;
&lt;h3&gt;🔥 堆在操作系统底层的实现&lt;/h3&gt;
&lt;p&gt;本质为 &lt;code&gt;malloc&lt;/code&gt; 机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当分配内存的大小 $\lt 128KB$ 时，先从内存池获取，否则通过 &lt;code&gt;brk&lt;/code&gt; 系统调用从堆区分配内存，回收时则回收到内存池&lt;/li&gt;
&lt;li&gt;当分配内存的大小 $\ge 128KB$ 时，通过 &lt;code&gt;mmap&lt;/code&gt; 系统调用从文件映射区分配内存，回收时通过 &lt;code&gt;munmap&lt;/code&gt; 释放内存&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么栈的分配速度比堆快？&lt;/h3&gt;
&lt;p&gt;栈只需要移动指针，堆需要查找空闲块、处理碎片和系统调用&lt;/p&gt;
&lt;h3&gt;多线程程序中，堆和栈如何隔离？&lt;/h3&gt;
&lt;p&gt;栈是线程私有的，无需隔离；&lt;/p&gt;
&lt;p&gt;堆是进程共享的，需同步机制如锁🔒&lt;/p&gt;
&lt;h2&gt;66. C++ mutable 关键字&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 wxg 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1️⃣ C++ &lt;code&gt;mutable&lt;/code&gt; 用于修饰非静态成员函数，使得该成员变量可以在 &lt;code&gt;const&lt;/code&gt; 成员函数中允许被修改&lt;/p&gt;
&lt;p&gt;2️⃣ 同时也可以用于修饰 &lt;code&gt;lambda&lt;/code&gt; 表达式，可以去掉函数调用操作符后 &lt;code&gt;const&lt;/code&gt; 关键字，从而可以在 lambda 函数体内可以修改按值捕获的外部变量。&lt;/p&gt;
&lt;h2&gt;67. 详解内存对齐（如何 padding）及其原因&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 csig 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;求 &lt;code&gt;sizeof(S)&lt;/code&gt; 的大小并解析，以及为什么进行内存对齐。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct B {
    int b;
    char c;
};

typedef struct {
    int a;
    char b;
    short c;
    char d;
    B e;
} S;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 C/C++ 中，结构体的大小与成员排列不仅取决于每个成员的大小，还受到**内存对齐（Alignment）**的影响，具体规则如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个成员变量的地址必须是其类型对齐大小（对齐边界）的整数倍。&lt;/li&gt;
&lt;li&gt;结构体本身的总大小必须是其&lt;strong&gt;最大对齐单位&lt;/strong&gt;的整数倍。&lt;/li&gt;
&lt;li&gt;编译器会在必要的位置插入 **padding（填充字节）**来满足上述要求，以提高内存访问效率。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;对结构体 S 的内存布局分析&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;结构体 B 的分析：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct B {
    int b;   // 4 bytes, offset 0
    char c;  // 1 byte, offset 4
             // padding 3 bytes to align struct B size to 8
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;成员 &lt;code&gt;int b&lt;/code&gt; 对齐为 4 字节，偏移量为 0。&lt;/li&gt;
&lt;li&gt;成员 &lt;code&gt;char c&lt;/code&gt; 占 1 字节，偏移量为 4。&lt;/li&gt;
&lt;li&gt;为满足结构体 &lt;code&gt;B&lt;/code&gt; 总大小为最大对齐成员（4 字节）的倍数，需要在末尾添加 3 字节填充。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 &lt;code&gt;sizeof(B) == 8&lt;/code&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;结构体 S 的分析：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;typedef struct {
    int a;    // 4 bytes, offset 0
    char b;   // 1 byte, offset 4
              // padding 1 byte, to align next &apos;short&apos; to 2 bytes
    short c;  // 2 bytes, offset 6
    char d;   // 1 byte, offset 8
              // padding 3 bytes, to align next &apos;B&apos; to 4 bytes
    B e;      // 8 bytes, offset 12
} S;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int a&lt;/code&gt;: 占 4 字节，从 offset 0 开始。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;char b&lt;/code&gt;: 占 1 字节，offset = 4。
&lt;ul&gt;
&lt;li&gt;padding 1 字节，使得 &lt;code&gt;short c&lt;/code&gt; 对齐到 2 的倍数（offset = 6）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;short c&lt;/code&gt;: 占 2 字节，offset = 6。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;char d&lt;/code&gt;: 占 1 字节，offset = 8。
&lt;ul&gt;
&lt;li&gt;padding 3 字节，使得结构体 &lt;code&gt;B e&lt;/code&gt; 对齐到 4 字节边界（offset = 12）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;B e&lt;/code&gt;: 占 8 字节（因为 &lt;code&gt;sizeof(B) == 8&lt;/code&gt;），offset = 12。&lt;/li&gt;
&lt;li&gt;最后 offset = 12 + 8 = 20&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此： &lt;code&gt;sizeof(S) == 20&lt;/code&gt;（假设最后总大小为 21 字节，需要 padding 到 24 字节）&lt;/p&gt;
&lt;h2&gt;68. &lt;code&gt;*(int *)0 = 0&lt;/code&gt; 的含义？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;*(int *)0 = 0;&lt;/code&gt; 这行代码的意思是：将整数值 &lt;code&gt;0&lt;/code&gt; 写入内存地址 &lt;code&gt;0&lt;/code&gt; 所指向的地方，也就是将值 &lt;code&gt;0&lt;/code&gt; 存储到内存的 &lt;strong&gt;地址 0&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(int *) 0&lt;/code&gt;：把整数 0 强转为 &lt;code&gt;int*&lt;/code&gt; 类型的指针&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*(int *) 0&lt;/code&gt;：对地址 &lt;code&gt;0&lt;/code&gt; 进行解引用，访问该地址存储的内容&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*(int *) 0 = 0&lt;/code&gt;：对地址 &lt;code&gt;0&lt;/code&gt; 存储的内容赋值为 &lt;code&gt;0&lt;/code&gt;，试图写入值 &lt;code&gt;0&lt;/code&gt; 到地址 &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在几乎所有现代操作系统中，地址 &lt;code&gt;0&lt;/code&gt; 是&lt;strong&gt;无效的地址&lt;/strong&gt;，属于操作系统保留的内存区域。&lt;/p&gt;
&lt;p&gt;尝试访问（特别是写入）地址 &lt;code&gt;0&lt;/code&gt; 会导致&lt;strong&gt;段错误（Segmentation Fault）&lt;/strong&gt;，程序异常终止。&lt;/p&gt;
&lt;p&gt;在某些低层次编程或嵌入式开发中，地址 &lt;code&gt;0&lt;/code&gt; 可能用于特殊用途，但在普通用户态程序中，这样写&lt;strong&gt;没有任何合法理由&lt;/strong&gt;，通常是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;故意制造崩溃&lt;/strong&gt;（例如测试信号处理）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调试用&lt;/strong&gt;，或者模拟空指针解引用的错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;考察面试者对指针、内存访问的理解&lt;/strong&gt;（比如本题）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;69. 宏定义展开&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#define SQR(x) (x * x)
int main()
{
	int a, b = 3;
	a = SQR(b + 2);
	printf(&quot;a = %d\n&quot;, a);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出为 &lt;code&gt;a = 11&lt;/code&gt; 而非 &lt;code&gt;a = 25&lt;/code&gt;，宏展开后为 &lt;code&gt;a = (b + 2 * b + 2)&lt;/code&gt;，所以结果为 &lt;code&gt;a = 11&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;70. 假设有一个位图数据结构定义为 &lt;code&gt;uint32_t bitmap[BSIZE];&lt;/code&gt;，请写出用于判断位图中第 bit 位是否为 1 的如下宏的实现&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;#define is_bit_set(bit) ((bitmap[(bit)/32] &amp;#x26; (1U &amp;#x3C;&amp;#x3C; ((bit)%32))) != 0)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;解释：一个 &lt;code&gt;uint32_t&lt;/code&gt; 只能表示 32 位，题目 bitmap 用的是 &lt;code&gt;uint32_t&lt;/code&gt; 数组，所以采用 &lt;code&gt;/32&lt;/code&gt; 找到在第几个 &lt;code&gt;uint32_t&lt;/code&gt; 中，&lt;code&gt;%32&lt;/code&gt; 找到在第几位。&lt;/p&gt;
&lt;h2&gt;71. 读写锁与读优先/写优先&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;读写锁和传统互斥锁的区别在于，能支持多个读线程并发执行&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在多线程编程中，读锁和写锁通常是配合**读写锁（读写互斥锁）**使用的，目的是在多个线程并发访问共享资源时提供更高的效率。简单来说，**读锁（共享锁）&lt;strong&gt;允许多个线程同时读取资源，但不允许写操作；而&lt;/strong&gt;写锁（独占锁）**则是排他的，同一时间只能有一个线程持有写锁，且写锁期间不允许任何其他线程读或写这个资源。&lt;/p&gt;
&lt;p&gt;这个机制的好处在于：在读多写少的场景下，我们不必像传统互斥锁那样每次都加排它锁，而是可以让多个读线程并发执行，提升性能。&lt;/p&gt;
&lt;p&gt;具体的使用场景比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读锁适合的场景&lt;/strong&gt;：多个线程同时查询一个共享缓存、配置文件、数据库快照等，数据是只读的，不会被修改。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写锁适合的场景&lt;/strong&gt;：当某个线程需要更新缓存、修改配置或写入日志文件等操作时，为了避免其他线程读到不一致的数据，就需要加写锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 C++ 中，像 &lt;code&gt;std::shared_mutex&lt;/code&gt; 就是一个典型的读写锁实现，可以配合 &lt;code&gt;std::shared_lock&lt;/code&gt; 和 &lt;code&gt;std::unique_lock&lt;/code&gt; 分别实现读锁和写锁。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;读/写优先是基于读写锁中的策略选择而已，不要搞混了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在使用读写锁时，读优先和写优先指的是当&lt;strong&gt;读线程和写线程同时竞争锁资源&lt;/strong&gt;时，系统会优先允许哪一类线程先获取锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;读优先&lt;/strong&gt;的策略意味着：如果当前有读线程在读，或者有新的读线程请求读锁，就会优先满足它们，哪怕有写线程已经在等待。这种策略的好处是&lt;strong&gt;读性能非常高&lt;/strong&gt;，适合“读远远多于写”的场景。但问题是如果读操作持续不断，写线程可能会&lt;strong&gt;长时间得不到执行&lt;/strong&gt;，造成“写饥饿”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;写优先&lt;/strong&gt;则反过来：一旦有写线程在等待，新的读线程就要等写线程先执行完。这种方式能保证写操作不会被饿死，但也意味着读线程可能会频繁被阻塞，尤其在写比较多时，&lt;strong&gt;整体并发度会下降&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;还有一种是&lt;strong&gt;公平策略&lt;/strong&gt;，就是无论读还是写，谁先请求谁先执行。这种方式平衡了读写，防止任何一方饿死，但也可能带来一点性能损耗。&lt;/p&gt;
&lt;p&gt;实际使用中，选择哪种策略要看业务特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果系统是典型的读多写少，比如缓存、配置系统，用读优先可以提升整体吞吐；&lt;/li&gt;
&lt;li&gt;如果写操作比较关键，比如数据库更新或者日志记录，写优先更合适；&lt;/li&gt;
&lt;li&gt;如果两者都重要，或者对延迟比较敏感，可以用公平策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++ 标准库里的 &lt;code&gt;std::shared_mutex&lt;/code&gt; 是不保证写优先的，如果确实要实现写优先或者公平策略，可能需要第三方库（比如 Boost）或者平台相关的原语。&lt;/p&gt;
&lt;h2&gt;72. 多线程之间是如何通信的？线程之间怎么交互的？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;mutex&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;condition_variable&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;答案就是&lt;strong&gt;条件变量&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;多线程之间的通信和交互主要是通过共享内存来实现的，也就是说多个线程可以访问同一个进程的内存空间，从而读写同一份数据。但因为线程可能同时访问同一块数据，会产生竞态条件，&lt;strong&gt;所以通常需要同步机制来保证数据的一致性和正确性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;常用的同步方式包括互斥锁（mutex）、读写锁（rwlock）、信号量（semaphore）、条件变量（condition variable）等，这些机制帮助线程协调访问顺序，防止数据冲突。&lt;/p&gt;
&lt;p&gt;此外，线程间还可以通过消息队列、事件通知等方式传递信息，尤其在异步或生产者-消费者模式中常见。现代操作系统和语言运行时通常提供了丰富的线程同步和通信工具，确保线程间可以高效且安全地交互和协作。&lt;/p&gt;
&lt;h2&gt;73. 关于使用 &lt;code&gt;==&lt;/code&gt; 比较 &lt;code&gt;double&lt;/code&gt; 类型&lt;/h2&gt;
&lt;p&gt;在 C++ 中处理&lt;code&gt;double&lt;/code&gt;类型时，我们需要理解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为什么不能直接用&lt;code&gt;==&lt;/code&gt;比较浮点数（浮点数为什么是近似存储的）&lt;/li&gt;
&lt;li&gt;如何正确比较浮点数&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1. 为什么不能直接用 &lt;code&gt;==&lt;/code&gt; 比较&lt;/h3&gt;
&lt;p&gt;浮点数在计算机中的存储是近似值而非精确值，&lt;code&gt;double&lt;/code&gt; 通常使用 64 位存储（1 符号位 + 11 指数位 + 52 尾数位)，像 0.1 这样的十进制小数无法精确表示为二进制分数，&lt;strong&gt;存储时必须进行舍入（截断无限循环部分），导致微小误差&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;cmath&gt;
#include &amp;#x3C;iomanip&gt;

int main() {
    double a = 0.1;
    double b = 0.2;
    double sum = a + b;
    
    std::cout &amp;#x3C;&amp;#x3C; std::setprecision(20);
    std::cout &amp;#x3C;&amp;#x3C; &quot;0.1 in memory: &quot; &amp;#x3C;&amp;#x3C; a &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;0.2 in memory: &quot; &amp;#x3C;&amp;#x3C; b &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;0.1 + 0.2 = &quot; &amp;#x3C;&amp;#x3C; sum &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;0.3 in memory: &quot; &amp;#x3C;&amp;#x3C; 0.3 &amp;#x3C;&amp;#x3C; std::endl;
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出可能类似于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0.1 in memory: 0.10000000000000000555
0.2 in memory: 0.2000000000000000111
0.1 + 0.2 = 0.30000000000000004441
0.3 in memory: 0.2999999999999999889
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以使用 &lt;code&gt;==&lt;/code&gt; 比较是容易出错的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

int main() {
    double x = 0.1 + 0.2;
    double y = 0.3;
    
    if (x == y) {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Exactly equal\n&quot;;
    } else {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Not exactly equal\n&quot;;  // 通常会输出这个
    }
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 正确的浮点数比较方法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;绝对误差比较&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;cmath&gt;
#include &amp;#x3C;cfloat&gt; // 对于 DBL_EPSILON

bool nearlyEqual(double a, double b, double absEpsilon = 1e-12) {
    return std::fabs(a - b) &amp;#x3C;= absEpsilon;
}

int main() {
    double x = 0.1 + 0.2;
    double y = 0.3;
    
    if (nearlyEqual(x, y)) {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Equal within tolerance\n&quot;;  // 会输出这个
    } else {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Not equal\n&quot;;
    }
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 什么时候可以使用 &lt;code&gt;==&lt;/code&gt; 直接比较&lt;/h3&gt;
&lt;p&gt;在C++中，虽然大多数情况下不推荐直接用&lt;code&gt;==&lt;/code&gt;比较浮点数，但在以下特定情况下可以安全使用（当你能 100% 确定数值来源和没有经过任何浮点运算时，才用&lt;code&gt;==&lt;/code&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比较精确赋值的相同字面值&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double a = 5.0;
double b = 5.0;
if (a == b) { /* 总是true */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;比较整数范围内的值&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double x = 42.0;  // 没有小数部分
double y = 42.0;
if (x == y) { /* 总是true */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;比较特殊浮点值&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 比较正负无穷大
double inf1 = std::numeric_limits&amp;#x3C;double&gt;::infinity();
double inf2 = std::numeric_limits&amp;#x3C;double&gt;::infinity();
if (inf1 == inf2) { /* 总是true */ }

// 比较零值
if (0.0 == -0.0) { /* 总是true，尽管符号不同 */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;比较编译期常量表达式：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;constexpr double PI = 3.141592653589793;
constexpr double PI_2 = PI / 2;
if (PI_2 == 1.5707963267948966) { /* 编译期计算，总是true */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;比较未经过算术运算的相同变量：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double val = getValue();  // 假设返回固定值
if (val == val) { /* 检测NaN的惯用方法 */ }
// 如果val是NaN，这个条件会是false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ 特别注意，以下情况&lt;strong&gt;绝不&lt;/strong&gt;应该使用&lt;code&gt;==&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 经过算术运算的结果
if (0.1 + 0.2 == 0.3) { /* 可能false */ }

// 从不同计算路径得到的结果
double a = calculateA();
double b = calculateB();
if (a == b) { /* 危险 */ }

// 循环累积的结果
double sum = 0.0;
for (int i = 0; i &amp;#x3C; 10; ++i) {
    sum += 0.1;
}
if (sum == 1.0) { /* 可能false */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;74. 堆与栈的优缺点比较&lt;/h2&gt;
&lt;h3&gt;栈 - Stack&lt;/h3&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快速分配/释放：栈内存的分配和释放只是移动栈指针，速度快&lt;/li&gt;
&lt;li&gt;自动管理：函数返回时自动释放，无需手动管理&lt;/li&gt;
&lt;li&gt;缓存友好：栈数据通常位于缓存热点区域，访问速度快&lt;/li&gt;
&lt;li&gt;不会产生碎片：严格的 LIFO 顺序避免了内存碎片问题&lt;/li&gt;
&lt;li&gt;线程安全：每个线程有自己的栈，无需同步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;大小有限：栈空间通常较小（几 MB），不适合大对象&lt;/li&gt;
&lt;li&gt;生命周期固定：只能在函数调用期间存在，不能跨函数使用&lt;/li&gt;
&lt;li&gt;灵活性差：无法动态调整大小，必须是编译时已知的大小&lt;/li&gt;
&lt;li&gt;容易溢出：&lt;strong&gt;递归过深&lt;/strong&gt;或&lt;strong&gt;大对象&lt;/strong&gt;可能导致栈溢出（stack overflow）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;堆 - Heap&lt;/h3&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;大容量：可用内存通常远大于栈空间&lt;/li&gt;
&lt;li&gt;动态分配：可以在运行时决定分配大小和生命周期&lt;/li&gt;
&lt;li&gt;全局可访问：分配的内存可以被程序任何部分访问&lt;/li&gt;
&lt;li&gt;灵活性高：可以动态调整大小(如realloc)&lt;/li&gt;
&lt;li&gt;适合大数据：能够处理大型数据结构&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;分配速度慢：需要寻找合适的内存块，可能涉及系统调用&lt;/li&gt;
&lt;li&gt;手动管理特性：堆内存需要显式分配 (&lt;code&gt;new/malloc&lt;/code&gt;) 和释放 (&lt;code&gt;delete/free&lt;/code&gt;)，存在&lt;strong&gt;内存泄漏&lt;/strong&gt;风险&lt;/li&gt;
&lt;li&gt;内存碎片：频繁分配释放可能导致碎片&lt;/li&gt;
&lt;li&gt;同步开销：多线程环境下需要同步机制&lt;/li&gt;
&lt;li&gt;缓存不友好：堆分配的数据可能分散在内存各处&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;使用建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;优先使用栈：适合小型、短生命周期的数据&lt;/li&gt;
&lt;li&gt;必要时用堆：大型数据或需要长生命周期时使用&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;75. 关于 &lt;code&gt;shared_ptr&lt;/code&gt; 的注意事项（20. 智能指针）&lt;/h2&gt;
&lt;p&gt;关于 &lt;code&gt;shared_ptr&lt;/code&gt; 的注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不要用一个裸指针初始化多个 &lt;code&gt;shared_ptr&lt;/code&gt;&lt;/strong&gt;，会出现 &lt;em&gt;&lt;strong&gt;double_free&lt;/strong&gt;&lt;/em&gt; 导致程序崩溃&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过 &lt;code&gt;shared_from_this()&lt;/code&gt; 返回 this 指针，不要把 this 指针作为 &lt;code&gt;shared_ptr&lt;/code&gt; 返回出来，因为 &lt;code&gt;this&lt;/code&gt; 指针本质就是裸指针，通过 this 返回可能会导致重复析构，&lt;strong&gt;不能把 this 指针交给智能指针管理&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;尽量使用 &lt;code&gt;std::make_shared&amp;#x3C;T&gt;()&lt;/code&gt;，少用 &lt;code&gt;new&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不要 &lt;code&gt;delete&lt;/code&gt; &lt;code&gt;get()&lt;/code&gt; 返回的裸指针&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不是 &lt;code&gt;new&lt;/code&gt; 出来的空间要自定义删除器&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;要避免循环引用&lt;/strong&gt;，循环引用导致内存永远不会被释放，造成内存泄漏（不在赘述）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 不要用一个裸指针初始化多个 &lt;code&gt;shared_ptr&lt;/code&gt;（会导致 double free）&lt;/h3&gt;
&lt;p&gt;问题场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int* raw_ptr = new int(42);
std::shared_ptr&amp;#x3C;int&gt; sp1(raw_ptr);
std::shared_ptr&amp;#x3C;int&gt; sp2(raw_ptr);  // 危险！
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;两个独立的 &lt;code&gt;shared_ptr&lt;/code&gt; 会&lt;strong&gt;各自维护&lt;/strong&gt;一个引用计数控制块（相互独立）&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;sp1&lt;/code&gt; 和 &lt;code&gt;sp2&lt;/code&gt; 销毁时都会尝试释放 &lt;code&gt;raw_ptr&lt;/code&gt;，导致 &lt;strong&gt;双重释放&lt;/strong&gt;（double free）&lt;/li&gt;
&lt;li&gt;结果通常是程序崩溃或未定义行为&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正确做法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 方法1：直接使用 make_shared
// make_shared 一次性分配内存，包含控制块（引用计数、弱引用计数等）；对象存储空间（存储实际值 42）
auto sp1 = std::make_shared&amp;#x3C;int&gt;(42);
auto sp2 = sp1;  // 只是复制指针并增加引用计数，两个 shared_ptr 指向同一个控制块，共享所有权

// 方法2：如果必须从裸指针创建，确保只创建一次 shared_ptr
int* raw_ptr = new int(42);
std::shared_ptr&amp;#x3C;int&gt; sp1(raw_ptr);
std::shared_ptr&amp;#x3C;int&gt; sp2 = sp1;  // 复制的是控制块指针，不是重新创建控制块，共享同一个控制块
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 正确使用 &lt;code&gt;shared_from_this()&lt;/code&gt; 而不是直接返回 &lt;code&gt;this&lt;/code&gt; 指针&lt;/h3&gt;
&lt;p&gt;问题场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class BadExample {
public:
    std::shared_ptr&amp;#x3C;BadExample&gt; get_this() {
        return std::shared_ptr&amp;#x3C;BadExample&gt;(this);  // 危险！
    }
};

auto obj = std::make_shared&amp;#x3C;BadExample&gt;();
auto another_ref = obj-&gt;get_this();  // 创建了独立的控制块
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这会创建两个独立的 &lt;code&gt;shared_ptr&lt;/code&gt; 控制块&lt;/li&gt;
&lt;li&gt;当两个 &lt;code&gt;shared_ptr&lt;/code&gt; 销毁时都会尝试析构同一个对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正确做法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class GoodExample : public std::enable_shared_from_this&amp;#x3C;GoodExample&gt; {
public:
    std::shared_ptr&amp;#x3C;GoodExample&gt; get_this() {
        return shared_from_this();  // 安全
    }
};

auto obj = std::make_shared&amp;#x3C;GoodExample&gt;();
auto another_ref = obj-&gt;get_this();  // 共享同一个控制块
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 优先使用 &lt;code&gt;std::make_shared&amp;#x3C;T&gt;()&lt;/code&gt; 而不是 &lt;code&gt;new&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;问题场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 不推荐
std::shared_ptr&amp;#x3C;MyClass&gt; sp(new MyClass(arg1, arg2));

// 推荐
auto sp = std::make_shared&amp;#x3C;MyClass&gt;(arg1, arg2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;性能更好：单次内存分配（对象 + 控制块）&lt;/li&gt;
&lt;li&gt;异常安全：不会在 &lt;code&gt;new&lt;/code&gt; 和 &lt;code&gt;shared_ptr&lt;/code&gt; 构造之间发生泄漏&lt;/li&gt;
&lt;li&gt;代码更简洁：不需要重复类型名称&lt;/li&gt;
&lt;li&gt;缓存友好：对象和控制块内存相邻&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例外情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要自定义删除器时&lt;/li&gt;
&lt;li&gt;需要指定特殊的内存分配方式时&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 不要 &lt;code&gt;delete&lt;/code&gt; &lt;code&gt;get()&lt;/code&gt; 返回的裸指针&lt;/h3&gt;
&lt;p&gt;问题场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto sp = std::make_shared&amp;#x3C;int&gt;(42);
int* raw_ptr = sp.get();
delete raw_ptr;  // 灾难性错误！

// 当 sp 超出作用域时，会再次尝试删除已删除的内存
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_ptr&lt;/code&gt; &lt;strong&gt;仍然拥有内存所有权&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;手动 &lt;code&gt;delete&lt;/code&gt; 会导致：
&lt;ul&gt;
&lt;li&gt;double free&lt;/li&gt;
&lt;li&gt;控制块状态不一致&lt;/li&gt;
&lt;li&gt;未定义行为（通常崩溃）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正确做法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto sp = std::make_shared&amp;#x3C;int&gt;(42);
int* raw_ptr = sp.get();
// 仅使用 raw_ptr 进行读取/写入操作，绝不手动删除它
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 非 &lt;code&gt;new&lt;/code&gt; 分配的内存需要自定义删除器&lt;/h3&gt;
&lt;p&gt;问题场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 从 malloc 分配的内存
void* mem = malloc(1024);
std::shared_ptr&amp;#x3C;void&gt; sp(mem);  // 错误！会用 delete 而不是 free

// 文件指针
FILE* fp = fopen(&quot;file.txt&quot;, &quot;r&quot;);
std::shared_ptr&amp;#x3C;FILE&gt; sp(fp);  // 错误！会用 delete 而不是 fclose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正确做法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 使用自定义删除器（lambda 表达式作为删除器）
void* mem = malloc(1024);
std::shared_ptr&amp;#x3C;void&gt; sp(mem, free);  // 使用 free 作为删除器

FILE* fp = fopen(&quot;file.txt&quot;, &quot;r&quot;);
std::shared_ptr&amp;#x3C;FILE&gt; sp(fp, [](FILE* f) { fclose(f); });

// 对于数组
int* arr = new int[10];
std::shared_ptr&amp;#x3C;int&gt; sp(arr, [](int* p) { delete[] p; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见删除器场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;C 风格内存分配（&lt;code&gt;malloc/calloc/realloc&lt;/code&gt;）→ 使用 &lt;code&gt;free&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;文件操作（&lt;code&gt;fopen&lt;/code&gt;）→ 使用 &lt;code&gt;fclose&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;系统资源（套接字、句柄等）→ 使用对应的释放函数&lt;/li&gt;
&lt;li&gt;数组 → 使用 &lt;code&gt;delete[]&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6. 避免循环引用导致的内存泄露&lt;/h3&gt;
&lt;h4&gt;问题场景 1&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class A;
class B;

class A {
public:
    std::shared_ptr&amp;#x3C;B&gt; b;
};

class B {
public:
    std::shared_ptr&amp;#x3C;A&gt; a;
};

int main() {
    std::shared_ptr&amp;#x3C;A&gt; ap = std::make_shared&amp;#x3C;A&gt;();
    std::shared_ptr&amp;#x3C;B&gt; bp = std::make_shared&amp;#x3C;B&gt;();
    ap-&gt;b = bp;
    bp-&gt;a = ap;
    // 此时，a 和 b 相互持有对方的 shared_ptr，形成循环引用
    // 程序结束时，a 和 b 的引用计数都不会降为零，导致内存泄漏
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;问题场景 2&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Node {
public:
    std::shared_ptr&amp;#x3C;Node&gt; next;
    std::shared_ptr&amp;#x3C;Node&gt; prev;  // 双向链表导致循环引用
    ~Node() { std::cout &amp;#x3C;&amp;#x3C; &quot;Node destroyed\n&quot;; }
};

auto node1 = std::make_shared&amp;#x3C;Node&gt;();
auto node2 = std::make_shared&amp;#x3C;Node&gt;();
node1-&gt;next = node2;
node2-&gt;prev = node1;  // 循环引用形成！
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;node1&lt;/code&gt; 和 &lt;code&gt;node2&lt;/code&gt; 离开作用域时：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;node1&lt;/code&gt; 的引用计数从 1→0？不，因为 &lt;code&gt;node2-&gt;prev&lt;/code&gt; 还持有引用（实际从 2→1）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node2&lt;/code&gt; 的引用计数同样从 2→1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;结果：&lt;strong&gt;两者引用计数永远不为 0&lt;/strong&gt;，内存永远不会释放&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;node1 [refcount=2] --&gt; Node1对象
  ↑next               ↓prev
Node2对象 &amp;#x3C;-- [refcount=2] node2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方案：&lt;code&gt;weak_ptr&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class SafeNode {
public:
    std::shared_ptr&amp;#x3C;SafeNode&gt; next;
    std::weak_ptr&amp;#x3C;SafeNode&gt; prev;  // 使用weak_ptr
    
    ~SafeNode() { std::cout &amp;#x3C;&amp;#x3C; &quot;SafeNode destroyed\n&quot;; }
};

auto node1 = std::make_shared&amp;#x3C;SafeNode&gt;();
auto node2 = std::make_shared&amp;#x3C;SafeNode&gt;();
node1-&gt;next = node2;
node2-&gt;prev = node1;  // weak_ptr不会增加引用计数
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;何时会出现循环引用？&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;双向链表、树结构等复杂数据结构&lt;/li&gt;
&lt;li&gt;对象相互持有对方的 &lt;code&gt;shared_ptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;父子对象互相强引用&lt;/li&gt;
&lt;li&gt;观察者模式中主体和观察者互相持有&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;76. &lt;code&gt;std::shared_ptr&lt;/code&gt; 可以通过 &lt;code&gt;std::make_shared&lt;/code&gt; 和直接使用 &lt;code&gt;new&lt;/code&gt; 表达式构造，二者有什么区别？&lt;/h2&gt;
&lt;p&gt;在 C++ 中，&lt;code&gt;std::shared_ptr&lt;/code&gt; 可以通过 &lt;code&gt;std::make_shared&lt;/code&gt; 和直接使用 &lt;code&gt;new&lt;/code&gt; 表达式构造，但二者在内存管理、性能和异常安全性方面有显著区别：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 推荐方式：高效且安全
auto sp1 = std::make_shared&amp;#x3C;int&gt;(42);

// 不推荐：性能更低且可能泄漏
std::shared_ptr&amp;#x3C;int&gt; sp2(new int(42));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. 内存分配方式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;make_shared&lt;/code&gt;：一次性分配内存，同时存储对象本身和控制块（引用计数等）。这是更高效的内存布局，减少了内存碎片和分配次数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto sp = std::make_shared&amp;#x3C;Widget&gt;(args...); // 单次分配
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;new&lt;/code&gt; + &lt;code&gt;shared_ptr&lt;/code&gt; 构造函数：需要两次内存分配，一次为对象（&lt;code&gt;new&lt;/code&gt;），另一次为控制块（由 &lt;code&gt;shared_ptr&lt;/code&gt; 构造函数触发）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::shared_ptr&amp;#x3C;Widget&gt; sp(new Widget(args...)); // 两次分配
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 性能差异&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;make_shared&lt;/code&gt; 更快：单次内存分配减少了开销，尤其当频繁创建/销毁 &lt;code&gt;shared_ptr&lt;/code&gt; 时更明显。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new&lt;/code&gt; 额外开销：两次分配可能引入性能损耗（尤其是对小型对象）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;解释&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;每次 &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;malloc&lt;/code&gt; 都是一次系统调用：操作系统需要查找合适的内存块、更新内存管理数据结构（如空闲链表），可能触发缺页中断或内核态切换，这些操作本身就有固定开销。两次分配就会有双倍开销。&lt;/p&gt;
&lt;p&gt;从内存局部性角度来看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;make_shared&lt;/code&gt; 的内存是连续的，对象和控制块在单块内存中紧密排列，缓存命中率更高。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new&lt;/code&gt; 的内存可能是分散的，对象和控制块位于不同内存区域，访问时可能触发多次缓存加载，同时产生更多内存碎片。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 异常安全性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;make_shared&lt;/code&gt; 是异常安全的：若构造函数抛出异常，不会发生内存泄漏（因为内存已由智能指针系统托管）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;process(std::make_shared&amp;#x3C;Widget&gt;(a, b), may_throw()); // 安全
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;new&lt;/code&gt; 可能泄漏：如果 &lt;code&gt;new&lt;/code&gt; 成功但 &lt;code&gt;shared_ptr&lt;/code&gt; 构造前发生异常，对象不会被释放：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;process(std::shared_ptr&amp;#x3C;Widget&gt;(new Widget(a, b)), may_throw()); // 可能泄漏
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 对象生命周期的影响&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;make_shared&lt;/code&gt; 的延迟释放：对象内存和控制块是连续的，即使引用计数归零，&lt;strong&gt;对象占用的内存可能直到控制块也被释放时才归还&lt;/strong&gt;（例如弱引用 &lt;code&gt;weak_ptr&lt;/code&gt; 仍存在时）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new&lt;/code&gt; 的独立释放：对象内存和控制块分离，对象内存会在引用计数归零时立即释放，控制块则等待所有 &lt;code&gt;weak_ptr&lt;/code&gt; 释放后才回收。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 使用场景限制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;必须用 &lt;code&gt;new&lt;/code&gt; 的情况：需要自定义删除器或需从已有裸指针构造时。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::shared_ptr&amp;#x3C;Widget&gt; sp(new Widget, custom_deleter);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;优先用 &lt;code&gt;make_shared&lt;/code&gt;：默认情况下推荐使用，除非有特殊需求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;| 特性         | &lt;code&gt;make_shared&lt;/code&gt;        | &lt;code&gt;new&lt;/code&gt; + &lt;code&gt;shared_ptr&lt;/code&gt;     |
| ------------ | -------------------- | ------------------------ |
| 内存分配次数 | 1 次                 | 2 次                     |
| 异常安全     | 是                   | 否（可能泄漏）           |
| 内存释放时机 | 对象和控制块一起释放 | 对象先释放，控制块后释放 |
| 自定义删除器 | 不支持               | 支持                     |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：默认使用 &lt;code&gt;make_shared&lt;/code&gt;，除非需要自定义删除器或特殊内存管理。&lt;/p&gt;
&lt;h2&gt;77. &lt;code&gt;shared_ptr&lt;/code&gt; 的引用计数是在栈还是堆？&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;std::shared_ptr&lt;/code&gt; 的实现中，引用计数（控制块）存储在堆上，为了&lt;strong&gt;共享&lt;/strong&gt;这个这个 reference count 值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_ptr&lt;/code&gt; 和 &lt;code&gt;weak_ptr&lt;/code&gt; 可能被拷贝、传递到不同的作用域（如函数调用、线程间共享）。如果引用计数在栈上，它会在栈帧销毁时被释放，导致其他指针无法正确跟踪计数。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;make_shared&lt;/code&gt; 会将对象数据和引用计数分配在同一块堆内存中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new&lt;/code&gt; + &lt;code&gt;shared_ptr&lt;/code&gt; 则是将对象数据和引用计数分配在两块独立的堆内存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;虽然引用计数本身在堆上，但 &lt;code&gt;shared_ptr&lt;/code&gt; 的实例（即指针对象本身）可以存储在栈上&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;总结&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;| 存储位置   | 内容                         | 原因                                                         |
| :--------- | :--------------------------- | :----------------------------------------------------------- |
| &lt;strong&gt;堆内存&lt;/strong&gt; | 引用计数（控制块）、对象数据 | 需要动态生命周期管理，支持多指针共享，跨作用域存活。         |
| &lt;strong&gt;栈内存&lt;/strong&gt; | &lt;code&gt;shared_ptr&lt;/code&gt; 实例本身        | 栈对象自动析构，通过析构函数减少堆上的引用计数，必要时释放对象内存。 |&lt;/p&gt;
&lt;h2&gt;78. 基类的虚函数可以声明为 private 吗&lt;/h2&gt;
&lt;p&gt;基类的虚函数可以声明为 &lt;code&gt;private&lt;/code&gt;，但这会影响其可访问性和多态行为的使用方式。&lt;/p&gt;
&lt;h3&gt;语法允许，但访问受限&lt;/h3&gt;
&lt;p&gt;从语法层面，C++ 允许虚函数为 &lt;code&gt;private&lt;/code&gt;，编译器不会报错。&lt;/p&gt;
&lt;p&gt;派生类不能通过基类指针/引用直接访问 &lt;code&gt;private&lt;/code&gt; 虚函数（违反访问控制规则）&lt;/p&gt;
&lt;h3&gt;多态行为仍然有效&lt;/h3&gt;
&lt;p&gt;如果基类提供公有方法调用私有虚函数，派生类重写该私有虚函数后，多态仍能正常工作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;派生类重写 &lt;code&gt;foo()&lt;/code&gt; 时也必须为 &lt;code&gt;private&lt;/code&gt;（访问权限可以更宽松，但不能更严格）。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Base {
public:
    void callFoo() { foo(); }  // 公有接口调用私有虚函数
private:
    virtual void foo() { std::cout &amp;#x3C;&amp;#x3C; &quot;Base::foo&quot;; }
};

class Derived : public Base {
private:
    void foo() override { std::cout &amp;#x3C;&amp;#x3C; &quot;Derived::foo&quot;; }  // 重写私有虚函数
};

int main() {
    Base* ptr = new Derived;
    ptr-&gt;callFoo();  // 输出 &quot;Derived::foo&quot;（多态生效）
    delete ptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;对比其他权限&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;| 虚函数权限  | 派生类能否直接调用 | 派生类能否重写        | 外部代码能否调用 |
| :---------- | :----------------- | :-------------------- | :--------------- |
| &lt;code&gt;public&lt;/code&gt;    | ✅                  | ✅                     | ✅                |
| &lt;code&gt;protected&lt;/code&gt; | ✅                  | ✅                     | ❌                |
| &lt;code&gt;private&lt;/code&gt;   | ❌                  | ✅（需同权限或更宽松） | ❌                |&lt;/p&gt;
&lt;h2&gt;79. 如果有 100 个对象，vector、list、map 哪个占用内存高&lt;/h2&gt;
&lt;p&gt;这个问题本质上考察 STL 容器的内存占用特性。假设我们存放 100 个对象（比如大小相同的自定义类对象），不同容器的内存开销差别主要来自于元素存储方式和额外的结构体开销。&lt;/p&gt;
&lt;h3&gt;1. &lt;code&gt;vector&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;存储方式&lt;/strong&gt;：底层是 &lt;strong&gt;动态数组&lt;/strong&gt;，所有元素连续存放。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;额外开销&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;仅仅是 &lt;code&gt;vector&lt;/code&gt; 自身维护的三个指针（begin/end/capacity_end），常见实现 24 字节左右。&lt;/li&gt;
&lt;li&gt;可能有 &lt;strong&gt;容量冗余&lt;/strong&gt;（capacity ≥ size），但不是很大。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存占用&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;接近 &lt;strong&gt;100 × sizeof(对象)&lt;/strong&gt;，几乎没有额外 per-element 的开销。&lt;/li&gt;
&lt;li&gt;在三者中最节省内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. &lt;code&gt;list&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;存储方式&lt;/strong&gt;：双向链表，每个节点独立分配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;额外开销&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;每个节点除了存放对象，还要存放两个指针（prev/next），常见实现 16 字节或更多。&lt;/li&gt;
&lt;li&gt;由于频繁分配，可能带来额外的 &lt;strong&gt;堆分配和内存碎片开销&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存占用&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;大约 &lt;strong&gt;100 × (sizeof(对象) + 2 × sizeof(指针))&lt;/strong&gt;，比 &lt;code&gt;vector&lt;/code&gt; 高不少。&lt;/li&gt;
&lt;li&gt;在这三者里通常是最浪费内存的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. &lt;code&gt;map&lt;/code&gt;（通常是红黑树）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;存储方式&lt;/strong&gt;：红黑树节点，每个节点存放 &lt;code&gt;(key, value)&lt;/code&gt;，还有指针和颜色信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;额外开销&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;一个节点要维护 &lt;strong&gt;父、左、右指针&lt;/strong&gt;（3 个指针 = 24 字节）+ &lt;strong&gt;颜色位&lt;/strong&gt;（通常填充到 4 字节）。&lt;/li&gt;
&lt;li&gt;还要存放键和值对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存占用&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;大约 &lt;strong&gt;100 × (sizeof(key) + sizeof(value) + 3 × sizeof(指针) + padding)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果 key/value 很小（比如 int/int），开销主要是结构体本身，&lt;strong&gt;比 list 更大&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;结论（内存占用比较）&lt;/h3&gt;
&lt;p&gt;在存放 100 个对象时，三者大致的内存占用从低到高为：&lt;code&gt;vector&lt;/code&gt; &amp;#x3C; &lt;code&gt;list&lt;/code&gt; &amp;#x3C; &lt;code&gt;map&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vector&lt;/code&gt;：最节省空间，几乎没有额外 per-element 开销。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list&lt;/code&gt;：由于链表节点指针和堆分配，开销中等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt;：红黑树节点开销更大（指针 + 平衡因子等），通常是最高的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;80. 虚函数表和虚函数指针存储在哪里？&lt;/h2&gt;
&lt;h3&gt;虚函数表（vtable）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;位置：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编译器生成的全局静态表&lt;/strong&gt;，在程序的 &lt;strong&gt;只读数据区（.rodata）&lt;/strong&gt; 或类似的静态存储区域。&lt;/li&gt;
&lt;li&gt;每个包含虚函数的类（以及其派生类）通常对应一张或多张虚函数表。&lt;/li&gt;
&lt;li&gt;表的内容是 &lt;strong&gt;函数指针数组&lt;/strong&gt;，指向该类对应的虚函数实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;特点：
&lt;ul&gt;
&lt;li&gt;程序运行时不会修改表的位置，只会改变对象中的指针指向哪一张表。&lt;/li&gt;
&lt;li&gt;继承 + 覆盖虚函数时，子类的 vtable 会覆盖父类条目。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;虚函数指针（vptr）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;位置&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;在 &lt;strong&gt;对象实例的内存中&lt;/strong&gt;，通常存放在对象的头部。&lt;/li&gt;
&lt;li&gt;每个对象都有一个 vptr，指向对应类的 vtable。&lt;/li&gt;
&lt;li&gt;如果类有多个虚继承/多继承，可能会有多个 vptr。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;编译器在构造函数中插入代码，把对象的 vptr 指向正确的 vtable。&lt;/li&gt;
&lt;li&gt;析构时，也会相应调整 vptr 以保证多态析构正确。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;举例与总结&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Base {
public:
    virtual void foo();
};

class Derived : public Base {
public:
    void foo() override;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;程序里会生成两张 vtable：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Base&lt;/code&gt; 的 vtable，里面有 &lt;code&gt;&amp;#x26;Base::foo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Derived&lt;/code&gt; 的 vtable，里面有 &lt;code&gt;&amp;#x26;Derived::foo&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Base&lt;/code&gt; 或 &lt;code&gt;Derived&lt;/code&gt; 对象实例的内存中，都会有一个 &lt;strong&gt;vptr&lt;/strong&gt; 指针，指向相应的 vtable。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;虚函数表（vtable）&lt;/strong&gt;：编译器生成的全局静态表，存放在只读数据段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚函数指针（vptr）&lt;/strong&gt;：存放在对象实例内存中（通常在开头），指向该对象对应的 vtable。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;81. 多线程编程一般用哪些库函数？&lt;/h2&gt;
&lt;h3&gt;C 语言层面&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;POSIX Threads (pthread)&lt;/strong&gt;：&lt;code&gt;pthread_create&lt;/code&gt;、&lt;code&gt;pthread_join&lt;/code&gt;、&lt;code&gt;pthread_mutex_lock&lt;/code&gt;、&lt;code&gt;pthread_cond_wait&lt;/code&gt; 等，Linux/Unix 常用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C11 标准库&lt;/strong&gt;：&lt;code&gt;thrd_create&lt;/code&gt;、&lt;code&gt;mtx_lock&lt;/code&gt;（但在实际项目里用得少）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;C++ 层面&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C++11 标准库&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::thread&lt;/code&gt;（线程创建/管理）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::mutex&lt;/code&gt;、&lt;code&gt;std::recursive_mutex&lt;/code&gt;、&lt;code&gt;std::timed_mutex&lt;/code&gt;（互斥量）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::lock_guard&lt;/code&gt;、&lt;code&gt;std::unique_lock&lt;/code&gt;（RAII 封装锁）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::condition_variable&lt;/code&gt;（条件变量）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::future&lt;/code&gt; / &lt;code&gt;std::promise&lt;/code&gt; / &lt;code&gt;std::async&lt;/code&gt;（任务并发模型）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;82. 为什么需要锁？什么时候需要多线程？举个例子&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么需要锁&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;多线程共享内存 → 存在 &lt;strong&gt;竞态条件（race condition）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果两个线程同时读写同一变量，可能导致数据不一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;锁（mutex）&lt;/strong&gt; 的作用就是保证某段代码在同一时刻只有一个线程能进入，维持数据一致性。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;什么时候需要多线程&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计算密集型任务&lt;/strong&gt;：利用多核 CPU，提高吞吐量（例如矩阵运算、图像处理）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I/O 密集型任务&lt;/strong&gt;：一个线程阻塞 I/O 时，其他线程可以继续执行（例如服务器同时处理多个客户端请求）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;举例&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;写一个多线程日志系统：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多个工作线程处理业务逻辑，把日志消息写入一个共享队列。&lt;/li&gt;
&lt;li&gt;一个单独线程从队列取日志写文件。&lt;/li&gt;
&lt;li&gt;共享队列必须加锁，否则多线程写入会导致数据错乱。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;83. 多线程场景下出现内存泄漏怎么调试解决？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;原因&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;某些线程申请的内存没有释放，线程退出后仍然占用。&lt;/li&gt;
&lt;li&gt;使用 TLS（线程局部存储）或全局变量，线程结束时忘记清理。&lt;/li&gt;
&lt;li&gt;线程之间交互复杂，某些分支忘记释放内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;常见调试手段&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工具&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;valgrind --tool=memcheck&lt;/code&gt;（Linux）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;asan&lt;/code&gt;（AddressSanitizer）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gdb&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;用 RAII（C++）避免手动 &lt;code&gt;new/delete&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;尽量使用智能指针（&lt;code&gt;std::unique_ptr&lt;/code&gt;、&lt;code&gt;std::shared_ptr&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;保证线程退出时执行清理逻辑（析构函数/&lt;code&gt;pthread_cleanup_push&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分而治之&lt;/strong&gt;：把怀疑的线程单独跑，缩小问题范围。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下文详细介绍下「gdb 多线程调试」与「打日志 + 线上定位 + 复现副本」这两种方式。&lt;/p&gt;
&lt;p&gt;当然能用 gdb 调多线程，而且&lt;strong&gt;线上定位&lt;/strong&gt;常常要配合“打日志 + 复现副本”的办法一起用。下面给你一套“面试可讲、实际可用”的流程和要点。&lt;/p&gt;
&lt;h3&gt;(1) 用 gdb 调多线程的实用套路&lt;/h3&gt;
&lt;h4&gt;1) 基本操作（本地或 attach 线上进程）&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 运行时调试
gdb ./your_program
(gdb) run ...

# 附加到线上/本地正在跑的进程
gdb -p &amp;#x3C;PID&gt;

# 一次性抓所有线程的栈
(gdb) thread apply all bt
(gdb) thread apply all bt full     # 带局部变量
(gdb) info threads                 # 线程列表
(gdb) thread &amp;#x3C;id&gt;                  # 切换线程
(gdb) bt                           # 当前线程回溯
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2) 让单步/断点更可控（避免线程乱切）&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;(gdb) set scheduler-locking on     # 单步时不让其它线程抢占（on/step可选）
(gdb) set pagination off
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3) 条件断点 / 竞争点观察&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 只在指定线程/条件触发
(gdb) break file.cpp:123 if pthread_self() == target_tid &amp;#x26;&amp;#x26; counter &amp;#x3C; 0

# 监视内存写（找“谁改了这个变量/指针”）
(gdb) watch some_global_ptr
(gdb) rwatch x   # 读监视
(gdb) awatch x   # 读写监视
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4) 线程与锁问题的专招&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;死锁/卡住&lt;/strong&gt;：先 &lt;code&gt;gdb -p PID&lt;/code&gt;，再 &lt;code&gt;thread apply all bt full&lt;/code&gt; 看各线程卡在什么锁/条件变量处；很多时候能直接看出两个线程的互相等待链。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;条件变量&lt;/strong&gt;：在 &lt;code&gt;pthread_cond_wait&lt;/code&gt;、&lt;code&gt;pthread_mutex_lock&lt;/code&gt; 处下断点或打条件断点，确认唤醒与加锁顺序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;跟踪线程创建&lt;/strong&gt;：记录是谁创建了问题线程。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;(gdb) break pthread_create
(gdb) commands
&gt; bt
&gt; c
&gt; end
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5) 核心转储（coredump）事后排查&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ulimit -c unlimited
# 触发崩溃后
gdb ./your_program core
(gdb) thread apply all bt full
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以线上“留存一份 core”，再在离线环境慢慢剖析。&lt;/p&gt;
&lt;h4&gt;6) 录制/重放&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;rr&lt;/strong&gt;（Linux）：&lt;code&gt;rr record ./prog&lt;/code&gt;，复现后用 &lt;code&gt;rr replay&lt;/code&gt; 进 gdb，可&lt;strong&gt;时间回溯&lt;/strong&gt;到变量被改写的一刻，定位数据竞争/时序问题非常好用。&lt;/li&gt;
&lt;li&gt;gdb 的 &lt;code&gt;record full&lt;/code&gt; 也能用，但 &lt;code&gt;rr&lt;/code&gt; 更稳定。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(2) 打日志→线上定位→复现副本→看日志&lt;/h3&gt;
&lt;p&gt;这是大厂很常见、也很靠谱的&lt;strong&gt;生产级排障方法&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;打结构化日志&lt;/strong&gt;（并带上标识）
&lt;ul&gt;
&lt;li&gt;必带：时间戳、&lt;strong&gt;线程ID&lt;/strong&gt;、请求ID/traceID、关键状态（队列长度、锁等待时长、对象地址/指针等）。&lt;/li&gt;
&lt;li&gt;在&lt;strong&gt;锁关键点&lt;/strong&gt;打点：&lt;code&gt;lock acquire start/ok/timeout&lt;/code&gt;、&lt;code&gt;cond wait/signal&lt;/code&gt;、&lt;code&gt;enqueue/dequeue&lt;/code&gt;、&lt;code&gt;state transition&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;遇到潜在死锁，记录&lt;strong&gt;锁名/顺序&lt;/strong&gt;和&lt;strong&gt;持有时长&lt;/strong&gt;，方便识别锁顺序反转。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定位线上问题副本&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;用日志把&lt;strong&gt;异常请求/异常时间窗&lt;/strong&gt;圈出来（比如特定用户、某个 shard、某台机器）。&lt;/li&gt;
&lt;li&gt;从线上环境&lt;strong&gt;抽一个最小有问题的副本&lt;/strong&gt;（相同配置/数据切片/版本/流量）到灰度或隔离环境。&lt;/li&gt;
&lt;li&gt;这样能在不影响大盘的情况下&lt;strong&gt;复现&lt;/strong&gt;，同时你可以随意加日志、开更高日志级别、甚至 attach gdb。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;看日志 + 还原时序&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;通过 traceID 串起多个线程/模块的跨调用链，按照时间戳排序，重放“谁先拿了哪个锁、谁等在 cond 上”，从而定位&lt;strong&gt;竞态/死锁/饥饿&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;日志中记录的&lt;strong&gt;内存地址/对象ID&lt;/strong&gt;可以让你把若干条看似无关的日志对应到&lt;strong&gt;同一对象实例&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;必要时动用在线栈&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;现场卡死时，用 &lt;code&gt;gdb -p PID&lt;/code&gt; 或运维脚本（比如发送信号触发 &lt;code&gt;backtrace()&lt;/code&gt; dump 全线程栈）把&lt;strong&gt;全线程栈&lt;/strong&gt;打印出来，和日志时间窗对比，锁点一目了然。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修复后再灰度验证&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在副本/灰度环境验证“日志告警消失、锁等待时长下降、吞吐恢复”，再全量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这套方法的核心是：&lt;strong&gt;用日志重建并发时序&lt;/strong&gt;，用&lt;strong&gt;副本&lt;/strong&gt;规避线上风险，用 &lt;strong&gt;gdb/栈/录制&lt;/strong&gt;补齐细节。&lt;/p&gt;
&lt;h2&gt;84. 程序的内存布局是怎样的？&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250821-Ovbyi3.png&quot; alt=&quot;image-20240725233029022&quot;&gt;&lt;/p&gt;
&lt;p&gt;一个典型 C/C++ 程序在内存里的分布大致如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;代码段：包括二进制可执行代码，通常只读，还可共享（多个进程运行同一程序时只存一份）；&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;静态存储区：数据段 + BSS 段&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据段：包括已初始化的静态常量和全局变量；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BSS 段：包括未初始化的静态变量和全局变量；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;堆（比如 &lt;code&gt;malloc/new&lt;/code&gt;）：包括动态分配的内存，从低地址开始向上增长，程序员负责管理，容易泄露；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件映射区（比如 &lt;code&gt;mmap&lt;/code&gt;）：包括共享库、文件映射、匿名映射、共享内存等；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;栈：包括局部变量和函数调用的上下文（参数、返回地址）等，函数调用或返回会自动分配与释放后。栈的大小是固定的，一般是 8 MB。当然系统也提供了参数，以便我们自定义大小；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内核空间（对用户程序不可见）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上图中的内存布局可以看到，代码段下面还有一段内存空间的（灰色部分），这一块区域是「保留区」，之所以要有保留区这是因为在大多数的系统里，我们认为比较小数值的地址不是一个合法地址，例如，我们通常在 C 的代码里会将无效的指针赋值为 NULL。因此，这里会出现一段不可访问的内存保留区，防止程序因为出现 bug，导致读或写了一些小内存地址的数据，而使得程序跑飞。&lt;/p&gt;
&lt;p&gt;在这 7 个内存段中，堆和文件映射段的内存是动态分配的。比如说，使用 C 标准库的 &lt;code&gt;malloc()&lt;/code&gt; 或者 &lt;code&gt;mmap()&lt;/code&gt;，就可以分别在堆和文件映射段动态分配内存。&lt;/p&gt;
&lt;h2&gt;85. delete 释放内存的时候并不知道内存大小，如何释放？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;涉及 &lt;code&gt;new&lt;/code&gt; 和 &lt;code&gt;delete&lt;/code&gt; 源码及流程&lt;/p&gt;
&lt;p&gt;链接：&lt;a href=&quot;https://www.bilibili.com/video/BV1ULYRziEbs/?spm_id_from=333.1387.homepage.video_card.click&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;pdd 二面&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;delete&lt;/code&gt; 运算符在释放内存时确实不需要显式指定大小，因为它依赖底层的内存管理器来跟踪分配块的信息。当使用 &lt;code&gt;new&lt;/code&gt; 分配内存时，内存管理器（如 glibc 的分配器）会在返回给用户的内存块前端或尾部存储额外的元数据（如分配大小、cookie 等），这些信息对用户透明。调用 &lt;code&gt;delete&lt;/code&gt; 时，运算符会根据传入的指针向前偏移一定位置来读取这些元数据，从而确定需要释放的内存块实际大小和边界，确保正确释放。因此，用户无需手动传递大小，所有细节由内存管理器和编译器生成的代码处理。&lt;/p&gt;
&lt;h2&gt;86. 一个空类大小是多少？如果有构造函数和析构函数呢？如果有虚函数？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 TEG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Q1：一个空类大小是多少？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 C++ 中，一个空类（没有任何非静态成员变量和虚函数）的大小通常为 &lt;strong&gt;1 字节&lt;/strong&gt;。这是因为编译器需要为每个对象分配一个唯一的地址标识，以确保不同对象在内存中拥有 distinct 的地址。如果大小为 0，可能导致多个对象地址相同，违反语言标准。需要注意的是，如果空类作为基类被继承，可能会发生空基类优化（EBO），此时基子对象不占用额外空间，从而节省内存。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Q2：如果空类只有构造函数和析构函数，该类大小是多少？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;大小仍然是 1 个字节。&lt;/strong&gt; 构造函数和析构函数是普通的成员函数，它们的代码并不存储在每一个对象实例中。这些函数在编译后位于代码段，所有该类的对象共享同一份函数代码。调用它们时，编译器会隐式地传入一个指向当前对象的 &lt;code&gt;this&lt;/code&gt; 指针。因此，添加普通的成员函数（包括构造和析构）不会影响对象实例的大小。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Q3：如果空类只有虚函数，该类大小是多少？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 64 位系统上，大小通常是 8 字节（一个指针的大小）；在 32 位系统上，通常是 4 字节。&lt;/p&gt;
&lt;p&gt;一旦一个类拥有虚函数，它就会拥有一个虚函数表（vtable），并且编译器会自动为该类的每一个对象实例添加一个隐藏的成员变量 —— 虚表指针（vptr）。这个 vptr 指向类的 vtable，用于在运行时实现多态（动态绑定）。因此，&lt;strong&gt;对象的大小会增加一个指针的开销&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;87. 指针的大小是多少？&lt;/h2&gt;
&lt;p&gt;指针的大小&lt;strong&gt;不取决于它指向的数据类型&lt;/strong&gt;，而&lt;strong&gt;完全取决于目标平台的寻址能力&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 32 位平台上，指针的大小是 4 字节，因为它需要能表示 $2^{32}$ 个不同的内存地址。&lt;/li&gt;
&lt;li&gt;在 64 位平台上，指针的大小是 8 字节，用于表示 $2^{64}$ 个地址空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;无论是指向 &lt;code&gt;int&lt;/code&gt;、&lt;code&gt;char&lt;/code&gt; 还是一个拥有虚函数的复杂类对象，所有数据指针的大小都遵循这个规则（函数指针可能在某些平台上有所不同）。&lt;/p&gt;
&lt;h2&gt;88. 内存对齐了解过吗，我如果不想对齐，怎么办？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 TEG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;内存对齐是编译器和硬件为了提升访问效率而实施的策略。&lt;/p&gt;
&lt;p&gt;如果不想对齐，可以通过编译器指令强制取消填充（如 GCC/Clang 的 &lt;code&gt;__attribute__((packed))&lt;/code&gt; 或 MSVC 的 &lt;code&gt;#pragma pack(1)&lt;/code&gt;），使结构体成员紧凑排列。但这样做有风险：未对齐访问在 x86 架构上会导致性能下降（多次内存访问），在其他架构（如 ARM）上可能直接触发硬件异常造成崩溃。除非有特殊需求（如协议解析或节省内存），否则不建议禁用对齐。&lt;/p&gt;
&lt;h2&gt;89. 讲讲 unordered_map 和 map 的底层实现和区别&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;腾讯 TEG 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;区别&lt;/h3&gt;
&lt;p&gt;| 特性            | &lt;code&gt;std::map&lt;/code&gt;                                     | &lt;code&gt;std::unordered_map&lt;/code&gt;                                  |
| :-------------- | :--------------------------------------------- | :---------------------------------------------------- |
| 底层数据结构    | &lt;strong&gt;红黑树&lt;/strong&gt; (一种自平衡的二叉搜索树)            | &lt;strong&gt;哈希表&lt;/strong&gt; (数组 + 链表/红黑树)                       |
| 元素顺序        | &lt;strong&gt;元素按 key 排序&lt;/strong&gt; (默认 &lt;code&gt;std::less&lt;/code&gt;，即升序) | &lt;strong&gt;元素无序&lt;/strong&gt; (顺序取决于哈希函数)                     |
| 搜索时间复杂度  | &lt;strong&gt;O(log n)&lt;/strong&gt;                                   | &lt;strong&gt;平均 O(1)&lt;/strong&gt;，最坏情况 O(n)                          |
| 插入时间复杂度  | &lt;strong&gt;O(log n)&lt;/strong&gt;                                   | &lt;strong&gt;平均 O(1)&lt;/strong&gt;，最坏情况 O(n)                          |
| 删除时间复杂度  | &lt;strong&gt;O(log n)&lt;/strong&gt;                                   | &lt;strong&gt;平均 O(1)&lt;/strong&gt;，最坏情况 O(n)                          |
| 迭代器稳定性    | &lt;strong&gt;稳定&lt;/strong&gt;（除非删除元素，否则迭代器始终有效）   | &lt;strong&gt;插入/删除可能使所有迭代器失效&lt;/strong&gt;                     |
| 需要为 key 定义 | &lt;code&gt;operator&amp;#x3C;&lt;/code&gt; 或 &lt;strong&gt;自定义比较器&lt;/strong&gt;                | &lt;code&gt;std::hash&lt;/code&gt; &lt;strong&gt;哈希函数&lt;/strong&gt; 和 &lt;code&gt;operator==&lt;/code&gt; &lt;strong&gt;相等比较&lt;/strong&gt; |
| 内存占用        | 通常较低（树节点开销）                         | 通常较高（需要维护数组桶和链表）                      |
| 常用场景        | 需要元素有序、顺序遍历、或要求最坏情况性能稳定 | 需要快速查找、插入、删除，且不关心顺序                |&lt;/p&gt;
&lt;h3&gt;底层实现详解&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;map&lt;/code&gt; - 基于红黑树 (Red-Black Tree)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据结构&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本质上是一颗&lt;strong&gt;二叉搜索树 (BST)&lt;/strong&gt;，这意味着任何节点的左子树所有节点的 key 都小于该节点的 key，右子树所有节点的 key 都大于该节点的 key。这使得中序遍历树时，可以得到有序的 key 序列。&lt;/li&gt;
&lt;li&gt;它更具体地是一颗&lt;strong&gt;红黑树&lt;/strong&gt;。红黑树是一种&lt;strong&gt;自平衡&lt;/strong&gt;的二叉搜索树。它通过为节点添加颜色属性（红或黑）和定义一系列旋转和重新着色的规则，来确保树始终保持大致平衡（没有一条路径会比其他路径长两倍以上）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如何工作&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;插入&lt;/strong&gt;：首先像普通的 BST 一样找到插入位置。插入新节点后（初始为红色），可能会破坏红黑树的平衡性质（如出现两个连续的红色节点）。这时需要通过&lt;strong&gt;旋转&lt;/strong&gt;（左旋、右旋）和&lt;strong&gt;重新着色&lt;/strong&gt;来恢复平衡。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查找&lt;/strong&gt;：从根节点开始，与当前节点的 key 比较。如果小于，进入左子树；如果大于，进入右子树；如果相等，则找到。由于树是平衡的，查找路径长度最多为树的高度 &lt;strong&gt;O(log n)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;删除&lt;/strong&gt;：同样先找到节点，执行 BST 删除操作后，可能会破坏平衡，需要再次通过旋转和重新着色来调整。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：元素始终有序，支持范围查询（如&lt;code&gt;lower_bound()&lt;/code&gt;），提供了稳定的 &lt;strong&gt;O(log n)&lt;/strong&gt; 操作时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：平均速度比哈希表慢，因为常数因子较大（需要多次比较和可能的内存跳跃）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;code&gt;unordered_map&lt;/code&gt; - 基于哈希表 (Hash Table)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据结构&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个&lt;strong&gt;数组&lt;/strong&gt;（通常称为“桶”bucket 数组），数组的每个元素是一个&lt;strong&gt;链表&lt;/strong&gt;的头指针（或一棵小红黑树的根节点）。&lt;/li&gt;
&lt;li&gt;在 C++11 中，标准要求使用“开链法”解决哈希冲突。在极端情况下（一个桶里元素太多），标准允许该桶用树 instead of 链表来实现，以避免最坏性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如何工作&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;插入&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;对 key 进行&lt;strong&gt;哈希函数&lt;/strong&gt;计算，得到一个整型的哈希值。&lt;/li&gt;
&lt;li&gt;用这个哈希值 &lt;strong&gt;&lt;code&gt;% 桶的数量&lt;/code&gt;&lt;/strong&gt; 来确定元素应该放在哪个桶里（即数组的哪个索引）。&lt;/li&gt;
&lt;li&gt;将键值对添加到这个桶对应的链表（或树）的末尾。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查找&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;同样先计算 key 的哈希值，找到对应的桶。&lt;/li&gt;
&lt;li&gt;然后遍历这个桶里的链表（或树），使用&lt;code&gt;operator==&lt;/code&gt; 进行精确匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重新哈希 (Rehashing)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;当元素数量超过&lt;code&gt;负载因子(load factor) * 桶的数量&lt;/code&gt;时，容器会自动进行重新哈希。&lt;/li&gt;
&lt;li&gt;这会创建一个新的、更大的桶数组，然后将所有现有元素重新计算哈希并插入到新的数组中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这个过程会使所有迭代器失效！&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：平均情况下的查找、插入、删除速度极快，接近常数时间 &lt;strong&gt;O(1)&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;元素无序。&lt;/li&gt;
&lt;li&gt;最坏情况下的性能是 &lt;strong&gt;O(n)&lt;/strong&gt;（例如所有 key 都哈希到同一个桶里）。&lt;/li&gt;
&lt;li&gt;迭代器不稳定（重新哈希会导致失效）。&lt;/li&gt;
&lt;li&gt;需要为 key 类型提供良好的哈希函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;如何选择？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用 &lt;code&gt;std::map&lt;/code&gt; 当：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你需要&lt;strong&gt;元素按 key 排序&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;你需要&lt;strong&gt;按顺序遍历&lt;/strong&gt;元素。&lt;/li&gt;
&lt;li&gt;你无法定义一个好的哈希函数 for your key type。&lt;/li&gt;
&lt;li&gt;你非常关心&lt;strong&gt;最坏情况的性能&lt;/strong&gt;（保证 O(log n)），而不是平均情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用 &lt;code&gt;std::unordered_map&lt;/code&gt; 当：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查找速度&lt;/strong&gt;是首要任务。&lt;/li&gt;
&lt;li&gt;你不需要维护元素的任何顺序。&lt;/li&gt;
&lt;li&gt;你愿意并且能够为你的 key 类型定义一个高效的哈希函数（例如，基本类型和&lt;code&gt;std::string&lt;/code&gt;已有内置哈希）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;90. 介绍一下B树/B+树/红黑树及其对应的应用场景有哪些&lt;/h2&gt;
&lt;h3&gt;B 树&lt;/h3&gt;
&lt;p&gt;B 树是一种多路平衡搜索树，它的每个节点可以拥有多于两个的子节点，这使得它能够保持矮胖的树形结构。&lt;/p&gt;
&lt;p&gt;B 树的设计核心是为了减少磁盘 I/O 次数，因为它一个节点的大小通常设置为一个磁盘页的大小，一次磁盘读取就能加载一个包含多个键的巨大节点，然后在内核中进行高效的二分查找。&lt;/p&gt;
&lt;p&gt;所以它特别适合用于文件系统和数据库（如 MySQL 的 InnoDB 存储引擎）的索引，这些场景下数据量巨大无法全部装入内存，需要频繁与磁盘交换数据。&lt;/p&gt;
&lt;h3&gt;B+ 树&lt;/h3&gt;
&lt;p&gt;B+树是 B 树的一种变体，也是目前数据库和文件系统索引的事实标准。&lt;/p&gt;
&lt;p&gt;它与 B 树的主要区别在于：首先，它的所有数据记录都只存储在叶子节点上，内部节点只存放键作为导航用的索引；其次，叶子节点之间通过指针相连形成了一个有序链表。&lt;/p&gt;
&lt;p&gt;这样的设计带来了几个巨大优势：一是内部节点能存放更多的键，使得树更矮，查询需要的磁盘 I/O 更少；二是范围查询性能极高，一旦找到范围的起点，只需顺着叶子节点的链表遍历即可，而不需要像 B 树那样回溯到上层节点。&lt;/p&gt;
&lt;p&gt;所以 MySQL 的 InnoDB 引擎、MongoDB、以及几乎所有关系型数据库的索引都在用 B+树。&lt;/p&gt;
&lt;h3&gt;红黑树&lt;/h3&gt;
&lt;p&gt;红黑树则是一种二叉平衡搜索树，它通过复杂的旋转和变色规则来维持大致的平衡，确保从根到任意叶子节点的最长路径不会超过最短路径的两倍，从而保证了最坏情况下搜索、插入、删除操作的时间复杂度都是 $O(log n)$。&lt;/p&gt;
&lt;p&gt;它的主要优势在于内存中操作的效率非常高，且维护平衡的代价相对于严格的 AVL 树更小（旋转次数更少）。&lt;/p&gt;
&lt;p&gt;因此，它的应用场景主要集中在内存计算领域，比如 C++ STL 中的 &lt;code&gt;map&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 就是用红黑树实现的，此外还有 Linux 系统的进程调度器 Completely Fair Scheduler (CFS) 也用红黑树来管理进程队列。&lt;/p&gt;
&lt;h2&gt;91. 智能指针申请的空间是在堆上还是在栈上？&lt;/h2&gt;
&lt;p&gt;智能指针（&lt;code&gt;unique_ptr&lt;/code&gt;、&lt;code&gt;shared_ptr&lt;/code&gt;）&lt;strong&gt;本身&lt;/strong&gt;是一个栈上的对象，但它&lt;strong&gt;所管理的内存&lt;/strong&gt;是在堆上申请的。&lt;/p&gt;
&lt;h2&gt;92. 介绍下 &lt;code&gt;vector&lt;/code&gt; 动态扩容机制&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;std::vector&lt;/code&gt; 的动态扩容机制是其核心特性之一，旨在平衡内存使用与操作效率。&lt;/p&gt;
&lt;p&gt;其本质在于管理三个核心属性：&lt;code&gt;size&lt;/code&gt;（当前元素数量）、&lt;code&gt;capacity&lt;/code&gt;（当前分配的内存可容纳的元素数量）和分配策略（通常按固定因子增长，如2倍或1.5倍）。&lt;/p&gt;
&lt;p&gt;当执行 &lt;code&gt;push_back&lt;/code&gt; 或 &lt;code&gt;emplace_back&lt;/code&gt; 等插入操作时，若当前 &lt;code&gt;size&lt;/code&gt; 已达到 &lt;code&gt;capacity&lt;/code&gt;，则触发扩容流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;计算新容量&lt;/strong&gt;：根据预定义的增长因子（Growth Factor，通常为2）确定新的容量值（&lt;code&gt;new_capacity = old_capacity * growth_factor&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配新内存&lt;/strong&gt;：在自由存储区（堆）上申请一块更大的、连续的内存空间，其大小足以容纳 &lt;code&gt;new_capacity&lt;/code&gt; 个元素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迁移数据&lt;/strong&gt;：将原有内存中的所有元素移动（若移动构造函数为 &lt;code&gt;noexcept&lt;/code&gt;）或拷贝到新内存的起始位置。此步骤会调用各元素的构造函数，是扩容过程中开销最大的操作，时间复杂度为 O(N)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放旧内存&lt;/strong&gt;：销毁原内存中的对象并释放原有内存块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新内部状态&lt;/strong&gt;：将内部指针指向新内存块，并将 &lt;code&gt;capacity&lt;/code&gt; 更新为 &lt;code&gt;new_capacity&lt;/code&gt;，最后在尾部构造新插入的元素。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此机制确保了插入操作的&lt;strong&gt;摊还时间复杂度为 O(1)&lt;/strong&gt;。尽管单次扩容成本较高，但由于容量呈指数级增长，扩容频率迅速下降，从而将总成本均摊到多次操作上。&lt;/p&gt;
&lt;p&gt;需要注意的是，&lt;strong&gt;扩容会使所有指向原 &lt;code&gt;vector&lt;/code&gt; 元素的迭代器、指针和引用失效&lt;/strong&gt;，因为数据的存储地址已发生改变。因此，在已知元素数量的场景下，使用 &lt;code&gt;reserve()&lt;/code&gt; 预先分配足够容量是避免多次冗余扩容、提升性能的最佳实践。&lt;/p&gt;
&lt;h2&gt;93. &lt;code&gt;vector&lt;/code&gt; 的 &lt;code&gt;push_back&lt;/code&gt; 和 &lt;code&gt;emplace_back&lt;/code&gt; 的区别？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;push_back&lt;/code&gt; 和 &lt;code&gt;emplace_back&lt;/code&gt; 的核心区别在于&lt;strong&gt;构造对象的时机和方式&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push_back&lt;/code&gt; 接受一个已存在的对象，并将其拷贝（左值）或移动（右值）到容器末尾，这个过程中可能产生临时对象的开销。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;emplace_back&lt;/code&gt; 则直接接受构造参数，&lt;strong&gt;通过完美转发在容器末尾的内存中原地构造对象&lt;/strong&gt;，省去了创建临时对象的步骤，避免了不必要的拷贝或移动操作，因此性能更高。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在大多数情况下，尤其是插入临时对象或构造代价较高的对象时，应优先选用 &lt;code&gt;emplace_back&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;94. 关于 &lt;code&gt;vector&lt;/code&gt; 迭代器失效&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;当 &lt;code&gt;vector&lt;/code&gt; 的底层存储发生改变，尤其是内存重新分配时，指向其元素的迭代器、指针和引用都会变得不可用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;以下是导致 &lt;code&gt;vector&lt;/code&gt; 迭代器失效的几种主要情况：&lt;/p&gt;
&lt;h3&gt;1. 插入元素 (&lt;code&gt;insert&lt;/code&gt;, &lt;code&gt;emplace&lt;/code&gt;, &lt;code&gt;push_back&lt;/code&gt;, &lt;code&gt;emplace_back&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;插入操作是否导致迭代器失效，取决于是否触发了&lt;strong&gt;重新分配（Reallocation）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;导致重新分配&lt;/strong&gt;：如果插入新元素后，&lt;code&gt;size()&lt;/code&gt; 超过了 &lt;code&gt;capacity()&lt;/code&gt;，&lt;code&gt;vector&lt;/code&gt; 会申请一块新的更大的内存，并将所有现有元素&lt;strong&gt;移动&lt;/strong&gt;或&lt;strong&gt;拷贝&lt;/strong&gt;到新内存中，然后释放旧内存。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;后果&lt;/strong&gt;：&lt;strong&gt;所有迭代器、指针、引用都会失效&lt;/strong&gt;，包括 &lt;code&gt;begin()&lt;/code&gt;, &lt;code&gt;end()&lt;/code&gt; 以及所有指向元素的迭代器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;未导致重新分配&lt;/strong&gt;：如果插入后 &lt;code&gt;size() &amp;#x3C;= capacity()&lt;/code&gt;，则只需要将&lt;strong&gt;插入点之后&lt;/strong&gt;的所有元素向后移动。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;后果&lt;/strong&gt;：&lt;strong&gt;所有指向插入点之后元素的迭代器、指针、引用都会失效&lt;/strong&gt;。插入点之前的迭代器仍然有效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 删除元素 (&lt;code&gt;erase&lt;/code&gt;, &lt;code&gt;pop_back&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;删除元素会改变序列，为了保持内存连续，需要将&lt;strong&gt;被删除元素之后&lt;/strong&gt;的所有元素向前移动。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;后果&lt;/strong&gt;：&lt;strong&gt;所有指向被删除元素及其之后元素的迭代器、指针、引用都会失效&lt;/strong&gt;。被删除元素之前的迭代器仍然有效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特别注意&lt;/strong&gt;：&lt;code&gt;erase()&lt;/code&gt; 函数会返回一个指向被删除元素之后第一个有效元素的&lt;strong&gt;新迭代器&lt;/strong&gt;，你可以利用它来安全地继续遍历。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 改变容量 (&lt;code&gt;reserve&lt;/code&gt;, &lt;code&gt;resize&lt;/code&gt;, &lt;code&gt;shrink_to_fit&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;任何可能改变 &lt;code&gt;vector&lt;/code&gt; 容量（&lt;code&gt;capacity&lt;/code&gt;）的操作都可能引起内存重新分配。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reserve(n)&lt;/code&gt;：如果 &lt;code&gt;n &gt; capacity()&lt;/code&gt;，则会申请新内存并进行数据迁移，导致&lt;strong&gt;全部失效&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shrink_to_fit()&lt;/code&gt;：请求减少容量以匹配大小，实现可能会进行重新分配，导致&lt;strong&gt;全部失效&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resize(n)&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;n &gt; capacity()&lt;/code&gt;（需要扩容），则&lt;strong&gt;全部失效&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果只是增大 &lt;code&gt;size()&lt;/code&gt; 但未触发重分配，则 &lt;code&gt;end()&lt;/code&gt; 迭代器会失效。&lt;/li&gt;
&lt;li&gt;如果是减小 &lt;code&gt;size()&lt;/code&gt;，则被“抹去”的那些元素的迭代器会失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 交换 (&lt;code&gt;swap&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;当两个 &lt;code&gt;vector&lt;/code&gt; 进行交换时，它们的底层数据指针会互换。&lt;/p&gt;
&lt;p&gt;迭代器、指针、引用&lt;strong&gt;不会失效，但它们会交换归属&lt;/strong&gt;。原来指向容器 A 中元素的迭代器，在交换后指向的是容器 B 中的元素，反之亦然。&lt;/p&gt;
&lt;h3&gt;5. 清空 (&lt;code&gt;clear&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;clear()&lt;/code&gt; 函数会移除所有元素，并将 &lt;code&gt;size()&lt;/code&gt; 设为 0。它&lt;strong&gt;不保证&lt;/strong&gt;会改变 &lt;code&gt;capacity()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所有指向被清除元素的迭代器、指针、引用都会失效&lt;/strong&gt;。因为元素对象已经被销毁了。&lt;/p&gt;
&lt;h2&gt;95. C++ 不同权限继承分别会有怎样的表现？&lt;/h2&gt;
&lt;p&gt;C++ 的继承有 &lt;code&gt;public&lt;/code&gt;、&lt;code&gt;protected&lt;/code&gt;、&lt;code&gt;private&lt;/code&gt; 三种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public&lt;/code&gt; 继承：基类的 &lt;code&gt;public&lt;/code&gt; 成员仍然是 &lt;code&gt;public&lt;/code&gt;，&lt;code&gt;protected&lt;/code&gt; 成员仍然是 &lt;code&gt;protected&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protected&lt;/code&gt; 继承：基类的 &lt;code&gt;public&lt;/code&gt; 和 &lt;code&gt;protected&lt;/code&gt; 成员都变成 &lt;code&gt;protected&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private&lt;/code&gt; 继承：基类的 &lt;code&gt;public&lt;/code&gt; 和 &lt;code&gt;protected&lt;/code&gt; 成员都变成 &lt;code&gt;private&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;96. 单例模式的概念和实现？懒汉式/饿汉式？线程安全？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;概念&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;单例模式（Singleton）是一种创建型设计模式，保证一个类在系统中只有一个实例，并提供一个全局访问点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;典型实现方式是将构造函数设为 &lt;code&gt;private&lt;/code&gt;，在类内维护一个静态指针或引用，并通过一个 &lt;code&gt;static&lt;/code&gt; 方法获取实例。&lt;/li&gt;
&lt;li&gt;这样可以确保外部无法随意构造对象，而只能通过该方法获得同一个实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;懒汉式&lt;/strong&gt;：在第一次调用 &lt;code&gt;getInstance()&lt;/code&gt; 时才创建实例，节省内存，&lt;strong&gt;但需要考虑多线程时的同步问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;饿汉式&lt;/strong&gt;：在程序启动时就初始化实例，线程安全且实现简单，但可能造成资源浪费。通常通过加锁（如 &lt;code&gt;mutex&lt;/code&gt;）或使用双重检查锁（DCLP）&lt;strong&gt;来保证多线程下实例只被创建一次&lt;/strong&gt;，同时避免每次访问都加锁带来的性能损耗。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前者加锁在 C++11 后一般都是利用 &lt;strong&gt;函数内静态变量&lt;/strong&gt; 的线程安全初始化，而非加锁&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;懒汉式（线程安全）&lt;/h3&gt;
&lt;p&gt;特点：进程启动阶段就构造实例；&lt;strong&gt;首次使用时无需加锁&lt;/strong&gt;；简单、安全（依赖静态对象初始化顺序规则）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;饿汉式之所以能保证线程安全，核心原因在于静态对象的初始化时机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EagerSingleton::s_instance&lt;/code&gt; 是一个 &lt;strong&gt;静态存储期对象&lt;/strong&gt;（全局 / namespace 作用域静态变量，或类中的静态成员）。
C++ 标准规定：
&lt;ul&gt;
&lt;li&gt;静态存储期对象的构造在 &lt;strong&gt;main 函数执行之前&lt;/strong&gt; 完成。&lt;/li&gt;
&lt;li&gt;这个初始化过程是由运行时（Runtime）在单线程环境下完成的。换句话说，在进入 &lt;code&gt;main()&lt;/code&gt; 之前，&lt;code&gt;s_instance&lt;/code&gt; 就已经被安全地构造好了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，在程序运行的并发环境里，任何线程调用 &lt;code&gt;EagerSingleton::instance()&lt;/code&gt; 时，&lt;code&gt;s_instance&lt;/code&gt; 已经是构造完成的对象，不存在“多个线程同时构造”的竞争条件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

class EagerSingleton {
public:
    static EagerSingleton&amp;#x26; instance() {
        return s_instance;               // 已在程序启动时构造完成
    }

    void hello() const { std::cout &amp;#x3C;&amp;#x3C; &quot;Eager\n&quot;; }

    // 禁用拷贝/移动
    EagerSingleton(const EagerSingleton&amp;#x26;) = delete;
    EagerSingleton&amp;#x26; operator=(const EagerSingleton&amp;#x26;) = delete;
    EagerSingleton(EagerSingleton&amp;#x26;&amp;#x26;) = delete;
    EagerSingleton&amp;#x26; operator=(EagerSingleton&amp;#x26;&amp;#x26;) = delete;

private:
    EagerSingleton() { /* 初始化资源 */ }
    ~EagerSingleton() = default;

    static EagerSingleton s_instance;    // 定义见 .cpp
};

// 若放在同一翻译单元（示例用），需提供定义：
EagerSingleton EagerSingleton::s_instance;

int main() {
    EagerSingleton::instance().hello();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;s_instance&lt;/code&gt; 是静态全局对象，程序启动期构造、退出时析构。&lt;/li&gt;
&lt;li&gt;如果跨多个翻译单元，建议把定义放进唯一的 &lt;code&gt;.cpp&lt;/code&gt; 中，避免 ODR 冲突。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;饿汉式（线程安全）&lt;/h3&gt;
&lt;h4&gt;方式 A：Meyers Singleton（推荐）&lt;/h4&gt;
&lt;p&gt;特点：利用 &lt;strong&gt;函数内静态变量&lt;/strong&gt; 的线程安全初始化（C++11 起标准保证），写法最简洁。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

class LazySingleton {
public:
    static LazySingleton&amp;#x26; instance() {
        static LazySingleton inst;   // 首次调用时构造，线程安全
        return inst;
    }

    void hello() const { std::cout &amp;#x3C;&amp;#x3C; &quot;Lazy (Meyers)\n&quot;; }

    LazySingleton(const LazySingleton&amp;#x26;) = delete;
    LazySingleton&amp;#x26; operator=(const LazySingleton&amp;#x26;) = delete;
    LazySingleton(LazySingleton&amp;#x26;&amp;#x26;) = delete;
    LazySingleton&amp;#x26; operator=(LazySingleton&amp;#x26;&amp;#x26;) = delete;

private:
    LazySingleton() { /* 初始化资源 */ }
    ~LazySingleton() = default;
};

int main() {
    LazySingleton::instance().hello();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优点：简洁、可靠、由标准保证一次性初始化；缺点：析构顺序不受你精细控制（通常不是问题）。&lt;/p&gt;
&lt;h4&gt;方式 B：双重检查锁（DCLP）+ &lt;code&gt;std::atomic&lt;/code&gt;（展示用）&lt;/h4&gt;
&lt;p&gt;在需要&lt;strong&gt;手动控制生命周期&lt;/strong&gt;（如显式 &lt;code&gt;destroy()&lt;/code&gt;）的场景可用；写法更复杂，需小心内存序语义。多数情况下 A 足够。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;atomic&gt;
#include &amp;#x3C;mutex&gt;
#include &amp;#x3C;iostream&gt;

class LazySingletonDCLP {
public:
    static LazySingletonDCLP* instance() {
        LazySingletonDCLP* p = ptr.load(std::memory_order_acquire);
        if (!p) {
            std::lock_guard&amp;#x3C;std::mutex&gt; lk(mtx);
            p = ptr.load(std::memory_order_relaxed);
            if (!p) {
                p = new LazySingletonDCLP();
                ptr.store(p, std::memory_order_release);
            }
        }
        return p;
    }

    static void destroy() {               // 若需要显式销毁（可选）
        std::lock_guard&amp;#x3C;std::mutex&gt; lk(mtx);
        LazySingletonDCLP* p = ptr.load(std::memory_order_relaxed);
        if (p) {
            delete p;
            ptr.store(nullptr, std::memory_order_release);
        }
    }

    void hello() const { std::cout &amp;#x3C;&amp;#x3C; &quot;Lazy (DCLP)\n&quot;; }

    LazySingletonDCLP(const LazySingletonDCLP&amp;#x26;) = delete;
    LazySingletonDCLP&amp;#x26; operator=(const LazySingletonDCLP&amp;#x26;) = delete;
    LazySingletonDCLP(LazySingletonDCLP&amp;#x26;&amp;#x26;) = delete;
    LazySingletonDCLP&amp;#x26; operator=(LazySingletonDCLP&amp;#x26;&amp;#x26;) = delete;

private:
    LazySingletonDCLP() { /* 初始化资源 */ }
    ~LazySingletonDCLP() = default;

    static std::atomic&amp;#x3C;LazySingletonDCLP*&gt; ptr;
    static std::mutex mtx;
};

std::atomic&amp;#x3C;LazySingletonDCLP*&gt; LazySingletonDCLP::ptr{nullptr};
std::mutex LazySingletonDCLP::mtx;

int main() {
    LazySingletonDCLP::instance()-&gt;hello();
    LazySingletonDCLP::destroy();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;首选&lt;/strong&gt;：懒汉式的 &lt;strong&gt;Meyers Singleton&lt;/strong&gt;（方式 A）或&lt;strong&gt;饿汉式静态对象&lt;/strong&gt;，代码最简、标准保证线程安全。&lt;/li&gt;
&lt;li&gt;仅当需要&lt;strong&gt;手动销毁&lt;/strong&gt;或&lt;strong&gt;自定义分配策略&lt;/strong&gt;时，再考虑 DCLP/&lt;code&gt;std::call_once&lt;/code&gt; 等更复杂写法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;97. 虚函数定义为析构函数能避免内存泄露的实现原理是什么？&lt;/h2&gt;
&lt;p&gt;当基类指针指向派生类对象并通过 &lt;code&gt;delete&lt;/code&gt; 释放时，如果基类的析构函数不是虚函数，那么只会调用基类析构函数，派生类部分无法正确析构，导致资源未释放从而造成内存泄露。而将析构函数声明为虚函数后，析构时会触发虚函数表的动态绑定，先调用派生类的析构函数，再调用基类析构函数，确保对象的所有资源被正确释放。&lt;/p&gt;
&lt;h2&gt;98. 为什么哈希表不适合做索引结构？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;阿里面试 · 数据库&lt;/p&gt;
&lt;p&gt;参考链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1se4y1U7Dn/?spm_id_from=333.337.search-card.all.click&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;一个视频带你了解常用存储引擎数据结构&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV15V411p7pi/?spm_id_from=333.788.top_right_bar_window_custom_collection.content.click&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;深入理解 B+ 树原理&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;这是一个非常核心的【数据库】问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;哈希表虽然拥有 &lt;strong&gt;O(1) 的极致点查询性能&lt;/strong&gt;，但它固有的特性使其在大多数情况下&lt;strong&gt;不适合作为数据库的主流索引结构&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;数据库索引的核心需求不仅仅是“快”，更是&lt;strong&gt;灵活性和高效地支持多种查询模式&lt;/strong&gt;。哈希表在这一点上存在致命缺陷。&lt;/p&gt;
&lt;p&gt;以下是哈希表不适合做（主流的）数据库索引结构的几个关键原因：&lt;/p&gt;
&lt;h3&gt;(1) 无法支持范围查询 (Range Queries)&lt;/h3&gt;
&lt;p&gt;这是哈希索引的&lt;strong&gt;最大死穴&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;哈希表的工作方式&lt;/strong&gt;：基于哈希函数将键映射到特定的存储位置。这个映射是&lt;strong&gt;分散的、无序的&lt;/strong&gt;。&lt;code&gt;id=100&lt;/code&gt; 和 &lt;code&gt;id=101&lt;/code&gt; 的记录可能被哈希到完全不相干的两个桶（bucket）里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库的常见需求&lt;/strong&gt;：查询如 &lt;code&gt;WHERE id &gt; 100 AND id &amp;#x3C; 200&lt;/code&gt;、&lt;code&gt;WHERE name LIKE &apos;A%&apos;&lt;/code&gt;、&lt;code&gt;ORDER BY date&lt;/code&gt;。这些查询需要数据是&lt;strong&gt;有序的&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;哈希表的困境&lt;/strong&gt;：为了完成一个范围查询，哈希表&lt;strong&gt;必须扫描表中的每一项&lt;/strong&gt;，因为数据在存储上是没有顺序的。这相当于全表扫描（O(n)），完全丧失了索引的意义。而 B+树等有序索引可以高效地定位范围的起点，然后通过叶子节点的链表顺序遍历即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(2) 无法支持排序 (ORDER BY) 和前缀匹配 (LIKE &apos;abc%&apos;)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;排序&lt;/strong&gt;：&lt;code&gt;ORDER BY&lt;/code&gt; 子句需要数据有序。哈希表输出的数据是杂乱无章的，要排序就必须将所有结果集放入内存或进行磁盘排序，成本极高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前缀匹配&lt;/strong&gt;：&lt;code&gt;LIKE &apos;abc%&apos;&lt;/code&gt; 本质上也是一个范围查询（从&apos;abc&apos;到&apos;abd&apos;），同样需要索引是有序的。哈希函数会把 &lt;code&gt;&apos;abc&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;abcd&apos;&lt;/code&gt; 哈希成完全不同的值，无法利用索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(3) 哈希冲突与性能退化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;冲突处理&lt;/strong&gt;：再好的哈希函数也可能存在冲突（两个不同的键被映射到同一个位置）。需要额外的机制来处理（如链表法），这会在冲突发生时增加查询时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最坏情况&lt;/strong&gt;：如果哈希函数设计不佳或数据有特定模式，大量键可能被哈希到少数几个桶中，导致某些链变得非常长。最坏情况下，查询性能会退化为 &lt;strong&gt;O(n)&lt;/strong&gt;，这与设计初衷背道而驰。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(4) 哈希函数的选择至关重要且困难&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;选择一个能均匀分散数据的哈希函数非常关键，但这并非易事，需要根据具体的数据分布来决定。&lt;/li&gt;
&lt;li&gt;一旦数据特征发生变化，原先合适的哈希函数可能不再高效，而更换哈希函数意味着要&lt;strong&gt;重建整个索引&lt;/strong&gt;，成本巨大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(5) 不支持部分索引（最左匹配原则）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;对于&lt;strong&gt;复合索引&lt;/strong&gt;（如索引 &lt;code&gt;(last_name, first_name)&lt;/code&gt;），B+树可以支持只查询 &lt;code&gt;last_name&lt;/code&gt; 的查询（最左匹配）。&lt;/li&gt;
&lt;li&gt;哈希索引必须使用&lt;strong&gt;所有键&lt;/strong&gt;来计算哈希值。如果你只知道 &lt;code&gt;last_name&lt;/code&gt; 而不知道 &lt;code&gt;first_name&lt;/code&gt;，哈希函数将无法计算，索引也就完全失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(6) 内存使用效率&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;为了避免频繁冲突，哈希表通常需要保持较大的空闲空间（较低的负载因子，如 70%），这会导致&lt;strong&gt;内存/磁盘空间的浪费&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;B+树的页面填充率通常可以很高（如 70%-100%），空间利用率更好。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;99. B-Tree、B+ Tree、LSM-Tree&lt;/h2&gt;
&lt;p&gt;详细探讨一下在数据库和存储系统中至关重要的三种数据结构：&lt;strong&gt;B 树&lt;/strong&gt;、&lt;strong&gt;B+ 树&lt;/strong&gt;和 &lt;strong&gt;LSM 树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它们都是为了解决不同场景下，如何高效地管理和访问磁盘上的大量数据而设计的。&lt;/p&gt;
&lt;h3&gt;1. B 树&lt;/h3&gt;
&lt;p&gt;B 树是一种自平衡的多路搜索树，它允许每个节点有多个子节点（超过两个）。它旨在最大限度地减少磁盘 I/O 操作，因为从磁盘读取一个节点（即一个页面或块）通常需要一次 I/O，而节点内键的比较则在内存中进行，速度很快。&lt;/p&gt;
&lt;h4&gt;核心特性与结构&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多路平衡&lt;/strong&gt;：一个节点可以拥有多个子节点（通常是上百甚至上千个），这大大降低了树的高度。树的高度与磁盘 I/O 次数直接相关，矮胖的树比高瘦的二叉树性能好得多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排序节点&lt;/strong&gt;：每个节点中的键（Key）都是&lt;strong&gt;按顺序存储&lt;/strong&gt;的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节点容量&lt;/strong&gt;：一个&lt;strong&gt;m 阶&lt;/strong&gt;的 B 树（order-m）满足以下属性：
&lt;ol&gt;
&lt;li&gt;每个节点最多有 &lt;strong&gt;m&lt;/strong&gt; 个子节点。&lt;/li&gt;
&lt;li&gt;每个内部节点（非根非叶）至少有 &lt;strong&gt;⌈m/2⌉&lt;/strong&gt; 个子节点。&lt;/li&gt;
&lt;li&gt;根节点至少有两个子节点（除非它是叶子节点）。&lt;/li&gt;
&lt;li&gt;所有&lt;strong&gt;叶子节点都位于同一层&lt;/strong&gt;，显示出完美的平衡。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据存储&lt;/strong&gt;：在&lt;strong&gt;经典的 B 树定义中，所有节点（包括内部节点）都可以存储数据&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;操作&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查询&lt;/strong&gt;：从根节点开始，在节点内部进行二分查找，找到合适的区间，然后递归地进入对应的子节点，直到找到目标键或确认键不存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插入&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;找到应插入的叶子节点。&lt;/li&gt;
&lt;li&gt;如果该节点有空间，则直接插入并排序。&lt;/li&gt;
&lt;li&gt;如果节点已满，则进行&lt;strong&gt;分裂（Split）&lt;/strong&gt;：将中间键提升到父节点，原节点分裂成两个。这个分裂过程可能会一直向上传播到根节点，导致树增高。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;删除&lt;/strong&gt;：稍微复杂一些，可能涉及从兄弟节点借键（借用）或者与兄弟节点合并，以保持树的平衡和节点的最小度数要求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;优点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;自动保持平衡，保证查询、插入、删除的时间复杂度为 &lt;strong&gt;O(log n)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;矮胖的结构减少了磁盘访问次数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;缺点（尤其是在数据库索引场景下）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;区间查询效率相对较低&lt;/strong&gt;：由于数据可能分布在所有节点（包括内部节点），进行范围查询（如&lt;code&gt;SELECT * FROM table WHERE key BETWEEN 10 AND 100&lt;/code&gt;）时需要在树中进行多次中序遍历，效率不高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节点结构相对浪费空间&lt;/strong&gt;：每个节点既存储键，也可能存储数据，导致每个节点能存放的键数量相对减少，树的高度可能略高于 B+树。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. B+树&lt;/h3&gt;
&lt;p&gt;B+ 树是 B 树的一种变体，是现代关系型数据库（如 MySQL 的 InnoDB、PostgreSQL）索引的&lt;strong&gt;事实标准&lt;/strong&gt;。它针对 B 树在数据库应用中的缺点进行了优化。&lt;/p&gt;
&lt;h4&gt;核心特性与结构（与 B 树的主要区别）&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据只存储在叶子节点&lt;/strong&gt;：内部节点&lt;strong&gt;只存储键&lt;/strong&gt;（索引信息）和指向子节点的指针。这使得内部节点可以容纳更多的键，从而进一步降低树的高度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;叶子节点通过指针串联&lt;/strong&gt;：所有叶子节点构成一个&lt;strong&gt;有序双向链表&lt;/strong&gt;。这是实现高效区间查询的关键。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;操作&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;插入和删除过程与 B 树类似，也涉及分裂和合并，但规则稍有不同，因为要维护叶子节点的链表结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;优点（相较于 B 树）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;更低的树高&lt;/strong&gt;：内部节点不存储数据，可以容纳更多的键，因此在相同数据量下，B+树比 B 树更矮胖，I/O 次数更少。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更稳定的查询性能&lt;/strong&gt;：任何查询都必须到达叶子节点，因此每次查找的路径长度都是相同的（O(log n)）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极高的区间查询效率&lt;/strong&gt;：这是 B+树最大的优势。一旦在叶子节点上找到了范围的起始点，就可以通过叶子节点的链表指针顺序扫描，无需回溯到上层节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;缺点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;由于数据只存在于叶子节点，每次查询都必须到达叶子节点，单点查询性能可能略逊于 B 树（如果数据在 B 树的内部节点就找到的话），但这种差异微乎其微。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. LSM 树&lt;/h3&gt;
&lt;p&gt;LSM 树的设计理念与 B+树完全不同。它&lt;strong&gt;牺牲了部分的读性能&lt;/strong&gt;，来换取&lt;strong&gt;极高的写吞吐量&lt;/strong&gt;。它广泛应用于&lt;strong&gt;写多读少&lt;/strong&gt;的场景，如 Google Bigtable、HBase、Cassandra、LevelDB、RocksDB 等 NoSQL 数据库中。&lt;/p&gt;
&lt;h4&gt;设计哲学&lt;/h4&gt;
&lt;p&gt;磁盘的&lt;strong&gt;顺序写入&lt;/strong&gt;速度远快于&lt;strong&gt;随机写入&lt;/strong&gt;。B+ 树需要原地更新数据，可能涉及随机写入（如分裂节点）。LSM 树则通过以下方式将随机写入转换为顺序写入：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先写入内存和日志&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在后台合并和排序&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;核心结构与工作流程&lt;/h4&gt;
&lt;p&gt;LSM 树通常由两个或多个主要组件构成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MemTable&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个常驻内存的数据结构（通常是跳表 SkipList 或平衡树）。&lt;/li&gt;
&lt;li&gt;所有新的写入操作首先被写入 MemTable，同时也会被追加到一个&lt;strong&gt;预写日志（Write-Ahead Log, WAL）&lt;/strong&gt; 中用于崩溃恢复。&lt;/li&gt;
&lt;li&gt;读操作需要同时查询 MemTable 和后续的 SSTable，然后合并结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Immutable MemTable&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 MemTable 的大小达到阈值时，它会变为只读状态（Immutable），并准备被刷写到磁盘。同时，一个新的 MemTable 会被创建来接收新的写入操作。&lt;/li&gt;
&lt;li&gt;这个设计避免了读写锁冲突，保证了持续的写入性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SSTable (Sorted String Table)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Immutable MemTable 被&lt;strong&gt;顺序、批量&lt;/strong&gt;地写入磁盘，形成一个 SSTable 文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSTable 是不可变的（Immutable）&lt;/strong&gt;，其中的数据是&lt;strong&gt;按键排序&lt;/strong&gt;的。&lt;/li&gt;
&lt;li&gt;随着写入不断进行，磁盘上会积累多个 SSTable 文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compaction（压缩合并）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是 LSM 树的核心后台进程。它会将多个较小的、可能有键重叠的 SSTable &lt;strong&gt;合并（Merge-Sort）&lt;/strong&gt; 成一个更大的、新的 SSTable，并在这个过程中丢弃已删除或过时的数据。&lt;/li&gt;
&lt;li&gt;这个过程保证了数据的有序性和减少了文件数量，从而优化读性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;优点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;极高的写入吞吐量&lt;/strong&gt;：写入几乎是纯顺序 I/O（写 WAL 和刷写 SSTable），速度极快，远超 B+树的随机写入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;良好的压缩效率&lt;/strong&gt;：由于 SSTable 是不可变且有序的，可以采用高效的压缩算法，节省磁盘空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;缺点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读放大（Read Amplification）&lt;/strong&gt;：一次读操作可能需要检查 MemTable 和多个 SSTable 文件（使用布隆过滤器等优化技术来减少不必要的文件查找），延迟可能不如 B+树稳定且通常更高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写放大（Write Amplification）&lt;/strong&gt;：Compaction 过程会反复重写数据，带来额外的磁盘写入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空间放大（Space Amplification）&lt;/strong&gt;：在 Compaction 发生前，重复的数据（不同版本）或已删除的数据可能同时存在于多个 SSTable 中，暂时占用额外空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;总结与对比&lt;/h3&gt;
&lt;p&gt;| 特性         | B 树                               | B+树                                                 | LSM 树                                              |
| :----------- | :--------------------------------- | :--------------------------------------------------- | :-------------------------------------------------- |
| &lt;strong&gt;设计理念&lt;/strong&gt; | 平衡的多路搜索树，节点存数据       | B 树的变体，&lt;strong&gt;数据只存于叶子&lt;/strong&gt;，&lt;strong&gt;叶子有链表&lt;/strong&gt;       | &lt;strong&gt;日志结构&lt;/strong&gt;，先内存后磁盘，&lt;strong&gt;顺序写入&lt;/strong&gt;            |
| &lt;strong&gt;最佳场景&lt;/strong&gt; | 通用（现在较少，多被 B+树替代）    | &lt;strong&gt;读多写少&lt;/strong&gt;，&lt;strong&gt;频繁区间查询&lt;/strong&gt;（OLTP，关系型数据库） | &lt;strong&gt;写多读少&lt;/strong&gt;，&lt;strong&gt;批量写入&lt;/strong&gt;（时序数据，日志，NoSQL） |
| &lt;strong&gt;写入性能&lt;/strong&gt; | 中等，可能涉及随机写入（节点分裂） | 中等，可能涉及随机写入（节点分裂）                   | &lt;strong&gt;极高&lt;/strong&gt;，基本是顺序写入                            |
| &lt;strong&gt;读取性能&lt;/strong&gt; | 良好（点查可能更快）               | &lt;strong&gt;优秀&lt;/strong&gt;（点查和范围查都非常高效）                   | 相对较差，存在读放大，需要查询多个结构              |
| &lt;strong&gt;空间开销&lt;/strong&gt; | 节点存储数据，树可能略高           | 内部节点只存键，更矮胖，但叶子链表有额外指针         | 写放大和空间放大（Compaction 前），但压缩率高       |
| &lt;strong&gt;复杂度&lt;/strong&gt;   | 中（分裂/合并）                    | 中（分裂/合并，维护链表）                            | 高（需管理 MemTable，SSTable，Compaction 策略）     |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你的应用是&lt;strong&gt;传统的 OLTP 业务&lt;/strong&gt;，有大量随机读和范围查询（如电商、ERP），&lt;strong&gt;B+ 树&lt;/strong&gt;是首选。&lt;/li&gt;
&lt;li&gt;如果你的应用是&lt;strong&gt;监控、日志采集、物联网传感器数据&lt;/strong&gt;等&lt;strong&gt;海量写入&lt;/strong&gt;的场景，对写入速度要求极高，并能接受稍慢的读速度，&lt;strong&gt;LSM 树&lt;/strong&gt;是更优的选择。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;100. &lt;code&gt;placement new&lt;/code&gt; 的使用场景及其内存分配约束&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;字节面试题、华为面试题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;C++ 中 &lt;code&gt;placement new&lt;/code&gt; 的使用场景和内存分配约束。这是一个高级但非常重要的特性，常用于需要精细控制内存管理的场景。&lt;/p&gt;
&lt;h3&gt;核心概念：将“内存分配”与“对象构造”分离&lt;/h3&gt;
&lt;p&gt;通常，&lt;code&gt;new&lt;/code&gt; 运算符做了两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;分配内存&lt;/strong&gt;：在堆上分配足够大小的内存以容纳该类型的对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构造对象&lt;/strong&gt;：在刚刚分配的内存上调用对象的构造函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;delete&lt;/code&gt; 运算符则做了相反的两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;析构对象&lt;/strong&gt;：调用对象的析构函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放内存&lt;/strong&gt;：释放对象所占用的内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;placement new&lt;/code&gt; 允许你将这两个步骤分离开来。&lt;strong&gt;它只执行第二步（构造对象），而不分配任何内存&lt;/strong&gt;。你负责提前提供一块内存，&lt;code&gt;placement new&lt;/code&gt; 只是在这块你提供的内存上调用构造函数。&lt;/p&gt;
&lt;p&gt;它的标准形式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;new&gt; // 必须包含的头文件

void* preallocatedMemory = ...; // 你事先获得的一块内存
MyClass* obj = new (preallocatedMemory) MyClass(constructorArgs);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;(preallocatedMemory)&lt;/code&gt; 就是“placement”参数。&lt;code&gt;placement new&lt;/code&gt; 是 &lt;code&gt;operator new&lt;/code&gt; 的一个重载版本。&lt;/p&gt;
&lt;h3&gt;主要使用场景&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;placement new&lt;/code&gt; 的应用几乎总是围绕着性能、自定义内存管理或特殊需求。&lt;/p&gt;
&lt;h4&gt;1. 内存池和自定义内存分配器&lt;/h4&gt;
&lt;p&gt;这是最常见和最重要的用途。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：在性能关键的系统中（如游戏引擎、高频交易），频繁地使用默认的 &lt;code&gt;new&lt;/code&gt; 和 &lt;code&gt;delete&lt;/code&gt; 会导致堆碎片和分配开销（因为需要查找合适的内存块、维护堆数据结构等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;做法&lt;/strong&gt;：程序启动时，一次性分配一大块内存（“内存池”）。当需要创建对象时，从这块大内存中手动划分出一小块空闲内存，然后使用 &lt;code&gt;placement new&lt;/code&gt; 在这小块内存上构造对象。对象销毁时，手动调用析构函数，然后将内存标记为空闲并归还给内存池，而不是调用 &lt;code&gt;delete&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;极快的分配/释放速度&lt;/strong&gt;：只是移动指针或操作空闲链表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免内存碎片&lt;/strong&gt;：所有对象都从连续的大块内存中分配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更好的局部性&lt;/strong&gt;：连续分配的对象在物理内存上也很可能连续，提高缓存命中率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 在特定内存地址构造对象&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：需要与硬件或特定系统接口交互时。例如，在嵌入式系统中，你可能需要将一个对象直接构造在某个已知的硬件寄存器地址或共享内存段上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;做法&lt;/strong&gt;：直接将硬件地址或共享内存地址作为 &lt;code&gt;placement new&lt;/code&gt; 的参数。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;volatile HardwareRegister* reg = new (0xFFFF0000) HardwareRegister;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 非标准内存布局（例如：数组的精确控制）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：你想手动管理一个对象数组的生命周期，或者需要绕过 &lt;code&gt;new[]&lt;/code&gt; 和 &lt;code&gt;delete[]&lt;/code&gt; 可能带来的额外开销（例如存储数组大小的开销）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;做法&lt;/strong&gt;：先分配一个足够大的 &lt;code&gt;char&lt;/code&gt; 数组（&lt;code&gt;sizeof(MyClass) * N&lt;/code&gt;），然后使用循环和 &lt;code&gt;placement new&lt;/code&gt; 在正确的位置逐个构造对象。销毁时，必须手动逆序调用每个对象的析构函数，最后释放整个 &lt;code&gt;char&lt;/code&gt; 数组。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;new&gt;
class MyClass {...};

// 1. 分配原始内存（不构造对象）
void* memory = operator new[](sizeof(MyClass) * 10);
MyClass* objects = static_cast&amp;#x3C;MyClass*&gt;(memory);

// 2. 使用 placement new 构造对象
for (std::size_t i = 0; i &amp;#x3C; 10; ++i) {
    new (&amp;#x26;objects[i]) MyClass(...); // 在指定地址构造
}

// 3. 手动调用析构函数
for (std::size_t i = 10; i &gt; 0; ) {
    --i;
    objects[i].~MyClass(); // 显式析构
}

// 4. 释放原始内存
operator delete[](memory);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 实现类似 &lt;code&gt;std::vector&lt;/code&gt; 和 &lt;code&gt;std::make_shared&lt;/code&gt; 的容器和智能指针&lt;/h4&gt;
&lt;p&gt;标准库容器（如 &lt;code&gt;std::vector&lt;/code&gt;）在底层使用 &lt;code&gt;allocator&lt;/code&gt; 来分配原始内存，然后使用 &lt;code&gt;placement new&lt;/code&gt; 在已分配的内存中构造元素。这使它们能够在重新分配时（&lt;code&gt;reserve&lt;/code&gt;）将“分配新内存”和“移动/构造元素”分离开来。&lt;/p&gt;
&lt;p&gt;类似地，&lt;code&gt;std::make_shared&lt;/code&gt; 通常在一次分配中同时获得控制块（引用计数）和对象本身所需的内存，然后使用 &lt;code&gt;placement new&lt;/code&gt; 在内存的正确偏移处构造对象。&lt;/p&gt;
&lt;h3&gt;内存分配约束与注意事项（极其重要）&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;placement new&lt;/code&gt; 意味着你接管了部分编译器的职责，因此必须严格遵守以下约束：&lt;/p&gt;
&lt;h4&gt;1. 内存必须提前分配且足够大&lt;/h4&gt;
&lt;p&gt;你提供的指针 &lt;code&gt;preallocatedMemory&lt;/code&gt; 必须指向一块已经分配好的内存，并且这块内存的大小&lt;strong&gt;至少&lt;/strong&gt;为 &lt;code&gt;sizeof(MyClass)&lt;/code&gt; 字节。如果内存不足，构造函数的行为是未定义的（通常是灾难性的崩溃或数据损坏）。&lt;/p&gt;
&lt;h4&gt;2. 内存对齐必须正确&lt;/h4&gt;
&lt;p&gt;你提供的内存地址必须满足该类型 &lt;code&gt;MyClass&lt;/code&gt; 的&lt;strong&gt;内存对齐要求&lt;/strong&gt;。C++11 之后可以使用 &lt;code&gt;alignof(MyClass)&lt;/code&gt; 来查询对齐要求，使用 &lt;code&gt;alignas(MyClass) char buf[sizeof(MyClass)];&lt;/code&gt; 来声明一个正确对齐的缓冲区。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;错误示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;char buffer[sizeof(MyClass)]; // 一个普通的char数组，可能只按1字节对齐
MyClass* obj = new (buffer) MyClass; // 如果MyClass要求4或8字节对齐，此行为未定义！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;正确做法&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// C++11 之前：使用编译器扩展或union技巧
// C++11 之后：
alignas(MyClass) std::byte buffer[sizeof(MyClass)]; // 正确对齐的缓冲区
MyClass* obj = new (buffer) MyClass; // 安全
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 必须手动调用析构函数&lt;/h4&gt;
&lt;p&gt;由于 &lt;code&gt;placement new&lt;/code&gt; 不分配内存，所以标准的 &lt;code&gt;delete obj;&lt;/code&gt; &lt;strong&gt;不能使用&lt;/strong&gt;。&lt;code&gt;delete&lt;/code&gt; 会尝试释放内存，但你提供的内存并不是由 &lt;code&gt;new&lt;/code&gt; 分配的，释放它会导致未定义行为。&lt;/p&gt;
&lt;p&gt;你&lt;strong&gt;必须&lt;/strong&gt;显式地调用析构函数来销毁对象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;obj-&gt;~MyClass(); // 正确：只执行析构，不释放内存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，你提供的原始内存如何处理（是复用、归还给内存池还是直接释放）取决于你最初是如何分配它的。&lt;/p&gt;
&lt;h4&gt;4. 生命周期管理的责任完全在程序员&lt;/h4&gt;
&lt;p&gt;你需要确保：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象在其生命周期内，其底层内存&lt;strong&gt;保持有效且未被覆盖&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要&lt;/strong&gt;对使用 &lt;code&gt;placement new&lt;/code&gt; 创建的对象调用 &lt;code&gt;delete&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在底层内存被释放或重用&lt;strong&gt;之前&lt;/strong&gt;，必须调用析构函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;| 特性         | 标准 &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt;     | &lt;code&gt;placement new&lt;/code&gt;                                  |
| :----------- | :---------------------- | :----------------------------------------------- |
| &lt;strong&gt;内存来源&lt;/strong&gt; | 堆                      | &lt;strong&gt;由程序员预先提供&lt;/strong&gt;                             |
| &lt;strong&gt;执行操作&lt;/strong&gt; | 分配内存 + 调用构造函数 | &lt;strong&gt;仅调用构造函数&lt;/strong&gt;                               |
| &lt;strong&gt;清理操作&lt;/strong&gt; | 调用析构函数 + 释放内存 | &lt;strong&gt;必须手动调用析构函数&lt;/strong&gt;                         |
| &lt;strong&gt;使用场景&lt;/strong&gt; | 通用                    | &lt;strong&gt;内存池、自定义分配器、特定地址构造、容器实现&lt;/strong&gt; |
| &lt;strong&gt;风险&lt;/strong&gt;     | 低                      | &lt;strong&gt;高（内存对齐、手动生命周期管理）&lt;/strong&gt;             |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总而言之，&lt;code&gt;placement new&lt;/code&gt; 是一个强大的工具，它将对象的构造与内存分配解耦，为你提供了极致的内存控制能力。然而，这种能力也带来了巨大的责任，你必须严格遵守内存对齐和生命周期管理的规则，否则极易引发难以调试的未定义行为&lt;/strong&gt;。在大多数日常应用开发中，你不需要直接使用它，但它却是许多高性能库和系统底层实现的基石。&lt;/p&gt;
&lt;h2&gt;101. 智能指针的应用场景和线程安全问题&lt;/h2&gt;
&lt;h3&gt;应用场景&lt;/h3&gt;
&lt;p&gt;智能指针用于自动化资源（尤其是动态内存）的生命周期管理，防止内存泄漏。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::unique_ptr&lt;/code&gt;（独占所有权）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：适用于资源在任何时刻都只能被一个所有者拥有的情况。是资源管理的默认选择。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子&lt;/strong&gt;：作为类的成员变量、在函数内部管理动态分配的对象、作为工厂函数的返回值。它替代了需要手动 &lt;code&gt;delete&lt;/code&gt; 的原始指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::shared_ptr&lt;/code&gt;（共享所有权）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：适用于多个对象需要共享同一个资源，并且只有在最后一个所有者被销毁时资源才能被释放的情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子&lt;/strong&gt;：实现缓存机制（多个客户端可能共享同一个缓存对象）、在复杂的数据结构中（如图、节点之间可能共享所有权）、需要将指针存入多个容器中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::weak_ptr&lt;/code&gt;（弱引用）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：与 &lt;code&gt;std::shared_ptr&lt;/code&gt; 搭配使用，解决 &lt;code&gt;shared_ptr&lt;/code&gt; 的循环引用问题。它提供对共享资源的“非拥有”引用，不会增加引用计数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子&lt;/strong&gt;：&lt;strong&gt;观察者模式&lt;/strong&gt;（主题持有观察者的 &lt;code&gt;weak_ptr&lt;/code&gt;，避免观察者无法析构）、&lt;strong&gt;缓存&lt;/strong&gt;（持有缓存对象的 &lt;code&gt;weak_ptr&lt;/code&gt;，当主所有者释放对象后，缓存自动失效）、&lt;strong&gt;打破循环引用&lt;/strong&gt;（如双链表节点中，父节点对子节点用 &lt;code&gt;shared_ptr&lt;/code&gt;，子节点对父节点用 &lt;code&gt;weak_ptr&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;线程安全问题&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;智能指针本身的引用计数操作是线程安全的，但其指向的对象的读写需要用户自己同步。&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;引用计数的线程安全&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::shared_ptr&lt;/code&gt; 的引用计数控制块（control block）是原子操作的。因此，&lt;strong&gt;在多线程中复制或析构 &lt;code&gt;shared_ptr&lt;/code&gt; 本身是安全的&lt;/strong&gt;，不会导致引用计数混乱。这是一个非常重要的保证。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指向数据的线程不安全&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;多个线程同时读写&lt;strong&gt;同一个 &lt;code&gt;shared_ptr&lt;/code&gt; 实例&lt;/strong&gt;（例如，对其赋值或重置）是&lt;strong&gt;不安全&lt;/strong&gt;的，需要加锁。这涉及到指针本身（&lt;code&gt;get()&lt;/code&gt; 返回的值）的更改。&lt;/li&gt;
&lt;li&gt;多个线程通过&lt;strong&gt;不同的 &lt;code&gt;shared_ptr&lt;/code&gt; 实例&lt;/strong&gt;（它们指向同一个对象）去访问和修改其指向的对象是&lt;strong&gt;不安全&lt;/strong&gt;的。这属于典型的数据竞争，也是必须使用互斥锁（如 &lt;code&gt;std::mutex&lt;/code&gt;）或其他同步机制来保护对对象本身的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;102. 介绍下布隆过滤器的工作原理和实现&lt;/h2&gt;
&lt;p&gt;布隆过滤器（Bloom Filter）是一个高效的概率型数据结构，用于快速判断一个元素是否存在于一个超大规模集合中。其核心特点是&lt;strong&gt;极其节省空间&lt;/strong&gt;，但代价是存在一定的&lt;strong&gt;误判率&lt;/strong&gt;（False Positive）。&lt;/p&gt;
&lt;h3&gt;工作原理/工作流&lt;/h3&gt;
&lt;p&gt;布隆过滤器的目标不是存储元素本身，而是用一个很小的位数组和一组哈希函数来表示一个集合，从而快速判断一个元素&lt;strong&gt;是否绝对不存在&lt;/strong&gt;或&lt;strong&gt;可能存在&lt;/strong&gt;于集合中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 初始化&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个长度为 &lt;code&gt;m&lt;/code&gt; 的位数组，所有位初始化为 0。&lt;/li&gt;
&lt;li&gt;选择 &lt;code&gt;k&lt;/code&gt; 个彼此独立的哈希函数（&lt;code&gt;h1, h2, ..., hk&lt;/code&gt;），每个函数能将输入元素映射到位数组的某个位置（范围在 &lt;code&gt;[0, m-1]&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 添加元素&lt;/strong&gt;：假设要添加元素 &quot;X&quot;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将 &quot;X&quot; 分别输入 &lt;code&gt;k&lt;/code&gt; 个哈希函数，得到 &lt;code&gt;k&lt;/code&gt; 个哈希值：&lt;code&gt;h1(X), h2(X), ..., hk(X)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将位数组中这 &lt;code&gt;k&lt;/code&gt; 个位置的值都设置为 1。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 查询元素&lt;/strong&gt;：假设要查询元素 &quot;Y&quot; 是否存在。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将 &quot;Y&quot; 分别输入同样的 &lt;code&gt;k&lt;/code&gt; 个哈希函数，得到 &lt;code&gt;k&lt;/code&gt; 个哈希值。&lt;/li&gt;
&lt;li&gt;检查位数组中这 &lt;code&gt;k&lt;/code&gt; 个位置的值：
&lt;ul&gt;
&lt;li&gt;如果这 &lt;code&gt;k&lt;/code&gt; 个位置中有任何一个为 0，那么元素 &quot;Y&quot; 肯定不存在（Definitely No）于集合中。&lt;/li&gt;
&lt;li&gt;如果这 &lt;code&gt;k&lt;/code&gt; 个位置全部为 1，那么元素 &quot;Y&quot; 可能存在（Probably Yes）于集合中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;影响因子：m 与 k&lt;/h3&gt;
&lt;p&gt;布隆过滤器的大小 &lt;code&gt;m&lt;/code&gt;（即比特数组的长度）是一个关键的设计参数，它直接影响了过滤器两个最重要的性能指标：误判率和空间效率。这是一个典型的空间换精度的权衡。&lt;/p&gt;
&lt;h4&gt;1. 对误判率的影响&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;m&lt;/code&gt; 越大，误判率越低。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原因： 更大的比特数组意味着更多的“坑位”，&lt;code&gt;k&lt;/code&gt; 个哈希函数映射到的位置更分散，不容易发生冲突。&lt;/li&gt;
&lt;li&gt;反面： &lt;code&gt;m&lt;/code&gt; 越小，比特数组很快就会被填满（更多的位被置为1），导致“假阳性”的概率急剧上升。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 对空间效率的影响&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;m&lt;/code&gt; 越大，空间开销越大。&lt;/p&gt;
&lt;h4&gt;3. 与哈希函数数量 &lt;code&gt;k&lt;/code&gt; 的相互作用&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;这是一个联动效应。最优的 &lt;code&gt;k&lt;/code&gt; 值取决于 &lt;code&gt;m&lt;/code&gt; 和要插入的元素数量 &lt;code&gt;n&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;公式： 最优的哈希函数数量 $k_{opt} = \frac{m}{n} \ln 2$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解释：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;m&lt;/code&gt; 很大，但 &lt;code&gt;k&lt;/code&gt; 很小（比如只有 1 个哈希函数），那么每个元素设置的位置很少，缺乏“冗余”，仍然容易发生冲突。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;m&lt;/code&gt; 很小，但 &lt;code&gt;k&lt;/code&gt; 很大，会导致一个元素设置太多的位，迅速将小的比特数组“填满”，使得几乎每次查询都会碰到所有位都是1的情况，&lt;strong&gt;误判率会接近 100%&lt;/strong&gt;，过滤器也就失效了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;所以不能孤立地谈 &lt;code&gt;m&lt;/code&gt; 的大小，必须结合计划存储的元素数量 &lt;code&gt;n&lt;/code&gt; 和选择的哈希函数个数 &lt;code&gt;k&lt;/code&gt; 来综合设计。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 对性能（查询/添加）的轻微影响&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;影响很小，但存在。添加和查询操作都需要进行 &lt;code&gt;k&lt;/code&gt; 次哈希计算和内存访问。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;m&lt;/code&gt; 的大小本身不影响计算速度（&lt;code&gt;k&lt;/code&gt; 次哈希和 &lt;code&gt;k&lt;/code&gt; 次内存访问是固定的）。&lt;/li&gt;
&lt;li&gt;但是，如果 &lt;code&gt;m&lt;/code&gt; 特别大，超出了 CPU 缓存的范围，可能会导致缓存缺失，使得每次内存访问的速度变慢。但在绝大多数应用中，这个影响微乎其微，性能瓶颈主要在哈希计算上。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;那你如何决定布隆过滤器的大小？&lt;/h3&gt;
&lt;p&gt;如果面试官追问：“那你如何决定布隆过滤器的大小？”&lt;/p&gt;
&lt;p&gt;在实际工程中，我们通常遵循以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确定容量 &lt;code&gt;n&lt;/code&gt;：首先预估要存入过滤器的元素最大数量是多少。&lt;/li&gt;
&lt;li&gt;设定目标误判率 &lt;code&gt;p&lt;/code&gt;：根据业务场景，确定一个可接受的误判率上限（例如 1%， 0.1%）。&lt;/li&gt;
&lt;li&gt;计算所需的比特数 &lt;code&gt;m&lt;/code&gt;：使用公式 $m = -\frac{nlnp}{(ln2)^2}$。这给出了在理论上达到目标误判率所需的最小的 &lt;code&gt;m&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;计算最优哈希函数数 &lt;code&gt;k&lt;/code&gt;：再根据公式 $k = \frac{m}{n}\ln2$  round 到最近的整数。&lt;/li&gt;
&lt;li&gt;向上取整： 最后，我会将计算出的 &lt;code&gt;m&lt;/code&gt; 向上取整到最接近的机器字长（例如64的倍数）的整数，以便内存对齐，实现高效的位操作。”&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Bloom filter 代码&lt;/h3&gt;
&lt;p&gt;以下是注释详细的 C++ 布隆过滤器实现，包含了添加、查询、误判率计算和性能测试。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;bitset&gt;
#include &amp;#x3C;functional&gt;
#include &amp;#x3C;cmath&gt;
#include &amp;#x3C;string&gt;

/**
 * @brief 布隆过滤器 (Bloom Filter) 实现类
 * 
 * @tparam Size 位数组的大小，以比特为单位。必须是编译期常量。
 */
template &amp;#x3C;size_t Size&gt;
class BloomFilter {
private:
    std::bitset&amp;#x3C;Size&gt; bits; // 位数组，用于存储元素的存在信息
    std::vector&amp;#x3C;std::function&amp;#x3C;size_t(const std::string&amp;#x26;)&gt;&gt; hashFunctions; // 哈希函数集合

public:
    /**
     * @brief 构造函数，初始化指定数量的哈希函数
     * @param numHashes 哈希函数的数量
     */
    BloomFilter(size_t numHashes) {
        // 使用简单的哈希函数种子来生成不同的哈希函数
        for (size_t i = 0; i &amp;#x3C; numHashes; ++i) {
            // 使用lambda捕获不同的种子来创建不同的哈希函数
            hashFunctions.emplace_back([i](const std::string&amp;#x26; item) {
                std::hash&amp;#x3C;std::string&gt; hasher;
                return hasher(item + std::to_string(i)); // 通过添加不同的后缀来创建不同的哈希函数
            });
        }
    }

    /**
     * @brief 向布隆过滤器中添加一个元素
     * @param item 要添加的元素（字符串形式）
     */
    void add(const std::string&amp;#x26; item) {
        for (const auto&amp;#x26; hashFunc : hashFunctions) {
            size_t hashValue = hashFunc(item);
            size_t index = hashValue % Size; // 映射到位数组的位置
            bits.set(index, true); // 将对应位置设为1
        }
    }

    /**
     * @brief 检查元素是否可能存在于布隆过滤器中
     * @param item 要检查的元素
     * @return true - 元素可能存在（有误判可能）
     * @return false - 元素肯定不存在
     */
    bool possiblyContains(const std::string&amp;#x26; item) const {
        for (const auto&amp;#x26; hashFunc : hashFunctions) {
            size_t hashValue = hashFunc(item);
            size_t index = hashValue % Size;
            if (!bits.test(index)) { // 如果任何一个位为0
                return false; // 肯定不存在
            }
        }
        return true; // 所有位都为1，可能存在
    }

    /**
     * @brief 计算当前布隆过滤器的理论误判率
     * @param insertedItems 已插入元素的数量
     * @return double 预期的误判率 (0.0 ~ 1.0)
     */
    double expectedFalsePositiveRate(size_t insertedItems) const {
        if (insertedItems == 0) return 0.0;
        
        // 理论误判率公式: (1 - e^(-k*n/m))^k
        double exponent = -static_cast&amp;#x3C;double&gt;(hashFunctions.size() * insertedItems) / Size;
        return std::pow(1 - std::exp(exponent), hashFunctions.size());
    }

    /**
     * @brief 获取位数组的使用率（已置为1的位数比例）
     * @return double 使用率 (0.0 ~ 1.0)
     */
    double getUsageRatio() const {
        return static_cast&amp;#x3C;double&gt;(bits.count()) / Size;
    }

    /**
     * @brief 重置布隆过滤器，清空所有数据
     */
    void reset() {
        bits.reset();
    }

    /**
     * @brief 获取位数组大小
     */
    size_t size() const {
        return Size;
    }

    /**
     * @brief 获取哈希函数数量
     */
    size_t hashCount() const {
        return hashFunctions.size();
    }
};

/**
 * @brief 运行特定配置的测试
 */
template &amp;#x3C;typename BloomFilterType&gt;
void runConfigurationTest(BloomFilterType&amp;#x26; bloomFilter, 
                         const std::string&amp;#x26; sizeDesc, 
                         size_t k, 
                         size_t testItems) {
    // 添加测试元素
    for (int i = 0; i &amp;#x3C; testItems; ++i) {
        bloomFilter.add(&quot;item_&quot; + std::to_string(i));
    }
    
    // 测试误判率
    int falsePositives = 0;
    for (int i = 0; i &amp;#x3C; testItems; ++i) {
        if (bloomFilter.possiblyContains(&quot;test_&quot; + std::to_string(i))) {
            falsePositives++;
        }
    }
    
    double fpr = static_cast&amp;#x3C;double&gt;(falsePositives) / testItems;
    std::cout &amp;#x3C;&amp;#x3C; &quot;Size: &quot; &amp;#x3C;&amp;#x3C; sizeDesc &amp;#x3C;&amp;#x3C; &quot;, Hashes: &quot; &amp;#x3C;&amp;#x3C; k 
              &amp;#x3C;&amp;#x3C; &quot;, FPR: &quot; &amp;#x3C;&amp;#x3C; (fpr * 100) &amp;#x3C;&amp;#x3C; &quot;%, Usage: &quot; 
              &amp;#x3C;&amp;#x3C; (bloomFilter.getUsageRatio() * 100) &amp;#x3C;&amp;#x3C; &quot;%&quot; &amp;#x3C;&amp;#x3C; std::endl;
}

/**
 * @brief 比较不同配置下的布隆过滤器性能
 */
void compareDifferentConfigurations() {
    std::cout &amp;#x3C;&amp;#x3C; &quot;\n\n===== 不同配置性能比较 =====&quot; &amp;#x3C;&amp;#x3C; std::endl;
    
    const size_t TEST_ITEMS = 1000;
    const size_t sizes[] = {5000, 10000, 20000}; // 不同的位数组大小
    const size_t hashes[] = {3, 7, 11};          // 不同的哈希函数数量
    
    for (size_t size : sizes) {
        for (size_t k : hashes) {
            // 使用模板特化来支持不同大小
            if (size == 5000) {
                BloomFilter&amp;#x3C;5000&gt; bf(k);
                runConfigurationTest(bf, &quot;5K bits&quot;, k, TEST_ITEMS);
            } else if (size == 10000) {
                BloomFilter&amp;#x3C;10000&gt; bf(k);
                runConfigurationTest(bf, &quot;10K bits&quot;, k, TEST_ITEMS);
            } else if (size == 20000) {
                BloomFilter&amp;#x3C;20000&gt; bf(k);
                runConfigurationTest(bf, &quot;20K bits&quot;, k, TEST_ITEMS);
            }
        }
    }
}

/**
 * @brief 测试布隆过滤器性能的函数
 */
void testBloomFilter() {
    std::cout &amp;#x3C;&amp;#x3C; &quot;===== 布隆过滤器性能测试 =====&quot; &amp;#x3C;&amp;#x3C; std::endl;
    
    const size_t BLOOM_FILTER_SIZE = 10000; // 位数组大小：10,000 位
    const size_t NUM_HASHES = 7;            // 哈希函数数量：7个
    const size_t TEST_ITEMS = 1000;         // 测试元素数量：1000个
    
    // 创建布隆过滤器实例
    BloomFilter&amp;#x3C;BLOOM_FILTER_SIZE&gt; bloomFilter(NUM_HASHES);
    
    // 生成测试数据
    std::vector&amp;#x3C;std::string&gt; insertedItems;
    std::vector&amp;#x3C;std::string&gt; testItems;
    
    for (int i = 0; i &amp;#x3C; TEST_ITEMS; ++i) {
        insertedItems.push_back(&quot;item_&quot; + std::to_string(i));
        testItems.push_back(&quot;test_&quot; + std::to_string(i)); // 这些是不会被插入的测试项
    }
    
    // 1. 添加元素
    std::cout &amp;#x3C;&amp;#x3C; &quot;正在添加 &quot; &amp;#x3C;&amp;#x3C; TEST_ITEMS &amp;#x3C;&amp;#x3C; &quot; 个元素...&quot; &amp;#x3C;&amp;#x3C; std::endl;
    for (const auto&amp;#x26; item : insertedItems) {
        bloomFilter.add(item);
    }
    
    // 2. 检查已插入的元素（应该全部&quot;可能存在&quot;）
    std::cout &amp;#x3C;&amp;#x3C; &quot;\n检查已插入的元素：&quot; &amp;#x3C;&amp;#x3C; std::endl;
    int truePositives = 0;
    for (const auto&amp;#x26; item : insertedItems) {
        if (bloomFilter.possiblyContains(item)) {
            truePositives++;
        }
    }
    std::cout &amp;#x3C;&amp;#x3C; &quot;真正例数: &quot; &amp;#x3C;&amp;#x3C; truePositives &amp;#x3C;&amp;#x3C; &quot;/&quot; &amp;#x3C;&amp;#x3C; TEST_ITEMS 
              &amp;#x3C;&amp;#x3C; &quot; (&quot; &amp;#x3C;&amp;#x3C; (truePositives * 100.0 / TEST_ITEMS) &amp;#x3C;&amp;#x3C; &quot;%)&quot; &amp;#x3C;&amp;#x3C; std::endl;
    
    // 3. 检查未插入的元素（测量误判率）
    std::cout &amp;#x3C;&amp;#x3C; &quot;\n检查未插入的元素（测量误判率）：&quot; &amp;#x3C;&amp;#x3C; std::endl;
    int falsePositives = 0;
    for (const auto&amp;#x26; item : testItems) {
        if (bloomFilter.possiblyContains(item)) {
            falsePositives++;
        }
    }
    
    double actualFPR = static_cast&amp;#x3C;double&gt;(falsePositives) / TEST_ITEMS;
    double expectedFPR = bloomFilter.expectedFalsePositiveRate(TEST_ITEMS);
    
    std::cout &amp;#x3C;&amp;#x3C; &quot;误判数: &quot; &amp;#x3C;&amp;#x3C; falsePositives &amp;#x3C;&amp;#x3C; &quot;/&quot; &amp;#x3C;&amp;#x3C; TEST_ITEMS &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;实际误判率: &quot; &amp;#x3C;&amp;#x3C; (actualFPR * 100) &amp;#x3C;&amp;#x3C; &quot;%&quot; &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;理论误判率: &quot; &amp;#x3C;&amp;#x3C; (expectedFPR * 100) &amp;#x3C;&amp;#x3C; &quot;%&quot; &amp;#x3C;&amp;#x3C; std::endl;
    
    // 4. 显示使用情况
    std::cout &amp;#x3C;&amp;#x3C; &quot;\n布隆过滤器使用情况：&quot; &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;位数组大小: &quot; &amp;#x3C;&amp;#x3C; bloomFilter.size() &amp;#x3C;&amp;#x3C; &quot; bits (&quot; 
              &amp;#x3C;&amp;#x3C; (bloomFilter.size() / 8) &amp;#x3C;&amp;#x3C; &quot; bytes)&quot; &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;哈希函数数量: &quot; &amp;#x3C;&amp;#x3C; bloomFilter.hashCount() &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;位数组使用率: &quot; &amp;#x3C;&amp;#x3C; (bloomFilter.getUsageRatio() * 100) &amp;#x3C;&amp;#x3C; &quot;%&quot; &amp;#x3C;&amp;#x3C; std::endl;
    
    // 5. 演示&quot;肯定不存在&quot;的特性
    std::cout &amp;#x3C;&amp;#x3C; &quot;\n演示&apos;肯定不存在&apos;特性：&quot; &amp;#x3C;&amp;#x3C; std::endl;
    std::string nonExistentItem = &quot;definitely_not_inserted_12345&quot;;
    bool result = bloomFilter.possiblyContains(nonExistentItem);
    std::cout &amp;#x3C;&amp;#x3C; &quot;检查未添加的元素 &apos;&quot; &amp;#x3C;&amp;#x3C; nonExistentItem &amp;#x3C;&amp;#x3C; &quot;&apos;: &quot; 
              &amp;#x3C;&amp;#x3C; (result ? &quot;可能存在&quot; : &quot;肯定不存在&quot;) &amp;#x3C;&amp;#x3C; std::endl;
}

int main() {
    // 运行基本测试
    testBloomFilter();
    
    // 运行配置比较
    compareDifferentConfigurations();
    
    std::cout &amp;#x3C;&amp;#x3C; &quot;\n===== 测试完成 =====&quot; &amp;#x3C;&amp;#x3C; std::endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Output&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt; ~/Desktop  ./bloom_filter 
===== 布隆过滤器性能测试 =====
正在添加 1000 个元素...

检查已插入的元素：
真正例数: 1000/1000 (100%)

检查未插入的元素（测量误判率）：
误判数: 4/1000
实际误判率: 0.4%
理论误判率: 0.819372%

布隆过滤器使用情况：
位数组大小: 10000 bits (1250 bytes)
哈希函数数量: 7
位数组使用率: 50.25%

演示&apos;肯定不存在&apos;特性：
检查未添加的元素 &apos;definitely_not_inserted_12345&apos;: 肯定不存在


===== 不同配置性能比较 =====
Size: 5K bits, Hashes: 3, FPR: 8.7%, Usage: 45.2%
Size: 5K bits, Hashes: 7, FPR: 14.4%, Usage: 75.5%
Size: 5K bits, Hashes: 11, FPR: 31.2%, Usage: 88.94%
Size: 10K bits, Hashes: 3, FPR: 1.4%, Usage: 26.09%
Size: 10K bits, Hashes: 7, FPR: 0.4%, Usage: 50.25%
Size: 10K bits, Hashes: 11, FPR: 1.3%, Usage: 66.44%
Size: 20K bits, Hashes: 3, FPR: 0.1%, Usage: 13.955%
Size: 20K bits, Hashes: 7, FPR: 0%, Usage: 29.425%
Size: 20K bits, Hashes: 11, FPR: 0%, Usage: 41.945%

===== 测试完成 =====
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;103. 常量数组和数组常量的区别？&lt;/h2&gt;
&lt;p&gt;常量数组指数组元素是常量（如 &lt;code&gt;const int arr[5]&lt;/code&gt;），内容不可修改；&lt;/p&gt;
&lt;p&gt;数组常量指数组本身是常量（如 &lt;code&gt;int* const arr&lt;/code&gt;），指针不可指向其他地址。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;构造方式：常量数组用 &lt;code&gt;const 类型名[]&lt;/code&gt; 声明并初始化；数组常量需用 &lt;code&gt;类型* const&lt;/code&gt; 声明并固定指向已分配内存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;104. 常量指针和指针常量区别？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;常量指针&lt;/strong&gt;通常有两种表示方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const int* ptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int const* ptr&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;指针常量&lt;/strong&gt;通常表示为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;double *const ptr&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;区分：从左往右读/看&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt; 读作常量， &lt;code&gt;*&lt;/code&gt; 读作指针；&lt;code&gt;const*&lt;/code&gt; 就是常量指针，&lt;code&gt;*const&lt;/code&gt; 就是指针常量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt; 靠近谁就是谁不变；&lt;code&gt;const char *p1&lt;/code&gt; 靠近 &lt;code&gt;char *&lt;/code&gt; 所以是内容不可变，&lt;code&gt;char *const p2&lt;/code&gt; 靠近 p2 所以是指针指向不可变。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;常量指针&lt;/strong&gt;：&lt;strong&gt;指向常量的指针&lt;/strong&gt;，与下面相反，指针地址的内容值不能修改，但指针地址指向可以改。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;指针常量&lt;/strong&gt;：指的是指针本身就是个常量，注意这里说的是指针本身是常量，指针是用来指向某个对象的（指针也就是这个对象的地址）&lt;strong&gt;指针本身是常量，就是这个地址是个常量，不能更换，但是可以更换这个地址存放的值&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;

int main() {
  char ch = &apos;h&apos;;
  char s = &apos;m&apos;;
  const char *p1 = &amp;#x26;ch;  // 常量指针: 可以通过它更换指向的对象，但是不能改变指向对象的值；
  char *const p2 = &amp;#x26;ch;  // 指针常量: 可以通过它更改对象的值，但是不能更改指向的对象；
  p1 = &amp;#x26;s;               // 正确;
  *p1 = &apos;k&apos;;             // 错误;
  *p2 = &apos;k&apos;;             // 正确;
  p2 = &amp;#x26;s;               // 错误;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;105. C++ 模板的原理？&lt;/h2&gt;
&lt;p&gt;C++ 模板的原理本质上是&lt;strong&gt;一种编译期的代码生成机制&lt;/strong&gt;，其核心是&lt;strong&gt;参数化多态&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当编译器遇到模板定义（如一个函数模板或类模板）时，它并不会立即生成任何实际的机器代码，而是将其视为一种生成代码的“蓝图”或“配方”存储在编译单元中。只有当编译器在源代码中看到模板被&lt;strong&gt;实例化&lt;/strong&gt;（即提供了具体的模板参数，如 &lt;code&gt;MyClass&amp;#x3C;int&gt;&lt;/code&gt;）时，它才会执行&lt;strong&gt;模板实例化&lt;/strong&gt;这个过程：将模板定义中的每个模板参数（如 &lt;code&gt;typename T&lt;/code&gt;）替换为提供的具体类型（如 &lt;code&gt;int&lt;/code&gt;），从而生成一个全新的、普通的类或函数的源代码副本（称为&lt;strong&gt;特化&lt;/strong&gt;或&lt;strong&gt;实例&lt;/strong&gt;）。这个过程是&lt;strong&gt;静态的&lt;/strong&gt;，发生在编译阶段，因此不同的实例化会生成完全独立的类型（例如，&lt;code&gt;vector&amp;#x3C;int&gt;&lt;/code&gt; 和 &lt;code&gt;vector&amp;#x3C;string&gt;&lt;/code&gt; 是两个毫无关联的类），这导致了著名的“代码膨胀”现象，但也确保了类型安全和极高的运行时效率。最终，编译器再像处理普通手写代码一样，对这些生成的特化代码进行编译和优化，生成目标代码。&lt;/p&gt;
&lt;h2&gt;106. 命名空间的作用？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;命名空间用于解决标识符（变量、函数、类名）冲突问题，将代码逻辑分组封装，形成独立作用域&lt;/strong&gt;。通过 &lt;code&gt;namespace&lt;/code&gt; 关键字定义，使用时需指定命名空间（如 &lt;code&gt;std::vector&lt;/code&gt;）或使用 &lt;code&gt;using&lt;/code&gt; 声明，&lt;strong&gt;避免全局作用域污染&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;107. 迭代器在 STL 中的作用？&lt;/h2&gt;
&lt;p&gt;迭代器是 STL 中抽象化的指针，&lt;strong&gt;提供统一访问容器元素的方式&lt;/strong&gt;（如遍历、修改），&lt;strong&gt;实现算法与容器的解耦&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它分为输入、输出、前向、双向、随机访问等类别，支持泛型算法（如 &lt;code&gt;sort&lt;/code&gt;）&lt;strong&gt;无需关心底层容器实现&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;108. 指针 &lt;code&gt;*&lt;/code&gt; 和引用 &lt;code&gt;&amp;#x26;&lt;/code&gt; 的区别（进阶版）&lt;/h2&gt;
&lt;h3&gt;一、引用和指针的区别&lt;/h3&gt;
&lt;p&gt;引用和指针本质上都是间接访问和操作其他对象的方式，但它们在语法、安全性和灵活性上有显著区别。&lt;/p&gt;
&lt;p&gt;| 特性         | 引用 (Reference)                                         | 指针 (Pointer)                                         |
| :----------- | :------------------------------------------------------- | :----------------------------------------------------- |
| &lt;strong&gt;本质&lt;/strong&gt;     | 是一个对象的别名，不是一个对象本身。                     | 是一个独立的对象，存储的是另一个对象的地址。           |
| &lt;strong&gt;初始化&lt;/strong&gt;   | 必须被初始化（绑定到一个对象），且不能更改绑定。         | 可以声明时不初始化（但极度危险），可以随时改变指向。   |
| &lt;strong&gt;空值&lt;/strong&gt;     | 不能为空（NULL）。必须总代表某个合法对象。               | 可以为nullptr，表示不指向任何对象。                    |
| &lt;strong&gt;操作&lt;/strong&gt;     | 所有操作都直接作用于其绑定的对象。                       | 有取地址(&amp;#x26;)、解引用(*) 等专门操作。                    |
| &lt;strong&gt;内存地址&lt;/strong&gt; | 引用自身不占用存储空间（通常由编译器在底层实现为指针）。 | 指针本身是一个对象，占用内存（通常是4或8字节）。       |
| &lt;strong&gt;安全性&lt;/strong&gt;   | 更安全，因为非空且无法重新绑定，避免了野指针等问题。     | 更灵活但也更危险，可能产生空指针解引用、野指针等问题。 |
| &lt;strong&gt;多级间接&lt;/strong&gt; | 不支持多级引用（如引用的引用，实际是原对象的引用）。     | 支持多级指针（如 &lt;code&gt;int** pp&lt;/code&gt;）。                        |&lt;/p&gt;
&lt;h3&gt;二、二者的应用场景&lt;/h3&gt;
&lt;h4&gt;引用的主要应用场景&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;函数参数传递（按引用传递）&lt;/strong&gt;：这是最常见用途。用于避免大型对象拷贝的开销，并允许函数修改传入的实参。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void swap(int&amp;#x26; a, int&amp;#x26; b) { // 修改实参
    int temp = a;
    a = b;
    b = temp;
}

void printLargeObject(const BigClass&amp;#x26; obj) { // 避免拷贝，且不允许修改
    // ... 只读操作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;函数返回值（返回引用）&lt;/strong&gt;：常用于返回函数调用者可以修改的对象，如重载赋值运算符&lt;code&gt;=&lt;/code&gt;、下标运算符&lt;code&gt;[]&lt;/code&gt;、流运算符&lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt;/&lt;code&gt;&gt;&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class MyVector {
    int data[100];
public:
    int&amp;#x26; operator[](size_t index) { return data[index]; } // 返回引用，允许v[i] = 5;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;范围for循环&lt;/strong&gt;：需要修改容器元素时，必须使用引用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for (auto&amp;#x26; element : myVector) {
    element *= 2; // 修改容器内的元素
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;指针的主要应用场景&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;动态内存管理&lt;/strong&gt;：使用 &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt; 或 &lt;code&gt;malloc&lt;/code&gt;/&lt;code&gt;free&lt;/code&gt; 在堆上分配资源时，必须用指针来管理。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int* array = new int[100];
delete[] array;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;多态和面向对象&lt;/strong&gt;：通过基类指针来管理派生类对象，实现运行时多态。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Base { virtual void func(); };
class Derived : public Base { void func() override; };

Base* ptr = new Derived();
ptr-&gt;func(); // 调用的是Derived::func()
delete ptr;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;需要表示“可选”或“可为空”的语义&lt;/strong&gt;：当需要一个可能不指向任何对象的变量时，必须使用指针。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;TreeNode* parent = nullptr; // 根节点的父节点为空
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;需要显式处理地址或进行底层操作&lt;/strong&gt;：如操作硬件、数据结构（链表、树等）等。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct ListNode {
    int val;
    ListNode* next; // 指向下一个节点的指针
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;三、函数传递引用和传递指针的区别？&lt;/h3&gt;
&lt;p&gt;这个问题更准确的表述是：&lt;strong&gt;函数按引用传递（Pass-by-Reference）和按值传递（Pass-by-Value）的区别？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;| 方面             | 按值传递 (Pass-by-Value)                           | 按引用传递 (Pass-by-Reference)                               |
| :--------------- | :------------------------------------------------- | :----------------------------------------------------------- |
| &lt;strong&gt;本质&lt;/strong&gt;         | 在函数栈上创建实参的一个&lt;strong&gt;副本&lt;/strong&gt;。                 | 传递的是实参本身的&lt;strong&gt;别名&lt;/strong&gt;（引用）或&lt;strong&gt;地址&lt;/strong&gt;（指针）。       |
| &lt;strong&gt;对原值的影响&lt;/strong&gt; | 函数内部对形参的任何修改&lt;strong&gt;都不会影响&lt;/strong&gt;外部的实参。 | 函数内部对形参（引用或解引用的指针）的修改&lt;strong&gt;会直接影响&lt;/strong&gt;外部的实参。 |
| &lt;strong&gt;性能开销&lt;/strong&gt;     | 如果实参是大型结构体或类，&lt;strong&gt;拷贝开销大&lt;/strong&gt;。         | &lt;strong&gt;几乎无开销&lt;/strong&gt;，只是传递了一个别名或一个地址（通常是4/8字节）。 |
| &lt;strong&gt;使用语法&lt;/strong&gt;     | &lt;code&gt;void func(MyType obj);&lt;/code&gt;                           | &lt;code&gt;void func(MyType&amp;#x26; ref);&lt;/code&gt; 或 &lt;code&gt;void func(MyType* ptr);&lt;/code&gt;       |
| &lt;strong&gt;安全性&lt;/strong&gt;       | 安全，函数内外的变量完全隔离。                     | 需要小心，尤其是传递指针时，可能需检查是否为&lt;code&gt;nullptr&lt;/code&gt;。      |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码示例对比：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;

// 1. 按值传递 - 拷贝副本
void byValue(int x) {
    x = 100; // 只修改了副本，外部的num1不变
}

// 2. 按引用传递 - 传递别名
void byReference(int&amp;#x26; x) {
    x = 200; // 直接修改外部的num2
}

// 3. 按指针传递（本质也是按值传递，但传递的是地址的值） - 传递地址
void byPointer(int* x) {
    *x = 300; // 解引用，修改x所指向的外部num3的值
}

int main() {
    int num1 = 1, num2 = 2, num3 = 3;

    byValue(num1);
    byReference(num2);
    byPointer(&amp;#x26;num3); // 需要显式取地址

    cout &amp;#x3C;&amp;#x3C; num1 &amp;#x3C;&amp;#x3C; endl; // 输出 1
    cout &amp;#x3C;&amp;#x3C; num2 &amp;#x3C;&amp;#x3C; endl; // 输出 200
    cout &amp;#x3C;&amp;#x3C; num3 &amp;#x3C;&amp;#x3C; endl; // 输出 300

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;总结：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;按值传递&lt;/strong&gt;：适用于不需要修改实参，且参数是内置类型（&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;等）或小型结构体的情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按引用传递&lt;/strong&gt;：适用于需要修改实参，或参数是大型对象需要避免拷贝开销的情况。使用 &lt;code&gt;const引用&lt;/code&gt;（如 &lt;code&gt;const BigObj&amp;#x26;&lt;/code&gt;）可以同时实现&lt;strong&gt;避免拷贝 + 不允许修改&lt;/strong&gt;，是最佳的只读参数传递方式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按指针传递&lt;/strong&gt;：在C++中，除非需要表达“可选”语义（可能为&lt;code&gt;nullptr&lt;/code&gt;）或需要重新指向其他对象，否则&lt;strong&gt;优先使用引用&lt;/strong&gt;。按指针传递在语法上更繁琐（需要&lt;code&gt;&amp;#x26;&lt;/code&gt;和&lt;code&gt;*&lt;/code&gt;），且安全性更低。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;109. &lt;code&gt;noexcept&lt;/code&gt; 原理，&lt;code&gt;noexcept&lt;/code&gt; 修饰函数内抛出异常怎么办？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;帮助编译器对“非抛出函数”进行优化&lt;/li&gt;
&lt;li&gt;影响标准库的行为&lt;/li&gt;
&lt;li&gt;异常传播处理&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 帮助编译器对“非抛出函数”进行优化&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;noexcept&lt;/code&gt; 关键字首先是一个给编译器的指令和承诺。当一个函数被标记为 &lt;code&gt;noexcept&lt;/code&gt;（或 &lt;code&gt;noexcept(true)&lt;/code&gt;），编译器会基于这个承诺进行两方面的优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;简化异常处理代码生成&lt;/strong&gt;：在通常情况下，编译器需要为可能抛出异常的函数生成复杂的“栈展开（Stack Unwinding）”代码。这套机制负责在异常抛出时，自动调用所有已构造的局部对象的析构函数，以保证资源的正确释放。对于 &lt;code&gt;noexcept&lt;/code&gt; 函数，编译器可以确信该函数执行过程中不会有异常抛出，因此&lt;strong&gt;完全可以省略这部分栈展开代码的生成&lt;/strong&gt;。这使得生成的机器码更小、更紧凑。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启用更激进的代码优化&lt;/strong&gt;：由于异常路径不存在，编译器的优化器可以更自由地对 &lt;code&gt;noexcept&lt;/code&gt; 函数的代码进行重组和简化。它不需要考虑在异常发生时保持一种可回滚的状态，从而可以更专注于优化正常的执行路径，这有可能带来性能上的提升。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：&lt;code&gt;noexcept&lt;/code&gt; 通过提供一份“绝不会抛出异常”的保证，允许编译器生成更精简、更高效的代码。&lt;/p&gt;
&lt;h3&gt;2. 影响标准库的行为&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;noexcept&lt;/code&gt; 更显著的作用是它会&lt;strong&gt;影响标准库组件的算法选择和行为&lt;/strong&gt;，尤其是在与移动语义和资源管理相关的操作上。标准库会利用 &lt;code&gt;noexcept&lt;/code&gt; 说明符在“强异常安全”和“性能”之间做出权衡。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;移动操作与拷贝操作的抉择&lt;/strong&gt;：最经典的例子是 &lt;code&gt;std::vector&lt;/code&gt; 的重新分配（reallocation）过程。当 vector 需要扩容时，它需要将旧元素移动到新内存中。如果元素的&lt;strong&gt;移动构造函数是 &lt;code&gt;noexcept&lt;/code&gt; 的&lt;/strong&gt;，vector 会安全地使用移动操作，因为这绝不会失败，效率极高。如果移动构造函数不是 &lt;code&gt;noexcept&lt;/code&gt; 的，vector 为了保持“强异常安全”保证（如果移动中抛出异常，旧容器状态不变），则会保守地&lt;strong&gt;使用拷贝操作&lt;/strong&gt;，即使拷贝通常更耗时。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;std::move_if_noexcept&lt;/code&gt;&lt;/strong&gt;：这个工具函数是上述行为的直接体现。它会在类型移动操作为 &lt;code&gt;noexcept&lt;/code&gt; 时返回右值引用（以启用移动），否则返回常量左值引用（以禁用移动，促使调用拷贝操作）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;其他标准库操作&lt;/strong&gt;：类似的行为也存在于 &lt;code&gt;std::swap&lt;/code&gt;、&lt;code&gt;std::sort&lt;/code&gt; 等算法中，它们可能会在内部使用不同的策略来确保异常安全，而 &lt;code&gt;noexcept&lt;/code&gt; 信息是决策的关键依据。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：&lt;code&gt;noexcept&lt;/code&gt; 是标准库选择更高效算法（特别是移动操作）而非更安全但低效算法（拷贝操作）的“许可证”。&lt;/p&gt;
&lt;h3&gt;3. 异常传播处理&lt;/h3&gt;
&lt;p&gt;这是关于契约违反后的处理机制，是面试中的关键考察点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;契约与承诺&lt;/strong&gt;：将函数声明为 &lt;code&gt;noexcept&lt;/code&gt; 是程序员向编译器和用户做出的一个&lt;strong&gt;坚定承诺&lt;/strong&gt;，保证该函数的执行绝不会导致异常传播到函数体外。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时行为&lt;/strong&gt;：如果这个承诺在运行时被打破（即函数内部抛出了异常），C++ 标准规定程序必须立即调用 &lt;strong&gt;&lt;code&gt;std::terminate()&lt;/code&gt;&lt;/strong&gt; 函数来终止执行。这个过程是&lt;strong&gt;不可捕获、不可恢复&lt;/strong&gt;的。&lt;code&gt;std::terminate()&lt;/code&gt; 的默认行为是中止程序（abort），可能不会调用局部对象的析构函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设计哲学&lt;/strong&gt;：这种行为背后的哲学是，违反 &lt;code&gt;noexcept&lt;/code&gt; 承诺是一个&lt;strong&gt;严重的逻辑错误&lt;/strong&gt;，其后果是不可预期的。与其让异常在缺乏正确异常处理机制的上下文中传播，导致未定义行为，不如以一种明确、果断的方式终止程序，这保证了程序行为的确定性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：在 &lt;code&gt;noexcept&lt;/code&gt; 函数中抛出异常会导致程序立即强制终止（&lt;code&gt;std::terminate()&lt;/code&gt;），这是一种“契约违反”的严厉惩罚机制，旨在防止更糟糕的未定义行为。&lt;/p&gt;
&lt;h2&gt;110. 虚函数表底层原理实现&lt;/h2&gt;
&lt;p&gt;C++ 中的虚函数表（vtable）是实现运行时多态（动态绑定）的核心机制。&lt;/p&gt;
&lt;p&gt;其底层原理是：编译器会为每个包含虚函数的类（或从该类继承的子类）秘密地创建一个虚函数表。这个表本质上是一个函数指针数组，其中的每个条目都指向该类的一个虚函数的具体实现。当一个类对象被创建时，编译器会自动在对象的内存布局的开头（或末尾，取决于编译器实现）插入一个隐藏的指针成员（vptr），该指针被初始指向这个类相应的虚函数表。当通过基类指针或引用调用虚函数时，代码会通过对象的 vptr 找到对应的 vtable，然后在 vtable 中查找并调用正确偏移量处的函数指针。这个过程是在运行时进行的，因此能够根据对象的实际类型来决定调用哪个函数，从而实现多态。如果子类重写了虚函数，那么子类的 vtable 中对应条目就会指向子类的函数，而非父类的。&lt;/p&gt;
&lt;h2&gt;111. &lt;code&gt;async&lt;/code&gt; 和 &lt;code&gt;thread pool&lt;/code&gt; 你在使用的时候是怎么选择的&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 是一个关键字或概念，用于声明一个函数是&lt;strong&gt;异步函数&lt;/strong&gt;。它是现代编程语言（如 JavaScript, Python, C#, Rust, C++ 等）中处理并发操作的核心特性，其目的是为了更高效、更简洁地编写非阻塞的、基于 I/O 操作的代码。&lt;/p&gt;
&lt;p&gt;其核心思想是：&lt;strong&gt;让一个耗时的操作（如网络请求、文件读写、数据库查询）在后台等待完成，而不阻塞主线程的执行&lt;/strong&gt;。程序可以在此期间去做其他工作，当那个耗时操作完成后，再回来处理它的结果。&lt;/p&gt;
&lt;p&gt;一个用 &lt;code&gt;async&lt;/code&gt; 标记的函数，通常被称为&lt;strong&gt;协程 (Coroutine)&lt;/strong&gt;。它与普通函数和线程都不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不同于普通函数&lt;/strong&gt;：普通函数是“一路走到黑”的，从调用开始直到 return 语句返回结果，期间调用者必须等待。而异步函数被调用时，会立即返回一个&lt;strong&gt;承诺对象&lt;/strong&gt;（通常叫 &lt;code&gt;Promise&lt;/code&gt;, &lt;code&gt;Future&lt;/code&gt;, 或 &lt;code&gt;Task&lt;/code&gt;），而不是最终结果。这个承诺对象代表一个“未来会完成的操作”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不同于线程&lt;/strong&gt;：协程是&lt;strong&gt;单线程&lt;/strong&gt;下的并发技术。多个协程在一个线程上交替运行，由&lt;strong&gt;事件循环 (Event Loop)&lt;/strong&gt; 调度。当一个协程遇到 &lt;code&gt;await&lt;/code&gt;（等待）关键字时，它会主动挂起（Suspends），将控制权交还给事件循环，事件循环 then 可以去执行其他就绪的协程。这避免了多线程带来的上下文切换开销和复杂的同步问题（如锁、死锁）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;关键搭档：&lt;code&gt;await&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 总是与 &lt;code&gt;await&lt;/code&gt; 关键字配对使用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;async&lt;/code&gt;：用于&lt;strong&gt;声明&lt;/strong&gt;一个函数是异步的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;await&lt;/code&gt;：用于&lt;strong&gt;调用&lt;/strong&gt;一个异步操作。它只能在 &lt;code&gt;async&lt;/code&gt; 函数内部使用。当执行到 &lt;code&gt;await expression&lt;/code&gt; 时，它会做两件事：
&lt;ol&gt;
&lt;li&gt;挂起当前 &lt;code&gt;async&lt;/code&gt; 函数的执行。&lt;/li&gt;
&lt;li&gt;等待 &lt;code&gt;expression&lt;/code&gt;（通常是一个 &lt;code&gt;Promise/Future&lt;/code&gt;）完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在等待期间，线程不会被阻塞，事件循环可以去执行其他代码。一旦等待的操作完成，事件循环会唤醒这个被挂起的协程，并从 &lt;code&gt;await&lt;/code&gt; 之后的地方继续执行，并获取最终结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我的选择标准主要基于任务的性质、对性能的控制需求以及资源的生命周期：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用 &lt;code&gt;std::async&lt;/code&gt; 的场景&lt;/strong&gt;：对于简单的、&lt;strong&gt;一次性&lt;/strong&gt;的异步任务，我会优先选择 &lt;code&gt;std::async&lt;/code&gt;。它通过 &lt;code&gt;std::future&lt;/code&gt; 提供了便捷的获取结果的机制，并且由标准库自动管理线程的创建和销毁（虽然可以通过启动策略 &lt;code&gt;std::launch::async&lt;/code&gt; 强制创建新线程，或用 &lt;code&gt;std::launch::deferred&lt;/code&gt; 延迟执行），代码非常简洁。这非常适合“发射后不管”或需要简单等待结果的任务，例如前端快速发起一个后台计算或查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用 &lt;code&gt;thread pool&lt;/code&gt; 的场景&lt;/strong&gt;：在高性能、高并发或需要精细控制的场景下，我一定会选择线程池。线程池的核心优势在于&lt;strong&gt;线程复用&lt;/strong&gt;，它预先创建并维护一组工作线程，避免了频繁创建和销毁线程带来的巨大开销。这非常适合处理大量、短小、频繁的&lt;strong&gt;任务队列&lt;/strong&gt;，例如 Web 服务器处理海量短连接请求。此外，线程池给予我更大的控制权，我可以明确管理线程数量、任务队列的饱和策略等，从而防止系统资源被无限增长的线程耗尽。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;112. Python 和 C++ 语言区别和特点，什么时候用 Python 什么时候用 C++？&lt;/h2&gt;
&lt;p&gt;Python 和 C++ 是两种设计哲学和应用领域截然不同的语言。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;区别与特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C++&lt;/strong&gt;：是一种&lt;strong&gt;编译型&lt;/strong&gt;、&lt;strong&gt;静态类型&lt;/strong&gt;的系统级语言。它追求&lt;strong&gt;零开销抽象&lt;/strong&gt;（Zero-overhead Abstraction），提供对硬件和内存的精细控制，性能极高。但代价是语法复杂，需要开发者手动管理内存等资源，开发迭代速度较慢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python&lt;/strong&gt;：是一种&lt;strong&gt;解释型&lt;/strong&gt;、&lt;strong&gt;动态类型&lt;/strong&gt;的高级语言。它强调代码的&lt;strong&gt;可读性和开发效率&lt;/strong&gt;，语法简洁，拥有庞大的开源库生态。但其运行时性能远低于 C++， 由于其全局解释器锁（GIL）和动态特性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;选择标准&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用 C++&lt;/strong&gt;：当性能是首要目标，或需要直接与操作系统/硬件交互时。典型场景包括：游戏引擎、高频交易系统、浏览器/数据库等大型基础软件、嵌入式系统、性能关键的中间件等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用 Python&lt;/strong&gt;：当开发速度和易用性更重要时。典型场景包括：Web 后端（Django/Flask）、数据科学/机器学习（NumPy/Pandas/TensorFlow/PyTorch）、自动化运维脚本、快速原型验证、以及作为“胶水语言”将各种 C++库粘合起来。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;113. 有没有在 Window 上编译开发的经验&lt;/h2&gt;
&lt;p&gt;我有在 Windows 平台上进行编译和开发的丰富经验。我的主要工具链是&lt;strong&gt;Visual Studio&lt;/strong&gt;及其集成的 MSVC 编译器，我熟悉其项目属性配置、调试器以及各种构建选项。此外，为了保持项目的跨平台兼容性，我广泛使用 &lt;strong&gt;CMake&lt;/strong&gt; 来生成 Visual Studio 的解决方案（.sln）文件。我也接触过 &lt;strong&gt;MinGW-w64&lt;/strong&gt; 和 Cygwin 这样的 GCC 移植版本，以便在 Windows 上获得类 Unix 的编译环境。对于构建过程的管理，我除了使用 Visual Studio IDE，也熟练使用 &lt;strong&gt;MSBuild&lt;/strong&gt; 命令行动态进行自动化构建和持续集成流程。&lt;/p&gt;
&lt;h2&gt;114. Linux 下用什么工具去编译&lt;/h2&gt;
&lt;p&gt;在 Linux 下，编译的核心工具是 &lt;strong&gt;GCC&lt;/strong&gt;（GNU Compiler Collection）和 &lt;strong&gt;Clang&lt;/strong&gt;。它们是实际的编译器前端/后端，负责将源代码编译成目标文件。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GCC&lt;/strong&gt;：是 Linux 生态系统的传统标准，支持语言广泛，非常成熟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clang&lt;/strong&gt;：以其更快的编译速度、更友好的错误/警告信息以及作为 LLVM 项目的一部分而闻名，近年来应用非常广泛。&lt;/li&gt;
&lt;li&gt;在实际开发中，我很少直接调用这些编译器，而是通过&lt;strong&gt;构建工具&lt;/strong&gt;来管理复杂的编译过程：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Make&lt;/strong&gt;：是最基础和最通用的构建工具。它通过读取 &lt;strong&gt;Makefile&lt;/strong&gt; 文件来定义源文件之间的依赖关系和构建规则，然后调用 GCC/Clang 等编译器进行增量编译和链接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CMake&lt;/strong&gt;：是一个更高级的跨平台构建系统&lt;strong&gt;生成器&lt;/strong&gt;。它读取开发者编写的&lt;code&gt;CMakeLists.txt&lt;/code&gt;文件，然后为底层构建工具（如 Make、Ninja）生成相应的构建文件（如 Makefile）。这使得项目可以轻松地在不同平台和 IDE 之间迁移。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ninja&lt;/strong&gt;：是一个专注于速度的小型构建系统，其构建文件通常由 CMake 或 GN 等高级工具生成，在大型项目（如 Chrome）中能显著提升构建速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;115. makefile 有什么格式&lt;/h2&gt;
&lt;p&gt;Makefile的基本格式由一系列&lt;strong&gt;规则&lt;/strong&gt;组成，每条规则定义了如何从一个或多个源文件（依赖）构建出目标文件。其标准结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-makefile&quot;&gt;target ... : prerequisites ...
    recipe
    ...
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;target（目标）&lt;/strong&gt;：通常是需要生成的文件名（如&lt;code&gt;main.o&lt;/code&gt;）或一个动作的名称（伪目标，如&lt;code&gt;clean&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;prerequisites（前置条件/依赖）&lt;/strong&gt;：是生成&lt;code&gt;target&lt;/code&gt;所需要的文件或另一个目标。如果任何前置条件比目标新，make 就会重新构建目标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;recipe（配方/命令）&lt;/strong&gt;：是make为了构建目标而需要执行的一条或多条Shell命令。&lt;strong&gt;这些命令必须以 Tab 字符开头&lt;/strong&gt;，而不能是空格。&lt;/li&gt;
&lt;li&gt;此外，Makefile还支持&lt;strong&gt;变量&lt;/strong&gt;（如&lt;code&gt;CC = gcc&lt;/code&gt;）、&lt;strong&gt;自动变量&lt;/strong&gt;（如&lt;code&gt;$@&lt;/code&gt;代表当前目标，&lt;code&gt;$^&lt;/code&gt;代表所有依赖）、&lt;strong&gt;隐含规则&lt;/strong&gt;和&lt;strong&gt;指令&lt;/strong&gt;（如&lt;code&gt;include&lt;/code&gt;），用于使 Makefile 更简洁、更易于维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;116. Linux 下的 &lt;code&gt;select&lt;/code&gt; 和 &lt;code&gt;epoll&lt;/code&gt;，这两个有什么不一样&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;select&lt;/code&gt;和&lt;code&gt;epoll&lt;/code&gt;都是 Linux 上用于 I/O 多路复用的系统调用，允许一个进程同时监视多个文件描述符（如 socket）的就绪状态，但它们在设计和性能上差异巨大。&lt;/p&gt;
&lt;h3&gt;select&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机制&lt;/strong&gt;：采用&lt;strong&gt;轮询&lt;/strong&gt;和&lt;strong&gt;线性扫描&lt;/strong&gt;的方式。每次调用时，内核需要将所有用户传入的文件描述符集合从用户态拷贝到内核态，然后内核线性扫描所有描述符以检查就绪状态。调用返回后，用户程序也需要线性扫描整个集合来找出就绪的描述符。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;文件描述符数量有上限（通常为 1024）&lt;/li&gt;
&lt;li&gt;每次调用都需要重复的拷贝和扫描开销，性能随描述符数量增加而线性下降；&lt;/li&gt;
&lt;li&gt;需要用户程序自己遍历所有描述符来查找就绪项。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;epoll&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机制&lt;/strong&gt;：采用&lt;strong&gt;事件回调&lt;/strong&gt;（基于红黑树和就绪链表）的方式。它通过&lt;code&gt;epoll_create&lt;/code&gt;创建一个上下文，通过&lt;code&gt;epoll_ctl&lt;/code&gt;&lt;strong&gt;单独地&lt;/strong&gt;添加或移除要监视的描述符（只需拷贝一次）。&lt;code&gt;epoll_wait&lt;/code&gt;调用则等待事件发生，返回时只提供已经就绪的描述符列表，无需扫描全部。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;支持的文件描述符数量是系统级别的，非常大；&lt;/li&gt;
&lt;li&gt;避免了每次调用时重复的文件描述符集合拷贝和内核扫描，性能不会随描述符数量增加而显著下降，非常适合连接数巨大的场景；&lt;/li&gt;
&lt;li&gt;直接返回就绪的描述符，用户程序无需遍历。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;select&lt;/code&gt;是 POSIX 标准，跨平台性好，适用于连接数少且对兼容性要求高的场景。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;epoll&lt;/code&gt;是 Linux 特有的高性能机制，是现代高并发网络服务器（如 Nginx, Redis）的首选。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;117. &lt;code&gt;malloc&lt;/code&gt; 和 &lt;code&gt;mmap&lt;/code&gt; 有什么区别，分别在什么场景下使用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;malloc&lt;/code&gt; 和 &lt;code&gt;mmap&lt;/code&gt; 都用于分配内存，但它们是不同层次的机制，适用于不同场景。&lt;/p&gt;
&lt;h3&gt;malloc&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;是 C 库提供的&lt;strong&gt;用户层内存分配函数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：它管理的是&lt;strong&gt;堆内存&lt;/strong&gt;。&lt;code&gt;malloc&lt;/code&gt; 通常会先通过系统调用（如 &lt;code&gt;brk&lt;/code&gt; 或 &lt;code&gt;sbrk&lt;/code&gt;）从操作系统申请一大块内存作为堆，然后使用自己的算法（如 &lt;code&gt;ptmalloc&lt;/code&gt;）将这块大内存分割、管理，分配给应用程序。它处理的是相对小块的内存分配请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：适用于程序运行过程中&lt;strong&gt;频繁分配和释放中小块内存&lt;/strong&gt;的通用场景，例如创建链表节点、临时字符串、小型数据结构等。它是程序员日常开发中最常用的分配方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;mmap&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;是直接由操作系统提供的&lt;strong&gt;系统调用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：它可以在进程的虚拟地址空间中直接创建一个新的&lt;strong&gt;内存映射&lt;/strong&gt;。这个映射可以是&lt;strong&gt;匿名映射&lt;/strong&gt;（不关联文件，只用于分配大块内存，相当于向操作系统直接要内存），也可以是&lt;strong&gt;文件映射&lt;/strong&gt;（将文件直接映射到内存，便于对文件进行随机访问）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;分配大块内存：当需要分配的内存非常大（例如几十 MB 甚至 GB 级别）时，使用 &lt;code&gt;mmap&lt;/code&gt; 直接映射比通过 &lt;code&gt;malloc&lt;/code&gt; 在堆上分配更高效，且避免碎片化问题。&lt;/li&gt;
&lt;li&gt;文件 IO：用于实现内存映射文件，将文件直接映射到内存空间，这样对内存的读写操作就相当于对文件进行读写，非常高效。&lt;/li&gt;
&lt;li&gt;进程间共享内存：通过映射同一个文件，可以实现多个进程间的共享内存通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;118. 虚表是一个类有一个还是一个对象有一个？&lt;/h2&gt;
&lt;h3&gt;类与虚表的关系&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;一个类有一个虚表&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;编译器在编译期为每个含有虚函数的类生成一张虚表，里面存放该类所有虚函数（以及可能的 RTTI 信息）的函数指针。&lt;/p&gt;
&lt;p&gt;如果子类重写（override）了某个虚函数，那么编译器在子类的虚表中把对应槽位替换为子类的函数指针。&lt;/p&gt;
&lt;h3&gt;对象与虚表的关系&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;每个对象有一个虚表指针（vptr）&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;当对象被构造时，构造函数会把 vptr 设置为指向该对象所属类的虚表。&lt;/p&gt;
&lt;p&gt;因此，同一个类的所有对象共享同一张虚表，只是它们各自的 vptr 都指向这张表。&lt;/p&gt;
&lt;h3&gt;内存示意图&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Base {
    virtual void foo();
    virtual void bar();
};
struct Derived : Base {
    void foo() override;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器会生成两张虚表：&lt;code&gt;Base::vtable&lt;/code&gt; 和 &lt;code&gt;Derived::vtable&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Base::vtable&lt;/code&gt; 里，槽 0 → &lt;code&gt;Base::foo&lt;/code&gt;，槽 1 → &lt;code&gt;Base::bar&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;🔥 在 &lt;code&gt;Derived::vtable&lt;/code&gt; 里，槽 0 → &lt;code&gt;Derived::foo&lt;/code&gt;（覆盖了），槽 1 → &lt;code&gt;Base::bar&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;每个 &lt;code&gt;Base&lt;/code&gt; 或 &lt;code&gt;Derived&lt;/code&gt; 对象的起始位置存放一个 vptr，分别指向对应的虚表。&lt;/p&gt;
&lt;h2&gt;119. 查虚表的时间复杂度是多少&lt;/h2&gt;
&lt;p&gt;在 C++ 中，虚函数调用通过虚表实现，其查找过程只需要：从对象中取出虚表指针 &lt;code&gt;vptr&lt;/code&gt;，根据已知的偏移量在虚表中找到对应的函数指针，然后执行间接跳转。由于这些步骤都是固定次数的指针访问和寻址操作，与对象数量或虚函数多少无关，所以查虚表的时间复杂度是 &lt;strong&gt;O(1)&lt;/strong&gt;，只比普通函数调用多一次间接寻址。&lt;/p&gt;
&lt;p&gt;在多继承或虚拟继承中，一个对象可能有多个 &lt;code&gt;vptr&lt;/code&gt;。这种情况下，调用某些虚函数时，编译器可能需要多一次指针调整（偏移修正）。但仍然是固定次数的操作，复杂度依然是 &lt;strong&gt;O(1)&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;120. &lt;code&gt;std::move&lt;/code&gt; 的原理（涉及移动吗）&lt;/h2&gt;
&lt;p&gt;在 C++ 中，&lt;code&gt;std::move&lt;/code&gt; 的作用是&lt;strong&gt;将一个对象显式地转换为右值引用&lt;/strong&gt;，从而让编译器有机会调用该对象的&lt;strong&gt;移动构造函数&lt;/strong&gt;或&lt;strong&gt;移动赋值运算符&lt;/strong&gt;，而不是拷贝版本。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;std::move&lt;/code&gt; 并不会真的“移动”数据，它只是做了一个 &lt;code&gt;static_cast&amp;#x3C;T&amp;#x26;&amp;#x26;&gt;&lt;/code&gt; 的强制类型转换，把传入的左值转为右值引用。&lt;/p&gt;
&lt;p&gt;一旦对象被当作右值使用，如果该类型定义了移动构造或移动赋值函数，就会发生资源所有权的“转移”，通常只需要指针交换，效率比深拷贝高。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;std::move&lt;/code&gt; 后，被转移的对象仍然处于有效但未指定状态（可以析构或赋新值，但不能假设它保留旧值）。&lt;/p&gt;
&lt;h2&gt;121. 智能指针原理&lt;/h2&gt;
&lt;p&gt;智能指针是 C++ 标准库中用来&lt;strong&gt;自动管理对象生命周期&lt;/strong&gt;的类模板，本质上是一个“带有内存管理功能的指针包装器”。它的原理可以从以下几个方面理解。&lt;/p&gt;
&lt;h3&gt;核心思想&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;普通指针需要手动 &lt;code&gt;delete&lt;/code&gt;，容易造成内存泄漏或悬垂指针。&lt;/li&gt;
&lt;li&gt;智能指针通过 RAII  原则，在智能指针对象析构时自动释放资源。&lt;/li&gt;
&lt;li&gt;它对指针的操作符 &lt;code&gt;*&lt;/code&gt;、&lt;code&gt;-&gt;&lt;/code&gt; 进行了重载，看起来像普通指针，但有更安全的内存管理机制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;std::unique_ptr&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;独占所有权&lt;/strong&gt;：同一时间只能有一个 &lt;code&gt;unique_ptr&lt;/code&gt; 拥有某个对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;内部保存一个裸指针。&lt;/li&gt;
&lt;li&gt;禁止拷贝（拷贝构造和赋值被 &lt;code&gt;delete&lt;/code&gt;），但支持 &lt;strong&gt;移动语义&lt;/strong&gt;（转移所有权）。&lt;/li&gt;
&lt;li&gt;析构时自动 &lt;code&gt;delete&lt;/code&gt; 管理的对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;简化实现&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template &amp;#x3C;typename T&gt;
class unique_ptr {
    T* ptr;
public:
    explicit unique_ptr(T* p = nullptr) : ptr(p) {}
    ~unique_ptr() { delete ptr; }

    unique_ptr(const unique_ptr&amp;#x26;) = delete;
    unique_ptr&amp;#x26; operator=(const unique_ptr&amp;#x26;) = delete;

    unique_ptr(unique_ptr&amp;#x26;&amp;#x26; other) noexcept : ptr(other.ptr) {
        other.ptr = nullptr;
    }
    unique_ptr&amp;#x26; operator=(unique_ptr&amp;#x26;&amp;#x26; other) noexcept {
        if (this != &amp;#x26;other) {
            delete ptr;
            ptr = other.ptr;
            other.ptr = nullptr;
        }
        return *this;
    }

    T&amp;#x26; operator*()  { return *ptr; }
    T* operator-&gt;() { return ptr; }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;std::shared_ptr&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;共享所有权&lt;/strong&gt;：多个智能指针可以共同管理一个对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;除了指针，还维护一个 &lt;strong&gt;引用计数（control block）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;每次拷贝 &lt;code&gt;shared_ptr&lt;/code&gt; 时，计数 +1；析构时，计数 -1；当计数归零，释放对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;简化实现&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template &amp;#x3C;typename T&gt;
class shared_ptr {
    T* ptr;
    int* count;
public:
    explicit shared_ptr(T* p = nullptr) : ptr(p), count(new int(1)) {}
    ~shared_ptr() {
        if (--(*count) == 0) {
            delete ptr;
            delete count;
        }
    }

    shared_ptr(const shared_ptr&amp;#x26; other) : ptr(other.ptr), count(other.count) {
        ++(*count);
    }

    shared_ptr&amp;#x26; operator=(const shared_ptr&amp;#x26; other) {
        if (this != &amp;#x26;other) {
            if (--(*count) == 0) {
                delete ptr;
                delete count;
            }
            ptr = other.ptr;
            count = other.count;
            ++(*count);
        }
        return *this;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;std::weak_ptr&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;弱引用&lt;/strong&gt;：不影响对象生命周期，避免 &lt;code&gt;shared_ptr&lt;/code&gt; 的循环引用问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;持有指向控制块的弱引用计数。&lt;/li&gt;
&lt;li&gt;不能直接解引用，需要通过 &lt;code&gt;lock()&lt;/code&gt; 临时获取 &lt;code&gt;shared_ptr&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;当对象已销毁时，&lt;code&gt;lock()&lt;/code&gt; 返回空指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;122. 假设有一个 1KB 的大对象，&lt;code&gt;move&lt;/code&gt; 能节省拷贝吗&lt;/h2&gt;
&lt;p&gt;当我们在 C++ 中处理一个 1KB 甚至更大的对象时，是否使用 &lt;code&gt;std::move&lt;/code&gt; 对性能会有明显影响。&lt;strong&gt;如果只进行拷贝&lt;/strong&gt;，编译器会调用拷贝构造函数或拷贝赋值函数，把这 1KB 的数据逐字节复制到新的内存空间里，这个过程的复杂度是 O(n)，而且对象越大，时间和内存消耗就越高。&lt;strong&gt;但如果使用 &lt;code&gt;std::move&lt;/code&gt;&lt;/strong&gt;，它会把对象转化为右值引用，从而触发类型的移动构造函数或移动赋值运算符。在标准库容器和大多数现代类的实现中，移动操作通常只是把内部资源的指针或句柄“偷走”，并将源对象的指针清空，这样就避免了逐字节拷贝，复杂度降为 O(1)。换句话说，&lt;code&gt;std::move&lt;/code&gt; 并不是直接完成“移动”的动作，而是告诉编译器“这个对象可以被当作右值处理”，从而让类的移动语义生效。结果是：对于像 &lt;code&gt;std::string&lt;/code&gt;、&lt;code&gt;std::vector&lt;/code&gt; 这样内部依赖堆内存的大对象，使用 &lt;code&gt;std::move&lt;/code&gt; 能显著节省拷贝成本，把原本昂贵的内存复制过程简化为一次指针交换，从而大幅提高效率。&lt;/p&gt;
&lt;h2&gt;123. new 和 malloc 有什么区别呢&lt;/h2&gt;
&lt;h3&gt;1. 类型安全与返回值&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;new&lt;/code&gt;&lt;/strong&gt;：返回的是指定类型的指针，不需要强制类型转换，例如 &lt;code&gt;int* p = new int;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;malloc&lt;/code&gt;&lt;/strong&gt;：返回 &lt;code&gt;void*&lt;/code&gt;，必须显式转换为目标类型，例如 &lt;code&gt;int* p = (int*)malloc(sizeof(int));&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 构造/析构函数调用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;new&lt;/code&gt;&lt;/strong&gt;：除了分配内存，还会自动调用对象的 &lt;strong&gt;构造函数&lt;/strong&gt;，初始化对象；&lt;code&gt;delete&lt;/code&gt; 会调用 &lt;strong&gt;析构函数&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;malloc&lt;/code&gt;&lt;/strong&gt;：只分配原始内存块，不会调用构造函数；对应的 &lt;code&gt;free&lt;/code&gt; 也不会调用析构函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 异常与错误处理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;new&lt;/code&gt;&lt;/strong&gt;：分配失败时会抛出 &lt;code&gt;std::bad_alloc&lt;/code&gt; 异常（除非用 &lt;code&gt;new(std::nothrow)&lt;/code&gt; 版本），所以更符合 C++ 的异常处理方式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;malloc&lt;/code&gt;&lt;/strong&gt;：分配失败时返回 &lt;code&gt;NULL&lt;/code&gt;，需要手动检查指针是否为空。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 内存大小计算&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;new&lt;/code&gt;&lt;/strong&gt;：不需要显式写 &lt;code&gt;sizeof&lt;/code&gt;，编译器根据类型自动计算内存大小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;malloc&lt;/code&gt;&lt;/strong&gt;：必须手动传入字节数，例如 &lt;code&gt;malloc(sizeof(MyClass) * 10)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 重载与自定义&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;new/delete&lt;/code&gt;&lt;/strong&gt;：可以被用户重载，支持自定义内存分配策略（如内存池）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;malloc/free&lt;/code&gt;&lt;/strong&gt;：是 C 语言库函数，不能被重载，只能按标准方式分配。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. 使用场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;推荐使用 &lt;code&gt;new/delete&lt;/code&gt;&lt;/strong&gt;：管理 C++ 对象，保证构造/析构函数的调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;malloc/free&lt;/code&gt; 主要用于 C 接口&lt;/strong&gt;：或者需要与 C 库交互的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;124. 用 &lt;code&gt;new&lt;/code&gt; 生成的对象，可以用 &lt;code&gt;free&lt;/code&gt; 释放吗&lt;/h2&gt;
&lt;p&gt;在 C++ 中，内存管理是&lt;strong&gt;成对出现&lt;/strong&gt;的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new&lt;/code&gt; 必须对应 &lt;code&gt;delete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new[]&lt;/code&gt; 必须对应 &lt;code&gt;delete[]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;malloc&lt;/code&gt; 必须对应 &lt;code&gt;free&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;原因&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;构造/析构函数&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new&lt;/code&gt; 不仅分配内存，还调用对象的构造函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete&lt;/code&gt; 在释放内存之前，会先调用析构函数，再释放内存。&lt;/li&gt;
&lt;li&gt;如果用 &lt;code&gt;free&lt;/code&gt; 去释放 &lt;code&gt;new&lt;/code&gt; 出来的对象，析构函数不会被调用，会造成资源泄漏（比如没有关闭文件、释放锁、释放堆内存等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行时实现不同&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;C++ 编译器对 &lt;code&gt;new/delete&lt;/code&gt; 可能有自己的内存分配策略（比如对象对齐、内存池）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;malloc/free&lt;/code&gt; 走的是 C 语言标准库的堆分配接口。&lt;/li&gt;
&lt;li&gt;混用可能导致堆结构损坏，甚至程序崩溃。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;正确做法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;new&lt;/code&gt; 分配 → 用 &lt;code&gt;delete&lt;/code&gt; 释放。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;new[]&lt;/code&gt; 分配数组 → 用 &lt;code&gt;delete[]&lt;/code&gt; 释放。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;malloc&lt;/code&gt; 分配 → 用 &lt;code&gt;free&lt;/code&gt; 释放。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;125. 如果 &lt;code&gt;new&lt;/code&gt; 基础类型，比如整型，可以用 &lt;code&gt;free&lt;/code&gt; 释放吗&lt;/h2&gt;
&lt;p&gt;即使是 &lt;code&gt;int&lt;/code&gt; 这样的基础类型，用 &lt;code&gt;new&lt;/code&gt; 分配的对象也必须用 &lt;code&gt;delete&lt;/code&gt; 来释放，而不能用 &lt;code&gt;free&lt;/code&gt;，因为在 C++ 中 &lt;code&gt;new/delete&lt;/code&gt; 与 &lt;code&gt;malloc/free&lt;/code&gt; 属于两个完全不同的内存管理体系，虽然 &lt;code&gt;int&lt;/code&gt; 没有构造和析构函数，看似没有区别，但 &lt;code&gt;new&lt;/code&gt; &lt;strong&gt;在分配时可能会附加一些编译器特定的元信息&lt;/strong&gt;，释放时需要 &lt;code&gt;delete&lt;/code&gt; 正确处理，&lt;strong&gt;否则就会触发未定义行为&lt;/strong&gt;；因此，哪怕是最简单的基础类型，也要严格遵循成对使用 &lt;code&gt;new/delete&lt;/code&gt;、&lt;code&gt;malloc/free&lt;/code&gt; 的规则，才能保证程序的正确性和可移植性。&lt;/p&gt;
&lt;h2&gt;126. 用 &lt;code&gt;new&lt;/code&gt; 创建数组时，释放的时候需要写出元素个数吗&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;new&lt;/code&gt; 创建数组时，释放的时候并不需要写出元素个数，只要保证使用 &lt;code&gt;delete[]&lt;/code&gt; 而不是 &lt;code&gt;delete&lt;/code&gt; 即可。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int* arr = new int[10];  
delete[] arr;  // 不需要写 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是因为编译器在用 &lt;code&gt;new[]&lt;/code&gt; 分配数组时，通常会在分配的内存块里额外保存数组的大小信息（比如在头部存储元素数量），这样在执行 &lt;code&gt;delete[]&lt;/code&gt; 时，它就能自动知道有多少个元素需要调用析构函数。反过来，如果你错误地用 &lt;code&gt;delete arr;&lt;/code&gt; 来释放一个 &lt;code&gt;new[]&lt;/code&gt; 出来的数组，就只会对第一个元素调用析构函数，其余元素不会被正确销毁，导致资源泄漏或未定义行为。&lt;/p&gt;
&lt;h2&gt;127. &lt;code&gt;std::map&lt;/code&gt; 和 B+ tree 有什么区别呢&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;std::map&lt;/code&gt; 多用于内存里的有序关联容器，底层常用红黑树实现，而 B+ 树是数据库和文件系统常用的磁盘友好型索引结构，前者注重通用性和内存效率，后者注重降低磁盘 I/O 和范围查询性能。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;数据结构和实现&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::map&lt;/code&gt;&lt;/strong&gt;：在 C++ 标准库里通常实现为 &lt;strong&gt;红黑树&lt;/strong&gt;（自平衡二叉查找树）。它的每个节点存储一个键值对 &lt;code&gt;(key, value)&lt;/code&gt;，树高大约是 &lt;code&gt;O(log n)&lt;/code&gt;，支持快速查找、插入和删除。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B+ 树&lt;/strong&gt;：属于多路平衡查找树，常用于数据库和文件系统。B+ 树的节点可以存放多个键，非叶子节点只保存索引信息，所有数据都放在叶子节点，叶子节点之间用链表相连，便于范围查询和磁盘顺序访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;时间复杂度&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;两者在理论上的查找、插入、删除复杂度都是 &lt;strong&gt;O(log n)&lt;/strong&gt;。区别在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::map&lt;/code&gt; 的红黑树一般是二叉树，高度相对较高（大约 &lt;code&gt;log₂ n&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;B+ 树是多路的（阶数可达上百甚至上千），树高很低（大约 &lt;code&gt;logₘ n&lt;/code&gt;，m 通常远大于 2），因此在磁盘 I/O 场景下效率更高。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;内存和存储&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::map&lt;/code&gt; 是内存型数据结构，适合内存中存储少量到中等规模数据。&lt;/li&gt;
&lt;li&gt;B+ 树设计初衷就是&lt;strong&gt;面向磁盘&lt;/strong&gt;，节点大小通常和磁盘页对齐（比如 4KB），减少磁盘 I/O 次数，特别适合大规模数据的索引结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;范围查询&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::map&lt;/code&gt; 可以通过迭代器顺序遍历，但元素分布在树节点里，缓存局部性较差。&lt;/li&gt;
&lt;li&gt;B+ 树所有数据存储在叶子节点，并且叶子节点通过链表连接，天然支持高效的范围查询和顺序扫描，非常适合数据库中的 &lt;code&gt;BETWEEN&lt;/code&gt; 查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;使用场景&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::map&lt;/code&gt;&lt;/strong&gt;：内存中需要有序字典映射的场景，如符号表、配置表等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B+ 树&lt;/strong&gt;：数据库索引、文件系统目录、键值存储引擎等大规模、基于磁盘的数据管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;128. 红黑树与 B+ Tree 的性能、内存空间占用对比&lt;/h2&gt;
&lt;p&gt;从性能角度比较 &lt;strong&gt;&lt;code&gt;std::map&lt;/code&gt;（红黑树）&lt;/strong&gt; 和 &lt;strong&gt;B+ 树&lt;/strong&gt;，主要差异在于它们设计时针对的场景不同：&lt;/p&gt;
&lt;h3&gt;性能&lt;/h3&gt;
&lt;h4&gt;1. 内存场景&lt;/h4&gt;
&lt;p&gt;在内存中存放数据时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::map&lt;/code&gt;&lt;/strong&gt; 往往更合适。它是二叉平衡树，节点存储较紧凑，指针操作简单；对查找、插入、删除的复杂度都是 &lt;code&gt;O(log n)&lt;/code&gt;，常数因子较小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B+ 树&lt;/strong&gt; 在内存里因为是多路节点，访问时需要在节点内部做一次线性查找或二分查找，常数因子比红黑树大一些，在数据量不算特别大时反而不如红黑树快。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 磁盘/大数据场景&lt;/h4&gt;
&lt;p&gt;当数据量非常大、不能完全放入内存，需要频繁访问磁盘时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;B+ 树&lt;/strong&gt; 更高效。它的节点大小和磁盘页对齐（比如 4KB 一页），一次磁盘 I/O 可以读入几十甚至几百个键，树高非常低（常见情况下只有 2~4 层），大大减少了磁盘访问次数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::map&lt;/code&gt;&lt;/strong&gt; 设计时并没有考虑磁盘访问，二叉树节点很分散，局部性差，如果把它用在磁盘数据结构上，I/O 开销会非常大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 范围查询&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;B+ 树&lt;/strong&gt; 的叶子节点用链表串联起来，做范围查询或顺序遍历时几乎是顺序扫描，效率很高，适合数据库中的 &lt;code&gt;WHERE x BETWEEN ...&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::map&lt;/code&gt;&lt;/strong&gt; 也能用迭代器遍历，但因为节点分散在内存，缓存命中率差，性能不如 B+ 树。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;空间占用&lt;/h3&gt;
&lt;h4&gt;1. &lt;code&gt;std::map&lt;/code&gt;（红黑树）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;每个节点只保存一个键值对 &lt;code&gt;(key, value)&lt;/code&gt;，外加几个指针（父、左、右，可能还有颜色位）。&lt;/li&gt;
&lt;li&gt;因为是二叉树，&lt;strong&gt;指针开销相对较大&lt;/strong&gt;，尤其是当 key 和 value 本身比较小的时候，指针反而成了主要开销。&lt;/li&gt;
&lt;li&gt;节点分散在堆上分配，&lt;strong&gt;内存局部性差&lt;/strong&gt;，缓存命中率低。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;举例：一个 &lt;code&gt;std::map&amp;#x3C;int, int&gt;&lt;/code&gt; 节点，数据只占 8 字节，但可能要额外存储 3–4 个指针（24–32 字节），加上对齐，内存开销可能是实际数据的几倍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. B+ 树&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;一个节点存储多个键和子指针，通常会填满一个固定大小的页（比如 4KB）。&lt;/li&gt;
&lt;li&gt;因为是多路树，树高更低，&lt;strong&gt;指针数量远少于红黑树&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;数据集中存放在叶子节点，且叶子节点通过链表连接，&lt;strong&gt;空间利用率更高&lt;/strong&gt;，局部性也更好。&lt;/li&gt;
&lt;li&gt;在数据库中，B+ 树的节点大小一般和磁盘页对齐，因此不仅节省内存，还能最大化磁盘 I/O 的效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 对比总结&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内存占用上&lt;/strong&gt;：红黑树（&lt;code&gt;std::map&lt;/code&gt;）因为每个节点只存一个元素，加上大量指针，空间效率偏低；B+ 树每个节点存很多元素，指针摊薄，空间效率明显更好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;局部性上&lt;/strong&gt;：B+ 树更紧凑、连续，缓存命中率高；红黑树分散，缓存命中率差。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;129. 为什么数据库里选择 &lt;strong&gt;B+Tree&lt;/strong&gt; 而不是红黑树？&lt;/h2&gt;
&lt;p&gt;数据库里选择 &lt;strong&gt;B+Tree&lt;/strong&gt; 而不是红黑树，核心原因是二者的设计目标不同：&lt;/p&gt;
&lt;h3&gt;1. 磁盘访问成本&lt;/h3&gt;
&lt;p&gt;数据库面对的是&lt;strong&gt;远超内存容量的大规模数据&lt;/strong&gt;，数据通常存放在磁盘里。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;B+Tree&lt;/strong&gt;：多路搜索树，一个节点可以容纳上百甚至上千个关键字，正好对应一个磁盘页（如 4KB）。一次磁盘 I/O 就能读入很多关键字，树的高度非常低（常常 2~4 层），意味着一次查询只需要少量磁盘访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;红黑树&lt;/strong&gt;：二叉树，每个节点只存一个关键字，树高约为 &lt;code&gt;log₂n&lt;/code&gt;，高度远高于 B+Tree。查找时要访问更多的节点，对应更多的磁盘 I/O，性能会非常差。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 顺序访问和范围查询&lt;/h3&gt;
&lt;p&gt;数据库里非常常见的操作是范围查询，比如 &lt;code&gt;WHERE age BETWEEN 20 AND 30&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;B+Tree&lt;/strong&gt;：所有数据都在叶子节点，叶子节点之间用链表相连，顺序遍历或范围扫描时只需要顺着链表走，效率极高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;红黑树&lt;/strong&gt;：虽然中序遍历可以得到有序结果，但节点分散，指针跳转频繁，对磁盘和缓存都不友好，顺序扫描效率低。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 空间与局部性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;B+Tree&lt;/strong&gt;：节点内的数据是连续存放的，空间利用率高，局部性好，CPU 缓存和磁盘预读都能发挥作用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;红黑树&lt;/strong&gt;：每个节点单独分配，指针开销大，内存局部性差，缓存和预读效果差。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 实际应用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;B+Tree&lt;/strong&gt;：被广泛用于数据库索引（MySQL、PostgreSQL、Oracle）和文件系统（NTFS、HFS+ 等），因为它能有效减少磁盘 I/O 并支持高效范围查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;红黑树&lt;/strong&gt;：常用于内存场景，比如 C++ STL 的 &lt;code&gt;std::map&lt;/code&gt;、&lt;code&gt;std::set&lt;/code&gt;，在内存里做有序字典或集合很合适。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;130. 在 STL 里，内存池是怎么实现的，有怎样的结构？&lt;/h2&gt;
&lt;p&gt;在 SGI STL 中，内存分配器分了两层。第一层直接调用 &lt;code&gt;malloc/free&lt;/code&gt;，主要处理大块内存（一般大于 128 字节），这种情况下它不再做额外优化。第二层才是所谓的 &lt;strong&gt;内存池机制&lt;/strong&gt;，专门用来管理小对象（≤128 字节）。&lt;/p&gt;
&lt;p&gt;这层内存池内部维护了 &lt;strong&gt;16 个自由链表（free lists）&lt;/strong&gt;，每个链表对应固定大小的内存块，大小从 8 字节到 128 字节，按 8 字节递增。比如需要分配 20 字节时，分配器会把它“上调”到 24 字节，然后从 24 字节对应的自由链表中取一个小块。如果链表里有可用内存，就直接返回；如果为空，就调用一个名为 &lt;code&gt;refill&lt;/code&gt; 的函数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;refill&lt;/code&gt; 的作用是一次性从堆里申请一大块内存（比如 4KB），把它切成若干个小块挂到自由链表上，供后续使用。这样下一次再分配同样大小的小对象时，就可以直接从链表里取，而不必再调用 &lt;code&gt;malloc&lt;/code&gt;。这种批量获取、切分、回收的方式大大减少了系统调用次数。&lt;/p&gt;
&lt;p&gt;在结构设计上，每个小块内存的前几个字节都会存储一个指针，指向同类下一个空闲小块，从而形成单链表。释放对象时，并不会真的把内存交还给系统，而是把小块重新挂回对应的链表，等待下次复用。这样回收操作也只需要修改指针，几乎是 O(1) 的操作。&lt;/p&gt;
&lt;p&gt;除了自由链表，SGI STL 的内存池还维护一个全局的“内存起始指针”和“内存结束指针”，用来追踪尚未切分的小块。当某个自由链表需要补充小块时，就从这段区域里分配，如果不够再向系统申请更大的内存块。&lt;/p&gt;
&lt;p&gt;对比现代 C++ 标准库的 &lt;code&gt;std::allocator&lt;/code&gt;，它通常只是对 &lt;code&gt;::operator new&lt;/code&gt; 的简单封装，不会自带复杂的池化机制。但由于 STL 容器都通过分配器抽象来管理内存，所以你完全可以自己实现类似 SGI STL 的 allocator，然后把它作为模板参数传给容器，从而让 STL 容器使用内存池。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;其结构和工作原理如下：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;核心结构：自由链表&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;内存池维护一个自由链表数组（例如16个），每个链表管理一种特定大小（8B, 16B, 24B...通常为8的倍数）的内存块。&lt;/li&gt;
&lt;li&gt;每个空闲内存块的开头是一个指向下一个空闲块的指针（&lt;code&gt;union _Obj { _Obj* _M_free_list_link; ... }&lt;/code&gt;），将这些块串成一个链表。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配过程（allocate）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;计算所需内存大小，对齐后找到对应的自由链表。&lt;/li&gt;
&lt;li&gt;如果链表不为空，直接从链表头取出一块内存返回给用户。这只需要修改几个指针，速度极快。&lt;/li&gt;
&lt;li&gt;如果链表为空，则调用 &lt;code&gt;_S_refill&lt;/code&gt; 函数向内存池“申请批发”。
&lt;ul&gt;
&lt;li&gt;内存池会一次性分配一大块内存（例如20个对象的大小）。&lt;/li&gt;
&lt;li&gt;将这大块内存切割成一个个小块，并用指针串起来，形成新的自由链表。&lt;/li&gt;
&lt;li&gt;然后从这条新链表中取出一块返回给用户。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放过程（deallocate）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;收到用户释放的指针，根据其大小找到对应的自由链表。&lt;/li&gt;
&lt;li&gt;直接将这块内存插回对应链表的头部（修改指针即可）。&lt;strong&gt;它不会立即归还给操作系统&lt;/strong&gt;，而是留在链表中以备下次分配。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;131. 执行 &lt;code&gt;vector&amp;#x3C;int&gt; v(4, 100)&lt;/code&gt; 会发生什么，在栈上还是堆上分配？&lt;/h2&gt;
&lt;p&gt;当你写下 &lt;code&gt;vector&amp;#x3C;int&gt; v(4, 100);&lt;/code&gt; 时，首先会调用 &lt;code&gt;std::vector&lt;/code&gt; 的构造函数，它接收两个参数 &lt;code&gt;(size_type n, const T&amp;#x26; value)&lt;/code&gt;，表示创建一个长度为 &lt;code&gt;n&lt;/code&gt; 的向量并将所有元素初始化为 &lt;code&gt;value&lt;/code&gt;。在这个例子里，&lt;code&gt;n = 4&lt;/code&gt;，&lt;code&gt;value = 100&lt;/code&gt;，所以最终生成的是一个长度为 4 的向量，内容为 &lt;code&gt;[100, 100, 100, 100]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;需要注意的是，&lt;code&gt;v&lt;/code&gt; 本身是一个局部变量，它作为一个对象存放在栈上，其中保存着三个核心信息：指向存储区的指针、当前元素个数 &lt;code&gt;size&lt;/code&gt; 和当前容量 &lt;code&gt;capacity&lt;/code&gt;。而那 4 个 &lt;code&gt;int&lt;/code&gt; 元素并不是直接放在栈里的，而是由 &lt;code&gt;vector&lt;/code&gt; 内部通过动态内存分配（通常使用 &lt;code&gt;new&lt;/code&gt;）在堆上申请一块连续的内存空间来存放的。这样做的好处是容器可以根据需要扩容，而不受栈空间大小的限制。&lt;/p&gt;
&lt;p&gt;因此，整体过程可以总结为：&lt;code&gt;vector&amp;#x3C;int&gt; v(4, 100);&lt;/code&gt; 会生成一个长度为 4、所有元素值都为 100 的向量，其中容器对象本身在栈上，而实际存储元素的空间在堆上。&lt;/p&gt;
&lt;h2&gt;132. 那如果是 &lt;code&gt;new vector&amp;#x3C;int&gt;(4,100)&lt;/code&gt; 呢&lt;/h2&gt;
&lt;p&gt;当你写成 &lt;code&gt;new vector&amp;#x3C;int&gt;(4, 100)&lt;/code&gt; 时，和直接 &lt;code&gt;vector&amp;#x3C;int&gt; v(4, 100)&lt;/code&gt; 的区别主要在于 &lt;strong&gt;容器对象本身的位置&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;vector&amp;#x3C;int&gt; v(4, 100);&lt;/code&gt; 这种形式里，&lt;code&gt;v&lt;/code&gt; 是一个局部变量，它作为对象存放在栈上，里面包含了三个核心成员：指向堆上元素存储区的指针、&lt;code&gt;size&lt;/code&gt;、&lt;code&gt;capacity&lt;/code&gt;。而那 4 个 &lt;code&gt;int&lt;/code&gt; 元素依然是通过 &lt;code&gt;new&lt;/code&gt; 在堆上分配的一块连续内存保存的。&lt;/p&gt;
&lt;p&gt;而在 &lt;code&gt;new vector&amp;#x3C;int&gt;(4, 100)&lt;/code&gt; 这种形式里，情况就变成了两层堆分配：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;new vector&amp;#x3C;int&gt;&lt;/code&gt; 本身会在堆上生成一个 &lt;code&gt;vector&lt;/code&gt; 对象，这个对象里包含指针、&lt;code&gt;size&lt;/code&gt; 和 &lt;code&gt;capacity&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在构造函数执行过程中，&lt;code&gt;vector&lt;/code&gt; 内部还会在堆上再分配一块内存，存放那 4 个 &lt;code&gt;int&lt;/code&gt; 元素，并全部初始化为 &lt;code&gt;100&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，可以总结为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vector&amp;#x3C;int&gt; v(4, 100);&lt;/code&gt; → &lt;strong&gt;对象在栈上，元素在堆上&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new vector&amp;#x3C;int&gt;(4, 100)&lt;/code&gt; → &lt;strong&gt;对象本身在堆上，元素依然在堆上&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 &lt;code&gt;new&lt;/code&gt; 的版本返回的是一个指针（类型为 &lt;code&gt;vector&amp;#x3C;int&gt;*&lt;/code&gt;），所以一般需要用 &lt;code&gt;delete&lt;/code&gt; 手动释放，否则会造成内存泄漏；而直接定义在栈上的 &lt;code&gt;vector&lt;/code&gt;，在作用域结束时会自动调用析构函数，释放堆上的元素存储区，更加安全。&lt;/p&gt;
&lt;h2&gt;133. 如何拿到类中私有成员变量的值？&lt;/h2&gt;
&lt;p&gt;在语法层面，私有成员就是不能直接访问的，这是 C++ 的语言设计。你如果真要拿到值，只能靠 &lt;strong&gt;友元&lt;/strong&gt;（正规的方式）或者通过 &lt;strong&gt;指针偏移、调试器、反射库&lt;/strong&gt; 之类的手段绕过，但这些都破坏了封装性，容易出问题。&lt;/p&gt;
&lt;h3&gt;正规途径&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;提供访问接口&lt;/strong&gt;：最推荐的做法是类内部提供 &lt;code&gt;getter&lt;/code&gt; 方法，或者通过友元（&lt;code&gt;friend&lt;/code&gt;）函数/类来获取。这样遵循语法规则，保证代码可维护性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;友元机制&lt;/strong&gt;：可以显式指定某个函数或类为友元，从而访问私有成员。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;非正规途径（绕过语法限制）&lt;/h3&gt;
&lt;p&gt;这些方法属于“黑科技”，更多是为了学习/调试用，工程里一般不建议：&lt;/p&gt;
&lt;h4&gt;通过指针偏移（内存布局）&lt;/h4&gt;
&lt;p&gt;类对象的内存里，私有成员和公有成员是按编译器布局存放的，访问权限只存在于编译阶段。拿到对象地址后，可以通过 &lt;code&gt;reinterpret_cast&lt;/code&gt; 或指针算偏移，直接读到对应位置的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Test {
private:
    int a = 42;
};

int main() {
    Test t;
    int* p = (int*)&amp;#x26;t;   // 直接把对象地址转成 int* 
    std::cout &amp;#x3C;&amp;#x3C; *p &amp;#x3C;&amp;#x3C; std::endl;  // 打印出私有成员 a
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种做法利用了内存布局，但属于未定义行为，不可移植。&lt;/p&gt;
&lt;h4&gt;宏/模板技巧&lt;/h4&gt;
&lt;p&gt;有些人会用模板 + 偏特化或者宏展开来“欺骗”编译器访问私有成员，不过实现复杂，常用于 C++ 奇技淫巧展示。&lt;/p&gt;
&lt;h4&gt;gdb&lt;/h4&gt;
&lt;p&gt;在调试环境下，可以通过 gdb/lldb 等调试器直接读取对象内存，或者用 Boost.PFR（Precise Function Reflection）等库在编译期推断对象的字段布局。&lt;/p&gt;
&lt;h2&gt;134. 有一个二维数组里面都有值，想要给每个数都加 100，行遍历和列遍历有什么区别？&lt;/h2&gt;
&lt;p&gt;在一个二维数组里，如果你要给每个元素都加 100，从结果上看，无论是按行遍历还是按列遍历，数组中的所有数都会变大 100，结果完全相同。但这两种遍历方式在底层运行效率上差别很大。&lt;/p&gt;
&lt;p&gt;二维数组在 C/C++ 中是 &lt;strong&gt;行优先存储&lt;/strong&gt;（row-major order）的，也就是说，一整行的数据在内存中是连续存放的，不同行之间是隔开的。行遍历时，访问顺序与内存布局一致，CPU 每次加载的缓存行和虚拟内存页都会被充分利用，缓存命中率高，虚拟内存的局部性原理得以发挥，性能较好。而列遍历则不同，相邻元素在内存中相隔一个整行的大小，访问时会“跳着走”。这导致 CPU 每次加载的缓存行里只有极少数数据被使用，缓存利用率低，&lt;strong&gt;大数组时还会频繁触发新的虚拟页调入和缓存替换&lt;/strong&gt;，带来额外的开销。&lt;/p&gt;
&lt;p&gt;所以，从虚拟内存和缓存的角度看，&lt;strong&gt;行遍历符合空间局部性，效率高；列遍历破坏局部性，效率低&lt;/strong&gt;。在数据量小的时候差别可能不明显，但在大规模数组或矩阵计算里，性能差距会非常显著，这也是科学计算和数据库引擎里都特别强调数据访问模式与存储布局匹配的原因。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;135. 关于静态多态和动态多态原理 (Leelham)&lt;/h2&gt;
&lt;h3&gt;静态多态&lt;/h3&gt;
&lt;p&gt;静态多态都是通过 name mangling 实现的。name mangling 是一种编译器技术，用于为程序中的变量和函数生成唯一的内部名称。mangling 规则具体看编译器。&lt;/p&gt;
&lt;p&gt;具体包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数重载：指在同一个作用域内，名称相同的函数参数类型或参数个数不同，从而可以根据调用时提供的实参来确定实际调用的函数
&lt;blockquote&gt;
&lt;p&gt;函数重载实现参数多样性。函数返回值多样性可以用 variant（可以再传一个模板参数，内部基于模板参数对 variant 进行转化）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;隐藏：指在子类中定义父类同名同参数函数&lt;/li&gt;
&lt;li&gt;模板：指编译时根据传递的模板参数不同，生成不同的代码&lt;/li&gt;
&lt;li&gt;CRTP（奇异模板递归）：通过继承自己（在子类中使用父类模板），允许在编译时进行多态操作，避免动态分发查虚表的开销。&lt;code&gt;std::enable_shared_from_this&lt;/code&gt; 即通过 CRTP 实现。
&lt;blockquote&gt;
&lt;p&gt;玄学：就像在心理学中，自我认知（Self-awareness）是理解个体行为的关键一样，CRTP 通过使基类能够“意识”到其派生类的类型，从而在编译时实现多态性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;interface 内部可以配合 &lt;code&gt;if constexpr&lt;/code&gt; 和 &lt;code&gt;std::is_same_v&lt;/code&gt; 对某些类型做特殊处理&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// CRTP 基类
template&amp;#x3C;typename Derived&gt;
class Base {
public:
    void interface() {
        // 静态多态：在编译时调用Derived的implementation
        static_cast&amp;#x3C;Derived*&gt;(this)-&gt;implementation();
    }
    // 一些公共的接口和实现...
};
// CRTP 派生类
class Derived : public Base&amp;#x3C;Derived&gt; {
public:
    void implementation() {
        // 特定于Derived的实现...
    }
};
int main() {
    Derived d;
    d.interface(); // 调用Derived的implementation
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;动态多态&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;动态多态原理：虚函数实现，底层通过虚函数表实现。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于含有虚函数的类，编译器会在编译阶段创建一个虚函数表 vftable（按照虚函数的声明顺序保存虚函数的地址），并在创建类对象时插入一个虚函数表指针 vfptr，该指向 vftable 的首地址（也就是第一个虚函数的地址）。&lt;/li&gt;
&lt;li&gt;虚函数表是静态的，供该类的所有对象共享，在编译期确定，存放在全局（静态）变量区的 .rodata 段。&lt;/li&gt;
&lt;li&gt;虚函数表指针是动态的，该类的每个对象都有一个，在运行期确定，存放位置和对象的存放位置相同（堆区/栈区）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;继承时，子类会深拷贝一份父类的虚函数表，如果子类重写了虚函数，那么子类的虚函数表中对应的虚函数地址会被覆盖为重写的虚函数地址。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为什么多态函数传参是传指针或者引用，按值传大概率不行？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按值传递会导致切片问题，对象的派生特征都会被截切掉，导致派生类的信息丢失，从而无法实现多态；&lt;/li&gt;
&lt;li&gt;按值传递需要拷贝副本，性能开销大；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造函数和析构函数可以定义为虚函数吗？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构造不可以：在继承中，先构造基类再构造子类。如果构造函数是虚函数，则需要通过虚函数表调用，但是此时对象还没有实例化，没有虚函数表指针 vfptr，无法访问虚函数表&lt;/li&gt;
&lt;li&gt;析构可以：父类不是虚析构，使用父类指针指向子类，就只会调用父类的析构函数，导致子类动态分配的资源无法释放（如果子类数据成员都是基本类型，不使用虚析构函数不会内存泄漏）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;哪些函数不能是虚函数？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构造函数、静态成员函数（编译期确定，不能动态绑定）、友元函数（不能继承）、内联函数（编译器展开，不能动态绑定）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;菱形继承&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;virtual 关键字虚继承，保证派生类只保留一份间接基类的成员。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继承多个类，会导致具有多个 vptr&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;136. 构造函数和析构函数可以定义为虚函数吗？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;构造不可以：在继承中，先构造基类再构造子类。如果构造函数是虚函数，则需要通过虚函数表调用，但是此时对象还没有实例化，没有虚函数表指针 vfptr，无法访问虚函数表&lt;/li&gt;
&lt;li&gt;析构可以：父类不是虚析构，使用父类指针指向子类，就只会调用父类的析构函数，导致子类动态分配的资源无法释放（如果子类数据成员都是基本类型，不使用虚析构函数不会内存泄漏）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;137. 哪些函数不能是虚函数？&lt;/h2&gt;
&lt;p&gt;构造函数、静态成员函数（编译期确定，不能动态绑定）、友元函数（不能继承）、内联函数（编译器展开，不能动态绑定）&lt;/p&gt;
&lt;h2&gt;138. 关于构造函数&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;拷贝构造/赋值函数&lt;/p&gt;
&lt;p&gt;默认构造函数、有参构造函数、拷贝构造函数（传参必须是引用方式，否则会无穷递归，因为按值传递时候会继续调用拷贝构造函数）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移动构造/赋值函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;移动构造函数（&lt;strong&gt;必须要加 &lt;code&gt;noexcept&lt;/code&gt;，否则如 vector 之类的在扩容时不会调用移动构造，只会调用拷贝构造&lt;/strong&gt;）。&lt;/li&gt;
&lt;li&gt;用于将资源所有权从一个对象转移到另一个对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;深拷贝与浅拷贝&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浅拷贝：仅复制对象的基本数据类型和指针成员的值，但是不会赋值指针指向的资源，可能导致两个对象共享相同资源；&lt;/li&gt;
&lt;li&gt;深拷贝：不仅拷贝对象的基本数据类型和指针成员的值，还会拷贝指针所指向的资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右值引用与完美转发&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;值分为左值（可以取地址，有名字）和右值（不可以取地址，无名字）。&lt;code&gt;std::move&lt;/code&gt; &lt;strong&gt;用于将左值引用强制转变为右值引用&lt;/strong&gt;（而非所有权转移）。&lt;/li&gt;
&lt;li&gt;右值赋值给右值引用变量就有了名字，变为左值，可以被左值引用捕获。&lt;code&gt;std::forward&lt;/code&gt; 用于保证函数参数的左值或右值特性被保留，&lt;strong&gt;允许函数将参数以几乎完全相同的形式转发给其他函数&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;139. 关于关键字&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;修饰变量：只读变量，不能修改&lt;/li&gt;
&lt;li&gt;修饰函数：修饰函数的返回值，表示函数的返回值只读，不能被修改；&lt;/li&gt;
&lt;li&gt;修饰指针：常量指针（int* const）、指向常量的指针（const int*）&lt;/li&gt;
&lt;li&gt;修饰成员函数：不能修改任何成员变量，因此也只能调用常成员函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;violate&lt;/code&gt;：指示变量可能被外部因素更改，防止编译器优化。常见场景：
&lt;ul&gt;
&lt;li&gt;多任务环境下各任务间共享的标志应该加 volatile；&lt;/li&gt;
&lt;li&gt;存储器映射的硬件寄存器通常也要加 volatile 说明，因为每次对它的读写都可能有不同意义&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;define&lt;/code&gt; 和 &lt;code&gt;inline&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;define宏定义只在预处理阶段起作用，仅仅是简单的文本替换，没有类型检查；&lt;/li&gt;
&lt;li&gt;inline 在编译阶段进行替换，有类型检查&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内联的好处
&lt;ul&gt;
&lt;li&gt;减少函数调用开销，函数调用涉及压栈、传参、弹栈，内联展开避免了这些开销，直接在调用点插入函数体；&lt;/li&gt;
&lt;li&gt;减少跳转和缓存不命中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static&lt;/code&gt;
&lt;ol&gt;
&lt;li&gt;修饰全局变量
&lt;ol&gt;
&lt;li&gt;将变量作用域限制在当前文件中，其他文件无法访问；&lt;/li&gt;
&lt;li&gt;static 变量可以定义在头文件中，并不破坏单一定义原则&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;修饰局部变量：静态局部变量在函数调用结束后并不会被销毁，而是保留其值，并在下一次调用该函数时继续使用；&lt;/li&gt;
&lt;li&gt;修饰成员变量：成为静态成员变量，属于整个类，在所有实例之间共享；&lt;/li&gt;
&lt;li&gt;修饰成员函数：静态成员函数，可以直接通过类名来访问，而无需创建对象，没有 this 指针。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extern&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;声明外部变量或函数，使得不同的源文件共享相同的变量或者函数&lt;/li&gt;
&lt;li&gt;extern “C”：可以让 C++ 来调用 C 语言当中的变量或函数（因为 C++ 函数会进行 name mangling，C 的不会，所以不能直接调用）。不使用该关键字，直接调用 C 语言中的变量或函数会发生链接错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类型转化（&lt;code&gt;static_cast&lt;/code&gt; 用于编译期安全的类型转换（如基本类型转换、父类子类指针转换）；&lt;code&gt;dynamic_cast&lt;/code&gt; 用于多态类型的向下转换，在运行时检查类型安全性，失败返回空指针；&lt;code&gt;const_cast&lt;/code&gt; 用于移除或添加 const/volatile 属性；&lt;code&gt;reinterpret_cast&lt;/code&gt; 用于低层次的强制类型转换（如指针转整数），不进行类型检查，需谨慎使用）
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;static_cast&lt;/code&gt;：基本数据类型间转换，如 int 转为 double，也可以用于基类和派生类间的上行转换（派生类指针—&gt;基类指针）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dynamic_cast&lt;/code&gt;：主要用于基类和派生类间的安全类型转换，运行时执行安全检查，要求基类必须有虚函数。流程：
&lt;ul&gt;
&lt;li&gt;首先，dynamic_cast 通过查询对象的 vptr 来获取其 RTTI；&lt;/li&gt;
&lt;li&gt;然后，dynamic_cast 比较请求的目标类型与从 RTTI 获得的实际类型。如果目标类型是实际类型或其基类，则转换成功；&lt;/li&gt;
&lt;li&gt;如果目标类型是派生类，dynamic_cast 会检查类层次结构，以确定转换是否合法；&lt;/li&gt;
&lt;li&gt;如果在类层次结构中找到了目标类型，则转换成功；否则，转换失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const_cast&lt;/code&gt;：去 const 属性，常量指针转换为非常量指针，并且仍然指向原来的对象。不可以用在 const 成员方法里复用非 const 成员方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reinterpreter_cast&lt;/code&gt;：仅仅重新解释类型，但没有进行二进制的转换&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;struct&lt;/code&gt; 和 &lt;code&gt;class&lt;/code&gt; 区别：
&lt;ul&gt;
&lt;li&gt;struct：结构体中的成员和继承默认都是 public 属性的。&lt;/li&gt;
&lt;li&gt;class：结构体中的成员和继承默认都是 private 属性的，且可以用于定义模板参数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;noexcept&lt;/code&gt;：表示函数内部不会抛出异常，有助于简化调用该函数的代码，而且编译器确认函数不会抛出异常，它就能执行某些特殊的优化操作。&lt;/li&gt;
&lt;li&gt;new/delete 和 malloc/free 区别
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new/delete&lt;/code&gt; 是 C++ 风格，分配/回收空间同时会调用构造/析构，而 &lt;code&gt;malloc/free&lt;/code&gt; 仅管理原始内存，不会自动初始化对象&lt;/li&gt;
&lt;li&gt;malloc 需要传入分配的大小返回一个 void 指针。&lt;strong&gt;大多数中 malloc 分配的内存前加一个头部，用于存储分配大小以进行 free&lt;/strong&gt;；new 传入类型信息会自动计算构造对象的大小，delete 不需要额外存储长度，但 delete[] 可能会在内存前存储数组大小。&lt;/li&gt;
&lt;li&gt;内存不足时，malloc 返回空指针；默认版本 new 会抛出异常 std::bad_alloc，或者使用 new(std::nothrow) 不抛出异常&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;new 可以 placement new 就地构造（emplace 相关方法实现原理）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutable&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;修饰类的成员变量，表示可以在 const 成员函数中修改&lt;/li&gt;
&lt;li&gt;修饰 lambda 函数，表示可以修改值捕获的成员&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto&lt;/code&gt; 与 &lt;code&gt;decltype&lt;/code&gt;（C++ 11）：编译器在编译期自动推导表达式类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;thread_local&lt;/code&gt;：指示对象拥有线程存储期，指示对象的存储在线程开始时分配，在线程结束时析构
&lt;ul&gt;
&lt;li&gt;可以与 static 和 extern 用于指定内部或外部链接（除了静态数据成员始终拥有外部链接），但附加的 static 不影响存储期&lt;/li&gt;
&lt;li&gt;只用于于命名空间作用域的对象、块作用域的对象、静态数据成员&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;补充 push_back 和 emplace_back&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// vector emplace_back 和 push_back 实现原理
template&amp;#x3C;typename T&gt;
class Vector {
private:
    T* data_;
    size_t size_;
    size_t capacity_;

public:
    template&amp;#x3C;typename... Args&gt;
    void emplace_back(Args&amp;#x26;&amp;#x26;... args) {
        if (size_ &gt;= capacity_) {
            reserve(capacity_ == 0 ? 1 : capacity_ * 2);
        }
        
        // 使用 placement new 在末尾直接构造对象
        new (data_ + size_) T(std::forward&amp;#x3C;Args&gt;(args)...);
        ++size_;
    }
    
    void push_back(const T&amp;#x26; value) {
        if (size_ &gt;= capacity_) {
            reserve(capacity_ == 0 ? 1 : capacity_ * 2);
        }
        
        // 需要拷贝构造（可能产生不必要的拷贝）
        data_[size_] = value;  // 或者 new (data_ + size_) T(value);
        ++size_;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;140. 关于内存管理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;内存空间（从低到高）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;代码段：存放程序的二进制机器码&lt;/li&gt;
&lt;li&gt;数据段 data：存放初始化的全局变量、静态变量和常量&lt;/li&gt;
&lt;li&gt;bss 区：存放未初始化的全局变量和静态变量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mmap 区&lt;/strong&gt;：存放动态库（共享库）、匿名映射（malloc 大块内存）和文件映射（mmap 文件）；动态内存管理&lt;/li&gt;
&lt;li&gt;堆区：&lt;strong&gt;由低向高增长&lt;/strong&gt;。用于动态分配内存（malloc/free，new/delete），如果没有及时释放内存会造成内存泄漏。因为存在内存管理、地址映射等复杂操作，故效率较低。此外还会产生内存碎片影响效率。但空间大&lt;/li&gt;
&lt;li&gt;栈区：&lt;strong&gt;由高向低增长&lt;/strong&gt;。存放局部变量、函数参数值、形参等。由编译器分配和释放，栈区只需要改变栈指针的位置，因此效率高。但栈区空间小&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;各种变量存放位置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接声明的变量、函数实参存储在栈区；&lt;/li&gt;
&lt;li&gt;new 创建的对象，较小的对象存放在堆区，较大的对象存放在共享内存区；&lt;/li&gt;
&lt;li&gt;常量和静态变量存放在静态存储区中的非代码区；&lt;/li&gt;
&lt;li&gt;所有函数存放在静态存储区中的代码区；&lt;/li&gt;
&lt;li&gt;字符常量也存放在代码区。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;allocator 与 placement_new：原来，new = 内存分配 + 调用构造函数，借助这两个工具实现内存分配与对象构造分离。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;allocator 可以实现自定义内存分配回收的逻辑，分配回收不会调用构造/析构
&lt;blockquote&gt;
&lt;p&gt;http://www.cnblogs.com/wpcockroach/archive/2012/05/10/2493564.html&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;stl 容器支持传入自定义的 allocator。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;placement_new 用于在指定的内存地址上构造对象，需要保证可用空间 ≥ 对象实际大小
&lt;ul&gt;
&lt;li&gt;可用于实现标准库的 emplace 方法，原地构造对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内存泄漏：申请了一块内存空间，使用完毕后没有释放掉。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设计上：基于 RAII，借助智能指针避免资源泄露。&lt;/li&gt;
&lt;li&gt;检测方法：
&lt;ul&gt;
&lt;li&gt;使用 GDB，执行某个函数前后，call malloc_stats()，对比 in use bytes 大小是否变化&lt;/li&gt;
&lt;li&gt;使用 valgrind，&lt;code&gt;valgrind --leak-check=yes program arg1 arg2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改代码，在 new/delete 处加一个计数器检测&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何区分 32 位机器和 64 位机器？用 &lt;code&gt;sizeof(void*)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;strong&gt;32 位系统&lt;/strong&gt; 中，指针是 &lt;strong&gt;4 字节&lt;/strong&gt;，可以寻址 &lt;strong&gt;4GB&lt;/strong&gt; 内存。&lt;/li&gt;
&lt;li&gt;在 &lt;strong&gt;64 位系统&lt;/strong&gt; 中，指针是 &lt;strong&gt;8 字节&lt;/strong&gt;，可以寻址 &lt;strong&gt;16GB&lt;/strong&gt; 内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;141. 关于内存对齐&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对齐系数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;绝对对齐系数：每种类型数据在内存中的首地址必须是绝对对齐系数内存系数的整数倍。在默认情况下，绝对对齐系数等于该类型数据的默认对齐系数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;相对对齐系数&lt;/strong&gt;：结构体内的数据相对于结构体起始地址的偏移量，单位为字节，结构体成员在结构体中的偏移量必须是该成员相对对齐系数的整数倍。在默认情况下，相对对齐系数等于该类型数据的默认对齐系数。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认对齐系数：绝对对齐系数和相对对齐系数等于默认对齐系数（数据的固有属性）。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对于基本数据类型，默认对齐系数等于该数据类型占用字节大小。在内存中的地址必须是其自身大小（以字节为单位）的整数倍；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于结构体类型：(1) 结构体的起始地址必须是结构体成员中占用空间最大的基本数据类型的整数倍，且结构体大小也必须是其整数倍；(2) 结构体内的成员，在结构体中的偏移量必须是该成员相对对齐系数的整数倍。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于默认对齐的原因，&lt;code&gt;struct {char a; int b; }&lt;/code&gt; 的 size 是 8 字节&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显式内存对齐&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;#pragma pack(N)（编译器支持）：N 需要为 2 的正数
&lt;ul&gt;
&lt;li&gt;当&lt;code&gt;N&lt;/code&gt;大于该结构体的默认对齐系数时候，结构体绝对对齐系数被设置为&lt;code&gt;N&lt;/code&gt;，当&lt;code&gt;N&lt;/code&gt;小于该结构体的默认对齐系数时候，结构体绝对对齐系数维持为默认对齐系数。&lt;/li&gt;
&lt;li&gt;当&lt;code&gt;N&lt;/code&gt;小于该结构体的默认对齐系数时候，结构体内所有成员的相对对齐系数被设置为&lt;code&gt;N&lt;/code&gt;，当&lt;code&gt;N&lt;/code&gt;大于该结构体的默认对齐系数时候，结构体内成员的相对对齐系数维持为默认对齐系数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;alignas(N)：仅指定结构体的绝对对齐系数（规则同上），不能指定相对对齐系数&lt;/li&gt;
&lt;li&gt;alignof(T)：用于获取指定类型的对齐系数&lt;/li&gt;
&lt;li&gt;std::aligned_storage：创建固定大小和满足对齐要求的未初始化内存&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用途&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;提高性能：字是 CPU 在一次内存读写操作中能处理的最大数据块。变量占据内存大小等于字长，如果其首地址在字长的整数倍处，CPU只需要读内存一次；如果其首地址不在字长整数倍处，CPU则需要读地址两次，影响程序性能。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;避免伪共享问题：多个 CPU 同时对同一个缓存行的数据进行修改，导致 CPU cache 的数据不一致，缓存失效，需要频繁从内存中重新读取数据&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么伪共享只发生在多线程的场景，而多进程的场景不会有问题？这是因为 linux 虚拟内存的特性，各个进程的虚拟地址空间是相互隔离的，也就是说在数据不进行缓存行对齐的情况下，CPU 执行进程 1 时加载的一个缓存行的数据，只会属于进程 1，而不会存在一部分是进程 1、另外一部分是进程 2。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SIMD 指令需要内存对齐&lt;/p&gt;
&lt;p&gt;x86 平台下的 SSE （Streaming SIMD Extensions）指令为例来说明内存对齐在 SIMD 中的应用。该架构使用128位的向量寄存器，数据总线长度位128位，每次能读取16字节。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;alignas(16) float A[4];
alignas(16) float B[4];
alignas(16) float C[4];

for (int i = 0; i &amp;#x3C; 4; i++) {
    C[i] = A[i] + B[i];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于浮点数默认是按照 4 字节对齐的，数组的首地址是4的倍数，因此如果数组没有进行显式对齐，那么每次计算4个浮点数可能就需要进行两次内存读写。然而，由于数组已经按照16字节对齐，每次计算4个浮点数就只需要进行一次内存读写。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;direct I/O 需要内存对齐&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;142. 关于异常&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;异常处理：当异常发生时，会进行栈展开 stack unwinding：
&lt;ul&gt;
&lt;li&gt;将暂停当前函数的执行，开始查找匹配的 catch 子句：首先检查 throw 本身是否在 try 块内部，如果是，检查与该 try 相关的 catch 子句，看是否有匹配的 catch。如果不能处理，就退出当前函数，并且释放当前函数的局部对象，继续到上层的调用函数中查找，直到找到一个可以处理该异常的 catch 。当处理该异常的 catch 结束之后，紧接着该 catch 之后的点继续执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;未捕获的异常将终止程序&lt;/strong&gt;：如果找不到匹配的 catch，程序就会调用库函数 std::terminate&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;异常安全：&lt;strong&gt;stack unwinding 只对栈上的变量进行析构，堆上的动态分配的 new 不会自动析构，所以可能内存泄漏&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;不希望抛出异常的函数
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;析构函数（会导致异常不安全）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;析构函数往往不仅仅释放一个资源。当前一个资源释放时抛出异常，此时跳过异常点后面的代码，使得后一块资源没有释放，造成内存泄漏。&lt;/li&gt;
&lt;li&gt;vector 析构所有元素时，&lt;strong&gt;那么当有一个元素抛出异常，此时 catch 之后的处理显然是继续销毁剩下的元素，但是假设运气很不好，又有一个元素抛出异常&lt;/strong&gt;，c++此时无能为力，要么结束执行，要么发生不预期的行为&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移动赋值函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 STL 标准库中很多容器在 resize 时都会通过&lt;code&gt;std::move_if_noexcept&lt;/code&gt;模板来判断元素是否提供了 noexcept 的移动赋值，如果提供那么 move，否则调用拷贝赋值函数。所以不抛出异常的移动赋值函数效率会更高。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;swap 函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据 copy and swap 惯用法，swap 是移动赋值基时。swap 不抛异常，移动赋值才不抛异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;析构函数发生错误（c++11 之后，默认会把析构函数看成&lt;code&gt;noexcept(true)&lt;/code&gt;，这意味着如果析构函数抛出异常，直接&lt;code&gt;std::terminal&lt;/code&gt;）：
&lt;ol&gt;
&lt;li&gt;try catch 吞下异常，除记录日志外，不做任何事&lt;/li&gt;
&lt;li&gt;直接终止程序&lt;/li&gt;
&lt;li&gt;释放会失败的资源，释放放在非析构函数中，由程序手动操作&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;copy - and -swap
&lt;ul&gt;
&lt;li&gt;好处：强异常安全、自复制安全、代码复用
&lt;ul&gt;
&lt;li&gt;强异常安全：如果 new 失败，即在值传递时，创建新的副本就会发生异常，此时 this*被没有被 delete。不会出现 this * 被 delelte，而 new 失败导致对象不完整的情况&lt;/li&gt;
&lt;li&gt;自复制安全：因为值传递是开辟新的副本，所以赋值没有问题，至于自拷贝的效率问题，一般不怎么出现自拷贝，所以在其他优点下，这点效率小瑕疵就忽略不计&lt;/li&gt;
&lt;li&gt;代码复用：拷贝赋值运算、移动赋值运算符、swap 函数共用一份代码。对于赋值函数只需写值传递的&lt;code&gt;operator=(T)&lt;/code&gt;，无需同时写&lt;code&gt;operator=(const T&amp;#x26;)&lt;/code&gt;和&lt;code&gt;operator=(T&amp;#x26;&amp;#x26;)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;swap 实现：分为 &lt;strong&gt;member&lt;/strong&gt;和&lt;strong&gt;non-memeber。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;member：由于 non-member 不能访问类成员，所以我们先实现 member，然后 non-member 调用
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;friend void swap(dumb_array&amp;#x26; first, dumb_array&amp;#x26; second) noexcept {
   using std::swap;

   swap(first.mSize, second.mSize);
   swap(first.mArray, second.mArray);
   // std::swap这里是非常高效的，只是换指针
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;non-member：非模板类可以直接全特化 std::swap；模板类由于 std::swap 不支持偏特化，需实现第三方 swap。在外界调用 swap 时，拒绝直接写&lt;code&gt;std::swap&lt;/code&gt;，而应该写&lt;code&gt;using std::swap; swap(item1,item2);&lt;/code&gt;，得益于 ADL（argument dependent lookup）机制，总是能匹配正确的 swap。
通过 using，把 std::swap，还有参数的类型 widget 所在命名空间下的 swap 都纳入函数候选集，在这个候选集中找到最优匹配的函数：class template 类匹配到 WidgetStuff 命名空间的 swap，而其他会匹配到 &lt;code&gt;std::swap&lt;/code&gt;。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; int main(){
     Widget widget1,widget2;
     using std::swap;
     swap(widget1,widget2);
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现：实现参数构造和拷贝构造，用 swap 实现移动构造，用 &lt;code&gt;operator=(T)&lt;/code&gt; 同时实现 &lt;code&gt;operator=(const T&amp;#x26;)&lt;/code&gt;和&lt;code&gt;operator=(T&amp;#x26;&amp;#x26;)&lt;/code&gt;。如果是左值那么会调用拷贝构造函数去初始化函数参数；如果是右值那么会调用移动构造函数去初始化函数参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;143. 关于编译/链接&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;误区 1：区分动态/静态链接 and 动态/静态库。前者是一个过程，包括合并符号、复制代码段、利用重定位表回填外部符号等；后者是可链接的文件，由于静态/动态链接需要，有不同的数据结构。&lt;/p&gt;
&lt;p&gt;误区 2：静态库含义是说链接时，其代码会拷贝到最终可执行文件中，而不是说其存储自身依赖库的代码。其只是在符号表中记录依赖关系，链接静态库时仍然需要链接静态库本身依赖的库。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;符号重定义（Symbol Redefinition）：指的是在同一个作用域内多次定义同名标识符（包括变量、函数、类等）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预处理阶段错误：同名宏&lt;/li&gt;
&lt;li&gt;编译错误
&lt;ul&gt;
&lt;li&gt;名称冲突：同一作用域或不同作用域内出现了相同的符号名定义&lt;/li&gt;
&lt;li&gt;头文件多重包含：当一个源文件包含多个头文件时，某个头文件可能会包含一个已经包含过的头文件，导致同一个函数或变量的定义被重复包含&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链接错误：多个编译单元中出现了相同的符号定义而导致的。这种错误会在链接时被检测到，表示无法解析符号引用，因为有多个定义存在。
目标文件头 ELF Header 内存放有符号信息，把多个目标文件链接到一起的时候，若发现文件头内有同名符号，就会报链接期符号重定义错误。符号表结构如下：&lt;/li&gt;
&lt;li&gt;运行期错误：在动态链接库或共享对象中，函数或变量可以在运行时加载和卸载。如果在两个动态链接库中定义了相同名称的函数或变量，它们可能会导致符号重定义错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解决符号重定义方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命名空间：解决同名宏、名称冲突&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#pragma once&lt;/code&gt; 或者使用 &lt;code&gt;#ifdef&lt;/code&gt; 包裹 &lt;code&gt;#include&lt;/code&gt;：解决头文件多重包含&lt;/li&gt;
&lt;li&gt;解决链接冲突
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static&lt;/code&gt; 关键字：将变量或函数限制在当前文件的作用域内，即使两个目标文件有同名变量，也不会报符号重定义错误。
&lt;blockquote&gt;
&lt;p&gt;在目标文件 ELF 符号表中，被  &lt;code&gt;static&lt;/code&gt;  修饰的变量会被标记为本地符号 (Local Symbols)，链接器检测到两个或者多个目标文件 ELF 符号表内有同名符号，若这个符号是本地符号，则认为它们是不同变量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inline&lt;/code&gt;关键字：将函数或者变量声明为内联类型时，即使两个目标文件有同名变量，也不会报符号重定义错误。
&lt;blockquote&gt;
&lt;p&gt;在 ELF 符号表中，被&lt;code&gt;inline&lt;/code&gt;修饰的函数会被标记为弱符号(Weak Symbol)，链接器检测到两个或者多个目标文件 ELF 符号表内有同名符号，那么选择其中占用空间最大的那一个，比如变量 a 在目标文件 A 中是 int，在目标文件 B 中是 double，那么选择 double 型的符号进行链接。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extern&lt;/code&gt; 关键字：在一个编译单元中使用  &lt;code&gt;extern&lt;/code&gt;关键字来声明一个变量或函数，并在另一个编译单元中定义该变量或函数，在链接时不会发生符号重定义的问题。
&lt;blockquote&gt;
&lt;p&gt;在 ELF 符号表中，被  &lt;code&gt;extern&lt;/code&gt;  声明的符号会被标记为未定义，链接器检测到两个或者多个目标文件 ELF 符号表内有同名符号，若这些符号只有一个已定义，其余都是未定义，则认为这些符号共用这一个已定义的值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt; 关键字：将变量或函数限制在当前文件的作用域内，即使两个目标文件有同名变量，也不会报符号重定义错误。
&lt;blockquote&gt;
&lt;p&gt;在目标文件 ELF 符号表中，默认情况下，被&lt;code&gt;const&lt;/code&gt;修饰的变量会被标记为本地符号 (Local Symbols)，链接器检测到两个或者多个目标文件 ELF 符号表内有同名符号，若这个符号是本地符号，则认为它们是不同变量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编译流程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预处理（&lt;code&gt;gcc -E&lt;/code&gt;）：主要处理 #include 指令，宏替换，条件编译等，生成 .i 文件&lt;/li&gt;
&lt;li&gt;编译（&lt;code&gt;gcc -S&lt;/code&gt;）：对源代码进行语法分析、词法分析，生成汇编代码，产生 .s 文件，&lt;/li&gt;
&lt;li&gt;汇编（&lt;code&gt;gcc -c&lt;/code&gt;）：将汇编代码翻译成机器码，生成 .o 目标文件&lt;/li&gt;
&lt;li&gt;链接（&lt;code&gt;gcc&lt;/code&gt;）：将目标文件和库文件链接在一起，生成最终的可执行文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ELF 文件：分为可执行文件和可链接文件。可链接文件有：目标文件、动态链接库、静态链接库&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20251013-bh01cm.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ELF Header（ELF 文件头）：文件的开头部分，记录了文件的基本信息，通过其中的字段信息可以找到 Section Header Table 的位置；&lt;/li&gt;
&lt;li&gt;Section Header Table（节区头表）：描述 ELF 文件中所有 section 的位置信息；&lt;/li&gt;
&lt;li&gt;用于链接的 Section
&lt;ul&gt;
&lt;li&gt;Symbol Table（符号表）存放有当前文件内所有符号——文件内定义的符号，以及在当前文件内被使用但未定义的符号。符号表记录了每个符号的符号名以及符号值，即与这个符号对应的函数或者变量地址。&lt;/li&gt;
&lt;li&gt;Relocation Table（重定位表）只存放了当前文件内被使用但未定义的符号。重定位表记录了这些符号的符号名以及地址。和符号表中的地址不同，这里的地址，指的是需要被重定位的指令（或数据）在代码段（或数据段）中的偏移位置。例如，需要对跳转指令&lt;code&gt;callq xxxx&lt;/code&gt;进行重定位，这里的地址指的是&lt;code&gt;xxxx&lt;/code&gt;所在位置的地址，而不是&lt;code&gt;xxxx&lt;/code&gt;指向的地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;动态库的 Section
&lt;ul&gt;
&lt;li&gt;GOT 表（Global Offset Table）全局地址偏移量表：在动态链接库被加载时被填充。GOT 表用于保存所有用到的函数或者变量地址。&lt;/li&gt;
&lt;li&gt;PLT 表（Procedure Linkage Table）代码段表：每个外部符号在 PLT 表中都有一个对应的代码段，这些的执行逻辑都是一样的，负责延迟绑定。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;动态链接与静态链接&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态链接：静态库中的代码和数据是链接时复制到可执行文件中的，而不是运行时加载到进程中的。
&lt;ul&gt;
&lt;li&gt;读取每个可链接文件中的符号表，将所有符号表合并到一张总的符号表中，并且对符号表进行排序和去重；&lt;/li&gt;
&lt;li&gt;将可链接文件合并；&lt;/li&gt;
&lt;li&gt;遍历每个可链接文件中的重定位表，然后对于每个重定位项：根据符号名从总的符号表中查找到与符号名对应的函数或者变量的实际地址，将重定位项的地址信息替换成实际地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;动态链接：动态库的代码和数据运行时加载到进程中。可执行文件只包含了程序本身的代码和一些对共享库函数的调用，但是并未复制共享库代码到可执行文件中。
&lt;ul&gt;
&lt;li&gt;读取每个可链接文件中的符号表，将所有符号表合并到一张总的符号表中，并且对符号表进行排序和去重；&lt;/li&gt;
&lt;li&gt;把各个可链接文件，映射到同一个虚拟地址空间中；&lt;/li&gt;
&lt;li&gt;遍历每个可链接文件中的重定位表，然后对于每个重定位项：将重定位项的地址信息替换成 PLT 表某一项的地址；&lt;/li&gt;
&lt;li&gt;当程序第一次调用某个外部函数时，跳转到 PLT 表，执行 PLT 表项内的代码，PLT 代码段会先根据总的符号表信息，查找到外部函数地址，填写到 GOT 表中，并跳转到外部函数所在地址，执行函数；&lt;/li&gt;
&lt;li&gt;当程序再次调用该外部函数时，仍然是先跳转到 PLT 表，执行 PLT 表项内的代码，不同的是它会从 GOT 表中获取外部函数地址，然后跳转到外部函数所在地址，执行函数。
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20251013-gvVJMG.png&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;区别：本质、执行效率、空间占用、可维护性
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20251013-o1QEWM.png&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;gcc 链接库时注意顺序问题&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.zhihu.com/question/387001677&quot;&gt;为什么 gcc/g++编译链接静态库，会有顺序问题呢？&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;链接器在考察库文件 (.a) 的时候，不是把库文件看做一个整体，而是将打包在其中的目标文件(.o)作为考察单元。连接时，每个库只扫描一遍，每个库中没用任何符号被使用的 .o 文件会被丢弃。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;链接器在工作过程中，维护 3 个集合：需要参与连接的目标文件集合 &lt;code&gt;E&lt;/code&gt;、一个未解析符号集合 &lt;code&gt;U&lt;/code&gt;、一个在 &lt;code&gt;E&lt;/code&gt; 中所有目标文件定义过的所有符号集合 &lt;code&gt;D&lt;/code&gt;。目标文件按照顺序解析, 如果目标文件没有包含任何一个集合 &lt;code&gt;U&lt;/code&gt; 中的符号就会被丢弃。扫描结束时如果 U 非空就会编译错误。&lt;/p&gt;
&lt;p&gt;因此需要基于依赖的拓扑序排列 -l（前面库依赖后面库）。如果存在循环依赖，gcc 提供 Xlinker 选项。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;强制使用静态库编译（可能导致出错，因为有些库只有动态库从而出现 undefined symbol 错误）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认是优先链接动态库，动态库不存在链接静态库&lt;/li&gt;
&lt;li&gt;-static：指定全用静态链接，但可能有些库只有动态链接，会出现问题。&lt;/li&gt;
&lt;li&gt;-Wl,-Bstatic … -Bdynamic：局部开关。-Wl 用于传递链接器参数，-Bstatic 指定接下来优先使用 static 链接，直到遇到 -Bdynamic&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;144. 关于并发编程&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;join()&lt;/code&gt; 和 &lt;code&gt;detach()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;join() 或 detach() 和线程的资源释放问题有关:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程的资源将在调用 t.join() 后且线程执行完成后被回收。&lt;/li&gt;
&lt;li&gt;t.detach() 会将线程 t_thread 与当前线程分离。但其归属权和控制权都将转移给 C++ 运行时库（runtime library，又名运行库），由此保证线程退出时与之关联的资源可以被正确回收。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果没有调用过 join() 或 detach()，也没有转移 std::thread 对象的所有权，那么它的 joinable() 返回值为 true。直接调用析构会导致整个进程结束。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;~thread() {
  if (joinable()) std::terminate();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程对象何时是 joinable 的？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;其是默认构造的。&lt;/li&gt;
&lt;li&gt;如果其已经被用来使用移动构造或赋值构造创建了另一个 std::thread 对象。&lt;/li&gt;
&lt;li&gt;其成员函数 join() 或 detach() 已经被调用过。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;被创建的 std::thread 需要调用 join() 或者 detach()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有关联的线程仍然活跃（是 joinable 的）的 std::thread 不能被销毁，否则报错 “terminate called without an active exception”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;std::thread 参数必须是可调用的或者可以转成右值，传入非常量引用需要 std::ref 套一下&lt;/p&gt;
&lt;p&gt;线程是有自己的内存存储空间的，在 std::thread 类的构造中，参数会先按照默认方式复制到线程的存储空间中，然后新创建的线程才能访问它们。这些副本被当做临时变量，以&lt;strong&gt;右值&lt;/strong&gt;的方式传递给新线程上的函数或者可调用对象。即便函数相关的参数按设想应该是引用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;145. 怎么设计哈希表，负载因子设置为多少，有什么办法提高哈希表的填充率？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;小红书 搜广推 C++ 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;设计哈希表需要综合考虑哈希函数、冲突解决机制、扩容策略和内存管理。&lt;/p&gt;
&lt;p&gt;哈希函数应具有良好的分布性和计算效率，常见的有 MurmurHash、CityHash 等；冲突解决可采用链地址法（链表存储冲突元素，简单稳定）或开放寻址法（线性探测、二次探测、双哈希，缓存友好但易产生聚集）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;扩容因子（负载因子）&lt;/strong&gt; 是哈希表中已存储元素数量与哈希表总容量的比值，用于衡量哈希表的填充程度和触发扩容的时机。&lt;/p&gt;
&lt;p&gt;一般是达到 0.75 后，直接扩容为原来的 2 倍&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;负载因子通常设置为 0.75，这是经验证能在时间效率和空间利用率间取得最佳平衡的阈值——超过此值冲突概率急剧上升，低于此值则内存浪费明显。&lt;/p&gt;
&lt;p&gt;提高填充率的核心方法包括：优化哈希函数减少冲突，使用二次探测或双哈希降低初级聚集，实现动态扩容（通常翻倍扩容并重哈希），采用布谷鸟哈希通过多个哈希函数和踢出机制提升空间利用率至 95%+，以及实现渐进式 rehash 避免扩容时的服务中断，这些技术能显著提升哈希表在保持高性能的同时的空间利用效率。&lt;/p&gt;
&lt;h2&gt;146. 哪些设计模式利用了虚函数的特点？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;高德 C++ 一面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;设计模式中，模板方法、策略、工厂方法、抽象工厂、观察者、装饰器、访问者、状态、职责链和命令等模式都深度依赖于虚函数实现的运行时多态性：这些模式通过在基类或接口中定义抽象的虚函数，并在各个具体子类中提供不同的实现，使得相同的接口调用能在运行时表现出不同的行为。这种机制使得程序架构能够将稳定的框架逻辑与易变的具体实现解耦，让算法骨架（模板方法）、对象创建（工厂）、行为策略（策略模式）、事件响应（观察者）、功能扩展（装饰器）等核心要素可以在不修改现有代码的前提下灵活扩展和替换，从而实现了面向接口而非实现的编程范式，为软件设计提供了强大的灵活性和可维护性。&lt;/p&gt;
&lt;h2&gt;147. 多重继承下（C 继承 A 和 B），C 类的对象内存布局是怎样的？&lt;/h2&gt;
&lt;h3&gt;🔥 虚函数必知前提&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个虚函数表对应一个类（前提有 virtual）&lt;/li&gt;
&lt;li&gt;一个类可能不止一个虚函数表（多重继承的情况下）&lt;/li&gt;
&lt;li&gt;派生类和基类都有各自的虚函数表（派生类拷贝基类虚函数表后，重写虚函数则覆盖函数地址，新增虚函数则增加函数地址）&lt;/li&gt;
&lt;li&gt;多重继承下 &lt;strong&gt;C 类有两个虚函数表&lt;/strong&gt;：第一个 vptr_a 指向的虚函数表包含 A 和 C 的虚函数，第二个 vptr_b 指向的虚函数表包含 B 和 C 的虚函数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多继承场景的内存布局（C 类有 2 张虚函数表，其中 1 张为主表）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;C 对象的内存布局：
+----------------+  &amp;#x3C;-- C* 指针, A* 指针
| AC的虚表指针    |  --&gt; C 虚函数表 1: [ &amp;#x26;C::funcA, &amp;#x26;A::~A, &amp;#x26;C::funcC ]
+----------------+
| A::a_data      |
+----------------+
| BC的虚表指针    |  --&gt; C 虚函数表 2: [ &amp;#x26;thunk_to_C::funcB, &amp;#x26;C::~C ]
+----------------+
| B::b_data      |
+----------------+
| C::c_data      |
+----------------+

A 的虚函数表：
[0] &amp;#x26;C::funcA    // C重写的funcA
[1] &amp;#x26;A::~A       // 析构函数  
[2] &amp;#x26;C::funcC    // C自己的虚函数

B 的虚函数表：
[0] &amp;#x26;thunk_to_C::funcB  // 需要调整this指针的跳转函数
[1] &amp;#x26;B::~B              // 析构函数
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么要调整 this 指针&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;不调整 Base2 的 this 指针时，默认是直接指向第二个虚函数表指针的位置（因为这样 Base2 可以直接在首地址访问到第二个虚表指针，快速调用自己重写的虚函数，但是对象首地址应该是第一个虚函数表指针的位置 —— 整体对象首地址），这会造成&lt;strong&gt;所有成员访问都会错位！&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;让我详细解释&lt;strong&gt;为什么非首基类调用需要调整this指针&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;1. 根本原因：内存布局导致的指针偏移&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Base1 {
public:
    int b1_data;
    virtual void func() { /* 期望this指向Base1开始 */ }
};

class Base2 {
public:
    int b2_data; 
    virtual void func() { /* 期望this指向Base2开始 */ }
};

class Derived : public Base1, public Base2 {
public:
    int d_data;
    void func() override { /* 需要访问所有数据：b1_data, b2_data, d_data */ }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 具体内存布局分析&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Derived对象内存布局：
+----------------+  &amp;#x3C;-- Derived* = 0x1000 (完整对象起始地址)
| Base1虚表指针   |  
+----------------+
| Base1::b1_data |  // 偏移量 +0
+----------------+
| Base2虚表指针   |  &amp;#x3C;-- Base2* = 0x1008 (Base2子对象起始地址)
+----------------+
| Base2::b2_data |  // 偏移量 +8 (相对于Base2*)
+----------------+
| Derived::d_data| // 偏移量 +16 (相对于Base2*)
+----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 问题所在：this 指针不匹配&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;通过不同指针调用时&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Derived derived;
Base1* b1_ptr = &amp;#x26;derived;  // b1_ptr = 0x1000
Base2* b2_ptr = &amp;#x26;derived;  // b2_ptr = 0x1008 (偏移了8字节!)

b1_ptr-&gt;func();  // 传入的this = 0x1000 ✓
b2_ptr-&gt;func();  // 传入的this = 0x1008 ✗ 问题！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Derived::func() 需要访问的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void Derived::func() {
    // 这些访问都基于完整的Derived对象地址
    cout &amp;#x3C;&amp;#x3C; b1_data;   // 在偏移量 +8 处
    cout &amp;#x3C;&amp;#x3C; b2_data;   // 在偏移量 +16 处  
    cout &amp;#x3C;&amp;#x3C; d_data;    // 在偏移量 +20 处
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 如果不调整 this 指针，所有成员访问都会错位！&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 假设不调整this指针，直接调用：
b2_ptr-&gt;func();  // this = 0x1008

// 在Derived::func()中：
cout &amp;#x3C;&amp;#x3C; b1_data;   // 实际访问 0x1008 + 8 = 0x1010 (错误！应该是0x1008)
cout &amp;#x3C;&amp;#x3C; b2_data;   // 实际访问 0x1008 + 16 = 0x1018 (错误！应该是0x1010)  
cout &amp;#x3C;&amp;#x3C; d_data;    // 实际访问 0x1008 + 20 = 0x101C (错误！应该是0x1014)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;148. 当 C 类继承 A 类时，A 和 C 的构造函数与对象的虚函数表指针之间存在什么样的交互关系？&lt;/h2&gt;
&lt;p&gt;我们来详细拆解 C 类继承 A 类时，构造函数和虚函数表指针的关系。简单来说，&lt;strong&gt;构造函数负责为本类及其基类“初始化”虚函数表指针，使其指向正确的虚表&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下面是详细的步骤和关系：&lt;/p&gt;
&lt;h3&gt;1. 对象的内存布局（继承时）&lt;/h3&gt;
&lt;p&gt;假设我们有如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class A {
public:
    virtual void vfunc1() { }
    virtual void vfunc2() { }
    int a_data;
};

class C : public A {
public:
    virtual void vfunc1() override { } // 重写
    virtual void vfunc3() { }          // 新的虚函数
    int c_data;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个 &lt;code&gt;C&lt;/code&gt; 类对象在内存中的典型布局如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;|------------------------|
| A::vptr (虚函数表指针) |  &amp;#x3C;- 对象开头，通常只有一个vptr
|------------------------|
| A::a_data              |
|------------------------|
| C::c_data              |
|------------------------|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：在单继承情况下，派生类对象通常&lt;strong&gt;只包含一个虚函数表指针&lt;/strong&gt;，这个指针位于对象的起始位置，由基类 &lt;code&gt;A&lt;/code&gt; 引入。&lt;/p&gt;
&lt;h3&gt;2. 构造函数的执行过程与 vptr 的初始化&lt;/h3&gt;
&lt;p&gt;当你创建一个 &lt;code&gt;C&lt;/code&gt; 对象时（&lt;code&gt;C* obj = new C();&lt;/code&gt;），构造函数的调用和 vptr 的设置流程如下：&lt;/p&gt;
&lt;h4&gt;步骤 1: 分配内存&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;操作系统为 &lt;code&gt;C&lt;/code&gt; 类对象分配足够的内存（包含 A 部分 + C 部分）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;步骤 2: 进入 C 的构造函数&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;在进入 &lt;code&gt;C&lt;/code&gt; 的构造函数体 &lt;strong&gt;之前&lt;/strong&gt;，&lt;strong&gt;编译器会插入代码，将对象的 vptr 设置为指向 C 类的虚函数表&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;然后调用基类 &lt;code&gt;A&lt;/code&gt; 的构造函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;步骤 3: 进入 A 的构造函数&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;在进入 &lt;code&gt;A&lt;/code&gt; 的构造函数体 &lt;strong&gt;之前&lt;/strong&gt;，&lt;strong&gt;编译器会插入代码，将对象的 vptr 设置为指向 A 类的虚函数表&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;执行 &lt;code&gt;A&lt;/code&gt; 的构造函数体。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;步骤 4: 返回 C 的构造函数&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;A&lt;/code&gt; 的构造函数执行完毕后，返回到 &lt;code&gt;C&lt;/code&gt; 的构造函数。&lt;/li&gt;
&lt;li&gt;此时，&lt;strong&gt;编译器会再次将对象的 vptr 重新设置为指向 C 类的虚函数表&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;最后执行 &lt;code&gt;C&lt;/code&gt; 的构造函数体。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 虚函数表指针的变化流程&lt;/h3&gt;
&lt;p&gt;这个过程中，vptr 的变化可以用以下伪代码表示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 创建 C 对象时的幕后代码
C* obj = operator new(sizeof(C));  // 1. 分配内存

// 2. 开始构造 C
obj-&gt;vptr = &amp;#x26;C::vtable;            // 先指向C的虚表（临时）
A::A(obj);                         // 3. 调用基类构造函数
    // 在 A::A 内部:
    obj-&gt;vptr = &amp;#x26;A::vtable;        // 指向A的虚表
    // ... 执行A的构造函数体 ...
// 从 A::A 返回后
obj-&gt;vptr = &amp;#x26;C::vtable;            // 重新指向C的虚表（最终）
// ... 执行C的构造函数体 ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 重要结论和影响&lt;/h3&gt;
&lt;h4&gt;为什么要在构造函数中这样设置 vptr？&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类型安全&lt;/strong&gt;：在 &lt;code&gt;A&lt;/code&gt; 的构造函数中调用虚函数时，应该调用 &lt;code&gt;A&lt;/code&gt; 版本的实现，而不是可能依赖 &lt;code&gt;C&lt;/code&gt; 中未初始化数据的 &lt;code&gt;C&lt;/code&gt; 版本重写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构造顺序&lt;/strong&gt;：对象是从基类到派生类依次构造的，在构造 &lt;code&gt;A&lt;/code&gt; 时，&lt;code&gt;C&lt;/code&gt; 还没有构造完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;虚函数表的内容&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A 的虚表&lt;/strong&gt;：&lt;code&gt;{ A::vfunc1, A::vfunc2 }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C 的虚表&lt;/strong&gt;：&lt;code&gt;{ C::vfunc1, A::vfunc2, C::vfunc3 }&lt;/code&gt; （注意 &lt;code&gt;vfunc1&lt;/code&gt; 被替换了）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;在构造函数中调用虚函数&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class A {
public:
    A() { 
        vfunc1(); // 这里调用的是 A::vfunc1，不是 C::vfunc1！
    }
    virtual void vfunc1() { cout &amp;#x3C;&amp;#x3C; &quot;A&apos;s vfunc1\n&quot;; }
};

class C : public A {
public:
    virtual void vfunc1() override { cout &amp;#x3C;&amp;#x3C; &quot;C&apos;s vfunc1\n&quot;; }
};

// 执行：
C obj; // 输出 &quot;A&apos;s vfunc1&quot;，而不是 &quot;C&apos;s vfunc1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;构造函数和虚函数表指针的关系&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;构造顺序决定 vptr 设置&lt;/strong&gt;：在进入每个类的构造函数体之前，vptr 被设置为指向当前类的虚表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vptr 会多次变化&lt;/strong&gt;：在构造过程中，vptr 会随着构造流程在基类和派生类的虚表之间切换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最终指向派生类&lt;/strong&gt;：构造完成后，vptr 最终指向最派生类（本例中的 &lt;code&gt;C&lt;/code&gt;）的虚表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保证构造安全&lt;/strong&gt;：这种机制确保了在构造函数中调用虚函数时，不会调用到尚未构造完成的派生类重写版本。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就是 C++ 对象构造与多态机制协同工作的精妙之处。&lt;/p&gt;
&lt;h2&gt;149. 在基类构造函数中调用虚函数，此时是调用基类还是派生类？那如果在派生类构造函数中调用虚函数呢？（上一个问题有提及）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;类型安全&lt;/strong&gt;：在 &lt;code&gt;A&lt;/code&gt; 的构造函数中调用虚函数时，应该调用 &lt;code&gt;A&lt;/code&gt; 版本的实现，而不是可能依赖 &lt;code&gt;C&lt;/code&gt; 中未初始化数据的 &lt;code&gt;C&lt;/code&gt; 版本重写。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;构造顺序&lt;/strong&gt;：对象是从基类到派生类依次构造的，在构造 &lt;code&gt;A&lt;/code&gt; 时，&lt;code&gt;C&lt;/code&gt; 还没有构造完成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;基类构造函数中调用虚函数：基类（此时派生类还没有构造好，因为构造顺序是先父类再派生类）&lt;/p&gt;
&lt;p&gt;派生类构造函数中调用虚函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用的是派生类重写的虚函数：派生类&lt;/li&gt;
&lt;li&gt;调用的是派生类未重写的虚函数：基类（因为未重写，虚函数表未覆盖原有的虚函数地址，仍然指向基类的虚函数地址）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;150. 你知道可重入锁吗？如何实现可重入锁？具体的字段和函数如何设计？&lt;/h2&gt;
&lt;p&gt;“重入”指的是&lt;strong&gt;同一个线程&lt;/strong&gt;，在已经持有某个锁的前提下，可以再次成功获取该锁，而不会被自己阻塞。&lt;/p&gt;
&lt;p&gt;可重入锁允许&lt;strong&gt;同一个线程&lt;/strong&gt;多次获取锁而不被自己阻塞，但这&lt;strong&gt;不影响&lt;/strong&gt;锁的基本互斥特性——&lt;strong&gt;其他线程仍然必须等待&lt;/strong&gt;当前线程完全释放锁后才能获取。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;mutex&gt;
#include &amp;#x3C;thread&gt;
#include &amp;#x3C;condition_variable&gt;
#include &amp;#x3C;atomic&gt;
#include &amp;#x3C;stdexcept&gt;

class ReentrantLock {
private:
    std::thread::id owner_;           // 当前持有锁的线程ID
    std::atomic&amp;#x3C;int&gt; count_;          // 重入计数
    std::mutex mutex_;                // 保护内部状态
    std::condition_variable cond_;    // 等待条件变量

public:
    ReentrantLock() : count_(0) {}
    
    // 获取锁
    void lock() {
        std::unique_lock&amp;#x3C;std::mutex&gt; lock(mutex_);
        std::thread::id this_thread = std::this_thread::get_id();
        
        // 如果锁被其他线程持有，等待
        while (count_ &gt; 0 &amp;#x26;&amp;#x26; owner_ != this_thread) {
            cond_.wait(lock);
        }
        
        // 现在可以获取锁
        owner_ = this_thread;
        count_++;
    }
    
    // 释放锁
    void unlock() {
        std::unique_lock&amp;#x3C;std::mutex&gt; lock(mutex_);
        std::thread::id this_thread = std::this_thread::get_id();
        
        if (count_ &amp;#x3C;= 0 || owner_ != this_thread) {
            throw std::logic_error(&quot;Attempt to unlock a lock not owned by current thread&quot;);
        }
        
        count_--;
        if (count_ == 0) {
            owner_ = std::thread::id();  // 清空所有者
            cond_.notify_one();          // 唤醒一个等待线程
        }
    }
    
    // 尝试获取锁
    bool try_lock() {
        std::unique_lock&amp;#x3C;std::mutex&gt; lock(mutex_);
        std::thread::id this_thread = std::this_thread::get_id();
        
        if (count_ == 0 || owner_ == this_thread) {
            owner_ = this_thread;
            count_++;
            return true;
        }
        return false;
    }
    
    // 获取重入次数（主要用于调试）
    int get_count() const {
        return count_.load();
    }
    
    // 检查当前线程是否持有锁
    bool is_owned_by_current_thread() const {
        return count_ &gt; 0 &amp;#x26;&amp;#x26; owner_ == std::this_thread::get_id();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;151. 常见的内存安全问题有哪些？&lt;/h2&gt;
&lt;p&gt;内存安全问题主要源于程序对计算机内存（如堆、栈）的错误访问或管理，通常会导致程序崩溃、数据损坏或严重的安全漏洞（如被攻击者利用以执行任意代码）。&lt;/p&gt;
&lt;p&gt;以下是常见的内存安全问题分类和详解：&lt;/p&gt;
&lt;h3&gt;1. 缓冲区溢出&lt;/h3&gt;
&lt;p&gt;这是最经典、最危险的内存安全问题之一。当程序向一个分配好大小的缓冲区（如数组）写入数据时，写入的数据量超过了缓冲区的容量，导致覆盖了相邻的内存区域。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;栈缓冲区溢出&lt;/strong&gt;：溢出发生在栈上，可能会覆盖函数的返回地址。攻击者可以精心构造数据，将返回地址指向其注入的恶意代码，从而控制程序流程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆缓冲区溢出&lt;/strong&gt;：溢出发生在堆上，可能会破坏堆中相邻的数据结构（如堆块头、其他对象等），导致程序行为异常或为攻击者提供利用机会。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void copy_string(char *input) {
    char buffer[16]; // 栈上分配一个16字节的缓冲区
    strcpy(buffer, input); // 如果input长度超过15字节（+1个结束符），就会发生溢出！
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 解引用空指针或野指针&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;空指针解引用&lt;/strong&gt;：试图访问内存地址 &lt;code&gt;0x0&lt;/code&gt;（通常表示空指针）的内容。这会导致程序立即崩溃（段错误）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;野指针解引用&lt;/strong&gt;：指针指向的内存已被释放或未初始化，但其值未被置空。此时解引用它，行为是未定义的——可能读到垃圾数据、写入到已不属于它的内存，导致难以调试的错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int *ptr = NULL;
*ptr = 5; // 解引用空指针，崩溃！

int *wild_ptr = (int*)malloc(sizeof(int));
free(wild_ptr); // 内存已释放...
*wild_ptr = 10; // ...但指针仍指向那里，现在是一个野指针，解引用它行为未定义！
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 使用后释放&lt;/h3&gt;
&lt;p&gt;这是野指针问题的一种常见形式。内存被释放后，指向它的指针仍然存在。如果该内存被重新分配用于其他用途，那么通过野指针修改它就会破坏新数据，造成信息泄露或控制流劫持。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char *ptr = (char*)malloc(100);
free(ptr); // 内存释放，ptr现在是野指针
// ... 一段时间后，系统可能将这块内存分配给另一个对象
strcpy(ptr, &quot;Hello&quot;); // 破坏了新对象的数据！
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 双重释放&lt;/h3&gt;
&lt;p&gt;对同一块动态分配的内存进行多次 &lt;code&gt;free()&lt;/code&gt; 操作。这会破坏内存管理器的数据结构（如glibc的堆元数据），可能导致程序崩溃或让攻击者插入恶意代码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int *ptr = (int*)malloc(sizeof(int));
free(ptr);
free(ptr); // 错误：双重释放！
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 内存泄漏&lt;/h3&gt;
&lt;p&gt;程序动态分配了内存（如使用 &lt;code&gt;malloc&lt;/code&gt;, &lt;code&gt;new&lt;/code&gt;），但在使用完毕后忘记释放。不断的内存泄漏会逐渐消耗掉系统的所有可用内存，最终导致程序或系统变慢甚至崩溃。虽然它不直接让攻击者利用，但会降低系统稳定性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void leak_memory() {
    while(1) {
        int *ptr = (int*)malloc(1024); // 每次循环都分配1KB，但从不释放
        // ... 使用ptr ...
        // 忘记 free(ptr);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 未初始化的内存读取&lt;/h3&gt;
&lt;p&gt;变量（尤其是栈或堆上的变量）在分配后没有赋初值就直接读取。此时读到的内容是之前留在内存中的随机值（垃圾值），会导致程序逻辑错误和不稳定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int value; // 未初始化
printf(&quot;%d&quot;, value); // 可能输出任意值，行为不确定
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7. 整数溢出与回绕&lt;/h3&gt;
&lt;p&gt;在进行算术运算时，结果超出了该整数类型所能表示的范围。这可能导致缓冲区大小的计算错误，进而引发缓冲区溢出。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无符号整数回绕&lt;/strong&gt;：&lt;code&gt;UINT_MAX + 1&lt;/code&gt; 会变成 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有符号整数溢出&lt;/strong&gt;：行为在C/C++标准中是&lt;strong&gt;未定义的&lt;/strong&gt;，但常见实现是回绕。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;uint32_t buffer_size = count * sizeof(int); // 如果count很大，乘法可能回绕成一个很小的数
int *buffer = (int*)malloc(buffer_size); // 现在分配的空间远小于所需
// 后续向buffer写入数据会导致堆缓冲区溢出
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8. 迭代器失效&lt;/h3&gt;
&lt;p&gt;在C++等语言中，当修改容器（如 &lt;code&gt;vector&lt;/code&gt;, &lt;code&gt;deque&lt;/code&gt;）时（例如插入、删除元素），指向容器元素的迭代器可能会变得无效。继续使用这些失效的迭代器会导致未定义行为。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;std::vector&amp;#x3C;int&gt; vec = {1, 2, 3};
auto it = vec.begin();
vec.push_back(4); // 可能导致vector重新分配内存，使所有迭代器失效
*it = 5; // 错误！使用已失效的迭代器
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;152. vector 扩容采用深拷贝还是浅拷贝？&lt;/h2&gt;
&lt;p&gt;深拷贝，避免资源 double free。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::vector&lt;/code&gt; 在扩容时（例如，当 &lt;code&gt;push_back&lt;/code&gt; 新元素导致 &lt;code&gt;size() &gt; capacity()&lt;/code&gt; 时），会执行以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;分配新内存&lt;/strong&gt;：在自由存储区（堆）上分配一块新的、更大的内存空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拷贝（或移动）元素&lt;/strong&gt;：将旧内存空间中的所有元素&lt;strong&gt;逐个&lt;/strong&gt;转移到新的内存空间中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放旧内存&lt;/strong&gt;：销毁旧内存空间中的对象并释放内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关键在于第2步：&lt;strong&gt;“逐个转移”&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 vector 中存储的是 &lt;strong&gt;内置类型&lt;/strong&gt; 或 &lt;strong&gt;可平凡拷贝的类型&lt;/strong&gt;，这个“拷贝”在行为上和 &lt;strong&gt;“逐位浅拷贝”&lt;/strong&gt; 效果相同，但因为调用的是对象的&lt;strong&gt;拷贝构造函数&lt;/strong&gt;或&lt;strong&gt;移动构造函数&lt;/strong&gt;，所以从语言机制上，我们依然称之为“拷贝”（一种高效的、按值复制的深拷贝）。&lt;/li&gt;
&lt;li&gt;如果 vector 中存储的是 &lt;strong&gt;自定义类对象&lt;/strong&gt;，那么这个过程会明确地调用每个对象的&lt;strong&gt;拷贝构造函数&lt;/strong&gt; 或 &lt;strong&gt;移动构造函数&lt;/strong&gt;。此时 vector 分配新内存，然后简单地将旧内存中的 &lt;code&gt;MyObject&lt;/code&gt; 指针（即 &lt;code&gt;data&lt;/code&gt; 成员）复制到新内存中。现在，&lt;strong&gt;两个 &lt;code&gt;MyObject&lt;/code&gt; 对象（旧内存和新内存中的）的 &lt;code&gt;data&lt;/code&gt; 成员指向了同一块堆内存&lt;/strong&gt;。vector 紧接着会&lt;strong&gt;释放旧内存&lt;/strong&gt;。在释放过程中，它会调用旧内存中每个 &lt;code&gt;MyObject&lt;/code&gt; 对象的析构函数，导致 &lt;code&gt;delete data;&lt;/code&gt;。结果就是，新内存中的 &lt;code&gt;MyObject&lt;/code&gt; 对象的 &lt;code&gt;data&lt;/code&gt; 指针变成了一个&lt;strong&gt;悬空指针&lt;/strong&gt;。当你后续尝试通过它访问数据时，行为未定义（通常是程序崩溃）。更糟糕的是，当这个 vector 最终被销毁时，它会再次 &lt;code&gt;delete&lt;/code&gt; 同一块内存，导致&lt;strong&gt;双重释放&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;如果你的对象支持移动语义，vector 在扩容时会优先使用&lt;strong&gt;移动构造函数&lt;/strong&gt;，这比深拷贝更高效，因为它“窃取”旧对象的资源，而不是复制它们。对于这样的类，vector 扩容时会调用“移动构造”，只是转移了指针所有权，没有进行昂贵的内存分配和值复制。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class MyObject {
public:
    int* data;
    MyObject(int value) {
        data = new int(value);
    }

    // 深拷贝拷贝构造函数
    MyObject(const MyObject&amp;#x26; other) {
        data = new int(*(other.data)); // 分配新内存，并复制值
        std::cout &amp;#x3C;&amp;#x3C; &quot;拷贝构造被调用&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
    // 深拷贝拷贝赋值运算符 (通常还需要实现)
    MyObject&amp;#x26; operator=(const MyObject&amp;#x26; other) {
        if (this != &amp;#x26;other) {
            delete data; // 释放原有资源
            data = new int(*(other.data));
        }
        std::cout &amp;#x3C;&amp;#x3C; &quot;拷贝赋值被调用&quot; &amp;#x3C;&amp;#x3C; std::endl;
        return *this;
    }

    // 移动构造函数 (noexcept 很重要，便于vector优化)
    MyObject(MyObject&amp;#x26;&amp;#x26; other) noexcept : data(other.data) {
        other.data = nullptr; // 使旧对象处于有效但可析构的状态
        std::cout &amp;#x3C;&amp;#x3C; &quot;移动构造被调用&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }

    // ... 相应的移动赋值运算符 ...

    ~MyObject() {
        delete data;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;153. 析构函数什么情况下才需要设置为虚析构？什么时候可以不用？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;需要虚析构函数的情况&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;✅ 类被设计为基类，且可能通过基类指针删除派生类对象&lt;/li&gt;
&lt;li&gt;✅ 类包含虚函数（有虚函数通常意味着多态使用）&lt;/li&gt;
&lt;li&gt;✅ 类有派生类且派生类需要资源清理（避免资源泄漏）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;不需要虚析构函数的情况&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;❌ 类不会被继承（如工具类、值类型）&lt;/li&gt;
&lt;li&gt;❌ 类标记为 &lt;code&gt;final&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;❌ 不会通过基类指针删除对象&lt;/li&gt;
&lt;li&gt;❌ 类没有虚函数且不打算多态使用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;经验法则&lt;/strong&gt;：如果一个类有&lt;strong&gt;任何一个虚函数&lt;/strong&gt;，那么它的析构函数也应该是虚的。&lt;/p&gt;
&lt;h2&gt;154. 钻石继承（虚继承）的内存布局&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Base {
public:
    int base_data = 0xBB;
    virtual void base_func() { cout &amp;#x3C;&amp;#x3C; &quot;Base::base_func&quot; &amp;#x3C;&amp;#x3C; endl; }
};

class Mid1 : virtual public Base {  // 虚继承
public:
    int mid1_data = 0xM1;
    virtual void mid1_func() { cout &amp;#x3C;&amp;#x3C; &quot;Mid1::mid1_func&quot; &amp;#x3C;&amp;#x3C; endl; }
};

class Mid2 : virtual public Base {  // 虚继承  
public:
    int mid2_data = 0xM2;
    virtual void mid2_func() { cout &amp;#x3C;&amp;#x3C; &quot;Mid2::mid2_func&quot; &amp;#x3C;&amp;#x3C; endl; }
};

class Bottom : public Mid1, public Mid2 {
public:
    int bottom_data = 0xBT;
    void base_func() override { cout &amp;#x3C;&amp;#x3C; &quot;Bottom::base_func&quot; &amp;#x3C;&amp;#x3C; endl; }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Bottom对象内存布局：
+----------------+  &amp;#x3C;-- Bottom*, Mid1* 
| Mid1虚表指针   |  --&gt; [ &amp;#x26;Mid1::mid1_func, ... + 虚基类信息 ]
+----------------+
| mid1_data      |
+----------------+
| Mid2虚表指针   |  --&gt; [ &amp;#x26;Mid2::mid2_func, ... + 虚基类信息 ]
+----------------+
| mid2_data      |
+----------------+
| bottom_data    |
+----------------+
| Base虚表指针   |  --&gt; [ &amp;#x26;Bottom::base_func, ... ]
+----------------+
| base_data      |
+----------------+

虚表数量：3张
1. Mid1部分的虚表
2. Mid2部分的虚表  
3. Base部分的虚表（共享）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;155. delete 和 delete[] 有什么区别？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;delete&lt;/code&gt; vs &lt;code&gt;delete[]&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;delete p&lt;/code&gt;：销毁&lt;strong&gt;一个对象&lt;/strong&gt;，调用其析构，随后调用对应的 &lt;code&gt;operator delete&lt;/code&gt; 释放内存。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete[] p&lt;/code&gt;：销毁&lt;strong&gt;数组中的每个元素&lt;/strong&gt;（通常逆序调用析构），再调用 &lt;code&gt;operator delete[]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成对使用&lt;/strong&gt;：&lt;code&gt;new&lt;/code&gt;→&lt;code&gt;delete&lt;/code&gt;，&lt;code&gt;new[]&lt;/code&gt;→&lt;code&gt;delete[]&lt;/code&gt;；混用是&lt;strong&gt;未定义行为&lt;/strong&gt;（无法知道元素个数）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;delete[]&lt;/code&gt; 的释放过程&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;运行时需要知道&lt;strong&gt;元素个数&lt;/strong&gt;以逐个析构——常见实现会在分配块前留“&lt;strong&gt;数组 cookie&lt;/strong&gt;”记录个数（标准不强制，但做法通行）。&lt;/li&gt;
&lt;li&gt;调用每个元素的析构（通常&lt;strong&gt;逆序&lt;/strong&gt;）。&lt;/li&gt;
&lt;li&gt;调 &lt;code&gt;operator delete[]&lt;/code&gt; 释放整块内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;156. delete[] 怎么知道删除几个元素？额外记录元素个数不会影响索引访问吗？&lt;/h2&gt;
&lt;p&gt;不会出问题的。&lt;strong&gt;数组 “cookie” 放在内存块起始处，返回给你的指针仍然指向第 0 个元素&lt;/strong&gt;，所以用下标访问完全正常。更具体点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;p = new T[n];&lt;/code&gt; 的典型内存布局大致是
&lt;code&gt;[ cookie | 可能的对齐填充 | T(0) | T(1) | ... | T(n-1) ]&lt;/code&gt;
这里 &lt;strong&gt;&lt;code&gt;p&lt;/code&gt; 等于 &lt;code&gt;&amp;#x26;T(0)&lt;/code&gt;&lt;/strong&gt;，而不是指向 cookie。你做 &lt;code&gt;p[i]&lt;/code&gt;、&lt;code&gt;*(p+i)&lt;/code&gt; 都是在元素区间里算术，和 cookie 没有任何交集。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cookie 仅供 &lt;code&gt;delete[] p&lt;/code&gt; 使用&lt;/strong&gt;：实现会在删除时把 &lt;code&gt;p&lt;/code&gt; 向前回退到隐藏头（或使用“带大小”的 delete 接口），取出元素个数，按需要逐个析构，再释放整块内存。正常访问阶段你看不到它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对齐不会被破坏&lt;/strong&gt;：分配器会多分配一些字节，确保把 &lt;code&gt;p&lt;/code&gt; 调整到满足 &lt;code&gt;alignof(T)&lt;/code&gt;（对于超对齐类型还会用到对齐版的 &lt;code&gt;operator new[]&lt;/code&gt;），因此 &lt;code&gt;p&lt;/code&gt; 的对齐是正确的。&lt;/li&gt;
&lt;li&gt;一些实现/场景甚至&lt;strong&gt;不需要 cookie&lt;/strong&gt;（例如使用“带大小”的 &lt;code&gt;operator delete[](void*, std::size_t)&lt;/code&gt; 或 trivially destructible 的类型），但这对你的索引访问没有影响。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;真正会出问题的情况是未定义行为&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;new T[n]&lt;/code&gt; 却配 &lt;code&gt;delete p&lt;/code&gt;，或 &lt;code&gt;new T&lt;/code&gt; 配 &lt;code&gt;delete[] p&lt;/code&gt;——删除路径不匹配，运行时找不到正确的元素个数，轻则泄漏，重则崩溃。&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;p&lt;/code&gt; 做越界/负向指针运算（例如试图访问 &lt;code&gt;*(p-1)&lt;/code&gt; 想“看到 cookie”）——这是未定义行为。&lt;/li&gt;
&lt;li&gt;自己实现/重载 &lt;code&gt;operator new[]/delete[]&lt;/code&gt; 却没有正确处理对齐与大小信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结：&lt;strong&gt;正常写法下，索引访问只落在元素区域内，和 cookie 完全隔离&lt;/strong&gt;；记得 &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt;、&lt;code&gt;new[]&lt;/code&gt;/&lt;code&gt;delete[]&lt;/code&gt; 要成对使用就行。&lt;/p&gt;
&lt;h2&gt;157. 什么是右值引用？必须用 move 才能转为右值吗吗？&lt;/h2&gt;
&lt;p&gt;不必须。&lt;code&gt;std::move&lt;/code&gt; 只是&lt;strong&gt;把一个左值显式地“转成将亡值(xvalue)”&lt;/strong&gt; 的工具（本质是 &lt;code&gt;static_cast&amp;#x3C;T&amp;#x26;&amp;#x26;&gt;&lt;/code&gt; 的封装），以便命中移动重载。得到右值/触发移动还有很多途径：&lt;/p&gt;
&lt;h3&gt;什么时候不必用 &lt;code&gt;move&lt;/code&gt; 也会移动&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;临时对象/返回值（prvalue）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::string s = std::string(&quot;hi&quot;); // 直接构造/拷贝省略；不需要 move
std::string f() { return std::string(&quot;hi&quot;); } // 返回值是右值
auto t = f(); // 若未省略拷贝，则优先用移动构造
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++17 起很多场景&lt;strong&gt;保证拷贝消除&lt;/strong&gt;，连“移动”都省了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;return 局部对象（隐式移动）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::string g() {
  std::string x = &quot;hi&quot;;
  return x;            // x 是本地对象、同类型返回——隐式当成右值（或直接省略）
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“值传参再返回”/sink 参数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void set_name(std::string s) { name_ = std::move(s); } // 调用处传实参时会移动
set_name(std::move(tmp));   // 显式
set_name(f());              // f() 返回值本就是右值
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;容器的 &lt;code&gt;emplace_\*&lt;/code&gt; / 工厂函数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;v.emplace_back(100, &apos;x&apos;); // 直接就地构造，没有 move 的必要
auto p = std::make_unique&amp;#x3C;Foo&gt;(); // 返回右值
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;迭代器适配器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::vector&amp;#x3C;std::string&gt; a, b;
b.insert(b.end(),
         std::make_move_iterator(a.begin()),
         std::make_move_iterator(a.end()));  // 把范围“视作右值”
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;确实需要 &lt;code&gt;move&lt;/code&gt; 的典型场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;把一个“有名字的对象”（左值）当右值用&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::string s = &quot;hi&quot;;
v.push_back(std::move(s));  // 不写 move 就会拷贝
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类内转移所有权&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;holder(ptr p) : p_(std::move(p)) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;模板里应当用 &lt;code&gt;forward&lt;/code&gt;（而非一律 &lt;code&gt;move&lt;/code&gt;）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;完美转发&lt;/strong&gt;：只在参数本来是右值时才转成右值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template&amp;#x3C;class T, class... Args&gt;
T make(Args&amp;#x26;&amp;#x26;... args) {
  return T(std::forward&amp;#x3C;Args&gt;(args)...);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常见误区 &amp;#x26; 边界&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;std::move&lt;/code&gt; 不会“移动”，它只是一个无开销的强制类型转换&lt;/strong&gt;；是否真的走移动构造/赋值，要看目标类型是否提供相应重载。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对 &lt;code&gt;const&lt;/code&gt; 对象用 &lt;code&gt;std::move&lt;/code&gt; 往往没有意义&lt;/strong&gt;：得到的是 &lt;code&gt;const T&amp;#x26;&amp;#x26;&lt;/code&gt;，大多数类型的移动构造签名是 &lt;code&gt;T(T&amp;#x26;&amp;#x26;)&lt;/code&gt;（非常量），因此会退化为拷贝。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const std::string s = &quot;hi&quot;;
std::string t = std::move(s); // 通常走拷贝，而不是移动
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;移动后对象仍然有效但处于“资源被转移”的状态&lt;/strong&gt;，只保证可析构/可赋新值，不保证内容。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;结论&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;右值不等于必须 &lt;code&gt;std::move&lt;/code&gt;。临时对象、返回值、隐式移动、&lt;code&gt;emplace&lt;/code&gt;/工厂函数、&lt;code&gt;move_iterator&lt;/code&gt; 等都能产生/利用右值。&lt;/li&gt;
&lt;li&gt;当且仅当&lt;strong&gt;你手里是一个左值且你确实要把它的资源转移&lt;/strong&gt;时，才需要写 &lt;code&gt;std::move&lt;/code&gt;；模板里用 &lt;code&gt;std::forward&lt;/code&gt; 保留原本的值类别。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;158. 内存上的问题：一种代码有好几种写法，他可能实现的功能都差不多，但是性能可能会有很大的差异，关于这点具体有什么要注意的地方，或者说你有什么要分享给我的？&lt;/h2&gt;
&lt;h3&gt;1) 数据布局（决定缓存命中）&lt;/h3&gt;
&lt;p&gt;高性能往往从“内存长相”开始：把 AoS（&lt;code&gt;struct{...}&lt;/code&gt; 的数组）改成 SoA（每个字段单独一块连续数组），让 CPU 一次线性扫到成批需要的字段，利于向量化与硬件预取；把“总被一起用”的字段挪到相邻位置，把大而冷的成员（如 &lt;code&gt;std::string&lt;/code&gt;/&lt;code&gt;std::vector&lt;/code&gt;）拆出去，形成“热-冷分离”，减少不必要的 cache line 装载；按自然对齐排布字段，避免错误使用 &lt;code&gt;#pragma pack&lt;/code&gt; 或位域导致跨行和非对齐访问；利用小对象优化（SBO）减少堆分配，但要盯住对象尺寸别超过 SBO 阈值；在构造路径用 &lt;code&gt;emplace&lt;/code&gt;/拷贝省略、在读路径用 &lt;code&gt;string_view&lt;/code&gt;/&lt;code&gt;span&lt;/code&gt; 等无拷贝视图（同时记住生命周期边界），这些都能显著降低 L1/L2 Miss 和内存带宽压力。&lt;/p&gt;
&lt;h3&gt;2) 分配与生命周期（降低 malloc/free 压力）&lt;/h3&gt;
&lt;p&gt;频繁的小分配是吞吐杀手：容器预留容量（&lt;code&gt;vector::reserve&lt;/code&gt;/&lt;code&gt;string::reserve&lt;/code&gt;）能一次到位，避免指数扩容的反复搬运；对“批量创建—一次性丢弃”的工作负载（解析、编译、RPC）用 &lt;strong&gt;Arena/Pool&lt;/strong&gt;，把释放从“逐个 free”变成“批量 reset”；用 PMR（&lt;code&gt;polymorphic_allocator&lt;/code&gt;）或自定义分配器，让一批相关容器共享内存资源，减少碎片和锁争用；同型小对象密集创建销毁用 slab/pool 固定大小块加速；跨层传递数据优先“指针/视图”而非拷贝（I/O 路径上用 &lt;code&gt;readv/writev&lt;/code&gt;、&lt;code&gt;sendfile&lt;/code&gt;、&lt;code&gt;splice&lt;/code&gt;、&lt;code&gt;mmap&lt;/code&gt;）实现零拷贝；写入容器使用 &lt;code&gt;emplace&lt;/code&gt; 或 &lt;code&gt;push_back(std::move(x))&lt;/code&gt;，把昂贵的深拷贝变为常数级的指针交换。&lt;/p&gt;
&lt;h3&gt;3) Cache/CPU 友好（L1/L2/TLB/分支）&lt;/h3&gt;
&lt;p&gt;让访问“像读磁带”而不是“打地鼠”：尽量把热点算法改成顺序扫描，超大数组分块（tiling）匹配 L1/L2 容量；避免链表/指针森林等随机追指，用扁平数组+索引（压平）减少跳转；可预测的遍历可加 &lt;code&gt;__builtin_prefetch&lt;/code&gt; 让硬件提前搬运；大数据吞吐可选 2MB hugepage 降 TLB Miss；分支方面把常走路径写成“卫语句”提早返回，结合 likely/unlikely 提示，减少 BTB 失误；能内联就别虚函数，必要时去虚化（CRTP/静态多态）；数据已 SoA 时再开启 &lt;code&gt;-O3 -march=native&lt;/code&gt;，很多循环可自动向量化，或用 intrinsics 明确使用 SIMD 指令，I-cache 命中与指令级并行都能上一个台阶。&lt;/p&gt;
&lt;h3&gt;4) 并发与假共享（false sharing）&lt;/h3&gt;
&lt;p&gt;多线程下的“无形开销”常来自 cache line 抖动：对不同线程各自频繁写的计数/状态，结构体上用 &lt;code&gt;alignas(std::hardware_destructive_interference_size)&lt;/code&gt;（常见 64B）或手动填充把它们“隔一行”；队列/环形缓冲尽量批处理 push/pop 降低同步频率，避免在共享结构上长时间自旋；跨 NUMA 机器要做“first-touch”初始化并绑定线程亲和，尽量让线程就近访问本地内存；原子操作别一律用 &lt;code&gt;seq_cst&lt;/code&gt;，看依赖选择 &lt;code&gt;release/acquire/relaxed&lt;/code&gt;，弱化内存屏障带来的流水线冲刷；高争用路径可以用 MPSC/SPSC 队列、分片计数（per-thread 计数+汇总）等结构，既稳又快。&lt;/p&gt;
&lt;h3&gt;5) I/O 与系统层（与页缓存配合）&lt;/h3&gt;
&lt;p&gt;顺序大读可用 &lt;code&gt;mmap&lt;/code&gt; 配合 &lt;code&gt;madvise(MADV_SEQUENTIAL)&lt;/code&gt; 让内核预读更积极，随机大写用 &lt;code&gt;pwrite&lt;/code&gt; 加 &lt;code&gt;posix_fadvise(DONTNEED)&lt;/code&gt; 避免脏页把缓存顶爆；网络/磁盘路径善用零拷贝原语（&lt;code&gt;sendfile&lt;/code&gt;、&lt;code&gt;splice&lt;/code&gt;、&lt;code&gt;readv/writev&lt;/code&gt;）减少用户态-内核态往返与内存拷贝；对延迟敏感的落盘要意识到 &lt;code&gt;fsync&lt;/code&gt;/barrier 极贵，采用写前日志+组提交（group commit）把多次同步合并成一次；离线/批处理任务可显式调大 readahead，在线请求则要避免“把冷数据抖进页缓存”影响热点；若 I/O 成为瓶颈，优先做批量与合并（coalescing），其次再考虑线程/异步模型切换。&lt;/p&gt;
&lt;h3&gt;6) 语言/库层面的易错点（以 C++ 为例）&lt;/h3&gt;
&lt;p&gt;容器与抽象的选择影响巨大：中小规模、读多改少的映射用 &lt;strong&gt;flat_map&lt;/strong&gt;（排序向量 + 二分）常优于 &lt;code&gt;unordered_map&lt;/code&gt;/&lt;code&gt;map&lt;/code&gt;，因为 cache 友好且无哈希/指针追逐；&lt;code&gt;shared_ptr&lt;/code&gt; 的原子引用计数在高频路径很贵，能用 &lt;code&gt;unique_ptr&lt;/code&gt; 别上共享，或采用侵入式计数/分离控制块减少原子写；循环中逐个 &lt;code&gt;erase&lt;/code&gt; 容器元素会导致 O(n²) 搬运，改为“打标签→一次性 &lt;code&gt;stable_partition&lt;/code&gt;/&lt;code&gt;erase_if&lt;/code&gt;”；&lt;code&gt;std::function&lt;/code&gt; 捕获大对象导致隐式堆分配，性能关键路径用模板/可调用对象直传或小型闭包；异常不应走热路径（抛掷与栈展开昂贵且破坏预测），把可预期分支改成状态返回或 &lt;code&gt;expected&lt;/code&gt; 类型更稳。&lt;/p&gt;
&lt;h3&gt;7) 度量方法（别猜，用数据说话）&lt;/h3&gt;
&lt;p&gt;优化从测量开始：先用 &lt;code&gt;perf record -g&lt;/code&gt; + 火焰图找前两大热点（80/20 原则），再用 &lt;code&gt;perf stat -e cycles,instructions,LLC-load-misses,branch-misses&lt;/code&gt; 看指令/CPI/缓存/分支画像；需要定位缓存行为时跑 &lt;code&gt;valgrind --tool=cachegrind&lt;/code&gt; 得到每函数/每行的 I/D-cache 失效率；内存分配与泄漏用 &lt;code&gt;heaptrack&lt;/code&gt; 或 Valgrind Massif 观察分配次数与峰值；遇到偶发长尾，结合 &lt;code&gt;perf top&lt;/code&gt; 抽样、反复 &lt;code&gt;gdb -p&lt;/code&gt; 抓多份线程栈，或上 eBPF/系统跟踪（lttng、bcc）给出系统调用与调度视角；所有实验都要可复现（固定数据/随机种子/并发度）并记录参数与版本，用基线对比量化收益。&lt;/p&gt;
&lt;h3&gt;8) 一个“改法对比”的实操范式&lt;/h3&gt;
&lt;p&gt;将“慢但易写”的实现按层次替换：①把链表/树改为 &lt;code&gt;vector&lt;/code&gt; 扁平结构并 &lt;code&gt;reserve&lt;/code&gt;；②把结构体数组拆成 SoA，循环里只取必要字段；③移除虚派发，改模板/策略类让编译器内联；④把元素处理改成块内循环（block size≈几 KB）以提升局部性；⑤若算法是简单算子（加减比较），尝试向量化（自动或 intrinsics）并适当 &lt;code&gt;prefetch&lt;/code&gt; 下一块；⑥若还不够，把内存管理切到 arena/池、把 I/O 改为批量/零拷贝。每一步做小型 A/B，对照 &lt;code&gt;perf stat&lt;/code&gt; 与火焰图，定位收益来自哪一层，避免“盲目堆技巧”。&lt;/p&gt;
&lt;h3&gt;9) 优先级与工作流程（落地打法）&lt;/h3&gt;
&lt;p&gt;别想一次把所有细节做满：先跑基准拿火焰图，&lt;strong&gt;只盯 Top2 热点&lt;/strong&gt;；先改数据布局（SoA/压平/&lt;code&gt;reserve&lt;/code&gt;），通常直接带来 2×—10×；若热点在并发/锁上，优先拆分成 per-thread 数据加批处理；若热点在 I/O，同步落盘合并到后台队列并组提交；每改一次都跑同一基准核实收益与正确性（单测+长时间 stress），收益达标就收工并写下原因/结论，避免“继续优化反而回退”；最后把关键开关做成可配置（SBO 阈值、块大小、是否 hugepage 等），便于在不同机器与负载上快速调参。&lt;/p&gt;
&lt;h2&gt;159. 还有哪些对内存强相关的且影响性能的点？&lt;/h2&gt;
&lt;h3&gt;高性能分配器的选择与争用&lt;/h3&gt;
&lt;p&gt;系统默认的 &lt;code&gt;malloc&lt;/code&gt;（如 glibc malloc）在多线程下容易产生锁争用与碎片。服务端场景优先试 &lt;code&gt;jemalloc&lt;/code&gt; 或 &lt;code&gt;tcmalloc&lt;/code&gt;：它们用线程局部/分级缓存降低加锁和跨核流量，明显减少分配/释放抖动。配合对象池/arena，把“高频小对象”从通用分配器挪走，进一步降低碎片与 TLB 压力。排查方法：统计分配次数与尺寸分布，观察 CPU 在 &lt;code&gt;malloc/free&lt;/code&gt; 上的火焰图占比。&lt;/p&gt;
&lt;h3&gt;缺页开销（minor/major fault）与预热&lt;/h3&gt;
&lt;p&gt;初次触碰页会触发 minor fault，冷热数据回收后再触碰可能变 major fault（磁盘 I/O），两者都能拉高尾延迟。批量初始化（first-touch）把数据预热到内存；对大块内存启动阶段做“线性预读”或后台预热，把故障集中到启动期而不是请求关键路径。&lt;/p&gt;
&lt;h3&gt;THP/大页与 TLB 命中&lt;/h3&gt;
&lt;p&gt;透明大页（Transparent Huge Pages）或显式 2MB/1GB 大页能显著降低 TLB miss，适合大数组/长循环。但 THP 的&lt;strong&gt;自动折叠&lt;/strong&gt;在写放大场景可能抖；对延迟敏感业务考虑手动 &lt;code&gt;madvise(MADV_HUGEPAGE)&lt;/code&gt; 或显式大页分配，并监控 THP 命中率与折叠开销。&lt;/p&gt;
&lt;h3&gt;NUMA 本地性与跨节点访问&lt;/h3&gt;
&lt;p&gt;跨 NUMA 读写等同“远程内存”，延迟/带宽大幅劣化。采用“线程绑核 + first-touch 初始化”，让每个工作线程初始化并处理自己的数据分区；共享结构按节点分片，避免频繁跨节点写。必要时用 &lt;code&gt;numactl&lt;/code&gt;/&lt;code&gt;mbind&lt;/code&gt; 固定内存到本地节点，监控远程访问比例。&lt;/p&gt;
&lt;h3&gt;写分配（write-allocate）与非临时写&lt;/h3&gt;
&lt;p&gt;普通写 miss 会先把整行读入（write-allocate），对“仅写一次”的大数组是浪费。能预测是“流式一次写”的路径可用非临时存储（如 &lt;code&gt;_mm_stream*&lt;/code&gt; 或 &lt;code&gt;std::hardware_constructive_interference_size&lt;/code&gt; 配合布局优化），减少读放大；相反，对会被很快读到的数据保留默认策略更好。&lt;/p&gt;
&lt;h3&gt;预取策略（硬件+软件）&lt;/h3&gt;
&lt;p&gt;硬件预取器偏好线性/规律访问。跨 stride 大、间隔不稳定、间接寻址时就会失效。把循环改成规则访问（SoA/压平/分块），在稳定模式下用 &lt;code&gt;__builtin_prefetch(addr, rw, locality)&lt;/code&gt; 提前装入下一块。别到处乱加预取：只在&lt;strong&gt;高 cache miss 的热点&lt;/strong&gt;里加，且要基准验证。&lt;/p&gt;
&lt;h3&gt;拷贝路径与 &lt;code&gt;memcpy/memset&lt;/code&gt; 微优化&lt;/h3&gt;
&lt;p&gt;反复小拷贝（几十到几百字节）会吃掉大量周期。把“拷贝-处理-丢弃”的流水改成就地处理或视图（&lt;code&gt;string_view&lt;/code&gt;/&lt;code&gt;span&lt;/code&gt;），把多次小拷贝合并为一次大拷贝；初始化大对象尽量延迟到真正需要时，避免“无意义 memset”。编译器通常会内联小 &lt;code&gt;memcpy&lt;/code&gt;，但跨 cache line、未对齐时收益骤降，关注结构体对齐与大小。&lt;/p&gt;
&lt;h3&gt;COW（写时复制）与 fork 的隐形成本&lt;/h3&gt;
&lt;p&gt;使用 COW 的 fork+exec 模式在父进程频繁写（尤其是大堆）时会触发&lt;strong&gt;大量页级写时复制&lt;/strong&gt;，造成 TLB 抖动与页故障。上线前把大对象移动到子进程初始化阶段，或用 &lt;code&gt;posix_spawn&lt;/code&gt;/&lt;code&gt;vfork&lt;/code&gt; 降低 COW 写放大；把“写多”的共享页分离到单独映射，避免 fork 后的写爆炸。&lt;/p&gt;
&lt;h3&gt;页回收策略与 swappiness/overcommit&lt;/h3&gt;
&lt;p&gt;内核回收策略不合适，会让热点页被回收、触发频繁 major fault。对在线服务，降低 swappiness、避免与大批量离线作业争内存；限制 overcommit 或为大分配使用 &lt;code&gt;mmap(MAP_NORESERVE)&lt;/code&gt; 的权衡清晰可控。监控 &lt;code&gt;pgfault/pgmajfault&lt;/code&gt;、&lt;code&gt;kswapd&lt;/code&gt; CPU 占比来发现问题。&lt;/p&gt;
&lt;h3&gt;缓存一致性与跨核写放大&lt;/h3&gt;
&lt;p&gt;多核共享数据频繁写会触发 MESI/CHI 的&lt;strong&gt;行失效风暴&lt;/strong&gt;。把“写多”的计数/队列分片为 per-thread/per-core，再做周期性汇总；写少读多的数据用 RCU/复制-更新（copy-on-write）模式，把写入改成构建新副本后原子切换，避免不断对共享行做写入。&lt;/p&gt;
&lt;h3&gt;对象大小与 cache line 对齐&lt;/h3&gt;
&lt;p&gt;让热点对象恰好“装进一行”能把命中率拉满；超过一行就会带来多次装载。对热结构体做字段重排/拆分，让“常用字段”落在前 64B 内；需要并发写隔离的成员，用 &lt;code&gt;alignas(64)&lt;/code&gt; 单独放一行避免假共享。&lt;/p&gt;
&lt;h3&gt;容器增长策略与重新散列&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;unordered_map&lt;/code&gt; 重hash 与 &lt;code&gt;vector&lt;/code&gt; 扩容搬运都可能成为尖刺。对“已知规模”的工作负载在构造时给足 &lt;code&gt;reserve(N)&lt;/code&gt;/&lt;code&gt;rehash(N)&lt;/code&gt;；对长寿命容器采用&lt;strong&gt;几何增长&lt;/strong&gt;的默认策略通常更优，但对短生命周期/可预估尺寸的容器，改为一次性预留最省。&lt;/p&gt;
&lt;h3&gt;页着色/冷热分离与指令缓存&lt;/h3&gt;
&lt;p&gt;除了数据缓存，I-cache 也会成为瓶颈。把“热循环”与“慢路径/错误处理”分离到不同函数，减少 I-cache 抖动；数据侧做冷热分离，把日志/元信息等冷字段挪出热点结构。大代码基下，开启 LTO/PGO（基于配置文件优化）让编译器按真实热点布局代码和数据。&lt;/p&gt;
&lt;h3&gt;内存屏障与原子序语义的成本&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;seq_cst&lt;/code&gt; 在多核上会导致全局序列化，性能代价极高。按需选择 &lt;code&gt;acquire/release/relaxed&lt;/code&gt;，用更弱的序保证实现正确性；对复合操作用“单生产者/单消费者”结构避免重栅栏；跨核通信用一次性发布-订阅（&lt;code&gt;std::atomic&amp;#x3C;T*&gt;&lt;/code&gt; 指针发布 + 不变量）替代频繁的强序列化。&lt;/p&gt;
&lt;h3&gt;压缩/解压与解引用延迟的权衡&lt;/h3&gt;
&lt;p&gt;在“内存带宽”受限的场景，用轻量压缩（如 LZ4、Zstd 低等级）把冷数据压缩后驻留内存，减少带宽与 cache 占用；热路径先解压再批量处理，往往比直接扫未压缩冷数据更快。关键是分层：热数据常驻、温数据轻压缩、冷数据更高压缩。&lt;/p&gt;
&lt;h3&gt;监控与门槛值（阈值要暴露为配置）&lt;/h3&gt;
&lt;p&gt;把关键内存参数做成可调：分配器种类、arena/池块大小、对象池上限、容器 reserve 值、块处理大小（tile size）、是否启用 THP、是否对齐到 cache line。不同机器与负载特征差异巨大，&lt;strong&gt;参数可调 + 基准脚本&lt;/strong&gt;比硬编码更能长久保证性能。&lt;/p&gt;
&lt;h2&gt;160. 你了解过哪些锁（或同步机制）？这些锁的应用场景分别是哪些？&lt;/h2&gt;
&lt;h3&gt;🔥互斥锁（mutex）/ 递归锁 / 带超时的 mutex&lt;/h3&gt;
&lt;p&gt;最常用的排它同步；&lt;strong&gt;临界区较长、需要睡眠等待时首选阻塞型 mutex&lt;/strong&gt;。递归锁仅在“同线程可重入”老代码里兜底，能不用尽量不用；带超时用在避免死等（比如调用外部组件）或做降级/回退策略。&lt;/p&gt;
&lt;h3&gt;自旋锁（spinlock）/ ticket spinlock&lt;/h3&gt;
&lt;p&gt;临界区极短、线程之间切换成本高或在内核/中断上下文中使用；适合“锁住就几条指令”的场景。若临界区可能阻塞或调度，禁止使用；在多核竞争高时用 ticket/队列式自旋锁减少饥饿。&lt;/p&gt;
&lt;h3&gt;读写锁（rwlock、shared_mutex）&lt;/h3&gt;
&lt;p&gt;读多写少；读者共享、写者独占。写者饥饿要注意（选择公平策略或写优先）；若读临界区很短、竞争高，rwlock 可能比 mutex 还慢，此时更适合 &lt;strong&gt;RCU/版本化读&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;条件变量（condvar）/ 事件（event/futex）&lt;/h3&gt;
&lt;p&gt;等待“状态变化”的典型工具：生产者-消费者、工作队列、状态机推进等。要配合外层 mutex；避免“丢信号”靠“while + 条件检查”。Linux 用户态可用 &lt;code&gt;futex&lt;/code&gt; 走快速路径。&lt;/p&gt;
&lt;h3&gt;信号量（semaphore、counting/binary）&lt;/h3&gt;
&lt;p&gt;计数控制并发度（连接池、限流“令牌桶”），或跨进程同步（POSIX sem）。和条件变量相比，信号量更像“资源票据”，支持先 &lt;code&gt;post&lt;/code&gt; 后 &lt;code&gt;wait&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;原子操作（atomic）与内存序（relaxed/acquire/release/seq_cst）&lt;/h3&gt;
&lt;p&gt;构建轻量同步或无锁结构的基石：计数器、标志位、一次性发布。选择恰当的内存序能显著降低屏障成本：大多场景 &lt;code&gt;release/acquire&lt;/code&gt; 足够，&lt;code&gt;seq_cst&lt;/code&gt; 少用。&lt;/p&gt;
&lt;h3&gt;RCU（Read-Copy-Update）/ Epoch 回收 / Hazard Pointers&lt;/h3&gt;
&lt;p&gt;读密集写稀少：读路径无锁、写时复制并原子切换指针；回收由 epoch/hazard 保障安全。典型用于只追加或少量更新的配置、路由表、元数据快照。&lt;/p&gt;
&lt;h3&gt;Seqlock（序列锁）/ 版本化读&lt;/h3&gt;
&lt;p&gt;读方乐观读取两次版本号一致即成功，写方独占更新并递增版本。适合写少且读可重试的结构（时间戳、统计信息）；缺点是读可能反复重试。&lt;/p&gt;
&lt;h3&gt;屏障（barrier）/ 栅栏&lt;/h3&gt;
&lt;p&gt;阶段性并行任务的“对齐点”，常见在并行算法的阶段切换；要避免过度细分导致闲等。&lt;/p&gt;
&lt;h3&gt;锁分片 / 分区锁（sharding / striping）&lt;/h3&gt;
&lt;p&gt;把一个大热点拆成多把锁（或 per-core/per-thread 数据），大幅降低竞争：哈希桶、分片计数器、分段 LRU。&lt;/p&gt;
&lt;h3&gt;文件锁 / 进程间锁（flock/fcntl、named semaphore、共享内存 + futex）&lt;/h3&gt;
&lt;p&gt;跨进程协调：单实例守护、批处理互斥、文件轮转。注意 &lt;code&gt;flock&lt;/code&gt; 和 &lt;code&gt;fcntl&lt;/code&gt; 语义差异与 NFS 兼容性。&lt;/p&gt;
&lt;h3&gt;分布式锁（ZK/etcd/Redis RedLock）&lt;/h3&gt;
&lt;p&gt;跨主机/服务实例的互斥：任务选主、全局限流。要考虑 &lt;strong&gt;租约（TTL）&lt;/strong&gt;、时钟/网络分区与“最少一次/至多一次”的副作用，尽量设计成可重入、幂等的业务操作。&lt;/p&gt;
&lt;h2&gt;161. 你遇到最复杂的多线程场景是什么？你具体怎么解决？对于你今后在分析问题有什么考量？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;代表性案例（读多写少的热点索引 + 多生产者/多消费者队列 + NUMA）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;症状：QPS 上不去且尾延迟抖动。Perf/火焰图显示 30% CPU 烧在 &lt;code&gt;pthread_mutex_lock&lt;/code&gt;，锁等待和 cache miss 居高；&lt;code&gt;top -H&lt;/code&gt; 与 &lt;code&gt;thread apply all bt&lt;/code&gt; 暴露热点是“全局索引读路径用同一把大锁”，加上队列端有假共享（多个线程写同一 cache line 的队头/队尾），在双路 NUMA 上跨节点访问严重。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;诊断步骤&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1）&lt;strong&gt;稳定复现与基准&lt;/strong&gt;：固定负载与数据集，记录 P50/P99、CPU/LLC miss/TLB miss。&lt;/p&gt;
&lt;p&gt;2）&lt;strong&gt;轮廓画像&lt;/strong&gt;：&lt;code&gt;perf record -g&lt;/code&gt; + 火焰图定位锁热点、&lt;code&gt;perf stat&lt;/code&gt; 看 CPI/branch/TLB；TSan 验证无数据竞争。&lt;/p&gt;
&lt;p&gt;3）&lt;strong&gt;锁分析&lt;/strong&gt;：统计加锁点的平均持有时间与竞争度（埋点或 eBPF uprobes），确认“读路径被大锁覆盖”。&lt;/p&gt;
&lt;p&gt;4）&lt;strong&gt;硬件/拓扑&lt;/strong&gt;：&lt;code&gt;numactl --hardware&lt;/code&gt; 查看节点，&lt;code&gt;hwloc-ls&lt;/code&gt; 看 CPU/内存拓扑，采样远程内存比例。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重构与优化&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读路径去锁化（RCU/版本化）&lt;/strong&gt;：把热索引改为“不可变快照 + 写时复制”，读线程直接读当前指针；写线程构建新副本并一次性切换。读延迟大幅降低且几乎无竞争。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分片锁 + 局部化&lt;/strong&gt;：对仍需加锁的子结构按 key 哈希分段（64/128 把锁），并将每段数据绑定到 NUMA 节点；读写都尽量落在同核/同节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;队列无锁化与假共享修复&lt;/strong&gt;：MPSC 改为 &lt;strong&gt;per-producer 环 + 单汇聚&lt;/strong&gt; 或使用经过验证的 MPMC（如 moodycamel），并对头尾指针用 &lt;code&gt;alignas(64)&lt;/code&gt; 隔离，避免 false sharing。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批处理与背压&lt;/strong&gt;：消费者批量出队、合并写 I/O（group commit），用条件变量或信号量替自旋，减少无效轮询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存管理&lt;/strong&gt;：对象池/arena 降低 &lt;code&gt;malloc/free&lt;/code&gt; 抖动；对大数组启用 hugepage 降 TLB miss。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;锁顺序与死锁预案&lt;/strong&gt;：明确全局锁级别与获取顺序，跨组件使用 &lt;code&gt;try_lock&lt;/code&gt; + 回退策略，配合超时与诊断日志避免“无声死锁”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;效果与验收&lt;/strong&gt;：读路径 P99 从毫秒级降到百微秒内，锁等待时间下降 &gt;90%，LLC/TLB miss 显著减少；在 NUMA 绑定后远程访问下降，尾延迟收敛。留下一套基准与回归测试（压力 + 混沌/抖动），并把关键参数（分片数、批大小、是否 RCU）做成可调。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;今后分析问题的考量清单&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;选择最弱同步&lt;/strong&gt;：能原子就不加锁，能分片就不全局，能读重试就不阻塞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读写比/临界区长度&lt;/strong&gt;：读多写少优先 RCU/版本化；短临界区可自旋，长临界区用阻塞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;竞争度与可分割性&lt;/strong&gt;：先量化锁竞争，再考虑分区/分层；避免把“不可分”的资源放进热路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NUMA 与内存亲和&lt;/strong&gt;：first-touch、线程绑核、数据分区；跨节点代价要显式评估。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可恢复与可观测&lt;/strong&gt;：超时、降级、try_lock 回退，日志能定位“谁拿着锁、持有多久”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;正确性优先&lt;/strong&gt;：无锁/乐观结构要配套内存回收（epoch/hazard）与 ABA 防护；单测 + TSan/模型检查覆盖边界。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;度量驱动&lt;/strong&gt;：每一次改动都用同一基准验证 P50/P99 与 CPU/缓存计数，避免“感觉良好型优化”&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;162. 手撕无锁队列&lt;/h2&gt;
&lt;p&gt;设计要点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;结构&lt;/strong&gt;：单向链表 + 虚拟哑元（dummy）头结点；两个原子指针：&lt;code&gt;head&lt;/code&gt;、&lt;code&gt;tail&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;入队（enqueue）&lt;/strong&gt;：尝试把 &lt;code&gt;tail-&gt;next&lt;/code&gt; 从 &lt;code&gt;nullptr&lt;/code&gt; CAS 成新结点；成功后再把 &lt;code&gt;tail&lt;/code&gt; 向后推进（CAS）。若 &lt;code&gt;tail-&gt;next&lt;/code&gt; 非空，先帮助推进 &lt;code&gt;tail&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;出队（dequeue）&lt;/strong&gt;：读 &lt;code&gt;head&lt;/code&gt;、&lt;code&gt;tail&lt;/code&gt;、&lt;code&gt;head-&gt;next&lt;/code&gt;；若 &lt;code&gt;head == tail&lt;/code&gt; 且 &lt;code&gt;next==nullptr&lt;/code&gt; 队列空；否则把 &lt;code&gt;head&lt;/code&gt; CAS 到 &lt;code&gt;next&lt;/code&gt;，返回 &lt;code&gt;next-&gt;value&lt;/code&gt;。并&lt;strong&gt;延迟释放旧头&lt;/strong&gt;（见回收）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ABA &amp;#x26; 回收&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;ABA：由于我们只单向推进，不会把已删除的节点再插回队列，可用&lt;strong&gt;epoch-based reclamation (EBR)&lt;/strong&gt; 避免悬挂回收；或用 Hazard Pointers（更复杂）。&lt;/li&gt;
&lt;li&gt;这里实现一个&lt;strong&gt;轻量 EBR&lt;/strong&gt;：每线程进入“临界区”时标记活跃 epoch，退出现有“安全点”；只有当所有活跃线程都不可能再见到某些旧节点时，才回收它们。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：真实生产可引入成熟 HP/EBR 库；此处给出教学用、可工作的极简 EBR。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;163. 手撕并发哈希表，如何设计细粒度锁&lt;/h2&gt;
&lt;h3&gt;设计要点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;结构&lt;/strong&gt;：分 &lt;code&gt;S&lt;/code&gt; 个&lt;strong&gt;分片（segment/stripe）&lt;/strong&gt;；每片包含若干桶（拉链法），每片一把 &lt;code&gt;shared_mutex&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查找&lt;/strong&gt;：读锁该片，遍历桶链。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插入/更新/删除&lt;/strong&gt;：写锁该片，操作对应桶。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩容&lt;/strong&gt;（再哈希）：
&lt;ul&gt;
&lt;li&gt;简洁稳妥做法：&lt;strong&gt;全局写锁&lt;/strong&gt;（短时）阻止并发写，重建 &lt;code&gt;buckets&lt;/code&gt; 并迁移；查找可用读锁和版本号防止看到中间态。&lt;/li&gt;
&lt;li&gt;或做&lt;strong&gt;增量迁移&lt;/strong&gt;（复杂）：分阶段把旧桶迁到新表，读写需能同时查两处。这里给&lt;strong&gt;工程友好&lt;/strong&gt;的“全局写锁扩容”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负载因子&lt;/strong&gt;：达到阈值（如 0.75）触发 &lt;code&gt;rehash(2x)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;：锁分片比“每桶一把锁”更经济，也更容易控制扩容一致性；热键均衡取决于哈希质量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// striped_hash_map.hpp
#pragma once
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;shared_mutex&gt;
#include &amp;#x3C;optional&gt;
#include &amp;#x3C;functional&gt;
#include &amp;#x3C;atomic&gt;
#include &amp;#x3C;list&gt;
#include &amp;#x3C;utility&gt;

template&amp;#x3C;class K, class V,
         class Hash = std::hash&amp;#x3C;K&gt;,
         class KeyEq = std::equal_to&amp;#x3C;K&gt;&gt;
class StripedHashMap {
    struct BucketKV { K k; V v; };

    struct Segment {
        mutable std::shared_mutex mtx;
        std::vector&amp;#x3C;std::list&amp;#x3C;BucketKV&gt;&gt; buckets; // separate chaining
        size_t size = 0; // 元素数（仅此 segment）
        Segment(size_t nb=8): buckets(nb) {}
    };

    std::vector&amp;#x3C;Segment&gt; segs;
    Hash hasher;
    KeyEq eq;
    std::atomic&amp;#x3C;size_t&gt; total{0};
    float max_load = 0.75f;

    // 一把全局写锁，仅用于 rehash（频度低）与全局操作
    mutable std::shared_mutex global_rw;

public:
    explicit StripedHashMap(size_t segments = 64, size_t buckets_per_seg = 8)
      : segs(segments, Segment(buckets_per_seg)) {}

    // 不存在则插入，存在则更新，返回是否新插入
    bool upsert(const K&amp;#x26; k, const V&amp;#x26; v) {
        auto [seg_idx, b_idx] = index_of(k);
        Segment&amp;#x26; s = segs[seg_idx];
        {
            std::unique_lock lk(s.mtx);
            auto&amp;#x26; lst = s.buckets[b_idx];
            for (auto&amp;#x26; kv : lst) {
                if (eq(kv.k, k)) { kv.v = v; return false; }
            }
            lst.push_front(BucketKV{ k, v });
            ++s.size;
            ++total;
        }
        maybe_rehash(); // 放锁后尝试触发扩容
        return true;
    }

    // 如果键不存在则构造，存在则不变，返回 (引用, 是否新插入)
    template&amp;#x3C;class... Args&gt;
    std::pair&amp;#x3C;V&amp;#x26;, bool&gt; get_or_emplace(const K&amp;#x26; k, Args&amp;#x26;&amp;#x26;... args) {
        auto [seg_idx, b_idx] = index_of(k);
        Segment&amp;#x26; s = segs[seg_idx];
        {
            std::unique_lock lk(s.mtx);
            auto&amp;#x26; lst = s.buckets[b_idx];
            for (auto&amp;#x26; kv : lst) {
                if (eq(kv.k, k)) return { kv.v, false };
            }
            lst.emplace_front(BucketKV{ k, V(std::forward&amp;#x3C;Args&gt;(args)...) });
            ++s.size; ++total;
            return { lst.front().v, true };
        }
    }

    std::optional&amp;#x3C;V&gt; find(const K&amp;#x26; k) const {
        auto [seg_idx, b_idx] = index_of(k);
        Segment const&amp;#x26; s = segs[seg_idx];
        std::shared_lock lk(s.mtx);
        for (auto const&amp;#x26; kv : s.buckets[b_idx]) {
            if (eq(kv.k, k)) return kv.v;
        }
        return std::nullopt;
    }

    bool erase(const K&amp;#x26; k) {
        auto [seg_idx, b_idx] = index_of(k);
        Segment&amp;#x26; s = segs[seg_idx];
        std::unique_lock lk(s.mtx);
        auto&amp;#x26; lst = s.buckets[b_idx];
        for (auto it = lst.begin(); it != lst.end(); ++it) {
            if (eq(it-&gt;k, k)) {
                lst.erase(it);
                --s.size; --total;
                return true;
            }
        }
        return false;
    }

    size_t size() const noexcept { return total.load(std::memory_order_acquire); }

private:
    std::pair&amp;#x3C;size_t,size_t&gt; index_of(const K&amp;#x26; k) const {
        size_t h = hasher(k);
        size_t seg_idx = h % segs.size();
        size_t b_idx = (h / segs.size()) % segs[seg_idx].buckets.size();
        return { seg_idx, b_idx };
    }

    void maybe_rehash() {
        // 轻量判断是否需要扩容（近似全局负载）
        size_t n = total.load(std::memory_order_acquire);
        size_t total_buckets = 0;
        for (auto&amp;#x26; s : segs) total_buckets += s.buckets.size();
        if (static_cast&amp;#x3C;float&gt;(n) / static_cast&amp;#x3C;float&gt;(total_buckets) &amp;#x3C; max_load) return;

        // 全局写锁，避免与并发 find/upsert 的重入扩容冲突
        std::unique_lock gL(global_rw);
        // Double-check
        n = total.load(std::memory_order_acquire);
        total_buckets = 0;
        for (auto&amp;#x26; s : segs) total_buckets += s.buckets.size();
        if (static_cast&amp;#x3C;float&gt;(n) / static_cast&amp;#x3C;float&gt;(total_buckets) &amp;#x3C; max_load) return;

        // 扩容：每个 segment 加独占锁，重建桶并迁移
        for (auto&amp;#x26; s : segs) s.mtx.lock();
        auto unlock_all = [&amp;#x26;]{ for (auto&amp;#x26; s : segs) s.mtx.unlock(); };

        for (auto&amp;#x26; s : segs) {
            size_t new_nb = s.buckets.size() * 2;
            std::vector&amp;#x3C;std::list&amp;#x3C;BucketKV&gt;&gt; nb(new_nb);
            for (auto&amp;#x26; lst : s.buckets) {
                for (auto&amp;#x26; kv : lst) {
                    size_t h = hasher(kv.k);
                    size_t bidx = (h / segs.size()) % new_nb;
                    nb[bidx].push_front(std::move(kv));
                }
            }
            s.buckets.swap(nb);
        }
        unlock_all();
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/20250823-GIegg2.DiGfEmmR.png"/><enclosure url="/_astro/20250823-GIegg2.DiGfEmmR.png"/></item><item><title>2025.08.09 网易雷火笔试题</title><link>https://coooredump.github.io/blog/recruitment/20250809-netease</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/20250809-netease</guid><description>网易雷火 20250809 笔试解析</description><pubDate>Sat, 09 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;题解链接：https://mp.weixin.qq.com/s/b2qohQxoMJk8Kysswtc7mQ&lt;/p&gt;
&lt;p&gt;测评链接：https://niumacode.com/training/143&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 部门旅游&lt;/h2&gt;
&lt;p&gt;今年小师妹将再次组织大家进行一次精彩的部门旅游。为了确保每位员工能够顺利参与，现有三个不同的旅游团可供选择。每个旅游团的人数限制各不相同，员工可根据自己的喜好选择报名的旅游团。为了公平起见，系统将采用先到先得的原则进行处理。 报名规则如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每位员工可以根据喜好报名参加1到3个旅游团。&lt;/li&gt;
&lt;li&gt;报名时，如果首选的旅游团已满员，系统将自动尝试加入员工的次选旅游团。&lt;/li&gt;
&lt;li&gt;如果员工所选择的所有旅游团均已满员，则该员工无法参加此次旅游。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 N，表示员工的数量（1 ≤ N ≤ 10000）。&lt;/li&gt;
&lt;li&gt;第二行包含三个整数，分别表示旅游团 A、B、C 的最大名额（1 ≤ A, B, C ≤ 10000）。&lt;/li&gt;
&lt;li&gt;接下来的 N 行，每行包含：
&lt;ul&gt;
&lt;li&gt;一个字符串，表示员工的 ID（由字母和数字组成，并且互不相同） ID 长度 ≤ 13。&lt;/li&gt;
&lt;li&gt;一个整数 X（1 ≤ X ≤ 3），表示该员工选择的旅游团数量。&lt;/li&gt;
&lt;li&gt;X 个字符（A/B/C），表示员工选择的旅游团的优先级，优先级高的选择在前。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;按 A,B,C 三个团的顺序输出三行，每行一个整数，表示这个团最终的人数 a，接下来是 a 个字符串，表示进入这个团的员工 ID， 请按照报名顺序输出。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;4
2 1 1
zhang01 2 A B
chen01 1 B
li02 3 B C A
yang 2 B A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;2 zhang01 yang
1 chen01
1 li02
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;4
2 1 1
zhang01 2 A B
chen01 2 A B
li02 3 A B C
yang 2 B A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;2 zhang01 chen01
1 li02
0
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：模拟&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;string&gt;
using namespace std;

int main() {
    int employeeCount;
    cin &gt;&gt; employeeCount;
    
    int maxA, maxB, maxC;
    cin &gt;&gt; maxA &gt;&gt; maxB &gt;&gt; maxC;
    
    vector&amp;#x3C;string&gt; groupA, groupB, groupC;
    
    for (int i = 0; i &amp;#x3C; employeeCount; ++i) {
        string employeeID;
        int choiceCount;
        cin &gt;&gt; employeeID &gt;&gt; choiceCount;
        
        vector&amp;#x3C;char&gt; choices(choiceCount);
        for (auto&amp;#x26; choice : choices) {
            cin &gt;&gt; choice;
        }
        
        for (char choice : choices) {
            if (choice == &apos;A&apos; &amp;#x26;&amp;#x26; groupA.size() &amp;#x3C; maxA) {
                groupA.push_back(employeeID);
                break;
            }
            else if (choice == &apos;B&apos; &amp;#x26;&amp;#x26; groupB.size() &amp;#x3C; maxB) {
                groupB.push_back(employeeID);
                break;
            }
            else if (choice == &apos;C&apos; &amp;#x26;&amp;#x26; groupC.size() &amp;#x3C; maxC) {
                groupC.push_back(employeeID);
                break;
            }
        }
    }
    
    cout &amp;#x3C;&amp;#x3C; groupA.size();
    for (const string&amp;#x26; id : groupA) {
        cout &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; id;
    }
    cout &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    
    cout &amp;#x3C;&amp;#x3C; groupB.size();
    for (const string&amp;#x26; id : groupB) {
        cout &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; id;
    }
    cout &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    
    cout &amp;#x3C;&amp;#x3C; groupC.size();
    for (const string&amp;#x26; id : groupC) {
        cout &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; id;
    }
    cout &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 战力极差的最小值&lt;/h2&gt;
&lt;p&gt;在《无主星渊》的竞技宇宙中，来自各个星系的顶尖战队齐聚一堂，准备迎战传说中的最终 BOSS。每个星系派出了最精锐的战队，共 n 支战队，每支战队有 m 名成员，每位成员的战力值分别为 $p_1, p_2, ..., p_m$。为了组成最强的终极挑战队，你需要从每支战队中各选 1 名成员（共 n 人），但团队配合至关重要。经过无数次模拟战斗，联盟科学家发现：战力越均衡的团队，越能激发协同共鸣。因此，选拔规则如下：在所有可能的组队方案中，选择战力极差（最大值 - 最小值）最小的方案，确保团队以最平衡的状态迎战 BOSS。&lt;/p&gt;
&lt;p&gt;你的任务：计算所有可能的组队方案中，战力极差的最小值。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一行两个正整数 $n (0 &amp;#x3C; n ≤ 3000)$、$m (0 &amp;#x3C; m ≤ 100)$，分别表示队伍数量与每只战队中的成员数量。&lt;/li&gt;
&lt;li&gt;之后 $n$ 行，每行输入 $m$ 个数字 $p_i (0 &amp;#x3C; p_i &amp;#x3C; 1e9)$，代表战队中成员的战力值。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;所有可能的组队方案中，战力极差的最小值&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;1 1
1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;3 4
10 15 24 26
0 9 12 20
5 18 22 30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：滑动窗口&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们需要从 n 支战队中各选 1 名成员，组成一个 n 人团队。在所有可能的组队方案中，找到战力极差（最大值 - 最小值）最小的方案。&lt;/p&gt;
&lt;p&gt;解题思路&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;暴力法不可行&lt;/strong&gt;：直接枚举所有可能的组合（共 $m^n$ 种）显然不现实，因为 n 和 m 可能较大（n ≤ 3000，m ≤ 100）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排序 + 滑动窗口&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将每支战队的成员战力值排序。&lt;/li&gt;
&lt;li&gt;将所有战队的成员合并到一个列表，并记录每个成员所属的战队。&lt;/li&gt;
&lt;li&gt;对这个合并后的列表按战力值排序。&lt;/li&gt;
&lt;li&gt;使用滑动窗口，找到一个窗口，其中包含所有 n 支战队的至少一个成员，且窗口的极差最小。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;algorithm&gt;
#include &amp;#x3C;climits&gt;
#include &amp;#x3C;unordered_map&gt;
using namespace std;

int main() {
    int n, m;
    cin &gt;&gt; n &gt;&gt; m;
    vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; teams(n, vector&amp;#x3C;int&gt;(m));
    for (int i = 0; i &amp;#x3C; n; ++i) {
        for (int j = 0; j &amp;#x3C; m; ++j) {
            cin &gt;&gt; teams[i][j];
        }
        sort(teams[i].begin(), teams[i].end()); // 每支战队内部排序
    }

    // 合并所有成员，并记录所属战队
    vector&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; members; // (value, team_id)
    for (int i = 0; i &amp;#x3C; n; ++i) {
        for (int val : teams[i]) {
            members.emplace_back(val, i);
        }
    }
    // 按战力值排序
    sort(members.begin(), members.end());

    int left = 0;
    unordered_map&amp;#x3C;int, int&gt; team_count;
    int min_diff = INT_MAX;

    for (int right = 0; right &amp;#x3C; members.size(); ++right) {
        int team = members[right].second;
        team_count[team]++;

        // 当窗口包含所有战队时，尝试移动left
        while (team_count.size() == n) {
            int current_diff = members[right].first - members[left].first;
            if (current_diff &amp;#x3C; min_diff) {
                min_diff = current_diff;
            }
            // 移动left
            int left_team = members[left].second;
            team_count[left_team]--;
            if (team_count[left_team] == 0) {
                team_count.erase(left_team);
            }
            left++;
        }
    }

    cout &amp;#x3C;&amp;#x3C; min_diff &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 金矿采集&lt;/h2&gt;
&lt;p&gt;在《无主星渊》的太空战场中，玩家需操控飞船从起点 (S) 出发，在 n×m 的网格中以最短时间采集所有的金矿。飞船每次仅能向上下左右四个方向移动一个网格，金矿可以以任何先后顺序被采集，飞船到达金矿后可以选择立即采集也可以选择路过。一共有 k 个金矿，金矿初始的矿产值为 Xi，当飞船采集到 a(0 ≤ a ≤ k) 个金矿后，每移动一步，所有未被采集的金矿都会减少 a 点矿产值，当金矿的矿产值减少到 1 的时候将不再减小。需要你帮玩家算一下，玩家最多可以采集到金矿的总价值。&lt;/p&gt;
&lt;p&gt;网格包含以下元素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#&lt;/code&gt;：不可穿越的障碍物&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.&lt;/code&gt;：可自由航行的太空区域&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1~5&lt;/code&gt;：表示 $k$ 个金矿的编号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;S&lt;/code&gt;：飞船起点&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;首行：&lt;code&gt;n m k (1 ≤ n, m ≤ 50, 1≤ k ≤5)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;后续 $n$ 行，每行 $m$ 个字符表示 $n*m$ 的网格的信息&lt;/li&gt;
&lt;li&gt;最后一行是 $k$ 个整数，第 $i$ 个数表示编号为 $i$ (1 ≤ i ≤ k) 的金矿的初始矿产值 Xi (1 ≤ Xi ≤ 1000)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;玩家最多可以采集到的金矿总矿产值，数据保证所有金矿都可以到达&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;5 7 3
S.....3
##.....
..##...
..1....
2..#...
10 20 30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;41
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;4 4 1
....
.##.
.##.
S##1
100 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;100
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 3&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;5 7 3
S.....3
##.....
..##...
..1....
2..#...
40 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;42
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：BFS + 全排列枚举&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(1) 数据输入与预处理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读取网格地图，记录起点 &lt;code&gt;S&lt;/code&gt; 和金矿的位置（保存到 &lt;code&gt;positions&lt;/code&gt; 数组）。&lt;/li&gt;
&lt;li&gt;金矿编号为 &lt;code&gt;1&lt;/code&gt; 到 &lt;code&gt;goldCount&lt;/code&gt;，起点编号为 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(2) BFS 计算最短路径&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对每个金矿（包括起点）运行 BFS，计算到其他所有金矿的最短路径步数：
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;distances[i][j]&lt;/code&gt; 表示从位置 &lt;code&gt;i&lt;/code&gt; 到位置 &lt;code&gt;j&lt;/code&gt; 的最短步数。&lt;/li&gt;
&lt;li&gt;通过BFS遍历地图，跳过障碍物和边界。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(3) 全排列搜索最优顺序&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成所有金矿的排列顺序（&lt;code&gt;1&lt;/code&gt; 到 &lt;code&gt;goldCount&lt;/code&gt;的全排列）。&lt;/li&gt;
&lt;li&gt;对每种顺序计算总得分：
&lt;ol&gt;
&lt;li&gt;从起点（&lt;code&gt;last = 0&lt;/code&gt;）出发。&lt;/li&gt;
&lt;li&gt;对于第 &lt;code&gt;k&lt;/code&gt; 个金矿 &lt;code&gt;order[k]&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;累加步数惩罚：&lt;code&gt;minus += stepCount * k&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;计算当前金矿得分：&lt;code&gt;max(initialValues[order[k]] - minus, 1)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;更新当前位置 &lt;code&gt;last&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;记录所有顺序中的最大得分 &lt;code&gt;maxTotal&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

vector&amp;#x3C;string&gt; grid;
int rows, cols, goldCount;
const int INF = 0x3f3f3f3f;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    
    cin &gt;&gt; rows &gt;&gt; cols &gt;&gt; goldCount;
    grid.resize(rows);
    for (auto&amp;#x26; row : grid) {
        cin &gt;&gt; row;
    }
    
    vector&amp;#x3C;int&gt; initialValues(goldCount + 1);
    for (int i = 1; i &amp;#x3C;= goldCount; ++i) {
        cin &gt;&gt; initialValues[i];
    }
    
    vector&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; positions(goldCount + 1, {-1, -1});
    pair&amp;#x3C;int, int&gt; startPos;
    
    for (int i = 0; i &amp;#x3C; rows; ++i) {
        for (int j = 0; j &amp;#x3C; cols; ++j) {
            if (grid[i][j] == &apos;S&apos;) {
                startPos = {i, j};
            }
            else if (isdigit(grid[i][j])) {
                int goldId = grid[i][j] - &apos;0&apos;;
                positions[goldId] = {i, j};
            }
        }
    }
    
    auto bfs = [](int startX, int startY) {
        vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; dist(rows, vector&amp;#x3C;int&gt;(cols, INF));
        queue&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; q;
        q.push({startX, startY});
        dist[startX][startY] = 0;
        
        vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
        
        while (!q.empty()) {
            auto [x, y] = q.front();
            q.pop();
            
            for (auto&amp;#x26; dir : directions) {
                int newX = x + dir[0];
                int newY = y + dir[1];
                
                if (newX &amp;#x3C; 0 || newX &gt;= rows || newY &amp;#x3C; 0 || newY &gt;= cols) {
                    continue;
                }
                if (grid[newX][newY] == &apos;#&apos; || dist[newX][newY] != INF) {
                    continue;
                }
                
                dist[newX][newY] = dist[x][y] + 1;
                q.push({newX, newY});
            }
        }
        return dist;
    };
    
    positions[0] = startPos;
    int totalPoints = goldCount + 1;
    vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; distances(totalPoints, vector&amp;#x3C;int&gt;(totalPoints));
    
    for (int i = 0; i &amp;#x3C; totalPoints; ++i) {
        auto dist = bfs(positions[i].first, positions[i].second);
        for (int j = 0; j &amp;#x3C; totalPoints; ++j) {
            distances[i][j] = dist[positions[j].first][positions[j].second];
        }
    }
    
    vector&amp;#x3C;int&gt; order(goldCount);
    iota(order.begin(), order.end(), 1);
    int maxTotal = 0;
    
    do {
        int minus = 0, total = 0, last = 0;
        for (int i = 0; i &amp;#x3C; goldCount; ++i) {
            int currentGold = order[i];
            int stepCount = distances[last][currentGold];
            minus += stepCount * i;
            total += max(initialValues[currentGold] - minus, 1);
            last = currentGold;
        }
        maxTotal = max(maxTotal, total);
    } while (next_permutation(order.begin(), order.end()));
    
    cout &amp;#x3C;&amp;#x3C; maxTotal &amp;#x3C;&amp;#x3C; endl;
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 篮球游戏&lt;/h2&gt;
&lt;p&gt;你正在篮球场上与其他玩家玩一场游戏。你需要站在看台边，用推车接住从看台上扔下来的篮球。&lt;/p&gt;
&lt;p&gt;篮球上标有不同的积分，你接到后就获得了对应的积分。但是其中有一部分玩家他们会扔其他种类的球，如果你不小心接到了这些球，你就需要停在原地 3 秒。期间你只能等待时间过去，或者正好有球进入车筐中。如果你在停止期间又接到了非篮球的球类，不论之前你的停止时间还剩多少，它都会重新刷新为 3 秒。&lt;/p&gt;
&lt;p&gt;所有的球类都会在1  秒的时间里下落 1 格高度，而你也可以在 1s 时间里向左或者向右移动一格，或者不动。当车筐与球重合时，表示你接到了球，你可以在一个位置同时接到多个球。&lt;/p&gt;
&lt;p&gt;一开始，你可以选择任意的位置，那么你怎么规划你的移动路线，能够使得接到的球总积分最高呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一行有两个整数 n (1 ≤ n ≤ 100)，m (1 ≤ m ≤ 1000)，n 表示可以接到的球以及人可以站立的横向长度，m 表示扔出的球的总数&lt;/li&gt;
&lt;li&gt;接下来 m 行，每行四个整数 $v_i (0 ≤ v_i ≤ 1e5)，x_i (0 ≤ x_i &amp;#x3C; n)，y_i (0 &amp;#x3C; y_i ≤ 1000)，t_i (0 &amp;#x3C; t_i ≤ 1000)$，$v_i$ 表示球的积分，$x_i$ 表示物品的横向坐标，$y_i$ 表示物品的初始高度，$t_i$ 表示物品开始掉落的时间。当接收到 $v_i==0$ 的球时，会使你困在原地 3 秒，如果此时已经处于被困住的状态，则时间会重置为 3 秒&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;一个整数，表示可以接到的球的最大总积分&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;10 3
3 5 3 3
0 3 2 1
1 0 10 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;10 1
0 3 2 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：动态规划&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是一个动态规划问题，我们需要在时间和空间维度上规划接球路线，最大化获得的积分，同时处理&quot;定身&quot;状态的干扰。&lt;/p&gt;
&lt;p&gt;时间处理：所有球的下落时间被预处理为 &lt;code&gt;time_start + height&lt;/code&gt;，即球到达底部的时间。&lt;/p&gt;
&lt;p&gt;状态表示：&lt;code&gt;dp[pos][stun]&lt;/code&gt; 表示在位置 &lt;code&gt;pos&lt;/code&gt; 且剩余定身时间为 &lt;code&gt;stun&lt;/code&gt; 时的最大得分&lt;/p&gt;
&lt;p&gt;定身状态有 4 种可能：0(无定身),1,2,3&lt;/p&gt;
&lt;p&gt;状态转移：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无定身时：可以左移、右移或不动&lt;/li&gt;
&lt;li&gt;有定身时：只能不动，定身时间减 1&lt;/li&gt;
&lt;li&gt;接到非篮球 (v=0) 时：强制将定身时间设为 3&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

constexpr int MAX_TIME = 2000;
constexpr long long NEG_INF = -(1LL &amp;#x3C;&amp;#x3C; 60);

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int positions, events;
    cin &gt;&gt; positions &gt;&gt; events;

    // scores[time][pos]: 当前时刻和位置能获得的分数总和
    vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; scores(MAX_TIME + 1, vector&amp;#x3C;int&gt;(positions, 0));
    // traps[time][pos]: 是否存在非篮球，触发定身
    vector&amp;#x3C;vector&amp;#x3C;bool&gt;&gt; traps(MAX_TIME + 1, vector&amp;#x3C;bool&gt;(positions, false));

    for (int i = 0; i &amp;#x3C; events; ++i) {
        int value, pos, height, time_start;
        cin &gt;&gt; value &gt;&gt; pos &gt;&gt; height &gt;&gt; time_start;
        int landing_time = time_start + height;
        scores[landing_time][pos] += value;
        if (value == 0) {  // 非篮球触发定身
            traps[landing_time][pos] = true;
        }
    }

    // dp[pos][stun]: 最大得分，stun表示剩余定身秒数（0~3）
    vector&amp;#x3C;vector&amp;#x3C;long long&gt;&gt; dp(positions, vector&amp;#x3C;long long&gt;(4, NEG_INF));
    // 初始状态：任何位置未定身，分数为0
    for (int i = 0; i &amp;#x3C; positions; ++i) {
        dp[i][0] = 0;
    }

    long long answer = 0;

    for (int time = 0; time &amp;#x3C;= MAX_TIME; ++time) {
        vector&amp;#x3C;vector&amp;#x3C;long long&gt;&gt; next_dp(positions, vector&amp;#x3C;long long&gt;(4, NEG_INF));

        for (int pos = 0; pos &amp;#x3C; positions; ++pos) {
            for (int stun = 0; stun &amp;#x3C;= 3; ++stun) {
                long long curr_score = dp[pos][stun];
                if (curr_score == NEG_INF) continue;

                int new_stun = (stun == 0) ? 0 : stun - 1;

                // 如果没有被定身，可以移动
                if (stun == 0) {
                    if (pos &gt; 0) {
                        next_dp[pos - 1][new_stun] = max(next_dp[pos - 1][new_stun], 
                                                        curr_score + scores[time][pos - 1]);
                    }
                    if (pos + 1 &amp;#x3C; positions) {
                        next_dp[pos + 1][new_stun] = max(next_dp[pos + 1][new_stun], 
                                                        curr_score + scores[time][pos + 1]);
                    }
                }
                // 原地等待
                next_dp[pos][new_stun] = max(next_dp[pos][new_stun], 
                                            curr_score + scores[time][pos]);
            }
        }

        // 处理定身刷新逻辑
        for (int pos = 0; pos &amp;#x3C; positions; ++pos) {
            // 更新答案
            for (int stun = 0; stun &amp;#x3C;= 3; ++stun) {
                if (next_dp[pos][stun] &gt; answer) {
                    answer = next_dp[pos][stun];
                }
            }

            if (traps[time][pos]) {
                // 非篮球出现，所有未满3秒定身状态强制变成3秒定身
                long long max_stun3 = next_dp[pos][3];
                for (int stun = 0; stun &amp;#x3C; 3; ++stun) {
                    if (next_dp[pos][stun] != NEG_INF) {
                        max_stun3 = max(max_stun3, next_dp[pos][stun]);
                        next_dp[pos][stun] = NEG_INF;
                    }
                }
                next_dp[pos][3] = max_stun3;
                if (next_dp[pos][3] &gt; answer) {
                    answer = next_dp[pos][3];
                }
            }
        }

        dp = move(next_dp);
    }

    cout &amp;#x3C;&amp;#x3C; answer &amp;#x3C;&amp;#x3C; &quot;\n&quot;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/20250809-r17Do9.BYPufRQG.png"/><enclosure url="/_astro/20250809-r17Do9.BYPufRQG.png"/></item><item><title>从一次 double free 深入理解 shared_ptr 的原理与最佳实践</title><link>https://coooredump.github.io/blog/cpp/from-double-free-to-shared_ptr</link><guid isPermaLink="true">https://coooredump.github.io/blog/cpp/from-double-free-to-shared_ptr</guid><description>从实际开发中遇到的 double free 问题出发，系统剖析智能指针 shared_ptr 的工作原理与 shared_ptr 的六大使用陷阱，最后提供 shared_ptr 非线程安全版本和基于原子操作的线程安全实现。</description><pubDate>Sat, 02 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在使用 C++ 开发过程中，最容易也是最麻烦的问题便是内存泄漏。相较于 Java、Python 或者 Go 语言都拥有垃圾回收机制，在对象没有引用时就会被系统自动回收而且基本上没有指针的概念，但是 C++ 则要求程序员自己管理内存，这一方面让程序员有更大的自由度但是也会很大影响程序员的开发效率。因此 C++11 标准中新推出了 &lt;code&gt;shared_ptr&lt;/code&gt;、&lt;code&gt;unique_ptr&lt;/code&gt; 和 &lt;code&gt;weak_ptr&lt;/code&gt; 三个智能指针来帮助管理内存。&lt;/p&gt;
&lt;p&gt;智能指针就是一个类，当超出了类的作用域时，类会自动调用析构函数，析构函数会自动释放资源，所以智能指针的作用原理就是在函数结束时自动释放内存空间，不需要手动释放。&lt;/p&gt;
&lt;p&gt;笔者在排查一个 &lt;strong&gt;double free&lt;/strong&gt; 问题时，重新回顾了 &lt;code&gt;shared_ptr&lt;/code&gt; 的工作原理，以及列出一些注意事项，本文着重介绍 &lt;code&gt;shared_ptr&lt;/code&gt;，其他智能指针不过多赘述。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;shared_ptr&lt;/code&gt; 本质&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;shared_ptr&lt;/code&gt; 能够自动记录共享对象的引用次数，并且在引用计数降至 $0$ 时自动删除对象，从而防止内存泄漏。每个 &lt;code&gt;shared_ptr&lt;/code&gt; 的拷贝都指向相同的内存，在最后一个 &lt;code&gt;shared_ptr&lt;/code&gt; 析构的时候其指向的内存资源才会被释放。&lt;/p&gt;
&lt;p&gt;本质上 &lt;code&gt;shared_ptr&lt;/code&gt; 是&lt;strong&gt;有两层析构&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_ptr&lt;/code&gt; 本身析构会使得指向的共享对象的引用数 $-1$，当共享对象引用数为 $0$ 时，则调用共享对象本身的析构函数&lt;/li&gt;
&lt;li&gt;这样就可以理解循环引用了：共享对象引用还是 $1$ 时，未调用共享对象本身的析构函数，其中成员 &lt;code&gt;shared_ptr&lt;/code&gt; 的析构函数也不会被调用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;shared_ptr&lt;/code&gt; 初始化方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构造函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::make_shared()&lt;/code&gt; 辅助函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reset()&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::shared_ptr&amp;#x3C;int&gt; p(new int(1));
std::shared_ptr&amp;#x3C;int&gt; p2 = p;
std::shared_ptr&amp;#x3C;A&gt; ap = std::make_shared&amp;#x3C;A&gt;();

// 对于一个未初始化的智能指针，可以通过调用 reset 方法初始化
std::shared_ptr&amp;#x3C;int&gt; ptr;
ptr.reset(new int(1));
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;不能将一个原始指针直接赋值给一个智能指针，如：&lt;code&gt;std::shared_ptr&amp;#x3C;int&gt; p = new int(1)&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于一个未初始化的智能指针，可以通过调用 &lt;code&gt;reset&lt;/code&gt; 方法初始化，当智能指针中有值的时候，调用 &lt;code&gt;reset&lt;/code&gt; 方法会使引用计数减 $1$。当需要获取原指针的时候可以通过 &lt;code&gt;get&lt;/code&gt; 方法返回原始指针：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::shared_ptr&amp;#x3C;int&gt; p(new int(1));
int *ptr = p.get();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;智能指针初始化时也可以指定删除器，当其引用计数为 $0$ 时将自动调用删除器来释放对象，删除器可以是一个函数对象。&lt;/p&gt;
&lt;p&gt;比如&lt;strong&gt;当使用 &lt;code&gt;shared_ptr&lt;/code&gt; 管理动态数组时，需要指定删除器，因为 &lt;code&gt;shared_ptr&lt;/code&gt; 默认删除器不支持数组对象&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// lambda 表达式作为删除器
std::shared_ptr&amp;#x3C;int&gt; p(new int[10], [](int *p) { delete []p; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;shared_ptr&lt;/code&gt; 注意事项&lt;/h2&gt;
&lt;p&gt;关于 &lt;code&gt;shared_ptr&lt;/code&gt; 的注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不要用一个裸指针初始化多个 &lt;code&gt;shared_ptr&lt;/code&gt;&lt;/strong&gt;，会出现 &lt;em&gt;&lt;strong&gt;double_free&lt;/strong&gt;&lt;/em&gt; 导致程序崩溃&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过 &lt;code&gt;shared_from_this()&lt;/code&gt; 返回 this 指针，不要把 this 指针作为 &lt;code&gt;shared_ptr&lt;/code&gt; 返回出来，因为 &lt;code&gt;this&lt;/code&gt; 指针本质就是裸指针，通过 this 返回可能会导致重复析构，&lt;strong&gt;不能把 this 指针交给智能指针管理&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;尽量使用 &lt;code&gt;std::make_shared&amp;#x3C;T&gt;()&lt;/code&gt;，少用 &lt;code&gt;new&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不要 &lt;code&gt;delete&lt;/code&gt; &lt;code&gt;get()&lt;/code&gt; 返回的裸指针&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不是 &lt;code&gt;new&lt;/code&gt; 出来的空间要自定义删除器&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;要避免循环引用&lt;/strong&gt;，循环引用导致内存永远不会被释放，造成内存泄漏（不在赘述）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 不要用一个裸指针初始化多个 &lt;code&gt;shared_ptr&lt;/code&gt;（会导致 double free）&lt;/h3&gt;
&lt;p&gt;问题场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int* raw_ptr = new int(42);
std::shared_ptr&amp;#x3C;int&gt; sp1(raw_ptr);
std::shared_ptr&amp;#x3C;int&gt; sp2(raw_ptr);  // 危险！
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;两个独立的 &lt;code&gt;shared_ptr&lt;/code&gt; 会&lt;strong&gt;各自维护&lt;/strong&gt;一个引用计数控制块（相互独立）&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;sp1&lt;/code&gt; 和 &lt;code&gt;sp2&lt;/code&gt; 销毁时都会尝试释放 &lt;code&gt;raw_ptr&lt;/code&gt;，导致 &lt;strong&gt;双重释放&lt;/strong&gt;（double free）&lt;/li&gt;
&lt;li&gt;结果通常是程序崩溃或未定义行为&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正确做法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 方法1：直接使用 make_shared
// make_shared 一次性分配内存，包含控制块（引用计数、弱引用计数等）；对象存储空间（存储实际值 42）
auto sp1 = std::make_shared&amp;#x3C;int&gt;(42);
auto sp2 = sp1;  // 只是复制指针并增加引用计数，两个 shared_ptr 指向同一个控制块，共享所有权

// 方法2：如果必须从裸指针创建，确保只创建一次 shared_ptr
int* raw_ptr = new int(42);
std::shared_ptr&amp;#x3C;int&gt; sp1(raw_ptr);
std::shared_ptr&amp;#x3C;int&gt; sp2 = sp1;  // 复制的是控制块指针，不是重新创建控制块，共享同一个控制块
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 正确使用 &lt;code&gt;shared_from_this()&lt;/code&gt; 而不是直接返回 &lt;code&gt;this&lt;/code&gt; 指针&lt;/h3&gt;
&lt;p&gt;问题场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class BadExample {
public:
    std::shared_ptr&amp;#x3C;BadExample&gt; get_this() {
        return std::shared_ptr&amp;#x3C;BadExample&gt;(this);  // 危险！
    }
};

auto obj = std::make_shared&amp;#x3C;BadExample&gt;();
auto another_ref = obj-&gt;get_this();  // 创建了独立的控制块
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这会创建两个独立的 &lt;code&gt;shared_ptr&lt;/code&gt; 控制块&lt;/li&gt;
&lt;li&gt;当两个 &lt;code&gt;shared_ptr&lt;/code&gt; 销毁时都会尝试析构同一个对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正确做法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class GoodExample : public std::enable_shared_from_this&amp;#x3C;GoodExample&gt; {
public:
    std::shared_ptr&amp;#x3C;GoodExample&gt; get_this() {
        return shared_from_this();  // 安全
    }
};

auto obj = std::make_shared&amp;#x3C;GoodExample&gt;();
auto another_ref = obj-&gt;get_this();  // 共享同一个控制块
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 优先使用 &lt;code&gt;std::make_shared&amp;#x3C;T&gt;()&lt;/code&gt; 而不是 &lt;code&gt;new&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;问题场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 不推荐
std::shared_ptr&amp;#x3C;MyClass&gt; sp(new MyClass(arg1, arg2));

// 推荐
auto sp = std::make_shared&amp;#x3C;MyClass&gt;(arg1, arg2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;性能更好：单次内存分配（对象 + 控制块）&lt;/li&gt;
&lt;li&gt;异常安全：不会在 &lt;code&gt;new&lt;/code&gt; 和 &lt;code&gt;shared_ptr&lt;/code&gt; 构造之间发生泄漏&lt;/li&gt;
&lt;li&gt;代码更简洁：不需要重复类型名称&lt;/li&gt;
&lt;li&gt;缓存友好：对象和控制块内存相邻&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例外情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要自定义删除器时&lt;/li&gt;
&lt;li&gt;需要指定特殊的内存分配方式时&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 不要 &lt;code&gt;delete&lt;/code&gt; &lt;code&gt;get()&lt;/code&gt; 返回的裸指针&lt;/h3&gt;
&lt;p&gt;问题场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto sp = std::make_shared&amp;#x3C;int&gt;(42);
int* raw_ptr = sp.get();
delete raw_ptr;  // 灾难性错误！

// 当 sp 超出作用域时，会再次尝试删除已删除的内存
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_ptr&lt;/code&gt; &lt;strong&gt;仍然拥有内存所有权&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;手动 &lt;code&gt;delete&lt;/code&gt; 会导致：
&lt;ul&gt;
&lt;li&gt;double free&lt;/li&gt;
&lt;li&gt;控制块状态不一致&lt;/li&gt;
&lt;li&gt;未定义行为（通常崩溃）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正确做法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto sp = std::make_shared&amp;#x3C;int&gt;(42);
int* raw_ptr = sp.get();
// 仅使用 raw_ptr 进行读取/写入操作，绝不手动删除它
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 非 &lt;code&gt;new&lt;/code&gt; 分配的内存需要自定义删除器&lt;/h3&gt;
&lt;p&gt;问题场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 从 malloc 分配的内存
void* mem = malloc(1024);
std::shared_ptr&amp;#x3C;void&gt; sp(mem);  // 错误！会用 delete 而不是 free

// 文件指针
FILE* fp = fopen(&quot;file.txt&quot;, &quot;r&quot;);
std::shared_ptr&amp;#x3C;FILE&gt; sp(fp);  // 错误！会用 delete 而不是 fclose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正确做法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 使用自定义删除器（lambda 表达式作为删除器）
void* mem = malloc(1024);
std::shared_ptr&amp;#x3C;void&gt; sp(mem, free);  // 使用 free 作为删除器

FILE* fp = fopen(&quot;file.txt&quot;, &quot;r&quot;);
std::shared_ptr&amp;#x3C;FILE&gt; sp(fp, [](FILE* f) { fclose(f); });

// 对于数组
int* arr = new int[10];
std::shared_ptr&amp;#x3C;int&gt; sp(arr, [](int* p) { delete[] p; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见删除器场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;C 风格内存分配（&lt;code&gt;malloc/calloc/realloc&lt;/code&gt;）→ 使用 &lt;code&gt;free&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;文件操作（&lt;code&gt;fopen&lt;/code&gt;）→ 使用 &lt;code&gt;fclose&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;系统资源（套接字、句柄等）→ 使用对应的释放函数&lt;/li&gt;
&lt;li&gt;数组 → 使用 &lt;code&gt;delete[]&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6. 避免循环引用导致的内存泄露&lt;/h3&gt;
&lt;h4&gt;问题场景 1&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class A;
class B;

class A {
public:
    std::shared_ptr&amp;#x3C;B&gt; b;
};

class B {
public:
    std::shared_ptr&amp;#x3C;A&gt; a;
};

int main() {
    std::shared_ptr&amp;#x3C;A&gt; ap = std::make_shared&amp;#x3C;A&gt;();
    std::shared_ptr&amp;#x3C;B&gt; bp = std::make_shared&amp;#x3C;B&gt;();
    ap-&gt;b = bp;
    bp-&gt;a = ap;
    // 此时，a 和 b 相互持有对方的 shared_ptr，形成循环引用
    // 程序结束时，a 和 b 的引用计数都不会降为零，导致内存泄漏
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;问题场景 2&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Node {
public:
    std::shared_ptr&amp;#x3C;Node&gt; next;
    std::shared_ptr&amp;#x3C;Node&gt; prev;  // 双向链表导致循环引用
    ~Node() { std::cout &amp;#x3C;&amp;#x3C; &quot;Node destroyed\n&quot;; }
};

auto node1 = std::make_shared&amp;#x3C;Node&gt;();
auto node2 = std::make_shared&amp;#x3C;Node&gt;();
node1-&gt;next = node2;
node2-&gt;prev = node1;  // 循环引用形成！
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;node1&lt;/code&gt; 和 &lt;code&gt;node2&lt;/code&gt; 离开作用域时：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;node1&lt;/code&gt; 的引用计数从 1→0？不，因为 &lt;code&gt;node2-&gt;prev&lt;/code&gt; 还持有引用（实际从 2→1）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node2&lt;/code&gt; 的引用计数同样从 2→1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;结果：&lt;strong&gt;两者引用计数永远不为 0&lt;/strong&gt;，内存永远不会释放&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;node1 [refcount=2] --&gt; Node1对象
  ↑next               ↓prev
Node2对象 &amp;#x3C;-- [refcount=2] node2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方案：&lt;code&gt;weak_ptr&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class SafeNode {
public:
    std::shared_ptr&amp;#x3C;SafeNode&gt; next;
    std::weak_ptr&amp;#x3C;SafeNode&gt; prev;  // 使用weak_ptr
    
    ~SafeNode() { std::cout &amp;#x3C;&amp;#x3C; &quot;SafeNode destroyed\n&quot;; }
};

auto node1 = std::make_shared&amp;#x3C;SafeNode&gt;();
auto node2 = std::make_shared&amp;#x3C;SafeNode&gt;();
node1-&gt;next = node2;
node2-&gt;prev = node1;  // weak_ptr不会增加引用计数
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;何时会出现循环引用？&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;双向链表、树结构等复杂数据结构&lt;/li&gt;
&lt;li&gt;对象相互持有对方的 &lt;code&gt;shared_ptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;父子对象互相强引用&lt;/li&gt;
&lt;li&gt;观察者模式中主体和观察者互相持有&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;手撕 &lt;code&gt;shared_ptr&lt;/code&gt;｜面试高频场景题&lt;/h2&gt;
&lt;h3&gt;1. 非线程安全的简单实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;memory&gt;

template&amp;#x3C;typename T&gt;
class smartPtr {
private:
    T *_ptr;
    size_t* _count;

public:
    smartPtr(T *ptr = nullptr):_ptr(ptr) {
        if (_ptr) {
            _count = new size_t(1);
        } else {
            _count = new size_t(0);
        }
    }

    smartPtr(const smartPtr &amp;#x26;ptr) {
        if (this != &amp;#x26;ptr) {
            this-&gt;_ptr = ptr._ptr;
            this-&gt;_count = ptr._count;
            ++(*this-&gt;_count)   ;
        }
    }

    smartPtr&amp;#x26; operator=(const smartPtr &amp;#x26;ptr) {
        if (this-&gt;_ptr == ptr._ptr)
            return *this;

        if (this-&gt;_ptr) {
            --(*this-&gt;_count);
            if (this-&gt;_count == 0) {
                delete this-&gt;_ptr;
                delete this-&gt;_count;
            }
        }

        this-&gt;_ptr = ptr._ptr;
        this-&gt;_count = ptr._count;
        ++(*this-&gt;_count);

        return *this;
    }

    ~smartPtr() {
        --(*this-&gt;_count);
        if (0 == *this-&gt;_count) {
            delete this-&gt;_ptr;
            delete this-&gt;_count;
        }
    }

    size_t use_count() {
        return *this-&gt;_count;
    }

    T&amp;#x26; operator*() {
        assert(this-&gt;_ptr == nullptr);
        return *(this-&gt;_ptr);
    }

    T* operator-&gt;() {
        assert(this-&gt;_ptr == nullptr);
        return this-&gt;_ptr;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 基于原子操作的线程安全实现&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-5imE2S.png&quot; alt=&quot;Screenshot 2025-08-02 at 18.49.37&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#pragma once

#include &amp;#x3C;atomic&gt;  // 引入原子操作

template &amp;#x3C;typename T&gt;
class shared_ptr {
private:
  T* ptr;                               // 指向管理的对象
  std::atomic&amp;#x3C;std::size_t&gt;* ref_count;  // 原子引用计数

  // 释放资源
  void release() {
    // P.S. 这里使用 std::memory_order_acq_rel 内存序，保证释放资源的同步
    if (ref_count &amp;#x26;&amp;#x26; ref_count-&gt;fetch_sub(1, std::memory_order_acq_rel) == 1) {
      delete ptr;
      delete ref_count;
    }
  }

public:
  // 默认构造函数
  shared_ptr() : ptr(nullptr), ref_count(nullptr) {}

  // 构造函数
  // P.S. 这里使用 explicit 关键字，防止隐式类型转换
  // shared_ptr&amp;#x3C;int&gt; ptr1 = new int(10);  不允许出现
  explicit shared_ptr(T* p) : ptr(p), ref_count(p ? new std::atomic&amp;#x3C;std::size_t&gt;(1) : nullptr) {}

  // 析构函数
  ~shared_ptr() { release(); }

  // 拷贝构造函数
  shared_ptr(const shared_ptr&amp;#x3C;T&gt;&amp;#x26; other) : ptr(other.ptr), ref_count(other.ref_count) {
    if (ref_count) {
      ref_count-&gt;fetch_add(1, std::memory_order_relaxed);  // 引用计数增加，不需要强内存序
    }
  }

  // 拷贝赋值运算符
  shared_ptr&amp;#x3C;T&gt;&amp;#x26; operator=(const shared_ptr&amp;#x3C;T&gt;&amp;#x26; other) {
    if (this != &amp;#x26;other) {
      release();  // 释放当前资源
      ptr = other.ptr;
      ref_count = other.ref_count;
      if (ref_count) {
        ref_count-&gt;fetch_add(1, std::memory_order_relaxed);  // 引用计数增加
      }
    }
    return *this;
  }

  // 移动构造函数
  // P.S. noexcept 关键字表示该函数不会抛出异常。
  // 标准库中的某些操作（如 std::swap）要求移动操作是 noexcept 的，以确保异常安全。
  // noexcept 可以帮助编译器生成更高效的代码，因为它不需要为异常处理生成额外的代码。
  shared_ptr(shared_ptr&amp;#x3C;T&gt;&amp;#x26;&amp;#x26; other) noexcept : ptr(other.ptr), ref_count(other.ref_count) {
    other.ptr = nullptr;
    other.ref_count = nullptr;
  }

  // 移动赋值运算符
  shared_ptr&amp;#x3C;T&gt;&amp;#x26; operator=(shared_ptr&amp;#x3C;T&gt;&amp;#x26;&amp;#x26; other) noexcept {
    if (this != &amp;#x26;other) {
      release();  // 释放当前资源
      ptr = other.ptr;
      ref_count = other.ref_count;
      other.ptr = nullptr;
      other.ref_count = nullptr;
    }
    return *this;
  }

  // 解引用运算符
  // P.S. const 关键字表示该函数不会修改对象的状态。
  T&amp;#x26; operator*() const { return *ptr; }

  // 箭头运算符
  T* operator-&gt;() const { return ptr; }

  // 获取引用计数
  std::size_t use_count() const { return ref_count ? ref_count-&gt;load(std::memory_order_acquire) : 0; }

  // 获取原始指针
  T* get() const { return ptr; }

  // 重置指针
  void reset(T* p = nullptr) {
    release();
    ptr = p;
    ref_count = p ? new std::atomic&amp;#x3C;std::size_t&gt;(1) : nullptr;
  }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>AI Infra 与 Infra</title><link>https://coooredump.github.io/blog/ai-infra/ai-infra-vs-traditional-infra</link><guid isPermaLink="true">https://coooredump.github.io/blog/ai-infra/ai-infra-vs-traditional-infra</guid><description>近些年来，关于 AI Infra 和传统 Infra 之间的差异引发了广泛讨论，尤其是面对 GPU、KVCache、3D 并行等新概念，究竟 AI Infra 真的与传统 Infra 有着天壤之别，还是说它们之间其实是某种延续与演变？</description><pubDate>Fri, 01 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;随着大模型技术的爆发，AI Infra 已成为基础设施领域的核心战场。过去 1 年多的时间，QQ 基础架构算法工程团队落地了多个大模型应用，包括语音合成大模型、内容理解多模态大模型、生成式推荐大模型，跑通大模型训练到推理的全链路。那 AI Infra 真的是和传统 Infra 差异很大的新体系吗，还是说它其实是过去 Infra 经验的演化？&lt;/p&gt;
&lt;p&gt;本文将分享传统后台工程师积累的技术栈和方法论，如何延续并迁移到 AI 系统，并系统性拆解 AI Infra 的硬件、软件、训练和推理挑战。&lt;/p&gt;
&lt;h2&gt;硬件演进&lt;/h2&gt;
&lt;p&gt;经济基础决定上层建筑。软件层面的架构设计，无法脱离硬件约束。了解现代 AI 硬件特性非常有必要。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一台高性能的 GPU 服务器可以换一套深圳房子&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-xuBu7X.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;从 CPU 为中心到 GPU 为中心&lt;/h3&gt;
&lt;p&gt;传统基础设施以 CPU 为核心，通过多线程和微服务构建分布式系统，处理高并发请求（如 Web 服务），这些都有成熟的方法论了（如“海量服务之道”），主要工作是&lt;strong&gt;逻辑事务&lt;/strong&gt;的处理，瓶颈在网络 I/O 和 CPU 核心数量。发展到今天，硬件已经很少是制约 CPU 系统设计的瓶颈。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-Rfsd6Z.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;而 AI Infra 以 GPU 为核心，其设计目标从&lt;strong&gt;逻辑事务&lt;/strong&gt;处理转向&lt;strong&gt;高吞吐浮点计算&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;此时 CPU 多线程被 GPU 并行计算替代，内存被显存替代。&lt;/p&gt;
&lt;p&gt;如下图所示，H20 单卡 96GB 显存，可以提供 44TFLOPS 的单精度浮点运算，算力和访存带宽是主流 CPU 数十倍甚至数百倍。每台机器安装 8 卡 = 768GB 显存，另外还有 CPU 192 核 384 线程 + 2.3 TB 内存。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-RuxC1g.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;GPU 成为核心是因为 LLM 大模型每次生成一个 &lt;code&gt;token&lt;/code&gt;，都需要&lt;strong&gt;读取全量的模型参数&lt;/strong&gt;。传统的【CPU + 内存】的算力和带宽无法满足如此恐怖的计算密度，计算和通信都必须转移（offload）到 GPU 内完成。&lt;/p&gt;
&lt;p&gt;CPU 成为数据搬运工和“辅助处理器”。&lt;/p&gt;
&lt;p&gt;为了更直观地理解这个计算密度，做一个简单的计算。不考虑计算的延时，LLM 大模型生成一个 &lt;code&gt;token&lt;/code&gt; 的耗时公式计算为：&lt;/p&gt;
&lt;p&gt;$$
计算耗时=\frac{模型参数量*数据精度}{显存带宽}
$$&lt;/p&gt;
&lt;p&gt;以 DeepSeek-R1-671B-A37B-FP8 模型为例，计算一个 &lt;code&gt;token&lt;/code&gt; 耗时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;H20：37B × 1byte ÷ 4000GB/s = &lt;strong&gt;9 ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;CPU：37B × 1byte ÷ 64GB/s = &lt;strong&gt;578 ms&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;从“去 IOE”到“AI 大型机”&lt;/h3&gt;
&lt;p&gt;显而易见，我们的现在身处新的一轮烈火烹油的硬件革命的历史进程中，各种专用硬件、专用网络层出不穷。DeepSeek-R1 和 QWen3-235B 千亿级参数训练需千卡 GPU 集群协同，通过专用网络互联构建“AI 超算”，其设计逻辑与以前的 IBM 大型机惊人相似——以硬件集中化换取极致性能与可靠性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;IBM 大型机&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-oiVLOc.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;传统 Infra 的分布式理念貌似在 AI 时代失效了？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;传统 Infra 追求横向扩展，而 AI Infra 呈现“AI 大型机”特性，是因为传统后台服务的可以容忍毫秒级延迟，但 AI 集群不行，GPU 的算力是 CPU 的数百倍，微秒级的延时等待也会造成很大的算力损耗，需要硬件的高度集成。在可预见的 1-3 年的未来，这样的专用硬件 + 网络的集中式架构很难发生比较大的改变。&lt;/p&gt;
&lt;p&gt;回顾历史，我们总是在寻求科技平权。前人推动“去 IOE”（IBM 小型机、Oracle 数据库、EMC 存储），用分布式廉价 x86 PC 机替代集中式高端硬件，本质上是利用软件创新重构一个高可用 + 低成本的互联网基础设施。&quot;AI 大型机&quot;是技术发展必由之路，但不是终极形态。长期（5 年）来看，必然会出现 &quot;AI 去 NVIDIA 化&quot;，重演“去 IOE”的历史。&lt;/p&gt;
&lt;h2&gt;软件演进&lt;/h2&gt;
&lt;p&gt;说完硬件体系的革命，接下来再关注下软件层面的变化。&lt;/p&gt;
&lt;p&gt;相比传统后台应用的增删查改，AI 应用的新范式是&lt;strong&gt;模型训练和推理&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型训练：通过海量数据拟合出一个复杂的神经网络模型&lt;/li&gt;
&lt;li&gt;模型推理：利用训练好的神经网络模型进行运算，输入的新数据来获得新的结论&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个例子，训练就是根据 &lt;code&gt;&amp;#x3C;年龄, 身高&gt;&lt;/code&gt; 的分布使用最小二乘法拟合模型 &lt;code&gt;y = ax + b&lt;/code&gt;，推理就是利用这个模型 &lt;code&gt;y = ax + b&lt;/code&gt;，输入一个新的年龄，预测身高。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-VNo8HU.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;深度学习框架&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;工欲善其事，必先利其器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;传统后台应用依赖 tRPC 或 Spring 等微服务框架，帮助我们屏蔽负载均衡、网络通信等底层细节，我们可以把精力放在业务实现上。&lt;/p&gt;
&lt;p&gt;与之相似，AI 应用则依赖深度学习框架。如果没有深度学习框架，我们就可能陷入在茫茫的数学深渊中，挣扎于痛苦的 GPU 编程泥潭里。有了深度学习框架，我们才可以把所有精力花在设计模型和创新本身上，而不用关注底层的实现细节，极大降低了 AI 应用的门槛。&lt;/p&gt;
&lt;p&gt;大家可能听说过不同的深度学习框架——Tensorflow，PyTorch。现在是 2025 年，不用纠结选哪个，因为 &lt;strong&gt;PyTorch 就是 AI 模型训练、推理的深度学习框架的事实标准&lt;/strong&gt;，开源模型和代码都是 PyTorch 一边倒。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-VXd8Cs.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;得益于&lt;strong&gt;动态计算图&lt;/strong&gt;、&lt;strong&gt;自动微分&lt;/strong&gt;和丰富的 Tensor &lt;strong&gt;操作算子&lt;/strong&gt;，PyTorch 能帮助我们快速实现模型设计。如下图所示，只需要描述模型结构 + 待学习的网络参数，不需要关心数学计算和 GPU 编程的细节。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-NPAXjQ.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;GPU 编程&lt;/h3&gt;
&lt;p&gt;绝大部分的 AI 应用，的确不需要我们手写数学计算的 GPU 代码。但为了满足模型创新的需求，有必要学习 GPU 编程。&lt;/p&gt;
&lt;p&gt;例如 Meta 发布的 HSTU 生成式推荐模型，核心的 &lt;code&gt;hstu_attn&lt;/code&gt; 计算：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果直接用 PyTorch 框架算子组合实现，则时间复杂度为 $O(M * N^2)$ ，其中 $M$ 和 $N$ 是一个数量级，相当于 $O(N^3)$&lt;/li&gt;
&lt;li&gt;但是通过自定义内核，可以优化到 $O(N^2)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 GPU 核心上运行的代码片段称为内核。编写高性能的 CUDA 内核需要丰富的经验，并且学习曲线陡峭。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/497036659&quot;&gt;如何理解 SIMD 和 SIMT&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为我们习惯于传统 CPU 编程处理串行的计算任务（即 SIMD, Single Instruction Multiple Data），通过多线程提高并发度。而 GPU 采用 SIMT (Single Instruction Multiple Thread) 架构，有大量计算单元（CUDA Cores）和数万个线程，但是被&lt;strong&gt;分组后的线程同一时刻只能执行相同的指令&lt;/strong&gt;。这与&lt;strong&gt;传统 CPU 的串行思维、不同线程处理不同任务&lt;/strong&gt;，存在根本性冲突，导致 GPU 编程学习难度大。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;现实生活中的 SIMT 架构&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-r89YFS.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在推荐使用 Triton 编程语言完成 GPU kernel 的开发，它提供类似 Python 的语法，无需深入理解 GPU 硬件细节（如线程调度、共享内存管理），而且和 PyTorch 深度学习框架的生态结合更好。推荐这个 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//github.com/SiriusNEO/Triton-Puzzles-Lite/&quot;&gt;Triton-Puzzles-Lite&lt;/a&gt; 项目用作 Triton 的入门学习。&lt;/p&gt;
&lt;h3&gt;Python 编程&lt;/h3&gt;
&lt;p&gt;正如客户端开发离不开 Kotlin/Objective-C，AI Infra 编程的第一公民就是 Python。&lt;/p&gt;
&lt;p&gt;PyTorch 深度学习框架的设计哲学强调 Python 优先 。&lt;/p&gt;
&lt;p&gt;以前大部分模型还可以轻松导出 ONNX、TorchScript 等用 C++ 部署，现在随着对模型的细粒度优化和控制越来越多，比如 KV Cache、MoE/模型并行、复杂的 if/for 控制流、自定义 Triton 算子等，模型越来越难以脱离 Python 的控制部署。&lt;/p&gt;
&lt;h2&gt;模型训练 de 挑战&lt;/h2&gt;
&lt;p&gt;我们一直追求更大的模型，DeepSeek-R1 有数千亿参数，使用了数十万亿 token 的训练数据，涉及算力、存储、通信等多维度的工程挑战。有了 PyTorch 深度学习框架，只是 AI 应用落地的万里长征第一步。接下来我们将讨论深度学习框架之上的模型训练的挑战。&lt;/p&gt;
&lt;h3&gt;存得下&lt;/h3&gt;
&lt;p&gt;DeepSeek-R1 模型大小为 670GB，而一台 GPU 服务器有 8 张 H20 卡，提供 768GB 显存，足够存下一个完整的 DeepSeek 模型。那整个行业为什么还投入大量的人力物力，顶着通信延时造成的算力损耗，也要建设分布式 GPU 集群？&lt;/p&gt;
&lt;p&gt;核心原因是单台 GPU 服务器“存不下”。&lt;/p&gt;
&lt;h4&gt;显存刺客：中间激活&lt;/h4&gt;
&lt;p&gt;如下图所示的模型，&lt;code&gt;x1/x2/x3/x4&lt;/code&gt; 这些中间变量就是“中间激活”。它们是神经网络前向传播的“堆栈帧”—— &lt;strong&gt;记录每一层处理后的数据快照，确保反向传播可回溯梯度&lt;/strong&gt;，根据预测误差调整模型权重，最小化损失函数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-7Gyeqh.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;这些中间激活为什么会成为“显存刺客”？是因为中间激活的空间复杂度是和输入数据长度正相关的，特别的，对于 LLM 来说是 $O(N^2)$，&lt;strong&gt;正比于输入数据长度的平方&lt;/strong&gt;，这是一个指数爆炸式增长的数字。类似函数递归不断增长的“堆栈帧”导致的内存溢出，我们遇到了 &lt;strong&gt;AI Infra 的 OOM（Out of Memory）挑战&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;借助 PyTorch 的 profiler 工具，我们可以直观地看到这个 OOM。下图是训练过程中不同阶段的显存分配，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型参数（Parameter）&lt;/li&gt;
&lt;li&gt;优化器状态（Optimizer State）&lt;/li&gt;
&lt;li&gt;中间激活（Activation）&lt;/li&gt;
&lt;li&gt;梯度（Gradient）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在前向传播结束后出现一个显存占用（中间激活）的尖峰，远大于模型参数本身。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-00zjlo.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h4&gt;模型并行&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;传统后台服务使用分片（Sharding）策略解决单机存不下的问题&lt;/strong&gt;。与之相似，&lt;strong&gt;AI Infra 提出“模型并行”&lt;/strong&gt;，就是将单个大模型拆分为多个子模块，并分布到不同 GPU 上协同工作，通过通信来共享数据。有不同的“拆分模型”策略，例如按模型模块划分，按张量（Tensor）划分的，也可以将多种拆分方法结合起来一起使用。PyTorch 深度学习框架和开源方案 Megatron 都能帮助我们高效地实现模型并行。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不同的模型并行策略&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-8YlNEU.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;算得快&lt;/h3&gt;
&lt;p&gt;建设分布式 GPU 集群的原因，一个是因为“单机存不下”，另外一个是提升训练速度。但简单的机器堆叠，算力不一定有线性的增长。&lt;strong&gt;因为分布式训练并不是简单地把原来一个 GPU 做的事情分给多个 GPU 各自做&lt;/strong&gt;。需要协调多个 GPU 机器计算任务分配，&lt;strong&gt;GPU 机器之间的数据传输会引入网络 I/O 和通信开销&lt;/strong&gt;，降低训练速度。&lt;/p&gt;
&lt;h4&gt;通信计算重叠&lt;/h4&gt;
&lt;p&gt;如下图所示的常规训练时序是串联式的，存在许多网络 I/O，GPU 利用率低，训练速度慢。我们希望 GPU 大部分时间都在计算，而不是花在数据传输或等待其他 GPU 的工作上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-64fqnG.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传统后台服务我们通过多线程或异步 I/O 避免阻塞 CPU 主线程&lt;/strong&gt;。与之相似，&lt;strong&gt;AI Infra 提出通信计算重叠&lt;/strong&gt;的方法论。&lt;/p&gt;
&lt;p&gt;GPU 编程模型中有流（stream）的概念，一个流表示一个 GPU 操作队列，该队列中的操作将以添加到流中的先后顺序而依次执行。&lt;strong&gt;不同流之间可以并行执行，那么通过令计算和通信操作加入不同的流中，可以做到二者的执行在时间上重叠&lt;/strong&gt;。例如 &lt;a href=&quot;https://github.com/pytorch/torchrec&quot;&gt;TorchRec&lt;/a&gt; 的训练流水线能帮助我们实现高效的通信计算重叠。&lt;/p&gt;
&lt;h2&gt;模型推理 de 挑战&lt;/h2&gt;
&lt;p&gt;AI 模型训练成本很高，优秀如 DeepSeek 也要烧掉 500 万美金，但再贵也只是一劳永逸的。而模型推理的成本更高，因为用户越多，AI 模型推理次数越多，总成本越高。AI Infra 模型推理面对的挑战和传统 Infra 非常相似，主要是 2 个挑战：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;高吞吐（降本）&lt;/li&gt;
&lt;li&gt;低延时（增效）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;降低延时&lt;/h3&gt;
&lt;p&gt;现在的 AI 模型越来越多地直面终端用户，需要和用户进行实时的交互，例如文本对话和语音合成。模型推理耗时过高，会直接造成用户体验受损，用户流失与转化率下降。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传统后台服务我们使用链接复用、缓存、柔性等技术降低系统响应时间&lt;/strong&gt;。AI Infra 也有相似的做法。&lt;/p&gt;
&lt;h4&gt;CUDA Graph&lt;/h4&gt;
&lt;p&gt;在 GPU 编程模型中，CPU 和 GPU 是异构的，CPU 通过 API（例如 CUDA API） 向 GPU 提交任务，然后异步等待 GPU 的计算结果返回。GPU 收到任务后，会执行内核启动、内存拷贝、计算等操作。这个过程中，涉及到 CPU 与 GPU 之间的通信、驱动程序的处理以及 GPU 任务的调度等环节，会产生一定的延迟。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CPU 与 GPU 通信&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-Z3cX84.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;模型推理需要执行大量重复的 GPU 操作，每个的 GPU 操作都要重复执行上述环节，这些非核心的 GPU 开销会成倍数地放大，影响最终响应时间。&lt;/p&gt;
&lt;p&gt;在传统后台服务，我们使用 Redis 的 &lt;code&gt;Lua&lt;/code&gt; 脚本封装多个 Redis 操作和计算逻辑，一次提交，减少网络开销。与之相似，&lt;strong&gt;AI Infra 利用 CUDA Graph 技术将多个 GPU 操作转化为一个有向无环图（DAG），然后一次性提交整个 DAG 提交到 GPU 执行，由 GPU 自身来管理这些操作的依赖关系和执行顺序，从而减少 CPU 与 GPU 之间的交互开销。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;多个 GPU 内核启动转化为 CUDA Graph&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-RQsZ6u.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h4&gt;KV Cache —— 空间换时间&lt;/h4&gt;
&lt;p&gt;LLM 大模型推理存在大量矩阵乘法运算，且高度依赖上下文信息。每次推理都需要将之前生成过的词重新输入模型进行计算（即上下文）。这种计算方式使得复杂度达到了 $O(N^2)$，其中必然存在大量的重复计算。&lt;/p&gt;
&lt;p&gt;例如，给定“天气”，模型会逐个预测剩下的字，假设接下来预测的两个字为“真好。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-rqtuDt.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;将“真”拼接到“天气”的后面，即新的输入为“天气真”，再预测“好”。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;KV Cache 的部分&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-xYa1FK.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;观察到，经过多次预测后，&lt;code&gt;X @ W_K&lt;/code&gt; 和 &lt;code&gt;X @ W_V&lt;/code&gt; 的结果上半部分都是相同的，这是由于 LLM 模型结构的特殊设计导致的。&lt;strong&gt;这些重复计算的结果可以缓存（即 KV Cache）下来，空间换时间，减少计算量&lt;/strong&gt;。几乎所有的 LLM 推理框架都支持了 KV Cache，例如 &lt;a href=&quot;https://github.com/vllm-project/vllm&quot;&gt;vLLM&lt;/a&gt; 。&lt;/p&gt;
&lt;h4&gt;流式响应&lt;/h4&gt;
&lt;p&gt;有时候模型推理延时实在避免不了，可以从工程交互上想办法。&lt;/p&gt;
&lt;p&gt;传统后台服务的 RPC 通信是一问一答方式，这种方式不太适合语音合成或者文本对话的场景。因为大模型推理需要几秒～几十秒，如果等待模型推理结束才展示结果，用户会等待较长的时间，体验很差。&lt;/p&gt;
&lt;p&gt;流式响应就是当模型推理计算得到第一个 token 或者第一个音频帧的时候，立马展示或者播放给用户，同时后续的模型推理结果在已经建立的 TCP 流上继续顺序传输。&lt;strong&gt;工程上从关注模型推理的整体耗时，改为关注首 token 或首个音频帧的耗时&lt;/strong&gt;。几乎所有的 LLM 推理框架都支持了流式响应。&lt;/p&gt;
&lt;h3&gt;提高吞吐量&lt;/h3&gt;
&lt;p&gt;提高吞吐量是程序员在传统 Infra 领域孜孜不倦的追求，因为更高的吞吐量意味着更低的机器成本。&lt;strong&gt;实现 AI 应用的高吞吐本质上就是提高昂贵的 GPU 的利用率&lt;/strong&gt;，让 GPU 单位时间能完成更多的任务。&lt;/p&gt;
&lt;p&gt;尽管模型推理需要执行万亿次浮点运算，但 &lt;strong&gt;GPU 有大量的计算单元（CUDA Cores），单个请求的模型推理很难令 GPU 利用率达到饱和&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;提高 GPU 利用率有 2 个方法：传统批处理和连续批处理。这里的“传统批处理”是相对于“连续批处理”这样的新型批处理方式而言的。&lt;/p&gt;
&lt;h4&gt;传统批处理&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;其实传统后台服务也大量使用了批处理，例如 Redis 的 &lt;code&gt;MGet&lt;/code&gt; 命令，单次请求就完成所有 key 的获取，将 N 次网络往返（RTT）压缩为1次。&lt;/li&gt;
&lt;li&gt;与之相似，模型推理的批处理就是将多个输入样本打包（&lt;code&gt;batch&lt;/code&gt;），&lt;strong&gt;将原本串行的 N 次轻量的推理计算，合并为 1 次重量的计算&lt;/strong&gt;，实现单位时间内处理更多的请求，提高了 GPU 利用率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“打包输入样本”是一个共性需求，大部分推理框架都提供该功能，例如 &lt;em&gt;Triton Inference Server&lt;/em&gt; 的 &lt;a href=&quot;https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/user_guide/batcher.html&quot;&gt;Batcher&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;模型批量推理流程图&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-MMrks0.gif&quot; alt=&quot;模型批量推理流程图&quot;&gt;&lt;/p&gt;
&lt;h4&gt;连续批处理&lt;/h4&gt;
&lt;p&gt;传统批处理类似“固定班次的公交车”：乘客（请求）必须等待发车时间（组建一个 batch），发车后所有乘客同步前进。即使有乘客提前下车（短请求完成），车辆仍需等待所有乘客到达终点（长请求完成）才能返程接新乘客。传统批处理存在着资源浪费：GPU 要等待长请求处理完，不能处理新的请求而空闲。&lt;/p&gt;
&lt;p&gt;这个问题在 LLM 应用领域显得特别突出，因为不同用户请求 Prompt，模型的回答结果长度差异巨大，如果使用传统批处理，GPU 空闲率很高。这个本质上是个任务调度问题，&lt;strong&gt;传统后台服务我们使用工作窃取算法（Work Stealing）解决线程空闲问题&lt;/strong&gt;，与之相似，&lt;strong&gt;AI Infra 提出“连续批处理”解决这个问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;连续批处理类似“随时随地拼车的顺风车”，每辆车（GPU）在行程中可随时上/下客：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新乘客（请求）直接加入当前车辆的空位（空闲计算单元）&lt;/li&gt;
&lt;li&gt;已完成的乘客立即下车（释放资源）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;几乎所有的 LLM 推理框架都支持了连续批处理能力，例如 vLLM 的 &lt;a href=&quot;https://github.com/vllm-project/vllm&quot;&gt;Continuous Batching&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;连续批推理流程图&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS@master/uPic/20250802-bm4PMg.webp&quot; alt=&quot;动图&quot;&gt;&lt;/p&gt;
&lt;h2&gt;AI Infra vs. Infra&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;AI Infra 入门教程：https://github.com/stas00/ml-engineering/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;关于传统 Infra 和 AI Infra 的差异，总的来说就是觉得这套东西和传统 Infra 差太远了。很多熟悉网络，计算，存储等传统 infra 的工程师，会觉得自己原来的经验在 AI 场景里难以直接利用，尤其是看到 &lt;strong&gt;GPU&lt;/strong&gt;，&lt;strong&gt;KV Cache&lt;/strong&gt;，&lt;strong&gt;3D parallelism&lt;/strong&gt; 这些新概念时，容易产生一种感觉完全换了一套体系的落差感。&lt;/p&gt;
&lt;p&gt;我个人感觉这些看法其实挺有代表性的，反映出很多工程师对 AI Infra 的第一印象：陌生，高门槛，甚至有些割裂感。下文就想聊聊我的一些粗浅的理解。AI Infra 真的是和传统 Infra 差异很大的新体系吗，还是说它其实是过去 Infra 经验的演化？&lt;/p&gt;
&lt;h3&gt;传统 Infra 与 AI Infra 概念&lt;/h3&gt;
&lt;p&gt;✅ 我的答案是：差异其实不大。AI Infra 是对传统 Infra 在新场景下的重构与延展。AI Infra 面对的工程挑战，例如计算、存储、通信，大部分是新时代的老问题，我们在传统 Infra 领域都能找到对应的场景和解决思路。差异只在于战场从 CPU 转移到 GPU，传统后台工程师积累的方法论，依然可以衔接到 AI Infra。&lt;/p&gt;
&lt;p&gt;🌟 如果从表面看起来，传统 Infra 和 AI Infra 确实很不一样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设计目标从&lt;strong&gt;逻辑事务&lt;/strong&gt;处理转向&lt;strong&gt;高吞吐浮点计算&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;传统 Infra 处理的是 web request，数据存储，和分布式服务协调&lt;/li&gt;
&lt;li&gt;而 AI Infra（特别是大模型）更多围绕的是 GPU 推理，KV Cache 管理，以及大模型训练框架等全新领域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;请求形态也不一样
&lt;ul&gt;
&lt;li&gt;web request 通常是毫秒级的 request，stateless&lt;/li&gt;
&lt;li&gt;而 LLM 推理一个 session 往往持续数秒甚至更久（随着 context window 和模型大小增加），还要动态维护 token-level 的上下文状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;tech stack 看起来也不同
&lt;ul&gt;
&lt;li&gt;传统用的是 Kubernetes + Docker&lt;/li&gt;
&lt;li&gt;现在大家在用 GPU, vLLM, DeepSpeed, FlashAttention, Triton, NCCL 这些仅仅从名字上听起来就很高大上的架构&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从这点来看，说传统经验无法直接迁移确实没错，但这只是表面的现象，不是本质。&lt;/p&gt;
&lt;p&gt;🔥 本质其实没变，仍然是系统设计和资源调度的问题&lt;/p&gt;
&lt;p&gt;回到工程本身，&lt;strong&gt;其实我们仍然在面对和传统 Infra 极其类似的问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何调度资源（从 &lt;strong&gt;CPU/内存&lt;/strong&gt; 变成了 &lt;strong&gt;GPU 显存&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;如何处理高并发请求（从 &lt;strong&gt;http resource request&lt;/strong&gt; ，变成了 &lt;strong&gt;prompt request&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们来看一组对比：&lt;/p&gt;
&lt;p&gt;| 传统 Infra 概念 | AI Infra 相对应概念       |
| --------------- | ------------------------- |
| Data Sharding   | Data Parallelism          |
| Load Balancer   | MoE Router                |
| OS Paging       | vLLM 的 KV Cache 分页机制 |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MoE Router：将 token 分发给多个 expert network，保证某一些 expert 不会overload（Deepseek，llama-4 使用的架构）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;这些机制其实都是传统 Infra 思维方式在 AI 场景中的利用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;🌰 拿 vLLM 举个例子：它像是给 LLM 写了一个操作系统，用来调度页面（KV Cache），管理进程（Request），本质上是引用了 OS 的内存管理 Principles 用来管理 KV Cache。&lt;/p&gt;
&lt;h3&gt;Infra 的“三大难题”：Scaling &amp;#x26; Sharding &amp;#x26; Copying&lt;/h3&gt;
&lt;p&gt;所有系统的底层挑战，基本都绕不开这三个关键词：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scaling（扩展）&lt;/strong&gt;：系统如何支持更大的规模和更高的并发？
&lt;ul&gt;
&lt;li&gt;在传统 Infra 中，这意味着如何横向扩展服务器，部署更多容器，使用负载均衡（load balancing）来分散请求&lt;/li&gt;
&lt;li&gt;在 AI Infra 中，这些问题转化为如何通过&lt;strong&gt;数据并行，模型并行，流水线并行&lt;/strong&gt;来分布和执行 GPU workload，以支持超大模型的训练以及 large number of inference requests&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sharding（切片）&lt;/strong&gt;：系统如何切分状态和计算，以实现并行处理？
&lt;ul&gt;
&lt;li&gt;在数据库系统中，这是将数据按照主键或范围切分到不同的分区，以支持高吞吐访问&lt;/li&gt;
&lt;li&gt;在 AI Infra 中，sharding 变成了对模型参数，KV Cache，activation，gradients，以及 optimizer states 的 split，比如 tensor parallelism 和 KV Paging 等，是实现分布式推理和训练的前提&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copying（复制）&lt;/strong&gt;：系统如何高效同步数据或状态？
&lt;ul&gt;
&lt;li&gt;传统系统中，复制体现在数据库副本同步或者缓存预热，以及 Kafka Replication&lt;/li&gt;
&lt;li&gt;在 AI Infra 中，复制的代价更加显著，比如 data parallelism 怎么 copy model to different GPUs（所以会有 ZeRO optimization 来 shard 参数，gradient 等等），通常需要依赖高性能通信机制（比如 RDMA 和 NCCL）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些挑战的本质没有变：&lt;strong&gt;仍然是如何高效并且低成本地协调跨不同机器的资源&lt;/strong&gt;。但在 AI Infra 中，由于 GPU 显存 limited，large context window，以及模型参数量大，它们变得更加脆弱和重要，&lt;strong&gt;也更需要更好的工程策略去解决这些问题&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;The Core of Infra: Cost Estimation and Identifying Key Issues after Deployment (from Jeff Dean)&lt;/h3&gt;
&lt;p&gt;Google 的 Jeff Dean 曾整理出一份广为流传的延迟参考 &lt;strong&gt;Key Numbers Every Programmer Should Know&lt;/strong&gt;。这些数据强调：在设计基础设施时，&lt;strong&gt;需要通过 estimate latency 来部署基础 system&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;深入理解这些延迟数据，能让你在系统设计时更加 aware 真正的 bottleneck 是什么，这样也能在部署后迅速找到性能 bottleneck，然后及时修复。&lt;/p&gt;
&lt;h4&gt;延迟参考值&lt;/h4&gt;
&lt;p&gt;| 操作                    | 延迟范围 |
| ----------------------- | -------- |
| L1 缓存访问             | ~0.5 ns  |
| L2 缓存访问             | ~7 ns    |
| 主内存访问              | ~100 ns  |
| 压缩 1 KB（Snappy）     | ~3 µs    |
| 通过 1 Gbps 网络发 1 KB | ~10 µs   |
| SSD 随机读 4 KB         | ~150 µs  |
| 数据中心内往返延迟      | ~0.5 ms  |
| 顺序读取 1 MB SSD 数据  | ~1 ms    |
| 磁盘 seek               | ~10 ms   |
| 读取 1 MB 磁盘数据      | ~20 ms   |
| 跨洋网络延迟            | ~150 ms  |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这些数值会随硬件不同而略有不同，但是相对的数量级非常值得记住，这是做系统设计时最基本的参考。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;在 AI Infra 中的 mapping&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Token-level KV Cache&lt;/strong&gt;：GPU 全局显存访问&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多个 GPU 通信&lt;/strong&gt;：通过 NCCL/RDMA 进行同步&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨机（cross server）通信&lt;/strong&gt;：比如在多个 server 之间调度推理任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;为什么会估算很重要？&lt;/h4&gt;
&lt;p&gt;就像在基础数据结构与算法课程中，我们必须熟练掌握各种数据结构以及算法的时间与空间复杂度，如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哈希查找 Average Time Complexity 是 $O(1)$&lt;/li&gt;
&lt;li&gt;快排是 $O(nlogn)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在数据库系统中，需要算 disk I/O 次数以及 index cost estimate。或者像以上的这些延迟参考值，应该是每一位做传统 infra 都应该牢牢记住并且经常会用到的。&lt;/p&gt;
&lt;p&gt;🕙 在 &lt;strong&gt;AI Infra&lt;/strong&gt;，我们也同样也需要估算大致**延迟（Latency）&lt;strong&gt;与&lt;/strong&gt;带宽（Bandwidth）**方面的数字，以便：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;事前估算&lt;/strong&gt;：训练一个模型需要多少时间，推理吞吐量，token latency 都应基于这些 latency numbers 进行初步估计&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事后诊断&lt;/strong&gt;：部署之后，如果性能不是很好，理解这些延迟能帮助你&lt;strong&gt;快速定位瓶颈究竟在哪里&lt;/strong&gt;（是 communication，memory bandwidth，or compute bound？）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;案例参考&lt;/strong&gt;：据说 Meta 在训练 LLaMA 系列模型时，GPU 报错或任务失败每几十分钟就会发生一次。因此，高质量的 log, error tracing 和 profiling 工具，对 LLM training Infra 稳定性至关重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🔥 非常推荐大家看一下李博杰的这篇博客（&lt;a href=&quot;https://zhuanlan.zhihu.com/p/655402388&quot;&gt;4090 适合做 training or inference 吗？&lt;/a&gt;）。这篇博客通过数据/模型大小以及 gpu compute/memory/通讯层面，讲解了为什么 4090 不适合用于 training，但适合用于 inference。&lt;/p&gt;
&lt;p&gt;🔥 真正有经验的 Infra 工程师，不仅仅是能搭件一个 working 的系统，而是有能力去&lt;strong&gt;从头到尾追踪每一个延迟点&lt;/strong&gt;，把系统之间的关联和可能存在的 bottleneck 拆解成一系列可量化的问题，并在上线后持续做 cost/performance profiling。&lt;strong&gt;这正是 AI Infra （或者传统 Infra）对工程基本功要求极高的原因&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;感觉现在有很多讨论 AI Infra，并且有点把它过度“神化”了。&lt;/p&gt;
&lt;p&gt;是的，LLM 的发展带来了&lt;strong&gt;新形态，新需求，和新的资源瓶颈&lt;/strong&gt;（主要是 GPU memory 和 communication bottleneck，GPU 本身设计就是算力非常强，因为有非常多的 cores）。但是，解决这些问题的工程本质从来没有变：&lt;strong&gt;系统的目标仍然是优化资源利用（降低成本），保障 service 的稳定性，提升吞吐量以及响应能力&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而这些问题，在传统 Infra 中我们已经解决过很多次了。只不过这次，我们需要重新设计整个框架，让它在 GPU 上，高并发 LLM 请求下，仍然能够跑得快，跑得稳。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI Infra 的门槛确实高&lt;/strong&gt;，但是它的高门槛不在于你熟不熟悉神经网络，而&lt;strong&gt;在于你能不能把已有的工程能力（system design thinking and implementation skills）转化为新的问题的视角&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你做过网络通信：你会发现 NCCL 的 ring topology 其实跟设计高性能集群异构调度非常像&lt;/li&gt;
&lt;li&gt;如果你知道缓存以及 OS Paging，你会非常快地理解 KV Cache 的重要性以及管理思路&lt;/li&gt;
&lt;li&gt;如果你写过服务调度器，那 dynamic batching 会让你产生一种这是流水线并发的熟悉感&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;我越来越觉得，AI Infra 是对传统 Infra 知识体系的一种融合以及拓展，是一些旧的问题在新的范式中的 rephrasing&lt;/strong&gt;；真正有竞争力的 AI Infra 工程师，不是只懂如何调个 prompt 或者跑个 inference/finetuning，而是&lt;strong&gt;能把底层系统逻辑与模型特性融合起来的人&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这种 shift of thinking 并不容易，但如果你愿意去搭建起传统 Infra → AI infra 的 mental map，会发现很多传统经验看起来和 AI 毫不相干的东西，其实都有非常相似的部分（俗话说，换汤不换药）。&lt;/p&gt;
&lt;p&gt;所以，传统 Infra 的经验/思维同样适用于 AI Infra，它们两之间有很多关联。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>堆（优先队列）</title><link>https://coooredump.github.io/blog/leetcode/priority_queue</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/priority_queue</guid><description>priority_queue 又称为 Heap</description><pubDate>Sun, 20 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;✅ 优先队列 &lt;code&gt;priority_queue&lt;/code&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;更多关于优先队列的知识，请跳转至：https://en.oi-wiki.org/lang/pb-ds/pq/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;大顶堆：&lt;code&gt;priority_queue&amp;#x3C;int&gt; pq&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;小顶堆：&lt;code&gt;priority_queue&amp;#x3C;int, vector&amp;#x3C;int&gt;, greater&amp;#x3C;&gt;&gt; pq&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;相关例题&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/last-stone-weight/&quot;&gt;1046. 最后一块石头的重量&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/top-k-frequent-elements/&quot;&gt;347. 前 K 个高频元素&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🔥&lt;a href=&quot;https://leetcode.cn/problems/sliding-window-maximum/&quot;&gt;239. 滑动窗口最大值&lt;/a&gt;（还可以手动构造一个单调队列，使用 &lt;code&gt;deque&lt;/code&gt; 数据结构）&lt;/li&gt;
&lt;li&gt;🔥&lt;a href=&quot;https://leetcode.cn/problems/find-median-from-data-stream/&quot;&gt;295. 数据流的中位数&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🔥&lt;a href=&quot;https://leetcode.cn/problems/sliding-window-median/&quot;&gt;480. 滑动窗口中位数&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;__gnu_pbds :: priority_queue&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;附： &lt;a href=&quot;https://gcc.gnu.org/onlinedocs/libstdc++/ext/pb_ds/pq_performance_tests.html#std_mod1&quot;&gt;官方文档地址——复杂度及常数测试&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;ext/pb_ds/priority_queue.hpp&gt;
using namespace __gnu_pbds;
__gnu_pbds ::priority_queue&amp;#x3C;T, Compare, Tag, Allocator&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;模板形参&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T&lt;/code&gt; : 储存的元素类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Compare&lt;/code&gt; : 提供严格的弱序比较类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tag&lt;/code&gt; 是 &lt;code&gt;__gnu_pbds&lt;/code&gt; 提供的不同的五种堆，&lt;strong&gt;&lt;code&gt;Tag&lt;/code&gt; 参数默认是 &lt;code&gt;pairing_heap_tag&lt;/code&gt;&lt;/strong&gt;，这五种分别是：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pairing_heap_tag&lt;/code&gt; ：配对堆，官方文档认为在非原生元素（如自定义结构体/ &lt;code&gt;std :: string&lt;/code&gt; / &lt;code&gt;pair&lt;/code&gt; ) 中，配对堆表现最好&lt;/li&gt;
&lt;li&gt;&lt;code&gt;binary_heap_tag&lt;/code&gt; ：二叉堆，官方文档认为在原生元素中二叉堆表现最好，不过我测试的表现并没有那么好&lt;/li&gt;
&lt;li&gt;&lt;code&gt;binomial_heap_tag&lt;/code&gt; ：二项堆，二项堆在合并操作的表现要优于配对堆*但是其取堆顶元素的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rc_binomial_heap_tag&lt;/code&gt; ：冗余计数二项堆&lt;/li&gt;
&lt;li&gt;&lt;code&gt;thin_heap_tag&lt;/code&gt; ：除了合并的复杂度都和 Fibonacci 堆一样的一个 tag&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Allocator&lt;/code&gt; ：空间配置器，由于 OI 中很少出现，故这里不做讲解&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;由于本篇文章只是提供给学习算法竞赛的同学们，故对于后四个 tag 只会简单的介绍复杂度，第一个会介绍成员函数和使用方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;经作者本机测试堆的基础操作，结合 GNU 官方的复杂度测试，Dijkstra 测试，都表明：至少对于 OIer 来讲，除了配对堆（即默认 tag）的其他四个 tag 都是鸡肋，要么没用，要么常数大到不如 &lt;code&gt;std&lt;/code&gt; 的，且有可能造成 &lt;code&gt;MLE&lt;/code&gt;，故这里只推荐用默认的配对堆。同样，配对堆也优于 &lt;code&gt;algorithm&lt;/code&gt; 库中的 &lt;code&gt;make_heap()&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;构造方式&lt;/h3&gt;
&lt;p&gt;要注明命名空间因为和 &lt;code&gt;std&lt;/code&gt; 的类名称重复。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__gnu_pbds ::priority_queue&amp;#x3C;int&gt; __gnu_pbds::priority_queue&amp;#x3C;int, greater&amp;#x3C;int&gt; &gt;
__gnu_pbds ::priority_queue&amp;#x3C;int, greater&amp;#x3C;int&gt;, pairing_heap_tag&gt;
__gnu_pbds ::priority_queue&amp;#x3C;int&gt;::point_iterator id; // 迭代器
// 在 modify 和 push 的时候都会返回一个 point_iterator，下文会详细的讲使用方法
id = q.push(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;成员函数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push()&lt;/code&gt; : 向堆中压入一个元素，返回该元素位置的迭代器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop()&lt;/code&gt; : 将堆顶元素弹出。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;top()&lt;/code&gt; : 返回堆顶元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt; 返回元素个数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty()&lt;/code&gt; 返回是否非空。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;modify(point_iterator, const key)&lt;/code&gt; : 把迭代器位置的 &lt;code&gt;key&lt;/code&gt; 修改为传入的 &lt;code&gt;key&lt;/code&gt; ，并对底层储存结构进行排序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(point_iterator)&lt;/code&gt; : 把迭代器位置的键值从堆中擦除。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;join(__gnu_pbds :: priority_queue &amp;#x26;other)&lt;/code&gt; : 把 &lt;code&gt;other&lt;/code&gt; 合并到 &lt;code&gt;*this&lt;/code&gt; 并把 &lt;code&gt;other&lt;/code&gt; 清空。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;时间复杂度&lt;/h3&gt;
&lt;p&gt;使用的 tag 决定了每个操作的时间复杂度：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507201751959.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;239. 滑动窗口最大值&lt;/h2&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt;，有一个大小为 &lt;code&gt;k&lt;/code&gt; 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 &lt;code&gt;k&lt;/code&gt; 个数字。滑动窗口每次只向右移动一位。&lt;/p&gt;
&lt;p&gt;返回 滑动窗口中的最大值 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：nums = [1,3,-1,-3,5,3,6,7], k = 3
输出：[3,3,5,5,6,7]
解释：
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    vector&amp;#x3C;int&gt; maxSlidingWindow(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
        // entry: &amp;#x3C;val, idx (whether_expired)&gt;
        priority_queue&amp;#x3C;pair&amp;#x3C;int, int&gt;, vector&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt;, less&amp;#x3C;&gt;&gt; pq;
        for (int i = 0; i &amp;#x3C; k; i++) {
            pq.emplace(nums[i], i);
        }
        vector&amp;#x3C;int&gt; ans{pq.top().first};
        for (int i = k; i &amp;#x3C; nums.size(); i++) {
            pq.emplace(nums[i], i);
            while (pq.top().second &amp;#x3C;= i - k) {
                pq.pop();
            }
            ans.push_back(pq.top().first);
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;295. 数据流的中位数&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;中位数&lt;/strong&gt;是有序整数列表中的中间值。如果列表的大小是偶数，则没有中间值，中位数是两个中间值的平均值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;例如 &lt;code&gt;arr = [2,3,4]&lt;/code&gt; 的中位数是 &lt;code&gt;3&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;例如 &lt;code&gt;arr = [2,3]&lt;/code&gt; 的中位数是 &lt;code&gt;(2 + 3) / 2 = 2.5&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现 MedianFinder 类:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MedianFinder() &lt;/code&gt;初始化 &lt;code&gt;MedianFinder&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void addNum(int num)&lt;/code&gt; 将数据流中的整数 &lt;code&gt;num&lt;/code&gt; 添加到数据结构中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;double findMedian()&lt;/code&gt; 返回到目前为止所有元素的中位数。与实际答案相差 &lt;code&gt;10-5&lt;/code&gt; 以内的答案将被接受。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入
[&quot;MedianFinder&quot;, &quot;addNum&quot;, &quot;addNum&quot;, &quot;findMedian&quot;, &quot;addNum&quot;, &quot;findMedian&quot;]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]

解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1);    // arr = [1]
medianFinder.addNum(2);    // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3);    // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class MedianFinder {
private:
    priority_queue&amp;#x3C;int&gt; left;                          // 大顶堆
    priority_queue&amp;#x3C;int, vector&amp;#x3C;int&gt;, greater&amp;#x3C;&gt;&gt; right; // 小顶堆

public:
    MedianFinder() {}

    void addNum(int num) {
        // 分类讨论, 并且合并为最终两种情况
        if (left.size() == right.size()) {
            right.push(num);
            left.push(right.top());
            right.pop();
        } else {
            left.push(num);
            right.push(left.top());
            left.pop();
        }
    }

    double findMedian() {
        return (left.size() + right.size()) % 2
                   ? left.top()
                   : static_cast&amp;#x3C;double&gt;(left.top() + right.top()) / 2;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;480. 滑动窗口中位数&lt;/h2&gt;
&lt;p&gt;中位数是有序序列最中间的那个数。如果序列的长度是偶数，则没有最中间的数；此时中位数是最中间的两个数的平均数。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[2,3,4]&lt;/code&gt;，中位数是 &lt;code&gt;3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[2,3]&lt;/code&gt;，中位数是 &lt;code&gt;(2 + 3) / 2 = 2.5&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;给你一个数组 &lt;em&gt;nums&lt;/em&gt;，有一个长度为 &lt;em&gt;k&lt;/em&gt; 的窗口从最左端滑动到最右端。窗口中有 &lt;em&gt;k&lt;/em&gt; 个数，每次窗口向右移动 &lt;em&gt;1&lt;/em&gt; 位。你的任务是找出每次窗口移动后得到的新窗口中元素的中位数，并输出由它们组成的数组。&lt;/p&gt;
&lt;p&gt;给出 &lt;em&gt;nums&lt;/em&gt; = &lt;code&gt;[1,3,-1,-3,5,3,6,7]&lt;/code&gt;，以及 &lt;em&gt;k&lt;/em&gt; = 3。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;窗口位置                      中位数
---------------               -----
[1  3  -1] -3  5  3  6  7       1
 1 [3  -1  -3] 5  3  6  7      -1
 1  3 [-1  -3  5] 3  6  7      -1
 1  3  -1 [-3  5  3] 6  7       3
 1  3  -1  -3 [5  3  6] 7       5
 1  3  -1  -3  5 [3  6  7]      6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，返回该滑动窗口的中位数数组 &lt;code&gt;[1,-1,-1,3,5,6]&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;1️⃣ 哈希表 + 逻辑删除&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    priority_queue&amp;#x3C;int&gt; left;                          // 滑动窗口中左半部分
    priority_queue&amp;#x3C;int, vector&amp;#x3C;int&gt;, greater&amp;#x3C;&gt;&gt; right; // 滑动窗口中右半部分
    unordered_map&amp;#x3C;int, int&gt; mp;                        // 用于逻辑删除

    double get(int k) {
        if (k % 2)
            return left.top();
        else
            return ((long long)left.top() + right.top()) / 2.0;
    }

    vector&amp;#x3C;double&gt; medianSlidingWindow(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
        for (int i = 0; i &amp;#x3C; k; i++) {
            int x = nums[i];
            if (left.size() == right.size()) {
                right.push(x);
                left.push(right.top());
                right.pop();
            } else {
                left.push(x);
                right.push(left.top());
                left.pop();
            }
        }
        vector&amp;#x3C;double&gt; ans{get(k)};

        for (int i = k; i &amp;#x3C; nums.size(); i++) {
            int balance = 0; // right.size() - left.size()

            // [fake] delete
            int l_num = nums[i - k];
            mp[l_num]++;
            if (!left.empty() &amp;#x26;&amp;#x26; l_num &amp;#x3C;= left.top()) {
                balance++;
            } else {
                balance--;
            }

            // add
            if (!left.empty() &amp;#x26;&amp;#x26; nums[i] &amp;#x3C;= left.top()) {
                left.push(nums[i]);
                balance--;
            } else {
                right.push(nums[i]);
                balance++;
            }

            // adjust
            if (balance &gt; 0) {
                left.push(right.top());
                right.pop();
            } else if (balance &amp;#x3C; 0) {
                right.push(left.top());
                left.pop();
            }

            // delete
            while (!left.empty() &amp;#x26;&amp;#x26; mp[left.top()] &gt; 0) {
                mp[left.top()]--;
                left.pop();
            }
            while (!right.empty() &amp;#x26;&amp;#x26; mp[right.top()] &gt; 0) {
                mp[right.top()]--;
                right.pop();
            }

            ans.push_back(get(k));
        }
        return ans;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 懒删除堆&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;本题等价于：295. 数据流的中位数 + 懒删除堆&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template&amp;#x3C;typename T, typename Compare = less&amp;#x3C;T&gt;&gt;
class LazyHeap {
    priority_queue&amp;#x3C;T, vector&amp;#x3C;T&gt;, Compare&gt; pq;
    unordered_map&amp;#x3C;T, int&gt; remove_cnt; // 每个元素剩余需要删除的次数
    size_t sz = 0; // 实际大小

    // 正式执行删除操作
    void apply_remove() {
        while (!pq.empty() &amp;#x26;&amp;#x26; remove_cnt[pq.top()] &gt; 0) {
            remove_cnt[pq.top()]--;
            pq.pop();
        }
    }

public:
    size_t size() {
        return sz;
    }

    // 删除
    void remove(T x) {
        remove_cnt[x]++; // 懒删除
        sz--;
    }

    // 查看堆顶
    T top() {
        apply_remove();
        return pq.top();
    }

    // 出堆
    T pop() {
        apply_remove();
        sz--;
        T x = pq.top();
        pq.pop();
        return x;
    }

    // 入堆
    void push(T x) {
        if (remove_cnt[x] &gt; 0) {
            remove_cnt[x]--; // 抵消之前的删除
        } else {
            pq.push(x);
        }
        sz++;
    }

    // push(x) 然后 pop()
    T push_pop(T x) {
        apply_remove();
        pq.push(x);
        x = pq.top();
        pq.pop();
        return x;
    }
};

class Solution {
public:
    vector&amp;#x3C;double&gt; medianSlidingWindow(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
        int n = nums.size();
        vector&amp;#x3C;double&gt; ans(n - k + 1);
        LazyHeap&amp;#x3C;int&gt; left; // 最大堆
        LazyHeap&amp;#x3C;int, greater&amp;#x3C;int&gt;&gt; right; // 最小堆

        for (int i = 0; i &amp;#x3C; n; i++) {
            // 1. 进入窗口
            int in = nums[i];
            if (left.size() == right.size()) {
                left.push(right.push_pop(in));
            } else {
                right.push(left.push_pop(in));
            }

            int l = i + 1 - k;
            if (l &amp;#x3C; 0) { // 窗口大小不足 k
                continue;
            }

            // 2. 计算答案
            if (k % 2) {
                ans[l] = left.top();
            } else {
                ans[l] = ((long long) left.top() + right.top()) / 2.0;
            }

            // 3. 离开窗口
            int out = nums[l];
            if (out &amp;#x3C;= left.top()) {
                left.remove(out);
                if (left.size() &amp;#x3C; right.size()) {
                    left.push(right.pop()); // 平衡两个堆的大小
                }
            } else {
                right.remove(out);
                if (left.size() &gt; right.size() + 1) {
                    right.push(left.pop()); // 平衡两个堆的大小
                }
            }
        }

        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>LLM 黄金时代下的 AI Infra</title><link>https://coooredump.github.io/blog/ai-infra/ai-infra-in-the-era-of-llm</link><guid isPermaLink="true">https://coooredump.github.io/blog/ai-infra/ai-infra-in-the-era-of-llm</guid><description>AI Infra 是连接算力和应用的 AI 中间层基础设施，涵盖了数据准备、模型训练、模型部署和应用整合等环节，其中的基础软件工具有较高商业化潜力；目前 AI Infra 产业处于高速增长的发展早期，未来几年内各细分赛道有望保持 30%+ 高速增长。</description><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;类比计算机系统的基础软件层以及云计算三层架构的 PaaS 层级，我们认为，AI 产业链中也有层级相似，定位于算力与应用之间的“桥梁”角色的基础软件设施层即 AI Infra。新一轮生成式 AI 浪潮，对于上层应用而言机遇与挑战并存，而 AI Infra 作为必要的基础设施，我们认为其技术及商业发展前景的确定性或更强。本文我们聚焦 AI Infra，揭示其内涵并总结目前国内外项目的商业化进展，再从工作流视角详细梳理各环节及代表厂商。&lt;strong&gt;我们认为，AI Infra 是 AI 产业必不可少的基础软件堆栈，“掘金卖铲”逻辑强、商业潜质高，建议投资者持续关注 AI Infra 相关投资机会&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181309148.jpeg&quot; alt=&quot;AI 产业链&quot;&gt;&lt;/p&gt;
&lt;h2&gt;摘要&lt;/h2&gt;
&lt;p&gt;在预训练大模型时代，我们可以从应用落地过程里提炼出标准化的工作流，AI Infra 的投资机会得以演绎。传统 ML 时代 AI 模型通用性较低，项目落地停留在“手工作坊”阶段，流程难以统一规范。而大规模预训练模型统一了“从 0 到 1”的技术路径，具备解决问题的泛化能力，能够赋能“从 1 到 100”的各类应用，并存在相对标准化的工作流，由此衍生出 AI Infra 投资机会。GPT-4 的开发经验也体现专业分工的必要性：根据 OpenAI 的披露，在 GPT-4 的开发过程中，其对 249 人研发团队进行了明确分工，并使用了数据标注、分布式计算框架、实验管理等点工具。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们认为这也说明了在大模型时代应用基础软件的必要性。目前，AI Infra 产业处于高速增长的发展早期，我们预计未来几年内各细分赛道空间或保持 30%+ 的高速增长，且各方向均有变现实践与养成独角兽企业的潜力。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;“AI = Data + Code”&lt;/strong&gt;，组织 AI 所需的养料即数据，管理 AI 模型的训练部署过程，以及支持从模型到应用的整合是 AI Infra 工具的关键能力。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据准备&lt;/strong&gt;：无论是支持经典的机器学习模型还是大规模预训练模型，数据准备都是耗时较久、较为关键的一环。我们认为，LLM 浪潮下高质量的标注数据和特征库需求将持续增长，未来海量训练数据的需求或由合成数据满足。此外，我们强调 Data + AI 平台厂商的关键卡位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型训练&lt;/strong&gt;：预训练模型的获取使得模型库更加流行，LLM 大规模训练需求也驱动底层分布式计算引擎和训练框架的迭代。此外，我们认为实验管理工具重要性较高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型部署&lt;/strong&gt;：LLM 模型端的突破释放出大规模应用落地的潜能，更多模型从实验走向生产环境，我们认为有望整体提振模型部署和监控的需求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用整合&lt;/strong&gt;：LLM 赋能应用催生对向量数据库和应用编排工具等的新需求。我们观察到经典的机器学习时代与大模型时代工具栈需求侧重点有所不同，同时，部分点工具正在拓宽产品功能边界，LLMOps 平台型产品的可及市场空间天花板或更高。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;AI Infra 是连接算力和应用的 AI 中间层基础设施&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;资料来源：Grand View Research，Foresight News，Gartner，MarketsandMarkets，拾象科技，Firstmark，a16z，各公司官网，中金公司研究部&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181315126.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;本章主要讨论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI Infra 在 AI 时代 IT 生态中的定位&lt;/li&gt;
&lt;li&gt;为什么大模型浪潮下需要格外关注 AI Infra 投资机会&lt;/li&gt;
&lt;li&gt;AI Infra 基础软件工具栈涵盖内容&lt;/li&gt;
&lt;li&gt;AI Infra 商业化初探&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AI Infra 是 AI 时代的中间层基础设施&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;从类比的角度理解 AI Infra：AI 时代连接硬件和上层应用的中间层基础设施&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;✅ 传统本地部署时代：三大基础软件（&lt;strong&gt;数据库&lt;/strong&gt;、&lt;strong&gt;操作系统&lt;/strong&gt;、&lt;strong&gt;中间件&lt;/strong&gt;）实现控制硬件交互、存储管理数据、网络通信调度等共性功能，抽象并隔绝底层硬件系统的复杂性，让上层应用开发者能够专注于业务逻辑和应用功能本身的创新实现。&lt;/p&gt;
&lt;p&gt;☁️ 云时代同理，形成了 &lt;code&gt;IaaS&lt;/code&gt;、&lt;code&gt;PaaS&lt;/code&gt;、&lt;code&gt;SaaS&lt;/code&gt; 三层架构，其中 PaaS 层提供应用开发环境和基础的数据分析管理服务。&lt;/p&gt;
&lt;p&gt;🤖 所以类比来看，我认为，进入 AI 时代也有承担类似功能的、连接算力和应用的基础设施中间层即 &lt;code&gt;AI Infra&lt;/code&gt;，提供基础模型服务、赋能模型微调和应用开发。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181323671.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;LLM 催生 AI Infra 投资机会&lt;/h3&gt;
&lt;p&gt;LLM 流行前，AI 模型通用性较低，项目落地停留在“手工作坊”阶段，流程难以统一规范。人工智能已有数十年的发展历史，尤其是 2006 年以来以深度学习为代表的训练方法的成熟推动第三波发展浪潮。然而，&lt;strong&gt;由于传统的机器学习模型没有泛化能力，大部分 AI 应用落地以定制化项目的形式，包括需求、数据、算法设计、训练评估、部署和运维等阶段，其中，数据和训练评估阶段往往需要多次循环，较难形成一套标准化的端到端的流程和解决方案，也由此造成了边际成本高、重复造轮子等问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;大规模预训练模型完成了“从 0 到 1”的技术统一，泛化能力和通用性释放出“从 1 到 100”的落地需求，且存在相对标准化的流程，衍生出 AI Infra 投资机会。基于 Transformer 算法、超大参数量的预训练模型拥有泛化能力，一定程度上解决了原先需要按项目定制训练的问题，过去正因为 ML 模型的非标和项目制，下游需求并未被完全激发出来，LLM 模型端的突破释放出更大规模的应用落地潜能。而后续的应用过程中主要涉及：高质量样本数据的准备、基础模型获取、模型微调及部署监控、应用编排开发上线等环节，工作流较为标准化，我们建议投资者持续关注 AI Infra 投资机会。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181327914.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;AI Infra 基础软件工具栈&lt;/h3&gt;
&lt;p&gt;参考海外 OpenAI 的率先尝试，工作流分工、点工具加持助力成功。一方面，OpenAI 在《GPT-4 Technical Report》论文中披露了参与 GPT-4 开发的人员分工，共 249 人，角色分工明确，预训练、强化学习和对齐、部署等 6 个大方向下又拆分成不同小组，其中数据集/数据基础设施、分布式训练基础设施、推理基础设施等分别对应工作流中的数据准备、模型训练、部署应用等环节；另一方面，OpenAI 使用了 Scale 数据标注服务、Ray 分布式计算框架和 Weights and Biases（W&amp;#x26;B）实验管理工具，且 W&amp;#x26;B 的创立灵感就来自于其创始人之一在 OpenAI 的实习经历。我们认为，OpenAI 的率先尝试经验一定程度上说明专业分工和 AI Infra 基础软件堆栈在大模型时代的必要性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181329085.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI Infra 广义上包含了基础模型和基础软件栈两层，本篇报告核心关注其中和工作流相关的基础软件工具栈&lt;/strong&gt;。工作流的视角下，LLM 的开发应用主要涉及&lt;strong&gt;数据准备&lt;/strong&gt;、&lt;strong&gt;模型训练&lt;/strong&gt;、&lt;strong&gt;模型部署&lt;/strong&gt;、&lt;strong&gt;产品整合&lt;/strong&gt;四个主要环节，每个环节都有对应的点工具，亦有集大成的 LLMOps 平台型产品，我们将在下一章详细解读。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181331132.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;AI Infra 商业化&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;商业化起步中，已有变现实践，细分赛道或均有长出独角兽的潜力&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;商业化起步阶段，有望在未来几年快速成长为百亿美元量级的产业。我们认为，AI Infra 整体处于高速增长的发展早期，根据第三方数据，目前大部分细分赛道规模在几亿至几十亿美元量级，我们预计在未来几年内或将保持 30+% 的高速增长。同时，Data + AI、MLOps/LLMOps 等平台型产品的市场空间天花板可能更高，我们也观察到点工具厂商正在积极拓展产品边界。我们认为，AI Infra 是 AI 时代不可或缺的基础设施中间层，“掘金卖铲”逻辑的确定性高，有望持续受益于 LLM、AI 应用的繁荣。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181334064.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;海外厂商积极探索变现，细分赛道或均有长出独角兽的潜力。从微观的视角，我们整理了 AI Infra 各细分赛道海外代表公司的商业模式，基本遵循按使用量付费的定价模式。大多数创业公司成立时间较短，目前收入体量在数千万至小几亿美元量级，其中数据相关的、平台型的厂商起步较早、已初具规模，我们认为这也符合数据需要前置于 AI 模型投入、平台型厂商收入天花板更高的逻辑。此外，我们认为 LLM 模型端突破将释放出更大规模应用落地的潜能，有望带动模型部署、应用整合等后续环节的逐步起量。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181335288.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;从工作流视角梳理 AI Infra 投资机会&lt;/h2&gt;
&lt;h3&gt;大模型时代和传统机器学习时代工具栈侧重点有所不同&lt;/h3&gt;
&lt;p&gt;本章从企业训练模型、构建 AI 赋能应用的工作流视角出发，详解涉及的主要环节，并关注 LLMOps 和 MLOps 在流程上的侧重点差异。我们认为 AI = Data + Code，历经数据准备、模型训练、模型部署、产品整合，分环节看：&lt;/p&gt;
&lt;p&gt;► &lt;strong&gt;数据准备&lt;/strong&gt;：高质量标注数据、特征库需求持续，合成数据或成未来趋势。数据准备无论在传统的 MLOps 还是 LLMOps 中都是耗时较久、较为重要的一环。无监督学习降低对标注数据的需求，但 RLHF 机制体现了高质量标注数据的重要性，我们认为未来超大参数量模型对海量训练数据的需求或由合成数据满足。此外，Data + AI 平台厂商卡位关键。&lt;/p&gt;
&lt;p&gt;► &lt;strong&gt;模型训练&lt;/strong&gt;：模型库更加刚需，训练框架持续迭代，软件工具协助实验管理。基于通用的 LLM 大模型微调、蒸馏出小模型成为高性价比的落地方式，因此需要能够高效便捷地获取预训练模型的模型库；也催生更适应 LLM 大规模训练需求的底层分布式计算引擎和训练框架。此外，我们认为实验管理工具的重要性或始终较高。&lt;/p&gt;
&lt;p&gt;► &lt;strong&gt;模型部署&lt;/strong&gt;：更多模型从实验走向真实业务环境，部署和监控需求提升。我们认为，LLM 模型端的突破释放出大规模应用落地的潜能，更多的模型从实验环境走向生产环境，有望整体提振模型部署和监控的需求。&lt;/p&gt;
&lt;p&gt;► &lt;strong&gt;应用整合&lt;/strong&gt;：催生向量数据库和应用编排框架新需求。LLM 赋能应用催生出对应用产品整合相关工具产品的需求，其中较为关键的是向量数据库和应用编排工具。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181340897.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;数据准备：高质量标注数据、特征库需求持续，合成数据或成未来趋势&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;数据是模型的起点，一定程度上决定了模型的效果和质量，数据准备无论在传统的 MLOps 还是 LLMOps 中都是耗时较久、较为重要的一环&lt;/strong&gt;。LLM 带来的新变化主要包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;虽然 LLM 的无监督学习机制降低了对标注数据的需求，但 OpenAI 的 RLHF 体现了高质量标注数据重要性；&lt;/li&gt;
&lt;li&gt;模型规模大幅提升，带来日益增长的训练数据需求，长期看可能无法仅通过真实世界数据满足，合成数据提供一种 AIGC 反哺 AI 的解法。此外，数据基础管理软件平台的卡位始终关键，Data + AI 平台化趋势持续演进。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;数据标注&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;GPT 的成功说明了高质量标注数据对提升模型效果的重要性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;数据标注位于模型开发的最上游，对图像、视频、文本、音频等非结构化原始数据添加标签，为 AI 提供人类先验知识的输入。近年，无监督学习、强化学习等不需要标注数据的机器学习分支方法论的出现引发市场对于数据标注必要性的讨论与担忧。&lt;/p&gt;
&lt;p&gt;不过，OpenAI 通过 RLHF 即基于人类反馈的强化学习来优化模型，且从 OpenAI 披露的分工中能看到有很多负责预训练、强化学习等的 AI 科学家也参与到数据准备中；开源的 &lt;a href=&quot;https://arxiv.org/abs/2307.09288&quot;&gt;LLAMA 2&lt;/a&gt; 的论文中也有一段强调高质量数据对模型训练结果影响的表述，Meta 与第三方供应商合作收集了近 3 万个高质量标注，又向市场证明了高质量数据标注工作的重要性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181344886.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据标注厂商正在寻求智能化转型、减少对人力的依赖&lt;/strong&gt;。在数据标注助力 AI 快速发展的同时，AI 也将反哺数据标注更加自动化、智能化，如利用模型进行数据预处理再人工审核等。Meta AI 发布的 Segment Anything Model 的训练数据集 SA-1B，就是通过智能数据引擎来辅助自动化生成的，该数据引擎经历了辅助手动标注-半自动标注-自动化标注的训练过程。&lt;/p&gt;
&lt;h4&gt;特征库&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;特征库（Feature Store）：高质量特征库持续受益&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;特征是预测模型的输入信号，可以简单理解为模型中的自变量 X，需要经过特征工程从原始数据中筛选得到。而特征库则是生产、管理、运营 ML 过程中所需数据及特征的系统，主要实现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;运行各类数据管道（Pipeline）将原始数据转换为特征值；&lt;/li&gt;
&lt;li&gt;存储和管理特征和数据；&lt;/li&gt;
&lt;li&gt;为训练和推理提供一致的特征服务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;目前该领域的代表性产品包括：开源项目如 Feast，独立商业化公司如 Tecton，大型科技厂商的 ML 平台如 Databricks、SageMaker 等中亦有相应模块。数据和特征的质量决定了机器学习的上限，我们认为高质量特征库有望持续受益，同时国内数据要素市场的蓬勃发展长期看有望为 AI 模型供应更多高质量的数据燃料。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181346863.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;合成数据&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;合成数据：做真实数据的“平替”，用 AIGC 反哺 AI&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一项来自 Epoch AI Research 团队的研究预测存量的高质量语言数据将在 2026 年耗尽，低质量的语言和图像数据存量也将在未来的数十年间枯竭。面对潜在的数据瓶颈，合成数据即运用计算机模拟生成的人造数据，提供了一种成本低、具有多样性、规避了潜在隐私安全风险的解决方法，生成式 AI 的逐渐成熟进一步提供技术支撑。比如，自然语言修改图片的 Instruct-Pix2Pix 模型在训练的时候就用到 GPT-3 和 Stable Diffusion 来合成需要的提示词和图像的配对数据集；Amazon 也利用合成数据来训练智能助手 Alexa，以避免用户隐私问题。合成数据市场参与者较多，独立公司/项目如 gretel、MOSTLY AI、datagen、hazy 等，数据标注厂商如 Scale 亦推出相关产品，此外主流科技公司英伟达、微软、亚马逊等均有不同场景的尝试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181350151.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Data + AI 是行业趋势&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;数据科学基础平台：数据卡位始终关键，Data + AI 是行业趋势&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;广义的数据科学涵盖利用各类工具、算法理解数据蕴藏含义的全过程，机器学习可以视为其中的一种方式和手段；狭义的数据科学也可以仅指代机器学习的前置步骤，包括准备、预处理数据并进行探索性分析等。&lt;/p&gt;
&lt;p&gt;正如我们从报告&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=MzI3MDMzMjg0MA==&amp;#x26;mid=2247626539&amp;#x26;idx=2&amp;#x26;sn=16f09dd8cc12354dbe8667b9de401bd3&amp;#x26;chksm=eade09ecdda980faf65b9326f6ccaf2a92e197e241e6e5ac3ef1a042d67d901c81af112134a7&amp;#x26;scene=21#wechat_redirect&quot;&gt;《人工智能十年展望（八）：探索 ChatGPT 根基——数据与人工智能如何相互成就？》&lt;/a&gt;开始一直强调的观点，数据和 AI 一体两翼，数据是模型的起点、且一定程度上决定了模型的最终效果和质量，数据基础设施厂商卡位关键，从 Data 向 AI 布局是技术能力和业务逻辑的自然延伸。LLM 等大模型的渗透发展不仅额外增加了数据平台上 AI 相关的工作流负载，还可以带动底层 Data 基础设施的需求。&lt;/p&gt;
&lt;h3&gt;模型训练：模型库更加刚需，训练框架持续迭代，软件工具协助实验管理&lt;/h3&gt;
&lt;p&gt;大模型具有一定通用性，开发者们可以“站在巨人的肩膀上”，在预训练模型的基础上通过少量增量训练蒸馏出专精的小模型以解决垂类场景的需求。&lt;/p&gt;
&lt;p&gt;LLM 带来的新变化主要包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;要想高效便捷地获取模型，则需要一个集成托管各类模型的社区也即模型库；&lt;/li&gt;
&lt;li&gt;催生更适应 LLM 大规模训练需求的底层分布式计算引擎和训练框架。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此外，模型训练过程涉及多次往复的修改迭代，无论是 ML 还是 LLM 都需要借助实验管理工具进行版本控制和协作管理。&lt;/p&gt;
&lt;h4&gt;模型库&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;模型库（Model Hub）：把握从数据到模型的工作流入口&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;模型库顾名思义是一个&lt;strong&gt;托管、共享了大量开源模型的平台社区&lt;/strong&gt;，供开发者下载各类预训练模型，除模型外，主流的 Model Hub 平台上还同时提供各类共享的数据集、应用程序 Demo 等，是 AI、ML 细分领域的“GitHub”。&lt;/p&gt;
&lt;p&gt;典型代表厂商包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;海外的 &lt;strong&gt;Hugging Face&lt;/strong&gt;、Replicate&lt;/li&gt;
&lt;li&gt;国内关注 Gitee（开源中国推出的代码托管平台）和 ModelScope（阿里达摩院推出的 AI 开源模型社区）等项目&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在商业模型上，Model Hub 厂商一般选择切入下游的 AutoTrain（自动创建、优化、评估模型）或模型推理服务，也在尝试就 Model Hub 功能收取订阅制会员费用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181357112.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;分布式计算和深度学习框架&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;分布式计算和深度学习框架：大模型“炼丹炉”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;分布式计算引擎方面，LLM 的训练过程需要大规模的 GPU 分布式计算集群，过去大数据已带动了以 &lt;strong&gt;MapReduce&lt;/strong&gt;、&lt;strong&gt;Spark&lt;/strong&gt; 为代表的分布式计算引擎的发展，但以 Ray 为代表的近年在 AI 大潮下兴起的分布式计算框架则更贴合 AI 需求（Ray 的首篇论文名为&lt;a href=&quot;https://www.usenix.org/system/files/osdi18-moritz.pdf&quot;&gt;《Ray: A Distributed Framework for Emerging AI Applications》&lt;/a&gt;），其核心模块 Ray Tune、Ray Rllib、Ray Train 分别对应机器学习调参、强化、深度学习调参的流程。Ray 在官网的用户案例中表示“Ray 是使 OpenAI 能够增强其训练 ChatGPT 和类似模型能力的关键”。此外，Ray 作为更底层的分布式计算引擎，和 TensorFlow、PyTorch 等深度学习框架兼容，而 DeepSpeed、ColossalAI 等则是在 PyTorch 等基础框架之上针对 LLM 的优化训练设计的新一代框架。&lt;/p&gt;
&lt;h4&gt;实验管理&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;实验管理：记录实验元数据，辅助版本控制，保障结果可复现&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;模型训练是一种实验科学，需要反复的修改与迭代，同时由于无法提前预知实验结果往往还涉及版本回溯、多次往复，因此模型的版本控制和管理就较为必要，实验管理软件可以辅助技术人员和团队追踪模型版本、检验模型性能。该领域代表厂商为 Weights and Biases（W&amp;#x26;B）和 Neptune，跟踪机器学习实验，记录实验元数据，包括训练使用数据集、框架、进度、结果等，支持以可视化的形式展现结果、多实验结果对比、团队协作共享等。此外，实验管理也是 LLMOps/MLOps 平台型产品如星环科技 Sophon、Google Vertex AI 等产品中的重要模块之一。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181400486.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;模型部署：更多模型从实验走向真实业务环境，部署和监控需求提升&lt;/h3&gt;
&lt;p&gt;模型部署是让模型从实验环境走向真实生产环境的重要环节，借助&lt;strong&gt;模型部署&lt;/strong&gt;工具能够解决模型框架兼容性差的问题并提升模型运行速度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模型监控&lt;/strong&gt;通过对模型输出结果和性能指标的追踪，保障模型上线后的可用性。&lt;/p&gt;
&lt;p&gt;我们认为，过去由于 ML 模型的非标和项目制，大规模、持续性的模型部署和监控需求未被完全激发出来，LLM 模型端的突破释放出大规模应用落地的潜能，更多的模型从实验环境走向生产环境，我们认为有望整体提振模型部署和监控的需求。&lt;/p&gt;
&lt;h4&gt;模型部署&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;模型部署：从实验走向生产的重要环节&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;模型部署指把训练好的模型在特定环境中运行，需要尽量最大化资源利用效率，保证用户使用端的高性能。模型部署领域参与者较多，比如 Ray、Tensorflow、PyTorch 等训练框架都提供配套的模型部署功能，模型库厂商如 Hugging Face、实验管理厂商如 W&amp;#x26;B 也有相关产品，此外还有如 Seldon、BentoML、OctoML 等独立项目/产品。和训练框架自带的部署模块相比，三方的综合性产品能够为不同框架下训练出来的模型提供一套相对统一的部署方式。以 Seldon 为例，在复杂的多模型推理场景下，Seldon 通过模型可解释性、异常值检测等模块，最终选出表现最好的模型进行结果反馈。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181402930.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;模型监控&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;模型监控：模型可观测性保障可靠可用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可观测性在传统 IT 系统运维中就是重要的数智化手段之一，通过监控各类机器、系统的运行数据对故障和异常值提前告警。模型监控同理，监测模型上线后的数据流质量以及表现性能，关注模型可解释性，对故障进行根因分析，&lt;strong&gt;预防数据漂移、模型幻觉等问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;模型可观测性领域有较多创业公司，包括 Fiddler、WhyLabs、Evidently AI 等，实验管理厂商如 W&amp;#x26;B、模型部署厂商如 Seldon 也有所涉及，此外，传统的 IT 运维可观测性厂商也有机会切入 AI 模型监控领域，海外如 Datadog 已经尝试将 Open AI 的模型服务加入纳管范畴，我们也建议关注国内相关厂商的后续进展。&lt;/p&gt;
&lt;h3&gt;应用整合：催生向量数据库和应用编排框架新需求&lt;/h3&gt;
&lt;p&gt;正如前文提及，LLM 模型端的突破释放出更多应用落地的潜能，由此&lt;strong&gt;催生出对应用产品整合相关工具产品的需求，其中较为关键的是向量数据库和 LLM 应用编排工具&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;/documents/academic/%E5%90%91%E9%87%8F%E6%95%B0%E6%8D%AE%E5%BA%93%C2%B7%E7%A7%91%E6%99%AE.pptx&quot;&gt;向量数据库&lt;/a&gt;&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;向量数据库：LLM 的外部知识库&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;让通用大模型具备专业知识主要有两种途径，一是通过微调将专有知识内化到 LLM 中；另一种则是利用向量数据库给 LLM 增加外部知识库，后者成本更低。&lt;/p&gt;
&lt;p&gt;向量数据库和 LLM 的具体交互过程为：用户首先将企业知识库的全量信息通过嵌入模型转化为向量后储存在向量数据库中，用户输入 &lt;code&gt;prompt&lt;/code&gt; 时，先将其同样向量化，并在向量数据库中检索最为相关的内容，再将检索到的相关信息和初始 &lt;code&gt;prompt&lt;/code&gt; 一起输入给 LLM 模型，以得到最终返回结果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181410989.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;向量化技术本身已较为成熟，海外模型如 &lt;strong&gt;Word2Vec&lt;/strong&gt;、FastText 等，国内中文 Embedding 模型有 MokaAI 开源的 M3E、IDEA CCNL 开源的二郎神系列。向量数据库厂商/产品主要包括 &lt;strong&gt;Pinecone&lt;/strong&gt;、&lt;strong&gt;Zilliz&lt;/strong&gt;、星环科技 Hippo 等，另外也有传统数据库、大数据平台厂商如 PGSQL、Databricks 通过&lt;strong&gt;增加向量查询引擎插件来实现支持&lt;/strong&gt;。我们认为，向量数据库是 AI Answers 类应用落地的刚需，同时本土厂商在中文 Embedding 方面可能更具优势。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181418267.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;应用编排框架&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;应用编排框架：LLM 应用“粘合剂”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;LLM 应用编排框架是一个封装了各种大语言模型应用开发所需逻辑和工具的代码库，LangChain 是当下最流行的框架之一，还有 Anarchy、Dust、AutoGPT、LlamaIndex 等。初始化的大模型存在无法联网、无法调用其他 API、无法访问本地文件、对 Prompt 要求高、生成能力强但内容准确度无法保证等问题，应用编排框架提供了相应功能模块，帮助实现从 LLM 到最终应用的跨越。&lt;/p&gt;
&lt;p&gt;以 LangChain 为例，它主要包含以下几个模块：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Prompt 实现指令的补全和优化；&lt;/li&gt;
&lt;li&gt;Chain 调用外部数据源、工具链；&lt;/li&gt;
&lt;li&gt;Agent 优化模块间的调用顺序和流程；&lt;/li&gt;
&lt;li&gt;Memory 增加上下文记忆。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;集成开发环境&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;集成开发环境：交互式 Notebook 逐渐流行&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在上述 AI 建模流程中，开发者需要处理大量代码编写、分析、编译、调试等工作，可以直接在对应环节或平台型产品的内置环境中进行，也可以使用专门的集成开发环境并调取所需功能。其中，Notebook 是一种交互式的开发环境，和传统的非交互式开发环境相比，Notebook 可以逐单元格（Cell）编写和运行程序，出现错误时，仅需调整并运行出现错误的单元格，大大提升开发效率，因此近年逐渐流行、深受数据科学家和算法工程师的喜爱，被广泛应用于 AI 算法开发训练领域。&lt;/p&gt;
&lt;h3&gt;LLMOps 一站式解决方案或更适应国内市场&lt;/h3&gt;
&lt;p&gt;前文我们详细介绍了模型训练、构建应用工作流涉及的主要环节及各环节点工具厂商，事实上，&lt;strong&gt;这些厂商在强项环节之外亦不断拓宽产品能力边界&lt;/strong&gt;，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据标注厂商 Scale AI 拓展合成数据业务并正在投入 LLMOps 领域的 Scale Spellbook（做一个基于大语言模型的开发者工具平台）；&lt;/li&gt;
&lt;li&gt;模型库厂商 Hugging face 切入 AutoTrain 和模型部署；&lt;/li&gt;
&lt;li&gt;实验管理厂商 W&amp;#x26;B 切入模型部署和模型监控等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;MLOps/LLMOps 提供一站式平台解决方案，可及市场空间更大，多采取 Data + AI 一体化战略&lt;/strong&gt;。除此之外还有平台型的 MLOps/LLMOps 产品，基本涵盖了上述流程的主要环节，大型科技企业、数据基础软件厂商均参与其中。&lt;/p&gt;
&lt;p&gt;我们认为，基于整体数字化进程和软件付费意愿习惯判断，海外企业客户可能倾向于选取各环节点工具自组工具栈，而国内客户可能倾向于一站式的解决方案。此外，从目前 AI Infra 领域独角兽的估值水平来看，平台型厂商多采取 Data + AI 一体化战略，起步较早、规模天花板更高。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507181416723.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>一文详解 26 届各大厂人才计划</title><link>https://coooredump.github.io/blog/recruitment/26-top-talent</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/26-top-talent</guid><description>从研究「腾讯青云计划」和「阿里星计划」到围观「字节筋斗云人才计划」，恰逢今年京东官宣「TGT·顶尖青年技术天才计划」，就从我的个人视角盘一盘各大厂项目的情况，顺便分析下各大厂推出的技术人才孵化器到底值不值得冲。</description><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、大厂人才项目认知：卷上天的技术圈入场券&lt;/h2&gt;
&lt;p&gt;大厂越来越卷战略级人才项目了，这类项目我个人觉得，本质上是&lt;strong&gt;企业技术护城河的人才储备计划&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;根据我的发现，在互联网大厂的人才争夺战中，核心技术人才项目应该是已经成为战略储备：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;腾讯青云计划聚焦 AI 算法与云计算，侧重基础研究能力；&lt;/li&gt;
&lt;li&gt;字节筋斗云人才计划覆盖大模型应用等前沿领域，强调快速迭代创新；&lt;/li&gt;
&lt;li&gt;阿里星计划通过滚动招聘锁定多模态大模型与自动驾驶人才，注重技术落地效率；&lt;/li&gt;
&lt;li&gt;今年的京东 TGT 主要研究方向包括多模态大模型与应用、机器学习、搜索推荐广告、空间与具身智能等，比较聚焦技术突破和有社会价值的前沿课题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;「腾讯青云计划」高门槛的技术理想主义&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;项目定位&lt;/strong&gt;：升级后聚焦 AI 大模型、基础设施等十大领域，要求候选人有技术执着且能穿透技术本质。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适合人群&lt;/strong&gt;：适合铁心走技术专家路线、能扛高压的学术派，双非学生基本没戏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;年薪范围&lt;/strong&gt;：70-200w&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;招聘规模&lt;/strong&gt;：每年约 80人&lt;/p&gt;
&lt;h3&gt;「字节筋斗云计划」创新压力下的极限挑战&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;课题设置&lt;/strong&gt;：2025 年开放 42 个前沿课题，涵盖大模型应用、AI Coding 等方向，要求博士在顶会发表过论文。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适合人群&lt;/strong&gt;：之前听过牛客社区大佬提到，项目考核还挺严苛的严苛，适合抗压能力强且追求快速产出的实战派。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;年薪范围&lt;/strong&gt;：100-150w&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;招聘规模&lt;/strong&gt;：每年约 20 人&lt;/p&gt;
&lt;h3&gt;「阿里星计划」学术派的终极战场&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;选拔标准&lt;/strong&gt;：仅招顶尖博士生，要求在 AI、云计算等领域有突破性成果，入职即参与达摩院核心项目。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适合人群&lt;/strong&gt;：学术顶会、工程实践高手、技术大神，压力是比较大的，需要长期闭关搞科研。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;年薪范围&lt;/strong&gt;：120-140w&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;招聘规模&lt;/strong&gt;：每年约 40 人&lt;/p&gt;
&lt;h3&gt;「京东 TGT 计划」技术与应用的实战主义&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;选拔标准&lt;/strong&gt;：招聘范围本硕博，近百个课题可选择，技术专家带教，直接参与智能仓储系统优化、物流机器人路径规划等落地项目。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适合人群&lt;/strong&gt;：薪资不设上限是今年京东 TGT 的亮点之一，与其他大厂项目侧重实验室研发不同，TGT 学员从入职起即深度参与京东核心业务场景，目前项目也在进行中，上车时机比较不错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;年薪范围&lt;/strong&gt;：不设上限&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;招聘规模&lt;/strong&gt;：今年第一年尚未明确，不过看了一下京东 TGT 有近百个课题，我觉得招聘量级应该不小&lt;/p&gt;
&lt;h2&gt;二、选择大厂人才项目的考量因素&lt;/h2&gt;
&lt;p&gt;作为学生党，选大厂项目就像选赛道，没有绝对的好坏，只有适配与否。通过整理了牛客社区内一些大佬的帖子以及我的对大厂项目的了解，在选择大厂人才项目上可以关注的因素。&lt;/p&gt;
&lt;h3&gt;1. 项目定位与职业规划的匹配度&lt;/h3&gt;
&lt;p&gt;商业融合类（如腾讯青云）：适合技术与管理结合的复合人才，需平衡技术深度与商业化落地能力&lt;/p&gt;
&lt;p&gt;业务融合类（如京东 TGT）：适合学术背景强、技术应用于业务的学生，以及关注新兴领域（如零售技术）、希望技术快速落地，需适应业务场景驱动的研发节奏。&lt;/p&gt;
&lt;h3&gt;2. 上车门槛与自身履历的匹配度&lt;/h3&gt;
&lt;p&gt;学历与成果要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阿里星侧重顶尖学术成果（如顶会论文、专利），双非学生几乎无机会&lt;/li&gt;
&lt;li&gt;京东 TGT 对学历包容性更高，本硕博学生均可以申请&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外从招聘规模来看，普遍是低于秋招，但京东 TGT 在几个大厂项目中招聘规模最大，竞争压力会低于其他项目。&lt;/p&gt;
&lt;h3&gt;3. 资源支持与未来规划的匹配度&lt;/h3&gt;
&lt;p&gt;导师配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;腾讯青云提供 “岗位导师 + 发展导师 + 项目顾问” 三轨制，导师多为首席科学家&lt;/li&gt;
&lt;li&gt;京东 TGT 首创 “技术 + 业务 + 成长” 三导师制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;薪酬方面：盘点了一下，基本上所有大厂都能给到年薪百万的水平，但今年京东 TGT 提出的薪酬不设上限还是挺值得期待一波的。&lt;/p&gt;</content:encoded><h:img src="/_astro/202507181223179.BYD3G4kK.png"/><enclosure url="/_astro/202507181223179.BYD3G4kK.png"/></item><item><title>C++ 性能优化</title><link>https://coooredump.github.io/blog/cpp/when-nanoseconds-matter-ultrafas-trading-systems-in-c</link><guid isPermaLink="true">https://coooredump.github.io/blog/cpp/when-nanoseconds-matter-ultrafas-trading-systems-in-c</guid><description>最近看了 Cppcon24 的一个分享 &quot;When Nanoseconds Matter, Ultrafast Trading Systems in C++&quot;，是顶级量化交易公司 Optiver 的工程师 David Gross 分享了构建低延时交易系统的一些思考与做法，列出了一些性能优化的指导原则。看完之后感觉干货满满，学到了很多 C++ 优化技巧，于是加入自己的理解，整理记录一下。</description><pubDate>Tue, 08 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Preface&lt;/h2&gt;
&lt;p&gt;最近看了 &lt;a href=&quot;https://github.com/CppCon/CppCon2024&quot;&gt;CppCon24&lt;/a&gt; 的一个分享 &quot;&lt;a href=&quot;https://www.bilibili.com/video/BV12LXoYCEki/&quot;&gt;When Nanoseconds Matter: Ultrafast Trading Systems in C++&lt;/a&gt;&quot;，是顶级量化交易公司 &lt;code&gt;Optiver&lt;/code&gt; 的工程师 &lt;strong&gt;David Gross&lt;/strong&gt; 分享了构建低延时交易系统的一些思考与做法，列出了一些性能优化的指导原则。看完之后感觉干货满满，学到了很多 C++ 优化技巧，于是加入自己的理解，整理记录一下。&lt;/p&gt;
&lt;h2&gt;Principle #1: &quot;Most of the time, you don&apos;t want node containers&quot;&lt;/h2&gt;
&lt;p&gt;作者首先以一个订单簿 (Order Book) 的实现为例。订单簿由不同价格档位 (&lt;code&gt;&amp;#x3C;price, volume&gt;&lt;/code&gt;) 组成，需按 &lt;code&gt;price&lt;/code&gt; 排序，并支持高频的插入、修改和删除。正常第一反应就是使用 &lt;code&gt;std::map&lt;/code&gt; 数据结构来实现。&lt;/p&gt;
&lt;p&gt;Ok，先用 &lt;code&gt;std::map&lt;/code&gt; 实现一个基准版本。这里学到的一点是，跑基准要尽量模拟生产环境的情况。在生产环境中，分配给 &lt;code&gt;std::map&lt;/code&gt; 节点的内存往往是分散的，因此在基准实现时要加入一些内存分配的扰动。&lt;/p&gt;
&lt;p&gt;在得到基准后，可以想想，这些 &lt;code&gt;node containers&lt;/code&gt; 的数据结构如 &lt;code&gt;std::map&lt;/code&gt;、&lt;code&gt;std::set&lt;/code&gt;、&lt;code&gt;std::unordered_map&lt;/code&gt; 等，数据节点在内存中是离散存储的，数据缓存局部性比较差的。追求高性能应优先选择连续内存的 &lt;code&gt;std::vector&lt;/code&gt; + &lt;code&gt;std::lower_bound&lt;/code&gt; 实现。测试发现在该场景下，使用 &lt;code&gt;std::vector&lt;/code&gt; 性能更好，但是一样有较大的长尾延迟。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507072353375.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Principle #2: &quot;Understanding your problem (by looking at data)&quot;&lt;/h2&gt;
&lt;p&gt;长尾的原因很简单，使用 &lt;code&gt;std::vector&lt;/code&gt; 在插入和删除数据会移动数据。作者分析了业务数据特征，发现频繁操作的数据集中在 &lt;code&gt;std::vector&lt;/code&gt; 的头部，导致移动数据成本较高。所以只需简单将反转数据存储顺序，就能减少数据移动成本，长尾延迟显著降低。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507072355837.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Principle #3: &quot;Hand tailored (specialized) algorithms are key to achieve performance&quot;&lt;/h2&gt;
&lt;p&gt;对上述实现运行 &lt;code&gt;perf&lt;/code&gt; 看看有什么瓶颈。&lt;/p&gt;
&lt;p&gt;这里又学到了一个技巧，使用 &lt;code&gt;fork&lt;/code&gt; + &lt;code&gt;execlp&lt;/code&gt; 在基准测试主要逻辑运行前启动 &lt;code&gt;perf&lt;/code&gt;，能避免初始化等无关函数干扰测试。又是一个 &lt;code&gt;fork&lt;/code&gt; 的骚操作！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507072357898.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;通过 &lt;code&gt;perf&lt;/code&gt; 发现该版本在调用 &lt;code&gt;std::lower_bound&lt;/code&gt; 时有不少分支预测错误，为此，作者实现了一个无分支的二分查找，核心是使用算术运算和条件移动指令 (&lt;code&gt;CMOV&lt;/code&gt;) 替代条件跳转。性能得到进一步提升！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507072357856.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里也学习到一个 &lt;code&gt;libpapi&lt;/code&gt; C++ 库的使用，可在代码中直接读取 &lt;code&gt;CPU&lt;/code&gt; 硬件性能计数器，如指令数、周期数、缓存缺失、分支误预测等，方便量化优化效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507072357718.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Principle #4: &quot;Simplicity is the ultimate sophistication&quot;&lt;/h2&gt;
&lt;h2&gt;Principle #5: &quot;Mechanical sympathy&quot;&lt;/h2&gt;
&lt;p&gt;二分查找会随机访问内存，如果直接简单使用顺序查找会如何？作者测试发现性能更好。所以有时最简单的算法在特定数据和规模下可能是最优的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507072358977.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;数据缓存优化得差不多了，接着看看指令缓存。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;[[likely]]&lt;/code&gt; 属性提示编译器将高频分支的代码放在主执行路径附近，优化指令缓存局部性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080002898.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;[[unlikely]]&lt;/code&gt;,&lt;code&gt;noinline&lt;/code&gt;,&lt;code&gt;cold&lt;/code&gt; 这些属性标记低频分支的代码。这些代码不会被内联到热点路径中，放得远远滴，避免它们污染指令缓存。真细啊！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080004896.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;优先使用 &lt;code&gt;lambda&lt;/code&gt;，比 &lt;code&gt;std::function&lt;/code&gt; 性能更优，后者可能有类型擦除和间接调用开销。&lt;/p&gt;
&lt;p&gt;原则里的 &lt;code&gt;Mechanical sympathy&lt;/code&gt; 翻译叫“机械共情”。我的理解就是编写高性能代码必须站在机器的角度思考，深刻理解机器的运行方式，如各级缓存，流水性指令、分支预测等，充分利用好其特性。&lt;/p&gt;
&lt;h2&gt;Principle #6: &quot;True efficiency is found not in the layers of complexity we add, but in the unnecessary layers we remove&quot;&lt;/h2&gt;
&lt;p&gt;作者接着开了另一个话题，主要讲网络和并发。&lt;/p&gt;
&lt;p&gt;网络通信上，尽量绕过内核，减少数据拷贝和用户态内核态的切换。这里介绍了一些工具。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080009896.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果多进程在同一机器通信，那就没必要使用 &lt;code&gt;Socket&lt;/code&gt;，直接使用共享内存。使用共享内存的话，一般需要有个高并发的消息队列。消息队列种类很多，设计前需明确需求边界。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080010033.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Principle #7: &quot;Choose the right tool for the right task&quot;&lt;/h2&gt;
&lt;p&gt;基于需求，作者设计了一个名为 &lt;code&gt;FastQueue&lt;/code&gt; 的单生产者多消费者共享内存无锁队列，主要通过两个原子变量 &lt;code&gt;mReadCounter&lt;/code&gt; 和 &lt;code&gt;mWriteCounter&lt;/code&gt; 来实现无锁。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;整体实现值得好好学习下&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080010544.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里值得学习是，这些变量都做了 &lt;code&gt;CACHE_LINE_SIZE&lt;/code&gt; 内存对齐，避免 &lt;code&gt;False Sharing&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Writer&lt;/code&gt; 具体实现&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080011024.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Reader&lt;/code&gt; 的具体实现&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080012009.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;第一版实现的性能比不上一些开源的库。于是作者又做了几个优化。&lt;/p&gt;
&lt;p&gt;第一：缓存写计数器 &lt;code&gt;mCachedWriteCounter&lt;/code&gt;，只有写入累积超过阈值，才更新 &lt;code&gt;mWriteCounter&lt;/code&gt;，避免频繁访问 &lt;code&gt;mWriteCounter&lt;/code&gt; 原子变量。注意这里也做了内存对齐，可以学学如何使用位运算进行高效的对齐计算。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080013993.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;第二：对每条消息做内存对齐。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080013950.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;第三：缓存读计数器 &lt;code&gt;mCachedReadCounter&lt;/code&gt;，避免频繁访问 &lt;code&gt;mReadCounter&lt;/code&gt;原子变量。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080014051.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;优化后，&lt;code&gt;FastQueue&lt;/code&gt; 实现性能优于一些著名开源实现。为什么作者一开始不直接使用开源库呢？因为作者贯彻“Simplicity is the ultimate sophistication”，觉得简单才是高效的额，开源的都太“重”了。&lt;/p&gt;
&lt;p&gt;最后作者还提出了 &lt;code&gt;api&lt;/code&gt; 设计的零拷贝优化，重新设计接口，允许调用者直接在队列的内部缓冲区进行序列化，能避免了一次内存拷贝。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080015637.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Principle #8: &quot;Being fast is good – staying fast is better&quot;&lt;/h2&gt;
&lt;p&gt;性能优化非一劳永逸，需持续监控。这里又学到一种新的统计函数信息的方式，就是 &lt;code&gt;Clang Xray Instrumentation&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这是一种低开销的函数级追踪工具，可以动态地给函数入口和出口打桩。作者提到程序只需编译一次，在运行时选择是否开始追踪。如果不开启，打桩的代码只会运行空指令，开销极低。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080016740.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Principle #9: &quot;Thinking about the system as a whole&quot;&lt;/h2&gt;
&lt;h2&gt;Principle #10: &quot;The performance of your code depends on your colleagues&apos; code as much as yours&quot;&lt;/h2&gt;
&lt;p&gt;作者通过一个随机访问内存的基准测试程序，展示了 &lt;code&gt;CPU&lt;/code&gt; 多级缓存的容量效应和缓存竞争的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当数据集能完全放入 &lt;code&gt;L1/L2 Cache&lt;/code&gt; (私有) 时，单核与多核 (6 核) 的吞吐量几乎线性扩展。&lt;/li&gt;
&lt;li&gt;当数据集增长到 &lt;code&gt;L3 Cache&lt;/code&gt; (共享) 大小时，6 核并行时的总吞吐量显著下降，接近单核吞吐量。因为多个核在激烈争抢共享的 &lt;code&gt;L3 Cache&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以如果在同一台机器里，即使你的程序优化到极致，但是其他程序对 &lt;code&gt;L3 Cache&lt;/code&gt; 滥用，也会拖慢你的程序。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507080017888.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;整个分享下来，学到不少东西，ns 级延迟优化真的很细！作者贯彻的原则就是尽量简单！less is more !&lt;/p&gt;
&lt;p&gt;结合演讲和个人理解，提炼以下优化原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基准测试尽量符合生产环境情况。&lt;/li&gt;
&lt;li&gt;在性能优化前，分析好业务特性，数据分布。&lt;/li&gt;
&lt;li&gt;在性能优化前，分析性能瓶颈，定位关键路径。&lt;/li&gt;
&lt;li&gt;多考虑缓存的重要性，如数据缓存，指令缓存。做好数据结构的选择、数据内存对齐，原子变量的 &lt;code&gt;Cache Line&lt;/code&gt; 对齐。&lt;/li&gt;
&lt;li&gt;尝试在业务中减少使用 &lt;code&gt;node containers&lt;/code&gt;，做好性能对比。&lt;/li&gt;
&lt;li&gt;标准库和通用算法虽好，但在热点路径上，针对数据和场景尝试自己实现一些算法。&lt;/li&gt;
&lt;li&gt;一些著名开源库功能丰富但可能臃肿。在明确边界的需求下，自研简洁高效的组件。&lt;/li&gt;
&lt;li&gt;根据实际需求来明确设计边界。&lt;/li&gt;
&lt;li&gt;简单设计和实现通常更易维护且性能更好。&lt;/li&gt;
&lt;li&gt;移除非必要的抽象层和复杂性。&lt;/li&gt;
&lt;li&gt;建立持续性能监控。&lt;/li&gt;
&lt;li&gt;考虑整个系统乃至整机的资源共享和竞争。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>2025.07.08</title><link>https://coooredump.github.io/blog/journal/2025-07-08</link><guid isPermaLink="true">https://coooredump.github.io/blog/journal/2025-07-08</guid><description>在所有失去的人中，我最怀念我自己</description><pubDate>Tue, 08 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;感觉我们的世界对慢节奏的人很不友好，再怎么想求知，如果要我以高强度、高精神压力的状态去做的话，我自己觉得没有什么知识值得我放弃悠闲平和的生活。有一句话说得很对，哲学是从闲暇中诞生的。既然如此，找不到故乡的孩子也是相当的可怜。&lt;/p&gt;
&lt;p&gt;那种田园牧歌的时代早就一去不复返了。我曾经对科研的憧憬完全来自于牛顿与苹果树，康德漫步于小径，梭罗隐居瓦尔登湖，而不是坐在电脑面前，脊柱侧弯、屁股久坐、眼睛散光，和计算机打交道，未来只有吃不完的苦。我曾经觉得我是会喜欢科研的，在我做出老师也不会做的题目的时候，压轴题答案对出来一样的时候，在每个新学期，发下的课本还弥散着油墨香和新知识香味的时候。&lt;/p&gt;
&lt;p&gt;也许我确实是，但是我连打游戏都没法一天八小时持续一个月，看书不行，跑步不行，吉他也不行，唯一成功做到过的只有读书，读高中的时候。但也得考虑到咱们学六门课呢，而且真是差点就死了。&lt;/p&gt;
&lt;p&gt;人擅长什么领域，就会在心中的价值天平上给它加码，以确保自己的价值在自己心中过得去。我想高考之于我几乎所有的意义就在于此了。我确认自己的才能、价值、潜力，我知道我的头脑不会背叛我。我所适应的这条路，它的走向大概就是科研吧？我也曾经这样想过。&lt;/p&gt;
&lt;p&gt;但好像不是的。&lt;/p&gt;
&lt;p&gt;严谨的逻辑，科学的思维方法，阴云之下的大厦，真理海边的贝壳，我现在也仍然憧憬着。可我不想走那条路。它也许确实延伸到美丽的景色边，可是车票呢？或者是昂贵到大部分人支付不起，或者是以其他的美景代为支付。后者是否值当，我不好说，这只能是个人的判断。&lt;/p&gt;
&lt;p&gt;那么，我要走到哪条路上去呢？&lt;/p&gt;
&lt;p&gt;走到无论怎样人烟稀少、终究是广袤的那片原野上。独属于我的这一条小径或是平原，独属于我的这一片湖海。我是离群的鸟，冒着饥饿受冻的风险，也要看一看规划以外的人生。生活节奏越来越快、越来越机械化、科技化、去诗意化是无法避免的现实，我无法接受这样的生活，也是无法避免的现实。我需要自己的空间和时间，而且必须以此为最高优先级。&lt;/p&gt;
&lt;p&gt;其实我潜意识里始终在以此为标准做选择。小到必须要看闲书、熬大夜、睡大觉，大到咱们学个理科玩玩就可以了。以前很喜欢 ilem 的白鸟过河滩，白鸟白鸟不要回头望，你要替我飞去那地方。现在一想，为什么呢？我又不是被笼子束缚的鸟，更不应该是被笼子束缚的人。&lt;/p&gt;
&lt;p&gt;或多或少早就明白这些，最早可追溯至我十一二岁对着王尔德童话，想象自己去森林里砍树盖小木屋的时候。花了八年向自己证明了自己的能力，又花了四年试探这个世界的底线。结果是这个世界根本没有底线。&lt;/p&gt;
&lt;p&gt;我时而惊叹于我朋友的文笔，也惊叹于我朋友与我价值观的一致。&lt;/p&gt;
&lt;p&gt;从小到大我见过的成绩好的人太多了，然而天下英雄如过江之鲫，即使你万里挑一，在这里也有 14 万个，更何况世界不止中国。&lt;/p&gt;
&lt;p&gt;然而幸福是一种独立于客观存在的能力，就像物债二分法，所有权的变动无效并不意味着买卖合同无效：&lt;strong&gt;你看到什么，听到什么，感受到什么，才是最重要的，跟客观世界是什么样的、跟别人怎么看你无关&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以幸福不是那种一下被人流推到癫狂的短暂欢乐，而是一种长期平和的满足。就按一线城市人均寿命 80 岁来算，再扣除最后几年没有质量的生命、扣除已经过去的 18 年，4 年本科、3 年硕士占据现有生命的份额不少。&lt;/p&gt;
&lt;p&gt;既然人生永远没有岸，你的体验不重要吗？&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>选择互联网大厂还是央国企？</title><link>https://coooredump.github.io/blog/recruitment/internet-companies-or-state-owned-enterprises</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/internet-companies-or-state-owned-enterprises</guid><description>从我短暂的学生时代所见，站在 2025 年的今天，我真实感受到了通缩所带来的压力</description><pubDate>Thu, 03 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;近些年互联网发展&lt;/h2&gt;
&lt;p&gt;站在 2025 年的今天，我真实感受到了通缩所带来的压力。从我短暂的学生时代所看见的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2018-2020&lt;/strong&gt; 互联网大厂的疯狂。BAT 大厂非常乐意接受本科生，并且当时就算是没有经验的只要会一些基本算法中小厂都是可以进入的。签完三方还有 2-4 万的签字费；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2020-2022&lt;/strong&gt; 笛子和华子的时代，这时候的比亚迪、华为、tplink、海康、大华都是应届毕业生保底的 offer。互联网在逐渐收缩，但是还没有开始大面积裁员，更多的是业务调整，出来的人也可以很快找到工作；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2022-2023&lt;/strong&gt; 这时候的华子、笛子难度逐渐增加但是还处于毕业生保底的 offer，互联网大厂开始裁员，减去没有增量的业务，仅保留营收正常、具有未来前景的业务；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2023-2025&lt;/strong&gt; 这时候互联网大厂大面积裁员已经成为常态，包括不限于海康威视解散多处研发基地；tplink 解散张江 wifi 芯片团队；美团砍掉优选业务；中电成都某所子公司裁员；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026+&lt;/strong&gt; 最近看到很多 26 届的同学拿到了大厂实习的 offer，我原本是感到开心的，第一反应是互联网回暖，但是回头想来，这并不是一个好兆头，因为当前互联网大厂主攻的 AI 大模型并没有真正盈利没有实际增量，结合 2024 全年阿里的财报显示出来的在职人数，我得到了一个可怕的结论：&lt;strong&gt;互联网大厂正在加速内部末位淘汰和部门裁撤，通过快速循环的方式接收和裁撤应届毕业生，可能不到一个合同期就会裁走大部分人&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最近也看到很多，从芯片行业、自动化电气行业、新能源行业、工业软件行业和互联网行业，越来越萧条。当我还抱有侥幸心理，认为已经到底了，但是种种迹象表明远没有结束。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;华为部分产品线将大量 24 届研发人员输送一线，说明市场在收缩，只能相互之间进行存量竞争&lt;/li&gt;
&lt;li&gt;IC 设计今年也是遇冷，岗位急剧减少&lt;/li&gt;
&lt;li&gt;比亚迪也传出裁撤智驾部门的消息&lt;/li&gt;
&lt;li&gt;58 同城大量裁员&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;就程序员可选的行业来说，包括半导体、新能源和互联网都在萎缩，而且速度超乎我们的想象。&lt;/p&gt;
&lt;h2&gt;大厂 or 央国企&lt;/h2&gt;
&lt;p&gt;当然这不是本次讨论的核心，今天讨论的核心是软件开发还值得进入吗？&lt;/p&gt;
&lt;p&gt;个人答案是：不值得，除非能够起薪一线年入 45W，并且每三年涨薪 30%。因为只有这样才能够在 35 岁之前攒够 500W，够以后养老的需要。否则二线 20W 的半垄断型央国企将会是更好的选择。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;进一步讨论如何实现应届起薪 45W&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在单纯做开发只能够实现一线收入在 30-40W，是无法达到一线 45W 年薪的。只有算法岗，包括大模型、nlp、cv 等才能够达到这样的薪资。并且这样的薪资多数只能由大厂开具，因此需要一至两篇的相关领域 B 以上论文➕一至两家大厂实习，这样的机会更大。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;从长远来看，半垄断型央国企更具有性价比&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;稳定的细水长流，将提升生活的幸福感，有时候人生追求的不是钱越多越好，而是不受金钱窘迫的束缚，细水长流在没有增量的当下何尝不是一种选择。当然这里所说的更多是央国企当中的二级单位也就是省分/区域分公司。&lt;/p&gt;
&lt;p&gt;从另一个角度看，十年前 2015 入行的程序员现在 35 岁要离开行业了，经历过行业的上行与下降，才有了现在的积累，你又如何能保证在只有下行的当下，能够坚持到他们的年龄，得到相同的财富积累？现在选择程序员在我看来是一个十分不明智的行为，缩量竞争将会像光伏产业一样，竞争越来越激烈，收益越来越微薄。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;半垄断型央国企有哪些？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;石油（中石油、中石化、中海油）&lt;/li&gt;
&lt;li&gt;发电（五大六小）&lt;/li&gt;
&lt;li&gt;供电（南网、国网）&lt;/li&gt;
&lt;li&gt;烟草（烟草局、卷烟厂）&lt;/li&gt;
&lt;li&gt;通信（联通、移动、电信不包括三产）&lt;/li&gt;
&lt;li&gt;国有行（中、农、工、建、交、邮）&lt;/li&gt;
&lt;li&gt;中证登、中债登、中国结算、北交所、资源性央国企二线研究院、四大资产管理公司&lt;/li&gt;
&lt;li&gt;还有地方性的交通、港口等等&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;半垄断型央国企适合哪些人？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;适合小镇做题家，可以将普通人维持在中产位置不滑落，等经济好转可以为自己的下一代提供稳定富足的基本生活，完成阶级跃迁。&lt;strong&gt;一代人有一代人的使命&lt;/strong&gt;，绝不能一口吃成胖子。完成阶级跃迁从来都是从农民到城市中产再到富裕阶层，稳定的中产才是小镇做题家的最优选择。&lt;/p&gt;
&lt;h3&gt;银行类&lt;/h3&gt;
&lt;p&gt;体系很大、数量也多，薪资也高，招聘季都会到各大高校或社会招聘大量的专业人才。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;计算机专业在银行对口的岗位是金融科技岗&lt;/strong&gt;，对应的岗位有软件开发、网络信息安全、系统运维和运维工具研发等。&lt;/p&gt;
&lt;p&gt;金融科技岗在银行招聘中，一般以市分行、省分行为起点，可分为总行及其直属机构、软件开发中心以及数据中心等。&lt;/p&gt;
&lt;p&gt;现在银行的科技子公司很盛行，比如建信金科、兴业数金等等，优点是薪酬较高，招聘量大，但是稳定性差，压力大，加班多，技术一般（不推荐）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;【第一梯队】监管机关：&lt;strong&gt;中国人民银行&lt;/strong&gt;（人行）、&lt;strong&gt;中国银行保险监督管理委员会&lt;/strong&gt;（银保监会） ，人行是有专门的考试，银保监会属于国考序列，竞争都很激烈&lt;/li&gt;
&lt;li&gt;【第二梯队】三大政策行：&lt;strong&gt;国开行&lt;/strong&gt;、&lt;strong&gt;进出口&lt;/strong&gt;、&lt;strong&gt;农发展&lt;/strong&gt;，竞争惨烈，相对商业银行来讲，工作压力不大&lt;/li&gt;
&lt;li&gt;【第三梯队】六大国有行：&lt;strong&gt;工农中建交邮储&lt;/strong&gt;，建行属于薪资更高些，邮储处于薪资中下游（但是业务扩张后薪资有所上涨）&lt;/li&gt;
&lt;li&gt;【第四梯队】十二家股份制：招商银行、中信银行、浦发银行、平安银行、中国光大银行、华夏银行、中国民生银行、广发银行、浙商银行、恒丰银行、兴业银行、渤海银行&lt;/li&gt;
&lt;li&gt;【第五梯队】一百余家&lt;strong&gt;城市商业行&lt;/strong&gt;：比如北京银行、上海银行、苏州银行、杭州银行、南京银行等等，好的城商行比当地的六大行的薪酬会更高些&lt;/li&gt;
&lt;li&gt;【第六梯队】一千余家&lt;strong&gt;农商行/农信社&lt;/strong&gt;：上海农商行、农信社、山西农商行、江苏农商行等等&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;证券类&lt;/h3&gt;
&lt;p&gt;证券公司程序员岗位上可以分为量化，算法，开发。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;量化人员的学历要求很高&lt;/strong&gt;，博士与海归、名校硕士比例非常高。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;证券公司程序员薪资蛮高的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中信证券，北京，base 17k，年终第一年很少，2-3 个月吧，第二年正常情况下 6-10 个月&lt;/li&gt;
&lt;li&gt;华泰证券，南京，base 22k，总包 38-40&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在证券公司做程序员，不仅要注重专业技术，也要重视业务逻辑，需要了解很多&lt;strong&gt;金融类相关的专业知识&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;烟草 / 电网&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;烟草&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;烟草公司包括各省&lt;strong&gt;烟草专卖局&lt;/strong&gt;和各省&lt;strong&gt;中烟工业&lt;/strong&gt;，&lt;strong&gt;是中国最赚钱的企业&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;烟草公司懂的都懂，是真的香。&lt;strong&gt;烟草公司每年基本上只招应届生&lt;/strong&gt;，想进入烟草公司的应届生一定要抓住机会！&lt;/p&gt;
&lt;p&gt;但是烟草录用名单，&lt;strong&gt;硕士占比很高&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;电网&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;电网首先选择的是输电卖电的国家电网、南方电网。&lt;/p&gt;
&lt;p&gt;另外还有发电的央企：国家能源、大唐集团、中国华能、国家电投、三峡集团、华润集团、国投集团、中核集团等等。&lt;/p&gt;
&lt;p&gt;国家电网的话，不是双一流的话或者研究生其实要进市公司是很难的，而且一般城市对计算机专业的&lt;strong&gt;需求量其实很少&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;计算机类的入职后，主要是到各省的信通公司，&lt;strong&gt;或者各地市供电公司的信通部门&lt;/strong&gt;。待遇方面也很不错的，和同岗级的电气类工资接近。由于是计算机类，不用和电气专业一样经常去一线生产岗位。&lt;/p&gt;
&lt;h3&gt;石油类（中石油、中石化、中海油）&lt;/h3&gt;
&lt;p&gt;我国三大石油公司中石油、中石化、中海油，每个省都有分公司。&lt;/p&gt;
&lt;p&gt;三桶油每年计算机专业岗位也很多，薪资待遇很不错。举个 2022 届校招生的例子：中石化新星新能源研究院郑州分公司，地点郑东新区，不包住，试用期半年 2600/月，转正后往年是第一年 10+，第二年 13+，计算机岗。&lt;/p&gt;
&lt;h3&gt;军工所&lt;/h3&gt;
&lt;p&gt;军工相关的央企有航天科技，航天科工，中电科，航空工业，中国兵器和中船。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中电科里面待遇排第一梯队的有 28 所，29 所等，可能和民企的 IT 公司不相上下，在军工单位里算是很好的了（个人了解之后不推荐）。&lt;/li&gt;
&lt;li&gt;航天科技的，待遇好的有航天一院，三院，五院相应的部，和排头的所（卡学历）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举几个校招生薪资的例子：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;中国船舶 722 所，武汉，试用期 6 个月，每月 11k，转正第一年 25w&lt;/li&gt;
&lt;li&gt;中国兵器 202 所，咸阳，事业编，试用期三个月每月 8k，转正后直接获得 10w 安家费，承诺每年不低于 18w&lt;/li&gt;
&lt;li&gt;中电 28 所（不推荐）&lt;/li&gt;
&lt;li&gt;中电 14 所（推荐，注意签约合同内容）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;通讯类&lt;/h3&gt;
&lt;p&gt;通信行业可以说是一个很吃香的行业，三大运营商，移动、电信、联通作为我国通信行业的&lt;strong&gt;垄断国企&lt;/strong&gt;，其运营的效益有多高更不用多说。&lt;/p&gt;
&lt;p&gt;选择运营商要注意区县公司绝对不要去，最低也要混在市公司一级，能去省公司则尽量去（不推荐区县级的全员营销）&lt;/p&gt;
&lt;p&gt;举两个校招生薪资的例子：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;浙江移动市级公司，系统开发岗位，据说待遇 16w-18w&lt;/li&gt;
&lt;li&gt;中国移动研究院，产品岗，待遇 24w&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;地方国企&lt;/h3&gt;
&lt;p&gt;地方国企有省属国企、市属国企、区县属国企。&lt;/p&gt;
&lt;p&gt;省属国企通常有交通投资集团、文化旅游集团、农业发展集团等等，省属国企的待遇都是很不错的&lt;/p&gt;
&lt;h3&gt;其他&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;假国企（国企包含央企）：求职者在真国企能享受到的很多福利待遇在假国企是享受不到的，钱少事多加班多。找到国家认证好的国企名单，直接在名单里面查就好了，严格意义上的国企就两种，就是国有独资公司和国有控股公司（参股也可）&lt;/li&gt;
&lt;li&gt;权力是自上而下的，能去省局不去市局，能去市局不去县局。层级低的单位，非常不利于个人发展&lt;/li&gt;
&lt;li&gt;普通员工 &amp;#x3C; 领导关系户 &amp;#x3C; 总部领导关系户 &amp;#x3C; 甲方领导关系户&lt;/li&gt;
&lt;li&gt;垄断国企滋润 &gt; 一级集团或者二级公司日子还好点 &gt; 三级子公司国企鱼龙混杂 &gt; 基层（也有相反的，比如党建办公室，以学校为例）&lt;/li&gt;
&lt;li&gt;烟草、供电、供水、燃气、供暖 &gt; 城投、水利泵站（垄断化的国企） &gt; &quot;中字头&quot;地区地产、施工（也就是参与市场竞争市场化的国企：这种盈利不佳可能会恶心人走）&lt;/li&gt;
&lt;li&gt;国企在市场竞争上没有优势，但是民用资源一定不会倒闭，也就是相当于铁饭碗，因为国企的目的是为了保障经济命脉在国家手里&lt;/li&gt;
&lt;li&gt;地铁比铁路局钱少，没有货运业务来赚钱。地铁能留城里，铁路局难留，所以地铁会压价，压的价就相当于留城的代价了&lt;/li&gt;
&lt;li&gt;国企房地产来历：政府拿不出钱承担项目，把地产抵押，国企地产再盖楼&lt;/li&gt;
&lt;li&gt;进国企不要想着升职加薪，毕竟关系户在那里，建议另辟蹊径，比如搞副业&lt;/li&gt;
&lt;li&gt;社招进国企都是顶雷或者拉马的，干事创业规避国企，大佬背书才走社招，民企打法别存幻想，集体决策神鬼难清（招聘会社招，其实早就内定了，做样子看的，其他人陪跑）&lt;/li&gt;
&lt;li&gt;大专、非全日制本科和往届几乎没有希望&lt;/li&gt;
&lt;li&gt;现在只有公务员和事业编有编制，国企都没有编制
&lt;ul&gt;
&lt;li&gt;备案制员工（多为应届生）：纳入工资总额，人员国资委备案&lt;/li&gt;
&lt;li&gt;合同制员工：不纳入工资总额，不在国资委备案&lt;/li&gt;
&lt;li&gt;劳务派遣员工非“编制”，不属于国企人员&lt;/li&gt;
&lt;li&gt;论级别，国企有：地方，市级，省级，央企&lt;/li&gt;
&lt;li&gt;论所有制，有&lt;strong&gt;全资&lt;/strong&gt;，独资，股权多元化，混改&lt;/li&gt;
&lt;li&gt;论岗位，有财务岗，战略岗，人资岗，技术岗，还有 xxx 生产一线岗&lt;/li&gt;
&lt;li&gt;论职级，同一个岗位有专员，主办，主管等级别&lt;/li&gt;
&lt;li&gt;论薪酬，要看你是公益类，还是市场类；营业能力，收入能力 ...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;家里没钱的，别来银行（拉存款，完成任务，这么一想出大钱赚死工资，诈骗和它一比都算和蔼可亲）&lt;/li&gt;
&lt;li&gt;国聘网，大部分单位也就是挂一下（也有个别），主要招聘名额还是各校线下宣讲会&lt;/li&gt;
&lt;li&gt;网申大部分是硕士起步，本科多试试宣讲会&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;更多&lt;/h3&gt;
&lt;p&gt;参考链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/285730093/answer/2506491786&quot;&gt;有哪些值得计算机专业学生加入的国企？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/594480491/answer/2985776810&quot;&gt;国企那么多副科、正科，都是有编制的吗？&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/202507031739233.BNssdB2L.jpg"/><enclosure url="/_astro/202507031739233.BNssdB2L.jpg"/></item><item><title>选调生备考计划</title><link>https://coooredump.github.io/blog/civil-servant/civil-servant-examination-preparation-plan</link><guid isPermaLink="true">https://coooredump.github.io/blog/civil-servant/civil-servant-examination-preparation-plan</guid><description>手把手带你备战选调/国考/省考</description><pubDate>Sat, 28 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;福建选调须知&lt;/h2&gt;
&lt;p&gt;这里插一句话，不同于一般省份的“&lt;strong&gt;定向选调生&lt;/strong&gt;“和”&lt;strong&gt;普通选调生&lt;/strong&gt;”分类，&lt;strong&gt;福建并没有严格意义上的定向选调生，只有“选调生”和“引进生”两种分类&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;其中讲述了「选调生」和「引进生」的细节&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;还不太清楚的同学可以先看看这篇文章补课：&lt;a href=&quot;http://zhuanlan.zhihu.com/p/434276788&quot;&gt;最偏爱本地人的省份？2022 年福建选调生全方位解读！&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;备战计划与资料&lt;/h2&gt;
&lt;h3&gt;行测（资料）&lt;/h3&gt;
&lt;p&gt;花生十三笔记总结&lt;/p&gt;
&lt;p&gt;粉笔 980 系统课&lt;/p&gt;
&lt;p&gt;行测 5000&lt;/p&gt;
&lt;p&gt;福建选调生招录考试专用教材&lt;/p&gt;
&lt;p&gt;粉笔 APP：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;智能组卷&lt;/li&gt;
&lt;li&gt;题库&lt;/li&gt;
&lt;li&gt;每周模考&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;申论（资料）&lt;/h3&gt;
&lt;p&gt;申论背诵规范词&lt;/p&gt;
&lt;p&gt;人民时评 45 篇&lt;/p&gt;
&lt;p&gt;2025 人民日报时评 12 个月｜电子版（每月更新）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.xiaohongshu.com/user/profile/64e40bd70000000001007366?xsec_token=ABuVq8utgwquWqQU0O1sPAgWp3d4IISdhjXO-N0I35Hfk=&amp;#x26;xsec_source=pc_note&quot;&gt;人民日报·精读&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;计划&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;参考 PPT 和两位选调学长/学姐的建议，找优公老师/观澜公考咨询（福建选调）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;🥕第一阶段：打基础（5-7 月）&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;每天&lt;/strong&gt;：言语 30、判断 30、数资 25、常识 15&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1、FB980 跟系统课程至少过一遍，对各个模块内容、重难点、出题逻辑有清晰的认知&lt;/p&gt;
&lt;p&gt;2、晨读背诵：《实词辦析 1500 个》、《常识 4600 问》&lt;/p&gt;
&lt;p&gt;3、错题不同颜色笔做好标记，方便后期复盘&lt;/p&gt;
&lt;p&gt;4、申论同时进行，每天 2h&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🥕第二阶段：转项拔高（8-10月） 根据自己的薄弱模块去找固定的老师听课&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;每天&lt;/strong&gt;：言语 30 题＋判断 30 题＋数资 30 题＋常识 15 题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1、判断：花生十三判断刷题、龙飞图推刷题&lt;/p&gt;
&lt;p&gt;2、言语：郭熙 100 题、欣说、阿里木江刷题&lt;/p&gt;
&lt;p&gt;3、数量：花生十三数量 1200 题、齐麟刷题&lt;/p&gt;
&lt;p&gt;4、资料：高照超大杯、齐麟、花生十三&lt;/p&gt;
&lt;p&gt;5、常识：FB（分模块刷）&lt;/p&gt;
&lt;p&gt;6、申论：李梦圆、站长申论刷题（每天一套，每周一篇大作文）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🥕第三阶段：考前冲刺（11月-考试前）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1、严格按照考试时间，早上一套行测，下午一套申论，晚上复盘，状态保持到考前&lt;/p&gt;
&lt;p&gt;2、错题集完完整整的再过至少 3 遍，不同颜色重新标记难点和知识模糊点，对于变型较多/复杂的题型，重点关注各模块用时及正确率&lt;/p&gt;
&lt;h2&gt;行测&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506212123621.png&quot; alt=&quot;image-20250621212308484&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐 UP：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://space.bilibili.com/21969231?spm_id_from=333.337.0.0&quot;&gt;网友红领巾&lt;/a&gt;：每期粉笔模考解析&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.xiaohongshu.com/user/profile/60d9500e000000002002a12d?xsec_token=ABjYaiA6rGXLmrWTyXIcaWiFGBZWRaLUrV0qt_gwkZCkM%3D&amp;#x26;xsec_source=pc_search&quot;&gt;拥抱昔日温度&lt;/a&gt;：第一视角做题&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;行测刷题建议&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最初期，可以随便写一套，不用计时，做完就好，熟悉一下题型的同时也看下自己的水平在哪里。&lt;/p&gt;
&lt;p&gt;刷的卷子可以用粉笔 app 的“智能组卷”功能来生成。&lt;/p&gt;
&lt;p&gt;每次做，都要把手机开飞行，对各部分计时。&lt;/p&gt;
&lt;p&gt;做题时打印出来做纸质版。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;尽量每天都拿出两小时，完整刷一套套题，保持手感&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;粉笔每周模考&lt;/strong&gt;，有空的话也可以参加一下。岗位排名做个参考，不用太放在心上，以免被搞心态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;要形成自己最舒服的做题顺序&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;常错的题，要善于积累。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506221742712.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;（一）常识&lt;/h3&gt;
&lt;p&gt;性价比最低的部分。但福建选调常识有 30 题，整体难度不难，但还是值得一点点的重视。&lt;/p&gt;
&lt;p&gt;当年度的大事一定要熟悉，例如我们去年的二十大。网络上有些常识蒙题法，我觉得还是值得一看。一是能够知道在出常识题时，最容易在什么地方设置考点、挖坑；二是在犹豫不决时可以交给蒙题。(刚刚已经说过了，犹豫不决是大忌)。&lt;/p&gt;
&lt;p&gt;最后，一些机构针对福建选调整理的福建相关的常识，在其它部分读累了的时候，也是可以看看的。&lt;/p&gt;
&lt;h3&gt;（二）言语&lt;/h3&gt;
&lt;p&gt;很重要的部分。多刷题，多积累。&lt;/p&gt;
&lt;p&gt;【逻辑填空】&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;善于积累，遇到了就记录，形成自己的记录本&lt;/li&gt;
&lt;li&gt;难题偏题不过多纠结，有时候机构之间答案都不一样&lt;/li&gt;
&lt;li&gt;个人比较推荐的老师：阿里木江（B站）。但找到适合自己的老师才是最重要的。&lt;/li&gt;
&lt;li&gt;“中心理解”这个东西一定要尽可能弄懂，懂了之后做言语会豁然开朗。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;（三）判断推理&lt;/h3&gt;
&lt;p&gt;很重要的部分。敢于放弃，回头再看。一定不要纠结!!!!&lt;/p&gt;
&lt;p&gt;【图推】&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要听课也要刷题，知晓命题点。&lt;/li&gt;
&lt;li&gt;个人比较推荐「花生十三」和「刘文超」。花生十三的六面体方法让我印象很深刻。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;【定义】&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;听课知晓命题点。&lt;/li&gt;
&lt;li&gt;善用排除法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;【类比】&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;听课熟悉命题点。&lt;/li&gt;
&lt;li&gt;积累。如果选项都看不懂那肯定很难做出来，&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;【逻辑推理】&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;个人推荐花生十三的加强削弱&lt;/li&gt;
&lt;li&gt;*福建选调在这部分的题可能会非常恶心，一定要果断跳。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;判断推理总结：再强调一下，判断推理，不管是哪类题型最重要的是做到这两点!&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;看课，熟悉题型与命题点&lt;/li&gt;
&lt;li&gt;不纠结!不纠结!不纠结!可以圈起来有空回头再看但一定不要纠结。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;（四）资料分析&lt;/h3&gt;
&lt;p&gt;非常非常重要。言语和判断是保底，资料是拉开差距的&lt;/p&gt;
&lt;p&gt;最好每次做套题都计时。资料对时间的把控特别重要。&lt;/p&gt;
&lt;p&gt;技巧性强。因此做得快与慢，准确与否，就差得非常多&lt;/p&gt;
&lt;p&gt;掌握技巧，尽量熟悉；刷题做题，保持手感。&lt;/p&gt;
&lt;p&gt;资料这部分可以多看一些老师，因为不同老师有不同老师的技巧。个人认为多学一些是没坏处的（但是还是要形成自己的体系，避免技巧太多不知道用哪个）&lt;/p&gt;
&lt;p&gt;推荐老师:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;刘文超、齐麟、高照、花生十三&lt;/li&gt;
&lt;li&gt;小红书 @拥抱昔日温度：第一视角做题，非常强，看厉害的人是怎么做题的，然后尽可能去模仿，对自己的提升会非常大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;（五）数量关系&lt;/h3&gt;
&lt;p&gt;“内幕”：不同于国考，福建选调的数量非常简单，至少 2023。年是这样。&lt;/p&gt;
&lt;p&gt;因此，定要留时间给数量，把分拿下。&lt;/p&gt;
&lt;p&gt;因此，做其它部分更要尽可能快。&lt;/p&gt;
&lt;h2&gt;申论&lt;/h2&gt;
&lt;p&gt;推荐：&lt;a href=&quot;https://www.xiaohongshu.com/explore/6858b321000000001001054c?xsec_token=ABkc88t8ckmKF0JVCAb-3XkzyA2To7n9kviVH6YqafLV0=&amp;#x26;xsec_source=pc_user&quot;&gt;人民日报申论精读（小红书）&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;仅针对福建选调&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;标点不占格&lt;/p&gt;
&lt;p&gt;分点作答&lt;/p&gt;
&lt;p&gt;多写对策&lt;/p&gt;
&lt;p&gt;注意时间，尽量写快点&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;福建选调生申论&lt;strong&gt;考试时间为 90 分钟，总分为 100 分&lt;/strong&gt;，近几年题量均为 2 题，而且&lt;strong&gt;更注重结合基层问题、民生问题、福建省省情的相关材料进行考察&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;内容与普通公考申论类似，考查综合分析题、归纳概括题、写作题（议论文），其中乡村振兴是常考题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506221750352.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;（1）篇幅：选调笔试中申论材料一般为 6~9 则，字数在 7000 左右，这就意味着材料字数相对较多，对于考生在材料的阅读理解能力的要求更高，同时提醒各位报考选调的考生需要合理安排时间，在思考全面的同时提高作答效率，做到事半功倍。&lt;/p&gt;
&lt;p&gt;（2）题材：申论题材常与选调生到基层工作的内容联系密切，与福建本地的实际情况密切联系，具有较强的实际意义，所以大家可以关注一些本地的新闻时事。&lt;/p&gt;
&lt;h2&gt;面试&lt;/h2&gt;
&lt;p&gt;考察人选从最低合格分数线以上的人员中确定。本、硕选调生考察人选按报考地区计划数 1 : 2，从高分到低分依次确定。&lt;/p&gt;
&lt;h3&gt;1. 面试方式&lt;/h3&gt;
&lt;p&gt;选调生面试为&lt;strong&gt;半结构化面试&lt;/strong&gt;，即面对面交谈模式，面试时长不固定，有点像企业面试。省委组织部会到各高校进行考察，考察分为两轮。每轮时间一般为 5-10 分钟不等，也有学员出现面谈达 30 分钟的情况。&lt;/p&gt;
&lt;h3&gt;2. 考察对象&lt;/h3&gt;
&lt;p&gt;选调考察会安排分管学生工作的领导、班级辅导员、班干部 (1-2 名) 以及同学舍友 (1-2 名) 进行谈话。一般谈话结束后进行考生面谈，根据高校的不同，会出现顺序调换的情况。&lt;/p&gt;
&lt;h3&gt;3. 面试真题&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506221753733.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506281425467.BNCf5h9g.png"/><enclosure url="/_astro/202506281425467.BNCf5h9g.png"/></item><item><title>2025.06.28</title><link>https://coooredump.github.io/blog/journal/2025-06-28</link><guid isPermaLink="true">https://coooredump.github.io/blog/journal/2025-06-28</guid><description>聚散离合，此乃常态，你要习惯</description><pubDate>Sat, 28 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前阵子暑期实习挺不顺利的，人很丧，没自信，没目标，也错过了很多机会...&lt;/p&gt;
&lt;p&gt;唯一有希望的华子也泡死在池子里了&lt;/p&gt;
&lt;p&gt;想回到暑期前重新开始，但没办法，回不去了，这就是生活&lt;/p&gt;
&lt;p&gt;就好像我这辈子也回不到大一/大二的学习状态一样&lt;/p&gt;
&lt;p&gt;或者说，人这一生，都不能走回头路&lt;/p&gt;
&lt;p&gt;很多年来，我一直持续在问自己一个问题，怎么就变成这样了呢？&lt;/p&gt;
&lt;p&gt;最近，这个问题变成了，害，那又能有什么办法呢 😮‍💨&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;看到一个北大的哥们，比我晚入池，前几天就开奖了&lt;/p&gt;
&lt;p&gt;hr 和我说，或许这就是北大的魅力吧&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506280325378.png&quot; alt=&quot;image-20250628032513786&quot;&gt;&lt;/p&gt;
&lt;p&gt;颓废在宿舍刷视频的时候，经常刷到蜡笔小新和海绵宝宝，不得不说在那个年代，做的是真好&lt;/p&gt;
&lt;p&gt;小学的时候是最应该看蜡笔小新的时候&lt;/p&gt;
&lt;p&gt;高中的时候是最应该看斗破苍穹的时候&lt;/p&gt;
&lt;p&gt;但我研究生的时候才开始沉迷这俩玩意&lt;/p&gt;
&lt;p&gt;不过仔细一想，我现在和高中时候确实有点不一样了，比如高中时候我玩 LOL 宁死不投，现在我看打不过了 15 分钟就上票了，不能让对面虐菜爽&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;能打过我的人，我不给他们打&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;人总应该是有点变化的，但我最近又有些新的理解了，成长是一件很难的事，它甚至意味着从头去修改你的一部分人生意义&lt;/p&gt;
&lt;p&gt;打个比方：你连体重都控制不了，怎么能控制自己的人生呢？&lt;/p&gt;
&lt;p&gt;朋友说没办法，现在大伙都找着了工作，体重确实不好减&lt;/p&gt;
&lt;p&gt;真不好减，最近东子、团子和阿里打外卖价格战，快给哥们喝出糖尿病了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506280349300.jpeg&quot; alt=&quot;Screenshot 2025-06-28 at 03.48.46&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;哈哈哈&lt;/p&gt;
&lt;p&gt;有个好朋友跑北京实习了，还有一个去了杭州，在厦门读书时哥几个总是经常打哈哈，不过一到上班，感受又不一样了&lt;/p&gt;
&lt;p&gt;出发前几天，我说了一句“今日一别，小半年又见不到了，不过咱们的学生生涯好像也就一年多了”，他们没说话，也许是我们经历这寻常的分别太多了，本来早就习惯了，这时候突然来了个人对你说：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;嘿，你已经和这么多人走散了呀&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;大多数人分开后就不会再见了，大多数人或许还有再见的机会&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;聚散离合，此乃常态，你要习惯&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506280358972.png&quot; alt=&quot;image-20250628035843883&quot;&gt;&lt;/p&gt;
&lt;p&gt;回忆会陪你一生，即使再模糊，你还是忘不掉&lt;/p&gt;
&lt;p&gt;有些事并不是一件需要被解决的事情，而是一件需要被接受的事情，或者说，绝大多数的事情，都只是需要接受的事情，你解决不了&lt;/p&gt;
&lt;p&gt;比如在地球 online 度过的这二十来年来看，目前我就这样一人，改变的话起码要拿年来衡量，或者十年来衡量&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;最近大模型发展的太快了，还记得 22 年刚出 GPT-3.5 的时候，有人感慨涌现就像宇宙给的礼物一样神奇，现在已经没有人再发出这种感慨了，默认大模型就存在和人类差不多甚至更强的智力了，没有上古时期 NLP 用 BERT 胡言乱语的折磨&lt;/p&gt;
&lt;p&gt;想起个很好笑的事，以前研究生方向不是 CV 就是 NLP，现在 CV 还有点，感觉 NLP 已经绝迹了，虽然大模型也算是一种 NLP 吧，但已经没有哪个老师招生的时候会给你说：同学，我的研究方向是 NLP 🤣&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506280417819.png&quot; alt=&quot;image-20250628041755728&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;也不知道为什么，这些年，身边的人总会有种觉得我很厉害的错觉，拜托，我要是真厉害，我还用发愁这些毕业、就业的事么，这样鼠鼠就不用吃生活的苦了，本就不好的胃也可以吃软饭度日了&lt;/p&gt;
&lt;p&gt;有时候就觉得自己活得很荒谬，明明近来也没谈恋爱，三天两头垂头丧气跟失恋似的&lt;/p&gt;
&lt;p&gt;朋友问我怎么情绪不对，我总不能告诉他最近暑期挂麻了，看不到未来吧&lt;/p&gt;
&lt;p&gt;很久以前看 anlin 老师的文章，记得评论区有句话说，说是如果有一天 anlin 老师不再更新文章了，说不定不是一件坏事，因为这说明 anlin 老师在努力生活，或者过得不错，所以不再写日寄了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可我不是&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我想把这句话改改，我的日寄大多数时候都很丧，其中虽然有我个人的原因在，但也有深夜写文章的 buff 加成，我希望有一天，大家都能过得不错，这样就不用看我的文章来找一些什么精神共鸣了，但我还是希望提供一些其他东西在，比如我的失败经历、我的踩坑经验，也希望对你有些帮助&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It is still summer, but mine is over&lt;/p&gt;
&lt;p&gt;没记错的话，是某一年高考的完形填空&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>如何利用「人民日报」提升申论？</title><link>https://coooredump.github.io/blog/civil-servant/how-to-improve-essay-writing</link><guid isPermaLink="true">https://coooredump.github.io/blog/civil-servant/how-to-improve-essay-writing</guid><description>总有公考大神和培训老师跟你说，学写申论就看人民日报。但实际上，如果你不懂看人民日报的技巧，你去刷人民日报 APP 纯粹是浪费时间。</description><pubDate>Wed, 25 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Link: https://www.zhihu.com/question/288739522&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;已购资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;电子版｜2025 年人民日报时评（2025.01 ~ 2025.12）&lt;/li&gt;
&lt;li&gt;纸质版｜人民日报时评 45 篇（2024.05 ~ 2025.04）&lt;/li&gt;
&lt;li&gt;纸质版｜申论背诵规范词&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;总有公考大神和培训老师跟你说，学写申论就看人民日报。但实际上，如果你不懂看人民日报的技巧，你去刷人民日报 app 纯粹是浪费时间！&lt;/p&gt;
&lt;p&gt;今天就专门讲讲人民日报 app 应该怎么用。&lt;/p&gt;
&lt;h2&gt;看人民日报 APP 什么内容？&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506252114892.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们只需要点击“&lt;strong&gt;人民日报&lt;/strong&gt;”四个字，就会有电子版的人民日报。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506252123083.jpg&quot; alt=&quot;IMG_3286&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;只看第五版的《评论》&lt;/p&gt;
&lt;p&gt;只看第五版的《评论》&lt;/p&gt;
&lt;p&gt;只看第五版的《评论》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;评论版块里的文章还可以点击，然后变成更加方便阅读的网页格式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506252123159.jpg&quot; alt=&quot;IMG_3287&quot;&gt;&lt;/p&gt;
&lt;h2&gt;重点选看对象&lt;/h2&gt;
&lt;p&gt;评论这个版块涉及的话题范围比较广，不是每一种话题都要重点研究。&lt;/p&gt;
&lt;p&gt;🔥重点看：&lt;strong&gt;民生、教育、医疗、养老、住房、食品药品安全、生态环保、文化、放管服改革、精准扶贫和乡村振兴等&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这些话题都有可能成为申论考试内容，一定要条分缕析地去研究。&lt;/p&gt;
&lt;p&gt;🥚粗略看：政治建设、司法改革，还有特别专业的比如金融。&lt;/p&gt;
&lt;p&gt;这些话题一般不会成为申论素材，只要大概看看文章结构、论证方式就行了。&lt;/p&gt;
&lt;p&gt;在评论这个版块里面，大家要特别留意“人民时评”类文章，就是下文箭头所指的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506252125337.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;人民时评文章主要针对社会热点&lt;/strong&gt;，作及时、准确、深刻的评论，其观点鲜明正统，文风清新流畅，是我们学习的主要对象。&lt;/p&gt;
&lt;p&gt;要想跟人民日报学习申论，其实最有效的方法不是通过 app 来看，而是应该把人民日报评论文章都收集起来，当成申论教材，系统研究。&lt;/p&gt;
&lt;p&gt;下文会继续讲解如何系统研究，从而临摹人民时评，形成自己的高分申论文章。&lt;/p&gt;
&lt;h2&gt;怎么看才有用&lt;/h2&gt;
&lt;p&gt;时评文章与今日头条、新浪新闻那些庸脂俗粉不一样，不能一看而过，而是要再三品尝。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每篇时评文章要从粗到细看 4 遍&lt;/strong&gt;，就像拍电影，先远镜头拍个轮廓，再慢慢拉近，拍细节。&lt;/p&gt;
&lt;p&gt;阅读前，要准备好本子和笔，边看边写。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 1 次：看大概，从头到尾浏览，了解文章基本内容；&lt;/li&gt;
&lt;li&gt;第 2 次：看结构，提炼思维导图，写在本子上；&lt;/li&gt;
&lt;li&gt;第 3 次：看段落，分析独特、可借鉴的论据和论证方法，标注在思维导图上；&lt;/li&gt;
&lt;li&gt;第 4 次：看语句，摘抄万能句和文采句。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;空说无益，我们举个栗子。&lt;/p&gt;
&lt;h3&gt;第 1 次：看大概，了解基本内容&lt;/h3&gt;
&lt;p&gt;读完第一遍，我们知道：这篇文章主要讲了青少年网络沉迷的相关问题。&lt;/p&gt;
&lt;p&gt;准备看第二遍，记得边看边画思维导图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506252149712.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;第 2 次：看结构，画思维导图&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506252150103.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;思维导图不用非常精确，更不用讲究是否美观，只要能让你回忆起整篇文章就好。&lt;/p&gt;
&lt;h3&gt;第 3 次：看段落，分析论据和论证方法&lt;/h3&gt;
&lt;p&gt;现在我们准备进行第三遍。细看每一段，具体用了哪些论据和论证方式？哪些值得我们写申论文章时候照抄？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506252150734.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;为了方便阅读，大家可以把每段值得借鉴和模仿的地方标注在段落的后面。&lt;/p&gt;
&lt;p&gt;大家在做自己的读书笔记时，可以把这些内容，用不同颜色的笔迹，简要标注在你的思维导图上。&lt;/p&gt;
&lt;p&gt;遇到特别典型、特别适合搬到申论试卷上的时评文章，还可以把文章打印出来，贴在读书笔记旁边，并把你对优秀段落、语句的分析标注在文章上。&lt;/p&gt;
&lt;h3&gt;第 4 次：看语句，摘抄万能句和文采句&lt;/h3&gt;
&lt;p&gt;现在准备开始第四遍阅读。&lt;/p&gt;
&lt;p&gt;这一遍要找万能句——那种换个主题词，就能直接搬到我们申论文章里的句子。&lt;/p&gt;
&lt;p&gt;不但要找出万能句，还要弄明白这种万能句要怎么用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506252152179.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;按照上图的要求，把万能句分类摘抄到本子上。&lt;/p&gt;
&lt;p&gt;阅读一定数量的时评文章后，各类万能句也会越积越多。&lt;/p&gt;
&lt;p&gt;到时，你可以把相同作用的句子汇总起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如引出总论点的句子，全部汇总到一个文档里；&lt;/li&gt;
&lt;li&gt;引出原因的，全部汇总到一个文档，如下图 ↓ ↓&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506252152011.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;最后一步：感悟分析&lt;/h3&gt;
&lt;p&gt;上了考场，随便从你这个积累文档中挑出几个句子，就能写出一篇行云流水般的申论。&lt;/p&gt;
&lt;p&gt;四遍读下来，你的本子上已经有了思维导图、论证标注和语句摘抄。&lt;/p&gt;
&lt;p&gt;此时你还有最后一个任务 —— 写“感悟分析”。&lt;/p&gt;
&lt;p&gt;“感悟分析”没有字数要求。有话则长，无话则短。&lt;/p&gt;
&lt;p&gt;既可以分析文章的亮点和值得借鉴学习的地方，比如第三遍阅读，你收获的论证方式和论据，也可以是你对文章话题的思考。&lt;/p&gt;
&lt;p&gt;很多人都有这种感觉，看过的书或者文章，不管看的时候多认真，隔几天或者几小时，就全忘了。&lt;/p&gt;
&lt;p&gt;时评文章不是普通的故事或小说！&lt;/p&gt;
&lt;p&gt;你一定要想方设法让自己记住。&lt;/p&gt;
&lt;p&gt;通过写感悟分析，一方面加深你对文章的理解，防止看过就忘，另一方面倒逼你勤动脑多动笔，慢慢提升文字水平和思想深度。&lt;/p&gt;
&lt;p&gt;综上，阅读人民时评的方法，&lt;/p&gt;
&lt;p&gt;今天唯一的知识点：“3个一”！！！&lt;/p&gt;
&lt;p&gt;一个思维导图、一些摘抄和一段感悟分析。&lt;/p&gt;
&lt;p&gt;比如例文最后完成的“3个一”，如下图 ↓ ↓&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506252153517.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;时评文章不是每一篇都结构清晰，建议大家实践“3个一”时，从易到难。&lt;/p&gt;</content:encoded><h:img src="/_astro/202506281435241.CSGDen9B.png"/><enclosure url="/_astro/202506281435241.CSGDen9B.png"/></item><item><title>2025.06.18</title><link>https://coooredump.github.io/blog/journal/2025-06-18</link><guid isPermaLink="true">https://coooredump.github.io/blog/journal/2025-06-18</guid><description>努力和实现目标并不是 causal relationship，更像是 multivariate regression</description><pubDate>Wed, 18 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;离开学校越久越感觉到，努力和运气，好像运气更重要&lt;/p&gt;
&lt;p&gt;在学校里不管是找工作还是上课大多都是单线程的赛跑，努力大多数是能得到反馈&lt;/p&gt;
&lt;p&gt;但工作后才慢慢意识到，努力和实现目标并不是 causal relationship，更像是 multivariate regression&lt;/p&gt;
&lt;p&gt;努力并不能解释一切，但现实是还有一大堆变量和你控制不了的 noise&lt;/p&gt;
&lt;p&gt;你拼了命优化自己的 loss function，结果人家模型早就加了一个隐藏变量叫「时机」或「运气」&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>2025.06.17</title><link>https://coooredump.github.io/blog/journal/2025-06-17</link><guid isPermaLink="true">https://coooredump.github.io/blog/journal/2025-06-17</guid><description>那一天我二十一岁，在我一生的黄金时代，我有好多奢望。我想爱，想吃，还想在一瞬间变成天上半明半暗的云。后来我才知道，生活就是个缓慢受槌的过程，人一天天老下去，奢望也一天天消失，最后变得像挨了槌的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去，什么也槌不了我。</description><pubDate>Tue, 17 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;从小到大，没穿过什么名牌衣服，没去过什么名胜场所，没学过任何才艺，也没坐飞机出过国。&lt;/p&gt;
&lt;p&gt;中学后，我的成绩就一直不好，高考发挥不尽人意，去了家乡附近一所普通大学。四年里，也没意识到学习的重要性，成绩平淡，目光短浅。&lt;/p&gt;
&lt;p&gt;几岁的时候，我以为，我将来会是一个很厉害的人。比如别人需要好久才能掌握的自行车，我几分钟就学会了。后来才发现我不是。走在河山林间里，就像是一块鹅卵石置于潮海之中，并没有什么不同。&lt;/p&gt;
&lt;p&gt;十几岁的时候，我意气风发。每当我爬到山顶的时候，跑向海边的时候，以为我不再是我的时候。我总以为山顶的石头不一样，升起的太阳不一样。我总以为海边吹的风不一样，尽头的那边不一样。我总觉得未来还有无限的可能。&lt;/p&gt;
&lt;p&gt;二十几岁的时候，我以为生活锤得我满是伤痕，我也会开朗乐观地面对。但是更多时候，我胆怯，我拘谨，我犹豫。我害怕认识陌生人，害怕自己没出息，害怕自己买不起房子车子，害怕遇不到双向奔赴的爱情。难过的时候，一个人难过，开心的时候，一个人开心。心情低落的时候，写些蹩脚的文字。&lt;/p&gt;
&lt;p&gt;比起一事无成，我还体会过很多糟糕的感觉，比如努力了好久的面试却被轻易挂掉了，非常期待的计划却突然落了空，曾经亲密的好友不再联系，至亲离别时的泪水。太多太多了，就像是一只耷拉着脑袋行走的小狗。&lt;/p&gt;
&lt;p&gt;庆幸的是，这些年来，我也读了很多书，去了很多地方，结交了几个挚友，写下了一些见闻，从波澜壮阔的星空，到至今馋味的寿喜烧。我还有着一些渺小的心愿，并一直在为它偷偷努力着。即使遥遥无期。我希望我的坚持，可以弥补运气和先天的不足。我也知道在这个世界上，有很多我穷尽一生都难以望其项背的人。但是我希望自己再普通，也要活出一点点不一样的光芒。我讨厌以前的自己，我想要变得有趣，变得特别，变得开朗。变成王小波说的那样，有好多奢望，一瞬间天上忽明忽暗的云。在这个一生的黄金时代，年轻人如果你感到现在很辛苦，就快要坚持不下去了，请为未来感同深受的自己，再坚持一下吧。&lt;/p&gt;
&lt;p&gt;一事无成并不要紧，重要的是：&lt;strong&gt;拜托别像以前的我那样，每天丧气满怀、无精打采的&lt;/strong&gt;，这个世界也是亮晶晶的。&lt;/p&gt;
&lt;p&gt;太阳明亮，水波温柔，街道车如流水马如龙，每个人都在为了更好地自己奔波着。&lt;/p&gt;
&lt;p&gt;树木蓬郁，微风和煦，这个少年，你也可以有着不一样的梦想。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>2025.06.14</title><link>https://coooredump.github.io/blog/journal/2025-06-14</link><guid isPermaLink="true">https://coooredump.github.io/blog/journal/2025-06-14</guid><description>落叶随着风一阵摆动，家乡的银杏树一直都在，可是我已经回不去了。</description><pubDate>Sat, 14 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前几天，有个好久不联系的朋友问我，“这些年，你过得还好吗？”&lt;/p&gt;
&lt;p&gt;哈哈哈，当然过得很好呀。&lt;/p&gt;
&lt;p&gt;因为在绝大多数情况下，我过得不好，也没有勇气说出来。&lt;/p&gt;
&lt;p&gt;有时候晚上一个人漫步在晚风里，月光落在身前，夜幕下的路灯和飞虫都显得落寞。&lt;/p&gt;
&lt;p&gt;今年过了生日，我就二十五岁了。&lt;/p&gt;
&lt;p&gt;这个年纪，身边的人好像都在往前走。毕业工作，买车买房，甚至是结婚生子，似乎只有自己留在了原地。&lt;/p&gt;
&lt;p&gt;我以为二十五岁的我，会像个大人一样，在风里雨里奔跑，却没想到喜怒哀乐仍在脸上。&lt;/p&gt;
&lt;p&gt;走在路上被小朋友叫叔叔了，会不开心。&lt;/p&gt;
&lt;p&gt;常常在路边和偶遇的猫咪对叫，望着天空漂浮的云朵，发呆、思考，想着一些不切实际的事情。&lt;/p&gt;
&lt;p&gt;只是有时候，想找人出去散散步时，却发现好像已经没了，可以约出来的朋友了。&lt;/p&gt;
&lt;p&gt;恍惚间发现，原来自己真的不是个孩子了。尤其是看着身边的同学、朋友陆续结婚生子，会莫名地感到心慌。&lt;/p&gt;
&lt;p&gt;爸妈，好像也已没了当初的活力，自己却还不能好好地照顾自己。时间只是让我成长了年岁，却还没让我成为一个合格的大人。&lt;/p&gt;
&lt;p&gt;以前，我不能理解年轻人的丧。在我的认知里，从小到大的教育就在告诉我，要用自己热爱的方式过这一生。&lt;/p&gt;
&lt;p&gt;后来，我才明白，这只是一个理想的状态。&lt;/p&gt;
&lt;p&gt;我们绝大多数的人，最终都会回到世俗的生活里，为家长里短，为柴米油盐烦恼着。&lt;/p&gt;
&lt;p&gt;不知从哪一天开始，只是简单熬一下夜，第二天起来做事，就会浑身没劲。我知道那个曾经炙热的少年，就已经与我渐行渐远了。&lt;/p&gt;
&lt;p&gt;可能成长，就是在不断地放下着东西。&lt;/p&gt;
&lt;p&gt;后来，我渐渐学会了收起自己的锋芒，把委屈藏在心中，说话做事都有所顾虑。在处理问题时，早没了当初初生牛犊不怕虎的干劲……&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;隐忍妥协，但有时候又会庆幸自己，依旧有着世俗无法改变的东西。比如说学不会抽烟喝酒，学不会逢迎欺骗，也有着自己的清高与小傲娇，还是那样喜欢沉浸在自己的世界里。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;还是会有孩子的心性，比如说贪玩懒惰。&lt;/p&gt;
&lt;p&gt;看着别人事业有成的时候，又会陷入焦虑，觉得自己这个样子不思进取，但却没了耐心和心思去学习。&lt;/p&gt;
&lt;p&gt;其实，自己并不是个不喜欢分享的人。只是在大多数情况下，没人愿意聆听我的琐碎。&lt;/p&gt;
&lt;p&gt;所以，我很沉默，一直都沉默。&lt;/p&gt;
&lt;p&gt;朋友圈已经慢慢没了生活的痕迹。&lt;/p&gt;
&lt;p&gt;身边的绝大多数人，好像都选择了三天可见。&lt;/p&gt;
&lt;p&gt;我觉得挺好的，因为现实里很少有人会真正关心你。大部分的人，根本不会多看一眼你的动态。&lt;/p&gt;
&lt;p&gt;我们这一生，注定会被很多人路过，也会路过很多人。&lt;/p&gt;
&lt;p&gt;可能只有等到哪天，真正遇见同频共振的那个人，才会像只刺猬样敞开心扉，让彼此看看内心深处，不为人知的优雅。&lt;/p&gt;
&lt;p&gt;或许相遇的那天，我会因为历经孤独，而格外懂得珍惜。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;以前我总以为，人生最美好的是相遇。后来才明白，其实最美好的可能是重逢。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为人生里的很多告别，都是毫无征兆的。那些悄无声息的离开，或许是永久的沉默和不回头。&lt;/p&gt;
&lt;p&gt;后来的我们，不再去追问心知肚明的答案，也不再轻易地将自己的情绪表露出来，开始尝试去做一个不动声色的人。&lt;/p&gt;
&lt;p&gt;那些曾以为生命之不能承受的事，就像是散落在风中的银杏叶，随着成长亦被岁月带走。&lt;/p&gt;
&lt;p&gt;那些曾以为刻骨铭心的经历，或是痛苦难捱的日子。后来提及，两个字就足以概括。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;从前，从前。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其实，自己这些年来，也并不是过得一点都不快乐。&lt;/p&gt;
&lt;p&gt;比如说在路上遇见的快乐小狗，久违的文章动态还有人给我点赞，这些就足够让我快乐。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但又好像不算是真的快乐，只是在一阵短暂的欢愉后，就没了动静。再也没了那种小时候，可以为一件事期待好久好久，就算是得到了还会一直回味的感觉。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;小时候的我无忧无虑，却总想着长大。长大后，却又开始怀恋小时候，或许是我还未做好准备，接纳成年人这个身份罢了。&lt;/p&gt;
&lt;p&gt;我记得小时候，姥姥家的院子里，种着一颗大银杏树。每年春夏，枝头总是挂着一片绿意。等秋天一到，金灿灿的叶子就在风中招摇。&lt;/p&gt;
&lt;p&gt;风将落叶带去远方，天空飘着的云很是明亮。&lt;/p&gt;
&lt;p&gt;我只是安静地看着，邻居家那只爱趴在我家屋檐上，呼呼大睡的肥猫，就能虚度一下午的时光。&lt;/p&gt;
&lt;p&gt;不知那时候，我们在树下一起追逐玩闹的孩子们。现在，你们怎么样了？这些年来，过得还好吗？&lt;/p&gt;
&lt;p&gt;当我在写这篇日寄时，正在为学习和生活上的一地鸡毛而烦恼着。不知你们会不会也像我一样，在过去或未来的某个时刻，也在怀念着那个无忧的年代。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;落叶随着风一阵摆动，家乡的银杏树一直都在，可是我已经回不去了。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;老友记&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171337734.jpg&quot; alt=&quot;@Yikun Wu&quot;&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171337196.jpg&quot; alt=&quot;@Yikun Wu&quot;&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171337883.jpg&quot; alt=&quot;@Yikun Wu&quot;&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171338752.jpg&quot; alt=&quot;@Yikun Wu&quot;&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171337972.jpg&quot; alt=&quot;@Yikun Wu&quot;&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171338593.jpg&quot; alt=&quot;@Yikun Wu&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>福建·福州</title><link>https://coooredump.github.io/blog/tourism/fuzhou_2025-06-14</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/fuzhou_2025-06-14</guid><description>落叶随着风一阵摆动，家乡的银杏树一直都在，可是我已经回不去了</description><pubDate>Sat, 14 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;老友记&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171337734.jpg&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171337196.jpg&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171337883.jpg&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171338752.jpg&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171337972.jpg&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506171338593.jpg&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506180249125.MVZ7TmxZ.jpg"/><enclosure url="/_astro/202506180249125.MVZ7TmxZ.jpg"/></item><item><title>2025.06.11</title><link>https://coooredump.github.io/blog/journal/2025-06-11</link><guid isPermaLink="true">https://coooredump.github.io/blog/journal/2025-06-11</guid><description>厦门今天的天气很不错，夜晚有风，但一点不冷，白天 30 度的余热还没散去，风吹来只觉得凉爽，很像大三刚保研完在湖边吹风的那个夜晚，意气风发。</description><pubDate>Wed, 11 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我一直觉得，我以前和现在，本质上并没有太大的变化，直到我看到曾经自己写的日寄&lt;/p&gt;
&lt;p&gt;我需要承认，我和以前不一样了&lt;/p&gt;
&lt;p&gt;我不再敢打敢拼，不再义无反顾，我会犹豫，会权衡利弊，会害怕未来，会怀疑自己&lt;/p&gt;
&lt;p&gt;厦门今天的天气很不错，夜晚有风，但一点不冷，白天 30 度的余热还没散去，风吹来只觉得凉爽&lt;/p&gt;
&lt;p&gt;很像大三刚保研完在湖边吹风的那个夜晚，意气风发&lt;/p&gt;
&lt;p&gt;哎，这一路走来，给过去的我丢人了&lt;/p&gt;
&lt;p&gt;我在想，人总会告别这个世界，如果是生病的话，应该会有一个时间周期，如果真有这么一天，我不希望你遇到我时，和我说你有多舍不得我，什么没我不行，什么明明知道没有希望还鼓励我的话，什么没有你日子怎么过，你有什么事情还没有做，等你好了我们一起去xxx怎样&lt;/p&gt;
&lt;p&gt;我更想听的是，你有多么怀念我，我这辈子已经完成了什么，告诉我接下来的日子没有我你们仍然会好好过&lt;/p&gt;
&lt;p&gt;这就足够了&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>《悉达多》</title><link>https://coooredump.github.io/blog/library/siddhartha</link><guid isPermaLink="true">https://coooredump.github.io/blog/library/siddhartha</guid><description>悉达多经历了学习、苦行、聆听教义、 觉醒、堕入世俗、悔恨、修行、学会爱，最终在内心平静中修得圆满。这也是每个人追寻自我生命意义的过程。一个人必须亲身去经历，任何教义和知识都无法让人真正明白道之真意，任何言辞也无法替代自身的体悟。用心去感受一切，不拘于表象，总有一天，我们都会找到自己。</description><pubDate>Wed, 11 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;1919年，赫尔曼·黑塞开始写《悉达多》，当时他40多岁，刚结束了战俘救济的工作，唯一的经济来源是出版社的资助。&lt;/p&gt;
&lt;p&gt;当时的环境对黑塞乃至整个世界的作家来说，都是一次巨大的冲击。&lt;/p&gt;
&lt;p&gt;与此同时，他的妻子因为精神分裂经常需要住院，在这样的情况下，他不得不把自己的三个孩子寄养到朋友家。&lt;/p&gt;
&lt;p&gt;毫无疑问，此时黑塞的人生正在走夜路，人在陷入困境的时候，难免会想：自己为什么来到这世间要受苦，人来到这世间究竟是为了什么？我们来世间走一趟真正重要的是什么？&lt;/p&gt;
&lt;p&gt;带着这样的疑问，黑塞用三年时间写出了《悉达多》，他把自己的人生经历融入到悉达多的经历里，追问世界的真相，探寻人生的意义。&lt;/p&gt;
&lt;p&gt;悉达多这一生经历的困惑，年轻时期的叛逆，中年时期的沉沦，晚年与孩子的别离之苦，这也是千千万万普通人会经历的一生。&lt;/p&gt;
&lt;p&gt;《悉达多》的译者姜乙曾经这样评价黑塞以及他的作品：他永远能找到他的读者，读者也永远能在100年前写的书里找到我们当代的生活。&lt;/p&gt;
&lt;p&gt;普通人和悉达多唯一不同的是，悉达多一直经历着，一直感悟着，最终他证悟涅槃了。&lt;/p&gt;
&lt;p&gt;悉达多离开父母，离开家乡，和乔文达跟沙门们出发去修行，他们是很纯正的修行人，每日除了打坐、化斋，听师父们讲经文、讲道义，无它。&lt;/p&gt;
&lt;p&gt;修行初期的悉达多认为，这世间一切都是欺骗，一切都是谎言，欲望、幸福、美丽都是虚的，活着就是一种折磨。他觉得只要达到无我的境地，他就能觉醒，继而发现那个更大的秘密。&lt;/p&gt;
&lt;p&gt;看起来这样的修行才离涅槃更近，可为什么悉达多最终选择入世呢？&lt;/p&gt;
&lt;p&gt;因为有一天他突然发现事情不对劲，他出世的时候确实学到思考、等待、斋戒、三个本领，但这三个本领真的必须要在沙门队伍中学得吗？&lt;/p&gt;
&lt;p&gt;答案当然是未必。禅定和斋戒都是暂时从“我”中逃离出来，这种逃离通过喝酒也能获得。&lt;/p&gt;
&lt;p&gt;悉达多跟乔文达说，他觉得修行和求知欲并没有让他们离解脱更近，他们一直在原地踏步。&lt;/p&gt;
&lt;p&gt;每次读到这里我都大为震撼，一语惊醒梦中人啊，现实中也有些修行人，会专门腾出一段时间去寺庙吃斋饭、打坐，可是回到现实，他依然是原来的他。&lt;/p&gt;
&lt;p&gt;还有些修行人，他们不用去修行团，每天在家冥想一段时间，剩下的时间就好好经营生活，有问题就处理问题，没问题就去思考关于自身的问题。&lt;/p&gt;
&lt;p&gt;其实，真正的修行不在别处，而在生活的每时每刻中。&lt;/p&gt;
&lt;p&gt;智慧无法通过学习得来，只有通过自己经历、觉知、感悟，才能真正转化成自己内在的东西。&lt;/p&gt;
&lt;p&gt;悉达多和乔文达找到乔达摩后，听了乔达摩的法义后，乔文达决定跟随乔达摩，但悉达多决定开启自己的求道之路，走之前他跟乔达摩说，您的法义让我敬佩，它如此清晰，没有瑕疵，但中间却有断裂之处。&lt;/p&gt;
&lt;p&gt;也就说，这些法义并没有能解决悉达多内心的疑惑，他没法证实通过学习这些法义，就能脱离苦海。&lt;/p&gt;
&lt;p&gt;我们每个人都是如此，听过很多道理，却过不好这一生，因为这道理是别人从生活里悟出来的，而我们只是道听途说，真正用的时候，要么想不起来，要么根本不会用。&lt;/p&gt;
&lt;p&gt;众所周知，人要对家人耐心一点，毕竟他们才是这世间除了自己之外最亲的人。&lt;/p&gt;
&lt;p&gt;但现实中很多人总是把和颜悦色留给外人，把最狠最毒的话刺进家人的身体里。&lt;/p&gt;
&lt;p&gt;有人吃了亏才能懂得一些道理，有人撞了南墙才能学到智慧，有人被骗过才能人间清醒，所有的道理只有通过检验才能转化成自己的经验，反复练习才能成为自己的行为模式，最终达到知行合一的效果。&lt;/p&gt;
&lt;p&gt;每个人的内心都有很多执念，我们渴望家人的爱，想要朋友常伴身边，希望和爱人相守，也渴望跟儿女长久相伴，互不生厌。&lt;/p&gt;
&lt;p&gt;随着时光的流逝，父母离开我们，朋友渐渐走散，相爱的人渐渐疏离，孩子们会径直走入自己的世界，所以，有人说，长大就是失去的过程。&lt;/p&gt;
&lt;p&gt;但再想想，这些执念不过是我们一厢情愿的臆想，而世间万物有自己的运行规则。&lt;/p&gt;
&lt;p&gt;你有你的安排，世界另有安排。&lt;/p&gt;
&lt;p&gt;小时候，我们总以为自己是世界的中心，长大后才发现，人这一生都在一个巨大的游乐场里，甚至这个游乐场都不是由时间和空间构成的二维空间，它可能是三维甚至更大的空间在主宰着。&lt;/p&gt;
&lt;p&gt;正如《正见》里的一句话：我们的血肉，我们所有的情绪，我们所有的觉受，都是由两个以上的元素组合而成。&lt;/p&gt;
&lt;p&gt;也就是所谓的世间万物都是由因缘和合构成的，所以无常才是常态。&lt;/p&gt;
&lt;p&gt;我们很容易遗忘它，继而进入一种痛苦的轮回。假如我们时时刻刻带着因缘和合觉知，就会明白自己经历的一切都是合理的。&lt;/p&gt;
&lt;p&gt;父母为什么会离我们而去，因为人一旦出生，死亡就是固定的结局。&lt;/p&gt;
&lt;p&gt;朋友为什么会渐渐走散，因为当初维系你们之前关系的纽带已经不在，渐行渐远也就不足为奇。&lt;/p&gt;
&lt;p&gt;爱人间之所以会渐渐疏离，因为每个人都在变化，关系模式不调，就是会越来越远。&lt;/p&gt;
&lt;p&gt;孩子们为什么离我们越来越远，因为他们对世界充满好奇，而我们总是跟他们讲旧故事。&lt;/p&gt;
&lt;p&gt;且不说这么人际关系这么复杂的事情，即便是你想在家吃个白水煮蛋，都需要讲究因缘和合。&lt;/p&gt;
&lt;p&gt;首先你要有一个鸡蛋，还要确保家里没停水，燃气可以使用，煮蛋的锅是完好无损的，最后你还要确保你有把鸡蛋煮熟的时间，然后才能拿到一个煮熟的鸡蛋。&lt;/p&gt;
&lt;p&gt;悉达多跟乔文达曾经因为有共同的目标，所以同行了一段时间，之后悉达多觉醒了，他走向了另外一条路，所以，他告别了朋友。&lt;/p&gt;
&lt;p&gt;他遇上了船夫，渡过那条河，继而遇上迦摩罗，学习爱经，遇到商人，学会经商，遇见赌徒，成为赌徒，当他发现自己沦陷到世俗中时，他选择再次出世，这一切都是因缘和合，也都是无常。&lt;/p&gt;
&lt;p&gt;普通人的生命也是这样，孤独地从生命的每个角落走过，承受不一样的玷污，承担属于自己的罪过，反反复复地跟自己和解，寻找一条属于自己的出路。&lt;/p&gt;
&lt;p&gt;这世界上没有两片相同的叶子，也没有两个相同的人，我们都是由无数人的碎片组成的独一无二的个体，生命里经历的人和事情都是合理的。&lt;/p&gt;
&lt;p&gt;史铁生有句话非常完美的印证了这样的观点：其实我们每时每刻都是幸运的，因为任何灾难的前面都可以再加上一个“更”字。&lt;/p&gt;
&lt;p&gt;所以，生而为人，一旦体认因缘和合，接纳无常，人就不用费劲去跟事情本身对抗，也就可以少受很多苦。&lt;/p&gt;
&lt;p&gt;既然无常是常态，那无论当下处于何种困境，只要你一直有在做些什么，就有极大的概率能从中走出来。&lt;/p&gt;
&lt;p&gt;悉达多曾经期待过人生圆满的状态，而乔文达这一生都在等着自己证悟涅槃那一刻。&lt;/p&gt;
&lt;p&gt;人类真的很喜欢谈未来，总觉得未来会更好，世界会更美好，人生也会更圆满。&lt;/p&gt;
&lt;p&gt;于是，我们从小到大一直在等，小学等到中学，中学等到大学，大学等到工作，工作等到退休......&lt;/p&gt;
&lt;p&gt;或早或晚，我们都会发现：哎，不对啊，万一我们活不到退休，之前的等待不就是在浪费生命吗？&lt;/p&gt;
&lt;p&gt;原来我期待的圆满的人生，不会到来，换句话说，每一个当下就是圆满。&lt;/p&gt;
&lt;p&gt;在悉达多生命的最后阶段，他跟乔文达说：一切皆有定数，一切只需要我的赞赏、顺从和爱的默许。&lt;/p&gt;
&lt;p&gt;老年时期的悉达多跟船夫一起渡人，有一天他遇见了迦摩罗带着他们的孩子来到河边，去追随乔达摩，但却死在了他的身边。&lt;/p&gt;
&lt;p&gt;他本想把孩子留在身边，好好照顾，但孩子想要回到富丽堂皇的家乡，享受佣人的照顾，享用锦衣玉食，他试图挽留孩子，但船夫让他去问问河水，河水只是笑他，颤抖着笑他的愚蠢。&lt;/p&gt;
&lt;p&gt;最终，他选择放手，让年轻人归于年轻人。&lt;/p&gt;
&lt;p&gt;每当他想要去找回儿子，船夫都让他去听听河水的意见，经过多次重复后，悉达多不再和命运搏斗，也不再和自己的想法作对，他脸上才呈现出真正喜悦。&lt;/p&gt;
&lt;p&gt;其实，人生没有所谓的遗憾，一切经历都是最好的安排，生命的每个瞬间都是独特的，美妙的。&lt;/p&gt;
&lt;p&gt;失业意味着有时间好好做个总结，梳理自我的得与失，也可以趁机想清楚下个阶段人生究竟要去向哪儿？&lt;/p&gt;
&lt;p&gt;结束一段亲密关系，意味着可以回顾自己在亲密关系里的行为模式究竟被什么控制，找到爱自己的方式，再去思考自己需要什么样的爱人？&lt;/p&gt;
&lt;p&gt;不要只是探求目标，而是要去发现，发现生活的要义，生命的有趣之处，自由、敞开、没有目的地沉浸在当下，发现生活给我们的启示。&lt;/p&gt;
&lt;p&gt;我特别喜欢史铁生在《病隙碎笔》中的一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;人可以走向天堂，但不可以走到天堂。走向，意味着彼岸的成立。走到，岂非彼岸的消失。彼岸的消失即信仰的终结、拯救的放弃。因为天堂不是一处空间，不是一种物质性存在，而是道路，是精神的恒途。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;生命是一个过程，而不是一种结果，抱着凡事发生皆有利于我的态度认真生活，就会发现任何体验都是财富，也都是命运附赠的礼物。&lt;/p&gt;
&lt;p&gt;无论世事如何变迁，都要爱生活，爱命运，毕竟我们也只来这世间一次，要好好体验，才不算浪费。&lt;/p&gt;
&lt;p&gt;《悉达多》并非佛陀的故事，而是讲述了一个普通人一生的经历。悉达多经历了学习、苦行、聆听教义、 觉醒、堕入世俗、悔恨、修行、学会爱，最终在内心平静中修得圆满。这也是每个人追寻自我生命意义的过程。一个人必须亲身去经历，任何教义和知识都无法让人真正明白道之真意，任何言辞也无法替代自身的体悟。用心去感受一切，不拘于表象， 总有一天，我们都会找到自己。&lt;/p&gt;</content:encoded><h:img src="/_astro/202506111502160.uUxOFBt9.jpeg"/><enclosure url="/_astro/202506111502160.uUxOFBt9.jpeg"/></item><item><title>📷 相机与摄影的知识科普</title><link>https://coooredump.github.io/blog/photography/camera-and-photography</link><guid isPermaLink="true">https://coooredump.github.io/blog/photography/camera-and-photography</guid><description>/ 无限进步 / 無限進步 /</description><pubDate>Wed, 11 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;2024 最佳相机&lt;/h2&gt;
&lt;p&gt;关于相机，可以先看看&lt;a href=&quot;https://www.bilibili.com/video/BV1M4k9YVEiE/&quot;&gt;影视飓风 2024 相机颁奖频道&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;影响相机成像质量的因素&lt;/h2&gt;
&lt;p&gt;大体来讲，相机拍照只需要两个部件：&lt;strong&gt;镜头和传感器&lt;/strong&gt;。其他部分都只是起到辅助功能，而这两个部件几乎决定了画质水平。&lt;/p&gt;
&lt;h3&gt;镜头&lt;/h3&gt;
&lt;p&gt;判断镜头的标准有很多，总的来说，&lt;strong&gt;定焦镜头的画质比变焦镜头好&lt;/strong&gt;，&lt;strong&gt;小变焦镜头画质比大变焦镜头好&lt;/strong&gt;。但镜头变焦能力越强，使用场景就越丰富，能拍摄到更多画面，所以变焦和定焦并没有绝对的好坏标准。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;镜头的最大光圈则是越大越好&lt;/strong&gt;，大光圈镜头一次可接受更多光线，暗光拍摄效果好，最重要的是&lt;strong&gt;大光圈能带来更好的背景虚化效果，凸显主体&lt;/strong&gt;，拍摄出梦幻的画面。定焦镜头通常体积也更小，光圈也容易做到更大。&lt;/p&gt;
&lt;h3&gt;传感器 · CMOS&lt;/h3&gt;
&lt;p&gt;首先我得科普一下传感器也就是 CMOS 相关的知识，摄影圈经常有一句话：底大一级压死人（底，一般指 CMOS 传感器）。&lt;/p&gt;
&lt;p&gt;传感器方面，&lt;strong&gt;像素数量决定了清晰度&lt;/strong&gt;，但&lt;strong&gt;最重要的还是传感器尺寸&lt;/strong&gt;，这也是一台相机最容易直观比较的部分。同技术条件下，大尺寸传感器相比小尺寸传感器拥有更大的单像素尺寸和更高的像素数量，暗光下拍摄噪点更少，画面更清晰。&lt;/p&gt;
&lt;p&gt;常见的相机传感器尺寸由小到大依次为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1/2.3 英寸&lt;/li&gt;
&lt;li&gt;1 英寸&lt;/li&gt;
&lt;li&gt;M4/3 画幅&lt;/li&gt;
&lt;li&gt;APS-C 画幅&lt;/li&gt;
&lt;li&gt;全画幅&lt;/li&gt;
&lt;li&gt;中画幅&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1/2.3 英寸传感器相当于手机传感器的尺寸，如今我们并不太推荐购买 1/2.3 英寸的相机，他们的画质水平已经赶不上拥有先进双摄算法的手机了。&lt;/p&gt;
&lt;p&gt;提升传感器尺寸能非常直观地提升画质，画质出众的相机，无不采用大传感器。但传感器也不能无限制地做大，镜头的体积和传感器尺寸是三次方的关系，需要在画质和便携性上作出取舍。大多数采用大型传感器的相机都采用体积小巧的定焦镜头，牺牲了变焦能力，这也是为了体积和画质作出的妥协。&lt;/p&gt;
&lt;h2&gt;单反相机中全画幅与中画幅的区别，如何选择合适的画幅？&lt;/h2&gt;
&lt;p&gt;画幅指的就是感光原件的大小（胶片就是指胶卷的尺寸，数码就是指传感器的尺寸）。对于画幅我们可以用 135 全画幅（或者称 35mm 全画幅）作为标准。&lt;/p&gt;
&lt;p&gt;比 135 全画幅更大的，就是中画幅和大画幅了。如果是数码的中画幅，非常贵，只有职业的风光或者商业摄影师才会选择。至于大画幅，数码的简直就是天价，一般都是特殊定制。玩中画幅和大画幅胶片也不会太便宜。所以几乎没有从这入门的。建议你拍到一定程度再考虑。&lt;/p&gt;
&lt;p&gt;135 全画幅（35mm 全画幅）又是怎么回事呢？话说很久很久以前，有个叫奥斯卡巴纳克的人做了一台叫徕卡的相机，后来这相机一直活到今天。这台相机就是用 35mm 宽的打孔电影胶片做底片的。所以这种相机叫 35mm 相机，一张底片的大小就叫 35mm 全画幅。后来柯达看着事儿能挣钱，就搞了一个一次性胶卷盒，于是大家就有了简单方便的胶卷。1 表示一次性，35 表示 35mm，于是这种胶卷就代号为 135 胶卷。使用这种胶卷的相机就叫 35mm 相机或者 135 相机。这种画幅尺寸除了叫 35mm 全画幅，也叫 135 全画幅。&lt;/p&gt;
&lt;p&gt;135 全画幅的传感器尺寸大小就是 36mm × 24mm，就是我们曾经用的最常见的胶卷的一张底片的大小。虽然曾经的胶卷叫 35mm（宽）胶卷，但是因为两边有打孔，所以实际成像的宽度就是 24mm。&lt;/p&gt;
&lt;p&gt;135 相机在胶片时代可以说是用的最广的。&lt;/p&gt;
&lt;p&gt;相机进入数码化之后，理所当然所有的数码相机应该也是 135 全画幅相机。但是因为电子传感器曾经很贵，于是很多厂商向成本妥协，推出了小传感器尺寸的各种版本相机。&lt;/p&gt;
&lt;p&gt;主流的就是 APS-C、M43 和 1 英寸。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506112046645.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;大家记住一点，不管厂商怎么宣传自己的传感器尺寸更加合理，凡是比 135 全画幅小的都是要么向体积妥协，要么向成本妥协。如果这相机体积没有特别小，一定就是向成本妥协了！&lt;/p&gt;
&lt;p&gt;同时代的数码相机，传感器尺寸越大，成像越好，这是一定的。所以我们尽量选择大尺寸传感器。另一方面，中画幅的数码相机因为其往往针对特殊领域，所以画质特别好，但是机身性能一般。&lt;/p&gt;
&lt;p&gt;目前这两年最好的选择就是 135 全画幅数码相机，各个方面比较平衡，一定要选择这样的产品。如果您因为暂时的预算问题，无法购买 135 全画幅相机，那么也要做好未来购买的准备——毕竟相机价格越来越低了。&lt;/p&gt;
&lt;p&gt;基于这一点，我们选择相机就不难了。&lt;/p&gt;
&lt;p&gt;中画幅其实是比全画幅（35mm 全画幅）更大的传感器尺寸。目前主流中画幅传感器大小是 44mm × 33mm。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506112102245.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;更大的传感器，意味着什么呢？一般来说：&lt;/p&gt;
&lt;p&gt;1、更加优异的画质 ✅&lt;/p&gt;
&lt;p&gt;2、更加出色的背景虚化能力 ✅&lt;/p&gt;
&lt;p&gt;3、更大更加不便携的体积 ❌&lt;/p&gt;
&lt;p&gt;4、更贵的价格 ❌&lt;/p&gt;
&lt;p&gt;在我看来，如果未来中画幅 645（44mm × 33mm）体积控制下来的话，将是民用神器。不过可见的未来，135 全画幅才是王道。&lt;/p&gt;
&lt;p&gt;记住，底大就是正义（底，一般指传感器），底大一级压死人。所以其实选择画幅的话，理论上肯定是越大越好，但是越大的画幅其实意味着相机越大越重，所以目前的话如果是刚入门或者刚学习摄影，我认为全画幅的可玩性还是更强的，价格也是更便宜一些。&lt;/p&gt;
&lt;p&gt;如果说你对于画质像素等有更高的追求，那么我觉得你可以选择中画幅。&lt;/p&gt;
&lt;h2&gt;焦距/焦段是什么，如何选择合适的镜头？&lt;/h2&gt;
&lt;p&gt;焦距是镜头上最重要的参数之一。&lt;/p&gt;
&lt;p&gt;比如一支镜头：Canon EF 70-200mm f/2.8L IS II USM&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其中 70-200mm 就是表示了这支镜头的焦距，是从 70mm 开始的，到 200mm 结束&lt;/li&gt;
&lt;li&gt;焦距有两个数字表示这是一支变焦镜头，焦距覆盖了从 70mm 到 200mm 的整个焦距段&lt;/li&gt;
&lt;li&gt;200÷70≈2.86：所以这是一支 2.86 倍的变焦镜头&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再比如：Canon 50mm F1.4 USM。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其中 50mm 就是这支镜头的焦距，只有一个数字表示这是一支定焦镜头&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;焦距一般是一个以 mm 为单位的数字，这个数字又是干嘛的呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个数字越小，我们叫焦距越短，我们的视野就越宽，取景范围就越广，画面中容纳的景物也就越多，但是每个景物在画面中占的面积就越小&lt;/li&gt;
&lt;li&gt;这个数字越大，我们叫焦距越长，我们的视野就越窄，取景范围就越窄，画面中容纳的景物也就越少，但是每个景物在画面中占得面积就越大&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;超广角&lt;/h3&gt;
&lt;p&gt;超广角一般指的是 24mm 以下的焦距，因为视角大，所以适合拍摄大场景，建筑，风景，都是可以的。因为往往可以离很近拍摄，所以有很强视觉冲击力，也很适合新闻摄影拍摄&lt;/p&gt;
&lt;h3&gt;24mm 和 28mm&lt;/h3&gt;
&lt;p&gt;这就是一个标准的广角焦距，主要就是用来拍摄风景。当然拍摄到此一游照也是很好的。如果你非抬杠说也能拍人像，确实也能，但是这个焦距的风光片要远远多于其它题材。24mm 相比 28mm 来说，就是更广一点，所以更好。&lt;/p&gt;
&lt;h3&gt;35mm&lt;/h3&gt;
&lt;p&gt;有人说这是大师的焦距，也被称为&lt;strong&gt;人文眼&lt;/strong&gt;。简单地说就是拍摄人文最好的焦距。之前有人问我：你之前不是说 85mm 不是拍摄人像最好的嘛？为什么又说 35mm 是拍人文的？恰恰一字之差，有了这个区别。拍摄人像是要突出人，自然要弱化背景——要么裁切出画面，要么背景虚化掉。85mm 擅长的就是这个。而拍摄人文照片的时候需要主体和背景环境的关系，所以小广角的 35mm 既能够将背景囊括进来，又可以保留足够的景深。&lt;/p&gt;
&lt;h3&gt;50mm（标准镜头）&lt;/h3&gt;
&lt;p&gt;这被称为&lt;strong&gt;标准镜头&lt;/strong&gt;，人文、人像都是很好的，也是大师的焦距。对于 50mm 标头的表述实在太多太多。我只说一点吧，我遇到过的人大多数要么喜欢 35mm（比如我），要么喜欢 50mm（比如布列松），这种差异基本上都是表现在人文拍摄时你习惯的视角。所以我觉得这俩焦距用好一个就不易，挑一个，一直坚持下去吧。&lt;/p&gt;
&lt;h3&gt;85mm&lt;/h3&gt;
&lt;p&gt;说得很多了，就是&lt;strong&gt;拍摄人像的镜头&lt;/strong&gt;。能有很好的背景虚化效果，很好地画面裁切能力，还能保持和模特之间适当的交流距离。&lt;/p&gt;
&lt;h3&gt;标准变焦镜头&lt;/h3&gt;
&lt;p&gt;一般就是 24mm 或者 28mm 起跳，70mm、85mm、120mm 左右截止。基本上就是你日常挂机的镜头，适合的题材很广。&lt;/p&gt;
&lt;h3&gt;100mm&lt;/h3&gt;
&lt;p&gt;这个焦距左右集中了最热门的一些微距镜头，因为拍摄距离适中。&lt;/p&gt;
&lt;h3&gt;135mm&lt;/h3&gt;
&lt;p&gt;也是拍摄人像的焦距，就是需要离人远一点。&lt;/p&gt;
&lt;h3&gt;200mm 和 300mm&lt;/h3&gt;
&lt;p&gt;这就是妥妥的长焦了，拍摄鸟类啊，荷花啊，运动啊，那种离得不是很远的就可以打得到了。同时也有不少用这个拍摄人像的，因为大光圈的背景虚化效果超级强烈。&lt;/p&gt;
&lt;h3&gt;超长焦&lt;/h3&gt;
&lt;p&gt;300mm 以上，野生动物、运动题材等等。也用于狗仔偷拍什么的。拍日出日落，满月弦月什么的也挺好的。初学者别觉得超长焦多牛，基本上不是做这类型题材的，很少用得到。&lt;/p&gt;
&lt;h3&gt;最后的最后&lt;/h3&gt;
&lt;p&gt;最后说一句，没什么是一定的，所以别和我抬杠非说我就要用广角拍人像，长焦拍风景。其实都可以，只是从出片量来说，某个焦距确实适合某些题材。&lt;/p&gt;</content:encoded><h:img src="/_astro/202506111804770.jAHP8QKn.jpeg"/><enclosure url="/_astro/202506111804770.jAHP8QKn.jpeg"/></item><item><title>📷 摄影作品集</title><link>https://coooredump.github.io/blog/photography/shot</link><guid isPermaLink="true">https://coooredump.github.io/blog/photography/shot</guid><description>Shot on LumixS5 by @YikunWu</description><pubDate>Wed, 11 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120421998.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120423351.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120424583.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120438626.jpg&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120438745.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120439124.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120441170.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120441474.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120441826.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120441445.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120442674.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120442028.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120443147.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120443069.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120443034.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120444658.PNG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120444746.PNG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120445445.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120445038.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120445099.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120445161.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506120446068.JPG&quot; alt=&quot;@YikunWu&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506111738722.DIeyqBc_.jpeg"/><enclosure url="/_astro/202506111738722.DIeyqBc_.jpeg"/></item><item><title>2025.05.21 华为笔试题</title><link>https://coooredump.github.io/blog/recruitment/20250521-huawei</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/20250521-huawei</guid><description>华为 20250521 笔试解析</description><pubDate>Wed, 21 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;题解链接：https://mp.weixin.qq.com/s/DawyjMfoNpKxmRfqqJR9hg&lt;/p&gt;
&lt;p&gt;测评链接：https://niumacode.com/training/127&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 开发一个简单任务调度系统&lt;/h2&gt;
&lt;p&gt;你需要开发一个简单的任务调度系统，该系统按任务优先级调度，优先级范围是 $(0, 99)$，数值越小优先级越高。只有高优先级任务执行完成后，低优先级任务才能执行，同等优先级的任务按照 $FIFO$ 原则，先进入调度系统的任务会优先调度，当优先级任务执行时，如果新增高优先级任务，高优先级任务会抢占低优先级任务。&lt;/p&gt;
&lt;p&gt;请你实现一个程序，模拟这个任务调度系统。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;添加任务&lt;/strong&gt;：将一个新任务添加到任务调度系统。任务包含一个唯一ID（&lt;code&gt;task_id&lt;/code&gt;）、优先级（&lt;code&gt;priority&lt;/code&gt;）$[0,99]$，运行时间（&lt;code&gt;time&lt;/code&gt;）$[1,10000]$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行任务&lt;/strong&gt;：任务调度系统按照调度策略，调度任务并执行。调度系统调度任务，并消耗对应时间片，时间片范围 $[1,100000]$。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;程序需要处理以下类型的输入：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;添加任务 &lt;code&gt;add task_id priority time&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;执行任务 &lt;code&gt;run time&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;输入命令总行数不超过 10000 行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run&lt;/code&gt; 命令可以有多个&lt;/li&gt;
&lt;li&gt;空行即命令结束&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;显示任务调度系统当前执行的任务 ID。若无任何任务，则显示 &lt;code&gt;idle&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;Sample 1&lt;/h3&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;add 101 0 10
add 20 1 3
add 300 0 1
run 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样例 1 解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add 101 0 10&lt;/code&gt;：添加任务 101，其优先级为 0，运行时间为 0 个时间片&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add 20 1 3&lt;/code&gt;：添加任务 20，其优先级为 1，运行时间为 3 个时间片&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add 300 0 1&lt;/code&gt;：添加任务 30，其优先级为 0，运行时间为 1 个时间片&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run 11&lt;/code&gt;：调度系统调度任务并执行。首先调度任务 101，运行了 10 个时间片，任务完成。接下来调度任务 300（其优先级高于任务 20），运行了 1 个时间片，任务完成。此时消耗完全部运行时间片（即 11）。&lt;/li&gt;
&lt;li&gt;此时调度系统要运行的任务 id 即为 20&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sample 2&lt;/h3&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;add 1 0 10
run 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;idle
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样例 2 解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add 1 0 10&lt;/code&gt;：添加任务 1，优先级 0，运行时间为 10 个时间片&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run 11&lt;/code&gt;：调度系统调度任务，并运行 11 个时间片。选择任务 1 运行了 10 个时间片，任务 1 完成。无任务待调度。&lt;/li&gt;
&lt;li&gt;调度系统无任何任务，因此显示 idle。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Solution (False)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;每执行一次 &lt;code&gt;run&lt;/code&gt; 则输出一次结果&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

struct Task {
    int id;
    int priority;
    int remain;  // 剩余时间片
};

// 每个优先级维护一个FIFO队列, 下标0是最高优先级
queue&amp;#x3C;Task&gt; slots[100];

// 添加任务
void addTask(int id, int priority, int time) {
    slots[priority].push({id, priority, time});
}

// 获取下一个待执行任务, 如果无任务则返回{-1,-1,-1}
Task getNext() {
    for (int p = 0; p &amp;#x3C; 100; ++p) {
        if (!slots[p].empty()) {
            Task t = slots[p].front();
            slots[p].pop();
            return t;
        }
    }
    return {-1, -1, -1};
}

int getCurrentTaskId() {
    for (int p = 0; p &amp;#x3C; 100; ++p) {
        if (!slots[p].empty()) {
            return slots[p].front().id;
        }
    }
    return -1;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string cmd;
    while (getline(cin, cmd)) {
        if (cmd.empty())
            break;
        stringstream ss(cmd);
        ss &gt;&gt; cmd;
        if (cmd == &quot;add&quot;) {
            int id, pr, t;
            ss &gt;&gt; id &gt;&gt; pr &gt;&gt; t;
            addTask(id, pr, t);
        } else if (cmd == &quot;run&quot;) {
            int time;
            ss &gt;&gt; time;
            while (time &gt; 0) {
                Task cur = getNext();
                if (cur.id == -1)
                    break;  // 无任务
                if (cur.remain &amp;#x3C;= time) {
                    time -= cur.remain;  // 任务完成, 消耗所有剩余时间
                } else {
                    cur.remain -= time;  // 任务未完成
                    time = 0;
                    slots[cur.priority].push(cur);  // 重新排队
                }
            }
            int nid = getCurrentTaskId();
            if (nid == -1)
                cout &amp;#x3C;&amp;#x3C; &quot;idle&quot;;
            else
                cout &amp;#x3C;&amp;#x3C; nid;
            cout &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
        }
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Solution (True)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;只输出最后一次 &lt;code&gt;run&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ 参考链接：https://codefun2000.com/p/P2972&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

// 任务结构体
struct Task {
    int id;     // 任务ID
    int prio;   // 优先级（越小越先）
    int rem;    // 剩余时间片
    int order;  // 到达顺序
};

// 优先队列比较器
struct Cmp {
    bool operator()(const Task &amp;#x26;a, const Task &amp;#x26;b) const {
        if (a.prio != b.prio)
            return a.prio &gt; b.prio;
        return a.order &gt; b.order;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    priority_queue&amp;#x3C;Task, vector&amp;#x3C;Task&gt;, Cmp&gt; pq;
    string cmd;
    int next_order = 0;
    vector&amp;#x3C;pair&amp;#x3C;string, vector&amp;#x3C;int&gt;&gt;&gt; ops;

    // 读取所有操作
    while (getline(cin, cmd)) {
        if (cmd.empty())
            break;
        stringstream ss(cmd);
        string op;
        ss &gt;&gt; op;
        if (op == &quot;add&quot;) {
            int id, p, t;
            ss &gt;&gt; id &gt;&gt; p &gt;&gt; t;
            ops.push_back({op, {id, p, t}});
        } else if (op == &quot;run&quot;) {
            int T;
            ss &gt;&gt; T;
            ops.push_back({op, {T}});
        }
    }

    // 模拟所有操作，只在最后一次 run 后输出
    int last_run_idx = -1;
    for (int i = 0; i &amp;#x3C; (int)ops.size(); i++) {
        if (ops[i].first == &quot;run&quot;)
            last_run_idx = i;
    }

    for (int i = 0; i &amp;#x3C; (int)ops.size(); i++) {
        auto &amp;#x26;op = ops[i];
        if (op.first == &quot;add&quot;) {
            // 入队一个新任务
            pq.push({op.second[0], op.second[1], op.second[2], next_order++});
        } else {
            int T = op.second[0];
            while (T &gt; 0 &amp;#x26;&amp;#x26; !pq.empty()) {
                Task t = pq.top();
                pq.pop();
                if (t.rem &amp;#x3C;= T) {
                    T -= t.rem;  // 任务完成，消耗全部剩余时间
                } else {
                    t.rem -= T;  // 任务未完成，更新剩余时间
                    T = 0;
                    pq.push(t);  // 重新入队
                }
            }
            // 如果是最后一次 run，输出结果
            if (i == last_run_idx) {
                if (pq.empty())
                    cout &amp;#x3C;&amp;#x3C; &quot;idle\n&quot;;
                else
                    cout &amp;#x3C;&amp;#x3C; pq.top().id &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
            }
        }
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505230401691.png&quot; alt=&quot;image-20250523040133465&quot;&gt;&lt;/p&gt;
&lt;h2&gt;2. 地震救灾路线&lt;/h2&gt;
&lt;p&gt;某市发生地震，为了尽快将救援物质输送到受灾乡镇，需要你设计出从救援物质集结点（有仅有一个）到某一个受灾乡镇的最短线路&lt;/p&gt;
&lt;p&gt;应急部门通过无人机助察了受灾地区地形，提供了各乡镇之间以及乡镇到救援物质集结点的距离，请你算出救援物质集结点到受灾多镇的最短路径。&lt;/p&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;第一行，N，受灾乡镇个数，3 ≤ N ≤ 20&lt;/p&gt;
&lt;p&gt;第二行至第 N+2 行，救援物质集结点以及各乡镇之间的距离矩阵（即 N+1 个节点之间的相互距离矩阵），距离取值范围是 $[0,100]$。序号 0 的节点表示救援物质集结点，序号 1 ~ N 的节点表示各个受灾乡镇。0 表示两个节点不相邻。&lt;/p&gt;
&lt;p&gt;第 N+3 行，m，要抵达的乡镇序号（范围 1~N）&lt;/p&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;从物质集结点（节点 0）到乡镇 m（节点 m）的最短路径长度&lt;/p&gt;
&lt;h3&gt;Sample 1&lt;/h3&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;5
0 5 13 0 0 0
5 0 12 0 75 0
13 12 0 25 0 0
0 0 25 0 35 20
0 75 0 35 0 40
0 0 0 20 40 0
3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;38
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样例 1 解释：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505230326852.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;从 0 到 3 的最短路径为 $0-2-3$，长度为 $13 + 25 = 38$&lt;/p&gt;
&lt;h3&gt;Sample 2&lt;/h3&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;5
0 5 13 0 0 0
5 0 12 0 75 0
13 12 0 25 0 0
0 0 25 0 35 20
0 75 0 35 0 40
0 0 0 20 40 0
5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;58
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样例 2 解释：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505230326852.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;从 0 到 5 的最短路径为 $0-2-3-5$，长度为 $13+25+20=58$&lt;/p&gt;
&lt;h3&gt;Solution&lt;/h3&gt;
&lt;p&gt;✅ 参考链接：https://codefun2000.com/p/P2973&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int main() {
    int N;
    cin &gt;&gt; N; // 受灾乡镇个数
    int total = N + 1;
    vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; d(total, vector&amp;#x3C;int&gt;(total));
    // 读入距离矩阵，节点 0 为物资集结点，1~N 为乡镇
    for (int i = 0; i &amp;#x3C; total; i++) {
        for (int j = 0; j &amp;#x3C; total; j++) {
            cin &gt;&gt; d[i][j];
        }
    }
    int m;
    cin &gt;&gt; m; // 目标乡镇编号

    const int INF = 1e9;
    vector&amp;#x3C;int&gt; dist(total, INF);
    vector&amp;#x3C;bool&gt; vis(total, false);

    dist[0] = 0; // 源点到自己的距离为 0

    // Dijkstra 算法主循环
    for (int i = 0; i &amp;#x3C; total; i++) {
        int u = -1, minDist = INF;
        // 找到未访问且 dist 最小的节点 u
        for (int j = 0; j &amp;#x3C; total; j++) {
            if (!vis[j] &amp;#x26;&amp;#x26; dist[j] &amp;#x3C; minDist) {
                u = j;
                minDist = dist[j];
            }
        }
        if (u == -1) break; // 剩余节点不可达
        vis[u] = true;
        // 松弛以 u 为起点的所有边
        for (int v = 0; v &amp;#x3C; total; v++) {
            if (!vis[v] &amp;#x26;&amp;#x26; d[u][v] &gt; 0 &amp;#x26;&amp;#x26; dist[v] &gt; dist[u] + d[u][v]) {
                dist[v] = dist[u] + d[u][v];
            }
        }
    }

    cout &amp;#x3C;&amp;#x3C; dist[m] &amp;#x3C;&amp;#x3C; endl; // 输出从 0 到 m 的最短距离
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;补充｜Dijkstra 算法&lt;/h3&gt;
&lt;p&gt;Dijkstra 算法是一种求解&lt;strong&gt;非负权图&lt;/strong&gt;上单源最短路径的算法。&lt;/p&gt;
&lt;h4&gt;算法思路‼️&lt;/h4&gt;
&lt;p&gt;将结点分成两个集合：已确定最短路长度的点集（记为 $S$ 集合）的和未确定最短路长度的点集（记为 $T$ 集合）。一开始所有的点都属于 $T$ 集合。&lt;/p&gt;
&lt;p&gt;初始化 $dis(s) = 0$，其他点的 $dis$ 均为 $+∞$。&lt;/p&gt;
&lt;p&gt;然后重复这些操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从 $T$ 集合中，选取一个最短路长度最小的结点，移到 $S$ 集合中。&lt;/li&gt;
&lt;li&gt;对那些刚刚被加入 $S$ 集合的结点的所有出边执行松弛操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;直到 $T$ 集合为空，算法结束。&lt;/p&gt;
&lt;h4&gt;时间复杂度&lt;/h4&gt;
&lt;p&gt;朴素的实现方法为每次「操作 2」执行完毕后，直接在 $T$ 集合中暴力寻找最短路长度最小的节点。「操作 2」总时间复杂度为 $O(m)$，「操作 1」总时间复杂度为 $O(n^2)$，全过程的时间复杂度为 $O(n^2+m)=O(n^2)$。&lt;/p&gt;
&lt;p&gt;可以用「&lt;strong&gt;堆&lt;/strong&gt;」来优化这一过程：每松弛一条边 $(u,v)$，就将 $v$ 插入堆中（如果 $v$ 已经在堆中，直接执行 Decrease-key），「操作 1」直接取堆顶节点即可。共计 $O(m)$ 次 Decrease-key，$O(n)$ 次 pop，堆优化能做到的最优复杂度为 $O(nlogn+m)$。特别地，&lt;strong&gt;可以用优先队列 &lt;code&gt;priority_queue&lt;/code&gt; 维护，此时无法执行 Decrease-key 操作，但可以通过每次松弛时重新插入该节点，且弹出时检查该节点是否已被松弛过，若是则跳过&lt;/strong&gt;，复杂度 $O(mlogn)$，优点是实现比较简单。&lt;/p&gt;
&lt;p&gt;这里的堆也可以用线段树实现，复杂度为 $O(mlogn)$，在一些特殊的非递归线段树实现下，该做法常数比堆更小。并且线段树支持的操作更多，在一些特殊图问题上只能用线段树来维护。&lt;/p&gt;
&lt;p&gt;✅ 在稀疏图（邻接矩阵）中，$m=O(n)$，堆优化的 Dijkstra 算法具有较大的效率优势；&lt;/p&gt;
&lt;p&gt;✅ 而在稠密图（邻接图）中，$m=O(n^2)$，这时候使用朴素实现更优。&lt;/p&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;这里同时给出 $O(n^2)$ 的暴力做法实现和 $O(m·logm)$ 的优先队列做法实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;dijkstra 算法推荐练习题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/network-delay-time/&quot;&gt;743. 网络延迟时间&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-minimum-time-to-reach-last-room-i/&quot;&gt;3341. 到达最后一个房间的最少时间 I&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-time-to-visit-disappearing-nodes/&quot;&gt;3112. 访问消失节点的最少时间&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1️⃣ 朴素实现｜稠密图（邻接图）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct edge {
  int v, w;
};

vector&amp;#x3C;edge&gt; e[MAXN];
int dis[MAXN], vis[MAXN];

void dijkstra(int n, int s) {
  memset(dis, 0x3f, (n + 1) * sizeof(int));
  dis[s] = 0;
  for (int i = 1; i &amp;#x3C;= n; i++) {
    int u = 0, mind = 0x3f3f3f3f;
    for (int j = 1; j &amp;#x3C;= n; j++)
      if (!vis[j] &amp;#x26;&amp;#x26; dis[j] &amp;#x3C; mind) u = j, mind = dis[j];
    vis[u] = true;
    for (auto ed : e[u]) {
      int v = ed.v, w = ed.w;
      if (dis[v] &gt; dis[u] + w) dis[v] = dis[u] + w;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 优先队列实现｜稀疏图（邻接矩阵）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct edge {
  int v, w;
};

struct node {
  int dis, u;

  bool operator&gt;(const node&amp;#x26; a) const { return dis &gt; a.dis; }
};

vector&amp;#x3C;edge&gt; e[MAXN];
int dis[MAXN], vis[MAXN];
priority_queue&amp;#x3C;node, vector&amp;#x3C;node&gt;, greater&amp;#x3C;node&gt;&gt; q;

void dijkstra(int n, int s) {
  memset(dis, 0x3f, (n + 1) * sizeof(int));
  memset(vis, 0, (n + 1) * sizeof(int));
  dis[s] = 0;
  q.push({0, s});
  while (!q.empty()) {
    int u = q.top().u;
    q.pop();
    if (vis[u]) continue;
    vis[u] = 1;
    for (auto ed : e[u]) {
      int v = ed.v, w = ed.w;
      if (dis[v] &gt; dis[u] + w) {
        dis[v] = dis[u] + w;
        q.push({dis[v], v});
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 云计算服务器 GPU 分配&lt;/h2&gt;
&lt;p&gt;某云计算服务商为客户提供 M 数量 GPU 核数的 GPU 分时租用服务，租用计费规则为：允许客户在每个时间单位按需租用不同的 GPU 核数，每个时间单位每个 GPU 核数的费用为 R。现有 N 个客户，每个客户有多个不重叠时间租用一定数量的 GPU 核数租用需求。对于有需求的客户，服务商可选择签约或不签约，若选择签约则需要满足租用需求中的所有时间段所需的 GPU 核数。&lt;/p&gt;
&lt;p&gt;为了实现租金最大化收益，服务商需在确保任意时间单位内分配的 GPU 核数总数不超过 M 的基础上选择与哪些客户签约租用协议。&lt;/p&gt;
&lt;p&gt;请输出租金最大化收益下的租金最大值。&lt;/p&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;第一行为 M、N、R 的数值，依次用空格隔开，输入格式为 M N R。&lt;/p&gt;
&lt;p&gt;从第二行开始，每行为一个客户的租用需求，共 N 行。每行的第一个数字为该客户端的时间段个数 timeSegmentNum，后续为 timeSegmentNum 个时间段及所需的 GPU 核数，时间段个数 timeSegmentNum 与时间段之间、多个时间段之间均用空格分割，同一个客户多个时间段已按起始时间增序排序给出。同个客户多个时间段不会重叠。同一个客户多个时间段已按起始时间增序排序给出。&lt;/p&gt;
&lt;p&gt;每个时间段及所需的 GPU 核数格式为 start 起始时间编号:end 结束时间编号:needcores 该时间段所需的 GPU 核数。&lt;/p&gt;
&lt;p&gt;变量取值范围：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$1≤ M ≤ 100000$&lt;/li&gt;
&lt;li&gt;$1≤ N ≤ 10$&lt;/li&gt;
&lt;li&gt;$1≤ R ≤ 10$&lt;/li&gt;
&lt;li&gt;$0≤ start ≤ end ≤ 10^9$&lt;/li&gt;
&lt;li&gt;$0≤ start ≤ end ≤ 10^9$&lt;/li&gt;
&lt;li&gt;$1≤ needCores ≤ 10000$&lt;/li&gt;
&lt;li&gt;$1≤ timeSegmentNum ≤ 100$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;客户的租用需求样例 $2$、$0:0:1$、$3:6:10$ 的含义是共有 2 个时间段，0:0:1 表示在第 0 个时间单位需要 1 个 GPU 核，3:6:10 表示从 3 到 6 的时间单位（包含 3 和 6）每个时间单位均需 10 个 GPU 核。&lt;/p&gt;
&lt;p&gt;图例为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505230359868.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;总租金最大值。如果任意一个客户的需求都无法满足，则输出 0&lt;/p&gt;
&lt;h3&gt;Sample 1&lt;/h3&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;10 3 2
2 0:8:5 9:23:10
2 0:8:5 9:18:10
1 0:8:5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;480
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样例 1 解释：&lt;/p&gt;
&lt;p&gt;共 3 个客户。&lt;/p&gt;
&lt;p&gt;由于第一个客户和第二个客户在 $9:18$ 时间范围段内总核数为 20 超过了 10，所以无法同时接受。&lt;/p&gt;
&lt;p&gt;最大日租金方案为：接纳第一个客户和第三个客户的需求。&lt;/p&gt;
&lt;p&gt;第一个客户共需要的GPU核数为 $9 * 5 + 15*10=195$&lt;/p&gt;
&lt;p&gt;第三个客户共需要的GPU核数为 $9 * 5=45$&lt;/p&gt;
&lt;h3&gt;Sample 2&lt;/h3&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;10 2 1
1 0:3:6
1 3:10:6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;48
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样例 2 解释：&lt;/p&gt;
&lt;p&gt;最大 GPU 核数为 10，共 2 个客户。&lt;/p&gt;
&lt;p&gt;第一客户和第二个客户在3时间点，总核数为 12 超过了 10，所以无法同时接受。&lt;/p&gt;
&lt;p&gt;第一个客户共需要的GPU核数为 $4 * 6=24$&lt;/p&gt;
&lt;p&gt;第二个客户共需要的GPU核数为 $8 * 6=48$&lt;/p&gt;
&lt;p&gt;为满足最大租金，采纳第二个客户，最大租金值为（48）* 1=48&lt;/p&gt;
&lt;h3&gt;Sample 3&lt;/h3&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;10 1 1
1 0:5:20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样例 3 解释：&lt;/p&gt;
&lt;p&gt;最大 GPU 核数为 10，共 1 个客户。
在 $0-5$ 时间段需要 20 个 GPU 核数，无法满足。&lt;/p&gt;
&lt;h3&gt;Sample 4&lt;/h3&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;10000 1 10
1 0:1000000000:10000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1000000000100000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样例 4 解释：&lt;/p&gt;
&lt;p&gt;最大 GPU 核数为 10000，共 1 个客户。&lt;/p&gt;
&lt;p&gt;客户在 $0-100000000$ 时间段需要 10000 个GPU核数，可以满足。&lt;/p&gt;
&lt;p&gt;租金最大值为 1000000000100000&lt;/p&gt;
&lt;h3&gt;Solution&lt;/h3&gt;
&lt;p&gt;✅ 参考链接：https://codefun2000.com/p/P2974&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
using ll = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);

    // 读入 M, N, R
    ll M; int N; ll R;
    cin &gt;&gt; M &gt;&gt; N &gt;&gt; R;

    // 存储每个客户的时间段需求
    vector&amp;#x3C;vector&amp;#x3C;tuple&amp;#x3C;ll,ll,ll&gt;&gt;&gt; segs(N);
    for (int i = 0; i &amp;#x3C; N; i++) {
        int t; cin &gt;&gt; t;
        while (t--) {
            ll s, e, c;
            char ch;
            cin &gt;&gt; s &gt;&gt; ch &gt;&gt; e &gt;&gt; ch &gt;&gt; c;  // 读入格式 s:e:c
            segs[i].emplace_back(s, e, c);
        }
    }

    ll ans = 0;
    // 枚举所有子集
    for (int mask = 0; mask &amp;#x3C; (1&amp;#x3C;&amp;#x3C;N); mask++) {
        vector&amp;#x3C;ll&gt; xs;
        ll W = 0;
        // 收集边界
        for (int i = 0; i &amp;#x3C; N; i++) if (mask &amp;#x26; (1&amp;#x3C;&amp;#x3C;i)) {
            for (auto &amp;#x26;seg: segs[i]) {
                ll s,e,c; tie(s,e,c) = seg;
                xs.push_back(s);
                xs.push_back(e+1);
                W += (e - s + 1) * c;
            }
        }
        if (xs.empty()) continue;
        // 离散化
        sort(xs.begin(), xs.end());
        xs.erase(unique(xs.begin(), xs.end()), xs.end());
        vector&amp;#x3C;ll&gt; diff(xs.size()+1);
        // 差分数组构造
        for (int i = 0; i &amp;#x3C; N; i++) if (mask &amp;#x26; (1&amp;#x3C;&amp;#x3C;i)) {
            for (auto &amp;#x26;seg: segs[i]) {
                ll s,e,c; tie(s,e,c) = seg;
                int l = lower_bound(xs.begin(), xs.end(), s) - xs.begin();
                int r = lower_bound(xs.begin(), xs.end(), e+1) - xs.begin();
                diff[l] += c;
                diff[r] -= c;
            }
        }
        // 扫描检查容量
        ll cur = 0;
        bool ok = true;
        for (int i = 0; i+1 &amp;#x3C; (int)xs.size(); i++) {
            cur += diff[i];
            if (cur &gt; M) { ok = false; break; }
        }
        if (ok) ans = max(ans, W * R);
    }

    cout &amp;#x3C;&amp;#x3C; ans;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202505250838756.Ch8_j8uv.jpeg"/><enclosure url="/_astro/202505250838756.Ch8_j8uv.jpeg"/></item><item><title>从 POSIX pthread 到 C++11 thread</title><link>https://coooredump.github.io/blog/cpp/from-posix-pthread-to-c11-thread</link><guid isPermaLink="true">https://coooredump.github.io/blog/cpp/from-posix-pthread-to-c11-thread</guid><description>网络编程实战与源码分析</description><pubDate>Fri, 16 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;推荐阅读：https://chengxumiaodaren.com/docs/concurrent/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 C++ 开发中，原生的线程库主要有两个，一个是 Linux 下的 &lt;code&gt;&amp;#x3C;pthread.h&gt;&lt;/code&gt;，另一个是 C++11 提供的 &lt;code&gt;&amp;#x3C;thread&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;以前一直用的是 pthread 的 API 写 C++ 的多线程程序，直到听说从 C++11 开始的标准库已经包含了对线程的支持。&lt;/p&gt;
&lt;h2&gt;pthread&lt;/h2&gt;
&lt;p&gt;pthread 中的 p 是 POSIX (Portable Operating System Interface) 的缩写，是 IEEE 为了在各种 UNIX 操作系统上运行软件，而定义 API 的一系列互相关联的标准总称。相比于 &lt;code&gt;std::thread&lt;/code&gt; 的简便易用，&lt;code&gt;pthread&lt;/code&gt; 功能比较强大。&lt;/p&gt;
&lt;h3&gt;线程的创建和管理&lt;/h3&gt;
&lt;h4&gt;创建线程｜&lt;code&gt;pthread_create&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;每个线程都有一个在进程中唯一的线程标识符，用一个数据类型 &lt;code&gt;pthread_t&lt;/code&gt; 表示，该数据类型在 Linux 中就是一个无符号长整型数据。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若创建成功，返回 0；若出错，则返回错误编号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;thread 是线程标识符，但这个参数不是由用户指定的，而是由 &lt;code&gt;pthread_create&lt;/code&gt; 函数在创建时将新线程的标识符放到这个变量中&lt;/li&gt;
&lt;li&gt;attr 指定线程的属性，可以用 NULL 表示默认属性&lt;/li&gt;
&lt;li&gt;start_routine 指定线程开始运行的函数&lt;/li&gt;
&lt;li&gt;arg 是 start_routine 所需的参数，是一个无类型指针&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;默认地，线程在被创建时要被赋予一定的属性，这个属性存放在数据类型 &lt;code&gt;pthread_attr_t&lt;/code&gt; 中，它包含了线程的调度策略，堆栈的相关信息，&lt;code&gt;join&lt;/code&gt; or &lt;code&gt;detach&lt;/code&gt; 的状态等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pthread_attr_init&lt;/code&gt; 和 &lt;code&gt;pthread_attr_destroy&lt;/code&gt; 函数分别用来创建和销毁 &lt;code&gt;pthread_attr_t&lt;/code&gt;，具体函数声明可参考 man 手册帮助。&lt;/p&gt;
&lt;h4&gt;结束线程｜&lt;code&gt;pthread_exit&lt;/code&gt;、&lt;code&gt;pthread_cancel&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;当发生以下情形之一时，线程就会结束：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程运行的函数 return 了，也就是线程的任务已经完成；&lt;/li&gt;
&lt;li&gt;线程调用了 &lt;code&gt;pthread_exit()&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;其他线程调用 &lt;code&gt;pthread_cancel()&lt;/code&gt; 结束了线程；&lt;/li&gt;
&lt;li&gt;进程调用 &lt;code&gt;exec()&lt;/code&gt; 或 &lt;code&gt;exit()&lt;/code&gt; 结束；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;main()&lt;/code&gt; 函数先结束了，而且 &lt;code&gt;main()&lt;/code&gt; 自己没有调用 &lt;code&gt;pthread_exit()&lt;/code&gt; 来等所有线程完成任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更抽象地说，线程结束执行的方式共有 3 种，分别是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线程将指定函数体中的代码执行完后自行结束；&lt;/li&gt;
&lt;li&gt;线程执行过程中，遇到 &lt;code&gt;pthread_exit()&lt;/code&gt; 函数结束执行。&lt;/li&gt;
&lt;li&gt;线程执行过程中，被同一进程中的其它线程（包括主线程）强制终止；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然，一个线程结束，并不意味着它的所有信息都已经消失，后面会看到&lt;strong&gt;僵尸线程&lt;/strong&gt;的问题。&lt;/p&gt;
&lt;p&gt;下面介绍两个函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void pthread_exit(void *retval);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;retval&lt;/code&gt; 是由用户指定的参数，&lt;code&gt;pthread_exit&lt;/code&gt; 完成之后可以通过这个参数获得线程的退出状态/信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_cancel(pthread_t thread);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;一个线程可以通过调用 &lt;code&gt;pthread_cancel&lt;/code&gt; 函数来请求取消同一进程中的线程，这个线程由 &lt;code&gt;thread&lt;/code&gt; 参数指定。&lt;/li&gt;
&lt;li&gt;如果操作成功则返回 0，失败则返回对应的错误编码。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;pthread.h&gt;
#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;stdlib.h&gt;  // sleep() 函数

// 线程执行的函数
void* thread_Fun(void* arg) {
    printf(&quot;新建线程开始执行\n&quot;);
    sleep(10);
}

int main() {
    pthread_t myThread;
    void* mess;
    int value;
    int res;

    // 创建 myThread 线程
    res = pthread_create(&amp;#x26;myThread, NULL, thread_Fun, NULL);
    if (res != 0) {
        printf(&quot;线程创建失败\n&quot;);
        return 0;
    }
    sleep(1);

    // 向 myThread 线程发送 Cancel 信号
    res = pthread_cancel(myThread);
    if (res != 0) {
        printf(&quot;终止 myThread 线程失败\n&quot;);
        return 0;
    }

    // 获取已终止线程的返回值
    res = pthread_join(myThread, &amp;#x26;mess);
    if (res != 0) {
        printf(&quot;等待线程失败\n&quot;);
        return 0;
    }
    
    // 如果线程被强制终止，其返回值为 PTHREAD_CANCELED
    if (mess == PTHREAD_CANCELED) {
        printf(&quot;myThread 线程被强制终止\n&quot;);
    } else {
        printf(&quot;error\n&quot;);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ ./pthread
新建线程开始执行
myThread 线程被强制终止
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;一个简单的多线程实现&lt;/h4&gt;
&lt;p&gt;这是一个非常简单的基于 pthread 的多线程实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;pthread.h&gt;
#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;stdlib.h&gt;

#define NUM_THREADS 5

void *printHello(void *thread_id) {
  long tid;
  tid = (long)thread_id;
  printf(&quot;Hello World! It&apos;s me, thread #%ld!\n&quot;, tid);
  pthread_exit(NULL);
}

int main(int argc, char *argv[]) {
  pthread_t threads[NUM_THREADS];
  int rc;
  long t;
  for (t = 0; t &amp;#x3C; NUM_THREADS; t++) {
    printf(&quot;In main: creating thread %ld\n&quot;, t);
    rc = pthread_create(&amp;#x26;threads[t], NULL, printHello, (void *)t);
    if (rc) {
      printf(&quot;ERROR; return code frome pthread_create() is %d\n&quot;, rc);
      exit(-1);
    }
  }
  // Last thing that main() should do
  pthread_exit(NULL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -Wall _pthread.c -lpthread -o pthread
./pthread
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;In main: creating thread 0
In main: creating thread 1
Hello World! It&apos;s me, thread #0!
In main: creating thread 2
Hello World! It&apos;s me, thread #1!
In main: creating thread 3
Hello World! It&apos;s me, thread #2!
In main: creating thread 4
Hello World! It&apos;s me, thread #3!
Hello World! It&apos;s me, thread #4!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意输出的顺序可能不同， 要特别注意的是，&lt;code&gt;main()&lt;/code&gt; 显示地调用了 &lt;code&gt;pthread_exit()&lt;/code&gt; 来等待其他线程的结束（如果不使用这个函数的话，可能 &lt;code&gt;main()&lt;/code&gt; 函数结束了也有线程没有执行完毕）&lt;/p&gt;
&lt;h4&gt;给线程传入初始化参数&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;pthread.h&gt;
#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;stdlib.h&gt;

#define NUM_THREADS 8

char *messages[NUM_THREADS];

struct thread_data {
  int tid;
  int sum;
  char *msg;
};

struct thread_data _datas[NUM_THREADS];

void *printHello(void *thread_arg) {
  int task_id, sum;
  char *hello_msg;
  struct thread_data *my_data;
  
  my_data = (struct thread_data *)thread_arg;
  task_id = my_data-&gt;tid;
  sum = my_data-&gt;sum;
  hello_msg = my_data-&gt;msg;
  
  // sleep(1);
  printf(&quot;Thread %d: %s Sum = %d\n&quot;, task_id, hello_msg, sum);
  pthread_exit(NULL);
}

int main(int argc, char *argv[]) {
  pthread_t threads[NUM_THREADS];
  int *task_ids[NUM_THREADS];
  int rc, t, sum;

  sum = 0;
  messages[0] = &quot;English: Hello World!&quot;;
  messages[1] = &quot;French: Bonjour, le monde!&quot;;
  messages[2] = &quot;Spanish: Hola al mundo&quot;;
  messages[3] = &quot;Klingon: Nuq neH!&quot;;
  messages[4] = &quot;German: Guten Tag, Welt!&quot;;
  messages[5] = &quot;Russian: Zdravstvytye, mir!&quot;;
  messages[6] = &quot;Japan: Sekai e konnichiwa!&quot;;
  messages[7] = &quot;Latin: Orbis, te saluto!&quot;;

  for (t = 0; t &amp;#x3C; NUM_THREADS; t++) {
    sum = sum + t;
    _datas[t].tid = t;
    _datas[t].sum = sum;
    _datas[t].msg = messages[t];
    printf(&quot;Creating thread %d\n&quot;, t);
    rc = pthread_create(&amp;#x26;threads[t], NULL, printHello, (void *)&amp;#x26;_datas[t]);
    if (rc) {
      printf(&quot;ERROR; return code from pthread_create() is %d\n&quot;, rc);
      exit(-1);
    }
  }
  pthread_exit(NULL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Creating thread 0
Creating thread 1
Thread 0: English: Hello World! Sum = 0
Creating thread 2
Thread 1: French: Bonjour, le monde! Sum = 1
Creating thread 3
Thread 2: Spanish: Hola al mundo Sum = 3
Creating thread 4
Thread 3: Klingon: Nuq neH! Sum = 6
Creating thread 5
Thread 4: German: Guten Tag, Welt! Sum = 10
Creating thread 6
Thread 5: Russian: Zdravstvytye, mir! Sum = 15
Creating thread 7
Thread 6: Japan: Sekai e konnichiwa! Sum = 21
Thread 7: Latin: Orbis, te saluto! Sum = 28
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;对线程的阻塞｜&lt;code&gt;pthread_join&lt;/code&gt;、&lt;code&gt;pthread_detach&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;阻塞时线程之间「&lt;strong&gt;同步&lt;/strong&gt;」的一种方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_join(pthread_t thread_id, void **value_ptr);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pthread_join&lt;/code&gt; 函数会让调用它的线程等待 &lt;code&gt;thread_id&lt;/code&gt; 线程运行结束之后再运行（如果是 &lt;code&gt;main()&lt;/code&gt; 调用，则阻塞 main 线程，直到 join 的所有线程执行结束 —— 常用于等待 main 中创建的所有线程执行完毕）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value_ptr&lt;/code&gt; 存放了其他线程的返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个可以被 join 的线程，仅仅可以被另一个线程 join，如果同时有多个线程尝试 join 同一个线程时，最终结果是未知的；另外，线程不能 join 自己。上面提到过，创建一个线程时，要赋予它一定的属性，这其中就包括 joinable or detachable 的属性，只有被声明称 joinable 的线程才可以被其他线程 join。&lt;/p&gt;
&lt;p&gt;POSIX 标准的最终版本指出线程应该被设置成 joinable 的，显式设置一个线程为 joinable，需要以下四个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Declare a pthread attribute variable of the &lt;code&gt;pthread_attr_t&lt;/code&gt; data type&lt;/li&gt;
&lt;li&gt;Initialize the attribute variable with &lt;code&gt;pthread_attr_init()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set the attribute detached status with &lt;code&gt;pthread_attr_setdetchstate()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;When done, free library resources used by the attribute with &lt;code&gt;pthread_attr_destroy()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;pthread_join()&lt;/code&gt; 函数会一直阻塞调用它的线程，直至目标线程执行结束（接收到目标线程的返回值），阻塞状态才会解除。如果 pthread_join() 函数成功等到了目标线程执行结束（成功获取到目标线程的返回值），返回值为数字 0；反之如果执行失败，函数会根据失败原因返回相应的非零值，每个非零值都对应着不同的宏，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EDEADLK&lt;/code&gt;：检测到线程发生了死锁。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EINVAL&lt;/code&gt;：分为两种情况，要么目标线程本身不允许其它线程获取它的返回值，要么事先就已经有线程调用 &lt;code&gt;pthread_join()&lt;/code&gt; 函数获取到了目标线程的返回值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ESRCH&lt;/code&gt;：找不到指定的 thread 线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再次强调，一个线程执行结束的返回值只能由一个 &lt;code&gt;pthread_join()&lt;/code&gt; 函数获取，当有多个线程调用 &lt;code&gt;pthread_join()&lt;/code&gt; 函数获取同一个线程的执行结果时，哪个线程最先执行 &lt;code&gt;pthread_join()&lt;/code&gt; 函数，执行结果就由那个线程获得，其它线程的 &lt;code&gt;pthread_join()&lt;/code&gt; 函数都将执行失败。&lt;/p&gt;
&lt;p&gt;对于一个默认属性的线程 A 来说，线程占用的资源并不会因为执行结束而得到释放。而通过在其它线程中执行&lt;code&gt;pthread_join(A,NULL);&lt;/code&gt;语句，可以轻松实现“及时释放线程 A 所占资源”的目的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;errno.h&gt;  //使用宏 ESRCH
#include &amp;#x3C;pthread.h&gt;
#include &amp;#x3C;stdio.h&gt;

// 线程执行的函数
void *ThreadFun(void *arg) { pthread_exit(&quot;test_msg&quot;); }

int main() {
    int res;
    void *thread_result;
    pthread_t myThread;
    // 创建 myThread 线程
    res = pthread_create(&amp;#x26;myThread, NULL, ThreadFun, NULL);
    if (res != 0) {
        printf(&quot;线程创建失败&quot;);
        return 0;
    }
    // 阻塞主线程，等待 myThread 线程执行结束
    res = pthread_join(myThread, &amp;#x26;thread_result);
    if (res != 0) {
        printf(&quot;1：等待线程失败&quot;);
    }
    // 输出获取到的 myThread 线程的返回值
    printf(&quot;%s\n&quot;, (char *)thread_result);

    // 尝试再次获取 myThread 线程的返回值
    res = pthread_join(myThread, &amp;#x26;thread_result);
    if (res == ESRCH) {
        printf(&quot;2：等待线程失败，线程不存在\n&quot;);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ ./pthread
test_msg
2：等待线程失败，线程不存在
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;__detachstate&lt;/code&gt; 属性值用于指定线程终止执行的时机，该属性的值有两个，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PTHREAD_CREATE_JOINABLE&lt;/code&gt;（默认值）：线程执行完函数后不会自行释放资源；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PTHREAD_CREATE_DETACHED&lt;/code&gt;：线程执行完函数后，会自行终止并释放占用的资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有 &lt;code&gt;pthread_detach()&lt;/code&gt; 函数，可以直接将目标线程的 &lt;code&gt;__detachstate&lt;/code&gt; 属性改为 &lt;code&gt;PTHREAD_CREATE_DETACHED&lt;/code&gt;，语法格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_detach(pthread_t thread);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 &lt;code&gt;__detachstate&lt;/code&gt; 属性，&lt;code&gt;&amp;#x3C;pthread.h&gt;&lt;/code&gt; 头文件中提供了 2 个与它相关的函数，分别是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_attr_getdetachstate(const pthread_attr_t * attr,int * detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *sttr，int detachstate);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以如下创建 detach 状态的线程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&amp;#x26;attr);
pthread_attr_setdetachstate(&amp;#x26;attr, PTHREAD_CREATE_DETACHED);
pthread_create(&amp;#x26;tid, &amp;#x26;attr, THREAD_FUNCTION, arg);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;⚠️ 值得注意的是：&lt;strong&gt;僵尸线程（zombie thread）是一种已经退出了的 joinable 线程，但是等待其他线程调用 &lt;code&gt;pthread_join&lt;/code&gt; 来 join 它，以收集它的退出信息&lt;/strong&gt;。如果没有其他线程调用 &lt;code&gt;pthread_join&lt;/code&gt; 来 join 它的话，它占用的一些系统资源不会被释放，比如堆栈。如果 &lt;code&gt;main()&lt;/code&gt; 函数需要长时间运行，并且创建大量 joinable 的线程，就有可能出现堆栈不足的 error。&lt;/p&gt;
&lt;p&gt;⚠️ &lt;strong&gt;对于那些不需要 join 的线程，最好利用 &lt;code&gt;pthread_detach&lt;/code&gt;，这样它运行结束后，资源就会及时得到释放&lt;/strong&gt;。注意一个线程被使用 &lt;code&gt;pthread_detach&lt;/code&gt; 之后，它就不能再被改成 joinable 的了。&lt;/p&gt;
&lt;p&gt;⚠️ 总而言之，创建的每一个线程都应该使用 &lt;code&gt;pthread_join&lt;/code&gt; 或者 &lt;code&gt;pthread_detach&lt;/code&gt; 其中一个，以防止僵尸线程的出现。&lt;/p&gt;
&lt;h4&gt;Linux 线程属性之线程栈大小｜&lt;code&gt;pthread_attr_t&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;线程的属性用 &lt;code&gt;pthread_attr_t&lt;/code&gt; 类型的变量表示，使用此变量前，必须调用 &lt;code&gt;pthread_attr_init()&lt;/code&gt; 函数进行初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_attr_init(pthread_attr_t * attr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pthread_attr_t&lt;/code&gt; 是一种结构体类型，内部包含多种线程属性（更多内容请看参考资料）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;typedef struct
{
   int __detachstate;	// 用于指定线程终止执行的时机
   int __schedpolicy;	// 指定系统调度该线程所用的算法
   struct sched_param __schedparam;		// 设置线程的优先级
   int __inheritsched;	// 默认遵循父线程的属性, 用于自定义线程的调度属性
   int __scope;			// 用于指定目标线程和哪些线程抢夺 CPU 资源
   size_t __guardsize;	// 用来设置警戒缓冲区的大小
   int __stackaddr_set;
   void* __stackaddr;
   size_t __stacksize;	// 每个线程都有属于自己的内存空间, 线程执行如果需要较大的栈内存，就需要自定义线程拥有的栈大小
} pthread_attr_t;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;POSIX 标准没有规定一个线程的堆栈大小，安全可移植的程序不会依赖于具体实现默认的堆栈限制，而是显式地调用 &lt;code&gt;pthread_attr_setstacksize&lt;/code&gt; 来分配足够的堆栈空间。&lt;/p&gt;
&lt;p&gt;关于堆栈大小的一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;pthread.h&gt;

#define N_THREADS 5
#define N 1000
#define MEGEXTRA 1000000

pthread_attr_t _attr;

void* do_work(void* thread_id) {
    double A[N][N];
    int i, j;
    long tid;
    size_t my_stack_size;
    tid = (long)thread_id;
    pthread_attr_getstacksize(&amp;#x26;_attr, &amp;#x26;my_stack_size);
    printf(&quot;Thread %ld: stack size = %ld bytes \n&quot;, tid, my_stack_size);
    for (i = 0; i &amp;#x3C; N; i++) {
        for (j = 0; j &amp;#x3C; N; j++) {
            A[i][j] = ((i * j) / 3.452) + (N - i);
        }
    }
    pthread_exit(NULL);
}

int main(int argc, char* argv[]) {
    pthread_t threads[N_THREADS];
    size_t stack_size;
    int rc;
    long t;

    pthread_attr_init(&amp;#x26;_attr);
    pthread_attr_getstacksize(&amp;#x26;_attr, &amp;#x26;stack_size);
    printf(&quot;Default stack size = %li\n&quot;, stack_size);  // 线程栈大小: 8 MB

    stack_size = sizeof(double) * N * N + MEGEXTRA;
    printf(&quot;Amount of stack needed per thread = %li\n&quot;, stack_size);

    pthread_attr_setstacksize(&amp;#x26;_attr, stack_size);
    printf(&quot;Creating threads with stack size = %li bytes\n&quot;, stack_size);

    for (t = 0; t &amp;#x3C; N_THREADS; t++) {
        rc = pthread_create(&amp;#x26;threads[t], &amp;#x26;_attr, do_work, (void*)t);
        if (rc) {
            printf(&quot;ERROR; return code from pthread_create() is %d\n&quot;, rc);
            exit(-1);
        }
    }
    printf(&quot;Creating %ld threads.\n&quot;, t);
    pthread_exit(NULL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ ./pthread 
Default stack size = 8388608
Amount of stack needed per thread = 9000000
Creating threads with stack size = 9000000 bytes
Creating 5 threads.
Thread 1: stack size = 9000000 bytes 
Thread 2: stack size = 9000000 bytes 
Thread 0: stack size = 9000000 bytes 
Thread 3: stack size = 9000000 bytes 
Thread 4: stack size = 9000000 bytes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他相关函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 返回 thread ID
pthread_self();
// 比较两个线程的 ID, 如果不同则返回 0, 否则返回一个非零值
pthread_equal(thread_1, thread_2);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;互斥锁 Mutex&lt;/h3&gt;
&lt;p&gt;Mutex 常常被用来保护那些可以被多个线程访问的共享资源，比如可以防止多个线程同时更新同一个数据时出现混乱。&lt;/p&gt;
&lt;p&gt;使用互斥锁的一般步骤是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个互斥锁，即声明一个 &lt;code&gt;pthread_mutex_t&lt;/code&gt; 类型的数据，然后初始化，只有初始化之后才能使用；&lt;/li&gt;
&lt;li&gt;多个线程尝试锁定这个互斥锁；&lt;/li&gt;
&lt;li&gt;只有一个成功锁定互斥锁，成为互斥锁的拥有者，然后进行一些指令；&lt;/li&gt;
&lt;li&gt;拥有者解锁互斥锁；&lt;/li&gt;
&lt;li&gt;其他线程尝试锁定这个互斥锁，重复上面的过程；&lt;/li&gt;
&lt;li&gt;最后互斥锁被显式地调用 &lt;code&gt;pthread_mutex_destroy&lt;/code&gt; 来进行销毁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有两种方式初始化一个互斥锁：&lt;/p&gt;
&lt;p&gt;1️⃣ 第一种，利用已经定义的常量初始化，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 第二种方式是调用  &lt;code&gt;pthread_mutex_init(mutex, attr)&lt;/code&gt; 进行初始化。&lt;/p&gt;
&lt;p&gt;当多个线程同时去锁定同一个互斥锁时，失败的那些线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是用 &lt;code&gt;pthread_mutex_lock&lt;/code&gt; 函数，那么会被阻塞，直到这个互斥锁被解锁，它们再继续竞争；&lt;/li&gt;
&lt;li&gt;如果是用 &lt;code&gt;pthread_mutex_trylock&lt;/code&gt; 函数，那么失败者只会返回一个错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后需要指出的是，保护共享数据是程序员的责任。程序员要负责所有可以访问该数据的线程都使用 &lt;code&gt;mutex&lt;/code&gt; 这种机制，否则，不使用 &lt;code&gt;mutex&lt;/code&gt; 的线程还是有可能对数据造成破坏。&lt;/p&gt;
&lt;p&gt;相关函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_mutex_init(pthread_mutex_t *__mutex, const pthread_mutexattr_t *__mutexattr);
int pthread_mutex_destroy(pthread_mutex_t *__mutex);
int pthread_mutex_lock(pthread_mutex_t *__mutex);
int pthread_mutex_unlock(pthread_mutex_t *__mutex);
int pthread_mutex_trylock(pthread_mutex_t *__mutex);
int pthread_mutexattr_init(pthread_mutexattr_t *__attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *__attr);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Example&lt;/h4&gt;
&lt;p&gt;下面是一个利用多线程进行向量点乘的程序，其中需要对 &lt;code&gt;dotstr.sum&lt;/code&gt; 这个共同读写的数据进行保护。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;pthread.h&gt;
#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;stdlib.h&gt;

/*
The following structure contains the necessary information
to allow the function &quot;dotprod&quot; to access its input data and
place its output into the structure.  This structure is
unchanged from the sequential version.
*/

typedef struct {
    double *a;
    double *b;
    double sum;
    int veclen;
} DOTDATA;

/* Define globally accessible variables and a mutex */

#define NUMTHRDS 4
#define VECLEN 100000
DOTDATA dotstr;
pthread_t callThd[NUMTHRDS];
pthread_mutex_t mutexsum;

/*
The function dotprod is activated when the thread is created.
As before, all input to this routine is obtained from a structure
of type DOTDATA and all output from this function is written into
this structure. The benefit of this approach is apparent for the
multi-threaded program: when a thread is created we pass a single
argument to the activated function - typically this argument
is a thread number. All  the other information required by the
function is accessed from the globally accessible structure.
*/

void *dotprod(void *arg) {
    /* Define and use local variables for convenience */

    int i, start, end, len;
    long offset;
    double mysum, *x, *y;
    offset = (long)arg;

    len = dotstr.veclen;
    start = offset * len;
    end = start + len;
    x = dotstr.a;
    y = dotstr.b;

    /*
    Perform the dot product and assign result
    to the appropriate variable in the structure.
    */
    mysum = 0;
    for (i = start; i &amp;#x3C; end; i++) {
        mysum += (x[i] * y[i]);
    }

    /*
    Lock a mutex prior to updating the value in the shared
    structure, and unlock it upon updating.
    */
    pthread_mutex_lock(&amp;#x26;mutexsum);
    dotstr.sum += mysum;
    printf(&quot;Thread %ld did %d to %d: mysum=%f global sum=%f\n&quot;, offset, start, end, mysum, dotstr.sum);
    pthread_mutex_unlock(&amp;#x26;mutexsum);
    pthread_exit((void *)0);
}

/*
The main program creates threads which do all the work and then print out result
upon completion. Before creating the threads, The input data is created. Since
all threads update a shared structure, we need a mutex for mutual exclusion.
The main thread needs to wait for all threads to complete, it waits for each one
of the threads. We specify a thread attribute value that allow the main thread to
join with the threads it creates. Note also that we free up handles  when they
are no longer needed.
*/

int main(int argc, char *argv[]) {
    long i;
    double *a, *b;
    void *status;
    pthread_attr_t attr;

    /* Assign storage and initialize values */

    a = (double *)malloc(NUMTHRDS * VECLEN * sizeof(double));
    b = (double *)malloc(NUMTHRDS * VECLEN * sizeof(double));

    for (i = 0; i &amp;#x3C; VECLEN * NUMTHRDS; i++) {
        a[i] = 1;
        b[i] = a[i];
    }

    dotstr.veclen = VECLEN;
    dotstr.a = a;
    dotstr.b = b;
    dotstr.sum = 0;

    pthread_mutex_init(&amp;#x26;mutexsum, NULL);

    /* Create threads to perform the dotproduct  */
    pthread_attr_init(&amp;#x26;attr);
    pthread_attr_setdetachstate(&amp;#x26;attr, PTHREAD_CREATE_JOINABLE);

    for (i = 0; i &amp;#x3C; NUMTHRDS; i++) {
        /* Each thread works on a different set of data.
         * The offset is specified by &apos;i&apos;. The size of
         * the data for each thread is indicated by VECLEN.
         */
        pthread_create(&amp;#x26;callThd[i], &amp;#x26;attr, dotprod, (void *)i);
    }

    pthread_attr_destroy(&amp;#x26;attr);

    /* Wait on the other threads */
    for (i = 0; i &amp;#x3C; NUMTHRDS; i++) {
        pthread_join(callThd[i], &amp;#x26;status);
    }
    /* After joining, print out the results and cleanup */

    printf(&quot;Sum = %f \n&quot;, dotstr.sum);
    free(a);
    free(b);
    pthread_mutex_destroy(&amp;#x26;mutexsum);
    pthread_exit(NULL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ ./pthread 
Thread 0 did 0 to 100000: mysum=100000.000000 global sum=100000.000000
Thread 2 did 200000 to 300000: mysum=100000.000000 global sum=200000.000000
Thread 1 did 100000 to 200000: mysum=100000.000000 global sum=300000.000000
Thread 3 did 300000 to 400000: mysum=100000.000000 global sum=400000.000000
Sum = 400000.000000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;条件变量 Condition Variable&lt;/h3&gt;
&lt;p&gt;互斥锁只有两种状态，这限制了它的用途。条件变量允许线程在阻塞的时候等待另一个线程发送的信号，当收到信号后，阻塞的线程就被唤醒并试图锁定与之相关的互斥锁。&lt;strong&gt;条件变量要和互斥锁结合使用&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;条件变量的声明和初始化&lt;/h4&gt;
&lt;p&gt;通过声明 &lt;code&gt;pthread_cond_t&lt;/code&gt; 类型的数据，并且必须先初始化才能使用。&lt;/p&gt;
&lt;p&gt;初始化的方法也有两种：&lt;/p&gt;
&lt;p&gt;1️⃣ 第一种，利用内部定义的常量，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_cond_t myconvar = PTHREAD_COND_INITIALIZER;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 第二种，利用函数 &lt;code&gt;pthread_cond_init(cond, attr)&lt;/code&gt;，其中 attr 由 &lt;code&gt;pthread_condattr_init()&lt;/code&gt; 和 &lt;code&gt;pthread_condattr_destroy()&lt;/code&gt; 创建和销毁；可以用 &lt;code&gt;pthread_cond_destroy()&lt;/code&gt; 销毁一个条件变量。&lt;/p&gt;
&lt;p&gt;相关函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_cond_wait(pthread_cond_t *__restrict__ __cond, pthread_mutex_t *__restrict__ __mutex);
int pthread_cond_signal(pthread_cond_t *__cond);
int pthread_cond_broadcast(pthread_cond_t *__cond);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pthread_cond_wait()&lt;/code&gt; 会阻塞调用它的线程，直到收到某一个信号：这个函数需要在 mutex 已经被锁之后进行调用，并且当线程被阻塞时，会自动解锁 mutex。信号收到后，线程被唤醒，这时 mutex 又会被这个线程锁定。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_cond_signal()&lt;/code&gt; 函数结束时，必须解锁 mutex，以供 &lt;code&gt;pthread_cond_wait()&lt;/code&gt; 锁定mutex。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当不止一个线程在等待信号时&lt;/strong&gt;，要用 &lt;code&gt;pthread_cond_broadcast()&lt;/code&gt; 代替 &lt;code&gt;pthread_cond_signal()&lt;/code&gt; 来告诉所有被该条件变量阻塞的线程结束阻塞状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Example&lt;/h4&gt;
&lt;p&gt;下面是一个例子，三个线程共同访问 &lt;code&gt;count&lt;/code&gt; 变量，thread 2 和 thread 3 竞争地对其进行加 1 的操作，thread 1 等 count 达到 12 的时候，一次性加 125 。 然后 thread 2 和 thread 3 再去竞争 count 的控制权，直到完成自己的对 count 加 10 次的任务。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;pthread.h&gt;
#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;stdlib.h&gt;

#define NUM_THREADS 3
#define TCOUNT 10
#define COUNT_LIMIT 12

int count = 0;
pthread_mutex_t count_mutex;
pthread_cond_t count_threshold_cv;

void *inc_count(void *t) {
    int i;
    long my_id = (long)t;

    for (i = 0; i &amp;#x3C; TCOUNT; i++) {
        pthread_mutex_lock(&amp;#x26;count_mutex);
        count++;

        /*
        Check the value of count and signal waiting thread when condition is
        reached.  Note that this occurs while mutex is locked.
        */
        if (count == COUNT_LIMIT) {
            printf(&quot;inc_count(): thread %ld, count = %d  Threshold reached. &quot;, my_id, count);
            pthread_cond_signal(&amp;#x26;count_threshold_cv);
            printf(&quot;Just sent signal.\n&quot;);
        }
        printf(&quot;inc_count(): thread %ld, count = %d, unlocking mutex\n&quot;, my_id, count);
        pthread_mutex_unlock(&amp;#x26;count_mutex);

        /* Do some work so threads can alternate on mutex lock */
        sleep(1);
    }
    pthread_exit(NULL);
}

void *watch_count(void *t) {
    long my_id = (long)t;

    printf(&quot;Starting watch_count(): thread %ld\n&quot;, my_id);

    /*
    Lock mutex and wait for signal.  Note that the pthread_cond_wait routine
    will automatically and atomically unlock mutex while it waits.
    Also, note that if COUNT_LIMIT is reached before this routine is run by
    the waiting thread, the loop will be skipped to prevent pthread_cond_wait
    from never returning.
    */
    pthread_mutex_lock(&amp;#x26;count_mutex);
    while (count &amp;#x3C; COUNT_LIMIT) {
        printf(&quot;watch_count(): thread %ld Count= %d. Going into wait...\n&quot;, my_id, count);
        pthread_cond_wait(&amp;#x26;count_threshold_cv, &amp;#x26;count_mutex);
        printf(&quot;watch_count(): thread %ld Condition signal received. Count= %d\n&quot;, my_id, count);
        printf(&quot;watch_count(): thread %ld Updating the value of count...\n&quot;, my_id, count);
        count += 125;
        printf(&quot;watch_count(): thread %ld count now = %d.\n&quot;, my_id, count);
    }
    printf(&quot;watch_count(): thread %ld Unlocking mutex.\n&quot;, my_id);
    pthread_mutex_unlock(&amp;#x26;count_mutex);
    pthread_exit(NULL);
}

int main(int argc, char *argv[]) {
    int i, rc;
    long t1 = 1, t2 = 2, t3 = 3;
    pthread_t threads[3];
    pthread_attr_t attr;

    /* Initialize mutex and condition variable objects */
    pthread_mutex_init(&amp;#x26;count_mutex, NULL);
    pthread_cond_init(&amp;#x26;count_threshold_cv, NULL);

    /* For portability, explicitly create threads in a joinable state */
    pthread_attr_init(&amp;#x26;attr);
    pthread_attr_setdetachstate(&amp;#x26;attr, PTHREAD_CREATE_JOINABLE);
    pthread_create(&amp;#x26;threads[0], &amp;#x26;attr, watch_count, (void *)t1);
    pthread_create(&amp;#x26;threads[1], &amp;#x26;attr, inc_count, (void *)t2);
    pthread_create(&amp;#x26;threads[2], &amp;#x26;attr, inc_count, (void *)t3);

    /* Wait for all threads to complete */
    for (i = 0; i &amp;#x3C; NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }
    printf(&quot;Main(): Waited and joined with %d threads. Final value of count = %d. Done.\n&quot;, NUM_THREADS, count);

    /* Clean up and exit */
    pthread_attr_destroy(&amp;#x26;attr);
    pthread_mutex_destroy(&amp;#x26;count_mutex);
    pthread_cond_destroy(&amp;#x26;count_threshold_cv);
    pthread_exit(NULL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ ./pthread 
Starting watch_count(): thread 1
inc_count(): thread 2, count = 1, unlocking mutex
inc_count(): thread 3, count = 2, unlocking mutex
watch_count(): thread 1 Count= 2. Going into wait...
inc_count(): thread 2, count = 3, unlocking mutex
inc_count(): thread 3, count = 4, unlocking mutex
inc_count(): thread 2, count = 5, unlocking mutex
inc_count(): thread 3, count = 6, unlocking mutex
inc_count(): thread 2, count = 7, unlocking mutex
inc_count(): thread 3, count = 8, unlocking mutex
inc_count(): thread 2, count = 9, unlocking mutex
inc_count(): thread 3, count = 10, unlocking mutex
inc_count(): thread 2, count = 11, unlocking mutex
inc_count(): thread 3, count = 12  Threshold reached. Just sent signal.
inc_count(): thread 3, count = 12, unlocking mutex
watch_count(): thread 1 Condition signal received. Count= 12
watch_count(): thread 1 Updating the value of count...
watch_count(): thread 1 count now = 137.
watch_count(): thread 1 Unlocking mutex.
inc_count(): thread 2, count = 138, unlocking mutex
inc_count(): thread 3, count = 139, unlocking mutex
inc_count(): thread 2, count = 140, unlocking mutex
inc_count(): thread 3, count = 141, unlocking mutex
inc_count(): thread 2, count = 142, unlocking mutex
inc_count(): thread 3, count = 143, unlocking mutex
inc_count(): thread 3, count = 144, unlocking mutex
inc_count(): thread 2, count = 145, unlocking mutex
Main(): Waited and joined with 3 threads. Final value of count = 145. Done.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;std::thread&lt;/h2&gt;
&lt;p&gt;在 C++11 中引入的线程库 &lt;code&gt;std::thread&lt;/code&gt; 实际是基于 &lt;code&gt;pthread&lt;/code&gt; 实现的，后续主要介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何使用 &lt;code&gt;std::thread&lt;/code&gt; 创建线程&lt;/li&gt;
&lt;li&gt;深入剖析 &lt;code&gt;std::thread&lt;/code&gt; 的设计原理&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用 std::thread&lt;/h3&gt;
&lt;p&gt;当你创建了一个（非空的）线程对象时，对应线程就会执行，不需要显式的调用 &lt;code&gt;start&lt;/code&gt; 或者 &lt;code&gt;run&lt;/code&gt;（pthread 也是）。如果之前你没有用过 pthread，也许不会理解何为“方便得出人意料”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pthread_create&lt;/code&gt; 只接受 &lt;code&gt;void *f(void *)&lt;/code&gt;，所以如果你想调用现成的函数，还需要包装一下；&lt;/li&gt;
&lt;li&gt;而且 &lt;code&gt;pthread_create&lt;/code&gt; 其函数接受参数（第四个参数）类型为 &lt;code&gt;void *arg&lt;/code&gt;，如果要传多个参数，还需要定义结构体，接着将结构体转为 &lt;code&gt;void *&lt;/code&gt; 类型再传递进去；&lt;/li&gt;
&lt;li&gt;这还没完，传递进去的参数还需要在其内部函数中，重新转型成（可能是一次性的）某个结构体，最后才能取出其中的变量；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建线程后，调用 &lt;code&gt;Thread.join&lt;/code&gt; 就会阻塞到线程执行结束为止（相当于&lt;code&gt;pthread_join&lt;/code&gt;）。你也可以选择 &lt;code&gt;detach&lt;/code&gt; 该线程，这时候线程会独立执行，不会随调用者终止而结束。&lt;/p&gt;
&lt;p&gt;在如下的 demo 中，主线程中使用 &lt;code&gt;std::thread&lt;/code&gt; 创建 3 个子线程，线程入口函数是 &lt;code&gt;do_some_work&lt;/code&gt;，在主线程运行结束前等待子线程的结束。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：在构造线程对象 &lt;code&gt;std::thread{do_some_work, i}&lt;/code&gt; 的时候，还是建议使用 &lt;code&gt;{}&lt;/code&gt; 而不是 &lt;code&gt;()&lt;/code&gt;，以防止编译器产生错误的决议，具体原因可以参考文章（&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=MzkyMjIxMzIxNA==&amp;#x26;mid=2247484397&amp;#x26;idx=1&amp;#x26;sn=02bdcfae05bf50963509187e0131ba6d&amp;#x26;chksm=c1f68ddcf68104ca42c6d11ace316b1e146b76e1473b1f17d25d7211635fe983735cf72834f8&amp;#x26;token=327902945&amp;#x26;lang=zh_CN#rd&quot;&gt;深入了解 C++：别再徘徊于 {} 与 () 之间了&lt;/a&gt;）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// #include &amp;#x3C;bits/stdc++.h&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;thread&gt;
#include &amp;#x3C;vector&gt;

const int N_THREADS = 3;

void do_some_work(int num) { std::cout &amp;#x3C;&amp;#x3C; &quot;thread: &quot; &amp;#x3C;&amp;#x3C; num &amp;#x3C;&amp;#x3C; std::endl; }

int main(int argc, char const* argv[]) {
    std::vector&amp;#x3C;std::thread&gt; thread_list;
    thread_list.reserve(N_THREADS);

    // start thread
    for (int i = 0; i &amp;#x3C; N_THREADS; i++) {
        // 🆗 thread_list.push_back(std::thread{do_some_work, i});
        // 🆗 thread_list.push_back(std::thread(do_some_work, i));
        thread_list.emplace_back(do_some_work, i);
    }
    std::cout &amp;#x3C;&amp;#x3C; &quot;work in main thread&quot; &amp;#x3C;&amp;#x3C; std::endl;

    // main() thread will waiting other threads
    for (int i = 0; i &amp;#x3C; N_THREADS; i++) {
        thread_list[i].join();
    }
    std::cout &amp;#x3C;&amp;#x3C; &quot;main thread end&quot; &amp;#x3C;&amp;#x3C; std::endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个子线程共享输出缓冲区 &lt;code&gt;std::cout&lt;/code&gt;，此时没有采取任何机制保护线程间共享数据，因此上面 demo 的输出可能不符合你的预期，即很可能不是按照如下格式输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ ./thread
thread: 0
thread: 1
thread: 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际输出结果（非常混乱）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ ./thread
thread: work in main thread
thread: 0
2
thread: 1
main thread end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从输出可以看出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先创建的线程，未必就先运行；&lt;/li&gt;
&lt;li&gt;而且几个线程之间是互相抢 CPU 资源的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程间数据共享问题及其应对措施，留到后文讲解，下面讲解 &lt;code&gt;std::thread&lt;/code&gt; 的设计。&lt;/p&gt;
&lt;h3&gt;深入剖析 std::thread&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;在 &lt;code&gt;g++&lt;/code&gt; 中，&lt;code&gt;thread&lt;/code&gt; 是基于 &lt;code&gt;pthread&lt;/code&gt; 实现的&lt;/strong&gt;。本次主要从以下三个方面分 &lt;code&gt;std::thread&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::thread&lt;/code&gt; 对象不可复制，只具有移动属性&lt;/li&gt;
&lt;li&gt;每个线程具有唯一的标志，即线程 id&lt;/li&gt;
&lt;li&gt;创建子线程（即构造 &lt;code&gt;std::thread&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. 移动属性&lt;/h4&gt;
&lt;p&gt;有很多书籍说，&lt;code&gt;std::thread&lt;/code&gt; 对象的所有权只能传递不能复制。实际上，就 &lt;code&gt;std::thread&lt;/code&gt; 对象，只具有移动属性，不具有复制属性。&lt;code&gt;std::thread&lt;/code&gt; 的构造函数如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class thread {
   private:
    id _M_id;

   public:
    thread() noexcept = default;

    template &amp;#x3C;typename _Callable, typename... _Args, typename = _Require&amp;#x3C;__not_same&amp;#x3C;_Callable&gt;&gt;&gt;
    explicit thread(_Callable&amp;#x26;&amp;#x26; __f, _Args&amp;#x26;&amp;#x26;... __args) {
        //...
    }

    ~thread() {
        if (joinable()) std::terminate();
    }
    // 禁止复制（复制构造、复制赋值）
    thread(const thread&amp;#x26;) = delete;
    thread&amp;#x26; operator=(const thread&amp;#x26;) = delete;

    // std::thread 只具有移动属性（移动构造、移动赋值）
    thread(thread&amp;#x26;&amp;#x26; __t) noexcept { swap(__t); }

    thread&amp;#x26; operator=(thread&amp;#x26;&amp;#x26; __t) noexcept {
        if (joinable()) std::terminate();
        swap(__t);
        return *this;
    }
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现，&lt;code&gt;std::thread&lt;/code&gt; 禁止了复制构造函数、复制赋值表达式，只留下了移动构造函数、移动赋值，使得 &lt;code&gt;std::thread&lt;/code&gt; 对象只能移动，不能复制。这就是之前 demo 中使用 &lt;code&gt;emplace_back&lt;/code&gt; 函数添加 &lt;code&gt;std::thread&lt;/code&gt; 对象的原因，防止触发复制构造函数。所以向 thread_list 中添加 &lt;code&gt;std::thread&lt;/code&gt; 对象有以下几种方式：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当 push_back 接受的是右值，底层调用的还是 emplace_back 函数，因此 4 和 5 是等价的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;thread_list.push_back(std::thread{do_some_work, i});	// 1.ok

thread_list.emplace_back(do_some_work, i);	 			// 2.ok
thread_list.emplace_back(std::thread{do_some_work, i});	 // 2.ok

std::thread trd{do_some_work, i};
thread_list.push_back(trd);					// 3.error❌

thread_list.push_back(std::move(trd));		 // 4.ok
thread_list.emplace_back(std::move(trd));	 // 5.ok
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三种办法报错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;/usr/include/c++/9/ext/new_allocator.h: In instantiation of ‘void __gnu_cxx::new_allocator&amp;#x3C;_Tp&gt;::construct(_Up*, _Args&amp;#x26;&amp;#x26; ...) [with _Up = std::thread; _Args = {std::thread&amp;#x26;}; _Tp = std::thread]’:
/usr/include/c++/9/bits/alloc_traits.h:483:4:   required from ‘static void std::allocator_traits&amp;#x3C;std::allocator&amp;#x3C;_CharT&gt; &gt;::construct(std::allocator_traits&amp;#x3C;std::allocator&amp;#x3C;_CharT&gt; &gt;::allocator_type&amp;#x26;, _Up*, _Args&amp;#x26;&amp;#x26; ...) [with _Up = std::thread; _Args = {std::thread&amp;#x26;}; _Tp = std::thread; std::allocator_traits&amp;#x3C;std::allocator&amp;#x3C;_CharT&gt; &gt;::allocator_type = std::allocator&amp;#x3C;std::thread&gt;]’
/usr/include/c++/9/bits/vector.tcc:115:30:   required from ‘void std::vector&amp;#x3C;_Tp, _Alloc&gt;::emplace_back(_Args&amp;#x26;&amp;#x26; ...) [with _Args = {std::thread&amp;#x26;}; _Tp = std::thread; _Alloc = std::allocator&amp;#x3C;std::thread&gt;]’
_thread.cpp:22:37:   required from here
/usr/include/c++/9/ext/new_allocator.h:146:4: error: use of deleted function ‘std::thread::thread(const std::thread&amp;#x26;)’
  146 |  { ::new((void *)__p) _Up(std::forward&amp;#x3C;_Args&gt;(__args)...); }
      |    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from _thread.cpp:3:
/usr/include/c++/9/thread:142:5: note: declared here
  142 |     thread(const thread&amp;#x26;) = delete;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. std::thread::id&lt;/h4&gt;
&lt;p&gt;可以发现，在 &lt;code&gt;std::thread&lt;/code&gt; 对象中，只有一个成员变量 &lt;code&gt;_M_id&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个类 id 全称是 &lt;code&gt;std::thread::id&lt;/code&gt;，实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class id {
    // // _M_thread 即 pthread_t 对象，线程的唯一辨识标志
    native_handle_type _M_thread;

   public:
    // _M_thread 默认值是 0
    id() noexcept : _M_thread() {}

    explicit id(native_handle_type __id) : _M_thread(__id) {}

   private:
    friend class thread;
    friend class hash&amp;#x3C;thread::id&gt;;
    // 为 std::thread::id 对象重载了 == 运算
    friend bool operator==(thread::id __x, thread::id __y) noexcept;

    friend bool operator&amp;#x3C;(thread::id __x, thread::id __y) noexcept;
    // 为 std::thread::id 对象重载了 &amp;#x3C;&amp;#x3C; 操作
    template &amp;#x3C;class _CharT, class _Traits&gt;
    friend basic_ostream&amp;#x3C;_CharT, _Traits&gt;&amp;#x26; operator&amp;#x3C;&amp;#x3C;(basic_ostream&amp;#x3C;_CharT, _Traits&gt;&amp;#x26; __out, thread::id __id);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，这个 &lt;code&gt;std::thread::id&lt;/code&gt; 实际上就是封装了 &lt;code&gt;pthread_t&lt;/code&gt; 对象，用作每个线程的标志。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在构造 &lt;code&gt;std::thread&lt;/code&gt; 对象的时候，如果没有设置线程入口函数，则线程 &lt;code&gt;_M_id._M_thread&lt;/code&gt; 的值是 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如下面的 demo，&lt;code&gt;trd&lt;/code&gt; 没有设置线程入口函数，&lt;code&gt;trd&lt;/code&gt; 调用默认构造函数时，&lt;code&gt;trd&lt;/code&gt; 的 &lt;code&gt;_M_id._M_thread&lt;/code&gt; 会被初始化为 0&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main(int argc, char const* argv[]) {
    std::thread trd;
    std::cout &amp;#x3C;&amp;#x3C; trd.get_id() &amp;#x3C;&amp;#x3C; std::endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，打印线程标志 &lt;code&gt;trd.get_id()&lt;/code&gt;，输出的是却不是0。这仅仅是 &lt;code&gt;std::thread::id&lt;/code&gt; 在重载 &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt; 操作符时的设定，用于提示调用者线程没有启动。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ g++  thread_.cc -o thread_ &amp;#x26;&amp;#x26; ./thread_
thread::id of a non-executing thread
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以到 &lt;code&gt;std::thread::id&lt;/code&gt; 重载的 &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt; 操作符的函数中一探究竟：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template&amp;#x3C;class _CharT, class _Traits&gt;
inline basic_ostream&amp;#x3C;_CharT, _Traits&gt;&amp;#x26; operator&amp;#x3C;&amp;#x3C;(basic_ostream&amp;#x3C;_CharT, _Traits&gt;&amp;#x26; __out, thread::id __id) {
    // 线程未启动 
    if (__id == thread::id())
    return __out &amp;#x3C;&amp;#x3C; &quot;thread::id of a non-executing thread&quot;;
    // 线程成功启动
    else
    return __out &amp;#x3C;&amp;#x3C; __id._M_thread;
}

// id的相等判断 
inline bool operator==(thread::id __x, thread::id __y) noexcept {
    return __x._M_thread == __y._M_thread;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此判断一个线程是否启动，可如下检测：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool thread_is_active(const std::thread::id&amp;#x26; thread_id) { 
    return thread_id != std::thread::id();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置了线程入口函数，&lt;code&gt;_M_id._M_thread&lt;/code&gt; 才是线程的&lt;code&gt;tid&lt;/code&gt;值，由 &lt;code&gt;pthread_create(&amp;#x26;tid, NULL, ...)&lt;/code&gt; 函数设置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main(int argc, char const* argv[]) {
    std::thread trd{[] { std::cout &amp;#x3C;&amp;#x3C; &quot;work in sub-thread\n&quot;; }};

    std::cout &amp;#x3C;&amp;#x3C; trd.get_id() &amp;#x3C;&amp;#x3C; std::endl;
    trd.join();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ ./thread 
140203273147968
work in sub-thread
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;by the way&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在创建 &lt;code&gt;std::thread&lt;/code&gt; 对象 &lt;code&gt;trd&lt;/code&gt; 时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果设置了线程入口函数，那么就必须使用 &lt;code&gt;trd.join()&lt;/code&gt; 或者 &lt;code&gt;trd.detach()&lt;/code&gt; 来表达子线程与主线程的运行关系，否则在 &lt;code&gt;std::thread&lt;/code&gt; 对象析构时，整个程序会被 &lt;code&gt;std::terminate()&lt;/code&gt; 中止&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果没有设置线程入口函数，&lt;code&gt;trd.joinable()&lt;/code&gt; 返回值就是 &lt;code&gt;false&lt;/code&gt;，因此不会触发 &lt;code&gt;std::terminate()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;~thread() {
  if (joinable())
    std::terminate();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 创建子线程&lt;/h4&gt;
&lt;p&gt;当构造 &lt;code&gt;std::thread&lt;/code&gt; 对象时，设置了线程入口函数，会在相匹配的构造函数里调用 &lt;code&gt;pthread_create&lt;/code&gt; 函数创建子线程。先看整体实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// std::thread 构造函数
template&amp;#x3C;typename _Callable, 
         typename... _Args,
         typename = _Require&amp;#x3C;__not_same&amp;#x3C;_Callable&gt;&gt;&gt;
explicit thread(_Callable&amp;#x26;&amp;#x26; __f, _Args&amp;#x26;&amp;#x26;... __args)
{
    static_assert( __is_invocable&amp;#x3C;typename decay&amp;#x3C;_Callable&gt;::type, 
                                  typename decay&amp;#x3C;_Args&gt;::type...&gt;::value,
                  &quot;std::thread arguments must be invocable after conversion to rvalues&quot;);

    // Create a reference to pthread_create, not just the gthr weak symbol.
    auto __depend = reinterpret_cast&amp;#x3C;void(*)()&gt;(&amp;#x26;pthread_create);
    // 启动线程
    _M_start_thread(_S_make_state(__make_invoker(std::forward&amp;#x3C;_Callable&gt;(__f), 
                                                 std::forward&amp;#x3C;_Args&gt;(__args)...)),
                    __depend);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再细看构造函数执行流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在编译期判断构造 &lt;code&gt;std::thread&lt;/code&gt; 对象时设置的线程入口函数 &lt;code&gt;__f&lt;/code&gt; 及其参数 &lt;code&gt;__args&lt;/code&gt; 能否调用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;比如，下面的 demo 中，线程入口函数 &lt;code&gt;thread_func&lt;/code&gt; 有个 &lt;code&gt;int&lt;/code&gt; 类型的参数 &lt;code&gt;arg&lt;/code&gt;，如果传入的参数 &lt;code&gt;__args&lt;/code&gt; 无法隐式转换为 &lt;code&gt;int&lt;/code&gt; 类型，或者没有设置 &lt;code&gt;__args&lt;/code&gt;，都会触发 &lt;code&gt;std::thread&lt;/code&gt; 构造函数中的静态断言 &lt;code&gt;static_assert&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;报错：&lt;strong&gt;error: static assertion failed: std::thread arguments must be invocable after conversion to rvalues&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void thread_func(int arg) { }

int main(int argc, char const *argv[]) {
    std::thread trd_1{thread_func, &quot;str&quot;};  // arg 类型不对
    std::thread trd_2{thread_func};	 	    // 缺少 arg

    // ...
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;将线程入口函数 &lt;code&gt;__f&lt;/code&gt; 及其参数 &lt;code&gt;__args&lt;/code&gt; 进一步封装起来。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里是使用 &lt;code&gt;__make_invoker&lt;/code&gt; 完成的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__make_invoker(std::forward&amp;#x3C;_Callable&gt;(__f), std::forward&amp;#x3C;_Args&gt;(__args)...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;__make_invoker&lt;/code&gt; 的作用是返回一个 &lt;code&gt;_Invoker&lt;/code&gt; 对象，&lt;strong&gt;&lt;code&gt;_Invoker&lt;/code&gt; 是个仿函数，通过 &lt;code&gt;_Invoker()&lt;/code&gt; 就可以以指定的参数 &lt;code&gt;__args&lt;/code&gt; 直接执行线程入口函数 &lt;code&gt;__f&lt;/code&gt;&lt;/strong&gt;。类似于 &lt;code&gt;std::bind&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; void print_num(int i) {
     std::cout &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &apos;\n&apos;;
 }

 int main(int argc, const char* argv[]) {
    // wrapper
    auto invoker =  std::bind(print_num, -9);
    // 直接调用 invoker() 就可以以指定参数 -9 调用 print_num
    invoker();
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;启动子线程&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在调用 &lt;code&gt;_M_start_thread&lt;/code&gt; 函数启动子线程前，执行过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建 &lt;code&gt;_State_ptr&lt;/code&gt; 的对象，来封装 &lt;code&gt;_Invoker&lt;/code&gt; 对象，再传递给 &lt;code&gt;_M_start_thread&lt;/code&gt; 函数。&lt;/li&gt;
&lt;li&gt;传递 &lt;code&gt;_M_start_thread&lt;/code&gt; 函数的过程，由 &lt;code&gt;_S_make_state&lt;/code&gt; 函数完成，&lt;code&gt;_S_make_state&lt;/code&gt; 最终返回 &lt;code&gt;_State_ptr&lt;/code&gt; 对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 基类
struct _State {
   virtual ~_State();          // 虚析构函数
   virtual void _M_run() = 0;  // 线程运行函数
};
using _State_ptr = unique_ptr&amp;#x3C;_State&gt;;	// 父类指针

// 子类
template&amp;#x3C;typename _Callable&gt;
struct _State_impl : public _State {
   _Callable		_M_func;	// 线程入口函数

   _State_impl(_Callable&amp;#x26;&amp;#x26; __f) : _M_func(std::forward&amp;#x3C;_Callable&gt;(__f))
   { }

   void _M_run() { _M_func(); } // 执行线程入口函数
};

// 传入_Invoker对象，返回 _State_ptr 对象
template&amp;#x3C;typename _Callable&gt;
static _State_ptr _S_make_state(_Callable&amp;#x26;&amp;#x26; __f)  {
   using _Impl = _State_impl&amp;#x3C;_Callable&gt;;
   // 使用子类对象来初始化父类
   return _State_ptr{new _Impl{std::forward&amp;#x3C;_Callable&gt;(__f)}};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;_S_make_state&lt;/code&gt; 函数，将线程入口函数 &lt;code&gt;__f&lt;/code&gt; 及其参数 &lt;code&gt;__args&lt;/code&gt; 封装到 &lt;code&gt;_State_ptr&lt;/code&gt; 对象 &lt;code&gt;_State_ptr_obj&lt;/code&gt; 中， 这样最后可以通过 &lt;code&gt;_State_ptr_obj-&gt;_M_run()&lt;/code&gt; 来调用 &lt;code&gt;__f&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下面到了 &lt;code&gt;_M_start_thread&lt;/code&gt; 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void thread::_M_start_thread(_State_ptr state, void (*)())
{
  const int err = __gthread_create(&amp;#x26;_M_id._M_thread,
                                    &amp;#x26;execute_native_thread_routine, // 线程执行函数
                                    state.get());
  if (err)
    __throw_system_error(err);
  state.release();
}

// 内部调用的是 pthread_create 函数
static inline int __gthread_create(pthread_t *__threadid, void *(*__func) (void*), void *__args)
{
  return pthread_create(__threadid, NULL, __func, __args);
}

// 内部执行线程入口函数
static void* execute_native_thread_routine(void* __p)
{
  thread::_State_ptr __t{static_cast&amp;#x3C;thread::_State*&gt;(__p)};
  __t-&gt;_M_run();		// 运行线程入口函数
  return nullptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，在执行完 &lt;code&gt;_M_start_thread&lt;/code&gt; 函数后，才具有 &lt;code&gt;_M_start_thread != 0&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;Mutex&lt;/h3&gt;
&lt;p&gt;有时候需要限制多个线程对同一资源的访问，这时候一般会使用 &lt;code&gt;Mutex&lt;/code&gt;。Mutex 就是一把锁，只有某些线程可以同时占用它（通过 lock 操作）。当线程不用的时候，就得通过 unlock 操作来释放它。&lt;/p&gt;
&lt;p&gt;对于 Mutex，&lt;code&gt;std::thread&lt;/code&gt; 和 &lt;code&gt;pthread&lt;/code&gt; 差不多，无非是 &lt;code&gt;pthread_mutex_lock(&amp;#x26;mutex)&lt;/code&gt; 变成了 &lt;code&gt;mutex.lock()&lt;/code&gt; 等等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不过在 &lt;code&gt;std::thread&lt;/code&gt; 中，mutex 往往和 lock 系列模板一起使用。这是因为 lock 系列模板包装了 mutex 类，提供了 RAII 风格的加锁解锁&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;{
    // 加锁
    std::lock_guard&amp;#x3C;std::mutex&gt; guard(mutex);
    ...

    // 自动解锁
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;mutex 有四种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::mutex&lt;/code&gt;：独占的互斥量，不能递归使用，不带超时功能&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::recursive_mutex&lt;/code&gt;：递归互斥量，可重入，不带超时功能&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::timed_mutex&lt;/code&gt;：带超时的互斥量，不能递归&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::recursive_timed_mutex&lt;/code&gt;：带超时的互斥量，可以递归使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;加解锁方式有三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::lock_guard&lt;/code&gt;：可以RAII方式加锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::unique_lock&lt;/code&gt;：比 &lt;code&gt;lock_guard&lt;/code&gt; 多了个手动加解锁的功能&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::scoped_lock&lt;/code&gt;：防止多个锁顺序问题导致的死锁问题而出世的一把锁&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Condition Variable&lt;/h3&gt;
&lt;p&gt;有时候线程之间需要某种同步：当某些条件不满足时，停止执行直到该条件被满足。&lt;/p&gt;
&lt;p&gt;这时候需要引入 condition variable —— 状态变量。&lt;/p&gt;
&lt;p&gt;在经典的「生产者消费者模式」下，生产者和消费者就是通过 condition variable 来实现同步的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当有限的生产力无法满足日益增长的消费需求时，消费者进程就会去睡一觉，直到它想要的东西生产出来才醒来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::condition_variable condvar;

consumer:
        std::unique_lock&amp;#x3C;std::mutex&gt; ulock(mutex);
        condvar.wait(ulock, []{ return msgQueue.size() &gt; 0;});

producer:
        condvar.notify_all();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;condition_variable&lt;/code&gt; 需要和 &lt;code&gt;unique_lock&lt;/code&gt; 搭配使用&lt;/li&gt;
&lt;li&gt;在一个线程调用 &lt;code&gt;wait&lt;/code&gt; 之前，它必须持有 &lt;code&gt;unique_lock&lt;/code&gt; 锁&lt;/li&gt;
&lt;li&gt;当  &lt;code&gt;wait&lt;/code&gt;  被调用时，该锁会被释放，线程会陷入沉睡，等待着生产者发过来的唤醒信号&lt;/li&gt;
&lt;li&gt;当生产者调用同一个 &lt;code&gt;condition_variable&lt;/code&gt; 的 &lt;code&gt;notify_all&lt;/code&gt; 方法时，所有沉睡在该变量前的消费者会被唤醒，并尝试重新获取之前释放的 &lt;code&gt;unique_lock&lt;/code&gt;，继续执行下去（注意这里发生了锁争用，只有一个消费者能够获得锁，其他消费者得等待该消费者释放锁）&lt;/li&gt;
&lt;li&gt;如果只想叫醒一个线程，可以用 &lt;code&gt;notify_one&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread&lt;/code&gt; 中也提供了对应的方法，分别是 &lt;code&gt;pthread_cond_wait&lt;/code&gt;, &lt;code&gt;pthread_cond_broadcast&lt;/code&gt;, &lt;code&gt;pthread_cond_signal&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;wait&lt;/code&gt;  可以接受两个参数，此时第二个参数用于判断当前是否要沉睡。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;[]{ return msgQueue.size() &gt; 0;});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相当于&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;while (msgQueue.size() &amp;#x3C;= 0) {
    condvar.wait()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了防止线程无限等待（可能一直没有唤醒），通过 &lt;code&gt;wait_until&lt;/code&gt; 和 &lt;code&gt;wait_for&lt;/code&gt;，你可以设定线程的等待时间。设置 &lt;code&gt;notify_all_at_thread_exit&lt;/code&gt; 也许能帮得上忙。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;pthread&lt;/code&gt; 中，对应的调用是 &lt;code&gt;pthread_cond_timedwait&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;More&lt;/h3&gt;
&lt;p&gt;C++11 的线程库还提供了其他多线程编程的概念，比如 &lt;code&gt;future&lt;/code&gt; 和 &lt;code&gt;atomic&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;future&lt;/h4&gt;
&lt;p&gt;future 位于头文件 &lt;code&gt;&amp;#x3C;future&gt;&lt;/code&gt; 下，包装了未来某个计算结果的期诺。&lt;/p&gt;
&lt;p&gt;当你对所获得的 &lt;code&gt;future&lt;/code&gt; 调用 &lt;code&gt;get&lt;/code&gt; 时，程序会一直阻塞直到 future 的值被计算出来（如果 future 的值已经计算出来了，get 调用会立刻获得返回值），而这一切都是在后台执行的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;chrono&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;future&gt;

using namespace std;

int main()
{
    future&amp;#x3C;int&gt; f1 = async(launch::async, [](){
        std::chrono::milliseconds dura(2000);
        std::this_thread::sleep_for(dura);
        return 0; 
    });
    
    future&amp;#x3C;int&gt; f2 = async(launch::async, [](){
        std::chrono::milliseconds dura(2000);
        std::this_thread::sleep_for(dura);
        return 1; 
    });
    
    cout &amp;#x3C;&amp;#x3C; &quot;Results are: &quot; &amp;#x3C;&amp;#x3C; f1.get() &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; f2.get() &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ g++ -std=c++11 -pthread ./future.cpp

$ time ./a.out 
Results are: 0 1
./a.out  0.00s user 0.00s system 0% cpu 2.012 total # 是两秒左右而不是四秒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了 &lt;code&gt;async&lt;/code&gt;， &lt;code&gt;packaged_task&lt;/code&gt; 和 &lt;code&gt;promise&lt;/code&gt; 也都返回一个 &lt;code&gt;future&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;atomic&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;atomic&lt;/code&gt; 位于头文件 &lt;code&gt;&amp;#x3C;atomic&gt;&lt;/code&gt;  下，实现了类似于 &lt;code&gt;java.util.concurrent.atomic&lt;/code&gt; 的功能。它提供了一组轻量级的、作用在单个变量上的原子操作，是 &lt;code&gt;volatile&lt;/code&gt; 的替代品，有些时候你也可以用它来替换掉 &lt;code&gt;lock&lt;/code&gt;（假如整个 race condition 中只有单个变量）&lt;/p&gt;
&lt;p&gt;下面这个例子解释了什么叫做原子操作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;atomic&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;thread&gt;

using namespace std;

const int NUM = 100;

int target = 0;
atomic&amp;#x3C;int&gt; atomicTarget(0);

template&amp;#x3C;typename T&gt;
void atomicPlusOne(int trys)
{
    while (trys &gt; 0) {
        atomicTarget.fetch_add(1);
        --trys;
    }
}

void plusOne(int trys)
{
    while (trys &gt; 0) {
        ++target;
        --trys;
    }
}

int main()
{
    thread threads[NUM];
    thread atomicThreads[NUM];
    for (int i = 0; i &amp;#x3C; NUM; i++) {
        atomicThreads[i] = thread(atomicPlusOne&amp;#x3C;int&gt;, 10000);
    }
    for (int i = 0; i &amp;#x3C; NUM; i++) {
        threads[i] = thread(plusOne, 10000);
    }

    for (int i = 0; i &amp;#x3C; NUM; i++) {
        atomicThreads[i].join();
    }
    for (int i = 0; i &amp;#x3C; NUM; i++) {
        threads[i].join();
    }

    cout &amp;#x3C;&amp;#x3C; &quot;Atomic target&apos;s value : &quot; &amp;#x3C;&amp;#x3C; atomicTarget &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    cout &amp;#x3C;&amp;#x3C; &quot;Non-atomic target&apos;s value : &quot; &amp;#x3C;&amp;#x3C; target &amp;#x3C;&amp;#x3C; &quot;\n&quot;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# atomicTarget 的值总是固定的，而 target 的值每次运行时各不相同
$ g++ -std=c++11 -pthread ./atom.cpp
Atomic target&apos;s value : 1000000
Non-atomic target&apos;s value : 842480
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pros &amp;#x26; Cons&lt;/h2&gt;
&lt;p&gt;最后总结下 &lt;code&gt;std::thread&lt;/code&gt; 对比于 &lt;code&gt;pthread&lt;/code&gt; 的优缺点：&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;简单，易用&lt;/li&gt;
&lt;li&gt;跨平台，&lt;code&gt;pthread&lt;/code&gt; 只能用在 POSIX 系统上（其他系统有其独立的 thread 实现）&lt;/li&gt;
&lt;li&gt;提供了更多高级功能，比如 &lt;code&gt;future&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;更加 C++（与&lt;strong&gt;匿名函数&lt;/strong&gt;，&lt;code&gt;std::bind&lt;/code&gt;，RAII 等 C++ 特性更好的集成）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;没有 RWlock：有一个类似的 &lt;code&gt;shared_mutex&lt;/code&gt;，不过它属于 C++14，你的编译器很有可能不支持&lt;/li&gt;
&lt;li&gt;操作线程和 Mutex 等的 API 较少：毕竟为了跨平台，只能选取各原生实现的子集。如果你需要设置某些属性，需要通过 API 调用返回原生平台上的对应对象，再对返回的对象进行操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;生产者消费者（pthread &amp;#x26; thread 版本）&lt;/h2&gt;
&lt;p&gt;附上我自己写的，分别用 &lt;code&gt;std::thread&lt;/code&gt; 和 &lt;code&gt;pthread&lt;/code&gt; 实现的多生产者多消费者程序。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意行数上的差距。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;pthread 版本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;pthread.h&gt;
#include &amp;#x3C;queue&gt;
#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;unistd.h&gt;

// 注意 pthread_* 函数返回的异常值，为了简单（偷懒），我没有去处理它们

pthread_mutex_t mutex;
pthread_cond_t condvar;

std::queue&amp;#x3C;int&gt; msgQueue;
struct Produce_range {
    int start;
    int end;
};

void *producer(void *args)
{
    int start = static_cast&amp;#x3C;Produce_range *&gt;(args)-&gt;start;
    int end = static_cast&amp;#x3C;Produce_range *&gt;(args)-&gt;end;
    for (int x = start; x &amp;#x3C; end; x++) {
        usleep(200 * 1000);
        pthread_mutex_lock(&amp;#x26;mutex);
        msgQueue.push(x);
        pthread_mutex_unlock(&amp;#x26;mutex);
        pthread_cond_signal(&amp;#x26;condvar);
        printf(&quot;Produce message %d\n&quot;, x);
    }
    pthread_exit((void *)0);
    return NULL;
}

void *consumer(void *args)
{
    int demand = *static_cast&amp;#x3C;int *&gt;(args);
    while (true) {
        pthread_mutex_lock(&amp;#x26;mutex);
        if (msgQueue.size() &amp;#x3C;= 0) {
            pthread_cond_wait(&amp;#x26;condvar, &amp;#x26;mutex);
        }
        if (msgQueue.size() &gt; 0) {
            printf(&quot;Consume message %d\n&quot;, msgQueue.front());
            msgQueue.pop();
            --demand;
        }
        pthread_mutex_unlock(&amp;#x26;mutex);
        if (!demand) break;
    }
    pthread_exit((void *)0);
    return NULL;
}


int main()
{
    pthread_attr_t attr;
    pthread_attr_init(&amp;#x26;attr);
    pthread_mutex_init(&amp;#x26;mutex, NULL);
    pthread_cond_init(&amp;#x26;condvar, NULL);

    pthread_t producer1, producer2, producer3, consumer1, consumer2;

    Produce_range range1 = {0, 10};
    pthread_create(&amp;#x26;producer1, &amp;#x26;attr, producer, static_cast&amp;#x3C;void *&gt;(&amp;#x26;range1));
    Produce_range range2 = {range1.end, range1.end + 10};
    pthread_create(&amp;#x26;producer2, &amp;#x26;attr, producer, static_cast&amp;#x3C;void *&gt;(&amp;#x26;range2));
    Produce_range range3 = {range2.end, range2.end + 10};
    pthread_create(&amp;#x26;producer3, &amp;#x26;attr, producer, static_cast&amp;#x3C;void *&gt;(&amp;#x26;range3));

    int consume_demand1 = 20;
    int consume_demand2 = 10;
    pthread_create(&amp;#x26;consumer1, &amp;#x26;attr, consumer, static_cast&amp;#x3C;void *&gt;(&amp;#x26;consume_demand1));
    pthread_create(&amp;#x26;consumer2, &amp;#x26;attr, consumer, static_cast&amp;#x3C;void *&gt;(&amp;#x26;consume_demand2));

    pthread_join(producer1, NULL);
    pthread_join(producer2, NULL);
    pthread_join(producer3, NULL);
    pthread_join(consumer1, NULL);
    pthread_join(consumer2, NULL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;std::thread 版本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;chrono&gt;
#include &amp;#x3C;condition_variable&gt;
#include &amp;#x3C;future&gt;
#include &amp;#x3C;mutex&gt;
#include &amp;#x3C;queue&gt;

// 注意某些调用可能会抛出std::system_error， 为了简单（偷懒），我没有去捕获
std::mutex mutex;
std::condition_variable condvar;

std::queue&amp;#x3C;int&gt; msgQueue;

void producer(int start, int end)
{
    for (int x = start; x &amp;#x3C; end; x++) {
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
        {        
            std::lock_guard&amp;#x3C;std::mutex&gt; guard(mutex);
            msgQueue.push(x);
        }
        printf(&quot;Produce message %d\n&quot;, x);
        condvar.notify_all();
    }
}

void consumer(int demand)
{
    while (true) {
        std::unique_lock&amp;#x3C;std::mutex&gt; ulock(mutex);
        condvar.wait(ulock, []{ return msgQueue.size() &gt; 0;});
        // wait的第二个参数使得显式的double check不再必要
        printf(&quot;Consume message %d\n&quot;, msgQueue.front());
        msgQueue.pop();
        --demand;
        if (!demand) break;
    }
}


int main()
{
    std::thread producer1(producer, 0, 10);
    std::thread producer2(producer, 10, 20);
    std::thread producer3(producer, 20, 30);
    std::thread consumer1(consumer, 20);
    std::thread consumer2(consumer, 10);

    producer1.join();
    producer2.join();
    producer3.join();
    consumer1.join();
    consumer2.join();
} 
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>推荐系统架构</title><link>https://coooredump.github.io/blog/ai-infra/recommendation-system-architecture</link><guid isPermaLink="true">https://coooredump.github.io/blog/ai-infra/recommendation-system-architecture</guid><description>基于用户广泛的历史行为，预测用户可能感兴趣的信息主动推送，从而达到用户与信息的匹配，由于用户在推荐之前并不知道内容是什么，这扩大了信息的候选范围，进一步提高了潜在的广告位数量，这成为互联网应用的核心竞争力，这是技术与业务的完美结合。</description><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505142200017.png&quot; alt=&quot;image-20250514220010926&quot;&gt;&lt;/p&gt;
&lt;h2&gt;背景需求&lt;/h2&gt;
&lt;p&gt;互联网的一个主要应用就是&lt;strong&gt;信息匹配&lt;/strong&gt;，只有与人发生关系的信息才具有价值，因此人和信息的匹配是互联网应用的核心问题。&lt;/p&gt;
&lt;p&gt;信息匹配的方式有多种: 门户/订阅/搜索/推荐。&lt;/p&gt;
&lt;p&gt;早期互联网应用以门户展示信息为主，类似专柜陈列的商品，由网站维护人员更新信息，所有人看到的信息都一样。代表就是搜狐/网易/新浪，互联网的技术发展主要推动因素就是计算广告的技术发展，为了提供更多的广告位，让网站能够展示更多的信息，因此由了订阅模式主流是 SRR / 社区等属性，基于社交关注关系，订阅感兴趣的内容主要代表由新浪微博，天涯社区等等，但有很多信息并能通过社交关系获得因此衍生出了搜索需求，用户通过一些 query 来查询到自己想要的信息，这还是信息找人的时代，主要代表就是谷歌/百度/搜狗。 再后来，随着互联网上的信息越来越多，人们陷入了选择困难，人们在阅读信息之前，可能对信息一无所知，只有看到时才知道是否喜欢，人们越来越懒，这就催生出了&lt;strong&gt;推荐系统：基于用户广泛的历史行为，预测用户可能感兴趣的信息主动推送，从而达到用户与信息的匹配&lt;/strong&gt;，由于用户在推荐之前并不知道内容是什么，这扩大了信息的候选范围，进一步提高了潜在的广告位数量，这成为互联网应用的核心竞争力，这是技术与业务的完美结合。&lt;/p&gt;
&lt;h2&gt;约束条件&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;业务目标: &lt;strong&gt;互联网技术本质上是为了用户需求服务的，推荐系统的目标就是增加留存，使用时长等&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;低延迟：&lt;strong&gt;推荐系统作为核心的在线系统，必须要尽可能的降低延迟，因为每降低 20ms 就可以上线一个策略，来优化业务目标&lt;/strong&gt;，同时刷首页信息流的延迟感会直接影响用户体验。&lt;/li&gt;
&lt;li&gt;稳定性:  推荐系统是跟业务目标直接相关的，如果出现问题会直接降低用户留存，使用时长等指标直接影响公司收入，因为通常广告业务需要依赖留存与使用时长进行变现。&lt;/li&gt;
&lt;li&gt;迭代效率 &amp;#x26; 质量：必须做的足够快，推荐系统的迭代速度直接影响公司的竞争能力，同时要对复杂系统保持高迭代效率，势必会导致问题频发，如何保证服务质量将是长久的拉锯战。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成本&lt;/strong&gt;：推荐系统需要大量的存储/计算/网络成本，如何优化庞大的机器资源成本，这将直接创造收入。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;技术方案&lt;/h2&gt;
&lt;p&gt;如果你要给一个朋友推荐一本书你需要怎么做？&lt;/p&gt;
&lt;p&gt;第一步: 你要自己读过很多书，这样朋友问你的时候你才能&lt;strong&gt;及时&lt;/strong&gt;的推荐给朋友&lt;/p&gt;
&lt;p&gt;第二步: 你应该尽可能的&lt;strong&gt;了解这个朋友&lt;/strong&gt;（你对他的印象/他现在的状态/他想看什么书）&lt;/p&gt;
&lt;p&gt;第三步: 基于你对这个&lt;strong&gt;朋友的了解以及对你读过的书的了解&lt;/strong&gt;，潜意识快速筛选出来几十本可以推荐的书&lt;/p&gt;
&lt;p&gt;第四步: 然后快速的对这几十本书进行&lt;strong&gt;猜测&lt;/strong&gt;，你这位朋友喜欢这几本书的概率有多大？然后按概率排个序&lt;/p&gt;
&lt;p&gt;第五步: 取 &lt;code&gt;topK&lt;/code&gt; 结果推荐，基于朋友的&lt;strong&gt;反馈&lt;/strong&gt;，你&lt;strong&gt;修正&lt;/strong&gt;了对朋友兴趣的理解，你的猜测将变得更加准确&lt;/p&gt;
&lt;p&gt;第六步: 如果你觉得朋友适合看你刚写的书，那么你会在&lt;strong&gt;推荐中夹带私货&lt;/strong&gt;，强烈安利自己的书（&lt;strong&gt;广告&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;第七步: 再次推荐书籍时可能会重复推荐，当你发现脑子想到了之前推荐过的书时，你要把他&lt;strong&gt;忽略&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;这个信息匹配的本质就是推荐的基本原理&lt;/em&gt;。将人的兴趣量化，再将要推荐的物品特征进行量化，将两个量化值进行计算得到一个分值，然后对所有的计算结果取 &lt;code&gt;topK&lt;/code&gt;，返回 &lt;code&gt;topK&lt;/code&gt; 的结果，这一过程就是推荐的基本原理。为了实现这一基本步骤，我们发明了多种推荐算法，关于推荐算法的介绍可以参考: &lt;a href=&quot;https://www.bilibili.com/video/BV1PS4y1A7za/?spm_id_from=333.337.search-card.all.click&amp;#x26;vd_source=442f1caea55e6e096da17e8366c2c513&quot;&gt;推荐系统公开课&lt;/a&gt;，接下来的内容将假设你已经对基本的推荐算法有所了解。&lt;/p&gt;
&lt;p&gt;根据对推荐算法的分类以及基本原理的理解，我们可以对推荐系统按数据流的构建过程划分为多个模块：&lt;strong&gt;候选构建/特征工程/召回系统/排序系统/模型训练/混排系统/消重系统&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;基本概念&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐阅读：&lt;a href=&quot;https://www.bilibili.com/video/BV1PS4y1A7za/?spm_id_from=333.337.search-card.all.click&amp;#x26;vd_source=442f1caea55e6e096da17e8366c2c513&quot;&gt;推荐系统的基础概念&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;item&lt;/strong&gt;：代表信息的容器，可以是一个视频/文章/商品/广告等等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消费&lt;/strong&gt;：用户浏览 item 所承载的信息，然后发生一系列行为，播放/点赞/收藏/关注/转发/购买等等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分发&lt;/strong&gt;：决定将哪些 item 与用户匹配，展示给用户进行消费的过程就是分发过程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打包&lt;/strong&gt;：推荐系统决定分发哪些 item 给到用户，但是推荐系统不关注 item 承载哪些信息，他只关注 item 具有的特征，因此打包就是将用户能够浏览的信息拼接封装到 item 的这个容器中展示给用户的过程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;user&lt;/strong&gt;：消费或者生产 item 的用户&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;候选&lt;/strong&gt;：准备推荐给用户消费的 item 数据集合&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预估&lt;/strong&gt;：深度学习模型的前向传播&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;召回&lt;/strong&gt;：信息检索的一个过程，通过一个 key 获得一堆相关的 id&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排序&lt;/strong&gt;：对召回的 id，按照某种分值进行排序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据流&lt;/strong&gt;：数据整个生命周期的处理过程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特征&lt;/strong&gt;：物理上客观事物所蕴含信息的数学表达&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;样本&lt;/strong&gt;：用于机器学习模型训练的数据特征&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;候选构建&lt;/h2&gt;
&lt;p&gt;当文章/视频 (item) 内容发布后，经过一些审核与处理后需要将 item 存储为易于推荐系统查询的格式，通常就是正排+倒排，正排就是以 itemID 为 key，然后一些 item 相关的结构化属性进行序列化后，存储好的 kv 数据项，而倒排就是以某个属性或者计算的 tag 为 key，value 是一个 timeID 的 list，这两种数据结构覆盖了基本的查询需求，用来为推荐引擎的候选构建提供基础的数据支撑。&lt;/p&gt;
&lt;p&gt;所以候选集合本质上就是一个易于推荐引擎查询数据的索引结构，是为推荐提供数据的模块。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505142146603.png&quot; alt=&quot;image-20250514214645525&quot;&gt;&lt;/p&gt;
&lt;h2&gt;特征工程&lt;/h2&gt;
&lt;p&gt;内容候选实在太多，将所有的 item 送入模型进行预估计算量非常大，不可能在秒级返回计算结果，这就要求必须在所有的内容候选中选择最有可能排序在前面的 item，返回一个 top k 的结果，这种大规模的筛选与搜索引擎非常相似，但搜索引擎基于 pagerank 计算的相关性，而&lt;strong&gt;推荐召回则基于用户的兴趣与 item 特征之间的相关性，通常通过向量检索，余弦相似度计算两个向量之间的距离来衡量二者的相似程度&lt;/strong&gt;（&lt;a href=&quot;https://nxwz51a5wp.feishu.cn/docs/doccnsvpGMaaLfmOu1LAmorDH4b&quot;&gt;分布式向量检索引擎&lt;/a&gt;），但真实的召回系统通常要有十几种召回策略，这些召回策略会并行使用，然后将召回结果统一合并。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505142148228.png&quot; alt=&quot;image-20250514214835153&quot;&gt;&lt;/p&gt;
&lt;h2&gt;排序系统&lt;/h2&gt;
&lt;p&gt;训练好的模型，要在线被服务所请求对外提供服务，用户每次主动请求首页的信息流，将携带这个用户的特征信息与返回的一堆 item 的特征，将这些特征信息组成模型的输入，然后经过计算图的计算，输入的特征值与权重参数计算得到一个分值，该分值表示用户如果观看了这个内容将使得损失函数最小化，损失函数被表示为距离业务目标的偏差（使用时长），反过来说就是使得使用时长得到增长。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hardcore.feishu.cn/docs/doccnEvEYcExjlf1DrTGlchedab&quot;&gt;分布式机器学习系统: Parameter Server 设计&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505142150995.png&quot; alt=&quot;image-20250514215021935&quot;&gt;&lt;/p&gt;
&lt;p&gt;为了增强效果，一次召回通常会返回上千条内容，如果直接放在模型中进行预估，同样延迟会超过 1s，不可接受，&lt;strong&gt;为此将排序阶段分为粗排和精排两部分&lt;/strong&gt;，利用一些简单的策略进行快速打分，将上千条内容过滤掉为几百条，粗排的目标是选择最有可能在排序中排在前面的内容，排序部分通常被称之为精排，为了与粗排进行区分，几百条内容通常在过精排的复杂模型时能够保障在毫秒时间内返回，从而在工程实现与业务目标之间取得权衡。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505142151495.png&quot; alt=&quot;image-20250514215104427&quot;&gt;&lt;/p&gt;
&lt;h2&gt;模型训练&lt;/h2&gt;
&lt;p&gt;机器学习模型本质上就是&lt;strong&gt;一堆参数 + 计算图&lt;/strong&gt;所组成的数据结构，所谓训练就是先给这些参数一个初始化的值，然后通过输入训练样本，反向传播来更新这些参数值，&lt;strong&gt;使其通过计算图可以得到一个使得损失函数最小化的结果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505142152095.png&quot; alt=&quot;image-20250514215237023&quot;&gt;&lt;/p&gt;
&lt;h2&gt;重排（混排）系统&lt;/h2&gt;
&lt;p&gt;推荐系统需要考虑多样化需求且要满足一定的运营能力，即&lt;strong&gt;同一个作者的内容不能集中推荐&lt;/strong&gt;，这需要一套复杂的规则系统，对推荐系统返回的 item list 整体负责，对于多样性问题，需要考虑一刷内 item 之间的相互影响，相同作者/相似内容等等，为了降低推荐 item 的相似性，需要有一定的打散模型，能够在精排选出的几百条内容中，考虑一次请求整体列表的特征得到整体收益最大的一个排列组合，以便于提高业务目标。&lt;/p&gt;
&lt;p&gt;具体的做法，就是使用 dfs 对 300 条内容进行检索，组合的多种排列形式当作候选，通过规则系统和精排打分作为剪枝依据，加速计算过程，最终整体作为输入，过重排模型，得到对排序 list 的打分，选择打分最高的一组 list，进行返回。&lt;/p&gt;
&lt;p&gt;当精排返回 top500 条内容后，根据客户端请求的条数进行截断，返回 8-16 条 top 内容，然后在其中插入广告，请求广告系统获取 N 个广告，广告展示在哪个位置上能够使得 ecpm 和用户体验达到最佳状态? 即: 让用户看到最贵的广告，同时要保证用户不那么反感。这是一个高价值的竞拍问题，将广告和 item 使用 dfs 深度遍历组合每一种情况，将这些情况交给算法模型进行评估，打分，选择整体分值最高的排序方式，最终将其放出这一过程就是混排。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505142156525.png&quot; alt=&quot;image-20250514215612459&quot;&gt;&lt;/p&gt;
&lt;h2&gt;消重系统&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;用户在一段时间内不能看到相同的内容&lt;/strong&gt;，这是没有意义的是影响用户体验的，为此我们需要避免同一用户在一段时间内，多次请求到相同内容，之前的召回系统每次召回的内容在一段时间内有可能是相同的，&lt;strong&gt;因为向量检索的更新难以做到实时性&lt;/strong&gt;。这是不符合预期的，所以需要有一个系统记录当前用户历史上浏览过了哪些内容，并在召回后将其过滤掉，这样就能保障送入排序阶段的内容都是用户没有流量过的新内容，消重系统应该尽量在接近用户侧写入，接近召回侧过滤。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505142157952.png&quot; alt=&quot;image-20250514215711871&quot;&gt;&lt;/p&gt;
&lt;h2&gt;整合架构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505142200017.png&quot; alt=&quot;image-20250514220010926&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;场景信息包括: 设备信息/当前时间/用户上一刷点赞的 item/用户最近下载了啥 app 等等信息&lt;/li&gt;
&lt;li&gt;从第二步开始真正的进入推荐引擎，重排 server 将作为信息流广告和推荐业务的入口服务&lt;/li&gt;
&lt;li&gt;广告是另一个复杂的系统，基于用户的特征，预测对哪些广告更加感兴趣，能够促进转化，此阶段也将真正开始请求推荐系统&lt;/li&gt;
&lt;li&gt;向量召回输入的是 user 的 embedding 信息，该 embedding 是 user feature server 实时计算的，其返回的是 item id 以及 embedding, 是一个双塔模型，除了主要的向量召回，也会有一些策略召回作为对向量召回的补充，输入的是聚类 id 输出的是 id list&lt;/li&gt;
&lt;li&gt;通过 item id 获取正排信息，这里会大量查询本地缓存，这些 item 的属性信息用于后续的过滤打散&lt;/li&gt;
&lt;li&gt;多个召回通道返回的内容需要合并在一起，通常采用蛇形 merge 的方式合并，然后执行一些过滤规则，例如摄政类/曾经刷过的内容/关键词屏蔽/作者屏蔽/版权屏蔽等扽过滤规则，确保放出的 id 有效性较高&lt;/li&gt;
&lt;li&gt;粗排阶段，通过 uid embedding 和 几千个 item 的 embedding，进行快速的排序预测，通常是一个简单的 ctr 模型，主要是计算快速，在几十毫秒内能够计算出几千个 item 的得分，粗排的目标是尽可能的将高价值的 item 排在前面。&lt;/li&gt;
&lt;li&gt;精排阶段，主要对推荐效果负责，将以复杂的算法模型作出准确的预估，通常消耗几百毫秒，是推荐最耗时消耗计算资源最多的地方，输入进去的关于 user 和 item 的 embedding 信息，作为模型的输入参数，精排的模型是多目标模型，会有很多输出，每个输出代表一个预测分数，例如转发的概率，点赞的概率，完播的概率等等，每个输出分数对应一个模型，都会讲输入参数入图计算，进入 tf 的 serving 中，根据图计算的 DAG 配置，从配置中确定权重的 id，通常叫做 feature id，根据这个 id 去 ps 中查询具体的 embedding 数值，然后在 worker server 中进行权重的计算，完成前向传播过程，完成计算过程，得到分数，然后精排服务还会对所有的分数过一个融合公式得到一个最终分，该融合公式是认为确定的，通常就是加权平均，根据业务目标调整分数权重即可，最终该融合公式的得分即位排序分&lt;/li&gt;
&lt;li&gt;精排返回结果后，执行控制流回到重排服务，将 item id 和 ad id 输入规则系统，进行 dfs 检索，选择符合规则策略并取得分最高的一列组合返回，此时决定了最终呈现给用户的 item id 的顺序以及内容&lt;/li&gt;
&lt;li&gt;此阶段会异步的返回 ack 请求回调精排服务，告诉精排服务哪些 item id 被正式选中进行曝光，此时精排服务会真正的发送 stream feature 用于进行实时的进行训练样本的拼接&lt;/li&gt;
&lt;li&gt;此时 item id + ad id 的 list 返回到了 feed server，feed server 将 id list 记录到历史消重服务中，该服务是为了记录用户已经看过哪些内容，用于在召回之后将其过滤的操作，同时作为曝光日志（用户看了哪些内容），也会作为训练样本拼接的数据流之一，通常为了覆盖足够的业务逻辑与场景，越接近客户侧的写消重操作效果越好，因为这样将约接近客户真实的曝光行为，越在接近召回的地方做过滤效果越好，因为这样将节约计算资源，确保之后执行的操作都是对可放出的 id 进行的。但过于接近客户侧的写消重，例如由客户端上报写消重，将会因为客户端跨公网传输数据，延迟较高无法实时记录历史数据而不得不放弃&lt;/li&gt;
&lt;li&gt;feed server 主要的作用就是根据 id 以及 id 的类型进行业务打包，也就是根据 id 点查该 id 所对应的内容数据，比如 id 的类型是小视频，则就通过 id 去查询小视频服务返回小视频的播放地址，点赞数/评论数/作者头像等等用于 feed 流展示的数据，如果 id 类型是一个商品，则就去商品服务打包商品的封面图，价格，销量等数据信息，如果是一个广告，则去广告的打包服务查询，总之这一步将根据 id 查询具体体裁内容的展示信息&lt;/li&gt;
&lt;li&gt;用户真正的看到了 feed 信息，然后根据自己的喜好表达一些消费行为，转发/点赞/评论/停留播放等行为，这些行为会被客户端上报给服务端&lt;/li&gt;
&lt;li&gt;服务端会对数据进行检查，然后对错误的数据进行剔除，加工转换后用于拼接训练样本，训练样本会进入 joiner server，该 server 将 精排服务上报的 关于 item 的特征信息 (embedding)，以及服务端上报的曝光 item id 信息缓存在一个大的 cache 中，缓存 1 小时，客户端上报数据代表着用户对 item 的行为，该数据如果在 1 小时内被回传则说明是正例，将拼接出一个正例样本，如果 1 小时内没有被回传则被认为是负例，为保证正负例一样多，才能保证训练模型不会过拟合，通常会对负例进行采样，丢弃一部分负例，这样保证正负例一样多，进行模型的实时训练，这部分数据就会进入 MLOPS 平台，训练模型参数&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>二叉树匹配问题｜子树匹配？子结构匹配？</title><link>https://coooredump.github.io/blog/leetcode/binary-tree-matching</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/binary-tree-matching</guid><description>匹配类二叉树可以使用一种套路相对固定的递归函数，这类题目与字符串匹配有些神似</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;✅ 二叉树匹配类题目总结&lt;/h2&gt;
&lt;p&gt;匹配类二叉树可以使用一种套路相对固定的递归函数，这类题目与字符串匹配有些神似，求解过程大致分为两步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先将根节点匹配；&lt;/li&gt;
&lt;li&gt;根节点匹配后，对子树进行匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相关例题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/same-tree/description/&quot;&gt;100. 相同的树&lt;/a&gt;（即 &lt;code&gt;check&lt;/code&gt; 函数本身）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/symmetric-tree/description/&quot;&gt;101. 对称二叉树&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/linked-list-in-binary-tree/description/&quot;&gt;1367. 二叉树中的链表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/subtree-of-another-tree/description/&quot;&gt;572. 另一棵树的子树&lt;/a&gt; &amp;#x26; &lt;a href=&quot;https://leetcode.cn/problems/check-subtree-lcci/description/&quot;&gt;面试题 04.10. 检查子树&lt;/a&gt;（匹配子树）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/description/&quot;&gt;LCR 143. 子结构判断&lt;/a&gt;（匹配子结构）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/same-tree/&quot;&gt;100. 相同的树&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280022251.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    bool isSameTree(TreeNode* p, TreeNode* q) {
        if (!p || !q)
            return p == q;
        return p-&gt;val == q-&gt;val &amp;#x26;&amp;#x26; isSameTree(p-&gt;left, q-&gt;left) &amp;#x26;&amp;#x26; isSameTree(p-&gt;right, q-&gt;right);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/symmetric-tree/&quot;&gt;101. 对称二叉树&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280022912.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    bool check(TreeNode* a, TreeNode* b) {
        if (a == nullptr &amp;#x26;&amp;#x26; b == nullptr)
            return true;
        if (a == nullptr || b == nullptr)
            return false;
        return a-&gt;val == b-&gt;val &amp;#x26;&amp;#x26; check(a-&gt;left, b-&gt;right) &amp;#x26;&amp;#x26; check(a-&gt;right, b-&gt;left);
    }

    bool isSymmetric(TreeNode* root) {
        return check(root-&gt;left, root-&gt;right);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/linked-list-in-binary-tree/&quot;&gt;1367. 二叉树中的链表&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;题意：「链表」在「二叉树」中的匹配&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280024859.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：head = [4,2,8], root = [1,4,4,null,2,2,null,1,null,6,8,null,null,null,null,1,3]
输出：true
解释：树中蓝色的节点构成了与链表对应的子路径。
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    bool check(ListNode* head, TreeNode* root) {
        if (head == nullptr)
            return true;
        if (root == nullptr)
            return false;
        return head-&gt;val == root-&gt;val &amp;#x26;&amp;#x26; (check(head-&gt;next, root-&gt;left) || check(head-&gt;next, root-&gt;right));
    }

    bool isSubPath(ListNode* head, TreeNode* root) {
        if (root == nullptr)
            return false;
        return check(head, root) || isSubPath(head, root-&gt;left) || isSubPath(head, root-&gt;right);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/subtree-of-another-tree/&quot;&gt;572. 另一棵树的子树&lt;/a&gt; &amp;#x26; &lt;a href=&quot;https://leetcode.cn/problems/check-subtree-lcci/description/&quot;&gt;面试题 04.10. 检查子树&lt;/a&gt;（匹配子树）&lt;/h2&gt;
&lt;p&gt;这道题的题意是这样的：输入两棵二叉树 A 和 B，判断 B 是不是 A 的子结构，且约定空树不是任意一个树的子结构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280016742.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;比如上面这个例子，我们发现 B 是 A 的子结构，因为它们的结构相同，且节点值相等。&lt;/p&gt;
&lt;p&gt;求解思路可以分解为以下两步：&lt;/p&gt;
&lt;p&gt;匹配根节点：首先在 A 中找到与 B 的根节点匹配的节点 C；&lt;/p&gt;
&lt;p&gt;匹配其他节点：验证 C 的子树与 B 的子树是否匹配。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    bool check(TreeNode* a, TreeNode* b) {
        // 以下四行代码也可以改成: if (a == nullptr || b == nullptr) { return a == b; }
        if (a == nullptr &amp;#x26;&amp;#x26; b == nullptr)
            return true;
        if (a == nullptr || b == nullptr)
            return false;
        return a-&gt;val == b-&gt;val &amp;#x26;&amp;#x26; check(a-&gt;left, b-&gt;left) &amp;#x26;&amp;#x26; check(a-&gt;right, b-&gt;right);
    }

    bool checkSubTree(TreeNode* t1, TreeNode* t2) {
        if (t1 == nullptr || t2 == nullptr)
            return false;
        return check(t1, t2) || checkSubTree(t1-&gt;left, t2) || checkSubTree(t1-&gt;right, t2);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/description/&quot;&gt;LCR 143. 子结构判断&lt;/a&gt;（匹配子结构）&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280019118.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：tree1 = [3,6,7,1,8], tree2 = [6,1]
输出：true
解释：tree2 与 tree1 的一个子树拥有相同的结构和节点值。即 6 - &gt; 1。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于本题来讲，与「面试题 04.10. 检查子树」很像，不同的是 B 属于 A 的一部分也可以，没必要一直匹配到叶子节点，因此只需对 &lt;code&gt;check&lt;/code&gt; 函树的基本条件进行修改即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    bool check(TreeNode* a, TreeNode* b) {
        if (b == nullptr)
            return true;
        if (a == nullptr)
            return false;
        return a-&gt;val == b-&gt;val &amp;#x26;&amp;#x26; check(a-&gt;left, b-&gt;left) &amp;#x26;&amp;#x26; check(a-&gt;right, b-&gt;right);
    }

    bool isSubStructure(TreeNode* t1, TreeNode* t2) {
        if (t1 == nullptr || t2 == nullptr)
            return false;
        return check(t1, t2) || isSubStructure(t1-&gt;left, t2) || isSubStructure(t1-&gt;right, t2);
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>根据（前中后序）构造二叉树系列</title><link>https://coooredump.github.io/blog/leetcode/constructing-binary-tree</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/constructing-binary-tree</guid><description>根据前序和后序遍历构造二叉树、从前序与中序遍历序列构造二叉树、从中序与后序遍历序列构造二叉树、前序遍历构造二叉搜索树</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;✅ 构造二叉树系列&lt;/h2&gt;
&lt;p&gt;相关例题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal/description/&quot;&gt;889. 根据前序和后序遍历构造二叉树&lt;/a&gt;（不唯一）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/&quot;&gt;105. 从前序与中序遍历序列构造二叉树&lt;/a&gt;（唯一）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/description/&quot;&gt;106. 从中序与后序遍历序列构造二叉树&lt;/a&gt;（唯一）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-search-tree-from-preorder-traversal/description/&quot;&gt;1008. 前序遍历构造二叉搜索树&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-search-tree-from-preorder-traversal/&quot;&gt;1008. 前序遍历构造二叉搜索树&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;题意：根据前序遍历结果构造二叉搜索树&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280108994.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：preorder = [8,5,1,7,10,12]
输出：[8,5,10,1,7,null,12]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ buildTree · 递归&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    TreeNode* buildTree(vector&amp;#x3C;int&gt;&amp;#x26; preorder, int left, int right) {
        if (left &gt; right) {
            return nullptr;
        }
        int i;
        for (i = left + 1; i &amp;#x3C;= right; i++) {
            if (preorder[i] &gt; preorder[left])
                break;
        }
        TreeNode* _left = buildTree(preorder, left + 1, i - 1);
        TreeNode* _right = buildTree(preorder, i, right);
        return new TreeNode(preorder[left], _left, _right);
    }

    TreeNode* bstFromPreorder(vector&amp;#x3C;int&gt;&amp;#x26; preorder) {
        if (preorder.empty())
            return nullptr;
        return buildTree(preorder, 0, preorder.size() - 1);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    TreeNode* bstFromPreorder(vector&amp;#x3C;int&gt;&amp;#x26; preorder) {
        if (preorder.empty())
            return nullptr;
        int i = upper_bound(preorder.begin() + 1, preorder.end(), preorder[0]) - preorder.begin();
        vector&amp;#x3C;int&gt; preleft(preorder.begin() + 1, preorder.begin() + i);
        vector&amp;#x3C;int&gt; preright(preorder.begin() + i, preorder.end());
        TreeNode* left = bstFromPreorder(preleft);
        TreeNode* right = bstFromPreorder(preright);
        return new TreeNode(preorder[0], left, right);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/&quot;&gt;106. 从中序与后序遍历序列构造二叉树&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280110031.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出：[3,9,20,null,null,15,7]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 递归&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280111507.png&quot; alt=&quot;LC106-c.png&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    TreeNode* buildTree(vector&amp;#x3C;int&gt;&amp;#x26; inorder, vector&amp;#x3C;int&gt;&amp;#x26; postorder) {
        if (inorder.empty())
            return nullptr;
        int i = ranges::find(inorder, postorder.back()) - inorder.begin();
        vector&amp;#x3C;int&gt; in1(inorder.begin(), inorder.begin() + i);
        vector&amp;#x3C;int&gt; in2(inorder.begin() + i + 1, inorder.end());
        vector&amp;#x3C;int&gt; post1(postorder.begin(), postorder.begin() + i);
        vector&amp;#x3C;int&gt; post2(postorder.begin() + i, postorder.end() - 1);
        TreeNode* left = buildTree(in1, post1);
        TreeNode* right = buildTree(in2, post2);
        return new TreeNode(postorder.back(), left, right);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/&quot;&gt;105. 从前序与中序遍历序列构造二叉树&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280112198.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 递归&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280113037.png&quot; alt=&quot;lc105-c.png&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    TreeNode* buildTree(vector&amp;#x3C;int&gt;&amp;#x26; preorder, vector&amp;#x3C;int&gt;&amp;#x26; inorder) {
        if (preorder.empty())
            return nullptr;
        int i = ranges::find(inorder, preorder[0]) - inorder.begin();
        vector&amp;#x3C;int&gt; pre1(preorder.begin() + 1, preorder.begin() + i + 1);
        vector&amp;#x3C;int&gt; pre2(preorder.begin() + i + 1, preorder.end());
        vector&amp;#x3C;int&gt; in1(inorder.begin(), inorder.begin() + i);
        vector&amp;#x3C;int&gt; in2(inorder.begin() + i + 1, inorder.end());
        TreeNode* left = buildTree(pre1, in1);
        TreeNode* right = buildTree(pre2, in2);
        return new TreeNode(preorder[0], left, right);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal/&quot;&gt;889. 根据前序和后序遍历构造二叉树&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;如果存在多个答案，您可以返回其中 &lt;strong&gt;任何&lt;/strong&gt; 一个。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280116935.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：preorder = [1,2,4,5,3,6,7], postorder = [4,5,2,6,7,3,1]
输出：[1,2,3,4,5,6,7]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 递归&lt;/h3&gt;
&lt;p&gt;首先说明，如果只知道前序遍历和后序遍历，这棵二叉树不一定是唯一的，如下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280116261.png&quot; alt=&quot;lc889-1.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;题目说，如果存在多个答案，我们可以返回其中任何一个。那么不妨&lt;strong&gt;规定&lt;/strong&gt;：无论什么情况，在前序遍历中，&lt;em&gt;preorder&lt;/em&gt;[1] 都是&lt;strong&gt;左子树&lt;/strong&gt;的根节点值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280116240.png&quot; alt=&quot;lc889-2-c.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;递归边界&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;em&gt;preorder&lt;/em&gt; 的长度是 0，对应着空节点，返回空。&lt;/li&gt;
&lt;li&gt;如果 &lt;em&gt;preorder&lt;/em&gt; 的长度是 1，对应着二叉树的叶子，创建一个叶子节点并返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    TreeNode* constructFromPrePost(vector&amp;#x3C;int&gt;&amp;#x26; preorder, vector&amp;#x3C;int&gt;&amp;#x26; postorder) {
        if (preorder.empty()) {
            return nullptr;
        }
        if (preorder.size() == 1) {
            return new TreeNode(preorder[0]);
        }
        int left_size = find(postorder.begin(), postorder.end(), preorder[1]) - postorder.begin() + 1;
        vector&amp;#x3C;int&gt; pre1(preorder.begin() + 1, preorder.begin() + left_size + 1);
        vector&amp;#x3C;int&gt; pre2(preorder.begin() + left_size + 1, preorder.end());
        vector&amp;#x3C;int&gt; post1(postorder.begin(), postorder.begin() + left_size);
        vector&amp;#x3C;int&gt; post2(postorder.begin() + left_size, postorder.end() - 1);
        TreeNode* root = new TreeNode(preorder[0]);
        root-&gt;left = constructFromPrePost(pre1, post1);
        root-&gt;right = constructFromPrePost(pre2, post2);
        return root;
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>差分数组</title><link>https://coooredump.github.io/blog/leetcode/difference-array</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/difference-array</guid><description>Difference Array</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;✅ 差分数组&lt;/h2&gt;
&lt;p&gt;相关例题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/points-that-intersect-with-cars/&quot;&gt;2848. 与车相交的点&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/car-pooling/&quot;&gt;1094. 拼车&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/corporate-flight-bookings/&quot;&gt;1109. 航班预订统计&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/divide-intervals-into-minimum-number-of-groups/&quot;&gt;2406. 将区间分为最少组数&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/shifting-letters-ii/&quot;&gt;2381. 字母移位 II&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/apply-operations-to-make-all-array-elements-equal-to-zero/&quot;&gt;2772. 使数组中的所有元素都等于零&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximize-the-minimum-powered-city/&quot;&gt;2528. 最大化城市的最小电量&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;难点&lt;/strong&gt;：如何快速地「把区间内的数都加一」呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280001866.png&quot; alt=&quot;LC2132-c.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/points-that-intersect-with-cars/&quot;&gt;2848. 与车相交的点&lt;/a&gt;（一维差分数组）&lt;/h2&gt;
&lt;p&gt;给你一个下标从 &lt;strong&gt;0&lt;/strong&gt; 开始的二维整数数组 &lt;code&gt;nums&lt;/code&gt; 表示汽车停放在数轴上的坐标。对于任意下标 &lt;code&gt;i&lt;/code&gt;，&lt;code&gt;nums[i] = [starti, endi]&lt;/code&gt; ，其中 &lt;code&gt;starti&lt;/code&gt; 是第 &lt;code&gt;i&lt;/code&gt; 辆车的起点，&lt;code&gt;endi&lt;/code&gt; 是第 &lt;code&gt;i&lt;/code&gt; 辆车的终点。&lt;/p&gt;
&lt;p&gt;返回数轴上被车 &lt;strong&gt;任意部分&lt;/strong&gt; 覆盖的整数点的数目。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：nums = [[3,6],[1,5],[4,7]]
输出：7
解释：从 1 到 7 的所有点都至少与一辆车相交，因此答案为 7 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：nums = [[1,3],[5,8]]
输出：7
解释：1、2、3、5、6、7、8 共计 7 个点满足至少与一辆车相交，因此答案为 7 。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 差分数组&lt;/h3&gt;
&lt;p&gt;核心思路：计算每个点被覆盖了多少次。统计覆盖次数大于 0 的点，即为答案。&lt;/p&gt;
&lt;p&gt;假设一开始有一个全为 0 的数组 a，用来保存每个点被覆盖了多少次。&lt;/p&gt;
&lt;p&gt;对于示例 1，我们可以把 a 中下标在 [3,6] 的元素都加一，下标在 [1,5] 的元素都加一，下标在 [4,7] 的元素都加一。&lt;/p&gt;
&lt;p&gt;然后，统计 &lt;code&gt;a[i] &gt; 0&lt;/code&gt; 的个数，即为答案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何快速地「把区间内的数都加一」呢&lt;/strong&gt;？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这可以用差分数组实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int numberOfPoints(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; nums) {
        int max_end = ranges::max(nums, {}, [](const auto&amp;#x26; a) { return a[1]; })[1];
        vector&amp;#x3C;int&gt; diff(max_end + 2);
		// 首尾两点做标记
        for (auto&amp;#x26; interval : nums) {
            diff[interval[0]]++;
            diff[interval[1] + 1]--;
        }
        // s += d
        int s = 0, ans = 0;
        for (int d : diff) {
            s += d;
            ans += s &gt; 0;
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/car-pooling/&quot;&gt;1094. 拼车&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;车上最初有 &lt;code&gt;capacity&lt;/code&gt; 个空座位。车 &lt;strong&gt;只能&lt;/strong&gt; 向一个方向行驶（也就是说，&lt;strong&gt;不允许掉头或改变方向&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;给定整数 &lt;code&gt;capacity&lt;/code&gt; 和一个数组 &lt;code&gt;trips&lt;/code&gt; ,  &lt;code&gt;trip[i] = [numPassengersi, fromi, toi]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 次旅行有 &lt;code&gt;numPassengersi&lt;/code&gt; 乘客，接他们和放他们的位置分别是 &lt;code&gt;fromi&lt;/code&gt; 和 &lt;code&gt;toi&lt;/code&gt; 。这些位置是从汽车的初始位置向东的公里数。&lt;/p&gt;
&lt;p&gt;当且仅当你可以在所有给定的行程中接送所有乘客时，返回 &lt;code&gt;true&lt;/code&gt;，否则请返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：trips = [[2,1,5],[3,3,7]], capacity = 4
输出：false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：trips = [[2,1,5],[3,3,7]], capacity = 5
输出：true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 差分数组&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    bool carPooling(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; trips, int capacity) {
        // ranges::max(list, comp, proj)
        int max_end = ranges::max(trips, {}, [](const auto&amp;#x26; a) { return a[2]; })[2];
        vector&amp;#x3C;int&gt; diff(max_end + 1);
        for (auto&amp;#x26; trip : trips) {
            int num = trip[0], from = trip[1], to = trip[2];
            diff[from] += num;
            // 不一定要 +1，根据题目情况而变
            diff[to] -= num;
        }
        int s = 0;
        for (int d : diff) {
            s += d;
            if (s &gt; capacity)
                return false;
        }
        return true;
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>高频「大数相加」面试题</title><link>https://coooredump.github.io/blog/leetcode/interview-add-two</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/interview-add-two</guid><description>面试经常考察的思维大数相加题型</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一种经常考察的思维：&lt;strong&gt;大数相加&lt;/strong&gt;。一般有以下几种数据结构类型的考察方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组：&lt;a href=&quot;https://leetcode.cn/problems/plus-one/description/&quot;&gt;66. 加一&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;字符串：&lt;a href=&quot;https://leetcode.cn/problems/add-strings/description/&quot;&gt;415. 字符串相加&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;链表：
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/add-two-numbers/description/&quot;&gt;2. 两数相加&lt;/a&gt;｜顺序➕&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/add-two-numbers-ii/&quot;&gt;445. 两数相加 II&lt;/a&gt;｜逆序➕（腾讯 CDG 一面）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;二进制：&lt;a href=&quot;https://leetcode.cn/problems/add-binary/description/&quot;&gt;67. 二进制求和&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;大数相乘：&lt;a href=&quot;https://leetcode.cn/problems/multiply-strings/&quot;&gt;43. 字符串相乘&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/plus-one/&quot;&gt;66. 加一&lt;/a&gt;｜数组版&lt;/h2&gt;
&lt;p&gt;给定一个由 &lt;strong&gt;整数&lt;/strong&gt; 组成的 &lt;strong&gt;非空&lt;/strong&gt; 数组所表示的非负整数，在该数的基础上加一。&lt;/p&gt;
&lt;p&gt;最高位数字存放在数组的首位， 数组中每个元素只存储&lt;strong&gt;单个&lt;/strong&gt;数字。&lt;/p&gt;
&lt;p&gt;你可以假设除了整数 0 之外，这个整数不会以零开头。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：digits = [1,2,3]
输出：[1,2,4]
解释：输入数组表示数字 123。
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    vector&amp;#x3C;int&gt; plusOne(vector&amp;#x3C;int&gt;&amp;#x26; digits) {
        int n = digits.size(), add = 1;
        for (int i = n - 1; i &gt;= 0 &amp;#x26;&amp;#x26; add; i--) {
            int res = digits[i] + 1;
            digits[i] = res % 10;
            add = res / 10;
        }
        if (add) {
            digits.insert(digits.begin(), 1);
        }
        return digits;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/add-strings/&quot;&gt;415. 字符串相加&lt;/a&gt;｜字符串版&lt;/h2&gt;
&lt;p&gt;给定两个字符串形式的非负整数 &lt;code&gt;num1&lt;/code&gt; 和&lt;code&gt;num2&lt;/code&gt; ，计算它们的和并同样以字符串形式返回。&lt;/p&gt;
&lt;p&gt;你不能使用任何內建的用于处理大整数的库（比如 &lt;code&gt;BigInteger&lt;/code&gt;）， 也不能直接将输入的字符串转换为整数形式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：num1 = &quot;11&quot;, num2 = &quot;123&quot;
输出：&quot;134&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    string addStrings(string num1, string num2) {
        int i = num1.length() - 1, j = num2.length() - 1, add = 0;
        string ans = &quot;&quot;;
        while (i &gt;= 0 || j &gt;= 0 || add) {
            int x = i &gt;= 0 ? num1[i] - &apos;0&apos; : 0;
            int y = j &gt;= 0 ? num2[j] - &apos;0&apos; : 0;
            int result = x + y + add;
            ans.push_back(&apos;0&apos; + result % 10);
            add = result / 10;
            i--;
            j--;
        }
        reverse(ans.begin(), ans.end());
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/add-two-numbers/&quot;&gt;2. 两数相加&lt;/a&gt;｜链表版 · 从头开始➕&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280048297.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：l1 = [2,4,3], l2 = [5,6,4]
输出：[7,0,8]
解释：342 + 465 = 807.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出：[8,9,9,9,0,0,0,1]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 迭代&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode dummy;
        ListNode* cur = &amp;#x26;dummy;
        int carry = 0;
        while (l1 || l2 || carry) {
            int res = 0;
            if (l1) {
                res += l1-&gt;val;
                l1 = l1-&gt;next;
            }
            if (l2) {
                res += l2-&gt;val;
                l2 = l2-&gt;next;
            }
            res += carry;
            carry = res / 10;
            cur = cur-&gt;next = new ListNode(res % 10);
        }
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 递归&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2, int carry = 0) {
        if (!l1 &amp;#x26;&amp;#x26; !l2) {
            return carry ? new ListNode(carry) : nullptr;
        }
        if (!l1) {
            swap(l1, l2);
        }
        int sum = carry + l1-&gt;val + (l2 ? l2-&gt;val : 0);
        l1-&gt;val = sum % 10;
        l1-&gt;next = addTwoNumbers(l1-&gt;next, (l2 ? l2-&gt;next : nullptr), sum / 10);
        return l1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/add-two-numbers-ii/&quot;&gt;445. 两数相加 II&lt;/a&gt;｜链表版 · 从尾开始➕&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280053643.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：l1 = [7,2,4,3], l2 = [5,6,4]
输出：[7,8,0,7]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ 两数相加 II = 两数相加 + 反转链表&lt;/p&gt;
&lt;h3&gt;1️⃣ 迭代｜206. 反转链表（迭代）+ 2. 两数相加（迭代）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        while (cur) {
            ListNode* nxt = cur-&gt;next;
            cur-&gt;next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }

    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        l1 = reverseList(l1);
        l2 = reverseList(l2);
        ListNode dummy;
        ListNode* cur = &amp;#x26;dummy;
        int carry = 0;
        while (l1 || l2 || carry) {
            int res = carry;
            if (l1) {
                res += l1-&gt;val;
                l1 = l1-&gt;next;
            }
            if (l2) {
                res += l2-&gt;val;
                l2 = l2-&gt;next;
            }
            carry = res / 10;
            cur = cur-&gt;next = new ListNode(res % 10);
        }
        return reverseList(dummy.next);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 递归｜206. 反转链表（递归）+ 2. 两数相加（递归）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (!head || !head-&gt;next) {
            return head;
        }
        ListNode* new_head = reverseList(head-&gt;next);
        head-&gt;next-&gt;next = head;
        head-&gt;next = nullptr;
        return new_head;
    }

    ListNode* addTwo(ListNode* l1, ListNode* l2, int carry = 0) {
        if (!l1 &amp;#x26;&amp;#x26; !l2) {
            return carry ? new ListNode(carry) : nullptr;
        }
        if (!l1) {
            swap(l1, l2);
        }
        int sum = carry + l1-&gt;val + (l2 ? l2-&gt;val : 0);
        l1-&gt;val = sum % 10;
        l1-&gt;next = addTwo(l1-&gt;next, (l2 ? l2-&gt;next : nullptr), sum / 10);
        return l1;
    }

    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        l1 = reverseList(l1);
        l2 = reverseList(l2);
        ListNode* head = addTwo(l1, l2, 0);
        return reverseList(head);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/add-binary/&quot;&gt;67. 二进制求和&lt;/a&gt;｜二进制版&lt;/h2&gt;
&lt;p&gt;给你两个二进制字符串 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; ，以二进制字符串的形式返回它们的和。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入:a = &quot;11&quot;, b = &quot;1&quot;
输出：&quot;100&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：a = &quot;1010&quot;, b = &quot;1011&quot;
输出：&quot;10101&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    string addBinary(string a, string b) {
        int carry = 0;
        int i = a.length() - 1, j = b.length() - 1;
        string ans;
        while (i &gt;= 0 || j &gt;= 0 || carry) {
            int x = i &gt;= 0 ? a[i--] - &apos;0&apos; : 0;
            int y = j &gt;= 0 ? b[j--] - &apos;0&apos; : 0;
            int s = x + y + carry;
            carry = s / 2;
            ans.push_back(s % 2 + &apos;0&apos;);
        }
        reverse(ans.begin(), ans.end());
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/multiply-strings/&quot;&gt;43. 字符串相乘&lt;/a&gt;｜大数相乘✖️&lt;/h2&gt;
&lt;p&gt;给定两个以字符串形式表示的非负整数 &lt;code&gt;num1&lt;/code&gt; 和 &lt;code&gt;num2&lt;/code&gt;，返回 &lt;code&gt;num1&lt;/code&gt; 和 &lt;code&gt;num2&lt;/code&gt; 的乘积，它们的乘积也表示为字符串形式。&lt;/p&gt;
&lt;p&gt;**注意：**不能使用任何内置的 Big Integer 库或直接将输入转换为整数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入: num1 = &quot;2&quot;, num2 = &quot;3&quot;
输出: &quot;6&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入: num1 = &quot;123&quot;, num2 = &quot;456&quot;
输出: &quot;56088&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 竖式相加&lt;/h3&gt;
&lt;p&gt;思路：建立在「大数相加」的基础上，因为多个数之间需要累加（容易理解）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280046243.png&quot; alt=&quot;fig1&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    // 大数相加
    string addStrings(string num1, string num2) {
        int i = num1.length() - 1, j = num2.length() - 1, add = 0;
        string ans = &quot;&quot;;
        while (i &gt;= 0 || j &gt;= 0 || add) {
            int x = i &gt;= 0 ? num1[i] - &apos;0&apos; : 0;
            int y = j &gt;= 0 ? num2[j] - &apos;0&apos; : 0;
            int result = x + y + add;
            add = result / 10;
            ans.push_back(&apos;0&apos; + result % 10);
            i--;
            j--;
        }
        reverse(ans.begin(), ans.end());
        return ans;
    }

    // 大数相乘
    string multiply(string num1, string num2) {
        if (num1 == &quot;0&quot; || num2 == &quot;0&quot;)
            return &quot;0&quot;;
        int multiply = 0;
        int m = num1.length(), n = num2.length();
        string ans = &quot;0&quot;;
        for (int i = m - 1; i &gt;= 0; i--) {
            int x = num1[i] - &apos;0&apos;;
            string num;
            int add = 0;
            for (int j = n - 1; j &gt;= 0 || add; j--) {
                if (x == 0) {
                    num = &quot;0&quot;;
                    break;
                }
                if (j &amp;#x3C; 0) {
                    num.push_back(&apos;0&apos; + add);
                    break;
                }
                int y = num2[j] - &apos;0&apos;;
                int result = x * y + add;
                add = result / 10;
                num.push_back(&apos;0&apos; + result % 10);
            }
            reverse(num.begin(), num.end());
            if (num != &quot;0&quot;)
                ans = addStrings(ans, num + string(multiply, &apos;0&apos;));
            multiply++;
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 相乘&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202502282211768.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;思路：直接做乘法，长度分别为 &lt;code&gt;m&lt;/code&gt; 和 &lt;code&gt;n&lt;/code&gt; 的数字相乘，值长度不超过 &lt;code&gt;m + n&lt;/code&gt;，&lt;code&gt;vector&amp;#x3C;int&gt; ansArr(m + n)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    string multiply(string num1, string num2) {
        if (num1 == &quot;0&quot; || num2 == &quot;0&quot;) {
            return &quot;0&quot;;
        }
        int m = num1.length(), n = num2.length();
        vector&amp;#x3C;int&gt; ansArr(m + n);
        for (int i = m - 1; i &gt;= 0; i--) {
            int x = num1[i] - &apos;0&apos;;
            for (int j = n - 1; j &gt;= 0; j--) {
                int y = num2[j] - &apos;0&apos;;
                ansArr[i + j + 1] += x * y;
            }
        }
        for (int i = m + n - 1; i &gt; 0; i--) {
            ansArr[i - 1] += ansArr[i] / 10;
            ansArr[i] %= 10;
        }
        int idx = ansArr[0] == 0 ? 1 : 0;
        string ans;
        while (idx &amp;#x3C; m + n) {
            ans.push_back(&apos;0&apos; + ansArr[idx]);
            idx++;
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>高频「链表」面试题</title><link>https://coooredump.github.io/blog/leetcode/interview-linked-list</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/interview-linked-list</guid><description>面试高频考点之链表题</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;✅ 高频链表面试题&lt;/h2&gt;
&lt;p&gt;相关例题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/linked-list-cycle/&quot;&gt;141. 环形链表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/linked-list-cycle-ii/&quot;&gt;142. 环形链表 II&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/intersection-of-two-linked-lists/&quot;&gt;160. 相交链表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list/&quot;&gt;206. 反转链表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list-ii/&quot;&gt;92. 反转链表 II&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/middle-of-the-linked-list/&quot;&gt;876. 链表的中间结点&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/palindrome-linked-list/&quot;&gt;234. 回文链表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/merge-two-sorted-lists/&quot;&gt;21. 合并两个有序链表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/add-two-numbers/&quot;&gt;2. 两数相加&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/add-two-numbers-ii/&quot;&gt;445. 两数相加 II&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/remove-nth-node-from-end-of-list/&quot;&gt;19. 删除链表的倒数第 N 个结点&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/swap-nodes-in-pairs/&quot;&gt;24. 两两交换链表中的节点&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-nodes-in-k-group/&quot;&gt;25. K 个一组翻转链表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/merge-k-sorted-lists/&quot;&gt;23. 合并 K 个升序链表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/lru-cache/&quot;&gt;146. LRU 缓存&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/copy-list-with-random-pointer/&quot;&gt;138. 随机链表的复制&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/&quot;&gt;82. 删除排序链表中的重复元素 II&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/rotate-list/&quot;&gt;61. 旋转链表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/partition-list/&quot;&gt;86. 分隔链表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/sort-list/&quot;&gt;148. 排序链表&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/linked-list-cycle/&quot;&gt;141. 环形链表&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280121512.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：head = [3,2,0,-4], pos = 1
输出：true
解释：链表中有一个环，其尾部连接到第二个节点。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 快慢指针&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    bool hasCycle(ListNode* head) {
        ListNode *slow = head, *fast = head;
        while (fast &amp;#x26;&amp;#x26; fast-&gt;next) {
            slow = slow-&gt;next;
            fast = fast-&gt;next-&gt;next;
            if (slow == fast)
                return true;
        }
        return false;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/linked-list-cycle-ii/&quot;&gt;142. 环形链表 II&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给定一个链表的头节点  &lt;code&gt;head&lt;/code&gt; ，返回链表开始入环的第一个节点。 &lt;em&gt;如果链表无环，则返回 &lt;code&gt;null&lt;/code&gt;。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;如果链表中有某个节点，可以通过连续跟踪 &lt;code&gt;next&lt;/code&gt; 指针再次到达，则链表中存在环。 为了表示给定链表中的环，评测系统内部使用整数 &lt;code&gt;pos&lt;/code&gt; 来表示链表尾连接到链表中的位置（&lt;strong&gt;索引从 0 开始&lt;/strong&gt;）。如果 &lt;code&gt;pos&lt;/code&gt; 是 &lt;code&gt;-1&lt;/code&gt;，则在该链表中没有环。&lt;strong&gt;注意：&lt;code&gt;pos&lt;/code&gt; 不作为参数进行传递&lt;/strong&gt;，仅仅是为了标识链表的实际情况。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不允许修改&lt;/strong&gt; 链表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280119476.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：head = [3,2,0,-4], pos = 1
输出：返回索引为 1 的链表节点
解释：链表中有一个环，其尾部连接到第二个节点。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 快慢指针&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280121614.png&quot; alt=&quot;fig1&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* detectCycle(ListNode* head) {
        ListNode *slow = head, *fast = head;
        while (fast &amp;#x26;&amp;#x26; fast-&gt;next) {
            slow = slow-&gt;next;
            fast = fast-&gt;next-&gt;next;
            if (slow == fast) {
                // a = k(b + c) + c
                ListNode* node = head;
                while (node != slow) {
                    node = node-&gt;next;
                    slow = slow-&gt;next;
                }
                return slow;
            }
        }
        return nullptr;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/intersection-of-two-linked-lists/&quot;&gt;160. 相交链表&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280122571.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
        ListNode *p = headA, *q = headB;
        while (p != q) {
            p = p ? p-&gt;next : headB;
            q = q ? q-&gt;next : headA;
        }
        return p;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list/&quot;&gt;206. 反转链表&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280123470.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1️⃣ 迭代｜三指针&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        while (cur) {
            ListNode* nxt = cur-&gt;next;
            cur-&gt;next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 递归&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (!head || !head-&gt;next)
            return head;
        ListNode* new_head = reverseList(head-&gt;next);
        head-&gt;next-&gt;next = head;
        head-&gt;next = nullptr;   // 为了反转后的末节点
        return new_head;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list-ii/&quot;&gt;92. 反转链表 II&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280141581.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;反转链表进阶：反转部分区间，找到区间 leftNode 与 rightNode，以及 leftNode 左节点 pre 与 rightNode 右节点 nxt，独立区间（断开连接）后反转再接回。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        while (cur) {
            ListNode* nxt = cur-&gt;next;
            cur-&gt;next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }

    ListNode* reverseBetween(ListNode* head, int left, int right) {
        ListNode dummy(0, head);
        ListNode* pre = &amp;#x26;dummy;
        int t = left;
        while (--t) {
            pre = pre-&gt;next;
        }
        ListNode* leftNode = pre-&gt;next;
        ListNode* rightNode = leftNode;
        t = right - left;
        while (t--) {
            rightNode = rightNode-&gt;next;
        }
        ListNode* nxt = rightNode-&gt;next;
        // 断开链接
        pre-&gt;next = nullptr;
        rightNode-&gt;next = nullptr;
        // 反转链表
        ListNode* node = reverseList(leftNode);
        // 重新链接
        pre-&gt;next = node;
        leftNode-&gt;next = nxt;
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/middle-of-the-linked-list/&quot;&gt;876. 链表的中间结点&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280127329.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：head = [1,2,3,4,5]
输出：[3,4,5]
解释：链表只有一个中间结点，值为 3 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280127215.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：head = [1,2,3,4,5,6]
输出：[4,5,6]
解释：该链表有两个中间结点，值分别为 3 和 4 ，返回第二个结点。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 快慢指针&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode *slow = head, *fast = head;
        while (fast &amp;#x26;&amp;#x26; fast-&gt;next) {
            slow = slow-&gt;next;
            fast = fast-&gt;next-&gt;next;
        }
        return slow;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/palindrome-linked-list/&quot;&gt;234. 回文链表&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280125316.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1️⃣ 回文链表判断 = 寻找中间节点 + 反转链表&lt;/h3&gt;
&lt;p&gt;前置题目：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/middle-of-the-linked-list/&quot;&gt;876. 链表的中间结点&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list/&quot;&gt;206. 反转链表&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode *slow = head, *fast = head;
        while (fast &amp;#x26;&amp;#x26; fast-&gt;next) {
            slow = slow-&gt;next;
            fast = fast-&gt;next-&gt;next;
        }
        return slow;
    }

    ListNode* reverseList(ListNode* head) {
        ListNode *pre = nullptr, *cur = head;
        while (cur) {
            ListNode* nxt = cur-&gt;next;
            cur-&gt;next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }

    bool isPalindrome(ListNode* head) {
        ListNode* mid = middleNode(head);
        ListNode* head2 = reverseList(mid);
        while (head2) {
            if (head-&gt;val != head2-&gt;val) {
                return false;
            }
            head = head-&gt;next;
            head2 = head2-&gt;next;
        }
        return true;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/merge-two-sorted-lists/&quot;&gt;21. 合并两个有序链表&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280128941.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：l1 = [1,2,4], l2 = [1,3,4]
输出：[1,1,2,3,4,4]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 迭代（常用）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode dummy{};
        ListNode* cur = &amp;#x26;dummy;
        while (list1 &amp;#x26;&amp;#x26; list2) {
            if (list1-&gt;val &amp;#x3C; list2-&gt;val) {
                cur-&gt;next = list1;
                list1 = list1-&gt;next;
            } else {
                cur-&gt;next = list2;
                list2 = list2-&gt;next;
            }
            cur = cur-&gt;next;
        }
        cur-&gt;next = list1 ? list1 : list2;
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 递归&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if (list1 == nullptr) return list2; // 注：如果都为空则返回空
        if (list2 == nullptr) return list1;
        if (list1-&gt;val &amp;#x3C; list2-&gt;val) {
            list1-&gt;next = mergeTwoLists(list1-&gt;next, list2);
            return list1;
        }
        list2-&gt;next = mergeTwoLists(list1, list2-&gt;next);
        return list2;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/add-two-numbers/&quot;&gt;2. 两数相加&lt;/a&gt;｜从头开始相加&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280130500.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1️⃣ 迭代&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode dummy;
        ListNode* cur = &amp;#x26;dummy;
        int carry = 0;
        while (l1 || l2 || carry) {
            if (l1) {
                carry += l1-&gt;val;
                l1 = l1-&gt;next;
            }
            if (l2) {
                carry += l2-&gt;val;
                l2 = l2-&gt;next;
            }
            cur = cur-&gt;next = new ListNode(carry % 10);
            carry /= 10;
        }
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 递归&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    // l1 和 l2 为当前遍历的节点，carry 为进位
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2, int carry = 0) {
        if (l1 == nullptr &amp;#x26;&amp;#x26; l2 == nullptr) { // 递归边界：l1 和 l2 都是空节点
            return carry ? new ListNode(carry) : nullptr; // 如果进位了，就额外创建一个节点
        }
        if (l1 == nullptr) { // 如果 l1 是空的，那么此时 l2 一定不是空节点
            swap(l1, l2); // 交换 l1 与 l2，保证 l1 非空，从而简化代码
        }
        int sum = carry + l1-&gt;val + (l2 ? l2-&gt;val : 0); // 节点值和进位加在一起
        l1-&gt;val = sum % 10; // 每个节点保存一个数位（直接修改原链表）
        l1-&gt;next = addTwoNumbers(l1-&gt;next, (l2 ? l2-&gt;next : nullptr), sum / 10); // 进位
        return l1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/add-two-numbers-ii/&quot;&gt;445. 两数相加 II&lt;/a&gt;｜从尾开始相加&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280132912.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1️⃣ 迭代｜反转链表 + 两数相加&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr, *cur = head;
        while (cur) {
            ListNode* nxt = cur-&gt;next;
            cur-&gt;next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }

    ListNode* addTwo(ListNode* l1, ListNode* l2) {
        ListNode dummy; // 哨兵节点
        auto cur = &amp;#x26;dummy;
        int carry = 0; // 进位
        while (l1 || l2 || carry) { // 有一个不是空节点，或者还有进位，就继续迭代
            if (l1) carry += l1-&gt;val; // 节点值和进位加在一起
            if (l2) carry += l2-&gt;val; // 节点值和进位加在一起
            cur = cur-&gt;next = new ListNode(carry % 10); // 每个节点保存一个数位
            carry /= 10; // 新的进位
            if (l1) l1 = l1-&gt;next; // 下一个节点
            if (l2) l2 = l2-&gt;next; // 下一个节点
        }
        return dummy.next; // 哨兵节点的下一个节点就是头节点
    }

public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        l1 = reverseList(l1);
        l2 = reverseList(l2);
        auto l3 = addTwo(l1, l2);
        return reverseList(l3);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 递归｜反转链表 + 两数相加&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
    ListNode* reverseList(ListNode* head) {
        if (head == nullptr || head-&gt;next == nullptr) {
            return head;
        }
        auto new_head = reverseList(head-&gt;next);
        head-&gt;next-&gt;next = head; // 把下一个节点指向自己
        head-&gt;next = nullptr; // 断开指向下一个节点的连接，保证最终链表的末尾节点的 next 是空节点
        return new_head;
    }

    // l1 和 l2 为当前遍历的节点，carry 为进位
    ListNode* addTwo(ListNode* l1, ListNode* l2, int carry = 0) {
        if (l1 == nullptr &amp;#x26;&amp;#x26; l2 == nullptr) { // 递归边界：l1 和 l2 都是空节点
            return carry ? new ListNode(carry) : nullptr; // 如果进位了，就额外创建一个节点
        }
        if (l1 == nullptr) { // 如果 l1 是空的，那么此时 l2 一定不是空节点
            swap(l1, l2); // 交换 l1 与 l2，保证 l1 非空，从而简化代码
        }
        carry += l1-&gt;val + (l2 ? l2-&gt;val : 0); // 节点值和进位加在一起
        l1-&gt;val = carry % 10; // 每个节点保存一个数位
        l1-&gt;next = addTwo(l1-&gt;next, (l2 ? l2-&gt;next : nullptr), carry / 10); // 进位
        return l1;
    }

public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        l1 = reverseList(l1);
        l2 = reverseList(l2); // l1 和 l2 反转后，就变成【2. 两数相加】了
        auto l3 = addTwo(l1, l2);
        return reverseList(l3);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/remove-nth-node-from-end-of-list/&quot;&gt;19. 删除链表的倒数第 N 个结点&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280135230.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1️⃣ 快慢指针&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode dummy(0, head);
        ListNode *fast = &amp;#x26;dummy, *slow = &amp;#x26;dummy;
        while (n--) {
            fast = fast-&gt;next;
        }
        while (fast-&gt;next) {
            slow = slow-&gt;next;
            fast = fast-&gt;next;
        }
        ListNode* d = slow-&gt;next;
        slow-&gt;next = d-&gt;next;
        delete d;
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/swap-nodes-in-pairs/&quot;&gt;24. 两两交换链表中的节点&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280137405.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1️⃣ 迭代｜四指针&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280137375.png&quot; alt=&quot;lc24-c.png&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode dummy(0, head); // 用哨兵节点简化代码逻辑
        ListNode* node0 = &amp;#x26;dummy;
        ListNode* node1 = head;
        while (node1 &amp;#x26;&amp;#x26; node1-&gt;next) { // 至少有两个节点
            ListNode* node2 = node1-&gt;next;
            ListNode* node3 = node2-&gt;next;

            node0-&gt;next = node2; // 0 -&gt; 2
            node2-&gt;next = node1; // 2 -&gt; 1
            node1-&gt;next = node3; // 1 -&gt; 3

            node0 = node1; // 下一轮交换，0 是 1
            node1 = node3; // 下一轮交换，1 是 3
        }
        return dummy.next; // 返回新链表的头节点
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 递归&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if (head == nullptr || head-&gt;next == nullptr) {
            return head;
        }

        ListNode* node1 = head;
        ListNode* node2 = head-&gt;next;
        ListNode* node3 = node2-&gt;next;

        node1-&gt;next = swapPairs(node3); // 1 指向递归返回的链表头
        node2-&gt;next = node1; // 2 指向 1

        return node2; // 返回交换后的链表头节点
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-nodes-in-k-group/&quot;&gt;25. K 个一组翻转链表&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280139936.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1️⃣ 从「反转链表 · 迭代版」到「K 个一组翻转链表」&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        int n = 0;
        for (ListNode* cur = head; cur; cur = cur-&gt;next) {
            n++;
        }
        ListNode dummy(0, head);
        ListNode* p0 = &amp;#x26;dummy;
        ListNode* prev = nullptr;
        ListNode* cur = head;
        for (; n &gt;= k; n -= k) {
            for (int i = 0; i &amp;#x3C; k; i++) {
                ListNode* nxt = cur-&gt;next;
                cur-&gt;next = prev;
                prev = cur;
                cur = nxt;
            }
            ListNode* last = p0-&gt;next;
            p0-&gt;next = prev;
            last-&gt;next = cur;
            p0 = last;
        }
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣「206. 反转链表」+「92. 反转链表 II」&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    // 206. 反转链表
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        while (cur) {
            ListNode* nxt = cur-&gt;next;
            cur-&gt;next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }

    // 92. 反转链表 II
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        ListNode dummy(0, head);
        ListNode* pre = &amp;#x26;dummy;
        for (int i = 0; i &amp;#x3C; left - 1; i++) {
            pre = pre-&gt;next;
        }
        ListNode* leftNode = pre-&gt;next;
        ListNode* rightNode = leftNode;
        for (int i = left; i &amp;#x3C; right; i++) {
            rightNode = rightNode-&gt;next;
        }
        ListNode* nxt = rightNode-&gt;next;
        rightNode-&gt;next = nullptr;
        reverseList(leftNode);
        pre-&gt;next = rightNode;
        leftNode-&gt;next = nxt;
        return dummy.next;
    }

    ListNode* reverseKGroup(ListNode* head, int k) {
        int n = 0;
        ListNode* cur = head;
        while (cur) {
            n++;
            cur = cur-&gt;next;
        }
        if (n &amp;#x3C; k) {
            return head;
        }
        ListNode* new_head = head;
        int times = n / k;
        for (int i = 0; times--; i += k) {
            ListNode* node = reverseBetween(new_head, i + 1, i + k);
            if (i == 0) {
                new_head = node;
            }
        }
        return new_head;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/merge-k-sorted-lists/&quot;&gt;23. 合并 K 个升序链表&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：lists = [[1,4,5],[1,3,4],[2,6]]
输出：[1,1,2,3,4,4,5,6]
解释：链表数组如下：
[
  1-&gt;4-&gt;5,
  1-&gt;3-&gt;4,
  2-&gt;6
]
将它们合并到一个有序链表中得到。
1-&gt;1-&gt;2-&gt;3-&gt;4-&gt;4-&gt;5-&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ &lt;strong&gt;快手一面&lt;/strong&gt;，面试官来一题简单题😊，最后还问了时间复杂度：假设 k 个链表，共 n 个节点，那时间复杂度为 $O(k·logk+n·logk)$ 即 $O(n·logk)$。&lt;/p&gt;
&lt;h3&gt;1️⃣ 最小堆&lt;/h3&gt;
&lt;p&gt;时间复杂度分析：假设 $k$ 个链表, 共 $n$ 个节点, 最小堆单次操作 $O(log k)$, 初始化堆需要 $O(k·logk)$, 那时间复杂度为 $O(k·logk + n·logk)$，即 $O(n·logk)$。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeKLists(vector&amp;#x3C;ListNode*&gt;&amp;#x26; lists) {
        auto cmp = [](const ListNode* a, const ListNode* b) {
            return a-&gt;val &gt; b-&gt;val;
        };
        priority_queue&amp;#x3C;ListNode*, vector&amp;#x3C;ListNode*&gt;, decltype(cmp)&gt; pq;
        for (auto&amp;#x26; head : lists) {
            if (head) {
                pq.push(head);
            }
        }
        ListNode dummy{};
        ListNode* head = &amp;#x26;dummy;
        while (!pq.empty()) {
            ListNode* node = pq.top();
            pq.pop();
            head-&gt;next = node;
            head = head-&gt;next;
            if (node-&gt;next)
                pq.push(node-&gt;next);
        }
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 分治法&lt;/h3&gt;
&lt;p&gt;前置题目：&lt;a href=&quot;https://leetcode.cn/problems/merge-two-sorted-lists/&quot;&gt;21. 合并两个有序链表&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode dummy{};
        ListNode* cur = &amp;#x26;dummy;
        while (list1 &amp;#x26;&amp;#x26; list2) {
            if (list1-&gt;val &amp;#x3C; list2-&gt;val) {
                cur-&gt;next = list1;
                list1 = list1-&gt;next;
            } else {
                cur-&gt;next = list2;
                list2 = list2-&gt;next;
            }
            cur = cur-&gt;next;
        }
        cur-&gt;next = list1 ? list1 : list2;
        return dummy.next;
    }

    ListNode* mergeKLists(vector&amp;#x3C;ListNode*&gt;&amp;#x26; lists, int l, int r) {
        if (l == r)
            return lists[l];
        if (l &gt; r)
            return nullptr;
        int m = (l + r) &gt;&gt; 1;
        auto left = mergeKLists(lists, l, m);
        auto right = mergeKLists(lists, m + 1, r);
        return mergeTwoLists(left, right);
    }

    ListNode* mergeKLists(vector&amp;#x3C;ListNode*&gt;&amp;#x26; lists) {
        return mergeKLists(lists, 0, lists.size() - 1);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/lru-cache/&quot;&gt;146. LRU 缓存&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280145436.png&quot; alt=&quot;图解 LRU&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Node {
    int key;
    int value;
    Node* prev;
    Node* next;
    Node(int k = 0, int v = 0) : key(k), value(v) {}
};

class LRUCache {
private:
    Node* dummy;
    int capacity;
    unordered_map&amp;#x3C;int, Node*&gt; key_to_node;

    void remove(Node* node) {
        node-&gt;prev-&gt;next = node-&gt;next;
        node-&gt;next-&gt;prev = node-&gt;prev;
    }

    void push_front(Node* node) {
        node-&gt;next = dummy-&gt;next;
        node-&gt;prev = dummy;
        dummy-&gt;next-&gt;prev = node;
        dummy-&gt;next = node;
    }

    Node* get_node(int key) {
        if (!key_to_node.count(key)) {
            return nullptr;
        }
        Node* node = key_to_node[key];
        remove(node);
        push_front(node);
        return node;
    }

public:
    LRUCache(int capacity) : capacity(capacity), dummy(new Node()) {
        dummy-&gt;next = dummy;
        dummy-&gt;prev = dummy;
    }

    int get(int key) {
        Node* node = get_node(key);
        return node == nullptr ? -1 : node-&gt;value;
    }

    void put(int key, int value) {
        Node* node = get_node(key);
        if (node != nullptr) {
            node-&gt;value = value;
            return;
        }
        node = new Node(key, value);
        if (key_to_node.size() == capacity) {
            Node* last = dummy-&gt;prev;
            remove(last);
            key_to_node.erase(last-&gt;key);
            delete last;
        }
        key_to_node[key] = node;
        push_front(node);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/copy-list-with-random-pointer/&quot;&gt;138. 随机链表的复制&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280147716.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出：[[7,null],[13,0],[11,4],[10,2],[1,0]]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;

    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/

class Solution {
public:
    unordered_map&amp;#x3C;Node*, Node*&gt; cacheNode;

    Node* copyRandomList(Node* head) {
        if (head == nullptr)
            return nullptr;
        if (!cacheNode.count(head)) {
            Node* newHead = new Node(head-&gt;val);
            cacheNode[head] = newHead;
            newHead-&gt;next = copyRandomList(head-&gt;next);
            newHead-&gt;random = copyRandomList(head-&gt;random);
        }
        return cacheNode[head];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/&quot;&gt;82. 删除排序链表中的重复元素 II&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给定一个已排序的链表的头 &lt;code&gt;head&lt;/code&gt; ， &lt;em&gt;删除原始链表中所有重复数字的节点，只留下不同的数字&lt;/em&gt; 。返回 &lt;em&gt;已排序的链表&lt;/em&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280148574.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        ListNode dummy(0, head);
        auto cur = &amp;#x26;dummy;
        while (cur-&gt;next &amp;#x26;&amp;#x26; cur-&gt;next-&gt;next) {
            int val = cur-&gt;next-&gt;val;
            if (cur-&gt;next-&gt;next-&gt;val == val) {
                while (cur-&gt;next &amp;#x26;&amp;#x26; cur-&gt;next-&gt;val == val) {
                    cur-&gt;next = cur-&gt;next-&gt;next;
                }
            } else {
                cur = cur-&gt;next;
            }
        }
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/rotate-list/&quot;&gt;61. 旋转链表&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给你一个链表的头节点 &lt;code&gt;head&lt;/code&gt; ，旋转链表，将链表每个节点向右移动 &lt;code&gt;k&lt;/code&gt; 个位置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280149901.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：head = [1,2,3,4,5], k = 2
输出：[4,5,1,2,3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;思路：我们可以先将给定的链表连接成环，然后将指定位置断开。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) {
        if (k == 0 || !head || !head-&gt;next) {
            return head;
        }
        int n = 1;
        ListNode* iter = head;
        while (iter-&gt;next) {
            iter = iter-&gt;next;
            n++;
        }
        int t = n - k % n;
        if (t == n)
            return head;
        iter-&gt;next = head; // 连成环
        while (t--) {
            iter = iter-&gt;next;
        }
        ListNode* ret = iter-&gt;next;
        iter-&gt;next = nullptr;
        return ret;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/partition-list/&quot;&gt;86. 分隔链表&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给你一个链表的头节点 &lt;code&gt;head&lt;/code&gt; 和一个特定值 &lt;code&gt;x&lt;/code&gt; ，请你对链表进行分隔，使得所有 &lt;strong&gt;小于&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt; 的节点都出现在 &lt;strong&gt;大于或等于&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt; 的节点之前。&lt;/p&gt;
&lt;p&gt;你应当 &lt;strong&gt;保留&lt;/strong&gt; 两个分区中每个节点的初始相对位置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280150286.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：head = [1,4,3,2,5,2], x = 3
输出：[1,2,2,4,3,5]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 模拟&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        ListNode largeDummy(0);
        ListNode* large = &amp;#x26;largeDummy;
        ListNode smallDummy(0);
        ListNode* small = &amp;#x26;smallDummy;
        while (head) {
            if (head-&gt;val &amp;#x3C; x) {
                small-&gt;next = head;
                small = small-&gt;next;
            } else {
                large-&gt;next = head;
                large = large-&gt;next;
            }
            head = head-&gt;next;
        }
        small-&gt;next = largeDummy.next;
        large-&gt;next = nullptr;
        return smallDummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/sort-list/&quot;&gt;148. 排序链表&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504280151182.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1️⃣ 归并排序（分治法）｜链表的中间结点 + 合并两个有序链表&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    // 876. 链表的中间结点（快慢指针）
    ListNode* middleNode(ListNode* head) {
        ListNode* pre = head;
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast &amp;#x26;&amp;#x26; fast-&gt;next) {
            pre = slow;
            slow = slow-&gt;next;
            fast = fast-&gt;next-&gt;next;
        }
        pre-&gt;next = nullptr; // 断开 slow 与前一个节点的连接
        return slow;
    }

    // 21. 合并两个有序链表 (双指针)
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode dummy;
        ListNode* cur = &amp;#x26;dummy;
        while (list1 &amp;#x26;&amp;#x26; list2) {
            if (list1-&gt;val &amp;#x3C; list2-&gt;val) {
                cur-&gt;next = list1;
                list1 = list1-&gt;next;
            } else {
                cur-&gt;next = list2;
                list2 = list2-&gt;next;
            }
            cur = cur-&gt;next;
        }
        cur-&gt;next = list1 ? list1 : list2;
        return dummy.next;
    }

    ListNode* sortList(ListNode* head) {
        if (!head || !head-&gt;next)
            return head;
        ListNode* mid = middleNode(head);
        ListNode* head1 = sortList(head);
        ListNode* head2 = sortList(mid);
        return mergeTwoLists(head1, head2);
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>快手一面｜合并 K 个升序链表</title><link>https://coooredump.github.io/blog/leetcode/kuaishou-merge-k-ascending-linked-lists</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/kuaishou-merge-k-ascending-linked-lists</guid><description>快手一面，面试官说来一题简单题，最后还问了时间复杂度：假设 k 个链表，共 n 个节点，时间复杂度是多少</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;快手一面 &amp;#x26; 时间复杂度&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;快手一面&lt;/strong&gt;，面试官说来一题简单题😊，最后还问了时间复杂度：假设 k 个链表，共 n 个节点，那时间复杂度为 $O(k·logk+n·logk)$ 即 $O(n·logk)$。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/merge-k-sorted-lists/&quot;&gt;23. 合并 K 个升序链表&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给你一个链表数组，每个链表都已经按升序排列。&lt;/p&gt;
&lt;p&gt;请你将所有链表合并到一个升序链表中，返回合并后的链表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：lists = [[1,4,5],[1,3,4],[2,6]]
输出：[1,1,2,3,4,4,5,6]
解释：链表数组如下：
[
  1-&gt;4-&gt;5,
  1-&gt;3-&gt;4,
  2-&gt;6
]
将它们合并到一个有序链表中得到。
1-&gt;1-&gt;2-&gt;3-&gt;4-&gt;4-&gt;5-&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 最小堆&lt;/h3&gt;
&lt;p&gt;时间复杂度分析：假设 $k$ 个链表, 共 $n$ 个节点, 最小堆单次操作 $O(log k)$, 初始化堆需要 $O(k·logk)$, 那时间复杂度为 $O(k·logk + n·logk)$，即 $O(n·logk)$。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeKLists(vector&amp;#x3C;ListNode*&gt;&amp;#x26; lists) {
        auto cmp = [](const ListNode* a, const ListNode* b) {
            return a-&gt;val &gt; b-&gt;val;
        };
        priority_queue&amp;#x3C;ListNode*, vector&amp;#x3C;ListNode*&gt;, decltype(cmp)&gt; pq;
        for (auto&amp;#x26; head : lists) {
            if (head) {
                pq.push(head);
            }
        }
        ListNode dummy{};
        ListNode* head = &amp;#x26;dummy;
        while (!pq.empty()) {
            ListNode* node = pq.top();
            pq.pop();
            head-&gt;next = node;
            head = head-&gt;next;
            if (node-&gt;next)
                pq.push(node-&gt;next);
        }
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 分治法&lt;/h3&gt;
&lt;p&gt;前置题目：&lt;a href=&quot;https://leetcode.cn/problems/merge-two-sorted-lists/&quot;&gt;21. 合并两个有序链表&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode dummy{};
        ListNode* cur = &amp;#x26;dummy;
        while (list1 &amp;#x26;&amp;#x26; list2) {
            if (list1-&gt;val &amp;#x3C; list2-&gt;val) {
                cur-&gt;next = list1;
                list1 = list1-&gt;next;
            } else {
                cur-&gt;next = list2;
                list2 = list2-&gt;next;
            }
            cur = cur-&gt;next;
        }
        cur-&gt;next = list1 ? list1 : list2;
        return dummy.next;
    }

    ListNode* mergeKLists(vector&amp;#x3C;ListNode*&gt;&amp;#x26; lists, int l, int r) {
        if (l == r)
            return lists[l];
        if (l &gt; r)
            return nullptr;
        int m = (l + r) &gt;&gt; 1;
        auto left = mergeKLists(lists, l, m);
        auto right = mergeKLists(lists, m + 1, r);
        return mergeTwoLists(left, right);
    }

    ListNode* mergeKLists(vector&amp;#x3C;ListNode*&gt;&amp;#x26; lists) {
        return mergeKLists(lists, 0, lists.size() - 1);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复习「堆排序」&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    // 向下调整
    void heapify(vector&amp;#x3C;int&gt;&amp;#x26; nums, int i, int n) {
        int largest = i;
        int left = i * 2 + 1;
        int right = i * 2 + 2;
        if (left &amp;#x3C; n &amp;#x26;&amp;#x26; nums[left] &gt; nums[largest])
            largest = left;
        if (right &amp;#x3C; n &amp;#x26;&amp;#x26; nums[right] &gt; nums[largest])
            largest = right;
        if (largest != i) {
            swap(nums[largest], nums[i]);
            heapify(nums, largest, n);
        }
    }

    void heapsort(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        int n = nums.size();
        // i = n / 2 也可, 多判断一次而已
        // 向上初始化构建, 获取数组最大值
        for (int i = n / 2 - 1; i &gt;= 0; i--) {
            heapify(nums, i, n);
        }
        // 最大值不断调整到末尾, 并对新元素 nums[0] 向下进行调整
        for (int i = n - 1; i &gt;= 0; i--) {
            swap(nums[i], nums[0]);
            heapify(nums, 0, i);    // 每次都从 0 开始调整
        }
    }

    vector&amp;#x3C;int&gt; sortArray(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        heapsort(nums);
        return nums;
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>动态规划｜子数组或子序列的乘积最大值</title><link>https://coooredump.github.io/blog/leetcode/maximum-product-of-subarray-or-subsequence</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/maximum-product-of-subarray-or-subsequence</guid><description>Maximum product of &quot;subarray&quot; or &quot;subsequence&quot;</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;字节面试原题⁉️&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;「子数组」乘积最大值：&lt;a href=&quot;https://leetcode.cn/problems/maximum-product-subarray/description/&quot;&gt;152. 乘积最大子数组&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;「子序列」乘积最大值：&lt;a href=&quot;https://leetcode.cn/problems/maximum-strength-of-a-group/description/&quot;&gt;2708. 一个小组的最大实力值&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;「三维子数组」乘积最大值：&lt;a href=&quot;https://leetcode.cn/problems/maximum-non-negative-product-in-a-matrix/&quot;&gt;1594. 矩阵的最大非负积&lt;/a&gt;（本题为「鹅厂」与「字节」面试算法题，也是「&lt;a href=&quot;https://leetcode.cn/problems/maximum-product-subarray/description/&quot;&gt;152. 乘积最大子数组&lt;/a&gt;」的升维算法题）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;「子数组」乘积最大值：&lt;a href=&quot;https://leetcode.cn/problems/maximum-product-subarray/description/&quot;&gt;152. 乘积最大子数组&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; ，请你找出数组中乘积最大的非空连续 子数组（该子数组中至少包含一个数字），并返回该子数组所对应的乘积。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int maxProduct(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        int n = nums.size();
        vector&amp;#x3C;long&gt; mx(nums.begin(), nums.end()), mn(nums.begin(), nums.end());
        for (int i = 1; i &amp;#x3C; n; i++) {
            mx[i] = max({mx[i - 1] * nums[i], (long)nums[i], mn[i - 1] * nums[i]});
            mn[i] = min({mn[i - 1] * nums[i], (long)nums[i], mx[i - 1] * nums[i]});
        }
        return *max_element(mx.begin(), mx.end());
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;「子序列」乘积最大值：&lt;a href=&quot;https://leetcode.cn/problems/maximum-strength-of-a-group/description/&quot;&gt;2708. 一个小组的最大实力值&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给你一个下标从 &lt;strong&gt;0&lt;/strong&gt; 开始的整数数组 &lt;code&gt;nums&lt;/code&gt; ，它表示一个班级中所有学生在一次考试中的成绩。老师想选出一部分同学组成一个 &lt;strong&gt;非空&lt;/strong&gt; 小组，且这个小组的 &lt;strong&gt;实力值&lt;/strong&gt; 最大，如果这个小组里的学生下标为 &lt;code&gt;i0&lt;/code&gt;, &lt;code&gt;i1&lt;/code&gt;, &lt;code&gt;i2&lt;/code&gt;, ... , &lt;code&gt;ik&lt;/code&gt; ，那么这个小组的实力值定义为 &lt;code&gt;nums[i0] * nums[i1] * nums[i2] * ... * nums[ik]&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;请你返回老师创建的小组能得到的最大实力值为多少。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：nums = [3,-1,-5,2,5,-9]
输出：1350
解释：一种构成最大实力值小组的方案是选择下标为 [0,2,3,4,5] 的学生。实力值为 3 * (-5) * 2 * 5 * (-9) = 1350 ，这是可以得到的最大实力值。
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：nums = [-4,-5,-4]
输出：20
解释：选择下标为 [0, 1] 的学生。得到的实力值为 20 。我们没法得到更大的实力值。
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    long long maxStrength(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        long long mn = nums[0], mx = mn;
        for (int i = 1; i &amp;#x3C; nums.size(); i++) {
            long long x = nums[i];
            long long tmp = mn;
            mn = min({mn, x, mn * x, mx * x});
            mx = max({mx, x, tmp * x, mx * x});
        }
        return mx;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;「三维子数组」乘积最大值：&lt;a href=&quot;https://leetcode.cn/problems/maximum-non-negative-product-in-a-matrix/&quot;&gt;1594. 矩阵的最大非负积&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给你一个大小为 &lt;code&gt;m x n&lt;/code&gt; 的矩阵 &lt;code&gt;grid&lt;/code&gt; 。最初，你位于左上角 &lt;code&gt;(0, 0)&lt;/code&gt; ，每一步，你可以在矩阵中 &lt;strong&gt;向右&lt;/strong&gt; 或 &lt;strong&gt;向下&lt;/strong&gt; 移动。&lt;/p&gt;
&lt;p&gt;在从左上角 &lt;code&gt;(0, 0)&lt;/code&gt; 开始到右下角 &lt;code&gt;(m - 1, n - 1)&lt;/code&gt; 结束的所有路径中，找出具有 &lt;strong&gt;最大非负积&lt;/strong&gt; 的路径。路径的积是沿路径访问的单元格中所有整数的乘积。&lt;/p&gt;
&lt;p&gt;返回 &lt;strong&gt;最大非负积&lt;/strong&gt; 对 &lt;strong&gt;&lt;code&gt;10^9 + 7&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;取余&lt;/strong&gt; 的结果。如果最大积为 &lt;strong&gt;负数&lt;/strong&gt; ，则返回 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：取余是在得到最大积之后执行的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504272301385.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：grid = [[-1,-2,-3],[-2,-3,-3],[-3,-3,-2]]
输出：-1
解释：从 (0, 0) 到 (2, 2) 的路径中无法得到非负积，所以返回 -1 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504272301666.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：grid = [[1,-2,1],[1,-2,1],[3,-4,1]]
输出：8
解释：最大非负积对应的路径如图所示 (1 * 1 * -2 * -4 * 1 = 8)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;✅ 本题为「鹅厂」与「字节」面试算法题，也是「&lt;a href=&quot;https://leetcode.cn/problems/maximum-product-subarray/description/&quot;&gt;152. 乘积最大子数组&lt;/a&gt;」的升维算法题&lt;/p&gt;
&lt;h3&gt;1️⃣ 三维数组&lt;/h3&gt;
&lt;p&gt;第三维度记录 min 与 max，需要单独初始化第一行和第一列。&lt;/p&gt;
&lt;p&gt;由于过程中无法确定最大值的由来，那么需要「左」和「上」的最大最小值来乘于当前值 &lt;code&gt;grid[i][j]&lt;/code&gt;，与「乘积最大子数组」思路一致。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int maxProductPath(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; grid) {
        const int MOD = 1e9 + 7;
        int m = grid.size(), n = grid[0].size();
        // [0]: min; [1]: max
        vector f(m, vector(n, vector(2, 0LL)));
        f[0][0] = {grid[0][0], grid[0][0]};
        for (int j = 1; j &amp;#x3C; n; j++) {
            f[0][j][0] = f[0][j - 1][0] * grid[0][j];
            f[0][j][1] = f[0][j - 1][1] * grid[0][j];
        }
        for (int i = 1; i &amp;#x3C; m; i++) {
            f[i][0][0] = f[i - 1][0][0] * grid[i][0];
            f[i][0][1] = f[i - 1][0][1] * grid[i][0];
        }
        for (int i = 1; i &amp;#x3C; m; i++) {
            for (int j = 1; j &amp;#x3C; n; j++) {
                int x = grid[i][j];
                f[i][j][0] = min({f[i - 1][j][0] * x, f[i - 1][j][1] * x,
                                  f[i][j - 1][0] * x, f[i][j - 1][1] * x});
                f[i][j][1] = max({f[i - 1][j][0] * x, f[i - 1][j][1] * x,
                                  f[i][j - 1][0] * x, f[i][j - 1][1] * x});
            }
        }
        return f[m - 1][n - 1][1] &gt;= 0 ? f[m - 1][n - 1][1] % MOD : -1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 两个二维数组&lt;/h3&gt;
&lt;p&gt;相当于将方法一三维数组的第三维度拆分成两个二维数组，与「乘积最大子数组」思路一致。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int maxProductPath(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; grid) {
        const int MOD = 1e9 + 7;
        int m = grid.size(), n = grid[0].size();
        vector&amp;#x3C;vector&amp;#x3C;long long&gt;&gt; mx(m, vector&amp;#x3C;long long&gt;(n));
        vector&amp;#x3C;vector&amp;#x3C;long long&gt;&gt; mn(m, vector&amp;#x3C;long long&gt;(n));
        mx[0][0] = mn[0][0] = grid[0][0];
        for (int j = 1; j &amp;#x3C; n; j++)
            mx[0][j] = mn[0][j] = mx[0][j - 1] * grid[0][j];
        for (int i = 1; i &amp;#x3C; m; i++)
            mx[i][0] = mn[i][0] = mx[i - 1][0] * grid[i][0];
        for (int i = 1; i &amp;#x3C; m; i++) {
            for (int j = 1; j &amp;#x3C; n; j++) {
                int x = grid[i][j];
                mx[i][j] = max({mx[i - 1][j] * x, mn[i - 1][j] * x,
                                mx[i][j - 1] * x, mn[i][j - 1] * x});
                mn[i][j] = min({mx[i - 1][j] * x, mn[i - 1][j] * x,
                                mx[i][j - 1] * x, mn[i][j - 1] * x});
            }
        }
        return mx[m - 1][n - 1] &gt;= 0 ? mx[m - 1][n - 1] % MOD : -1;
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>模运算</title><link>https://coooredump.github.io/blog/leetcode/modulo</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/modulo</guid><description>模运算的世界：当加减乘除遇上取模（模运算恒等式/费马小定理）</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;✅ 模运算&lt;/h2&gt;
&lt;p&gt;✅ &lt;strong&gt;更多细节&lt;/strong&gt;：&lt;a href=&quot;https://leetcode.cn/circle/discuss/mDfnkW/&quot;&gt;0x3f 分享丨模运算的世界：当加减乘除遇上取模（模运算恒等式/费马小定理）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;代码实现时，加减乘除如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;MOD = 1_000_000_007

// 加‼️
(a + b) % MOD = ((a % MOD) + (b % MOD)) % MOD

// 减
(a - b + MOD) % MOD

// 把任意整数 a 取模到 [0,MOD-1] 中，无论 a 是正是负
(a % MOD + MOD) % MOD

// 乘（注意使用 64 位整数）‼️
a * b % MOD = ((a % MOD) * (b % MOD)) % MOD

// 多个数相乘，要步步取模，防止溢出
a * b % MOD * c % MOD

// 除（MOD 是质数且 b 不是 MOD 的倍数）
a * qpow(b, MOD - 2, MOD) % MOD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;qpow&lt;/code&gt; 为&lt;strong&gt;快速幂&lt;/strong&gt;，具体请看&lt;a href=&quot;https://leetcode.cn/problems/powx-n/solution/tu-jie-yi-zhang-tu-miao-dong-kuai-su-mi-ykp3i/&quot;&gt;【图解】一张图秒懂快速幂&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：Python 内置快速幂函数 &lt;code&gt;pow(x, y, m)&lt;/code&gt; 用于计算 $x^y\ mod\ m$。特别地，除法也可以写成 &lt;code&gt;a * pow(b, -1, MOD) % MOD&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;总之，如果发现解答错误，可以检查下代码，看看是不是哪里漏掉取模了。&lt;/p&gt;
&lt;h2&gt;取模练习题｜&lt;a href=&quot;https://leetcode.cn/problems/transformed-array/&quot;&gt;3379. 转换数组&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt;，它表示一个循环数组。请你遵循以下规则创建一个大小 &lt;strong&gt;相同&lt;/strong&gt; 的新数组 &lt;code&gt;result&lt;/code&gt; ：&lt;/p&gt;
&lt;p&gt;对于每个下标 &lt;code&gt;i&lt;/code&gt;（其中 &lt;code&gt;0 &amp;#x3C;= i &amp;#x3C; nums.length&lt;/code&gt;），独立执行以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;nums[i] &gt; 0&lt;/code&gt;：从下标 &lt;code&gt;i&lt;/code&gt; 开始，向 &lt;strong&gt;右&lt;/strong&gt; 移动 &lt;code&gt;nums[i]&lt;/code&gt; 步，在循环数组中落脚的下标对应的值赋给 &lt;code&gt;result[i]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;nums[i] &amp;#x3C; 0&lt;/code&gt;：从下标 &lt;code&gt;i&lt;/code&gt; 开始，向 &lt;strong&gt;左&lt;/strong&gt; 移动 &lt;code&gt;abs(nums[i])&lt;/code&gt; 步，在循环数组中落脚的下标对应的值赋给 &lt;code&gt;result[i]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;nums[i] == 0&lt;/code&gt;：将 &lt;code&gt;nums[i]&lt;/code&gt; 的值赋给 &lt;code&gt;result[i]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;返回新数组 &lt;code&gt;result&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;**注意：**由于 &lt;code&gt;nums&lt;/code&gt; 是循环数组，向右移动超过最后一个元素时将回到开头，向左移动超过第一个元素时将回到末尾。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输入：&lt;/strong&gt; nums = [3,-2,1,1]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出：&lt;/strong&gt; [1,1,1,3]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于 &lt;code&gt;nums[0]&lt;/code&gt; 等于 3，向右移动 3 步到 &lt;code&gt;nums[3]&lt;/code&gt;，因此 &lt;code&gt;result[0]&lt;/code&gt; 为 1。&lt;/li&gt;
&lt;li&gt;对于 &lt;code&gt;nums[1]&lt;/code&gt; 等于 -2，向左移动 2 步到 &lt;code&gt;nums[3]&lt;/code&gt;，因此 &lt;code&gt;result[1]&lt;/code&gt; 为 1。&lt;/li&gt;
&lt;li&gt;对于 &lt;code&gt;nums[2]&lt;/code&gt; 等于 1，向右移动 1 步到 &lt;code&gt;nums[3]&lt;/code&gt;，因此 &lt;code&gt;result[2]&lt;/code&gt; 为 1。&lt;/li&gt;
&lt;li&gt;对于 &lt;code&gt;nums[3]&lt;/code&gt; 等于 1，向右移动 1 步到 &lt;code&gt;nums[0]&lt;/code&gt;，因此 &lt;code&gt;result[3]&lt;/code&gt; 为 3。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    vector&amp;#x3C;int&gt; constructTransformedArray(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        // 由于数组是循环数组，把下标对 n 取模
        int n = nums.size();
        vector&amp;#x3C;int&gt; result(n);
        for (int i = 0; i &amp;#x3C; n; i++) {
            result[i] = nums[((i + nums[i]) % n + n) % n];
        }
        return result;
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>单调栈</title><link>https://coooredump.github.io/blog/leetcode/monotonic-stack</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/monotonic-stack</guid><description>Monotonic Stack</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;✅ 单调栈&lt;/strong&gt;相关例题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/daily-temperatures/description/&quot;&gt;739. 每日温度&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/next-greater-element-i/description/&quot;&gt;496. 下一个更大元素 I&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/next-greater-element-ii/description/&quot;&gt;503. 下一个更大元素 II&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/largest-rectangle-in-histogram/description/&quot;&gt;84. 柱状图中最大的矩形&lt;/a&gt;｜Hard&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/trapping-rain-water/description/&quot;&gt;42. 接雨水&lt;/a&gt;｜Hard&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/trapping-rain-water-ii/description/&quot;&gt;407. 接雨水 II&lt;/a&gt;｜Hard&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/sum-of-subarray-minimums/&quot;&gt;907. 子数组的最小值之和&lt;/a&gt;｜Medium&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-score-of-a-good-subarray/&quot;&gt;1793. 好子数组的最大分数&lt;/a&gt;｜Hard&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/largest-rectangle-in-histogram/&quot;&gt;84. 柱状图中最大的矩形&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给定 &lt;em&gt;n&lt;/em&gt; 个非负整数，用来表示柱状图中各个柱子的高度。每个柱子彼此相邻，且宽度为 1 。&lt;/p&gt;
&lt;p&gt;求在该柱状图中，能够勾勒出来的矩形的最大面积。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504272306878.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：heights = [2,1,5,6,2,3]
输出：10
解释：最大的矩形为图中红色区域，面积为 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 单调栈&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int largestRectangleArea(vector&amp;#x3C;int&gt; &amp;#x26;heights) {
        int n = heights.size();
        vector&amp;#x3C;int&gt; left(n, -1);
        stack&amp;#x3C;int&gt; st;
        for (int i = 0; i &amp;#x3C; n; i++) {
            while (!st.empty() &amp;#x26;&amp;#x26; heights[i] &amp;#x3C;= heights[st.top()]) {
                st.pop();
            }
            if (!st.empty()) {
                left[i] = st.top();
            }
            st.push(i);
        }

        vector&amp;#x3C;int&gt; right(n, n);
        st = stack&amp;#x3C;int&gt;();
        for (int i = n - 1; i &gt;= 0; i--) {
            while (!st.empty() &amp;#x26;&amp;#x26; heights[i] &amp;#x3C;= heights[st.top()]) {
                st.pop();
            }
            if (!st.empty()) {
                right[i] = st.top();
            }
            st.push(i);
        }

        int ans = 0;
        for (int i = 0; i &amp;#x3C; n; i++) {
            ans = max(ans, heights[i] * (right[i] - left[i] - 1));
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/trapping-rain-water/description/&quot;&gt;42. 接雨水&lt;/a&gt;（常用「相向双指针」方法）&lt;/h2&gt;
&lt;p&gt;给定 &lt;code&gt;n&lt;/code&gt; 个非负整数表示每个宽度为 &lt;code&gt;1&lt;/code&gt; 的柱子的高度图，计算按此排列的柱子，下雨之后能接多少雨水。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504272308439.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出：6
解释：上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图，在这种情况下，可以接 6 个单位的雨水（蓝色部分表示雨水）。 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 前后缀分离&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int trap(vector&amp;#x3C;int&gt;&amp;#x26; height) {
        int n = height.size();
        vector&amp;#x3C;int&gt; pre_max(n); // pre_max[i] 表示从 height[0] 到 height[i] 的最大值
        pre_max[0] = height[0];
        for (int i = 1; i &amp;#x3C; n; i++) {
            pre_max[i] = max(pre_max[i - 1], height[i]);
        }

        vector&amp;#x3C;int&gt; suf_max(n); // suf_max[i] 表示从 height[i] 到 height[n-1] 的最大值
        suf_max[n - 1] = height[n - 1];
        for (int i = n - 2; i &gt;= 0; i--) {
            suf_max[i] = max(suf_max[i + 1], height[i]);
        }

        int ans = 0;
        for (int i = 0; i &amp;#x3C; n; i++) {
            ans += min(pre_max[i], suf_max[i]) - height[i]; // 累加每个水桶能接多少水
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 相向双指针（谁小谁移动）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;利用这个思路可以完成「3D 接雨水」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意 &lt;code&gt;while&lt;/code&gt; 循环可以不加等号，因为在「&lt;strong&gt;谁小移动谁&lt;/strong&gt;」的规则下，相遇的位置一定是最高的柱子，这个柱子是无法接水的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int trap(vector&amp;#x3C;int&gt;&amp;#x26; height) {
        int ans = 0, left = 0, right = height.size() - 1, pre_max = 0, suf_max = 0;
        while (left &amp;#x3C; right) {
            pre_max = max(pre_max, height[left]);
            suf_max = max(suf_max, height[right]);
            ans += pre_max &amp;#x3C; suf_max ? pre_max - height[left++] : suf_max - height[right--];
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3️⃣ 单调栈&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;上面的方法相当于「竖着」计算面积，单调栈的做法相当于「横着」计算面积。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个方法可以总结成 16 个字：找上一个更大元素，在找的过程中填坑。&lt;/p&gt;
&lt;p&gt;注意 while 中加了等号，这可以让栈中没有重复元素，从而在有很多重复元素的情况下，使用更少的空间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int trap(vector&amp;#x3C;int&gt;&amp;#x26; height) {
        int ans = 0;
        stack&amp;#x3C;int&gt; st;
        for (int i = 0; i &amp;#x3C; height.size(); i++) {
            while (!st.empty() &amp;#x26;&amp;#x26; height[i] &gt;= height[st.top()]) {
                int bottom_h = height[st.top()];
                st.pop();
                if (st.empty()) {
                    break;
                }
                int left = st.top();
                int dh = min(height[left], height[i]) - bottom_h; // 面积的高
                ans += dh * (i - left - 1);
            }
            st.push(i);
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/trapping-rain-water-ii/&quot;&gt;407. 接雨水 II&lt;/a&gt;（短板效应）&lt;/h2&gt;
&lt;p&gt;给你一个 &lt;code&gt;m x n&lt;/code&gt; 的矩阵，其中的值均为非负整数，代表二维高度图每个单元的高度，请计算图中形状最多能接多少体积的雨水。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504272313358.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入: heightMap = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]]
输出: 4
解释: 下雨后，雨水将会被上图蓝色的方块中。总的接雨水量为1+2+1=4。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504272313432.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入: heightMap = [[3,3,3,3,3],[3,2,2,2,3],[3,2,1,2,3],[3,2,2,2,3],[3,3,3,3,3]]
输出: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 优先队列 priority_queue 维护短板&lt;/h3&gt;
&lt;p&gt;哪个格子的接水量，在一开始就能确定？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最外面一圈的格子是无法接水的。&lt;/li&gt;
&lt;li&gt;假设 (0,1) 的高度是最外面一圈的格子中最小的，且高度等于 5，那么和它相邻的 (1,1)，我们能知道：
&lt;ul&gt;
&lt;li&gt;(1,1) 的水位不能超过 5，否则水会从 (0,1) 流出去。&lt;/li&gt;
&lt;li&gt;(1,1) 的水位一定可以等于 5，这是因为 (0,1) 的高度是最外面一圈的格子中最小的，(1,1) 的水不可能从其他地方流出去。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们从最外面一圈的格子开始。想象成一个木桶，最外面一圈格子的高度视作木板的高度。&lt;/p&gt;
&lt;p&gt;接着上面的讨论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 (1,1) 的高度 ≥ 5，那么 (0,1) 这块木板就没用了，我们去掉 (0,1) 这块木板，改用 (1,1) 这块木板。&lt;/li&gt;
&lt;li&gt;如果 (1,1) 的高度 &amp;#x3C; 5，假设我们接的不是水，是水泥。那么把 (1,1) 的高度填充为 5，仍然可以去掉 (0,1) 这块木板，改用 (1,1) 这块（填充水泥后）高为 5 的木板水泥板。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;继续，从当前木板中，找到一根最短的木板。假设 (1,1) 是当前所有木板中最短的，那么其邻居 (1,2) 和 (2,1) 的水位就是 (1,1) 的高度，因为超过 (1,1) 高度的水会流出去。然后，去掉 (1,1) 这块木板，改用 (1,2) 和 (2,1) 这两块木板。依此类推。&lt;/p&gt;
&lt;p&gt;由于每次都要找最短的木板，所以用一个最小堆维护木板的高度。按照上述做法，不断循环，直到堆为空。&lt;/p&gt;
&lt;p&gt;为方便实现，代码在初始化堆的时候，直接遍历了整个矩阵。&lt;/p&gt;
&lt;p&gt;「&lt;a href=&quot;https://leetcode.cn/problems/trapping-rain-water/&quot;&gt;42. 接雨水&lt;/a&gt;」那题需要维护左右两个指针，本题相当于维护了“一圈”指针。42 那题每次取左右最小的指针，然后移动到相邻位置上；本题也是取最小的指针（出堆），往周围的邻居移动（入堆）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
    static constexpr int DIRS[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};

public:
    int trapRainWater(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; heightMap) {
        int m = heightMap.size(), n = heightMap[0].size();
        priority_queue&amp;#x3C;tuple&amp;#x3C;int, int, int&gt;, vector&amp;#x3C;tuple&amp;#x3C;int, int, int&gt;&gt;, greater&amp;#x3C;&gt;&gt; pq;
        for (int i = 0; i &amp;#x3C; m; i++) {
            for (int j = 0; j &amp;#x3C; n; j++) {
                if (i == 0 || i == m - 1 || j == 0 || j == n - 1) {
                    pq.push({heightMap[i][j], i, j});
                    heightMap[i][j] = -1;
                }
            }
        }
        int ans = 0;
        while (!pq.empty()) {
            auto [min_height, i, j] = pq.top();
            pq.pop();
            for (auto&amp;#x26; [dx, dy] : DIRS) {
                int x = i + dx;
                int y = j + dy;
                if (x &gt;= 0 &amp;#x26;&amp;#x26; y &gt;= 0 &amp;#x26;&amp;#x26; x &amp;#x3C; m &amp;#x26;&amp;#x26; y &amp;#x3C; n &amp;#x26;&amp;#x26; heightMap[x][y] != -1) {
                    ans += max(min_height - heightMap[x][y], 0);
                    pq.push({max(min_height, heightMap[x][y]), x, y});
                    heightMap[x][y] = -1;
                }
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/sum-of-subarray-minimums/&quot;&gt;907. 子数组的最小值之和&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给定一个整数数组 &lt;code&gt;arr&lt;/code&gt;，找到 &lt;code&gt;min(b)&lt;/code&gt; 的总和，其中 &lt;code&gt;b&lt;/code&gt; 的范围为 &lt;code&gt;arr&lt;/code&gt; 的每个（连续）子数组。&lt;/p&gt;
&lt;p&gt;由于答案可能很大，因此 &lt;strong&gt;返回答案模 &lt;code&gt;10^9 + 7&lt;/code&gt;&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：arr = [3,1,2,4]
输出：17
解释：
子数组为 [3]，[1]，[2]，[4]，[3,1]，[1,2]，[2,4]，[3,1,2]，[1,2,4]，[3,1,2,4]。 
最小值为 3，1，2，4，1，1，2，1，1，1，和为 17。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 单调栈&lt;/h3&gt;
&lt;p&gt;解法等价于「84. 柱状图中最大的矩形」，本题计算以 &lt;code&gt;arr[i]&lt;/code&gt; 为最小值的子数组的个数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
    const int MOD = 1e9 + 7;

public:
    // 类似题目: 84. 柱状图中最大的矩形
    int sumSubarrayMins(vector&amp;#x3C;int&gt;&amp;#x26; arr) {
        int n = arr.size();
        // 左边界 left[i] 为左侧严格小于 arr[i] 的最近元素位置（不存在时为 -1）
        vector&amp;#x3C;int&gt; left(n, -1);
        stack&amp;#x3C;int&gt; st;
        for (int i = 0; i &amp;#x3C; n; i++) {
            while (!st.empty() &amp;#x26;&amp;#x26; arr[st.top()] &gt;= arr[i])
                st.pop();
            if (!st.empty())
                left[i] = st.top();
            st.push(i);
        }

        st = stack&amp;#x3C;int&gt;();
        // 右侧找 &amp;#x3C;= 是为了避免重复统计
        // 右边界 right[i] 为右侧小于等于 arr[i] 的最近元素位置（不存在时为 n）
        vector&amp;#x3C;int&gt; right(n, n);
        for (int i = n - 1; i &gt;= 0; i--) {
            while (!st.empty() &amp;#x26;&amp;#x26; arr[st.top()] &gt; arr[i])
                st.pop();
            if (!st.empty())
                right[i] = st.top();
            st.push(i);
        }
        long ans = 0l;
        for (int i = 0; i &amp;#x3C; n; i++) {
            ans += (long)arr[i] * (i - left[i]) * (right[i] - i);
        }
        return ans % MOD;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-score-of-a-good-subarray/&quot;&gt;1793. 好子数组的最大分数&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; **（下标从 0 开始）**和一个整数 &lt;code&gt;k&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;一个子数组 &lt;code&gt;(i, j)&lt;/code&gt; 的 &lt;strong&gt;分数&lt;/strong&gt; 定义为 &lt;code&gt;min(nums[i], nums[i+1], ..., nums[j]) * (j - i + 1)&lt;/code&gt; 。一个 &lt;strong&gt;好&lt;/strong&gt; 子数组的两个端点下标需要满足 &lt;code&gt;i &amp;#x3C;= k &amp;#x3C;= j&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;请你返回 &lt;strong&gt;好&lt;/strong&gt; 子数组的最大可能 &lt;strong&gt;分数&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：nums = [1,4,3,7,4,5], k = 3
输出：15
解释：最优子数组的左右端点下标是 (1, 5) ，分数为 min(4,3,7,4,5) * (5-1+1) = 3 * 5 = 15 。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 背向双指针&lt;/h3&gt;
&lt;p&gt;我们尝试从 $i=k, j=k$ 出发，通过不断移动指针来找到最大矩形。比较 &lt;code&gt;nums[i−1]&lt;/code&gt; 和 &lt;code&gt;nums[j+1]&lt;/code&gt; 的大小，&lt;strong&gt;谁大就移动谁&lt;/strong&gt;（一样大移动哪个都可以）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int maximumScore(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
        int max_score = nums[k], n = nums.size(), mn = nums[k];
        int l = k, r = k;
        for (int t = 1; t &amp;#x3C; n; t++) {
            if (r == n - 1 || (l &amp;#x26;&amp;#x26; nums[l - 1] &gt; nums[r + 1])) {
                mn = min(mn, nums[--l]);
            } else {
                mn = min(mn, nums[++r]);
            }
            max_score = max(max_score, mn * (r - l + 1));
        }
        return max_score;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 单调栈&lt;/h3&gt;
&lt;p&gt;本题要计算的分数，和「&lt;a href=&quot;https://leetcode.cn/problems/largest-rectangle-in-histogram/description/&quot;&gt;84. 柱状图中最大的矩形&lt;/a&gt;」是一样的，计算的是最大矩形面积，&lt;strong&gt;只不过多了一个约束：矩形必须包含下标 &lt;code&gt;k&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int maximumScore(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
        int n = nums.size();
        vector&amp;#x3C;int&gt; left(n), right(n);
        stack&amp;#x3C;int&gt; st;
        for (int i = 0; i &amp;#x3C; n; i++) {
            while (!st.empty() &amp;#x26;&amp;#x26; nums[st.top()] &gt;= nums[i]) {
                st.pop();
            }
            left[i] = st.empty() ? -1 : st.top();
            st.push(i);
        }
        st = stack&amp;#x3C;int&gt;();
        for (int i = n - 1; i &gt;= 0; i--) {
            while (!st.empty() &amp;#x26;&amp;#x26; nums[st.top()] &gt;= nums[i]) {
                st.pop();
            }
            right[i] = st.empty() ? n : st.top();
            st.push(i);
        }
        int ans = 0;
        for (int i = 0; i &amp;#x3C; n; i++) {
            // 分数的定义其实就是矩形面积: 同「84. 柱状图中最大的矩形」
            int score = nums[i] * (right[i] - left[i] - 1);
            // 仅仅加一个判断条件即可
            if (left[i] &amp;#x3C; k &amp;#x26;&amp;#x26; right[i] &gt; k)
                ans = max(ans, score);
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>快速幂</title><link>https://coooredump.github.io/blog/leetcode/qpow</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/qpow</guid><description>qpow</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;✅ 快速幂&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;快速幂相关题目&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/double-modular-exponentiation/&quot;&gt;2961. 双模幂运算&lt;/a&gt; [1451]&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/count-collisions-of-monkeys-on-a-polygon/&quot;&gt;2550. 猴子碰撞的方法数&lt;/a&gt; [1663]&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/super-pow/&quot;&gt;372. 超级次方&lt;/a&gt; [算术评级 5]&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/powx-n/&quot;&gt;50. Pow(x, n)&lt;/a&gt; [算术评级 5]&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# python 自带快速幂库函数用于计算 x^y mod m
pow(x, y, m)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CPP 手撕「快速幂」库函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 本题 mod 很小，即使平方也不会超过 int 范围，所以不需要用 long long
int pow(int x, int n, int mod) {
    // long long res
    int res = 1;
    while (n) {
        if (n &amp;#x26; 1) {
            res = res * x % mod;
        }
        x = x * x % mod;
        n &gt;&gt;= 1;
    }
    return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;快速幂实现思路（workflow）如下图：&lt;/p&gt;
&lt;p&gt;代码实现时，注意 &lt;em&gt;n&lt;/em&gt;=−$2^{31}$ 的情况，取反后 &lt;em&gt;n&lt;/em&gt;=$2^{31}$ 超出 int 最大值。可以转成 64 位 int 解决（即 &lt;code&gt;long long&lt;/code&gt;）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202409021559758.png&quot; alt=&quot;LC50-1.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/powx-n/&quot;&gt;50. Pow(x, n)&lt;/a&gt;（包含负数与浮点数）&lt;/h2&gt;
&lt;p&gt;实现 &lt;a href=&quot;https://www.cplusplus.com/reference/valarray/pow/&quot;&gt;pow(&lt;em&gt;x&lt;/em&gt;, &lt;em&gt;n&lt;/em&gt;)&lt;/a&gt; ，即计算 &lt;code&gt;x&lt;/code&gt; 的整数 &lt;code&gt;n&lt;/code&gt; 次幂函数（即，$x^n$ ）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：x = 2.00000, n = 10
输出：1024.00000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：x = 2.10000, n = 3
输出：9.26100
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 快速幂（模板代码）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    double myPow(double x, int N) {
        double ans = 1;
        long long n = N;
        if (n &amp;#x3C; 0) { // x^-n = (1/x)^n
            n = -n;
            x = 1 / x;
        }
        while (n) { // 从低到高枚举 n 的每个比特位
            if (n &amp;#x26; 1) { // 这个比特位是 1
                ans *= x; // 把 x 乘到 ans 中
            }
            x *= x; // x 自身平方
            n &gt;&gt;= 1; // 继续枚举下一个比特位
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/super-pow/&quot;&gt;372. 超级次方&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;你的任务是计算 $a^b$ 对 &lt;code&gt;1337&lt;/code&gt; 取模，&lt;code&gt;a&lt;/code&gt; 是一个正整数，&lt;code&gt;b&lt;/code&gt; 是一个非常大的正整数且会以数组形式给出。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：a = 2, b = [3]
输出：8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：a = 2, b = [1,0]
输出：1024
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ DFS + 快速幂&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int MOD = 1337;

    int qpow(int x, int n) {
        int res = 1;
        // due to dfs()
        x %= MOD;
        while (n) {
            if (n &amp;#x26; 1) {
                res = res * x % MOD;
            }
            x = x * x % MOD;
            n &gt;&gt;= 1;
        }
        return res;
    }

    int dfs(int a, vector&amp;#x3C;int&gt;&amp;#x26; b, int idx) {
        if (idx == -1)
            return 1;
        return qpow(a, b[idx]) * qpow(dfs(a, b, idx - 1), 10) % MOD;
    }

    int superPow(int a, vector&amp;#x3C;int&gt;&amp;#x26; b) {
        return dfs(a, b, b.size() - 1);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/count-collisions-of-monkeys-on-a-polygon/&quot;&gt;2550. 猴子碰撞的方法数&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;现在有一个正凸多边形，其上共有 &lt;code&gt;n&lt;/code&gt; 个顶点。顶点按顺时针方向从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;n - 1&lt;/code&gt; 依次编号。每个顶点上 &lt;strong&gt;正好有一只猴子&lt;/strong&gt; 。下图中是一个 6 个顶点的凸多边形。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504272354393.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;每个猴子同时移动到相邻的顶点。顶点 &lt;code&gt;i&lt;/code&gt; 的相邻顶点可以是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;顺时针方向的顶点 &lt;code&gt;(i + 1) % n&lt;/code&gt; ，或&lt;/li&gt;
&lt;li&gt;逆时针方向的顶点 &lt;code&gt;(i - 1 + n) % n&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果移动后至少有两只猴子停留在同一个顶点上或者相交在一条边上，则会发生 &lt;strong&gt;碰撞&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;返回猴子至少发生 &lt;strong&gt;一次碰撞&lt;/strong&gt; 的移动方法数。由于答案可能非常大，请返回对 &lt;code&gt;109+7&lt;/code&gt; 取余后的结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;，每只猴子只能移动一次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：n = 3
输出：6
解释：共计 8 种移动方式。
下面列出两种会发生碰撞的方式：
- 猴子 1 顺时针移动；猴子 2 逆时针移动；猴子 3 顺时针移动。猴子 1 和猴子 2 碰撞。
- 猴子 1 逆时针移动；猴子 2 逆时针移动；猴子 3 顺时针移动。猴子 1 和猴子 3 碰撞。
可以证明，有 6 种让猴子碰撞的方法。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 正难则反｜快速幂&lt;/h3&gt;
&lt;p&gt;只有&lt;strong&gt;全部顺时针&lt;/strong&gt;和&lt;strong&gt;全部逆时针&lt;/strong&gt;这 2 种不会碰撞，所以只需要计算 $2^n-2$，这就需要用到&lt;strong&gt;快速幂&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C++：手搓快速幂&lt;/li&gt;
&lt;li&gt;Python：使用内置 &lt;code&gt;pow(x, n, mod)&lt;/code&gt; 快速幂库函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int MOD = 1e9 + 7;

    int qpow(long long x, int n) {
        int res = 1;
        while (n) {
            if (n &amp;#x26; 1) {
                res = ((res % MOD) * (x % MOD)) % MOD;
            }
            x = ((x % MOD) * (x % MOD)) % MOD;
            n &gt;&gt;= 1;
        }
        return res;
    }

    int monkeyMove(int n) {
        int ans = qpow(2, n);
        return (ans - 2 + MOD) % MOD;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Solution:
    def monkeyMove(self, n: int) -&gt; int:
        MOD = 1_000_000_007
        return (pow(2, n, MOD) - 2) % MOD
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>质数筛法（暴力、埃式筛、欧拉筛）</title><link>https://coooredump.github.io/blog/leetcode/sieve-of-prime-number</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/sieve-of-prime-number</guid><description>Sieve of Prime Number</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;如何判断一个数是不是&lt;strong&gt;质数&lt;/strong&gt;，现在求区间 $[1,1e7]$ 内所有质数，学习「埃式筛法」和「欧拉筛法」之前，先介绍下暴力筛选。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可借此题验证下：&lt;a href=&quot;https://leetcode.cn/problems/count-primes/&quot;&gt;204. 计数质数&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 暴力筛选&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;0&lt;/code&gt; 表示质数，&lt;code&gt;1&lt;/code&gt; 表示合数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;static final int N = 1e7 + 5;
int st[N]; // 初始化为0，0表示质数，1表示合数

for(int i = 2; i &amp;#x3C;= n; i++){
	for(int j = 2; j * j &amp;#x3C;= i; j++){ //试除法
		if(i % j == 0){
			st[i] = 1; // 合数，标记为1
            break;
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 埃式筛法&lt;/h2&gt;
&lt;p&gt;这种方法无疑是最慢的，换一种思路：&lt;strong&gt;一个质数的倍数一定是合数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以假设 P 是质数，我们可以筛选掉区间 $[1,1e7]$ 中所有 P 的倍数。&lt;/p&gt;
&lt;p&gt;为什么这样能筛去所有的合数呢，因为一个合数一定能被分解为几个质数的幂的乘积，并且这个数的&lt;strong&gt;质因子一定是小于它本身的&lt;/strong&gt;，所以当我们从&lt;strong&gt;小到大将每个质数的倍数都筛去的话&lt;/strong&gt;，当&lt;strong&gt;遍历到一个合数时，它一定已经被它的质因子给筛去了&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;

const int N = 1e7 + 5;
int st[N]; // st[i] == 1 表示 i 是合数；0 表示 i 是素数

void E_sieve(int n) {
    for (int i = 2; i &amp;#x3C;= n; i++) {
        if (st[i] == 0) {
            for (int j = 2 * i; j &amp;#x3C;= n; j += i) {
                st[j] = 1; // j 是 i 的倍数，是合数，标记为 1
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们还可以对其进行优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们会先筛 2 的所有倍数，然后筛 3 的所有倍数，但筛除3的倍数时，我们还是从 3 的 2 倍开始筛，其实 $3 * 2$ ，已经被 $2 * 3$ 时筛过了。又比如说筛 5 的倍数时，我们从 5 的 2 倍开始筛，但是 $5 * 2$ 会先被 $2 * 5$ 筛去，$5 * 3$ 会先被 $3 * 5$ 筛去，$5 * 4$ 会先被 $2 * 10$ 筛去，所以我们每一次只需要从 $i * i$ 开始筛，因为 $(2，3,…,i - 1)$ 倍已经被筛过了。&lt;/li&gt;
&lt;li&gt;另外，判断一个数 n 是不是质数，我们只需要判断 $[2,\sqrt{n}]$ 内有没有它的因子，在筛选合数时，我们也可以这样做，因为一个合数的最小质因子一定小于等于 $\sqrt{n}$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优化后的埃式筛法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

const int N = 1e7 + 5;
int st[N]; // st[i] == 1 表示 i 是合数，0 表示 i 是素数

void E_sieve(int n) {
    for (int i = 2; i &amp;#x3C;= n / i; i++) {
        if (st[i] == 0) {
            for (int j = i * i; j &amp;#x3C;= n; j += i) {
                st[j] = 1; // j 是 i 的倍数，标记为合数
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度可以近似看成 $O(n)$&lt;/p&gt;
&lt;p&gt;但是我们还可以更快，那就是欧拉筛，又称为线性筛。&lt;/p&gt;
&lt;h2&gt;3. 欧拉筛法/线性筛法&lt;/h2&gt;
&lt;p&gt;欧拉筛的核心思想就是确保每个合数只被最小质因数筛掉，或者说被合数的最大因子筛掉。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;比如 $120 = 2^3 * 3 * 5$，120 会被 2 筛一次，3 筛一次，5 筛一次。&lt;/p&gt;
&lt;p&gt;多做了两次不必要的操作，如何确保 120 只 2 筛选掉。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;时间复杂度：$O(n)$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

const int N = 1e7 + 5;
int st[N];           // st[i] == 1 表示 i 是合数
int primes[N];       // 存所有质数
int cnt = 0;         // 质数的个数

void ola(int n) {
    for (int i = 2; i &amp;#x3C;= n; i++) {
        if (st[i] == 0) {
            primes[cnt++] = i; // i 是质数，加入 primes 数组
        }
        for (int j = 0; j &amp;#x3C; cnt &amp;#x26;&amp;#x26; primes[j] &amp;#x3C;= n / i; j++) {
            st[primes[j] * i] = 1; 	// 标记合数
            if (i % primes[j] == 0) // 保证每个合数只被它的最小质因子筛一次
                break;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>排序数组（堆排、快排、归排）</title><link>https://coooredump.github.io/blog/leetcode/sort-algorithm</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/sort-algorithm</guid><description>解构三个经典排序算法：Heap Sort &amp; Quick Sort &amp; Merge Sort</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1️⃣ 堆排序&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    // 向下调整
    void heapify(vector&amp;#x3C;int&gt;&amp;#x26; nums, int i, int n) {
        int largest = i;
        int left = i * 2 + 1;
        int right = i * 2 + 2;
        if (left &amp;#x3C; n &amp;#x26;&amp;#x26; nums[left] &gt; nums[largest])
            largest = left;
        if (right &amp;#x3C; n &amp;#x26;&amp;#x26; nums[right] &gt; nums[largest])
            largest = right;
        if (largest != i) {
            swap(nums[largest], nums[i]);
            heapify(nums, largest, n);
        }
    }

    void heapsort(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        int n = nums.size();
        // i = n / 2 也可, 多判断一次而已
        // 向上初始化构建, 获取数组最大值
        for (int i = n / 2 - 1; i &gt;= 0; i--) {
            heapify(nums, i, n);
        }
        // 最大值不断调整到末尾, 并对新元素 nums[0] 向下进行调整
        for (int i = n - 1; i &gt;= 0; i--) {
            swap(nums[i], nums[0]);
            heapify(nums, 0, i);    // 每次都从 0 开始调整
        }
    }

    vector&amp;#x3C;int&gt; sortArray(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        heapsort(nums);
        return nums;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2️⃣ 快速排序（朴素版）&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int partition(vector&amp;#x3C;int&gt;&amp;#x26; nums, int l, int r) {
        int pivot = nums[l];
        int i = l, j = r + 1;
        while (i &amp;#x3C; j) {
            // while (i &amp;#x3C; r &amp;#x26;&amp;#x26; nums[++i] &amp;#x3C; pivot) 也可
            while (++i &amp;#x3C; r &amp;#x26;&amp;#x26; nums[i] &amp;#x3C; pivot)
                ;
            // while (j &gt; l &amp;#x26;&amp;#x26; nums[--j] &gt; pivot) 也可
            while (--j &gt; l &amp;#x26;&amp;#x26; nums[j] &gt; pivot)
                ;
            if (i &amp;#x3C; j) {
                swap(nums[i], nums[j]);
            }
        }
        swap(nums[l], nums[j]);
        return j;
    }

    void quicksort(vector&amp;#x3C;int&gt;&amp;#x26; nums, int l, int r) {
        if (l &amp;#x3C; r) {
            int pivot = partition(nums, l, r);
            quicksort(nums, l, pivot - 1);
            quicksort(nums, pivot + 1, r);
        }
    }

    vector&amp;#x3C;int&gt; sortArray(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        quicksort(nums, 0, nums.size() - 1);
        return nums;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3️⃣ 快速排序（随机版 · 性能🔝）&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int partition(vector&amp;#x3C;int&gt;&amp;#x26; nums, int l, int r) {
        int pivot = nums[l];
        int i = l, j = r + 1;
        while (i &amp;#x3C; j) {
            while (++i &amp;#x3C; r &amp;#x26;&amp;#x26; nums[i] &amp;#x3C; pivot)
                ;
            while (--j &gt; l &amp;#x26;&amp;#x26; nums[j] &gt; pivot)
                ;
            if (i &amp;#x3C; j) {
                swap(nums[i], nums[j]);
            }
        }
        swap(nums[l], nums[j]);
        return j;
    }

    int randomized_partition(vector&amp;#x3C;int&gt;&amp;#x26; nums, int l, int r) {
        int i = l + rand() % (r - l + 1);
        swap(nums[i], nums[l]);
        return partition(nums, l, r);
    }

    void quicksort(vector&amp;#x3C;int&gt;&amp;#x26; nums, int l, int r) {
        if (l &amp;#x3C; r) {
            int pivot = randomized_partition(nums, l, r);
            quicksort(nums, l, pivot - 1);
            quicksort(nums, pivot + 1, r);
        }
    }

    vector&amp;#x3C;int&gt; sortArray(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        quicksort(nums, 0, nums.size() - 1);
        return nums;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4️⃣ 归并排序&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    void merge(vector&amp;#x3C;int&gt;&amp;#x26; nums, int l, int m, int r) {
        int i = l, j = m + 1, k = 0;
        vector&amp;#x3C;int&gt; temp(r - l + 1);
        while (i &amp;#x3C;= m || j &amp;#x3C;= r) {
            if (i &gt; m) {
                temp[k++] = nums[j++];
            } else if (j &gt; r) {
                temp[k++] = nums[i++];
            } else if (nums[i] &amp;#x3C; nums[j]) {
                temp[k++] = nums[i++];
            } else {
                temp[k++] = nums[j++];
            }
        }
        for (int idx = 0; idx &amp;#x3C; (r - l + 1); idx++) {
            nums[l + idx] = temp[idx];
        }
    }

    void mergesort(vector&amp;#x3C;int&gt;&amp;#x26; nums, int l, int r) {
        if (l &gt;= r)
            return;
        int m = (l + r) / 2;
        mergesort(nums, l, m);
        mergesort(nums, m + 1, r);
        merge(nums, l, m, r);
    }

    vector&amp;#x3C;int&gt; sortArray(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        mergesort(nums, 0, nums.size() - 1);
        return nums;
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>腾讯 wxg 一面｜354. 俄罗斯套娃信封问题</title><link>https://coooredump.github.io/blog/leetcode/tecent-wxg-russian-doll-envelope</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/tecent-wxg-russian-doll-envelope</guid><description>腾讯 wxg 测开一面，A 了 3 题还是给我挂了！</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;腾讯 WXG 一面｜&lt;a href=&quot;https://leetcode.cn/problems/russian-doll-envelopes/&quot;&gt;354. 俄罗斯套娃信封问题&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给你一个二维整数数组 &lt;code&gt;envelopes&lt;/code&gt; ，其中 &lt;code&gt;envelopes[i] = [wi, hi]&lt;/code&gt; ，表示第 &lt;code&gt;i&lt;/code&gt; 个信封的宽度和高度。&lt;/p&gt;
&lt;p&gt;当另一个信封的宽度和高度都比这个信封大的时候，这个信封就可以放进另一个信封里，如同俄罗斯套娃一样。&lt;/p&gt;
&lt;p&gt;请计算 &lt;strong&gt;最多能有多少个&lt;/strong&gt; 信封能组成一组“俄罗斯套娃”信封（即可以把一个信封放到另一个信封里面）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：不允许旋转信封。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出：3
解释：最多信封的个数为 3, 组合为: [2,3] =&gt; [5,4] =&gt; [6,7]。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入：envelopes = [[1,1],[1,1],[1,1]]
输出：1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;#x3C;= envelopes.length &amp;#x3C;= 10^5&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;envelopes[i].length == 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;#x3C;= wi, hi &amp;#x3C;= 10^5&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;0️⃣ 前置题目&lt;/h3&gt;
&lt;p&gt;✅ &lt;a href=&quot;https://leetcode.cn/problems/longest-increasing-subsequence/&quot;&gt;300. 最长递增子序列&lt;/a&gt;｜DP / 二分&lt;/p&gt;
&lt;h3&gt;1️⃣ LIS · 动态规划（超时 TLE）&lt;/h3&gt;
&lt;p&gt;时间复杂度 $O(n^2)$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int maxEnvelopes(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; envelopes) {
        int n = envelopes.size();
        vector&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; arr(n);
        for (int i = 0; i &amp;#x3C; n; i++)
            arr[i] = {envelopes[i][0], envelopes[i][1]};
        ranges::sort(arr);

        vector&amp;#x3C;int&gt; f(n, 1);
        for (int i = 0; i &amp;#x3C; n; i++) {
            for (int j = 0; j &amp;#x3C; i; j++) {
                if (arr[i].first &gt; arr[j].first &amp;#x26;&amp;#x26; arr[i].second &gt; arr[j].second) {
                    f[i] = max(f[i], f[j] + 1);
                }
            }
        }
        return ranges::max(f);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣ 贪心 + 二分查找&lt;/h3&gt;
&lt;p&gt;时间复杂度：$O(n·logn)$&lt;/p&gt;
&lt;p&gt;先排序，再按照 LIS 二分贪心模板求最长递增子序列。因为二者都必须是递增的，所以第二维度需要逆序排序，使得第一维度相同的多个数，最后一个插入的一定是最小值，这样能嵌套的信封最多。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int maxEnvelopes(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; envelopes) {
        sort(envelopes.begin(), envelopes.end(), [](auto&amp;#x26; a, auto&amp;#x26; b) {
            return a[0] &amp;#x3C; b[0] || (a[0] == b[0] &amp;#x26;&amp;#x26; a[1] &gt; b[1]);
        });
        vector&amp;#x3C;int&gt; g;
        for (auto&amp;#x26; e : envelopes) {
            auto it = lower_bound(g.begin(), g.end(), e[1]);
            if (it == g.end()) {
                g.push_back(e[1]);
            } else {
                *it = e[1];
            }
        }
        return g.size();
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>并查集</title><link>https://coooredump.github.io/blog/leetcode/union-search</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/union-search</guid><description>并查集是一种用于管理元素所属集合的数据结构，实现为一个森林，其中每棵树表示一个集合，树中的节点表示对应集合中的元素。</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;✅ 并查集&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504272343302.svg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;相关例题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-if-path-exists-in-graph/&quot;&gt;1971. 寻找图中是否存在路径&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/redundant-connection/&quot;&gt;684. 冗余连接&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/redundant-connection-ii/&quot;&gt;685. 冗余连接 II&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并查集是一种用于管理元素所属集合的数据结构，实现为一个森林，其中每棵树表示一个集合，树中的节点表示对应集合中的元素。&lt;/p&gt;
&lt;p&gt;顾名思义，并查集支持两种操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;合并（Union）：合并两个元素所属集合（合并对应的树）&lt;/li&gt;
&lt;li&gt;查询（Find）：查询某个元素所属集合（查询对应的树的根节点），这可以用于判断两个元素是否属于同一集合&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并查集在经过修改后可以支持单个元素的删除、移动；使用动态开点线段树还可以实现可持久化并查集。&lt;/p&gt;
&lt;h3&gt;模板代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;vector&amp;#x3C;int&gt; p(n);

iota(p.begin(), p.end(), 0);

vector&amp;#x3C;int&gt; size(n, 1);

int find(int x) {
    if (p[x] != x) {
        // 路径压缩
        p[x] = find(p[x]);
    }
    return p[x];
}

void unite(int a, int b) {
    int pa = find(a), pb = find(b);
    if (pa == pb) return;
    p[pa] = pb;
    size[pb] += size[pa];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://leetcode.cn/problems/redundant-connection/&quot;&gt;684. 冗余连接&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;树可以看成是一个连通且 &lt;strong&gt;无环&lt;/strong&gt; 的 &lt;strong&gt;无向&lt;/strong&gt; 图。&lt;/p&gt;
&lt;p&gt;给定往一棵 &lt;code&gt;n&lt;/code&gt; 个节点 (节点值 &lt;code&gt;1～n&lt;/code&gt;) 的树中添加一条边后的图。添加的边的两个顶点包含在 &lt;code&gt;1&lt;/code&gt; 到 &lt;code&gt;n&lt;/code&gt; 中间，且这条附加的边不属于树中已存在的边。图的信息记录于长度为 &lt;code&gt;n&lt;/code&gt; 的二维数组 &lt;code&gt;edges&lt;/code&gt; ，&lt;code&gt;edges[i] = [ai, bi]&lt;/code&gt; 表示图中在 &lt;code&gt;ai&lt;/code&gt; 和 &lt;code&gt;bi&lt;/code&gt; 之间存在一条边。&lt;/p&gt;
&lt;p&gt;请找出一条可以删去的边，删除后可使得剩余部分是一个有着 &lt;code&gt;n&lt;/code&gt; 个节点的树。如果有多个答案，则返回数组 &lt;code&gt;edges&lt;/code&gt; 中最后出现的那个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504272343711.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入: edges = [[1,2], [1,3], [2,3]]
输出: [2,3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504272343560.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;输入: edges = [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1️⃣ 并查集&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    vector&amp;#x3C;int&gt; p;

    void init(int n) {
        p.resize(n + 1);
        iota(p.begin(), p.end(), 0);
    }

    int find(int x) {
        if (x != p[x])
            p[x] = find(p[x]); // 路经压缩
        return p[x];
    }

    bool isSame(int u, int v) {
        int pu = find(u);
        int pv = find(v);
        return pu == pv;
    }

    void join(int u, int v) {
        int pu = find(u);
        int pv = find(v);
        p[pu] = pv;
    }

    vector&amp;#x3C;int&gt; findRedundantConnection(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; edges) {
        init(edges.size());
        for (auto e : edges) {
            int u = e[0], v = e[1];
            if (isSame(u, v)) {
                return e;
            } else {
                join(u, v);
            }
        }
        return {};
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>C++ ACM 模式</title><link>https://coooredump.github.io/blog/recruitment/cpp-acm-input-output</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/cpp-acm-input-output</guid><description>归纳 C++ 的 ACM 输入输出常见场景与各数据类型的构造方式，常见于「面试手撕」与「笔试」</description><pubDate>Sun, 27 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;✅ 华为校招机考备考题单：https://rttxvuqg3f.feishu.cn/docx/PcBHdy3GsoIhxnx1RqucVqRdnAf&lt;/p&gt;
&lt;h2&gt;C++ ACM 模式输入输出&lt;/h2&gt;
&lt;h3&gt;1. 输入输出的相关库函数&lt;/h3&gt;
&lt;h4&gt;1️⃣ 输出格式化（精度）&lt;/h4&gt;
&lt;p&gt;C++ 提供了多种方式来控制输入输出的格式，常用的包括 &lt;code&gt;std::setw&lt;/code&gt;、&lt;code&gt;std::setprecision&lt;/code&gt;、&lt;code&gt;std::fixed&lt;/code&gt; 等。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;iomanip&gt;  // 提供格式化工具

int main() {
    double pi = 3.14159265358979;
    
    std::cout &amp;#x3C;&amp;#x3C; &quot;原始值: &quot; &amp;#x3C;&amp;#x3C; pi &amp;#x3C;&amp;#x3C; std::endl;

    // 设置输出精度为 2 位小数
    std::cout &amp;#x3C;&amp;#x3C; &quot;保留两位小数: &quot; &amp;#x3C;&amp;#x3C; std::fixed &amp;#x3C;&amp;#x3C; std::setprecision(2) &amp;#x3C;&amp;#x3C; pi &amp;#x3C;&amp;#x3C; std::endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2️⃣ cin&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;cin&lt;/code&gt; 是标准输入流对象，通常用于从用户那里读取数据。当我们用 &lt;code&gt;while (cin)&lt;/code&gt; 来读取输入时，它的工作原理是不断检查输入流是否有效。如果用户输入了数据并且没有遇到错误或者文件结束标志（例如 &lt;code&gt;Ctrl+Z&lt;/code&gt; 或 &lt;code&gt;Ctrl+D&lt;/code&gt; 表示 EOF），那么 &lt;code&gt;cin&lt;/code&gt; 就会继续读取并进入循环。&lt;/p&gt;
&lt;p&gt;注意，&lt;code&gt;cin &gt;&gt; val&lt;/code&gt; 会一直从 &lt;strong&gt;标准输入流&lt;/strong&gt; 中读取数据，&lt;strong&gt;以空白字符为分隔符&lt;/strong&gt;，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;空格 &lt;code&gt;&apos; &apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;回车 &lt;code&gt;&apos;\n&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;制表符 &lt;code&gt;&apos;\t&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都是分隔符，但 &lt;strong&gt;不会终止输入流&lt;/strong&gt;，只是划分输入的不同部分。&lt;/p&gt;
&lt;h4&gt;3️⃣ stringstream&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;std::stringstream&lt;/code&gt; 是 C++ 标准库中的一个类，位于 &lt;code&gt;&amp;#x3C;sstream&gt;&lt;/code&gt; 头文件中。它提供了一个用于在内存中进行输入输出操作的字符串流。&lt;code&gt;std::stringstream&lt;/code&gt; 允许你像使用 &lt;code&gt;std::cin&lt;/code&gt; 和 &lt;code&gt;std::cout&lt;/code&gt; 一样操作字符串，它可以用来从字符串中读取数据，或将数据写入到字符串中。它的主要用途是进行字符串的格式化和数据的转换。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::stringstream&lt;/code&gt; 继承自 &lt;code&gt;std::iostream&lt;/code&gt;，因此可以使用 &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt; 和 &lt;code&gt;&gt;&gt;&lt;/code&gt; 运算符来进行数据流的输入输出。如果想清空 &lt;code&gt;stringstream&lt;/code&gt; 中的数据，可以使用 &lt;code&gt;str(&quot;&quot;)&lt;/code&gt; 方法，将流的内容设置为空字符串，或者使用 &lt;code&gt;clear()&lt;/code&gt; 来重置流的状态。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;sstream&gt;

int main() {
    std::stringstream ss1;
    int x = 10;
    double y = 3.14;
    ss1 &amp;#x3C;&amp;#x3C; &quot;Integer: &quot; &amp;#x3C;&amp;#x3C; x &amp;#x3C;&amp;#x3C; &quot;, Double: &quot; &amp;#x3C;&amp;#x3C; y;
    std::cout &amp;#x3C;&amp;#x3C; ss.str() &amp;#x3C;&amp;#x3C; std::endl;
    
    std::stringstream ss2(&quot;123 456 3.14&quot;);
    int a, b;
    double c;
    ss &gt;&gt; a &gt;&gt; b &gt;&gt; c;
    std::cout &amp;#x3C;&amp;#x3C; &quot;a: &quot; &amp;#x3C;&amp;#x3C; a &amp;#x3C;&amp;#x3C; &quot;, b: &quot; &amp;#x3C;&amp;#x3C; b &amp;#x3C;&amp;#x3C; &quot;, c: &quot; &amp;#x3C;&amp;#x3C; c &amp;#x3C;&amp;#x3C; std::endl;
    
    // 清空: ss.str(&quot;&quot;)
    ss1.str(&quot;&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4️⃣ getline&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;getline&lt;/code&gt; 函数是 C++ 中用于从输入流中读取一行文本的函数，通常用于读取用户输入或文件中的一行数据。它的基本用法是：读取一整行数据，直到遇到换行符（&lt;code&gt;\n&lt;/code&gt;）为止。它不会将换行符包含在返回的字符串中。函数原型为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;istream&amp;#x26; getline (istream&amp;#x26; is, string&amp;#x26; str);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它接受两个参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;is&lt;/code&gt;：输入流对象（如 &lt;code&gt;cin&lt;/code&gt; 或 &lt;code&gt;ifstream&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;str&lt;/code&gt;：存储读取内容的 &lt;code&gt;string&lt;/code&gt; 对象。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;
using namespace std;

int main() {
    // 输入 1 2 3 4
    string line;
    while (getline(cin, line)) {
        cout &amp;#x3C;&amp;#x3C; &quot;输入的行是: &quot; &amp;#x3C;&amp;#x3C; line &amp;#x3C;&amp;#x3C; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;
using namespace std;

int main() {
    std::string input;
    std::cin &gt;&gt; input;  // 输入 1,2,3,4

    std::stringstream ss(input);
    std::vector&amp;#x3C;int&gt; nums;
    std::string number;
	// stringstream, string
    while (getline(ss, number, &apos;,&apos;)) {
        nums.push_back(std::stoi(number));
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. A+B+C+...（单行输入版）&lt;/h3&gt;
&lt;p&gt;输入样例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 2 3 4 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出样例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;题解：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int main() {
  // 数据范围: -10^9 &amp;#x3C;= x &amp;#x3C;= 10^9
  long long val, s = 0;
  while (cin &gt;&gt; val) {
    s += val;
  }
  cout &amp;#x3C;&amp;#x3C; s &amp;#x3C;&amp;#x3C; endl;
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. A+B+C+...（多行输入版）&lt;/h3&gt;
&lt;p&gt;输入样例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;1 2 3
4 5 6 7
8 9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出样例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;6
22
17
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;题解：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;sstream&gt;
using namespace std;

int main() {
  string line;
  while (getline(cin, line)) { // 持续读取完整一行，一直到 EOF
    // 使用 stringstream 解析每一行的输入
    // 假设读到的是&quot;1 2 3&quot;
    stringstream ss(line);
    long long num, sum = 0;
    while (ss &gt;&gt; num) { // 逐个读取这一行的整数
      sum += num;       // 将读取的整数累加到 sum 中
                        // 先是sum += 1
                        // 再是sum += 2
                        // 最后是sum += 3
    }
    cout &amp;#x3C;&amp;#x3C; sum &amp;#x3C;&amp;#x3C; endl; // 输出这一行所有整数的和 : 6
  }
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;🔥 4. A+B+C+...（带元素个数的多行输入版）&lt;/h3&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;5 3
1 2 2 3 2
2
3
4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;3
1
0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ 本题反而要注意：&lt;code&gt;cin&lt;/code&gt; 不是读到 &lt;code&gt;\n&lt;/code&gt; 停止，而是 EOF，所以 line 14 不能用 &lt;code&gt;while(cin &gt;&gt; val)&lt;/code&gt; 来替代，否则后续元素都会被吸收到 &lt;strong&gt;nums&lt;/strong&gt; 数组中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;algorithm&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
using namespace std;

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  int n, Q;
  cin &gt;&gt; n &gt;&gt; Q;

  vector&amp;#x3C;int&gt; nums(n);
  for (int i = 0; i &amp;#x3C; n; i++) {
    cin &gt;&gt; nums[i];
  }
  vector&amp;#x3C;int&gt; query(Q);
  for (int i = 0; i &amp;#x3C; Q; i++) {
    cin &gt;&gt; query[i];
  }
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者可以使用 &lt;code&gt;cin.peek() != &apos;\n&apos;&lt;/code&gt; 搭配 &lt;code&gt;cin &gt;&gt; val&lt;/code&gt; 使用（这一刻我才明白 &lt;code&gt;cin.peek()&lt;/code&gt; 与 &lt;code&gt;cin.ignore()&lt;/code&gt; 的作用）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;记得先处理上一行的末尾（如果需要处理）：&lt;code&gt;cin.ignore()&lt;/code&gt; 或 &lt;code&gt;cin.get()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;再使用 &lt;code&gt;cin.peek() != &apos;\n&apos; &amp;#x26; cin &gt;&gt; val&lt;/code&gt; 来循环读取当前行元素&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;algorithm&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
using namespace std;

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  int n, Q;
  cin &gt;&gt; n &gt;&gt; Q;
  //vector&amp;#x3C;int&gt; nums(n);
  //for (int i = 0; i &amp;#x3C; n; i++) {
  //  cin &gt;&gt; nums[i];
  //}
    
// 等价于
    
  // 🔥注意这里需要处理第一行的 &apos;\n&apos;, 因为 cin &gt;&gt; n &gt;&gt; Q 后还没跳到下一行。
  cin.ignore();	// 或者 cin.get();
  vector&amp;#x3C;int&gt; nums;
  int val;
  while (cin.peek() != &apos;\n&apos; &amp;#x26;&amp;#x26; cin &gt;&gt; val) {
      nums.push_back(val);
  }
  vector&amp;#x3C;int&gt; query(Q);
  for (int i = 0; i &amp;#x3C; Q; i++) {
    cin &gt;&gt; query[i];
  }
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ &lt;strong&gt;拓展延伸&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1️⃣ 上述的输入是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5 3⏎(换行)
1 2 2 3 2⏎(换行)
2⏎(换行)
3⏎(换行)
4⏎(换行)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2️⃣ 假设输入变成以下这种（即第二行换行符前还有一个␣(空格)）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5 3⏎(换行)
1 2 2 3 2␣(空格)⏎(换行)
2⏎(换行)
3⏎(换行)
4⏎(换行)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那 nums 的个数会有 6 个，即第 3 行的 2 也会当成 nums 的元素，因为 ␣(空格) 不是 &lt;code&gt;\n&lt;/code&gt;，所以会再触发一次 &lt;code&gt;cin&lt;/code&gt; 操作，所以建议使用 &lt;code&gt;for (int i = 0; i &amp;#x3C; n; i++) { cin &gt;&gt; nums[i]; }&lt;/code&gt; 的方式替代 &lt;code&gt;peek()&lt;/code&gt; 判断！&lt;/p&gt;
&lt;h2&gt;OJ 时间复杂度限制与预估&lt;/h2&gt;
&lt;p&gt;在编写程序时，分析其时间复杂度（Time Complexity）是评估程序效率的重要手段。时间复杂度描述了程序运行时间与输入规模之间的关系，通常使用大O符号表示（如O(n)、O(n²)等）。下面将详细解释时间复杂度的概念，并分析这段代码的时间复杂度。&lt;/p&gt;
&lt;p&gt;🔥 &lt;strong&gt;OJ 一般 C++ 1秒（即1000ms）大概能跑 1e8 量级&lt;/strong&gt;（很多题目都会限制时间和内存，如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;时间限制: C/C++ 1000ms , 其他语言： 2000ms&lt;/p&gt;
&lt;p&gt;内存限制: C/C++ 256MB , 其他语言： 512MB&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;
int x;
int ans = 0;
int main() {
	cin&gt;&gt;x;
	for (int i = 1; i &amp;#x3C;= x; i++) {
		ans++;
	}
	cout&amp;#x3C;&amp;#x3C;ans;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🔥对于这个简单的代码，x &amp;#x3C; &lt;strong&gt;1e8&lt;/strong&gt; , 运行不会超时 ， x &gt; &lt;strong&gt;1e8&lt;/strong&gt; , 运行超时&lt;/p&gt;
&lt;p&gt;✅ 时间复杂度衡量的是算法执行所需的时间增长率，随着输入规模的增加，算法的运行时间如何变化。常见的时间复杂度包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;O(1)&lt;/strong&gt;：常数时间，无论输入规模多大，执行时间保持不变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;O(log n)&lt;/strong&gt;：对数时间，随着输入规模增加，执行时间按对数增长。例如二分操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;O(n)&lt;/strong&gt;：线性时间，执行时间与输入规模成正比。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;O(n log n)&lt;/strong&gt;：线性对数时间，常见于高效排序算法如快速排序、归并排序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;O(n²)&lt;/strong&gt;：平方时间，常见于简单的嵌套循环，如冒泡排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ &lt;strong&gt;如何计算时间复杂度&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;识别基本操作：确定算法中最频繁执行的操作，如循环中的语句、递归调用等。&lt;/li&gt;
&lt;li&gt;计算基本操作的执行次数：根据输入规模，计算这些操作随着输入增长的次数。&lt;/li&gt;
&lt;li&gt;忽略低阶项和常数系数：在大O表示法中，只保留增长最快的项，忽略常数和低阶项。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;🔥 &lt;strong&gt;对于一般情况&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;n=$10^5$ 或 n=$10^6$ 左右考虑 $O(n log n)$ 以下的做法&lt;/li&gt;
&lt;li&gt;n=$5 * 10^3$ 左右考虑 $O(n^2)$ 以下的做法&lt;/li&gt;
&lt;li&gt;n=$10^2$ 左右考虑 $O(n^3)$ 以下的做法&lt;/li&gt;
&lt;li&gt;n=$20$ 左右考虑 $O(2^n)$ 以下的做法&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;各数据类型的读入与构造&lt;/h2&gt;
&lt;h3&gt;数组&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#include &amp;#x3C;numeric&gt;
using namespace std;

int main() {
  int n;
  cin &gt;&gt; n;
  vector&amp;#x3C;int&gt; nums(n);
  for (int i = 0; i &amp;#x3C; n; i++) {
    cin &gt;&gt; nums[i];
  }
  cout &amp;#x3C;&amp;#x3C; accumulate(nums.begin(), nums.end(), 0) &amp;#x3C;&amp;#x3C; endl;
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;链表&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

struct ListNode {
  int val;
  ListNode *next;
  ListNode(int x) : val(x), next(nullptr) {}
};

ListNode *createLinkedList(vector&amp;#x3C;int&gt; &amp;#x26;nums) {
  ListNode dummy(0);
  ListNode *cur = &amp;#x26;dummy;
  for (int x : nums) {
    cur-&gt;next = new ListNode(x);
    cur = cur-&gt;next;
  }
  return dummy.next;
}

void printLinkedList(ListNode *head) {
  for (ListNode *cur = head; cur; cur = cur-&gt;next) {
    cout &amp;#x3C;&amp;#x3C; cur-&gt;val &amp;#x3C;&amp;#x3C; endl;
  }
}

int main() {
  int n;
  cin &gt;&gt; n; // 读取数组长度

  vector&amp;#x3C;int&gt; nums(n);
  for (int i = 0; i &amp;#x3C; n; i++) {
    cin &gt;&gt; nums[i]; // 读取数组元素
  }

  ListNode *head = createLinkedList(nums); // 创建链表
  printLinkedList(head);                   // 遍历链表并输出

  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二叉树的读入与构建（输入为数组形式）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;本题相当于根据「层序遍历」结果来构造二叉树：本质就是根据数组索引来构造&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ 推荐阅读（&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal/solutions/3653952/qian-zhong-hou-gou-zao-er-cha-shu-xi-lie-z1au/&quot;&gt;&lt;strong&gt;题解&lt;/strong&gt;&lt;/a&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal/&quot;&gt;889. 根据前序和后序遍历构造二叉树&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/&quot;&gt;105. 从前序与中序遍历序列构造二叉树&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/&quot;&gt;106. 从中序与后序遍历序列构造二叉树&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/construct-binary-search-tree-from-preorder-traversal/&quot;&gt;1008. 前序遍历构造二叉搜索树&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;输入&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;1 2 3 4 5 -1 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;树的结构&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;       1
      / \
     2   3
    / \   \
   4   5   6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;1
2
3
4
5
6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;题解&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

struct TreeNode {
  int val;
  TreeNode *left;
  TreeNode *right;
  TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

TreeNode *buildTree(const vector&amp;#x3C;int&gt; &amp;#x26;nums) {
  if (nums.empty() || nums[0] == -1)
    return nullptr;
  vector&amp;#x3C;TreeNode *&gt; nodes(nums.size(), nullptr);
  for (size_t i = 0; i &amp;#x3C; nums.size(); i++) {
    if (nums[i] != -1)
      nodes[i] = new TreeNode(nums[i]);
  }
  for (size_t i = 0; i &amp;#x3C; nums.size(); i++) {
    if (nodes[i]) {
      if (2 * i + 1 &amp;#x3C; nums.size())
        nodes[i]-&gt;left = nodes[2 * i + 1];
      if (2 * i + 2 &amp;#x3C; nums.size())
        nodes[i]-&gt;right = nodes[2 * i + 2];
    }
  }
  return nodes[0];
}

void levelOrder(TreeNode *root) {
  if (!root)
    return;
  queue&amp;#x3C;TreeNode *&gt; q;
  q.push(root);
  while (!q.empty()) {
    TreeNode *curr = q.front();
    q.pop();
    cout &amp;#x3C;&amp;#x3C; curr-&gt;val &amp;#x3C;&amp;#x3C; endl;
    if (curr-&gt;left)
      q.push(curr-&gt;left);
    if (curr-&gt;right)
      q.push(curr-&gt;right);
  }
}

int main() {
  string line;
  getline(cin, line);
  stringstream ss(line);
  vector&amp;#x3C;int&gt; nums;
  int val;
  while (ss &gt;&gt; val) {
    nums.push_back(val);
  }
  TreeNode *root = buildTree(nums);
  levelOrder(root);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;普通树的读入与构建（输入为相邻边 &amp;#x26; father 数组）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;分成两种形式的读入，一起讲解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;题目描述&lt;/h4&gt;
&lt;p&gt;给定一棵 &lt;strong&gt;n&lt;/strong&gt; 个节点的树，节点编号为1−n1−&lt;em&gt;n&lt;/em&gt;，树的根节点固定为 &lt;code&gt;1&lt;/code&gt;。我们有两种方式表示树的结构：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;方式一&lt;/strong&gt;：通过 &lt;strong&gt;n-1&lt;/strong&gt; 条边的形式，每条边 &lt;code&gt;u v&lt;/code&gt; 表示节点 &lt;code&gt;u&lt;/code&gt; 和节点 &lt;code&gt;v&lt;/code&gt; 之间存在一条边。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方式二&lt;/strong&gt;：通过一个 &lt;strong&gt;father&lt;/strong&gt; 数组，&lt;code&gt;father[i]&lt;/code&gt; 表示节点 &lt;code&gt;i+1&lt;/code&gt; 的父节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;请你编写程序，读入树的结构并使用&lt;code&gt;深度优先搜索&lt;/code&gt;遍历打印这棵树的节点编号。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为了输出统一，从根节点开始遍历，优先访问序号小的子节点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;输入&lt;/h4&gt;
&lt;p&gt;输入包含三部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一行包含一个整数 &lt;code&gt;n&lt;/code&gt;，表示树的节点个数。&lt;/li&gt;
&lt;li&gt;第二行包含一个整数 &lt;code&gt;type&lt;/code&gt;，表示树的表示方式：
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;type = 1&lt;/code&gt;，表示通过边的形式输入。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;type = 2&lt;/code&gt;，表示通过 &lt;code&gt;father&lt;/code&gt; 数组输入。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;type = 1&lt;/code&gt;，接下来会有 &lt;code&gt;n-1&lt;/code&gt; 行，每行两个整数 &lt;code&gt;u v&lt;/code&gt;，表示树中节点 &lt;code&gt;u&lt;/code&gt; 和节点 &lt;code&gt;v&lt;/code&gt; 之间存在一条边。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;type = 2&lt;/code&gt;，接下来一行有 &lt;code&gt;n&lt;/code&gt; 个整数，&lt;code&gt;father[i]&lt;/code&gt; 表示节点 &lt;code&gt;i+1&lt;/code&gt; 的父节点，其中 &lt;code&gt;father[0] = 0&lt;/code&gt;，表示 &lt;code&gt;1&lt;/code&gt; 号节点为根节点,没有父节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;输出&lt;/h4&gt;
&lt;p&gt;打印遍历这棵树的节点编号。&lt;/p&gt;
&lt;h4&gt;输入样例 1&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;5
1
1 2
1 3
2 4
2 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;输出样例 1&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;1 2 4 5 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;样例1 图例&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;      1
     / \
    2   3
   / \
  4   5
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;输入样例 2&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;5
2
0 1 1 2 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;输出样例 2&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;1 2 4 5 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;数据范围&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;$1\le n \le 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;🔥题解&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;algorithm&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
using namespace std;

#define MAX 100005 // 假设最大节点数 10^5

vector&amp;#x3C;int&gt; adjList[MAX];    // 邻接表
vector&amp;#x3C;int&gt; traversalResult; // 存储先序遍历结果

void DFS(int node, int parent) {
  traversalResult.push_back(node);
  for (auto &amp;#x26;child : adjList[node]) {
    if (child != parent) {
      DFS(child, node);
    }
  }
}

int main() {
  int n, type;
  cin &gt;&gt; n &gt;&gt; type;

  if (type == 1) {
    // type 1: 通过边的形式读入
    for (int i = 0; i &amp;#x3C; n - 1; i++) {
      int u, v;
      cin &gt;&gt; u &gt;&gt; v;
      adjList[u].push_back(v);
      adjList[v].push_back(u);
    }
  } else if (type == 2) {
    // type 2: 通过 father 数组输入
    vector&amp;#x3C;int&gt; father(n + 1);
    for (int i = 1; i &amp;#x3C;= n; i++) {
      cin &gt;&gt; father[i];
      if (father[i] != 0) {
        adjList[father[i]].push_back(i);
        adjList[i].push_back(father[i]);
      }
    }
  }
  // 为了保证遍历顺序的一致性，先对每个节点的子节点进行排序
  for (int i = 1; i &amp;#x3C;= n; i++) {
    sort(adjList[i].begin(), adjList[i].end());
  }

  DFS(1, 0);

  for (int i = 0; i &amp;#x3C; (int)traversalResult.size(); i++) {
    if (i &gt; 0)
      cout &amp;#x3C;&amp;#x3C; &apos; &apos;;
    cout &amp;#x3C;&amp;#x3C; traversalResult[i];
  }
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;图的构造（邻接矩阵 &amp;#x26; 邻接表）&lt;/h3&gt;
&lt;h4&gt;题目描述&lt;/h4&gt;
&lt;p&gt;给定两张有向图 A 和 B，其中图 A 以邻接矩阵形式给出，图 B 以邻接表形式给出。请判断这两张图是否完全一样。我们将“完全一样”的定义为：每个节点的邻居集合完全一致。&lt;/p&gt;
&lt;h4&gt;输入&lt;/h4&gt;
&lt;p&gt;输入的第一行包含两个整数 n，表示图的节点数。&lt;/p&gt;
&lt;p&gt;接下来的 n 行，给出图 A 的邻接矩阵。该矩阵的第 i 行第 j 列表示节点 i 和节点 j 之间是否有边。如果存在边，则该位置的值为 1，否则为 0。&lt;/p&gt;
&lt;p&gt;接下来的 n 行，给出图 B 的邻接表。每行第一个数 node,后面跟的第一个数 k 表示接下来输入 k 个数 val 表示节点 node 向这些节点 val 连一条边。&lt;/p&gt;
&lt;h4&gt;输出&lt;/h4&gt;
&lt;p&gt;如果图 A 和图 B 完全一样，则输出 &quot;YES&quot;；否则输出 &quot;NO&quot;。&lt;/p&gt;
&lt;h4&gt;注意&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;图 A 和图 B 是有向图，即如果 &lt;code&gt;A[i][j]=1&lt;/code&gt;，那么 i 到 j 有条有向边。&lt;/li&gt;
&lt;li&gt;节点编号从 1 到 n。&lt;/li&gt;
&lt;li&gt;图 A 和图 B 的节点数相同。&lt;/li&gt;
&lt;li&gt;数据范围：
&lt;ul&gt;
&lt;li&gt;$1≤n≤10^3$&lt;/li&gt;
&lt;li&gt;图 A 的邻接矩阵大小为 n×n，其中每个元素为 0 或 1。&lt;/li&gt;
&lt;li&gt;图 B 的邻接表中每个节点的邻居数量不超过 n−1。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;样例输入 1&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;3
0 1 1
1 0 1
1 1 0
1 2 2 3
2 2 1 3
3 2 1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;样例输出 1&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;YES
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;样例输入 2&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;3
0 1 1
1 0 1
1 1 0
1 2 2 3
2 2 1 3
3 1 1 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;样例输出 2&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;NO
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;样例 2 提示&lt;/h4&gt;
&lt;p&gt;图 A 的邻接矩阵为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;0 1 1
1 0 1
1 1 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示图 A 中，节点 1 与节点 2 和节点 3 相连，节点 2 与节点 1 和节点 3 相连，节点 3 与节点 1 和节点 2 相连。&lt;/p&gt;
&lt;p&gt;图 B 的邻接表为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;1 2 2 3
2 2 1 3
3 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示图 B 中，节点 1 与节点 2 和节点 3 相连，节点 2 与节点 1 和节点 3 相连，节点 3 与节点 1 相连。&lt;/p&gt;
&lt;p&gt;对比可以发现，在图 B 中，节点 3 不连向 节点 2。因此，图 A 和图 B 不完全一样，输出 &quot;NO&quot;。&lt;/p&gt;
&lt;h4&gt;邻接矩阵&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504171613151.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h4&gt;邻接表&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504171612514.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h4&gt;题解&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;algorithm&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
using namespace std;

int main() {
  int n; // 顶点数
  cin &gt;&gt; n;
  vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; adjA(n + 1), adjB(n + 1);

  // 读取并转换图 A
  for (int i = 1; i &amp;#x3C;= n; i++) {
    for (int j = 1; j &amp;#x3C;= n; j++) {
      int val;
      cin &gt;&gt; val;
      if (val == 1)
        adjA[i].push_back(j);
    }
  }

  // 读取图 B
  for (int i = 0; i &amp;#x3C; n; i++) {
    int node, k;
    cin &gt;&gt; node &gt;&gt; k;
    adjB[node].resize(k);
    for (int j = 0; j &amp;#x3C; k; j++) {
      cin &gt;&gt; adjB[node][j];
    }
  }

  // 对邻接表排序
  for (int i = 1; i &amp;#x3C;= n; i++) {
    sort(adjA[i].begin(), adjA[i].end());
    sort(adjB[i].begin(), adjB[i].end());
  }

  // 比较邻接表
  bool same = true;
  for (int i = 1; i &amp;#x3C;= n; i++) {
    if (adjA[i] != adjB[i]) {
      same = false;
      break;
    }
  }

  cout &amp;#x3C;&amp;#x3C; (same ? &quot;YES&quot; : &quot;NO&quot;) &amp;#x3C;&amp;#x3C; endl;
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202507201320470.BJLLDVOk.png"/><enclosure url="/_astro/202507201320470.BJLLDVOk.png"/></item><item><title>2025.04.24 吉比特笔试题</title><link>https://coooredump.github.io/blog/recruitment/20250424-gbits</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/20250424-gbits</guid><description>吉比特 20250424 笔试解析</description><pubDate>Thu, 24 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 数组查询&lt;/h2&gt;
&lt;p&gt;给定一个数组 &lt;code&gt;a[n]&lt;/code&gt;，以及 &lt;code&gt;q&lt;/code&gt; 次查询 &lt;code&gt;(l, r)&lt;/code&gt;，输出 &lt;code&gt;a[l] - a[l+1] - a[l+2] - ... - a[r-1] - a[r]&lt;/code&gt; 的值。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 &lt;code&gt;n&lt;/code&gt; 和 &lt;code&gt;q&lt;/code&gt;，表示数组的长度和查询的次数。&lt;/li&gt;
&lt;li&gt;第二行包含 &lt;code&gt;n&lt;/code&gt; 个整数，表示数组 &lt;code&gt;a&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;接下来的 &lt;code&gt;q&lt;/code&gt; 行，每行包含两个整数 &lt;code&gt;l&lt;/code&gt; 和 &lt;code&gt;r&lt;/code&gt;，表示查询的区间（假设数组下标从 1 开始）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;对于每个查询，输出一个整数，表示对应的计算结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;5 3
1 2 3 4 5
1 5
2 4
3 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;-13
-5
3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个查询：$1 - 2 - 3 - 4 - 5 = -13$&lt;/li&gt;
&lt;li&gt;第二个查询：$2 - 3 - 4 = -5$&lt;/li&gt;
&lt;li&gt;第三个查询：$3$&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：模拟 / 前缀和&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;直接按照题目要求计算即可。对于每个查询 &lt;code&gt;(l, r)&lt;/code&gt;，从 &lt;code&gt;a[l]&lt;/code&gt; 开始，依次减去 &lt;code&gt;a[l+1]&lt;/code&gt; 到 &lt;code&gt;a[r]&lt;/code&gt; 的值。时间复杂度为 $O(q * n)$，在 &lt;code&gt;n&lt;/code&gt; 和 &lt;code&gt;q&lt;/code&gt; 较小的情况下可以通过。&lt;/p&gt;
&lt;p&gt;优化思路：可以预处理前缀和数组 &lt;code&gt;prefix&lt;/code&gt;，其中 &lt;code&gt;prefix[i]&lt;/code&gt; 表示 &lt;code&gt;a[1] - a[2] - ... - a[i]&lt;/code&gt;。然后对于查询 &lt;code&gt;(l, r)&lt;/code&gt;，结果为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;l == 1&lt;/code&gt;，直接取 &lt;code&gt;prefix[r]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;否则，结果为 &lt;code&gt;a[l] - (prefix[r] - prefix[l])&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
using namespace std;

int main() {
    int n, q;
    cin &gt;&gt; n &gt;&gt; q;
    vector&amp;#x3C;int&gt; a(n + 1);
    vector&amp;#x3C;int&gt; prefix(n + 1, 0);
    
    for (int i = 1; i &amp;#x3C;= n; ++i) {
        cin &gt;&gt; a[i];
        if (i == 1) {
            prefix[i] = a[i];
        } else {
            prefix[i] = prefix[i - 1] - a[i];
        }
    }

    while (q--) {
        int l, r;
        cin &gt;&gt; l &gt;&gt; r;
        if (l == 1) {
            cout &amp;#x3C;&amp;#x3C; prefix[r] &amp;#x3C;&amp;#x3C; endl;
        } else {
            cout &amp;#x3C;&amp;#x3C; a[l] - (prefix[r] - prefix[l]) &amp;#x3C;&amp;#x3C; endl;
        }
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 多米诺骨牌推倒&lt;/h2&gt;
&lt;p&gt;给定一排多米诺骨牌，每个骨牌有一个数值。每次操作可以选择一个位置和一个方向（左或右）进行推倒。推倒的规则是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果相邻骨牌（根据方向）的数值比当前骨牌的数值小，则会被推倒，并继续向该方向传播，直到遇到不小于当前骨牌数值的骨牌为止。&lt;/li&gt;
&lt;li&gt;问最少需要多少次操作才能将所有骨牌都推倒。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 &lt;code&gt;n&lt;/code&gt;，表示骨牌的数量。&lt;/li&gt;
&lt;li&gt;第二行包含 &lt;code&gt;n&lt;/code&gt; 个整数，表示每个骨牌的数值。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;输出一个整数，表示最少需要的操作次数。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;5
3 1 2 4 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次操作：选择 4，向右推倒，可以推倒 1（因为 1 &amp;#x3C; 4）&lt;/li&gt;
&lt;li&gt;第二次操作：选择 3，向右推倒，可以推倒位置 1&lt;/li&gt;
&lt;li&gt;第二次操作：选择 2 推倒&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：转化为区间覆盖问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后半部分代码参考 LC &lt;a href=&quot;https://leetcode.cn/problems/jump-game-ii/&quot;&gt;45. 跳跃游戏 II&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个问题是一个&lt;strong&gt;链式反应模拟 + 贪心优化&lt;/strong&gt;问题。目标是最小化推动次数，使得所有积木都被推倒。&lt;/p&gt;
&lt;p&gt;我们可以对问题进行如下处理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;模拟倒塌过程&lt;/strong&gt;（从某个位置向左或向右传递）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预处理每个积木从左或右可以推倒的“影响范围”&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;贪心策略&lt;/strong&gt;：用最少的“倒塌段”覆盖所有积木（区间覆盖）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;步骤详解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Step 1：预处理每个积木向左/右能影响多远&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于每个位置 &lt;code&gt;i&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;向右倒：不断检查 &lt;code&gt;A[i+1] &amp;#x3C; A[i]&lt;/code&gt;, &lt;code&gt;A[i+2] &amp;#x3C; A[i+1]&lt;/code&gt;... 直到不满足，记录影响区间 &lt;code&gt;R[i]&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;向左倒：不断检查 &lt;code&gt;A[i-1] &amp;#x3C; A[i]&lt;/code&gt;, &lt;code&gt;A[i-2] &amp;#x3C; A[i-1]&lt;/code&gt;... 类似，记录影响区间 &lt;code&gt;L[i]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Step 2：把每个可能的倒塌行为看作一个“区间”覆盖&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从 &lt;code&gt;i&lt;/code&gt; 向右能推倒到 &lt;code&gt;j&lt;/code&gt;，我们记录一个区间 &lt;code&gt;[i, j]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从 &lt;code&gt;i&lt;/code&gt; 向左能推倒到 &lt;code&gt;k&lt;/code&gt;，我们记录一个区间 &lt;code&gt;[k, i]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;总共会有 &lt;code&gt;2n&lt;/code&gt; 个这样的区间。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Step 3：用最少的这些区间覆盖整个 &lt;code&gt;[1, n]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个就是经典的 &lt;strong&gt;区间覆盖问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将所有区间按起点排序；&lt;/li&gt;
&lt;li&gt;每次选择起点 ≤ 当前覆盖末尾，终点最大的区间；&lt;/li&gt;
&lt;li&gt;如果无法延伸则失败；&lt;/li&gt;
&lt;li&gt;否则计数操作次数，直到覆盖整个 &lt;code&gt;[1, n]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;algorithm&gt;

using namespace std;

int main() {
    int n;
    cin &gt;&gt; n;
    vector&amp;#x3C;int&gt; A(n + 2);  // 1-based indexing, pad for safety
    for (int i = 1; i &amp;#x3C;= n; ++i) {
        cin &gt;&gt; A[i];
    }

    vector&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; intervals;

    // 向右推
    for (int i = 1; i &amp;#x3C;= n; ++i) {
        int j = i;
        while (j + 1 &amp;#x3C;= n &amp;#x26;&amp;#x26; A[j + 1] &amp;#x3C; A[j]) {
            ++j;
        }
        intervals.emplace_back(i, j);
    }

    // 向左推
    for (int i = 1; i &amp;#x3C;= n; ++i) {
        int j = i;
        while (j - 1 &gt;= 1 &amp;#x26;&amp;#x26; A[j - 1] &amp;#x3C; A[j]) {
            --j;
        }
        intervals.emplace_back(j, i);
    }

    // 贪心区间覆盖 [1, n]
    sort(intervals.begin(), intervals.end());

    int res = 0, end = 0, next_end = 0, idx = 0;
    while (end &amp;#x3C; n) {
        while (idx &amp;#x3C; intervals.size() &amp;#x26;&amp;#x26; intervals[idx].first &amp;#x3C;= end + 1) {
            next_end = max(next_end, intervals[idx].second);
            ++idx;
        }
        if (next_end == end) {
            cout &amp;#x3C;&amp;#x3C; -1 &amp;#x3C;&amp;#x3C; endl;  // 理论上不会出现
            return 0;
        }
        end = next_end;
        ++res;
    }

    cout &amp;#x3C;&amp;#x3C; res &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 绳子分割与多边形面积&lt;/h2&gt;
&lt;p&gt;牛牛小朋友和他的朋友们，一共 $n$ 个人在操场上玩一个圈地盘的游戏。 这个游戏的规则是这样的，将一根的绳子剪成 $n$ 段， 让每个小朋友都能有一小段绳子。 每个小朋友用拿到的一小段绳子分别圈地，要求绳子头尾相接（头尾相接时产生的损耗忽略不计），并且第 $i$ 个小朋友要求他用绳子在地上圈起来的地盘是 $a_i$ 边形的（有些小朋友对他圈起来的地盘是什么形状的并不关心，用 $-1$ 表示）。&lt;/p&gt;
&lt;p&gt;小朋友们只能找到一根长度为 $l$ 的绳子，需要把绳子剪开之后，所有小朋友可以圈出地盘的面积的&lt;strong&gt;最小值为 $s$&lt;/strong&gt;。为了让小朋友们尽可能高兴，需牛牛要一种剪绳子的方法，&lt;strong&gt;让 $s$ 尽可能大&lt;/strong&gt;。你能帮助牛牛解决这个问题吗？你只需要精确到小数点后 6 位即可（和标准答案相对误差低于 $1e^{-5}$ 则被判定为正确）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 &lt;code&gt;l&lt;/code&gt; 和 &lt;code&gt;n&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第二行包含 &lt;code&gt;n&lt;/code&gt; 个整数，表示数组 &lt;code&gt;a&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出格式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;输出一个浮点数，表示面积最小值的最大可能值，保留足够的小数位数。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;10 2
4 -1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;2.5
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个问题可以转化为 &lt;strong&gt;二分答案 + 几何判断&lt;/strong&gt; 的问题：&lt;/p&gt;
&lt;p&gt;核心思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：将长度为 &lt;code&gt;l&lt;/code&gt; 的绳子分成 &lt;code&gt;n&lt;/code&gt; 段，使得所有小朋友围成的图形的面积的&lt;strong&gt;最小值最大化&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;限制条件&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;每个小朋友的形状是一个正多边形，边数为 &lt;code&gt;a_i&lt;/code&gt;（若为 &lt;code&gt;-1&lt;/code&gt;，代表形状不限制，可以视为正圆）。&lt;/li&gt;
&lt;li&gt;总绳长为 &lt;code&gt;l&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;我们用 &lt;strong&gt;二分搜索&lt;/strong&gt; 来猜测最小的面积 &lt;code&gt;s&lt;/code&gt;，然后验证是否可以在绳长为 &lt;code&gt;l&lt;/code&gt; 的情况下满足每个人的要求。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;面积计算公式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于边数为 &lt;code&gt;k&lt;/code&gt; 的正多边形，边长为 &lt;code&gt;x / k&lt;/code&gt;，周长为 &lt;code&gt;x&lt;/code&gt;，面积为：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
A = \frac{k}{4} \cdot x^2 \cdot \cot\left(\frac{\pi}{k}\right)
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是圆（&lt;code&gt;a_i == -1&lt;/code&gt;），设周长为 &lt;code&gt;x&lt;/code&gt;，则半径 &lt;code&gt;r = x / (2π)&lt;/code&gt;，面积为：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
A = \pi \cdot \left(\frac{x}{2\pi}\right)^2 = \frac{x^2}{4\pi}
$$&lt;/p&gt;
&lt;p&gt;给定一个猜测的最小面积 &lt;code&gt;s&lt;/code&gt;，我们尝试为每个小朋友分配一段最短绳长，使得他能围成面积至少为 &lt;code&gt;s&lt;/code&gt;，然后计算所有这些最短绳长的和是否不超过 &lt;code&gt;l&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;vector&gt;
#include &amp;#x3C;algorithm&gt;
#include &amp;#x3C;cmath&gt;
#include &amp;#x3C;iomanip&gt;

using namespace std;

const double PI = acos(-1.0);
const double EPS = 1e-7;

int n;
double l;
vector&amp;#x3C;int&gt; a;

double get_min_len(int k, double s) {
    if (k == -1) { // Circle
        return sqrt(4 * PI * s);
    } else {
        double tan_val = tan(PI / k);
        double coeff = k / (4 * tan_val); // cotangent = 1 / tan
        return sqrt(s / coeff);
    }
}

bool check(double s) {
    double total_len = 0;
    for (int i = 0; i &amp;#x3C; n; ++i) {
        double min_len = get_min_len(a[i], s);
        total_len += min_len;
        if (total_len &gt; l)
            return false;
    }
    return true;
}

int main() {
    cin &gt;&gt; n &gt;&gt; l;
    a.resize(n);
    for (int i = 0; i &amp;#x3C; n; ++i) {
        cin &gt;&gt; a[i];
    }

    double left = 0, right = 1e18; // Upper bound high enough
//    while (right - left &gt; EPS)
    for (int iter = 0; iter &amp;#x3C; 100; ++iter) { // binary search
        double mid = (left + right) / 2;
        if (check(mid)) {
            left = mid;
        } else {
            right = mid;
        }
    }

    cout &amp;#x3C;&amp;#x3C; fixed &amp;#x3C;&amp;#x3C; setprecision(6) &amp;#x3C;&amp;#x3C; left &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/20250810-yTVVig.B9QMNOx9.png"/><enclosure url="/_astro/20250810-yTVVig.B9QMNOx9.png"/></item><item><title>Effective C++</title><link>https://coooredump.github.io/blog/cpp/effective-c</link><guid isPermaLink="true">https://coooredump.github.io/blog/cpp/effective-c</guid><description>改善程序与设计的 55 个具体做法</description><pubDate>Sun, 13 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Effective C++&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;视 C++ 为一个语言联邦（C、Object-Oriented C++、Template C++、STL）&lt;/li&gt;
&lt;li&gt;宁可以编译器替换预处理器（尽量以 &lt;code&gt;const&lt;/code&gt;、&lt;code&gt;enum&lt;/code&gt;、&lt;code&gt;inline&lt;/code&gt; 替换 &lt;code&gt;#define&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;尽可能使用 const&lt;/li&gt;
&lt;li&gt;确定对象被使用前已先被初始化（构造时赋值（copy 构造函数）比 default 构造后赋值（copy assignment）效率高）&lt;/li&gt;
&lt;li&gt;了解 C++ 默默编写并调用哪些函数（编译器暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符、析构函数）&lt;/li&gt;
&lt;li&gt;若不想使用编译器自动生成的函数，就应该明确拒绝（将不想使用的成员函数声明为 private，并且不予实现）&lt;/li&gt;
&lt;li&gt;为多态基类声明 virtual 析构函数（如果 class 带有任何 virtual 函数，它就应该拥有一个 virtual 析构函数）&lt;/li&gt;
&lt;li&gt;别让异常逃离析构函数（析构函数应该吞下不传播异常，或者结束程序，而不是吐出异常；如果要处理异常应该在非析构的普通函数处理）&lt;/li&gt;
&lt;li&gt;绝不在构造和析构过程中调用 virtual 函数（因为这类调用从不下降至 derived class）&lt;/li&gt;
&lt;li&gt;令 &lt;code&gt;operator=&lt;/code&gt; 返回一个 &lt;code&gt;reference to *this&lt;/code&gt; （用于连锁赋值）&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;operator=&lt;/code&gt; 中处理 “自我赋值”&lt;/li&gt;
&lt;li&gt;赋值对象时应确保复制 “对象内的所有成员变量” 及 “所有 base class 成分”（调用基类复制构造函数）&lt;/li&gt;
&lt;li&gt;以对象管理资源（资源在构造函数获得，在析构函数释放，建议使用智能指针，资源取得时机便是初始化时机（Resource Acquisition Is Initialization，RAII））&lt;/li&gt;
&lt;li&gt;在资源管理类中小心 copying 行为（普遍的 RAII class copying 行为是：抑制 copying、引用计数、深度拷贝、转移底部资源拥有权（类似 auto_ptr））&lt;/li&gt;
&lt;li&gt;在资源管理类中提供对原始资源（raw resources）的访问（对原始资源的访问可能经过显式转换或隐式转换，一般而言显示转换比较安全，隐式转换对客户比较方便）&lt;/li&gt;
&lt;li&gt;成对使用 new 和 delete 时要采取相同形式（&lt;code&gt;new&lt;/code&gt; 中使用 &lt;code&gt;[]&lt;/code&gt; 则 &lt;code&gt;delete []&lt;/code&gt;，&lt;code&gt;new&lt;/code&gt; 中不使用 &lt;code&gt;[]&lt;/code&gt; 则 &lt;code&gt;delete&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;以独立语句将 newed 对象存储于（置入）智能指针（如果不这样做，可能会因为编译器优化，导致难以察觉的资源泄漏）&lt;/li&gt;
&lt;li&gt;让接口容易被正确使用，不易被误用（促进正常使用的办法：接口的一致性、内置类型的行为兼容；阻止误用的办法：建立新类型，限制类型上的操作，约束对象值、消除客户的资源管理责任）&lt;/li&gt;
&lt;li&gt;设计 class 犹如设计 type，需要考虑对象创建、销毁、初始化、赋值、值传递、合法值、继承关系、转换、一般化等等。&lt;/li&gt;
&lt;li&gt;宁以 pass-by-reference-to-const 替换 pass-by-value （前者通常更高效、避免切割问题（slicing problem），但不适用于内置类型、STL迭代器、函数对象）&lt;/li&gt;
&lt;li&gt;必须返回对象时，别妄想返回其 reference（绝不返回 pointer 或 reference 指向一个 local stack 对象，或返回 reference 指向一个 heap-allocated 对象，或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。）&lt;/li&gt;
&lt;li&gt;将成员变量声明为 private（为了封装、一致性、对其读写精确控制等）&lt;/li&gt;
&lt;li&gt;宁以 non-member、non-friend 替换 member 函数（可增加封装性、包裹弹性（packaging flexibility）、机能扩充性）&lt;/li&gt;
&lt;li&gt;若所有参数（包括被this指针所指的那个隐喻参数）皆须要类型转换，请为此采用 non-member 函数&lt;/li&gt;
&lt;li&gt;考虑写一个不抛异常的 swap 函数&lt;/li&gt;
&lt;li&gt;尽可能延后变量定义式的出现时间（可增加程序清晰度并改善程序效率）&lt;/li&gt;
&lt;li&gt;尽量少做转型动作（旧式：&lt;code&gt;(T)expression&lt;/code&gt;、&lt;code&gt;T(expression)&lt;/code&gt;；新式：&lt;code&gt;const_cast&amp;#x3C;T&gt;(expression)&lt;/code&gt;、&lt;code&gt;dynamic_cast&amp;#x3C;T&gt;(expression)&lt;/code&gt;、&lt;code&gt;reinterpret_cast&amp;#x3C;T&gt;(expression)&lt;/code&gt;、&lt;code&gt;static_cast&amp;#x3C;T&gt;(expression)&lt;/code&gt;、；尽量避免转型、注重效率避免 dynamic_casts、尽量设计成无需转型、可把转型封装成函数、宁可用新式转型）&lt;/li&gt;
&lt;li&gt;避免使用 handles（包括 引用、指针、迭代器）指向对象内部（以增加封装性、使 const 成员函数的行为更像 const、降低 “虚吊号码牌”（dangling handles，如悬空指针等）的可能性）&lt;/li&gt;
&lt;li&gt;为 “异常安全” 而努力是值得的（异常安全函数（Exception-safe functions）即使发生异常也不会泄露资源或允许任何数据结构败坏，分为三种可能的保证：基本型、强列型、不抛异常型）&lt;/li&gt;
&lt;li&gt;透彻了解 inlining 的里里外外（inlining 在大多数 C++ 程序中是编译期的行为；inline 函数是否真正 inline，取决于编译器；大部分编译器拒绝太过复杂（如带有循环或递归）的函数 inlining，而所有对 virtual 函数的调用（除非是最平淡无奇的）也都会使 inlining 落空；inline 造成的代码膨胀可能带来效率损失；inline 函数无法随着程序库的升级而升级）&lt;/li&gt;
&lt;li&gt;将文件间的编译依存关系降至最低（如果使用 object references 或 object pointers 可以完成任务，就不要使用 objects；如果能够，尽量以 class 声明式替换 class 定义式；为声明式和定义式提供不同的头文件）&lt;/li&gt;
&lt;li&gt;确定你的 public 继承塑模出 is-a（是一种）关系（适用于 base classes 身上的每一件事情一定适用于 derived classes 身上，因为每一个 derived class 对象也都是一个 base class 对象）&lt;/li&gt;
&lt;li&gt;避免遮掩继承而来的名字（可使用 using 声明式或转交函数（forwarding functions）来让被遮掩的名字再见天日）&lt;/li&gt;
&lt;li&gt;区分接口继承和实现继承（在 public 继承之下，derived classes 总是继承 base class 的接口；pure virtual 函数只具体指定接口继承；非纯 impure virtual 函数具体指定接口继承及缺省实现继承；non-virtual 函数具体指定接口继承以及强制性实现继承）&lt;/li&gt;
&lt;li&gt;考虑 virtual 函数以外的其他选择（如 Template Method 设计模式的 non-virtual interface（NVI）手法，将 virtual 函数替换为 “函数指针成员变量”，以 &lt;code&gt;tr1::function&lt;/code&gt; 成员变量替换 virtual 函数，将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数）&lt;/li&gt;
&lt;li&gt;绝不重新定义继承而来的 non-virtual 函数&lt;/li&gt;
&lt;li&gt;绝不重新定义继承而来的缺省参数值，因为缺省参数值是静态绑定（statically bound），而 virtual 函数却是动态绑定（dynamically bound）&lt;/li&gt;
&lt;li&gt;通过复合塑模 has-a（有一个）或 “根据某物实现出”（在应用域（application domain），复合意味 has-a（有一个）；在实现域（implementation domain），复合意味着 is-implemented-in-terms-of（根据某物实现出））&lt;/li&gt;
&lt;li&gt;明智而审慎地使用 private 继承（private 继承意味着 is-implemented-in-terms-of（根据某物实现出），尽可能使用复合，当 derived class 需要访问 protected base class 的成员，或需要重新定义继承而来的时候 virtual 函数，或需要 empty base 最优化时，才使用 private 继承）&lt;/li&gt;
&lt;li&gt;明智而审慎地使用多重继承（多继承比单一继承复杂，可能导致新的歧义性，以及对 virtual 继承的需要，但确有正当用途，如 “public 继承某个 interface class” 和 “private 继承某个协助实现的 class”；virtual 继承可解决多继承下菱形继承的二义性问题，但会增加大小、速度、初始化及赋值的复杂度等等成本）&lt;/li&gt;
&lt;li&gt;了解隐式接口和编译期多态（class 和 templates 都支持接口（interfaces）和多态（polymorphism）；class 的接口是以签名为中心的显式的（explicit），多态则是通过 virtual 函数发生于运行期；template 的接口是奠基于有效表达式的隐式的（implicit），多态则是通过 template 具现化和函数重载解析（function overloading resolution）发生于编译期）&lt;/li&gt;
&lt;li&gt;了解 typename 的双重意义（声明 template 类型参数是，前缀关键字 class 和 typename 的意义完全相同；请使用关键字 typename 标识嵌套从属类型名称，但不得在基类列（base class lists）或成员初值列（member initialization list）内以它作为 base class 修饰符）&lt;/li&gt;
&lt;li&gt;学习处理模板化基类内的名称（可在 derived class templates 内通过 &lt;code&gt;this-&gt;&lt;/code&gt; 指涉 base class templates 内的成员名称，或藉由一个明白写出的 “base class 资格修饰符” 完成）&lt;/li&gt;
&lt;li&gt;将与参数无关的代码抽离 templates（因类型模板参数（non-type template parameters）而造成代码膨胀往往可以通过函数参数或 class 成员变量替换 template 参数来消除；因类型参数（type parameters）而造成的代码膨胀往往可以通过让带有完全相同二进制表述（binary representations）的实现类型（instantiation types）共享实现码）&lt;/li&gt;
&lt;li&gt;运用成员函数模板接受所有兼容类型（请使用成员函数模板（member function templates）生成 “可接受所有兼容类型” 的函数；声明 member templates 用于 “泛化 copy 构造” 或 “泛化 assignment 操作” 时还需要声明正常的 copy 构造函数和 copy assignment 操作符）&lt;/li&gt;
&lt;li&gt;需要类型转换时请为模板定义非成员函数（当我们编写一个 class template，而它所提供之 “与此 template 相关的” 函数支持 “所有参数之隐式类型转换” 时，请将那些函数定义为 “class template 内部的 friend 函数”）&lt;/li&gt;
&lt;li&gt;请使用 traits classes 表现类型信息（traits classes 通过 templates 和 “templates 特化” 使得 “类型相关信息” 在编译期可用，通过重载技术（overloading）实现在编译期对类型执行 if...else 测试）&lt;/li&gt;
&lt;li&gt;认识 template 元编程（模板元编程（TMP，template metaprogramming）可将工作由运行期移往编译期，因此得以实现早期错误侦测和更高的执行效率；TMP 可被用来生成 “给予政策选择组合”（based on combinations of policy choices）的客户定制代码，也可用来避免生成对某些特殊类型并不适合的代码）&lt;/li&gt;
&lt;li&gt;了解 new-handler 的行为（set_new_handler 允许客户指定一个在内存分配无法获得满足时被调用的函数；nothrow new 是一个颇具局限的工具，因为它只适用于内存分配（operator new），后继的构造函数调用还是可能抛出异常）&lt;/li&gt;
&lt;li&gt;了解 new 和 delete 的合理替换时机（为了检测运用错误、收集动态分配内存之使用统计信息、增加分配和归还速度、降低缺省内存管理器带来的空间额外开销、弥补缺省分配器中的非最佳齐位、将相关对象成簇集中、获得非传统的行为）&lt;/li&gt;
&lt;li&gt;编写 new 和 delete 时需固守常规（operator new 应该内涵一个无穷循环，并在其中尝试分配内存，如果它无法满足内存需求，就应该调用 new-handler，它也应该有能力处理 0 bytes 申请，class 专属版本则还应该处理 “比正确大小更大的（错误）申请”；operator delete 应该在收到 null 指针时不做任何事，class 专属版本则还应该处理 “比正确大小更大的（错误）申请”）&lt;/li&gt;
&lt;li&gt;写了 placement new 也要写 placement delete（当你写一个 placement operator new，请确定也写出了对应的 placement operator delete，否则可能会发生隐微而时断时续的内存泄漏；当你声明 placement new 和 placement delete，请确定不要无意识（非故意）地遮掩了它们地正常版本）&lt;/li&gt;
&lt;li&gt;不要轻忽编译器的警告&lt;/li&gt;
&lt;li&gt;让自己熟悉包括 TR1 在内的标准程序库（TR1，C++ Technical Report 1，C++11 标准的草稿文件）&lt;/li&gt;
&lt;li&gt;让自己熟悉 Boost（准标准库）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;More Effective C++&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;仔细区别 pointers 和 references（当你知道你需要指向某个东西，而且绝不会改变指向其他东西，或是当你实现一个操作符而其语法需求无法由 pointers 达成，你就应该选择 references；任何其他时候，请采用 pointers）&lt;/li&gt;
&lt;li&gt;最好使用 C++ 转型操作符（&lt;code&gt;static_cast&lt;/code&gt;、&lt;code&gt;const_cast&lt;/code&gt;、&lt;code&gt;dynamic_cast&lt;/code&gt;、&lt;code&gt;reinterpret_cast&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;绝不要以多态（polymorphically）方式处理数组（多态（polymorphism）和指针算术不能混用；数组对象几乎总是会涉及指针的算术运算，所以数组和多态不要混用）&lt;/li&gt;
&lt;li&gt;非必要不提供 default constructor（避免对象中的字段被无意义地初始化）&lt;/li&gt;
&lt;li&gt;对定制的 “类型转换函数” 保持警觉（单自变量 constructors 可通过简易法（explicit 关键字）或代理类（proxy classes）来避免编译器误用；隐式类型转换操作符可改为显式的 member function 来避免非预期行为）&lt;/li&gt;
&lt;li&gt;区别 increment/decrement 操作符的前置（prefix）和后置（postfix）形式（前置式累加后取出，返回一个 reference；后置式取出后累加，返回一个 const 对象；处理用户定制类型时，应该尽可能使用前置式 increment；后置式的实现应以其前置式兄弟为基础）&lt;/li&gt;
&lt;li&gt;千万不要重载 &lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt;，&lt;code&gt;||&lt;/code&gt; 和 &lt;code&gt;,&lt;/code&gt; 操作符（&lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt; 与 &lt;code&gt;||&lt;/code&gt; 的重载会用 “函数调用语义” 取代 “骤死式语义”；&lt;code&gt;,&lt;/code&gt; 的重载导致不能保证左侧表达式一定比右侧表达式更早被评估）&lt;/li&gt;
&lt;li&gt;了解各种不同意义的 new 和 delete（&lt;code&gt;new operator&lt;/code&gt;、&lt;code&gt;operator new&lt;/code&gt;、&lt;code&gt;placement new&lt;/code&gt;、&lt;code&gt;operator new[]&lt;/code&gt;；&lt;code&gt;delete operator&lt;/code&gt;、&lt;code&gt;operator delete&lt;/code&gt;、&lt;code&gt;destructor&lt;/code&gt;、&lt;code&gt;operator delete[]&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;利用 destructors 避免泄漏资源（在 destructors 释放资源可以避免异常时的资源泄漏）&lt;/li&gt;
&lt;li&gt;在 constructors 内阻止资源泄漏（由于 C++ 只会析构已构造完成的对象，因此在构造函数可以使用 try...catch 或者 auto_ptr（以及与之相似的 classes） 处理异常时资源泄露问题）&lt;/li&gt;
&lt;li&gt;禁止异常流出 destructors 之外（原因：一、避免 terminate 函数在 exception 传播过程的栈展开（stack-unwinding）机制种被调用；二、协助确保 destructors 完成其应该完成的所有事情）&lt;/li&gt;
&lt;li&gt;了解 “抛出一个 exception” 与 “传递一个参数” 或 “调用一个虚函数” 之间的差异（第一，exception objects 总是会被复制（by pointer 除外），如果以 by value 方式捕捉甚至被复制两次，而传递给函数参数的对象则不一定得复制；第二，“被抛出成为 exceptions” 的对象，其被允许的类型转换动作比 “被传递到函数去” 的对象少；第三，catch 子句以其 “出现于源代码的顺序” 被编译器检验对比，其中第一个匹配成功者便执行，而调用一个虚函数，被选中执行的是那个 “与对象类型最佳吻合” 的函数）&lt;/li&gt;
&lt;li&gt;以 by reference 方式捕获 exceptions（可避免对象删除问题、exception objects 的切割问题，可保留捕捉标准 exceptions 的能力，可约束 exception object 需要复制的次数）&lt;/li&gt;
&lt;li&gt;明智运用 exception specifications（exception specifications 对 “函数希望抛出什么样的 exceptions” 提供了卓越的说明；也有一些缺点，包括编译器只对它们做局部性检验而很容易不经意地违反，与可能会妨碍更上层的 exception 处理函数处理未预期的 exceptions）&lt;/li&gt;
&lt;li&gt;了解异常处理的成本（粗略估计，如果使用 try 语句块，代码大约整体膨胀 5%-10%，执行速度亦大约下降这个数；因此请将你对 try 语句块和 exception specifications 的使用限制于非用不可的地点，并且在真正异常的情况下才抛出 exceptions）&lt;/li&gt;
&lt;li&gt;谨记 80-20 法则（软件的整体性能几乎总是由其构成要素（代码）的一小部分决定的，可使用程序分析器（program profiler）识别出消耗资源的代码）&lt;/li&gt;
&lt;li&gt;考虑使用 lazy evaluation（缓式评估）（可应用于：Reference Counting（引用计数）来避免非必要的对象复制、区分 operator[] 的读和写动作来做不同的事情、Lazy Fetching（缓式取出）来避免非必要的数据库读取动作、Lazy Expression Evaluation（表达式缓评估）来避免非必要的数值计算动作）&lt;/li&gt;
&lt;li&gt;分期摊还预期的计算成本（当你必须支持某些运算而其结构几乎总是被需要，或其结果常常被多次需要的时候，over-eager evaluation（超急评估）可以改善程序效率）&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>网络编程与 I/O 多路复用</title><link>https://coooredump.github.io/blog/cpp/network-programming-and-io-multiplexing</link><guid isPermaLink="true">https://coooredump.github.io/blog/cpp/network-programming-and-io-multiplexing</guid><description>网络编程实战与源码分析</description><pubDate>Sun, 13 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;网络编程&lt;/h2&gt;
&lt;h3&gt;Socket&lt;/h3&gt;
&lt;p&gt;如果我们要将数据从电脑 A 的某个进程发到电脑 B 的某个进程，如果需要确保数据能够发送给对方，那就选可靠的 TCP 协议，否则可以采用 UDP 协议。&lt;/p&gt;
&lt;p&gt;那这时候就需要用 socket 进行编程，第一步就是创建一个关于 TCP 的 socket。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;下文皆以 TCP 为例&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// SOCK_STREAM：TCP 流套接字
// SOCK_DGRAM：UDP 数据报套接字
int sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sock_fd&lt;/code&gt; 相当于文件句柄，客户端和服务端都需要各自创建 &lt;code&gt;fd&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于服务端：就可以根据 &lt;code&gt;fd&lt;/code&gt; 依次执行 &lt;code&gt;bind()&lt;/code&gt;、&lt;code&gt;listen()&lt;/code&gt;、&lt;code&gt;accept()&lt;/code&gt; 方法，然后坐等客户端的连接请求&lt;/li&gt;
&lt;li&gt;对于客户端：根据 &lt;code&gt;fd&lt;/code&gt; 来执行 &lt;code&gt;connect()&lt;/code&gt; 向服务端发起建立连接的请求，此时就会发生 TCP 三次握手&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505162322110.gif&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;以下是 TCP 三次握手的示意图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505162024496.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;连接建立完成后，客户端可以执行 &lt;code&gt;send()&lt;/code&gt; 发送消息，服务端可以执行 &lt;code&gt;recv()&lt;/code&gt; 接收消息；反过来，服务端也可以执行 &lt;code&gt;send()&lt;/code&gt;，客户端执行 &lt;code&gt;recv()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;相关函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int socket(int __domain, int __type, int __protocol)
int bind(int __fd, const struct sockaddr *__addr, socklen_t __len)
int listen(int __fd, int __n)
int connect(int __fd, const struct sockaddr *__addr, socklen_t __len)
int accept(int __fd, struct sockaddr *__restrict__ __addr, socklen_t *__restrict__ __addr_len)
ssize_t recv(int __fd, void *__buf, size_t __n, int __flags)
ssize_t send(int __fd, const void *__buf, size_t __n, int __flags)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;服务端&lt;/h3&gt;
&lt;p&gt;可以使用 &lt;code&gt;socket()&lt;/code&gt; 系统调用创建套接字，它在 &lt;code&gt;&amp;#x3C;sys/socket.h&gt;&lt;/code&gt; 中定义。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Definition&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int socket(int __domain, int __type, int __protocol)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__domain&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AF_INET&lt;/code&gt;：IPv4&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AF_INET6&lt;/code&gt;：IPv6&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__type&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SOCK_STREAM&lt;/code&gt;：TCP 流套接字&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SOCK_DGRAM&lt;/code&gt;：UDP 数据报套接字&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__protocol&lt;/code&gt;：指定协议，当 &lt;code&gt;__protocol&lt;/code&gt; 为 0 时，会自动选择 &lt;code&gt;type&lt;/code&gt; 类型对应的默认协议。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IPPROTO_TCP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IPPTOTO_UDP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IPPROTO_SCTP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IPPROTO_TIPC&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Usage&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
// 定义服务器地址
sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons(8080);
serverAddress.sin_addr.s_addr = INADDR_ANY;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sockaddr_in&lt;/code&gt;：用于存储套接字地址的数据类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;htons&lt;/code&gt;：该函数用于将 &lt;code&gt;unsigned int&lt;/code&gt; 从机器字节序转换为网络字节序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INADDR_ANY&lt;/code&gt;：当我们不想将套接字绑定到任何特定网卡 IP，而是让它监听所有网卡（所有可用 IP）时使用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;bind(serverSocket, (struct sockaddr*)&amp;#x26;serverAddress, sizeof(serverAddress));
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;调用 &lt;code&gt;bind()&lt;/code&gt; 绑定套接字&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;listen(serverSocket, 5);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;然后监听 &lt;code&gt;serverSocket&lt;/code&gt; 引用的套接字&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int clientSocket = accept(serverSocket, nullptr, nullptr);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;accept()&lt;/code&gt; 调用用于接受应用程序正在监听的套接字上收到的连接请求&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char buffer[1024] = {0};
recv(clientSocket, buffer, sizeof(buffer), 0);
cout &amp;#x3C;&amp;#x3C; &quot;Message from client: &quot; &amp;#x3C;&amp;#x3C; buffer &amp;#x3C;&amp;#x3C; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;然后开始从客户端接收数据，我们可以指定所需的缓冲区大小，以便有足够的空间接收客户端发送的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;close(serverSocket);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;最后使用 &lt;code&gt;close()&lt;/code&gt; 关闭套接字&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;server.cpp&lt;/em&gt; 完整代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// C++ program to show the example of server application in
// socket programming
#include &amp;#x3C;cstring&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;netinet/in.h&gt;
#include &amp;#x3C;sys/socket.h&gt;
#include &amp;#x3C;unistd.h&gt;

using namespace std;

int main()
{
    // creating socket
    int serverSocket = socket(AF_INET, SOCK_STREAM, 0);

    // specifying the address
    sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_port = htons(8080);
    serverAddress.sin_addr.s_addr = INADDR_ANY;

    // binding socket.
    bind(serverSocket, (struct sockaddr*)&amp;#x26;serverAddress,
         sizeof(serverAddress));

    // listening to the assigned socket
    listen(serverSocket, 5);

    // accepting connection request
    int clientSocket = accept(serverSocket, nullptr, nullptr);

    // recieving data
    char buffer[1024] = { 0 };
    recv(clientSocket, buffer, sizeof(buffer), 0);
    cout &amp;#x3C;&amp;#x3C; &quot;Message from client: &quot; &amp;#x3C;&amp;#x3C; buffer &amp;#x3C;&amp;#x3C; endl;

    // closing the socket
    close(serverSocket);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;更完整的写法：&lt;em&gt;server.c&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;errno.h&gt;
#include &amp;#x3C;netinet/in.h&gt;
#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;stdlib.h&gt;
#include &amp;#x3C;sys/socket.h&gt;
#include &amp;#x3C;unistd.h&gt;

int main() {
    // int socket(int __domain, int __type, int __protocol)
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 0.0.0.0
    server_addr.sin_port = htons(2000);               // system used: 0 ~ 1023

    if (bind(sock_fd, (struct sockaddr*)&amp;#x26;server_addr, sizeof(struct sockaddr)) == -1) {
        printf(&quot;bind failed: %s\n&quot;, strerror(errno));
    }

    // int listen(int __fd, int __n): 后者为等待队列的长度
    listen(sock_fd, 10);
    printf(&quot;listen finished: %d\n&quot;, sock_fd);  // 3
    system(&quot;netstat -ano | grep 2000&quot;);
    // getchar();  // 方便查看 netstat -ano | grep 2000: tcp  0    0 0.0.0.0:2000   0.0.0.0:*   LISTEN   off (0.00/0/0)

    struct sockaddr_in client_addr;
    // int accept(int __fd, struct sockaddr *__restrict__ __addr, socklen_t *__restrict__ __addr_len)
    socklen_t len = sizeof(client_addr);
    int client_fd = accept(sock_fd, (struct sockaddr*)&amp;#x26;client_addr, &amp;#x26;len);
    printf(&quot;accept finished\n&quot;);
    system(&quot;netstat -ano | grep 2000&quot;);

    char buffer[1024] = {0};
    // ssize_t recv(int __fd, void *__buf, size_t __n, int __flags)
    int count = recv(client_fd, buffer, sizeof(buffer), 0);
    printf(&quot;RECV: %s\n&quot;, buffer);

    // ssize_t send(int __fd, const void *__buf, size_t __n, int __flags)
    count = send(client_fd, buffer, count, 0);
    printf(&quot;SEND: %d\n&quot;, count);

    // getchar();
    while (1);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;客户端&lt;/h3&gt;
&lt;p&gt;与服务器类似，我们也需要创建一个套接字并指定地址。不过，我们不会接受请求，而是在能够使用 &lt;code&gt;connect()&lt;/code&gt; 调用发送数据时，发送连接请求。然后我们使用 &lt;code&gt;send()&lt;/code&gt; 函数发送数据。所有操作完成后，我们使用 &lt;code&gt;close()&lt;/code&gt; 函数关闭连接。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;client.cpp&lt;/em&gt; 完整代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// C++ program to illustrate the client application in the
// socket programming
#include &amp;#x3C;cstring&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;netinet/in.h&gt;
#include &amp;#x3C;sys/socket.h&gt;
#include &amp;#x3C;unistd.h&gt;

int main()
{
    // creating socket
    int clientSocket = socket(AF_INET, SOCK_STREAM, 0);

    // specifying address
    sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_port = htons(8080);
    serverAddress.sin_addr.s_addr = INADDR_ANY;

    // sending connection request
    connect(clientSocket, (struct sockaddr*)&amp;#x26;serverAddress,
            sizeof(serverAddress));

    // sending data
    const char* message = &quot;Hello, server!&quot;;
    send(clientSocket, message, strlen(message), 0);

    // closing socket
    close(clientSocket);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;更完整的写法：&lt;em&gt;client.c&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;netinet/in.h&gt;
#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;sys/socket.h&gt;

int main() {
    unsigned short port = 2000;
    // char *server_ip = &quot;10.26.57.8&quot;;      // 应该发送到对应IP的网卡地址: 10.26.57.3
    // char *server_ip = &quot;localhost&quot;;       // ✅ 两个程序在同一个服务器上跑就可
    char *server_ip = &quot;10.26.57.3&quot;;         // ✅ 网卡地址能接收到

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd &amp;#x3C; 0) {
        perror(&quot;socket error&quot;);
        exit(-1);
    }

    struct sockaddr_in server_addr;
    // bzero &amp;#x3C;==&gt; memset(&amp;#x26;server_addr, 0, sizeof(server_addr));
    bzero(&amp;#x26;server_addr, sizeof(server_addr));  // 初始化服务器地址
    // AF_INET:  IPv4
    // AF_INET6: IPv6
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    // inet_pton() 是一个通用的地址转换函数，不知道你打算转换成 IPv4 还是 IPv6，必须由你告诉它目标地址族
    // AF_INET:  IPv4
    // AF_INET6: IPv6
    // 客户端绑定具体服务端IP必须使用该方法
    inet_pton(AF_INET, server_ip, &amp;#x26;server_addr.sin_addr.s_addr);
    system(&quot;netstat -ano | grep 2000&quot;);

    // int connect(int __fd, const struct sockaddr *__addr, socklen_t __len)
    if (connect(sockfd, (struct sockaddr *)&amp;#x26;server_addr, sizeof(server_addr)) == -1) {
        perror(&quot;connect error&quot;);
        close(sockfd);
        exit(-1);
    }

    system(&quot;netstat -ano | grep 2000&quot;);

    char buffer[1024] = &quot;Client INFO: test...&quot;;
    // ssize_t send(int __fd, const void *__buf, size_t __n, int __flags)
    int count = send(sockfd, buffer, sizeof(buffer), 0);
    printf(&quot;SEND: %d\n&quot;, count);

    while (1);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;源码分析&lt;/h3&gt;
&lt;h4&gt;sockaddr&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;sockaddr&lt;/code&gt; 在头文件 &lt;code&gt;&amp;#x3C;sys/socket.h&gt;&lt;/code&gt; 中定义，&lt;code&gt;sockaddr&lt;/code&gt; 的缺陷是 &lt;code&gt;sa_data&lt;/code&gt; 把「目标地址」和「端口信息」混在一起了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct sockaddr
{ 
　　unsigned short sa_family;	// 2 字节，地址族，AF_xxx
　　char sa_data[14]; 		// 14 字节，包含套接字中的目标地址和端口信息 
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;sockaddr_in&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;sockaddr_in&lt;/code&gt; 在头文件 &lt;code&gt;&amp;#x3C;netinet/in.h&gt;&lt;/code&gt; 或 &lt;code&gt;&amp;#x3C;arpa/inet.h&gt;&lt;/code&gt; 中定义，该结构体解决了 &lt;code&gt;sockaddr&lt;/code&gt; 的缺陷，把 &lt;code&gt;port&lt;/code&gt; 和 &lt;code&gt;addr&lt;/code&gt; 分开存储在两个变量中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct sockaddr_in {
    sa_family_t    sin_family; // 地址族，一般是 AF_INET（IPv4），也可以是 AF_INET6（IPv6）
    in_port_t      sin_port;   // 端口号（使用 htons() 转换为网络字节序）
    struct in_addr sin_addr;   // IP 地址（结构体）
    char           sin_zero[8];// 填充字段，保持结构体大小与 sockaddr 一致
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct in_addr {
    unsigned long s_addr;      // 32 位 IPv4 地址打印的时候可以调用 inet_ntoa() 函数将其转换为 char* 类型
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sockaddr&lt;/code&gt; 常用于 &lt;code&gt;bind&lt;/code&gt;、&lt;code&gt;connect&lt;/code&gt;、&lt;code&gt;recvfrom&lt;/code&gt;、&lt;code&gt;sendto&lt;/code&gt; 等函数的参数，指明地址信息，是一种通用的套接字地址。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sockaddr_in&lt;/code&gt; 是 &lt;code&gt;internet&lt;/code&gt; 环境下套接字的地址形式。所以在网络编程中我们会对 &lt;code&gt;sockaddr_in&lt;/code&gt; 结构体进行操作，使用 &lt;code&gt;sockaddr_in&lt;/code&gt; 来建立所需的信息，最后使用类型转化 &lt;code&gt;(struct sockaddr*)&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;一般先把 &lt;code&gt;sockaddr_in&lt;/code&gt; 变量赋值后，强制类型转换后传入用 &lt;code&gt;sockaddr&lt;/code&gt; 做参数的函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sockaddr_in&lt;/code&gt; 用于 &lt;code&gt;socket&lt;/code&gt; 定义和赋值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sockaddr&lt;/code&gt; 用于函数参数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Usage&lt;/strong&gt;：程序员不应该操作 &lt;code&gt;sockaddr&lt;/code&gt;，而是使用 &lt;code&gt;sockaddr_in&lt;/code&gt; 来表示地址，并强转为 &lt;code&gt;(struct sockaddr *)&lt;/code&gt; 传入函数中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// int accept(int __fd, struct sockaddr *__restrict__ __addr, socklen_t *__restrict__ __addr_len)
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);

int client_fd = accept(sock_fd, (struct sockaddr*)&amp;#x26;client_addr, &amp;#x26;len);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// int connect(int __fd, const struct sockaddr *__addr, socklen_t __len)
struct sockaddr_in server_addr;
bzero(&amp;#x26;server_addr, sizeof(server_addr));  // 初始化服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(2000);
inet_pton(AF_INET, &quot;10.26.57.3&quot;, &amp;#x26;server_addr.sin_addr.s_addr);

connect(sockfd, (struct sockaddr *)&amp;#x26;server_addr, sizeof(server_addr)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;讲一讲 I/O 多路复用｜select、poll、epoll 的区别是什么？&lt;/h2&gt;
&lt;p&gt;I/O 多路复用是一种 I/O 的处理方式，指的是&lt;strong&gt;复用一个线程处理多个 socket 中的事件&lt;/strong&gt;。能够复用资源，防止创建过多线程导致的上下文切换的开销。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010129252.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们熟悉的 select/poll/epoll 内核提供给用户态的&lt;strong&gt;多路复用系统调用&lt;/strong&gt;，进程可以通过一个系统调用函数从内核中获取多个事件。&lt;/p&gt;
&lt;p&gt;select/poll/epoll 是如何获取网络事件的呢？在获取事件时，先把所有连接（文件描述符）传给内核，再由内核返回产生了事件的连接，然后在用户态中再处理这些连接对应的请求即可。&lt;/p&gt;
&lt;h3&gt;select、poll&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ select 图解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010215740.png&quot; alt=&quot;image-20250501021504644&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ poll 图解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010216153.png&quot; alt=&quot;image-20250501021646782&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;select&lt;/code&gt; 实现多路复用的方式是，将已连接的 Socket 都放到一个文件描述符集合，然后调用 select 函数将文件描述符集合拷贝到内核里，让内核来检查是否有网络事件产生，检查的方式很粗暴，就是通过遍历文件描述符集合的方式，当检查到有事件产生后，将此 Socket 标记为可读或可写， 接着再把整个文件描述符集合拷贝回用户态里，然后用户态还需要再通过遍历的方法找到可读或可写的 Socket，然后再对其处理。&lt;/p&gt;
&lt;p&gt;所以，对于 select 这种方式，需要进行 2 次「遍历」文件描述符集合，一次是在内核态里，一个次是在用户态里 ，而且还会发生 2 次「拷贝」文件描述符集合，先从用户空间传入内核空间，由内核修改后，再传出到用户空间中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;select&lt;/code&gt; 使用固定长度的 BitsMap，表示文件描述符集合，而且所支持的文件描述符的个数是有限制的，&lt;strong&gt;在 Linux 系统中，由内核中的 &lt;code&gt;FD_SETSIZE&lt;/code&gt; 限制， 默认最大值为 1024，只能监听 0~1023 的文件描述符&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;poll&lt;/code&gt; 不再用 BitsMap 来存储所关注的文件描述符，取而代之用动态数组，以链表形式来组织，突破了 select 的文件描述符个数限制，当然还会受到系统文件描述符限制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是 poll 和 select 并没有太大的本质区别，都是使用「线性结构」存储进程关注的 Socket 集合&lt;/strong&gt;，因此都需要遍历文件描述符集合来找到可读或可写的 Socket，时间复杂度为 $O(n)$，而且也需要在用户态与内核态之间拷贝文件描述符集合，&lt;strong&gt;这种方式随着并发数上来，性能的损耗会呈指数级增长&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;epoll&lt;/h3&gt;
&lt;p&gt;Linux 2.6 版本诞生了 epoll 模型，彻底解决了 select/poll 性能不足的问题&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ epoll 图解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010232627.png&quot; alt=&quot;image-20250501023238509&quot;&gt;&lt;/p&gt;
&lt;p&gt;先复习下 &lt;code&gt;epoll&lt;/code&gt; 的用法。如下的代码中，先用 &lt;code&gt;epoll_create&lt;/code&gt; 创建一个 epoll 对象 &lt;code&gt;epoll_fd&lt;/code&gt;，再通过 &lt;code&gt;epoll_ctl&lt;/code&gt; 将需要监视的 socket 添加到 &lt;code&gt;epoll_fd&lt;/code&gt; 中，最后调用 &lt;code&gt;epoll_wait&lt;/code&gt; 等待数据。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...);

// epoll_fd
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); // 将所有需要监听的 socket 添加到 epfd 中

while(1) {
    int n = epoll_wait(...);
    for (接收到数据的 socket) {
        //处理
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;epoll&lt;/code&gt; 通过两个方面，很好解决了 select/poll 的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一点，&lt;strong&gt;epoll 在内核里使用「红黑树」来跟踪进程所有待检测的文件描述字&lt;/strong&gt;，把需要监控的 socket 通过 &lt;code&gt;epoll_ctl()&lt;/code&gt; 函数加入内核中的红黑树里，红黑树是个高效的数据结构，增删改一般时间复杂度是 $O(logn)$。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构，所以 select/poll 每次操作时都传入整个 socket 集合给内核，而 epoll 因为在内核维护了红黑树，可以保存所有待检测的 socket ，所以只需要传入一个待检测的 socket，减少了内核和用户空间大量的数据拷贝和内存分配。&lt;/li&gt;
&lt;li&gt;第二点，&lt;strong&gt;epoll 使用事件驱动的机制&lt;/strong&gt;，内核里维护了一个链表来记录就绪事件，当某个 socket 有事件发生时，内核通过&lt;strong&gt;回调函数&lt;/strong&gt;将其加入到这个就绪事件列表中，当用户调用 &lt;code&gt;epoll_wait()&lt;/code&gt; 函数时，只会返回有事件发生的文件描述符的个数，不需要像 select/poll 那样轮询扫描整个 socket 集合，大大提高了检测的效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从下图你可以看到 epoll 相关的接口作用：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202505010200863.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;epoll&lt;/code&gt; 的方式即使监听的 Socket 数量越多的时候，效率不会大幅度降低，能够同时监听的 Socket 的数目也非常的多了，上限就为系统定义的进程打开的最大文件描述符个数。因而，epoll 被称为解决 &lt;strong&gt;C10K&lt;/strong&gt; 问题（服务器同时处理 10,000个 客户端连接的挑战）的利器。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>技术人求职指南</title><link>https://coooredump.github.io/blog/recruitment/job-hunting-guide-for-technical-personnel</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/job-hunting-guide-for-technical-personnel</guid><description>始于求职，却不止步于求职，虽然未曾包罗职场所有的软技能，但是对于技术人职场这个板块，许多方面都有所涉及。同时，尽管整个小册未牵扯任何技术内容，看着学起来难度不大，但其中无一不是实用的技巧分享，重要性远超你去学习某个单一的技术，能得到的回报也绝对会远超你所花费的成本～</description><pubDate>Sun, 13 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;小册链接：https://juejin.cn/book/7211868947363135545&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;序言：求职市场凛冬已至，技术人该如何杀出重围&lt;/h2&gt;
&lt;p&gt;整个小册内容可分为 &lt;code&gt;求职前&lt;/code&gt;、&lt;code&gt;求职中&lt;/code&gt;、&lt;code&gt;求职后&lt;/code&gt;、&lt;code&gt;入职后&lt;/code&gt; &lt;strong&gt;四个阶段&lt;/strong&gt;，具体如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202306021058457.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;本小册始于求职，却不止步于求职，虽然未曾包罗职场所有的软技能，但是对于技术人职场这个板块，许多方面都有所涉及。同时，尽管整个小册未牵扯任何技术内容，看着学起来难度不大，但其中无一不是实用的技巧分享，重要性远超你去学习某个单一的技术，能得到的回报也绝对会远超你所花费的成本。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;整体收益清单&lt;/code&gt;如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工作多年的技术人，能给自己做好技术总结与定位；&lt;/li&gt;
&lt;li&gt;能够掌握平时学习、面试前复盘的技巧与方法论；&lt;/li&gt;
&lt;li&gt;能搞懂招聘方的用人需求、&lt;code&gt;HR&lt;/code&gt;筛选简历的具体过程；&lt;/li&gt;
&lt;li&gt;学会简历优化的技巧，打造一份最适合自己的简历；&lt;/li&gt;
&lt;li&gt;理解投递简历的时机、技巧，增多自己的面试机会；&lt;/li&gt;
&lt;li&gt;能摸透求职面试过程中的面试官，到底是什么角色；&lt;/li&gt;
&lt;li&gt;学会消除面试前的焦虑，及调研要入职的目标公司；&lt;/li&gt;
&lt;li&gt;掌握各类面试场景中的引导、控场技巧、忌讳点；&lt;/li&gt;
&lt;li&gt;能从容应对&lt;code&gt;HR&lt;/code&gt;面中的各类人事问题，以及谈薪技巧；&lt;/li&gt;
&lt;li&gt;学会面试之后如何对面试进行总结、复盘与自我优化；&lt;/li&gt;
&lt;li&gt;多个&lt;code&gt;Offer&lt;/code&gt;在手时，学会自行判断哪个才最适合自己；&lt;/li&gt;
&lt;li&gt;作为新人入职时，如何快速融入新环境和安稳转正；&lt;/li&gt;
&lt;li&gt;具备做职业规划的能力，合理安排职业的生涯路线；&lt;/li&gt;
&lt;li&gt;大龄程序员该怎样避免中年危机，保持核心竞争力；&lt;/li&gt;
&lt;li&gt;如果被提拔成技术管理层，如何才能带好自己的团队；&lt;/li&gt;
&lt;li&gt;学会如何优雅地向上级领导、公司提出涨薪与离职；&lt;/li&gt;
&lt;li&gt;当工作出现意外状况，学会利用法律进行合理维权；&lt;/li&gt;
&lt;li&gt;了解如何实现“睡后收入”，避开“副业兼职”的坑。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;自我认知篇：作为技术人该如何定位自己在行业内的级别？&lt;/h2&gt;
&lt;h3&gt;一、如何理解技术行业内的级别？&lt;/h3&gt;
&lt;p&gt;人分三六九等，技术行业内的潜规则同样如此。&lt;/p&gt;
&lt;p&gt;不同行业对于技术工种的等级划分，具体职称上也许会有不同，但殊归同途，凡是涉及到技术性的岗位，都能被分为“&lt;strong&gt;&lt;code&gt;初级、中级、高级、资深&lt;/code&gt;&lt;/strong&gt;”这四个级别，简单总结如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初级：掌握岗位要求的基本技术，满足日常工作的基本需求（&lt;code&gt;能干活&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;中级：不仅仅只局限于满足基本需求，在广度上也有着丰富认知（&lt;code&gt;能干更多的活&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;高级：对技术不是停留在表面应用，而是在技术的深度上有一定研究（&lt;code&gt;懂原理&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;资深：除开有着广度、深度认知外，能充分了解各个细节并解决问题（&lt;code&gt;懂原理还有实际经验&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看出，越往后级别越高，专业能力也会越强。这里对高级和资深稍作解释：高级更偏向于理论派，懂很多知识但没有太多实战经验；而资深则属于实战派，不仅懂，还具备丰富的经验沉淀，资深既代表了能力，还代表了资历，以及权威性。&lt;/p&gt;
&lt;p&gt;作为技术人的我们，了解这些级别划分后，同时也要清楚自己的定位，否则就会出现如下这种尴尬场景：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;目前处于初级水平，但偏偏学习时在看底层原理，求职路上发现面试官压根不问。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;学习是件好事，但必须掌握科学的方式方法，千万别盲目跟风学习，只有明确了自身的能力定位后，才能带来最大的学习收益。比如，认识到自己目前处于“爬”阶段，那下一阶段则要学习“走”的能力，而跳过“走”直接去学“跑”的方案也并非不可取，只是并没有学“走”给自己带来的收益大罢了。&lt;/p&gt;
&lt;h3&gt;二、技术人该如何明确自我定位？&lt;/h3&gt;
&lt;p&gt;关于明确自我定位的好处在前面已重点说明，一方面有利于清晰地认识自我水平，另一方面则可以确定提升的方向。&lt;/p&gt;
&lt;p&gt;不过自我定位是一个比较虚的概念，针对不同的人、不同的工作年限，实际情况会存在些许差异。为此，下面会先讲明不同年限应该要达到的级别，接着再阐述技术人该如何进行自我总结，最后则会针对一些特殊情况进行讲解。&lt;/p&gt;
&lt;h4&gt;不同年限的工作者应该达到的级别&lt;/h4&gt;
&lt;p&gt;在技术行业中都有一个潜规则，那就是一位从事&lt;code&gt;N&lt;/code&gt;年的人，应该要达到&lt;code&gt;XXX&lt;/code&gt;水平，比如今天公司招了一位具有九年经验的新员工，虽然你与他之间素未谋面，但潜意识下，就会觉得来了个大牛，这也是工作年限带来的“附加&lt;code&gt;Buff&lt;/code&gt;加成”。&lt;/p&gt;
&lt;p&gt;不过虽说年限不一定代表能力，但对于业内的技术人员而言，都可以依据不同年限做出一定判断，在这件事上所有技术人都有一个“共识”，通常来说，不同年限对应的标准如下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工作&lt;code&gt;0~2&lt;/code&gt;年：至少需要达到初级水准。&lt;/li&gt;
&lt;li&gt;工作&lt;code&gt;3~5&lt;/code&gt;年：至少需要达到中级水准。&lt;/li&gt;
&lt;li&gt;工作&lt;code&gt;5~8&lt;/code&gt;年：至少需要达到高级水准。&lt;/li&gt;
&lt;li&gt;工作&lt;code&gt;8&lt;/code&gt;年以上：至少需要达到资深水准。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;至少要将能力与工作年限保持正比，否则一方面薪资不如意，另一方面求职机会也会少上许多&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;技术人该如何做好自我总结？&lt;/h4&gt;
&lt;p&gt;自我总结依旧是一个比较泛的概念，有工作总结、年度总结、人生总结等各色各样的总结，相信大家也一定写过不少，尤其是入职一家要求写日报、周报、月报的企业时，&lt;code&gt;XXX&lt;/code&gt;总结会令人写到麻木。不过我这里提到的并非常规性的自我总结，而是技术人的能力总结，更偏于撰写一份自己的“知识图谱”。&lt;/p&gt;
&lt;p&gt;只有总结了自己的技术能力，才能帮助我们更加清晰地认识自我。但往往很多人都缺乏这种“技术总结”的能力，尤其是随着工作年限的增长，工作中会出现如下场景：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;新入职的工作要用到这个，我得去学一学。
下周接手的项目上会用到&lt;code&gt;XXX&lt;/code&gt;技术，我得抽空去看看。
新项目中要&lt;code&gt;XXX&lt;/code&gt;来做，我得去瞅瞅。
.......&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;相信碰到这种情况的人不在少数，很多时候去学习一项新技术，都会有点身不由己，并不是自己想学，而是工作需要，不得不去掌握！最终就造成了“这也会，那也会，什么都会，但似乎又什么都不会”的尴尬局面出现。&lt;/p&gt;
&lt;p&gt;当遇到这种情况，如果长期处于一家企业不动还好，但当你再次踏上求职路时，此前埋下的诸多弊端就会凸显出来，不知道如何写简历、如何准备面试、不清楚自己该找什么级别的工作……彻底迷失了自我，并失去了前进的方向。&lt;/p&gt;
&lt;p&gt;这种现象往往在工作年限较长的群体中额外明显，&lt;strong&gt;想要走出这个困境，首先得学会对自我的技术进行总结&lt;/strong&gt;！如何总结呢？最有效的方法是：&lt;strong&gt;知识树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;知识树是指通过思维导图之类的软件，例如通过支持在线编辑的 XMind 进行技术归纳，先将自己掌握的诸多技术按属性进行总结，再从知识树中得出反馈，从而确定自己的能力级别，如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202306051349824.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但在做知识树总结时，有&lt;code&gt;三点需要注意&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一点，整棵知识树应当围绕着一个明确的方向展开&lt;/strong&gt;（⭐比如 C++ 方向）。因为知识树要具备高强度的专业性，不能将日常生活中懂的方方面面全部写进去，这显然并不合适。开始撰写前必须先挑选自己最擅长的方向作为主体。以开发岗为例，如果你掌握了多门语言，那么一定要先选择一门语言作为主体，其他技能可以归纳到另一个主体分支中：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202306051351465.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二点，知识树不应该无限制延伸&lt;/strong&gt;。这里主要针对树的深度和分支数量，对于某个具体的技术栈，千万别把每个细节都详细罗列，也不必无限往下深究原理，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202306051352849.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;上述只是简单示例，重点是要记住不要无限拓展即可，毕竟这是总结，不是写书、写手册，如果不控制树的深度，浪费时间不说，同时还失去了总结的初衷。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;就好比写小说，咱们只需要将情节脉络、目录规划写出即可，无需在其中详细写出每一个情节、章节的具体内容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;第三点，首次进行总结，不必追求完美&lt;/strong&gt;。很多人首次画知识树时，往往都会追求完善性，写一个技术栈时，刻意去搜索还有哪些没写，还有哪些可以补充的。但记住：在首次画知识树时，你记得多少写多少就行，后面可以继续完善。&lt;/p&gt;
&lt;p&gt;同时在画知识树时，千万要牢记：列出来的技术点一定是自己熟悉的，不要把只听过或者只简单了解过的技术点罗列在其中！画知识树不是设计技术大纲，实事求是才能得到最准确的答案。&lt;/p&gt;
&lt;h4&gt;如何根据知识树判断自己的能力？&lt;/h4&gt;
&lt;p&gt;当各位将知识树画好后，首先纵观知识树的二/三级分支数量，如果仅有几个，这意味着你很大可能处于“初级水平”，也就是能满足工作的基本需求。反之，如果分支数量较多，整棵树的主干已经涉及了很多主流技术栈，那也就意味着你很大可能达到了“中级水准”。&lt;/p&gt;
&lt;p&gt;那如何判断自己是否达到了“高级”水平呢？这点依旧可以靠知识树来给出答案，如果知识树广度足够，挨个去看罗列出的技术栈，询问自己是否理解技术的大体原理。如果你认为对大部分技术，都有一定深度的见解，再加上自己具有不错的经验沉淀，那么恭喜你，你已经达到了“高级”水平。&lt;/p&gt;
&lt;p&gt;最后，如果技术广度、深度都有了，如何判断自己是不是资深呢？对于这个问题，知识树和我都给不了你答案，此时的你应该扪心自问，自己是否达到了资深水平？&lt;/p&gt;
&lt;p&gt;因为高级和资深之间，其实并没有明确的边界，更多的是技术见解、经验沉淀上的差异，如果一个已达资深水平的技术人，当你询问内心时，你就一定能够给出自信的答案。&lt;/p&gt;
&lt;h4&gt;技术能力与工作年限不匹配怎么办？&lt;/h4&gt;
&lt;p&gt;在经过技术总结后，如果你发现能力和年限严重不匹配，好比你有六年经验，结果到头来一看，哦豁！能力就中级怎么办？这时需要做的自然就是学习，但学习的过程无疑是痛苦且难坚持的，尤其是工作并不轻松的小伙伴，代表需要拿出休闲、下班后的时间去学习。&lt;/p&gt;
&lt;p&gt;但凡事先苦而后甜，如果连学习的苦都吃不下，那你只能本本分分地当一个“菜鸟”，更高的薪资？更好的发展？不存在的，既然选择在技术行业继续打拼，那就不要停下学习的脚步！当你学不进去想要放弃时，记住一句话：&lt;strong&gt;这是未来的你，正在向你发出的求救信号！&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不过学习也并非靠死办法，产生兴趣才是最好的良药，后面会有关于学习方法的分享，但这些是后话了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;如何根据能力定位，确定提升方向？&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;事先声明：如果急着找工作，可以先跳过这点，因为学习是长久的事，不能操之过急。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;针对前面的多种情况，如果是初级，那下一步的提升计划，应该围绕着技术广度展开，至少要将当前专业内的大部分主流技术掌握后，才能达到“中级水准”。&lt;/p&gt;
&lt;p&gt;🎯对于「初级」的小伙伴来说，提升广度的技术要学哪些呢？很简单，去看市场招聘，&lt;strong&gt;看招聘中频繁出现的技术栈&lt;/strong&gt;，对于你不会的，统统记录下来，然后用不同的颜色，加入到知识树中，最后去进行相应学习即可。&lt;/p&gt;
&lt;p&gt;🎯而目前已经达到「中级水准」时，下一步的提升应该针对技术深度，也就是对自己掌握的技能，别再停留在表面应用，而是深入内部探寻其原理，使用经验+深度，才能帮你抵达“高级水平”。&lt;/p&gt;
&lt;p&gt;那中级水准的小伙伴，加强深度的方向是什么呢？比较快的做法就是找培训机构，也就是那些打着“&lt;code&gt;XXX&lt;/code&gt;进阶课程”名号的机构，对于他们课程的含金量先不做评价，重点是&lt;strong&gt;参考他们的课程大纲&lt;/strong&gt;！虽说课程质量不一定能保障，但大纲这块绝对花了心血，毕竟这是招牌，想要吸引“进阶培训”的学员，课程大纲必然要令人满意。&lt;/p&gt;
&lt;p&gt;培训机构卖的进阶课程，绝对是技术的风向标，任何一家机构都不会掺杂无用内容在里面，大纲是市场需求提炼后的精华内容。当你拿到课程大纲后，直接和自己的知识树比对，把自己没有掌握的技术，同样用不同颜色标注上去即可。&lt;/p&gt;
&lt;p&gt;知识树标出自己要提升的技术点后，接着可以慢慢学习，学习的同时要记得完善知识树，毕竟前面只是简单的罗列，后续学会某个技术栈后，可以将大体梗概补充在知识树中，这也能够帮助你后续快速复习。&lt;/p&gt;
&lt;p&gt;⭐额外说明，不是你把某个技术学了，就代表你一定会了它，牢记：&lt;strong&gt;学是一回事，用是另外一回事&lt;/strong&gt;！千万不要产生一个错觉，也就是看了一套教程就代表学会了它，只有当你能把这项技术灵活运用时，才代表着真正学会了它。&lt;/p&gt;
&lt;p&gt;🚫所以大家在学习工作用不到的技术时，请一定要多加思考！很多时候，尤其是看视频学习，人会处于“接受式思维”，别人怎么教，自己就怎么理解，这样带来的弊端就是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当相同的技术换一个场景，你就不会用了，这显然不能说你会了这项技术，只能说有所了解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为此，想要真正学会一项新技术，要带有“&lt;strong&gt;质疑式思维&lt;/strong&gt;”去看！学习时多思考，才能真正帮你掌握它。尤其是针对一些还在校的小伙伴来说，因为没有过实际的业务经验，所以学习时更需如此，否则会导致面试中，换个方式问，结果就一问三不知。&lt;/p&gt;
&lt;h3&gt;三、怎样得到自我画像？&lt;/h3&gt;
&lt;p&gt;前面讲清楚了技术级别、自我定位以及如何制定提升的计划，下面开始接入“求职话题”的正轨。&lt;/p&gt;
&lt;p&gt;求职是一场没有硝烟的战争，想要把这场仗打得漂亮，就必须要牢记一点：&lt;strong&gt;成功需要缜密的计划和精心的准备&lt;/strong&gt;！对于冲动之下就提桶跑路的做法，我个人不是很赞成，因为这会让求职变得异常被动，如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快速与目前公司交接工作，办理离职的相关手续；&lt;/li&gt;
&lt;li&gt;草草地写好简历，开始通过各大招聘渠道投递简历；&lt;/li&gt;
&lt;li&gt;简单地刷刷面试题八股文，准备迎接面试开始；&lt;/li&gt;
&lt;li&gt;面试发挥不够理想，人事通知：先暂时回家等通知；&lt;/li&gt;
&lt;li&gt;反复投简历、去面试、等通知、不断处于焦虑中……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面这个找工作的过程，我将其称之为&lt;code&gt;运气流面试法&lt;/code&gt;，尤其当能力一般时，会显得异常被动，能否成功上岸找到下家，主动权统统交给了市场。运气好，可能很快就找到了新工作。运气略微背一点，稍微拖上几周的时间，你内心就会慢慢变得焦虑、心态逐渐变差……&lt;/p&gt;
&lt;p&gt;要记住！找工作是你选公司，而并不是公司选你，求职的目标应该是钱多事少离家近，而不是跌跌撞撞碰运气。&lt;/p&gt;
&lt;p&gt;那怎么做到你选公司呢？这需要你对自身有全面的了解，在前面认识到自身的技术能力后，接着还要看看自己的综合条件。&lt;/p&gt;
&lt;p&gt;古人讲究吾日三省吾身，而作为求职者的我们，也理当如此，但这&lt;code&gt;三省&lt;/code&gt;究竟是什么呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自己的学历背景怎么样？&lt;/li&gt;
&lt;li&gt;自己的工作履历怎么样？&lt;/li&gt;
&lt;li&gt;自己的技术能力怎么样？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在正式踏上求职路之前，一定要想明白这三个问题，因为这三点贯穿咱们整个求职旅途。&lt;/p&gt;
&lt;h4&gt;学历背景怎么样？&lt;/h4&gt;
&lt;p&gt;学历背景，这将直接决定着你面试机会的多与少，虽说许多人都在高呼：“学历其实并没有那么重要！”&lt;/p&gt;
&lt;p&gt;但不可置疑的一点就是：&lt;strong&gt;学历是你求职路上的敲门砖，如果学历不行，很多人事会直接用软件过滤掉&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;除开少部分公司，如今大部分企业的&lt;code&gt;HR&lt;/code&gt;压根不看本科以下的简历，本科以下的会直接过滤，连看都不看，是不是很残忍？但事实就是如此。&lt;/p&gt;
&lt;p&gt;连能力都不看就直接&lt;code&gt;pass&lt;/code&gt;，有人也许会觉得这未免太不公平了吧！但请记住：&lt;strong&gt;&lt;code&gt;95%+&lt;/code&gt;的&lt;code&gt;HR&lt;/code&gt;并不懂技术&lt;/strong&gt;，同时也不缺应聘者，工作年限、能力无法三言两语确认，所以学历便成了筛选的第一关，从高学历的群体中挑选人才，会比从所有群体中挑选人才更简单。&lt;/p&gt;
&lt;h4&gt;工作履历怎么样？&lt;/h4&gt;
&lt;p&gt;工作履历是指自己的工作年限与经历，这其中包含了上家公司的性质、背景等。如果你有大厂背景，例如&lt;code&gt;BATJ&lt;/code&gt;企业的工作经验，这无疑将成为你求职路上的闪光点，至少在你出去面试时，许多人都会下意识地为你打上大佬的标签。&lt;/p&gt;
&lt;p&gt;不过除开大厂背景外，你目前的工作年限以及上家公司的性质，&lt;strong&gt;是外包、自研、外企还是独角兽&lt;/strong&gt;？（下文有详细介绍）&lt;/p&gt;
&lt;p&gt;这些多少会对你后面的求职有些许影响，&lt;code&gt;Why&lt;/code&gt;？因为你的工作履历将决定着你下份工作的收入，不同履历的求职者，市场会给予不同的报价，很少有人能打破涨薪&lt;code&gt;30%&lt;/code&gt;的限制。例如一位只具备两年外包经验的求职者，市场能否给到&lt;code&gt;30K&lt;/code&gt;的薪酬呢？显然并不现实，就算你技术再优秀，也很难打破市场的潜规则。&lt;/p&gt;
&lt;p&gt;✍不过，履历虽然决定求职报价，但 &lt;code&gt;30%&lt;/code&gt; 的薪资涨幅（最多 30%，&lt;strong&gt;潜规则&lt;/strong&gt;），也并不是所有人都能拿到，除非你能力十分出色，否则一般涨幅只会处于 &lt;code&gt;10%～20%&lt;/code&gt; 这个区间，按如今的环境来看，「&lt;strong&gt;平跳&lt;/strong&gt;」都是很常见的事。&lt;/p&gt;
&lt;h4&gt;技术能力怎么样？&lt;/h4&gt;
&lt;p&gt;学历决定了面试机会的多与少，履历决定了薪资的高或低，而技术能力则是最后一省，这也是最重要的！&lt;/p&gt;
&lt;p&gt;任何一个技术行业，不存在只靠优秀的学历、出色的履历，就能成功入职的情况，技术行业的根本是以技术作为驱动，你技术能力的强弱，将直接决定着你能否通过面试。&lt;/p&gt;
&lt;p&gt;学历虽然决定着面试机会，但在技术能力够强的前提下，也并非绝对，比如&lt;code&gt;Vue&lt;/code&gt;的作者尤大（尤雨溪），假设他就算是初中学历，此时去找工作，学历高或低，这对他会有影响吗？答案很显然。&lt;/p&gt;
&lt;p&gt;当然，有人或许会觉得举例很极端，技术能达到这个层次的人，才多少呢？&lt;/p&gt;
&lt;p&gt;确实，但我要表达的意思是：&lt;strong&gt;只要你能力够强，学历其实没有那么重要&lt;/strong&gt;！一般社招（三年以上）有本科学历，不管背景、性质如何，相对来说影响就不大了。同时，市场对“高能力的人才”的包容性很强，大家可以去看“六年以上”的社招市场，就算你的学历是专科，依旧存在一大批能达标的招聘。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因此，当你达到某些条件后，就算学历、履历不行，只要技术足够&lt;code&gt;OK&lt;/code&gt;，可以忽略前面两条。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;三省吾身后得到自我画像&lt;/h4&gt;
&lt;p&gt;在真正准备面试前，作为求职者的我们必然要思考前面三个问题，思考清楚后同样用思维导图的形式进行总结：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202306051435590.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;学历决定面试数量，履历决定薪资待遇，技术决定能否入职&lt;/strong&gt;！其中最重要的是第三条，大家都身处在这个行业，对于这条的含义，诸位也许比我更明白。在我的工作生涯中，也见过许多名校毕业、高学历者面试被&lt;code&gt;pass&lt;/code&gt;，毕竟能做事才是这个行业最重要的前提。&lt;/p&gt;
&lt;p&gt;仔细想明白这三点后，相信大家对于下份工作，该找什么类型、什么薪资范围的下家，心里多多少少有谱了。&lt;/p&gt;
&lt;p&gt;不过这并非是让大家“三省吾身”的初衷，做这一步只是为了让诸位更全面地认识自己，尤其对条件较差的小伙伴而言，如学历较低的伙伴，很多时候会下意识逃避这一点，但只有当认识到自身的缺陷后，才能便于后续做出调整。&lt;/p&gt;
&lt;h2&gt;求职意向篇：怎样定下合理的期望薪资及确定目标公司？&lt;/h2&gt;
&lt;p&gt;现在应该对自己的技术、现状有了新的认知，但也仅仅止步于此。&lt;/p&gt;
&lt;p&gt;如何根据自我画像，制定合理的期望薪资呢？如何选择适合自己的公司呢？&lt;/p&gt;
&lt;h3&gt;一、如何制定合理的期望薪资？&lt;/h3&gt;
&lt;p&gt;求职者的面试期望，主要包含&lt;code&gt;期望薪资&lt;/code&gt;、&lt;code&gt;目标公司&lt;/code&gt;这两方面，想要制定合理的求职期望，需要结合自身情况、市场环境来综合考虑。下面一起聊聊吧～&lt;/p&gt;
&lt;h4&gt;影响期望薪资的因素&lt;/h4&gt;
&lt;p&gt;先讲明几个影响期望薪资的因素，接着再说如何制定期望薪资，影响薪资的因素有以下四个。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;市场氛围&lt;/code&gt;：如果大环境整体氛围不行，跳槽时几乎很难找工作，涨薪的可能性也会很小。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;技术级别&lt;/code&gt;：不同的技术能力，好比初级和高级，两者拿到的薪水待遇自然不同。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;所在城市&lt;/code&gt;：一线、新一线、&lt;a href=&quot;https://zhuanlan.zhihu.com/p/467669777&quot;&gt;省会&lt;/a&gt;、非省会城市之间，薪资待遇必然有所差异。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一线城市&lt;/strong&gt;：北京、上海、广州、深圳。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新一线城市&lt;/strong&gt;：成都、重庆、杭州、武汉、苏州、西安、南京、长沙、天津、郑州、东莞、青岛、昆明、宁波、合肥。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二线城市&lt;/strong&gt;：泉州、厦门、福州、...&lt;/li&gt;
&lt;li&gt;三线城市：...&lt;/li&gt;
&lt;li&gt;四线城市：...&lt;/li&gt;
&lt;li&gt;五线城市：...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;工作履历&lt;/code&gt;：很多人上一份工作履历，将会成为下一份工作的基础，薪资涨幅会受限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;大环境的市场氛围如何判断？&lt;/h5&gt;
&lt;p&gt;很多人在跳槽前，最想知道的就是目前外面的行情怎么样，接着会根据行情来决定是提桶跑路、还是继续窝着。&lt;/p&gt;
&lt;p&gt;行情好不好其实可以直观地从招聘软件看出，根据招聘方的活跃度进行判断。如果你所在的城市，发布招聘需求的大部分招聘者活跃度很低，这意味着当前城市的行情并不是很好，很多&lt;code&gt;HR、Boss&lt;/code&gt;都停止了招聘。&lt;/p&gt;
&lt;p&gt;但这种手段需要一定的数据支持，单纯靠人工手动来进行判断，需要查看大量招聘才能得到具体的反馈，因此这种方式最好&lt;strong&gt;结合数据采集手段（爬虫）完成&lt;/strong&gt;，效率更快的同时，精准性也会更高。&lt;/p&gt;
&lt;p&gt;当然，大家也可以通过技术群/社区等渠道，直接询问相关城市的同行，以此得到大概行情。&lt;/p&gt;
&lt;p&gt;如果目前行情并不佳，最好的选择是骑驴找马，也就是暂时先不辞去当前工作，先出去试试水感受氛围，找到满意的下家后可以选择无缝衔接。但如果目前已经离职，想要在行情较差的市场中找到工作，那这时就需要做好降低期望的心理准备（尤其是在如今这个四处喊着“&lt;code&gt;XXX&lt;/code&gt;已死”的时段内，新工作很有可能是平跳，甚至降薪）。&lt;/p&gt;
&lt;h5&gt;不同城市、技术级别的薪资标准&lt;/h5&gt;
&lt;p&gt;自身技术级别、所在城市，都会影响你的期望薪资。&lt;code&gt;IT&lt;/code&gt;行业公认的薪资梯队如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202306051505059.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里以一线城市的后端开发岗为例，来说明不同级别的薪资范畴。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初级开发工程师：&lt;code&gt;7K～13K&lt;/code&gt;左右。&lt;/li&gt;
&lt;li&gt;中级开发工程师：&lt;code&gt;13K～20K&lt;/code&gt;左右。&lt;/li&gt;
&lt;li&gt;高级开发/技术专家：&lt;code&gt;20K～32K&lt;/code&gt;左右。&lt;/li&gt;
&lt;li&gt;资深开发/架构师：&lt;code&gt;32K&lt;/code&gt;以上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述薪资范畴，在不同的岗位上也许存在差异，例如前端、&lt;code&gt;C/C++、Go&lt;/code&gt;……但级别对应的薪资中位数浮动区间不大，因此可以适当参考上述给出的薪资标准。&lt;/p&gt;
&lt;p&gt;不过假设你是中级水平，到底拿&lt;code&gt;14K&lt;/code&gt;，还是&lt;code&gt;18K&lt;/code&gt;呢？这要根据上份履历决定，下面聊聊这点。&lt;/p&gt;
&lt;h5&gt;上份履历是重要基础&lt;/h5&gt;
&lt;p&gt;前面在强调行情、城市、技术决定期望薪资，但更重要的一点是：&lt;strong&gt;你上份工作履历&lt;/strong&gt;，这将直接决定着下份工作的待遇。通常情况下&lt;code&gt;30%&lt;/code&gt;是极限，比如上份工作是&lt;code&gt;10K&lt;/code&gt;，但你自认为能力到了高级，直接开价&lt;code&gt;25K&lt;/code&gt;，这属于漫天要价，基本不可能拿到。&lt;/p&gt;
&lt;p&gt;但如果你目前的薪资较低，能力特别出色的情况下，跳槽薪资翻倍的可能性也有，例如原本是&lt;code&gt;6K&lt;/code&gt;，经过跳槽后要到&lt;code&gt;12K&lt;/code&gt;，这种现象比较常见。&lt;/p&gt;
&lt;p&gt;不过随着工作年限的增长，到手薪资的不断提高，这意味着你越来越接近行业薪资的天花板，所以跳槽时，薪资的涨幅空间会越来越小，一般不会超过前面所说的&lt;code&gt;30%&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;有时你的能力明明达到了高级水平，但你在上家公司一待就是三四年，老板也不怎么给你涨薪，所以导致上份工作的薪资并不是很高，此时该如何摆脱上份工作带来的涨薪限制呢？其实这种情况也有解决方案。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实话实说：只要能力足够出色，招聘方也会给到满意的报价。&lt;/li&gt;
&lt;li&gt;编织谎言：说自己之前在“非一线”的分公司上班，所以薪资谈的是非一线城市的待遇。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一个方案比较容易理解，第二个方案的原理则是：享受“非一线转一线”带来的“红利”，这时原本所说的&lt;code&gt;30%&lt;/code&gt;限制会被打破（但一个谎言需要更多的谎言来圆，这样做的话要想好说辞）。&lt;/p&gt;
&lt;h4&gt;制定合理的期望薪资&lt;/h4&gt;
&lt;p&gt;经过前面的内容熏陶后，大家对影响期望薪资的各个因素都已了解，那究竟如何制定合理的期望薪资呢？接下来重点聊聊这个。&lt;/p&gt;
&lt;p&gt;很多人在面试前没有想过期望薪资，因此喜欢临时报价，但要牢记：&lt;strong&gt;期望薪资并不该是“顺来逆受”&lt;/strong&gt;！你必须得先自己坚定立场，如果你自己报出的期望薪资都很随便，那很大程度上就与这个薪资无缘了。&lt;/p&gt;
&lt;p&gt;当然，适当参考面试情况来选择性报价，这种方案是可取的。坚定立场并非死守立场，如面试前的期望是&lt;code&gt;20K&lt;/code&gt;，但面试发挥得一塌糊涂，结果还楞生生喊&lt;code&gt;20K&lt;/code&gt;的报价，这合理吗？显然并不合理。&lt;/p&gt;
&lt;p&gt;不过选择性报价的前提是：&lt;strong&gt;你得有一个自己的最低要价&lt;/strong&gt;。所以，这里要借助一些招聘平台，从招聘平台得到市场反馈的报价，大家要学会善用招聘网站的“条件筛选”，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202306051536609.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;先选择好求职城市，然后把上节得到的自我画像信息依次填入，最终就能得到市场给予的报价。&lt;/p&gt;
&lt;p&gt;通常情况下，市场对相同条件的求职者，给予的薪资区间大致相同，所以这个报价，也是你期望薪资的参考之一。同时还要结合上份履历、技术能力去进行微调，经过一番调整后，你将得到一个与你最符合的期望薪资。&lt;/p&gt;
&lt;p&gt;接着可以优化成范围值，例如期望&lt;code&gt;15K&lt;/code&gt;，则可以变为&lt;code&gt;14K~16K&lt;/code&gt;，设立最低期望和最高期望，这样方便你根据面试的发挥情况，去进行合理报价。&lt;/p&gt;
&lt;p&gt;同时，因为该薪资范围是结合自我画像得来的，所以你几乎是与该薪资最匹配的人选，毕竟该薪酬范围内的招聘要求你完全满足！&lt;/p&gt;
&lt;p&gt;但制定好期望薪资后还不够，找工作、找工作，一定要理解里面的“找”字，这意味着你可以挑公司，而并不是公司挑你。因此，接着我们就来聊聊如何确定自己的目标公司。&lt;/p&gt;
&lt;h3&gt;二、如何确定最合适的目标公司？&lt;/h3&gt;
&lt;p&gt;大部分小伙伴刚踏入社会时，对第一家任职的公司基本没有要求，想的是有口饭吃就成。不过随着自己的年限、经验、技术不断增长，有了资本之后，开始对下家公司也有了一定要求，好比：“打死不去外包！”&lt;/p&gt;
&lt;p&gt;不同类型、规模的企业，所具备的氛围有所不同，入职后的工作内容也存在差异。比如微小创业型公司，你进去之后可能需要干特别多的活；而中大型规模的企业，入职后的分工会很详细，多个岗位各司其职，相互之间配合起来完成工作任务。&lt;/p&gt;
&lt;p&gt;接着大致介绍一下企业的分类，大家可以根据不同类型企业的特点，选择一类自己期望入职的企业。&lt;/p&gt;
&lt;h4&gt;技术人眼中的企业分类&lt;/h4&gt;
&lt;p&gt;这几年由于疫情影响，倒下了一大批公司，但留下来的依旧不少，对于所有招聘&lt;code&gt;IT&lt;/code&gt;技术类岗位的企业而言，咱们可以用体量、性质、业务三个维度来进行划分。下面先做大致罗列，后面再列出具体区别。&lt;/p&gt;
&lt;p&gt;站在 &lt;code&gt;体量或规模&lt;/code&gt; 的维度来看，企业可以划分为四大类。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小型/创业型企业：公司规模通常在 &lt;code&gt;100&lt;/code&gt; 人以下。&lt;/li&gt;
&lt;li&gt;中型/成长型企业：规模通常在 &lt;code&gt;200~1000&lt;/code&gt; 人左右。&lt;/li&gt;
&lt;li&gt;大型/成熟型企业：&lt;strong&gt;上市企业&lt;/strong&gt;、&lt;strong&gt;国企&lt;/strong&gt;、&lt;strong&gt;独角兽&lt;/strong&gt;、次级大厂等，规模通常在几千到几万人左右。&lt;/li&gt;
&lt;li&gt;互联网顶级大厂：典型的&lt;code&gt;BATJ&lt;/code&gt;、美团、拼多多、字节……以纯互联网业务为主的企业。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以 &lt;code&gt;公司性质&lt;/code&gt; 的角度来说，企业大致可分为两大类、五小类。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自研型企业&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;软件产品即核心：软件作为公司主营业务，依靠互联网产品盈利，如阿里、字节、腾讯等。&lt;/li&gt;
&lt;li&gt;软件产品非核心：公司主营业务为实体产品，软件系统用于辅助功能，如车企、餐饮等。&lt;/li&gt;
&lt;li&gt;国外企业（&lt;strong&gt;外企&lt;/strong&gt;）：国内的外企通常是为了打开亚洲市场，可能是官网、平台、产品等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;外包型企业&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;人力外包型：对外招聘技术人员，外派到项目方驻场开发。&lt;/li&gt;
&lt;li&gt;项目外包型：类似于接单工作室，招聘技术人员开发外部接到的项目。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从&lt;code&gt;业务&lt;/code&gt;的划分而言，企业大致可以分为两类。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互联网型企业：主要以卖服务为主，例如直播、金融、电商等类型的业务。&lt;/li&gt;
&lt;li&gt;传统型企业：主要依托于实际产品，例如工业、银行、保险等类型的业务。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;PS：互联网型和传统型企业的划分，其实有多种解读，有些以技术老旧来区分，这里就以性质来分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面分别从三个维度对企业进行了划分，之前也说过不同类型的企业，以&lt;code&gt;IT&lt;/code&gt;技术人员的身份入职，工作量、工作内容、福利待遇、内在地位等方面有所差异，下面展开讲一讲。&lt;/p&gt;
&lt;h5&gt;以体量规模维度划分&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;小型企业：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优势：初级进入后成长速度快。
&lt;ul&gt;
&lt;li&gt;①地位不错：由于公司团队人数不多，因此每一个技术成员都是核心。&lt;/li&gt;
&lt;li&gt;②成长最快：因为技术人员配比不全，所以工作承担的责任更大。&lt;/li&gt;
&lt;li&gt;③具备潜力：如果公司后面业务做大，你将成为元老级骨干，待遇也许咔咔涨。&lt;/li&gt;
&lt;li&gt;④氛围很好：由于公司人员较少，每个人之间都很熟悉，同事之间能打成一片。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;劣势：工作内容繁杂且风险大。
&lt;ul&gt;
&lt;li&gt;①工作繁杂：很大几率成为全干人才，前端、后端、测试、运维一体化。&lt;/li&gt;
&lt;li&gt;②风险较大：公司规模较小，资金链也较短，一点意外因素可能就欠薪倒闭。&lt;/li&gt;
&lt;li&gt;③成长较少：对于初级开发而言收获的成长最多，但中高级以上工作带来的成长很小。&lt;/li&gt;
&lt;li&gt;④隐私性低：人员少、资金小，代表场地不会太大，回头一看会发现老板正盯着你。&lt;/li&gt;
&lt;li&gt;⑤加班较多：由于制度不完善，所以加班、下班后再上线等情况是常事。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;中型企业：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优势：业务稳定且潜力很高。
&lt;ul&gt;
&lt;li&gt;①业务稳定：能做到这个规模说明至少稳住了一块业务，入职后倒闭风险较低。&lt;/li&gt;
&lt;li&gt;②潜力很高：每一个大型企业都是从这步跨过去的，比小企业的晋升空间大。&lt;/li&gt;
&lt;li&gt;③团队完善：通常至少会配备完善的技术团队，不会出现哪里需要哪里搬的现象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;劣势：
&lt;ul&gt;
&lt;li&gt;①加班严重：由于公司正在高速成长，所以平时加班会较多。&lt;/li&gt;
&lt;li&gt;②裙带关系：这个规模的企业中，领导层裙带关系最多，公司高层普遍是老板亲朋。&lt;/li&gt;
&lt;li&gt;③福利较差：该规模的企业中还未搭建出完善的福利体系，能享受到的福利随机。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;大型企业：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优势：某个领域的独角兽，风险性极低。
&lt;ul&gt;
&lt;li&gt;①福利不错：具备完善的福利体系，生日会、下午茶、奖金、年终奖等都有。&lt;/li&gt;
&lt;li&gt;②制度完善：加班有补贴，晋升体系明确，管理制度正规化……&lt;/li&gt;
&lt;li&gt;③加班较少：因为已经打下了一块业务，所以平时工作量也不大，加班较少。&lt;/li&gt;
&lt;li&gt;④风险极低：由于已经啃下了某块业务，抗风险性更高，不用担心跑路。&lt;/li&gt;
&lt;li&gt;⑤影响力好：虽然没有顶级大厂的含金量高，但也具备一定知名度，可以“镀银”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;劣势：官僚主义严重，归属感不高。
&lt;ul&gt;
&lt;li&gt;①官僚主义：由于企业不算小，所以高层容易拉帮结派，需要一定情商才好晋升。&lt;/li&gt;
&lt;li&gt;②归属感低：新人进入团队后容易成为螺丝仔，同事之间较为冷漠，归属感不高。&lt;/li&gt;
&lt;li&gt;③结构复杂：内部管理结构比较复杂，各项工作流程也相对复杂，不利于快速发展。&lt;/li&gt;
&lt;li&gt;④发展缓慢：由于已经啃下一大块业务，更多的是求稳，而并非追求激进发展。&lt;/li&gt;
&lt;li&gt;⑤成长较低：内部已经具备完善的技术体系，熟悉后很难从工作中获得技术成长。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;顶级大厂：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优势：薪资待遇高，镀金的完美跳板。
&lt;ul&gt;
&lt;li&gt;①薪资超高：相同技术对比其他规模的企业，薪资待遇要高出&lt;code&gt;30%&lt;/code&gt;以上。&lt;/li&gt;
&lt;li&gt;②福利超好：正常年终奖在&lt;code&gt;3～6&lt;/code&gt;个月，有购房低息贷、期权分红等。&lt;/li&gt;
&lt;li&gt;③镀金跳板：业内影响力大且名气高，“毕业后”具备大厂背景加持。&lt;/li&gt;
&lt;li&gt;④流程规范：相较于外部企业，大厂的工作流程更正规，能收获不少知识。&lt;/li&gt;
&lt;li&gt;⑤积累人脉：身边都是社会精英人士，对比外部更容易积累优质的人脉资源。&lt;/li&gt;
&lt;li&gt;⑥晋升明确：新人进入后，能力决定薪资待遇，前期成长的速度较为可观。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;劣势：内卷超级严重，个人地位不高。
&lt;ul&gt;
&lt;li&gt;①内卷严重：因为绩效末尾淘汰机制的存在，同事之间内卷十分严重。&lt;/li&gt;
&lt;li&gt;②地位不高：如果不是影响力较大的巨佬入职，在团队中的影响力较低。&lt;/li&gt;
&lt;li&gt;③暗斗颇重：大厂一般倡导狼性文化，部门、员工之间竞争相对严重。&lt;/li&gt;
&lt;li&gt;④难晋高岗：普通员工干到一定级别线后，很难“立大功”晋升更高的职位。&lt;/li&gt;
&lt;li&gt;⑤易成螺丝：大厂讲究专而精，入职后通常会分配到某个岗位上打螺丝。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;以公司性质维度划分&lt;/h5&gt;
&lt;p&gt;以公司性质来划分，主要可分为自研型、外包型这两类企业。自研型企业中主要分为软件产品为核心、软件产品非核心这两种，但为了全面性，咱们也将外企列入了自研型企业中。反观外包型企业，主要涵盖人力外包与项目外包。&lt;/p&gt;
&lt;p&gt;接着简单说明一下各类公司的优劣势。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;软件即核心型自研&lt;/strong&gt;：技术人员的地位很高，并且福利待遇相对不错，负责的产品也会更加追求完善性。但由于是自研型企业，所以能接触到的业务线不多，入职一段时间后，工作能给自己带来的成长不多，并且以普通员工的身份入职，很容易成为打螺丝的那位。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;软件非核心型自研&lt;/strong&gt;：整体与前者相差不大，但技术人员的地位相对没有那么高，毕竟公司主要以实体产品为主。但入职后十分安逸，忙完项目产品的初版研发后，后续的工作特别轻松，不过个人成长会随之受限，后续也很难以技术身份晋升内部高管，比较适合养老。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;外企&lt;/strong&gt;：有机会进的小伙伴都可以尝试，除开少数外企，大部分外企的福利待遇特别好，并且十分强调工作与休息时间的界限，钱多事少的类型。不过想进需要一定的英语能力，并且有些外企所用的技术会跟国内主流技术脱轨，不利于后续发展（&lt;strong&gt;研究所、军工类的也大致相同&lt;/strong&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;人力型外包&lt;/strong&gt;：地位特别特别特别低，著名的那句“外包仔别偷吃公司零食”就源自于这类外包公司，一般入职后会被外派到甲方驻场，负责大型项目的边角料业务开发，十分不利于后续发展。至于优势的话似乎没有太多，也许钱会多一点点？如果不是走投无路，不建议选这类&lt;code&gt;Offer&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;项目型外包&lt;/strong&gt;：这类外包比前者要好，地位也没有那么低，主要是负责开发承接的外部项目，特别适合初中级积累业务经验，能够接触到各行各业的项目（我有位老同学堪称外包狂人，巅峰时期一人负责&lt;code&gt;17&lt;/code&gt;个外包项目）。但项目更追求开发速度，并不追求质量，讲究“能跑就行、又不是不能用……”，不利于后期成长。&lt;/p&gt;
&lt;h5&gt;以业务性质维度划分&lt;/h5&gt;
&lt;p&gt;站在业务性质的维度上划分，主要可分为互联网企业和传统型企业。&lt;/p&gt;
&lt;p&gt;前者一般技术较新，技术上也更为激进一些，对于技术人来说，工作带来的成长会更大。&lt;/p&gt;
&lt;p&gt;后者则技术相对较老，发展较为缓慢，技术上较为保守，求稳为上策。&lt;/p&gt;
&lt;p&gt;对于技术人员而言，互联网型企业必然会比传统型企业好上许多，不仅仅是薪资待遇上的差距，更多的是工作上带来的个人成长。&lt;/p&gt;
&lt;h4&gt;技术人该如何选目标公司？&lt;/h4&gt;
&lt;p&gt;经过一番啰嗦后，相信诸位对各类企业有了一定了解，不同类型的企业多少会有差异，前面给出优劣分析，虽说不一定绝对权威，但大体符合，因此可以作为客观参考项。&lt;/p&gt;
&lt;p&gt;了解各类企业的优劣特点后，又该如何选择目标公司呢？&lt;/p&gt;
&lt;p&gt;这得因人而异，每个人的追求不同，所以选择也会有所不同。比如有人要钱，就算加班也能接受；而有人追求轻松，钱少一点也不在乎……为此，大家可以根据自己内心所想，去确定目标公司。&lt;/p&gt;
&lt;p&gt;站在我个人的角度给出的建议，初级可以选业务比较丰富或成长价值不错的企业，如项目型外包、小型创业公司等，因为现阶段对个人成长最有益的，是追求业务和工作经验上的积累。&lt;/p&gt;
&lt;p&gt;中级则可以选技术成长类的企业，主要关注入职后能给自己带来能力增长的企业。&lt;/p&gt;
&lt;p&gt;高级水准左右的小伙伴，则主要追求好的平台，如大厂、大型企业等，这有利于后续个人的发展空间（职位、薪资）。&lt;/p&gt;
&lt;p&gt;当然，&lt;strong&gt;初级选业务积累，中级选技术成长，高级选后期发展&lt;/strong&gt;，这也仅是我个人的建议，也不一定要这么选，大家适当参考即可，具体还是要遵从自己的内心想法，如果你内心没有要求，遵守“给钱就干”的原则也不是不行。&lt;/p&gt;
&lt;h4&gt;自己到底要不要尝试冲击大厂？&lt;/h4&gt;
&lt;p&gt;冲击大厂，这应该是很多技术人的梦想之一，毕竟大厂不仅代表更高的薪资福利，而且也是一种镀金手段。当履历上增添一笔大厂的工作经验后，也能成为以后简历的闪光点，有助于拿到更多的面试。同时从大厂“毕业”之后，薪资报酬也会高上许多。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202306051649834.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;入职大厂带来的好处毋庸置疑，以大厂作为跳板也的确是很不错的镀金方案，但大厂的招聘往往更严格，除非你的综合条件都不差，并对自身技术有绝对自信，否则我并不建议冲大厂，因为大厂的面试流程比较长，有些能达到好几周。所以目前如果处于待业状态，在没有一定把握的情况下，冲击大厂反而会带来一定的影响（时间开销）。&lt;/p&gt;
&lt;p&gt;不过大厂也并非遥不可及，如果你具备以下条件，那我的建议是可以尝试尝试。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;远超同龄人的技术，至少对比相同年限的工作经验者，技术能力上有不小优势。&lt;/li&gt;
&lt;li&gt;具备不错的学历背景，不要求一定要是&lt;code&gt;985/211&lt;/code&gt;名校、硕士等学历，但至少还过得去。&lt;/li&gt;
&lt;li&gt;具备不错的项目经历，有参与过核心系统的架构与开发工作，只有简单的项目就不必尝试。&lt;/li&gt;
&lt;li&gt;年龄最好是在&lt;code&gt;25&lt;/code&gt;岁及以下，最多不能超过&lt;code&gt;28&lt;/code&gt;岁，超过该年龄时再尝试，技术要求更高。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述这四点，是通过「&lt;strong&gt;社招&lt;/strong&gt;」进大厂需满足的四个前提，如果其中某条不曾具备，那你去尝试的结果很有可能会以失败告终。&lt;/p&gt;
&lt;p&gt;但还有一类人除外，那就是「&lt;strong&gt;应届生&lt;/strong&gt;」，满足两点即可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不错的学校背景，如&lt;code&gt;985/211&lt;/code&gt;院校的应届生，大厂会去校园招聘；&lt;/li&gt;
&lt;li&gt;具备扎实的计算机功底、不错的项目经历，或者不错的实习经验。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;学历不错的应届生是最容易进大厂的一批人，因为不仅招聘要求会放低，而且流程也会快很多，并且无需对某个技术有特别深入的研究，大厂招聘应届生更关注“培养价值”，毕竟还未真正出学校的应届生，专业方向还未彻底定性，所以可栽培的价值很高。&lt;/p&gt;
&lt;p&gt;如果符合上述两个条件的小伙伴，可以参加校招会尝试看看。错过了校招进大厂的小伙伴，那就只能走社招进入，网申通道投递的简历，通过率比较低，因此社招想要进大厂，最好还是找人内推。&lt;/p&gt;
&lt;h4&gt;如何挖掘高潜力的企业&lt;/h4&gt;
&lt;p&gt;大家都做过“买彩票中五百万”的美梦，也一定有过“入职的公司突然成为独角兽，从此自身地位水涨船高”的念头。&lt;/p&gt;
&lt;p&gt;当自身冲击大厂不够格时，找一家具有“成为独角兽”潜力的企业，这听起来似乎很不错，对吧？但从数不尽的创业公司中，想找到这样的企业，难度无疑堪比大海捞针！&lt;/p&gt;
&lt;p&gt;难道我们只能凭运气来碰“高潜力”公司吗？答案是&lt;code&gt;No&lt;/code&gt;，其实找高潜力的“千里马”，也存在一定的技巧与方法！哦？具体怎么做呢？下面来聊一聊。&lt;/p&gt;
&lt;p&gt;以个人的视角和认知，想要挑选出“千里马”格外困难，就好比在股市中，普通人买到潜力股的概率也很小，不过股市中我们可以抄作业，抄一些大佬的持仓配置，这个技巧同样可以换到“挑公司”中！那挑选高潜力公司时，究竟要抄谁的作业呀？很简单，&lt;strong&gt;&lt;code&gt;抄大厂和资本的&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们可以借助企业工商信息查询系统，如企查查、天眼查等，直接搜索大厂的名字，接着查看其对外的投资情况，例如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202306051655240.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;又或者可以去抄资本机构的作业，如红杉资本、纪源资本、高瓴资本、达晨创投、深创投等投资公司，根据其对外的投资情况，选择心仪的“高潜力”公司：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202306051656976.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这背后的原理相信大家都懂，作为专业的投资手，以及行业的龙头，投资的目光自然比咱们看得更远，所以当大家想要真正找一家高潜力的公司时，就可以优先往大厂、资本投资的优质企业投递简历。&lt;/p&gt;
&lt;h3&gt;三、应届毕业生又该如何抉择？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;事先声明：非应届生身份的小伙伴，可跳过这个阶段。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;应届毕业生属于一个比较特殊的群体，目前的处境也有些尴尬，现在整个行业的人才较为饱和，企业招聘时也不缺人来应聘，因此如今留给应届生的空间越来越小，纵观整个行业趋势，涌入这个行业的人每年都在逐步增多，所以应届生将要面临的是“狼超多、肉很少”的情况。&lt;/p&gt;
&lt;p&gt;应届毕业生中，如果学历背景较为不错的，那一定要尝试冲击大厂，这将是一个改写以后人生走向的路口，因为第一份工作的薪水、经历，将直接决定你的起点，大厂无疑是起点最高的一个选择。例如普通公司&lt;code&gt;4K&lt;/code&gt;的实习生，和大厂&lt;code&gt;15K&lt;/code&gt;的实习生，两者在找第二份工作时，薪资待遇将会有着天差地别。&lt;/p&gt;
&lt;p&gt;但如果学历、院校背景没有那么好，在有其他更好选择的情况下，我不建议再往这行钻，为什么呢？下面一起来探讨一下。&lt;/p&gt;
&lt;p&gt;相较于早些年人才稀缺的那个时代，如&lt;code&gt;2010&lt;/code&gt;年左右，只要你说是计算机专业毕业的，一大堆公司会抢着要。而回头再看如今，除开规模不错的企业外，很多公司对应届生的招聘开始减少，这个现象大家应该能够明显感受到。随着时间往后推移，尤其是疫情影响之后，很多企业甚至都不再招聘实习生，这是为什么呢？&lt;/p&gt;
&lt;p&gt;主要有如下四点原因。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;人才饱和：略微多花一点钱，就能招聘一个具备实际动手能力的初级开发。&lt;/li&gt;
&lt;li&gt;事情很多：因为刚出学校，很多东西不懂，往往许多工作需要老人协助完成。&lt;/li&gt;
&lt;li&gt;效率很低：花费了多于老员工的&lt;code&gt;N&lt;/code&gt;倍时间，做出来的结果还需要再次改善。&lt;/li&gt;
&lt;li&gt;忠诚度低：企业花了几个月的时间培养成型，结果因为薪资不满意很容易跑路。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，除开大厂外，很多企业，尤其是中小型公司基本上不会选择招聘实习岗，因为除开少数优秀的实习生外，选择招聘其他实习生反而是一种吃力不讨好的行为。&lt;/p&gt;
&lt;p&gt;当然，看到这里我相信有部分小伙伴，可能会感到略微有些难受，觉得这些话有些难听，但这就是现实。&lt;/p&gt;
&lt;p&gt;对于应届毕业生而言，这个行业如今没有那么美好，那应届生该如何抉择呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最好的选择是大厂/中厂，大厂有足够的魅力留下实习生的心（薪资待遇/镀金履历）。&lt;/li&gt;
&lt;li&gt;如果学历不够，第二选择是考研，市场对高学历的应届生很友好，考研后选择机会更多。&lt;/li&gt;
&lt;li&gt;如果自认考研无法上岸，第三选择是靠亲戚朋友转行，转到一个发展、薪资不错的行业。&lt;/li&gt;
&lt;li&gt;如果原生家庭没有人脉资源，最后的选择才是踏入这行死磕，开启一条“异常苦逼之路”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🌗总之，对于应届生来说，只需认准一条原则即可，那就是&lt;strong&gt;尽可能地去提升你的起点&lt;/strong&gt;！你的第一份工作，很大几率下，将会直接影响着你未来的发展，当有更好的起点可供选择时，那请千万不要犹豫，进大厂、考编、考公、考研都是这个道理，起点不同，未来的人生轨迹也会完全不同！&lt;/p&gt;
&lt;h2&gt;技术突击篇：如何根据求职意向进行快速提升与复盘？&lt;/h2&gt;
&lt;h3&gt;一、突击与复盘并不是闭眼摸虾&lt;/h3&gt;
&lt;p&gt;如何准备面试才最高效呢？那就是：&lt;strong&gt;请围绕着期望薪资准备面试，千万不要闭眼摸虾！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;既然有了一个具体的期望薪资，那你就能确定自己要准备哪些技术，因为同一薪资范围内的招聘要求大致都相同，此时你可以拿着自己的期望薪资，去到一些招聘平台上，多查看一些招聘要求，&lt;strong&gt;从中选取出现频率较高的技术栈与要求，然后进行定制化复盘&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为何要这么做呢？因为许多小伙伴在准备面试时，都会显得有些无厘头，这或许是因为环境造成的，如今四处都在喊着面试要造火箭，培训机构天天跟你说：你是个只懂&lt;code&gt;CRUD&lt;/code&gt;的螺丝仔……久而久之，这些话也成了业内所有人的共识，大家在准备面试时，也潜意识地会去看原理性的内容，但这有必要吗？也许有一定意义，毕竟前面的那些话，也在一定程度上影响了面试官，但请牢记：&lt;strong&gt;千万不要把重心放错了地方&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;比如典型的案例：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;高并发&lt;/strong&gt;，这玩意儿重要吗？重要，但如果你只想找初级的工作，天天研究高并发有意义吗？有人可能会回答：“没办法，面试要问！”&lt;/p&gt;
&lt;p&gt;但请记住，初级面试中会不会问高并发呢？显然大部分不会，除开少数个例除外。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从上述案例中要明白一个道理，前面做的技术定位、求职意向并非白用功，你应该要搞清自己需要的、摒弃不需要的，不要去做无意义的技术准备。通过招聘需求得到自己的复习/学习重心，再结合目前的现状来制定计划，才能让面试前的技术准备最高效。&lt;/p&gt;
&lt;p&gt;一般要准备面试的群体我将其分为如下两类，这两类群体都有各自的面试计划。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;时间充裕的骑驴找马者：目前有工作，考虑换个新环境。&lt;/li&gt;
&lt;li&gt;焦急万分的自由技术人：目前已辞职，迫切需要新工作。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;二、技术突击：时间充裕的骑驴找马者&lt;/h3&gt;
&lt;p&gt;对于目前还未离职的小伙伴而言，时间方面会比较充足，这意味着可以慢慢准备面试，在这种情况之下，去刷面试题反而不是最好的选择，应该要考虑的是做技术突击，逐步将自己的能力提升到符合期望薪资的水准。毕竟刷面试题属于临时抱佛脚、治标不治本的做法，很难真正理解那些自己不懂的知识点。&lt;/p&gt;
&lt;p&gt;既然要做技术突击，切入点在哪？到底是追求广度还是深度？对于这点，要因人而异。如若想要找“中级水平”的工作，首先应该追求广度；如果打算尝试“高级水平”的面试，此时应该注重技术深度。但牢记：&lt;strong&gt;广度不是涵盖全部，深度不是死钻到底&lt;/strong&gt;，一定要把握好分寸，追求广度也好，深度也罢，想要做到极致很难，不仅要耗费很多时间，而且还需投入大量精力。&lt;/p&gt;
&lt;p&gt;确定了学习方向后，接着再回到之前所画的“知识树”，通过调研招聘需求后，用其他颜色把自己所缺乏的技术标注出来，这是接下来要学习的内容，尤其对技术与年限严重不匹配者来说，这将是你技术追上工作年限的最佳时机。&lt;/p&gt;
&lt;p&gt;确定了要完善的技术内容后，接着还需规划学习路线，凡事都有轻重缓急之分，学习也不例外，对于自己缺乏的技术，要先&lt;strong&gt;做好优先级排序&lt;/strong&gt;，首先把那些热度高、通用性强的技术放前面，闲暇之余再考虑那些能用到、但热度不高的技术。这样做的好处在于可以应对突发情况，就算中途被“优化”了，因为重要的技术已经弄懂了，那些优先级不高的内容在面试中也不会影响大局。&lt;/p&gt;
&lt;h4&gt;最快的与最有质量的学习方法&lt;/h4&gt;
&lt;p&gt;学习的方向、欠缺的知识、学习的路线都确定了后，&lt;strong&gt;又该如何去学习不足的技术呢&lt;/strong&gt;？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;效率最高的学习方法：读经典书籍，看优质专栏。&lt;/li&gt;
&lt;li&gt;质量最高的学习方法：看教学视频，跟培训课程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于具备不错基础及自学能力的人，个人建议选第一种方式！一方面是因为速度快，另一方面还能顺便加强自学能力。&lt;/p&gt;
&lt;p&gt;不过有一点不可否认：&lt;strong&gt;通过第二种方式学习，无疑会比前者更加轻松且质量更高&lt;/strong&gt;，因为任何课程/视频的设计者在规划时，绝对已经考虑周全，你想要掌握的技术点，基本都能学到，能录制课程的人绝对比“目前的你”更懂这项技术。但相较前者会更加浪费时间，同时还有一点要注意：&lt;strong&gt;通过看视频/课程的方式学习，容易让人产生依赖性，丧失一定程度上的自主学习能力&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;这点相信有过长期靠视频学习的小伙伴，应该能感同身受，当你想要研究一个新问题/新技术，如果在网上没有找到相关视频时，将会显得有些无从下手，不知道如何开展研究，这就是长期依靠第二种方式学习带来的弊端。&lt;/p&gt;
&lt;p&gt;有人会说：“你这意思是一定要靠第一种方式学习咯？”实则不然，要分具体情况来定，如果目前要研究一个从未接触过的新技术，或自己底子不是特别好，那选择第二种方式依旧是最好的方案。&lt;/p&gt;
&lt;p&gt;第一种方式更适合一些目前至少达到了中级以上水平，且具备一定自主学习能力的小伙伴！如果不熟悉某项技术，再加上自主学习能力较弱，那么自己去尝试研究时，花费的时间会远超第二种方式。毕竟现在各种资料满天飞，但大多数仅停留在表面阐述，或仅有只言片语，想要找到一套较完整且有深度的资料，也会比较困难。&lt;/p&gt;
&lt;h4&gt;找到适合自己的优质学习资源&lt;/h4&gt;
&lt;p&gt;选择一种适合自己的学习方式后，如何找到优质的资源呢？&lt;/p&gt;
&lt;p&gt;先来聊聊如何&lt;strong&gt;寻找质量不错的视频教程&lt;/strong&gt;（对应前面&lt;code&gt;第二种学习方式&lt;/code&gt;），这里也可以加上一个前缀：&lt;strong&gt;免费&lt;/strong&gt;，但事先声明，我并非引导大家成为白嫖佬，只是希望大家别花冤枉钱。这里提供几种方式，如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;学习前请先查查自己的网盘，如果平时你爱收集资源，或许能给你一个意料外的惊喜；&lt;/li&gt;
&lt;li&gt;网盘没有对应资源时，请先去常用的资源平台看看，如大家比较熟悉的&lt;code&gt;B&lt;/code&gt;站；&lt;/li&gt;
&lt;li&gt;合理使用搜索引擎，天下没有不透风的墙，搜索引擎玩得溜，想要啥都能找出来；&lt;/li&gt;
&lt;li&gt;善用技术交流圈，技术群、社区、论坛，略微花点代价，大部分学习资料都能换到；&lt;/li&gt;
&lt;li&gt;学会通过网购渠道低价买入，起步大几千的课程舍不得买，此时你去闲鱼、淘宝能有惊喜。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过并非所有视频资源都算优质，大家一定也遇到过很多优劣不一的资源，因此找到一份学习资源时，要记住：“客官，你不要那么猴急啊，讨厌~”优不优质全靠同行衬托，最好的办法是找几套出处不同的教程，简单听一下相同知识点的讲解，就能高低立判，选择最好或最适合的那份再学习也不迟。&lt;/p&gt;
&lt;p&gt;接着再简单聊聊，如果选择通过&lt;code&gt;第一种方式学习&lt;/code&gt;，又该如何&lt;strong&gt;寻找优质的资料&lt;/strong&gt;呢？如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果比较热门且出现时间较长的技术，先去看看有没有对应的经典书籍；&lt;/li&gt;
&lt;li&gt;如果是比较新的热门技术，可以直接去参考对应技术栈的官方文档；&lt;/li&gt;
&lt;li&gt;通过搜索引擎、技术网站输入具体的关键字，多点开几篇资料，对比后再阅读；&lt;/li&gt;
&lt;li&gt;寻找优质的连载资料，例如某些大佬的付费专栏、电子书、掘金的小册子等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自己研究某项技术时，通常就是依靠这四种方法。&lt;/p&gt;
&lt;p&gt;第一种方法无疑最好，至少专业性最高，但大部分书籍的描述会有些拗口，这时需要你具备一定的理解、推导能力。&lt;/p&gt;
&lt;p&gt;第二种方法比较适合研究新技术，官网的文档绝对是最准确的，但有时会看到的是英文版手册，所以需要不错的英语阅读能力。&lt;/p&gt;
&lt;p&gt;第三种方法绝对是大家最常用的方案，也是较为特殊的一种方法。一项技术对应的文章资料有很多，但资料五花八门，同一个知识点可能会被解读出不同的释义，这时你需要具备一定的辨别能力，千万别听风就是雨，最好多对比不同的资料，再结合自己的理解去推导出你想要的答案。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说实话，这样做会比较耗精力，想要从一堆资料中去研究明白一个知识点，需要不少的时间，毕竟有时找到了满意的、但描述却不全面，有些描述全面、但内容又不满意，所以想要通过这种方法去学习，往往要靠自己去提炼精华，多看多理解，最终形成自己的技术认知。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不难发现，上述一些问题也是我强调第一种学习方式得具备一定底子的原因。如果底子较为薄弱，当你看书、看官方文档时就会很难理解，甚至犯困，当你去看文章时，也很难做到“&lt;strong&gt;取其精华，去其糟粕&lt;/strong&gt;”。&lt;/p&gt;
&lt;h4&gt;理性看待线下培训与线上网课&lt;/h4&gt;
&lt;p&gt;相信大家对“培训”这个词汇都不陌生，随着国内互联网的发展，再加上近些年疫情影响，培训机构、在线教育的崛起速度异常惊人，但对于培训机构的评价却褒贬不一，尤其是身为程序员的我们，对它们是又爱又恨，爱是因为能从它们身上学到一些知识（大部分视频教程都源自于培训机构），恨是因为它们加速了行业内卷。&lt;/p&gt;
&lt;p&gt;这里不对“培训机构”做过多评价，我们重点是要聊一聊它们的核心业务：&lt;strong&gt;付费课程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但凡接触过这些机构的小伙伴，经过一番营销洗脑以及课程推广后，对于它们的付费课程多少有过幻想，对比传统的教学视频，付费课程的最大吸引力在于：&lt;strong&gt;课程内容全面，都以面授或直播的形式开展，不懂的可以当场提问，有疑惑的课后随时解答。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是付费课程的最大优势，也是大家想买课的最大原因。但记住！并不是所有机构都那么负责，很多机构付费前对你无微不至，但加入后需要解答时，你会发现解答的时间很慢、甚至压根没人回（这里有相关经验的小伙伴应该深有体会）。&lt;/p&gt;
&lt;p&gt;那我们到底需不需要这些付费课程呢？或者付费课程值不值得入手呢？这同样得因人而异，如果你的自主学习能力差、自律性比较差、底子比较薄弱，那应该会比较合适，毕竟花钱了你会更加珍惜一些。&lt;/p&gt;
&lt;p&gt;反观前面对于能选择第一种学习方式的人来说，这个课程的价值就不大了，毕竟课程里面有的东西你都能靠自己研究明白，唯一珍贵的一点是有人能帮你解答疑惑，但对比其昂贵的学费，显然投入和产出不成正比。&lt;/p&gt;
&lt;p&gt;但如果你坚决要报课学习，那也没有问题，只不过以我接触过的人来说，大部分人报课后，只是给自己买了个教训（里面坑很多）。因此，在付费前一定要货比三家，请擦亮眼睛仔细分辨，选择一家好的机构，不仅后续能拥有更好的服务，而且也会帮你省去很多麻烦。&lt;/p&gt;
&lt;p&gt;综上，我们就可以提炼其主线内容啦，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202306060953651.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;三、快速复盘：焦急万分的自由技术人&lt;/h3&gt;
&lt;p&gt;前面针对还在职的小伙伴，提供了一系列的准备方案，那再来到大家比较关心的一种情况，如果我目前已经离职了，现在迫切需要一份新工作怎么办？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我：我知道这时的你会很急，但你先别急！&lt;/li&gt;
&lt;li&gt;你：！？？！&lt;/li&gt;
&lt;li&gt;你：哥，我是想要你教我一套能快速找工作的秘诀啊，不是听废话的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;怎样才能快速找到新工作呢？记住，等我有条件开公司了，第一时间就给你直接发&lt;code&gt;Offer&lt;/code&gt;，哈哈~&lt;/p&gt;
&lt;h4&gt;心态调整与制定复盘计划的核心&lt;/h4&gt;
&lt;p&gt;话回正题，在很多人看来“你很急但先别急”这句话是废话，尤其是所投简历如石沉大海、面试回家等通知的小伙伴，听到这句话会更来气：“奶奶个熊的，火都快烧到我屁股上了，你还叫我不要急！”&lt;/p&gt;
&lt;p&gt;但要明白一个道理：&lt;strong&gt;你的焦虑并不能改变你的处境&lt;/strong&gt;，急躁只能加速你内心的焦虑，最终反而会影响你的心态，所以离职后首先要做的是调整好自己的心态，做好“长期奋战”的心理准备。&lt;/p&gt;
&lt;p&gt;以我的某位朋友为例，他当时就是这种情况，求职碰壁后并未焦虑，而是调节好了心理并放松了心态，您猜这结果怎么着？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;高情商：他硬生生享受了前所未有的一年长假。&lt;/li&gt;
&lt;li&gt;低情商：一年没找着工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面这个故事告诉我们，放松心态不是完全摆烂，调节心理不是放弃挣扎，否则最终你也将成为一位享受长假的自由人！调整心态目的是在于减少不必要的焦虑，但自己的面试计划也要如常进行。那待业的小伙伴又该如何快速做技术复盘呢？和前面类似，先定位自身技术能力，接着根据自身所缺，按优先级制定一份复盘计划即可。&lt;/p&gt;
&lt;p&gt;但千万要记住一点：&lt;strong&gt;复盘计划不是学习计划&lt;/strong&gt;，相较于前一种的未离职人员，你没有那么多时间去做提升，因此复盘计划是围绕着你自身的技术在做准备，复盘的核心是：&lt;strong&gt;我目前会哪些技术？期望薪资的面试中，会问到哪些技术？问到什么程度？针对自身不足去准备即可。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里额外说明一点：&lt;strong&gt;火候欠缺者应该适当降低期望&lt;/strong&gt;。如果你目前的能力最多只能拿到&lt;code&gt;15K&lt;/code&gt;，但你的期望薪资偏偏定在&lt;code&gt;18K&lt;/code&gt;，此时距离&lt;code&gt;18K&lt;/code&gt;还有好些个未掌握的技术需要学习，但目前已经没了工作，想要去学习还未掌握的技术时，你的时间扛不住这么消耗，所以最好的做法是适当降低期望薪资，最好降低到一个自己有把握的范围内。人要量力而行，千万不要打肿脸充胖子，否则就算接到了面试也很难通过。&lt;/p&gt;
&lt;h4&gt;如何高效地刷面试八股文&lt;/h4&gt;
&lt;p&gt;接着再说说，当制定好复盘计划后，如何基于复盘计划进行快速复盘呢？&lt;/p&gt;
&lt;p&gt;其实这就没有秘诀了，老老实实去看面试八股文，八股文既然能有这么高的热度，绝不是偶然造成的！它们都是经过精心地整理、认真地撰写，最终才形成的面试题合集，这将是你快速复盘的最大助力。&lt;/p&gt;
&lt;p&gt;刷面试题最快的方法不是死记硬背，而是尝试理解。以我的职业生涯为例，从工作以来，我也负责过许多场大大小小的面试，考察的求职者中不乏有很多“八股文选手”，我提出的诸多问题，他们都能给出准确的回答，不过其中至少有&lt;code&gt;70%&lt;/code&gt;以上的选手，其回答让我感到异常僵硬，给人一种背书的感觉，回答的说法也比较官方化，缺乏自己的理解，简单来说就是回答不接地气。&lt;/p&gt;
&lt;p&gt;所以！&lt;strong&gt;看八股文时一定要学会自己去理解&lt;/strong&gt;，只有当你理解了才能在面试中谈出自己的看法，而不是死板地僵硬回答。毕竟八股文涵盖了诸多面试问题，而热度高的那些问题，绕来绕去就那么些，面试前的很多求职者基本上都看八股文，相同的问题别人这么回答，你也这么回答，最终只会导致你显得平平无奇。&lt;/p&gt;
&lt;p&gt;虽说要试图理解面试八股文，但有时会碰到这种情况（举个形象的例子）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问&lt;/strong&gt;：&lt;strong&gt;为什么&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;AlCl3&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;是共价化合物？&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;答&lt;/strong&gt;：金属元素与非金属元素形成的化合物通常是离子化合物，如&lt;code&gt;NaCl、K2S&lt;/code&gt;等，但&lt;code&gt;AlCl3&lt;/code&gt;是共价化合物。&lt;code&gt;AlCl3&lt;/code&gt;的熔点&lt;code&gt;192.4℃&lt;/code&gt;（&lt;code&gt;2.5&lt;/code&gt;个大气压），沸点为&lt;code&gt;177.8℃&lt;/code&gt;（沸点比熔点低是因为测定&lt;code&gt;AlCl3&lt;/code&gt;熔点需加压，因而使得熔点升高）。&lt;code&gt;AlCl3&lt;/code&gt;在熔融态、气态和非极性溶剂中均以二聚体&lt;code&gt;Al2Cl6&lt;/code&gt;的形式存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这你能看懂吗？&lt;code&gt;90%&lt;/code&gt;的人看了直摇头，看不懂，根本看不懂，其实我也看不懂，因为我是随手复制的。&lt;/p&gt;
&lt;p&gt;但这种情况在刷题过程中又会碰到，有些面试题你可能压根不懂，无法理解时怎么办呢？最好的办法是选择跳过，毕竟连看都看不懂了，意味着这个题远超你目前的技术认知，所以不要浪费时间去试图理解它。但你又担心面试会被问到这个题咋办？实在担心的话也可以简单背一下答案，给自己简单留个印象和心理安慰。&lt;/p&gt;
&lt;p&gt;同时，不要去试图找到一份十全十美的面试题，因为就算找到了也不一定适用，刷面试题也要多看多比对才行，多用搜索引擎搜索&lt;code&gt;XXX&lt;/code&gt;面试题，例如：&lt;code&gt;Spring&lt;/code&gt;面试题、&lt;code&gt;Vue&lt;/code&gt;面试题……从搜索结果中挑几篇自己认为合适的，然后进行复盘，切记：&lt;strong&gt;不要拿着一份相同的八股文反复看！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;相同的技术栈要多找几份面试题来刷，看第一篇的时候带着理解思维，看完第一篇，后面的速度就会比较快，把自己找的几份面试题都看了之后，再选一份自己认为最好的，接着去遮住答案，在心里模拟回答一下，回答后再仔细看一遍原文的答案，以此加深脑海中的印象。&lt;/p&gt;
&lt;p&gt;通过这种方式刷题，好处如下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一遍以理解思维去看：能让你对相应知识点形成自己的认知。&lt;/li&gt;
&lt;li&gt;后续针对相同技术多刷不同面试题：能纠正你前面的错误认知，增强你对知识点的记忆。&lt;/li&gt;
&lt;li&gt;最后模拟回答再看解析：能联想面试场景回答，再次加深脑海里的印象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然这样刷题的进度会慢点，但绝对是效果最好的，而且会比反复看同一份面试题要有趣得多，最主要的是能形成自我理解，而并非死记原文的官方回答。&lt;/p&gt;
&lt;p&gt;对这一阶段进行简单总结：&lt;strong&gt;&lt;code&gt;先放松心态做好心理准备 → 针对自己的已有技术找出不足 → 制定好复盘计划与路线 → 按计划开始有技巧地刷面试题&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;四、降维打击才是求职的灵丹妙药&lt;/h3&gt;
&lt;p&gt;如今四处都在喊着&lt;code&gt;XXX&lt;/code&gt;寒冬、&lt;code&gt;XXX&lt;/code&gt;已死、&lt;code&gt;XXX&lt;/code&gt;已凉……当大家听到、看到这些话时，难免内心会产生焦虑。身处技术行业的我们，又该如何摆脱这些困境呢？最后就再来聊一个求职必胜的小妙招，也就是：&lt;strong&gt;如何增强自己在面试中的核心竞争力！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其实具体方法在标题中就给出了，那就是依靠 “&lt;strong&gt;降维打击&lt;/strong&gt;” 来做到鹤立鸡群。&lt;/p&gt;
&lt;p&gt;道理很简单，现在很多岗位在招聘时，都处于狼多肉少的情况，如果你不能在众多面试者中脱颖而出，那就很难收到入职邀约，先来看个例子：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;假设一个企业发布了招聘需求，总共来了五位应聘者面试，恰巧你也是其中之一，但你们五个候选者的技术水平都差不多，面试中的发挥也差异不大，这时企业会怎么挑选合适的人选呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个时候就得看眼缘了，比如你因为面试时左脚先进门，所以不合适。另一个人因为嘴角有颗痣，所以也不合适。最终入职的人，也许就是一个面试官、&lt;code&gt;HR&lt;/code&gt;看着更顺眼的人！这公平吗？不公平，但现实中有些情况下，的确如此。&lt;/p&gt;
&lt;p&gt;所以，面对于上述那种场景，想要做到真正的求职必胜，就必须让自己的面试表现更为突出才行！具体该如何做呢？以我来举例子，场景如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;现在有一个&lt;code&gt;8K&lt;/code&gt;的初级招聘，目前有三位应届毕业生争得焦头烂额，都想自己拿到入职邀约。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此时我去面试后，并且极力表现出想加入这家公司，这时面试官会怎么选？通常情况下会选我，&lt;code&gt;Why&lt;/code&gt;？因为对比其他三位候选人，我的经验、能力、面试表现绝对会亮眼一些，招聘和购物都遵循着同样的道理，也就是追求性价比，此时我的性价比更高，所以肯定是招聘方的不二人选。&lt;/p&gt;
&lt;p&gt;从上面这则小故事中，希望诸位能明白一个道理：&lt;strong&gt;降维打击到底是什么意思？就是以超出对应岗位的能力去面试&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;比如，你目前具备找&lt;code&gt;18K&lt;/code&gt;工作的能力，但你偏偏去面&lt;code&gt;15K&lt;/code&gt;的招聘，这会显得你额外突出，自然也能在众多候选者中脱颖而出。但反过来，如果目前只具备&lt;code&gt;18K&lt;/code&gt;的能力，求职时也往&lt;code&gt;18K&lt;/code&gt;的招聘上冲，最终结果也就是显得平平无奇，毕竟和你竞争的人都处于同一个水平。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;但能面&lt;code&gt;18K&lt;/code&gt;为啥要去拿&lt;code&gt;15K&lt;/code&gt;？其实我的意思并非让大家降低自己的期望薪资，而是&lt;strong&gt;提高自己的技术水平&lt;/strong&gt;。比如你目前的期望薪资是&lt;code&gt;18K&lt;/code&gt;，那你就把能力提升到&lt;code&gt;20K&lt;/code&gt;的水准，然后再去面&lt;code&gt;18K&lt;/code&gt;的招聘，这才是我口中所谓的“降维打击”。这也是为什么我推荐大家提前准备面试的原因，充分准备和临时抱佛脚，两者之间相差甚大！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;PS：上面所述的&lt;code&gt;15K、18K、20K&lt;/code&gt;只是为了将技术能力具体化，实际求职过程中，&lt;code&gt;15K、18K&lt;/code&gt;的技术能力可能大致相同，相同技术能力的人，到底是拿&lt;code&gt;15K&lt;/code&gt;还是&lt;code&gt;18K&lt;/code&gt;，要取决于对应求职者的工作履历，也存在一定的运气成分。&lt;/p&gt;
&lt;h2&gt;洞悉人事篇：HR 是如何在成百上千份简历中挑选候选者的？&lt;/h2&gt;
&lt;p&gt;前面一节聊了面试前如何技术准备，这节来说说求职路上必遇的角色：&lt;code&gt;HR&lt;/code&gt;/人事，一次求职面试过程中，通常都会先与这个角色打交道。&lt;/p&gt;
&lt;p&gt;人事这个角色分歧很大，有令人厌烦的尖酸刻薄者，有善于交际的亲和派，也有狐假虎威的二五仔，甚至有啥也不懂的毕业生。&lt;/p&gt;
&lt;p&gt;在很多公司举行的匿名投票中，票选最让人讨厌的职位时，&lt;code&gt;HR&lt;/code&gt;往往都是首当其冲的那个。&lt;/p&gt;
&lt;p&gt;相对来说，&lt;code&gt;HR&lt;/code&gt;是一个吃力不讨好的职位，需要很高的情商才能做得很好，否则很容易遭人嫌，对外让应聘者讨厌（态度不好），对内让领导嫌弃（招不到人）、让员工厌恶（克扣考勤），所以做好&lt;code&gt;HR&lt;/code&gt;其实很难。&lt;/p&gt;
&lt;p&gt;不过这里就不对&lt;code&gt;HR&lt;/code&gt;的难处做过多描述了，下面重点谈一谈&lt;code&gt;HR&lt;/code&gt;对求职途中的影响。&lt;/p&gt;
&lt;h3&gt;一、重新认识一下 HR&lt;/h3&gt;
&lt;p&gt;任何企业中都会有&lt;code&gt;HR（Human Resource）&lt;/code&gt;，翻译过来叫人力资源，但也习惯被称为人事。小一些的公司也许只会有&lt;code&gt;1~2&lt;/code&gt;个人事专员，具备一定规模的企业则会组建人力资源部/人事部。其工作职责分为六大模块。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;人力资源规划：根据公司业务发展需求，规划内部各部门岗位需求和人员招聘数量。&lt;/li&gt;
&lt;li&gt;执行招聘计划：在各渠道发布招聘信息、负责应聘者的初次筛选、面试接待与&lt;code&gt;HR&lt;/code&gt;面等。&lt;/li&gt;
&lt;li&gt;组织内部培训：负责内部岗位技能、企业文化、新人入职等培训，督导培训计划的实施。&lt;/li&gt;
&lt;li&gt;参与绩效管理：参与制定绩效方案、组织绩效实施、评估、反馈等工作，完善绩效体系。&lt;/li&gt;
&lt;li&gt;核对员工考勤：核实员工的打卡、迟到、缺勤、旷工、请假、工牌、卫生等各类考勤情况。&lt;/li&gt;
&lt;li&gt;处理劳动关系：处理入职、试用、转正、升职、请假、异动、劝退、裁员等劳动管理工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，并非所有公司的人事都具备处理上述所有工作的权利，小一些的公司也许老板会直接参与其中，或者规模较大的企业还会有行政部辅助，但组织架构完善的企业，基本上都会规划出人力资源部，上述职责将会统统放权给该部门，但这些与我们没有太大关系，重点把注意力放在第二条，这是作为求职者需要关注的。&lt;/p&gt;
&lt;h4&gt;那些千年不变的只招不聘岗&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;HR&lt;/code&gt;是求职路上的第一关，通常会负责应聘者的简历筛选，但先记住：&lt;strong&gt;有些&lt;code&gt;HR&lt;/code&gt;并非真心招聘&lt;/strong&gt;。这具体是怎么回事呢？&lt;/p&gt;
&lt;p&gt;想要弄明白这个问题，就不得不先说清&lt;code&gt;HR&lt;/code&gt;的绩效机制。&lt;code&gt;KPI&lt;/code&gt;绩效考核制度是大部分企业中都存在的，一个员工绩效系数的高低，也将直接影响其工资收入，而人事部门同样涵盖在绩效考核的范围内，一般&lt;code&gt;HR&lt;/code&gt;的绩效指标有如下几点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;成本控制率：对员工的工资、奖金、福利补贴等成本不能超出预算。&lt;/li&gt;
&lt;li&gt;员工离职率：通常是指自己负责招入的新员工，需要控制离职率。&lt;/li&gt;
&lt;li&gt;内部满意度：主要指内部员工是否对人事有不满意、投诉，出现则会影响绩效。&lt;/li&gt;
&lt;li&gt;培训完成率：督办一场培训时，是否顺利完成、员工到场比例、员工满意度等。&lt;/li&gt;
&lt;li&gt;招聘完成率：主要跟面试邀约数量、到公司应聘人数、新员工的入职人数等挂钩。&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里抛开其他无关的绩效指标不谈，重点看最后一条关于招聘类的绩效指标，其中面试邀约数、实际应聘数、新员工入职人数等都会影响&lt;code&gt;HR&lt;/code&gt;的绩效，这一条也是专门负责招聘的&lt;code&gt;HR&lt;/code&gt;，在绩效考核制定中，对绩效系数影响最大的一个指标，意味着该指标会直接影响某些公司的&lt;code&gt;HR&lt;/code&gt;收入，因此市场上也有不少“非真心”的&lt;code&gt;HR&lt;/code&gt;出现。&lt;/p&gt;
&lt;p&gt;当你投递简历后，被对方邀请过去面试时，可能你只是她刷&lt;code&gt;KPI&lt;/code&gt;的工具，对方发布这个招聘需求属于“只招不聘”，你的作用在于帮她完成当月的绩效指标，为什么有些&lt;code&gt;HR&lt;/code&gt;会这么干呢？&lt;/p&gt;
&lt;p&gt;其实经过前面的绩效分析后，相信大家不用我说也就明白了，也就是为了把自己的招聘完成率提高，拉高自己的绩效系数罢了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PS：对于这点大家简单了解即可，虽然市场上有这类情况，但毕竟与整体情况对比，那也只是少数，所以不必太过在意，心里有谱就行。更多的招聘需求还是真心为了招人，而并非&lt;code&gt;HR&lt;/code&gt;刷绩效的手段。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;为什么大部分 HR 会比较热情？&lt;/h4&gt;
&lt;p&gt;相信大家见到的很多&lt;code&gt;HR&lt;/code&gt;，在接待你时都比较热情，这是为什么呢？&lt;/p&gt;
&lt;p&gt;一方面是出于礼貌和职业素养，而另一方面则有关她的利益，除开能够帮她提高绩效系数外，同时还有一些额外的好处：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因为有人过去面试可以帮她刷绩效，同时假设入职了，她有一定的奖金（这笔奖金不多），如果后续入职员工比较稳定，在厂里转正之后，又会得到一笔不小的奖金。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;听了上述回答后，相信大家应该能够想明白下面这三个问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你的面试能给 HR 带来什么？&lt;/li&gt;
&lt;li&gt;你的入职能给 HR 带来什么？&lt;/li&gt;
&lt;li&gt;你的转正能给 HR 带来什么？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但她当时是在工厂做&lt;code&gt;HR&lt;/code&gt;，负责工厂的招工。对于&lt;code&gt;IT&lt;/code&gt;公司的&lt;code&gt;HR&lt;/code&gt;来说，可能细节上会有所不同，但你的面试、入职、转正，多多少少都会给她带来一些好处或奖励。&lt;/p&gt;
&lt;p&gt;OK，对于&lt;code&gt;HR&lt;/code&gt;招聘的那些事就此打住，大家只要稍微对这些有了解就好了，毕竟只是为了让你更熟悉&lt;code&gt;HR&lt;/code&gt;，更重要的是接下来的内容：技术行业的&lt;code&gt;HR&lt;/code&gt;是如何筛选简历的呢？&lt;/p&gt;
&lt;h3&gt;二、技术行业的 HR 是如何筛选简历的？&lt;/h3&gt;
&lt;p&gt;想要了解技术行业的&lt;code&gt;HR&lt;/code&gt;是如何筛选简历的，那得先明白一个道理：&lt;strong&gt;九成八以上的人事并不懂技术&lt;/strong&gt;，但你投递简历后第一步就是人事做筛选。不知是否有人存在这种经历：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;明明我简历上技术写得很牛逼，项目写得也很优秀，但为啥一投简历就没反应啊？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你有这个困扰，那很有可能是因为你的简历太丑了，或者太过花哨了，这容易遭到&lt;code&gt;HR&lt;/code&gt;的排斥。毕竟她也不懂技术啊，当她打开你的简历之后，映入眼帘的就是排版不公正、样式特别丑、模板花里胡哨……问题时，可能她就是啪地一下，很快啊，就把简历关了，然后拖进回收站，整套动作一气呵成。&lt;/p&gt;
&lt;p&gt;所以，牢记一点，你想要简历的通过率高一点，第一步应该至少保证足够整洁，不说要让人家眼前一亮，但至少别给人家留下负面印象，因为一个岗位往往有很多人投简历，多你一个不多，少你一个也不少，简历如果很丑的话，的确会影响简历通过率。&lt;/p&gt;
&lt;h4&gt;HR 筛选简历的流程&lt;/h4&gt;
&lt;p&gt;明白&lt;code&gt;HR&lt;/code&gt;并不懂技术这个前提后，接着来聊聊&lt;code&gt;HR&lt;/code&gt;筛选简历的流程，以及&lt;code&gt;HR&lt;/code&gt;在不懂技术的情况下，她是如何筛选简历的、关注点又会放在哪儿。&lt;/p&gt;
&lt;p&gt;先来说说筛选流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;美好的一天，元气满满的&lt;code&gt;HR&lt;/code&gt;坐到电脑桌前，打开了招聘平台……&lt;/li&gt;
&lt;li&gt;随机点开一份投递的简历，简单看两眼快速扫描简历的整体信息。&lt;/li&gt;
&lt;li&gt;如果简历整体信息满足需求，再花费&lt;code&gt;8~15&lt;/code&gt;秒搜索简历中的关键指标。&lt;/li&gt;
&lt;li&gt;浏览简历搜索到关键信息后，接着会对简历进行评估，合适的单独拎出来。&lt;/li&gt;
&lt;li&gt;对于评估合格的简历，&lt;code&gt;HR&lt;/code&gt;会花费&lt;code&gt;1~3&lt;/code&gt;分钟仔细阅读，重点关注是否与招聘需求匹配。&lt;/li&gt;
&lt;li&gt;简历经过仔细审阅后，各项条件都符合招聘需求，接着联系应聘者，发出面试邀约。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述这个过程是“人肉式简历筛选”的过程，也是中小型企业会采用的方式。但国内一些大厂除外，它们往往具备完善的人力资源系统，内部会有招聘流程模块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;大厂的大致简历筛选流程如下&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简历首先被录入系统（从招聘渠道对接），简历会进入初审状态。&lt;/li&gt;
&lt;li&gt;然后交给系统&lt;code&gt;AI&lt;/code&gt;进行筛选，&lt;code&gt;AI&lt;/code&gt;会快速过滤掉一批不合适的简历。&lt;/li&gt;
&lt;li&gt;接着会从简历中快速提取关键指标、词汇，生成应聘者的个人画像（报表/图像）。&lt;/li&gt;
&lt;li&gt;通过求职者的个人画像比对投递的岗位&lt;code&gt;JD&lt;/code&gt;，判断求职者是否适合这个岗位。&lt;/li&gt;
&lt;li&gt;匹配岗位&lt;code&gt;JD&lt;/code&gt;的求职者，才会真正由系统交给&lt;code&gt;HR&lt;/code&gt;进行最终评估，合适则会锁住简历。&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比前面“纯手工”筛选简历的方式，大厂这种使用程序介入处理的方式更为智能，尤其是应对大厂每天接到的海量简历，这种系统能极大程度上减轻&lt;code&gt;HR&lt;/code&gt;的工作量。&lt;/p&gt;
&lt;p&gt;而且要注意一点，大厂为了防止每天出现海量的重复简历，通常这类系统对简历都有冷却期，也就是当你的简历被&lt;code&gt;pass&lt;/code&gt;过一次之后，需要等待一段时间后才能继续投递。在冷却期间内，就算多次反复投递也无法生效，系统在最开始就会自动过滤掉冷却期内的简历。&lt;/p&gt;
&lt;h4&gt;HR 在筛选简历时的关注点&lt;/h4&gt;
&lt;p&gt;前面简单了解简历筛选的流程后，下面说说&lt;code&gt;HR&lt;/code&gt;在筛选简历时的关注点，这将直接影响到简历的通过率。只有当大家真正明白了&lt;code&gt;HR&lt;/code&gt;的关注点之后，才能写出一份比较不错的简历，但本章不会过多阐述简历优化的内容，这些东西都是后话，本章旨在&lt;strong&gt;洞悉人事&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HR&lt;/code&gt;在上述不同的环节中，对简历信息的关注是不同的，如果你简历在任一环节有问题，都有可能影响简历的通过率。&lt;/p&gt;
&lt;p&gt;先来聊聊&lt;code&gt;HR&lt;/code&gt;刚打开简历的第一眼会关注什么呢。简历整洁性、应聘者的基本信息，如果简历上的字七扭八歪或者比较“辣眼睛”，这是很有可能被淘汰的，毕竟&lt;code&gt;HR&lt;/code&gt;大多数是女性，女性通常喜美厌丑，简历的美观整洁程度确实会成为筛选的第一原则。&lt;/p&gt;
&lt;p&gt;不过正常套用简历模板的情况下，基本不会由于外观被&lt;code&gt;pass&lt;/code&gt;，所以下面就来聊聊&lt;code&gt;HR&lt;/code&gt;会关注哪些基本信息。学历、年龄、工作年限，看这些信息是因为能够通过这些信息做最快筛选，学历、工作年限不合格者会直接&lt;code&gt;pass&lt;/code&gt;，同时看年龄是为了核实简历真实性，可以根据年龄来简单推断学历、工作年限是否真实。&lt;/p&gt;
&lt;p&gt;通过前面的快速筛选阶段后，接着&lt;code&gt;HR&lt;/code&gt;会对满足条件的简历进行快速过滤，这时的关注点会在期望薪资、上家公司信息与业务、以及历史工作履历上，第一点就不必我多说了，从这点能精准地得知是否能开起你想要的工资，如果给不到你想要的期望薪资，也会直接&lt;code&gt;pass&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;正是由于这点原因，所以很多人会在简历写上“薪资面议”，要不要写面议呢？简历优化篇再细聊。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;除开期望薪资外，还会去瞅你的工作经历，主要是看你任职的上家公司，在此期间，会关注上家规模、你的岗位以及公司业务性质，这三点会决定你与公司的匹配度。&lt;/p&gt;
&lt;p&gt;如果你上家公司是&lt;code&gt;500&lt;/code&gt;强、国内/外大厂，这无疑是你很大的加分项。&lt;/p&gt;
&lt;p&gt;看了你上家公司的规模后，接着会看一下你在上家公司的岗位，是否与目前招聘的岗位匹配，对于匹配度高的应聘者会优先选择，比如招项目经理，你有过项目经理的履历，无疑你会更合适一些。&lt;/p&gt;
&lt;p&gt;最后，&lt;code&gt;HR&lt;/code&gt;还会分析你的上家，与目前公司的业务匹配度，如果是同一业务类型的项目，这绝对将成为大大滴加分项。比如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个做金融性质的企业招聘，那最希望的是：招到一个之前具有金融经验的人。毕竟有经验代表上手速度快，如果招了一个之前专门写管理系统的新人进来，先不论技术如何，就光金融领域内的一堆业务概念，理解起来也需要不短的时间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;分析完你上家公司的情况后，接着会看看你的工作生涯，也就是你历史从业经历。如果你频繁地在不同公司内横跳，这无疑是会被&lt;code&gt;pass&lt;/code&gt;的类型。例如你在每家公司待的平均工龄是一年，那&lt;code&gt;HR&lt;/code&gt;心里会想：“我假设把你招进来了，一年之后你业务熟了，活干得也越来越快了，是不是很有几率也会跑路呢？”&lt;/p&gt;
&lt;p&gt;企业用人的第一原则是追求稳定，谁都不希望自己的企业内招到一个不稳定的人，所以具备“不稳定因素”的应聘者会直接在简历筛选中&lt;code&gt;pass&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;经过上述两轮筛选后，&lt;code&gt;HR&lt;/code&gt;手中留下的简历，既满足公司招聘的硬性条件，同时又贴合公司的业务线、岗位需求，所以接下来&lt;code&gt;HR&lt;/code&gt;会做最终的评选阶段，也就是看应聘者的“个人技术栈与项目经历”。诸多小伙伴看到这里就疑惑了：你前面不是说&lt;code&gt;HR&lt;/code&gt;不懂技术吗？！？？为啥&lt;code&gt;HR&lt;/code&gt;会去看个人技术栈啊？&lt;/p&gt;
&lt;p&gt;想要弄懂此问题，就得先知道：&lt;strong&gt;&lt;code&gt;HR&lt;/code&gt;发布的招聘需求怎么来的&lt;/strong&gt;？无非就两个方式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源一：用人部门写的&lt;/strong&gt;。需要招聘一个什么能力的人，最清楚的莫过于用人部门本身。当部门缺人时，尤其存在技术岗位空缺时，&lt;code&gt;HR&lt;/code&gt;并不清楚要招一个什么技术的人进来啊，所以大部分公司会由用人部门本身去写招聘需求，然后交给&lt;code&gt;HR&lt;/code&gt;对外发布。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源二：用人部门给的关键字去复制同行的&lt;/strong&gt;。不是所有公司的技术部门，都会亲自给&lt;code&gt;HR&lt;/code&gt;写招聘需求，毕竟大部分“技术人”的文笔水平欠佳，你叫我形容一下招什么人可以，但叫我写出标准的招聘需求就有些为难，通常这种情况下，用人部门只会给出一些技术栈关键字。但&lt;code&gt;HR&lt;/code&gt;看不懂这些技术栈啊，为此就直接去参考同行的招聘，将对应的招聘关键字套入其中，从而形成了自己的招聘需求。&lt;/p&gt;
&lt;p&gt;但无论&lt;code&gt;HR&lt;/code&gt;是通过哪种方式弄到的招聘需求，但最终手里都会有一份技术关键字的清单，虽然&lt;code&gt;HR&lt;/code&gt;不能直接筛选出技术达标的简历，但起码也能大概淘汰一些技术不达标的简历。&lt;/p&gt;
&lt;p&gt;所以，在最后这个筛选阶段中，&lt;code&gt;HR&lt;/code&gt;会通过用人部门给出的关键字，对简历进行精准匹配，尽量把简历的精准度提高（最后这个详细评审阶段中，有些专业的&lt;code&gt;HR&lt;/code&gt;会详细阅读你的简历，从而判断你与当前岗位到底合不合适）。&lt;/p&gt;
&lt;h4&gt;HR 也许不是最终决断者&lt;/h4&gt;
&lt;p&gt;经过前面&lt;code&gt;HR&lt;/code&gt;的一系列筛选后，你的简历经过千辛万苦，终于通过了重重难关，此时屏幕对面的你，也许会歪嘴一笑：“嘿，就你这小小&lt;code&gt;HR&lt;/code&gt;，能难住我？”&lt;/p&gt;
&lt;p&gt;但此刻别高兴得太早，因为&lt;code&gt;HR&lt;/code&gt;只是你简历的第一关，我们都懂&lt;code&gt;HR&lt;/code&gt;不会技术这个道理，相信招聘的公司也不会不懂，所以当&lt;code&gt;HR&lt;/code&gt;筛选出一批相对满意的简历后，接着会递给懂技术的用人部门！&lt;/p&gt;
&lt;p&gt;纳尼？技术面试官这时就出场了？&lt;code&gt;Yes&lt;/code&gt;，越专业、规模越大的企业招聘，技术面试官参与简历筛选的几率也就越大。身为求职方的我们，不想去参加一场无意义的面试，而作为招聘方的企业同样如此，与其喊很多人过来面试，不如从中再挑选出一批优秀者发出邀约，这样也能极大程度上减少多场不必要的面试。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;既然技术面试官会参与简历筛选，而同为技术人的他，在看简历时会关注什么呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在幻想你是一位面试官，现在手里有份简历，你站在技术人的角度出发，首先会关注什么？&lt;/p&gt;
&lt;p&gt;毫无疑问，必然是关注对方的技术，当&lt;code&gt;HR&lt;/code&gt;给坐在工位上的你，递来一份简历时，场景如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;（用漫不经心的目光一瞥，稍后嘴角上扬邪魅一笑）：嘿，让我看看这小子技术怎么样！&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;技术面试官在看简历时，并不会去关注你年龄多大、学历多高，更看重的是你的技术能力，也正因他与你同为技术人，所以对技术的考查会更加专业，看简历时大多数抱着这三个想法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;①先看看找工作的这小子，他会的我会不会，懂得有没有我多～&lt;/li&gt;
&lt;li&gt;②这小子会得不少，再看看他懂得有多深！&lt;/li&gt;
&lt;li&gt;③这小子技术不错，让我看看他做过什么项目，有没有我手上的项目牛～&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上，技术面试官在筛选简历时，通常会看技术广度、深度，以及项目经验这三点，所以想要通过技术面试官的简历筛选，这三点上面要下功夫，既不能太装，也不能显得太弱，毕竟太装了容易面试遭到惨打，太弱了入不了对方法眼。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这三点具体该如何写，也会在后面《简历优化篇》中详细阐述，这篇只了解大概的简历筛选流程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最后记住一点：&lt;strong&gt;简历是第一关，也是最重要的一关&lt;/strong&gt;！当技术面试官看完你的简历后，通知人事给你发起面试邀约，这意味着你的简历至少得到了他的认可，你要做的就是在面试中发挥出简历上的水平，薪资能在对方接受的范围之内时，你拿到&lt;code&gt;Offer&lt;/code&gt;的几率八九就不离十了（除开有比你更合适的人选，或你的性格、价值观不合适外）。&lt;/p&gt;
&lt;h3&gt;三、公司招聘时的意中人是怎样的？&lt;/h3&gt;
&lt;p&gt;截至目前，大家认真阅读完前面的内容之后，相信对&lt;code&gt;HR&lt;/code&gt;、技术面试官如何筛选简历的流程，已经熟透于心了。最后这个阶段，来聊聊企业招聘时，到底想招到什么样的人。&lt;/p&gt;
&lt;p&gt;众所周知的一点，现如今找工作越来越难，当准备开启一场面试之旅时，从自信到自闭的人不在少数，似乎现在找工作变得很困难了，是不？你这么想，实则企业也是这样想的，&lt;code&gt;HR&lt;/code&gt;也是这么想的。&lt;/p&gt;
&lt;p&gt;此时你小小的脑袋应该有着大大的疑惑：“找工作的人这么多，为什么企业招人还难啊？”&lt;/p&gt;
&lt;p&gt;其实更为具体一点来说，是企业想招到一个满意的人难，如今市场的技术人才鱼龙混杂，会吹技术差的、技术强不会吹的、没经验硬包装的、技术好又会吹但脾气差的……各色各样的求职者比比皆是，这就导致了企业想要招到“意中人”的难度大大提升，那企业眼中的“意中人”长啥样呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;身披黄金甲，脚踏七彩云……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;呸，走错片场了，通常企业招聘时真正的意中人要求如下。&lt;/p&gt;
&lt;p&gt;① &lt;strong&gt;能干活&lt;/strong&gt;：掌握的技术能力可以满足公司业务的基本需求。&lt;/p&gt;
&lt;p&gt;② &lt;strong&gt;上手快&lt;/strong&gt;：做过与公司业务接近的项目，熟悉公司所用的技术栈，要花费的培养成本低。&lt;/p&gt;
&lt;p&gt;③ &lt;strong&gt;够稳定&lt;/strong&gt;：入职后能够持续给公司贡献价值，不会入职一段时间后就提桶跑路。&lt;/p&gt;
&lt;p&gt;④ &lt;strong&gt;高性价比&lt;/strong&gt;：除开能完全与空缺岗位相匹配外，能干活的前提下还“不贵”。&lt;/p&gt;
&lt;p&gt;⑤ &lt;strong&gt;潜力高&lt;/strong&gt;：具备不错的学习能力，能随着公司业务的不断发展持续成长。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;活好价廉够稳定&lt;/code&gt;&lt;/strong&gt;，这是企业眼中的意中人，是不是与大家求职时的“&lt;strong&gt;钱多事少离家近&lt;/strong&gt;”很像？理想很丰满，现实却很骨感，求职往往很难找到一份“&lt;strong&gt;钱多事少离家近&lt;/strong&gt;”的工作，最终结局大多以“&lt;strong&gt;凑合着干&lt;/strong&gt;”结尾，而企业招聘时亦是如此，最终也只能满足于“&lt;strong&gt;又不是不能用&lt;/strong&gt;”这个水准。&lt;/p&gt;
&lt;p&gt;我们聊这个话题有何意义呢？很简单，我们要做的就是：&lt;strong&gt;把自己变成别人喜欢的样子&lt;/strong&gt;。企业招聘想要上手快的，我们投简历时，就可以专门去找和上家公司业务接近的岗位。企业想要高性价比的，那就执行之前聊到的降维打击方案……总之，尽量去迎合企业的用人需求，呈现给企业一种 &lt;strong&gt;“我是最合适”&lt;/strong&gt; 的感觉即可。&lt;/p&gt;
&lt;p&gt;最后也对本章的简历筛选流程做个简单总结，不多废话了，上个图就一清二楚，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506290804895.awebp&quot; alt=&quot;筛选流程&quot;&gt;&lt;/p&gt;
&lt;h2&gt;简历优化篇（上）：怎样撰写一份与自身情况最匹配的简历？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;简历优化&lt;/strong&gt;，一个十分美妙的词汇，也是很多人在苦苦追求的“求职秘方”，这也是许多标题党、网课惯用的词汇。不知大家是否见过以下这些标题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简历到底怎么写，才能让面试邀约的电话被打到爆？&lt;/li&gt;
&lt;li&gt;如何优化才能打造一份让&lt;code&gt;HR&lt;/code&gt;为之倾心的高光简历？&lt;/li&gt;
&lt;li&gt;最近面试接到手软，原来全靠这样去做简历优化！&lt;/li&gt;
&lt;li&gt;如何有效打造出一份杀手级的王牌简历！&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大家首次看到这些标题时，当时一定是两眼放光，但当你点击详细了解之后，绝对是期待有多高、失望就有多大，因为这类标题的背后，往往都是一些软文引流、营销广告……相信经历过的小伙伴肯定深有体会！&lt;/p&gt;
&lt;p&gt;那简历到底有没有优化技巧呢？其实有，优化简历能为你接到更多的面试，但打铁还需自身硬，它并不能成为你拿下&lt;code&gt;Offer&lt;/code&gt;的王牌手段，属于锦上添花。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;事先声明：简历优化属于求职途中比较重要的阶段，不会三言两语草草结尾，而是尽量做到事无巨细，所以篇幅较长，分为了上下两篇，大家阅读时请保持耐心。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;一、个人简历的基本原则与要素&lt;/h3&gt;
&lt;p&gt;撰写、优化简历时，首先要记住：&lt;strong&gt;不存在最好的简历&lt;/strong&gt;！没有哪份简历能让所有&lt;code&gt;HR&lt;/code&gt;都满意，就好像一个人再怎么完美，也不可能受到所有人的喜欢。为此，在写简历时不要追求所谓的最好，写出一份适合自己实际情况的简历才最重要。&lt;/p&gt;
&lt;p&gt;同时，往往诸多小伙伴在写简历前，喜欢去从网上找写好的简历模板、找朋友要简历，这种做法能理解，毕竟自己写简历时脑子难免有些短路，不知从何下手，所以想要参考一些相关的简历，尝试从中得到启发。&lt;/p&gt;
&lt;p&gt;参考他人简历确实是一种不错的方法，但参考不是照搬，千万不能找到一份简历后，把里面的名字等各类信息一改，大部分内容直接照搬，最后就形成了自己的个人简历，显然这并不合适。&lt;/p&gt;
&lt;p&gt;虽然照搬的效率最高，但每个人的技术掌握度、经历都有所差异，所以大家的简历也要根据自身情况进行调整，结合之前的知识树以及自我画像，多花点时间认真去写，才能得到一份最符合自己的简历。&lt;/p&gt;
&lt;p&gt;但写简历时怎么根据个人情况去写呢？下面先一起聊聊&lt;code&gt;写简历时的基本要素&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;写简历时最基本的第一要素，是保证简历的排版工整性。在上章中曾聊到过，如果简历不够工整，&lt;code&gt;HR&lt;/code&gt;打开后可能会直接关掉。比如这样的简历：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506290805758.awebp&quot; alt=&quot;排版不工整&quot;&gt;&lt;/p&gt;
&lt;p&gt;大家以正常人的目光来看，简历信息七扭八歪、中英文混乱、字体忽大忽小……如果你每天都会收到几十份简历，突然打开了一份这样的简历，会怎么做？相信你也会关掉它，毕竟一眼扫过去连信息都分辨不清，更别说美观度了，所以简历的第一要素就是&lt;strong&gt;排版工整&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;排版工整不仅是上下两行要对齐，而且最好也不要留白，留白会很别扭，并拉长简历篇幅，如下反例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506290806273.awebp&quot; alt=&quot;留白示例&quot;&gt;&lt;/p&gt;
&lt;p&gt;尤其是上面留白，但下面铺满时，看起来的感觉会更加不协调。最好的方式是多行归并为单行，以平铺的方式撰写基本信息（上述案例中，单行&lt;code&gt;3~4&lt;/code&gt;个信息左右）。&lt;/p&gt;
&lt;p&gt;除开信息排版工整外，大家在选用简历模板时，别选太花哨的，颜色尽量单调，一份简历中不要超过三种，并且样式也要足够清晰，简历布局最好是从上到下，这样才方便&lt;code&gt;HR&lt;/code&gt;阅读。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;颜色单调、样式简洁、布局从上到下&lt;/strong&gt;，这三点要素要遵守。不要试图用花哨的颜色、炫酷的样式、另类的布局来吸引&lt;code&gt;HR&lt;/code&gt;注意，更多时候反而起到反作用！我看过许多份求职简历，投简历的人才们，可谓是八仙过海各显神通，举几个典型的反例。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有个应届生的简历中，字体用了渐变色，持续盯个十多秒连眼睛都花了。&lt;/li&gt;
&lt;li&gt;还有个简历是用&lt;code&gt;PPT&lt;/code&gt;来做的，刚点开就是咔的一下，整出一个开场动画……&lt;/li&gt;
&lt;li&gt;还有许多简历布局花哨，左右布局、前面左右，后面上下布局，阅读起来特别麻烦。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样做能引起筛选简历的人注意吗？答案是绝对行，比如我至今都记得一些“人才”的简历，但这样“另类吸引力”，结果适得其反，影响阅读效率的简历，大多数都会被直接&lt;code&gt;pass&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;除开前面几点外，还要注意一点：&lt;strong&gt;字体大小、间距、行距合适&lt;/strong&gt;。如果简历上的字体大小跟蚂蚁一样，并且前字紧挨后字，上行紧贴下行时，这同样会十分影响阅读体验，当打开这样的简历后，想要看清还得先掏出老花镜。&lt;/p&gt;
&lt;p&gt;同时，作为技术人的我们，也要学会省略不必要的信息，比如身高、体重、民族、户籍、是否婚配等，这些信息有必要写吗？&lt;/p&gt;
&lt;p&gt;其实没有必要，因为这些不是技术行业的招聘要求，又不是去面服装模特，你人帅气质佳也好、貌美大长腿也罢，都跟你去面技术岗无关，所以尽量把这些无关信息省掉，没有人愿意去花时间，看一些与自己需求无关的内容。&lt;/p&gt;
&lt;p&gt;同时，对简历的篇幅也要稍加控制，最好是&lt;code&gt;2~3&lt;/code&gt;页左右，没人愿意浪费时间看长篇大论，为此，必须要用足够短的篇幅吸引眼球！&lt;/p&gt;
&lt;p&gt;我见过的部分简历中，有些求职者恨不得把整个人生经历中的每个细节写上去，最夸张的简历高达十多页。说句公道话，先不说这么长的简历&lt;code&gt;HR&lt;/code&gt;有没有耐心看，就算它通过了简历筛选，去面试打印简历时，光打印费就多出五六倍，不知情的人还以为是个作家要出本书呢。&lt;/p&gt;
&lt;p&gt;OK，前面把写简历时一些要注意的基本要素就讲明了，最后给一个通用的简历顺序。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;①基本信息：能第一时间让&lt;code&gt;HR&lt;/code&gt;了解到你的基本情况。&lt;/li&gt;
&lt;li&gt;②求职意向：说明到岗时间、期望薪资、工作性质等需求。&lt;/li&gt;
&lt;li&gt;③教育经历：额外明显地把你自己的教育背景体现出来。&lt;/li&gt;
&lt;li&gt;④工作经历：可以十分清晰地看到你的职业工作生涯。&lt;/li&gt;
&lt;li&gt;⑤专业技能：对于你的专业能力、水平、技术的良好体现。&lt;/li&gt;
&lt;li&gt;⑥项目经历：自己接手过、负责过的项目可以依次罗列出来。&lt;/li&gt;
&lt;li&gt;⑦个人荣誉：这点有没有都行，如果有特别值得说明的可以单独列出来。&lt;/li&gt;
&lt;li&gt;⑧自我评价：自我给自我的总结，但很多情况下大家都套模板（这点最后细聊）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简历会出现的几个大项如上，当你写简历时，没有特殊情况，就可以按照上面的优先级撰写，因为这个顺序正好符合&lt;code&gt;HR&lt;/code&gt;筛选简历的顺序。当你的简历能让&lt;code&gt;HR&lt;/code&gt;看得更舒心时，也自然会给&lt;code&gt;HR&lt;/code&gt;留下一个不错的印象（前提是你能够满足人家招聘的要求）。&lt;/p&gt;
&lt;p&gt;最后对这些撰写简历的基本要素稍加提炼，给出一个小的总结。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简历信息的排版一定要工整。&lt;/li&gt;
&lt;li&gt;选择的简历模板不要太花哨。&lt;/li&gt;
&lt;li&gt;简历色调尽量保持单调或统一。&lt;/li&gt;
&lt;li&gt;简历信息的布局选择从上到下。&lt;/li&gt;
&lt;li&gt;简历中的字体大小、间距、行距要合适。&lt;/li&gt;
&lt;li&gt;简历的篇幅最好控制在&lt;code&gt;2~3&lt;/code&gt;页左右。&lt;/li&gt;
&lt;li&gt;按照&lt;code&gt;HR&lt;/code&gt;筛选的流程做好简历排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一份简历遵守上述各个要素，基本上能够得到一个初稿，为啥是初稿呢？&lt;/p&gt;
&lt;p&gt;因为这样写出来的简历属于通用版，后续还需要根据自身的情况，对初稿进行适当调整，从而显得更加合理，也能让自己的简历做到扬长避短！但往往许多小伙伴写简历时，写到这里就止步了，原因在于不清楚如何继续优化，那接下来就一起聊聊这个话题吧。&lt;/p&gt;
&lt;h3&gt;二、如何优化出最适合自己的简历？&lt;/h3&gt;
&lt;p&gt;经过前面的一些叨叨絮絮，现在终于来到了大家最感兴趣的话题，也就是简历优化。但这里做个声明，下面确实会教大家一些简历优化的技巧，不过技巧永远只是技巧，可以提高简历的通过率，但还是那句话：&lt;strong&gt;打铁必须自身硬&lt;/strong&gt;，如果本身自己的技术就不强，履历很一般，那再好的技巧也是无力回天。&lt;/p&gt;
&lt;p&gt;简历优化这个话题比较大，本节先说通用的优化技巧，也就是任何人都可以用的优化手段；在下节中再聊大家比较关心的内容，即如何制造亮点、如何描述技术栈、怎样阐述项目经验等内容。&lt;/p&gt;
&lt;h4&gt;重视简历上的优先级&lt;/h4&gt;
&lt;p&gt;“优先级”这个概念，在&lt;a href=&quot;https://juejin.cn/book/7211868947363135545/section/7211873972005109760&quot;&gt;《技术突击篇》&lt;/a&gt;中曾讲到过，对于自己需要提升/复盘的技术栈，首先应该按优先级进行排序，接着再制定相应的学习路线，这样才能确保自己准备的内容对目前最有利。&lt;/p&gt;
&lt;p&gt;而任何事情都分轻重缓急，简历上的信息也不例外。简历各个信息，也应该分清先后顺序，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;①自我评价&lt;/li&gt;
&lt;li&gt;②兴趣爱好&lt;/li&gt;
&lt;li&gt;③所获荣誉&lt;/li&gt;
&lt;li&gt;④基本信息&lt;/li&gt;
&lt;li&gt;⑤项目经历&lt;/li&gt;
&lt;li&gt;⑥工作经验&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上述这个顺序撰写简历行不行？&lt;code&gt;No&lt;/code&gt;，如果你敢这样写简历，&lt;code&gt;HR&lt;/code&gt;就敢第一个&lt;code&gt;pass&lt;/code&gt;你，为什么？因为&lt;code&gt;HR&lt;/code&gt;筛选简历时，大半天都找不到她要的信息在哪个位置，每次寻找一个信息都需要做“全表扫描”，所以遇到这样的简历时，第一时间会被&lt;code&gt;pass&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这也是为何强调顺序的原因，方便&lt;code&gt;HR&lt;/code&gt;就是方便自己，所以&lt;strong&gt;一份基本的简历应该分清主次&lt;/strong&gt;，哪些内容写前面，哪些内容写后面，心里要有个底。前面第一阶段中已经给了一个通用排序，这里就不重复啰嗦了，此处强调优先级的目的，主要是为了在后面讲优化技巧时，能帮助大家理解为什么要这样优化。&lt;/p&gt;
&lt;h4&gt;基本信息的优化&lt;/h4&gt;
&lt;p&gt;简历简历，意味着简历该简，不应该过多叙述的一些内容，在简历上就要省去。这点在前面就已提及，但一个技术人的求职简历，基本信息这栏要留下哪些内容呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;姓名：起码能够让人家知道你叫啥，这点必须在。&lt;/li&gt;
&lt;li&gt;性别：这点写不写都无所谓，但一般邀约中都会有先生/女士这类称呼，所以写上最好。&lt;/li&gt;
&lt;li&gt;年龄：有些招聘方会卡年龄，例如大厂，所以超过&lt;code&gt;30&lt;/code&gt;岁的小伙伴可以省略。&lt;/li&gt;
&lt;li&gt;工作年限：招聘方一般都有工作经验要求，这点必须在，突出匹配度。&lt;/li&gt;
&lt;li&gt;电话：这点不做过多解释，毕竟人家面试邀约、电话面试、入职邀约等都可能直接打电话。&lt;/li&gt;
&lt;li&gt;邮箱：这点大家也都懂，有些公司发面试邀约、入职&lt;code&gt;Offer&lt;/code&gt;，都会直接以邮件形式通知。&lt;/li&gt;
&lt;li&gt;学历：敲门砖，大多数&lt;code&gt;HR&lt;/code&gt;第一时间关注的信息中，就包含了这项内容，必须写。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里把工作年限、学历单独拎出来讲一下，我知许多人喜欢包装年限，尤其是培训出身的小伙伴。&lt;/p&gt;
&lt;p&gt;毕竟在之前的&lt;code&gt;IT&lt;/code&gt;市场中，工作年限越高就意味着薪资越高，包括现在的市场也依旧遵守这个潜规则，所以包装年限的人不在少数。但记住包装的经验要经得起推敲，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;年龄&lt;code&gt;22&lt;/code&gt;岁，学历专科，工作经验四年。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这显然并不合理，正常的专科毕业就&lt;code&gt;20&lt;/code&gt;岁了，你目前&lt;code&gt;22&lt;/code&gt;岁，哪儿来的四年工作经验？所以这类经不起推敲的简历，必然是&lt;code&gt;HR&lt;/code&gt;第一批淘汰的目标。为此，包装可以，但请一定要合理！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;也许这时有一些特殊的小伙伴又要说了：“我情况不一样啊，专科是自考/成考/函授等方式拿到的，我&lt;code&gt;18&lt;/code&gt;岁就做程序员了，按理来说的的确确有四年工作经验呀！”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于这类特殊群体而言，如果学历并不是统招全日制，就只能说明自身情况，这样可以避免你在第一轮筛选中被淘汰（或者你简历上的年龄也包装一下，但入职时如何解释就看个人发挥了）。&lt;/p&gt;
&lt;p&gt;那把年龄从简历上删了可以吗？不行，大部分&lt;code&gt;HR&lt;/code&gt;在看不到年龄的情况下，因为她不清楚你的具体情况，只会感觉你的简历经不起推敲，所以你的简历依旧会被淘汰。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PS：我给出的基本信息这一栏，似乎没写毕业院校是不？很多人会习惯性地把这个写上，其实写的意义不大，毕竟后面还会有教育经历这一栏，里面会包含毕业院校的，所以你在基本信息中省掉也无大碍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最后，简历要不要放照片呢？贴照片这点本身没有错，但最好不要贴照片，要贴的话也别贴生活照，而是从专业照相馆中拍出来的证件照。&lt;/p&gt;
&lt;p&gt;除非你拥有例如彭于晏、胡歌……以及我这样的颜值，这时你贴什么类型的照片都没关系，而且还能给你的简历加分，毕竟人都是追求美的动物，更何况许多&lt;code&gt;HR&lt;/code&gt;都是女孩子呢。&lt;/p&gt;
&lt;p&gt;当然，还有一种人特别适合贴照片，即看起来就很强的大佬，看起来很强是啥意思呢？聪明绝顶的大佬！说人话就是拥有地中海发型的大佬，当你贴上一张这样的照片时，整个简历无需过多的描述，懂行的人一眼就能看出你是大牛，发型就是技术的最好证明！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;小总结：对于基本信息的优化，主要是省去不必要的信息，能够经得起推敲，以及方便&lt;code&gt;HR&lt;/code&gt;阅读即可。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;求职意向的优化&lt;/h4&gt;
&lt;p&gt;求职意向这一栏，有些小伙伴会下意识忽略它，或者就简单写个目标岗位，但其实，这栏最好还是在简历上单独列出来，并且优先级排第二最佳！&lt;/p&gt;
&lt;p&gt;在我看过的简历中，求职意向通常都写成这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506290808985.awebp&quot; alt=&quot;求职意向&quot;&gt;&lt;/p&gt;
&lt;p&gt;先来聊聊上面的到岗时间，许多人喜欢写随时到岗，其实这种做法并不太好，虽然&lt;code&gt;HR&lt;/code&gt;面的时候，招聘方会再次主动询问到岗时间，但这里最好也写成“一周内到岗”。&lt;/p&gt;
&lt;p&gt;这样做的好处是：当你收到&lt;code&gt;Offer&lt;/code&gt;之后，可以谈出一周左右的缓冲期，在这个时间内可以再去面其他公司，拿到多个&lt;code&gt;Offer&lt;/code&gt;后再综合考虑选择谁！身为求职者的我们，也应该具备选择的权利，把主动权牢牢掌握在自己的手中。&lt;/p&gt;
&lt;p&gt;同时求职意向这栏中，如果自己目前还在职，属于骑驴找马者的话，也应适当描述自己的现状，例如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506290809130.awebp&quot; alt=&quot;求职意向&quot;&gt;&lt;/p&gt;
&lt;p&gt;写出自己的现状，这方便&lt;code&gt;HR&lt;/code&gt;筛选时，判断你是否合适，如果无法接受较长的到岗时间，就自然不会邀请你去面试。同时也方便了自己，因为如果不写明这点，当你面试通过拿&lt;code&gt;Offer&lt;/code&gt;时，对方发现你无法在短时间内与上家交接，很可能就会放弃你重新招聘，最终导致你这场面试相当于白搞。&lt;/p&gt;
&lt;p&gt;接下来，对期望薪资这点再展开聊聊，这点不用想，至少&lt;code&gt;80%+&lt;/code&gt;的人都喜欢写“面议”，那到底该不该写“面议”呢？&lt;/p&gt;
&lt;p&gt;这其实要分情况来看，写“面议”的好处在于能接到更多的面试，毕竟招聘方不清楚你的期望薪资，所以在你简历还不错的情况下，都会对你发出面试邀约。&lt;/p&gt;
&lt;p&gt;这样听起来似乎写“面议”很好对不对？但并非如此，你把具体的期望薪资写上会更好，&lt;code&gt;Why&lt;/code&gt;？说到这里很多人犯迷糊了，为什么面试多了还不好啊？道理很简单，虽然写上期望薪资后面试会变少，但这些面试邀约将会更精准！&lt;/p&gt;
&lt;p&gt;更加精准的含义是指：既然招聘方看完了你的期望薪资，还依旧对你发出面试邀约，这代表对方可以接受你的报价！也就是只要你能力达标，对方就可以给你满意的薪资，所以，虽然表面看起来面试少了，但得到的效果反而会更佳。&lt;/p&gt;
&lt;p&gt;那到底要不要写上期望薪资呢？前面说过要因人而异，如果你目前处于待业状态，迫切想找到一个新工作，此时简历写“面议”的效果比较好，毕竟这样面试机会更多。&lt;/p&gt;
&lt;p&gt;但如果你目前还在职，属于骑驴找马这类人，写出具体薪资会更合适，既能够减少很多不必要的面试（毕竟面试需要请假或者抽时间），同时还能让自己的面试邀约更精准。&lt;/p&gt;
&lt;p&gt;最后，还有一类人就算离职了，也适合写具体薪资，就是对自己的技术较为自信，或者技术能力比较出色，同时也不急着找到一份新工作的人，毕竟你都不急了，自然面试更精准更好一些。&lt;/p&gt;
&lt;p&gt;如果你决定写期望薪资，尽量写成范围值，并且比真实的期望薪资，多出一点点，这样有利于后续的谈薪。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;小总结：对于求职意向的优化，主要针对到岗时间、目前现状、期望薪资这三点，小伙伴可以根据实际情况来做优化调整。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;教育经历的优化&lt;/h4&gt;
&lt;p&gt;教育经历这玩意儿呢，很多小伙伴都喜欢放在第二栏，重点突出自己的教育背景，&lt;code&gt;HR&lt;/code&gt;打开简历之后，第一眼就能瞅着，把它放在这里真的合适吗？也要因人而异，比如这种情况：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506290813040.awebp&quot; alt=&quot;教育经历&quot;&gt;&lt;/p&gt;
&lt;p&gt;合适吗？不合适，因为专科学历并非是一种优势，或者说并不是一个亮点，所以对于专科、双非本或者非统招学历者，我的建议是放到后面几栏去。&lt;/p&gt;
&lt;p&gt;什么样的人适合把教育经历放在第二栏呢？三类人：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;①具备国内&lt;code&gt;985/211&lt;/code&gt;名校背景，例如清华大学、北京大学、复旦大学等；&lt;/li&gt;
&lt;li&gt;②具备高学历，例如硕士研究生学位、博士研究生学位等；&lt;/li&gt;
&lt;li&gt;③具备海外名校的留学经历，如英国剑桥大学、美国哈佛大学等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;符合这三类标准的小伙伴，就可以把教育经历放在第二栏这个显眼的位置，毕竟这属于你的优势，也是你本身的亮点之一，不仅要把位置靠前，还可以重点把这块区域的字体加粗（虽然有点刻意，但不免是一种引起&lt;code&gt;HR&lt;/code&gt;注意力的手段）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;PS&lt;/code&gt;：走校招路线的应届生，不管院校背景如何，都可以把教育经历放在第二栏。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;小总结：对于教育经历的优化，主要强调了在简历中的优先级，什么人才适合放在第二栏。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;工作经历的优化&lt;/h4&gt;
&lt;p&gt;工作经历这一栏，相信参考过一些简历的小伙伴，应该熟悉其通用模板，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506290814132.awebp&quot; alt=&quot;工作经历&quot;&gt;&lt;/p&gt;
&lt;p&gt;先写任职时间、公司名称、所在部门、担任职务这四项，下面的子栏中，则详细写出自己的工作职责，将自己的从业经验按公司划分，分别套入其中之后，能够清晰反映出自己的工作经历，但这里面有三个小技巧要说一下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一个技巧，工作经历以倒序的形式描述&lt;/strong&gt;。有些人写工作经验时，喜欢根据从业时间线从早写到如今，但这种方式有点不好就在于：无法将上家第一时间呈现给&lt;code&gt;HR&lt;/code&gt;。所以最好以倒序的手法描述工作经历，也就是最近的一家公司放最前面，因为&lt;code&gt;HR&lt;/code&gt;看工作经验这一栏时，重点会关注你的上家公司。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二个技巧，如果存在多段比较短的工作经历，可以合并成一份工作经验&lt;/strong&gt;。虽然这样做听起来不地道，但却是求职途中惯用的伎俩，毕竟上章聊过，&lt;code&gt;HR&lt;/code&gt;除开关注上家公司外，还会关注你在每家公司的任职时间，如果每家公司的任职时间都不长，说明你是个十分不稳定的人，很有可能由于此原因造成简历被淘汰（但多段工作经验合成一段时，最好是将之前的工作经验合并，如果是最近的一两家公司，是可以通过社保缴纳情况查出来的，所以合并经历时也要视情况而定）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三个技巧，适当控制工作经历的篇幅&lt;/strong&gt;。工作年限较长的小伙伴，可能待过很多家公司，如果每段经历都描述得很详细，一方面自己很难回忆起来，另一方面也会大幅度拉长篇幅。为此，如果年限比较长，那把最近两家公司的工作职责写细即可，其他公司的经历可以略写（如上图中的第三段工作经历）。&lt;/p&gt;
&lt;p&gt;OK，掌握上述三个技巧后，接着再说说另外两个比较重要的优化手段，但聊之前得先认识项目中的不同角色，我不知大家的简历，是否属于这样的情况，以&lt;code&gt;Java&lt;/code&gt;为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;求职意向：&lt;code&gt;Java&lt;/code&gt;开发工程师。&lt;/li&gt;
&lt;li&gt;第一份工作担任的岗位：&lt;code&gt;Java&lt;/code&gt;开发工程师。&lt;/li&gt;
&lt;li&gt;第二份工作担任的岗位：&lt;code&gt;Java&lt;/code&gt;开发工程师。&lt;/li&gt;
&lt;li&gt;第三份工作担任的岗位：&lt;code&gt;Java&lt;/code&gt;开发工程师。&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然我没见过诸位的简历，但我相信简历类似这种情况的小伙伴有很多，求职意向也好，还是工作履历也罢，所有职位都写&lt;code&gt;XXX&lt;/code&gt;工程师，首先声明：其实这样写并没有问题，但也可以做得更好，怎么做呢？&lt;/p&gt;
&lt;p&gt;先聊聊项目中的不同角色，通常项目中都会分为三类人。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通成员：负责项目中边角业务的相关工作，例如开发、文档撰写、测试……&lt;/li&gt;
&lt;li&gt;核心骨干：承担项目中核心业务功能的处理工作。&lt;/li&gt;
&lt;li&gt;项目主管：作为项目的负责人，主导项目进度的正常推进，参与产品设计、规划等工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;任何技术团队都有这三类角色，初中级水平的技术人一般站在成员梯队，中高级水平的技术人通常站在骨干梯队，而高级/资深水平的人，往往站在主管梯队，这也是业内的一种潜规则，能力越强的人必然职位越高，因此你可以借助这些去优化工作经历，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一份工作：&lt;code&gt;XXX&lt;/code&gt;开发工程师。&lt;/li&gt;
&lt;li&gt;第二份工作：&lt;code&gt;XXX&lt;/code&gt;主程、&lt;code&gt;XXX&lt;/code&gt;核心开发。&lt;/li&gt;
&lt;li&gt;第三份工作：项目负责人、项目经理、技术总监、&lt;code&gt;XXX&lt;/code&gt;架构师。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，我上面只是举例说明，这么干的好处在哪儿呢？能让&lt;code&gt;HR&lt;/code&gt;看到你的职位在随着工作不断上升，也就是你的履历呈现上升趋势，而不是干了&lt;code&gt;N&lt;/code&gt;年的“基层杂役”，从而制造一定的优势。&lt;/p&gt;
&lt;p&gt;看到这里许多人又来了疑惑：“可是我的项目就只有三个人啊，项目很小怎么办？”其实这是好事，&lt;code&gt;Why&lt;/code&gt;？你想想，既然你这个项目只有三个人，那你属不属于项目的核心开发？是不是项目的主程？答案当然是的，所以请放心大胆地写在工作经历上，别再写&lt;code&gt;XXX&lt;/code&gt;工程师了，直接写&lt;code&gt;XXX&lt;/code&gt;主程或&lt;code&gt;XXX&lt;/code&gt;核心开发工程师。&lt;/p&gt;
&lt;p&gt;当然，如果你这个项目只有你一个人负责的话，那这更是天大的好事啊，信我的，直接写项目负责人、项目经理，在&lt;code&gt;HR&lt;/code&gt;筛选你简历的时候，绝对能让她高看一眼。就事论事，这撒谎了吗？显然没有，只是把事实换了一种手法论述罢了，但最终呈现的结果却完全不同。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：当你在简历上写下&lt;code&gt;XXX&lt;/code&gt;核心、主程、负责人时，也一定要做好相关的准备，如何准备呢？相信你在以往公司一定有上级吧？你就把他的工作职责写成你的就行，毕竟任何一家企业在招聘时，不可能详细了解到你在以往公司的内部情况，所以请把你那颗忐忑不安的心放在肚子里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最后，对于工作职责如何写呢？如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参与了&lt;code&gt;XXX&lt;/code&gt;系统的研发工作，主要负责&lt;code&gt;AAA、BBB、CCC...&lt;/code&gt;工作；&lt;/li&gt;
&lt;li&gt;与某某部门的&lt;code&gt;XX&lt;/code&gt;成员进行对接，协助其完成&lt;code&gt;MMM、NNN、ZZZ.....&lt;/code&gt;工作；&lt;/li&gt;
&lt;li&gt;负责项目的&lt;code&gt;aaa、bbb、ccc....&lt;/code&gt;等工作，基于&lt;code&gt;XXX&lt;/code&gt;完成某某工作；&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看着上面这些用语，有没有一种熟悉的感觉？相信大部分人都是这样写的，看着似乎没有大毛病，但没有问题就是最大的问题，这样去写能否突出你的优势？显然不能，你与其他人对比，同样平平无奇，没有任何亮点存在，那究竟该如何写呢？比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参与系统重构工作，解决了长期存在的代码臃肿问题，极大程度上提升了项目的拓展性；&lt;/li&gt;
&lt;li&gt;负责主导项目中&lt;code&gt;XX&lt;/code&gt;模块的优化工作，解决了&lt;code&gt;XXX&lt;/code&gt;延迟问题，成功将响应速度提升&lt;code&gt;200%&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;与&lt;code&gt;XX&lt;/code&gt;部门携手攻克性能问题，解决了&lt;code&gt;XX&lt;/code&gt;时间段的并发问题，自此项目的吞吐量提升四倍；&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比前面常规的通用写法，后面这种手法描看着是不是更有冲击力？至少技术面试官在筛选你简历时，他肯定对你这些工作中，如何解决问题的具体手段额外感兴趣，因此对比前面那种普通写法的求职者来说，你的简历将会更有竞争力。&lt;/p&gt;
&lt;p&gt;这种阐述手法，也被称之为**&lt;code&gt;STAR&lt;/code&gt;法则**，也就是在形容一项工作时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先说执行此项工作的情景（&lt;code&gt;Situation&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;接着再说本次工作的任务（&lt;code&gt;Task&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;然后再说本项工作的行动过程（&lt;code&gt;Action&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;最后再说此项工作落实后取得的结果（&lt;code&gt;Result&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大家在描述工作职责时，尽量摆脱传统简历模板中的描述手法即可，多运用这种所谓的&lt;code&gt;STAR&lt;/code&gt;法则来套入就行。&lt;/p&gt;
&lt;p&gt;最后，对于个人的工作职责来说，有两类人可以适当做出其他调整。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;①技术管理：可以写明自己的管理规模，再写出自己带团队的成果等。&lt;/li&gt;
&lt;li&gt;②技术牛人：写出实际工作收益，如在职期间解决了大流量、高并发（亿级流量、百万并发）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;小总结：对于工作经历的优化技巧不算少，首先说了描述工作经历时的三个小技巧，接着讲明白了项目中的不同角色，最后又聊到工作职责该如何去写。写工作经历时要多运用前面提到的原则、技巧，尽可能地提升“简历的竞争力”。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;简历优化篇（下）：如何美化专业技能与打造项目技术亮点？&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;https://juejin.cn/book/7211868947363135545/section/7211874336826163252&quot;&gt;《简历优化上篇》&lt;/a&gt;中，我们已经打造了一份“基本”的简历，但对于求职的技术人而言并不够，毕竟技术行业的简历，最关键的还是专业技能的表达，以及项目经验的描述。为此，本章中会详细讲到这些内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;怎样写专业技能才能显得不一样？&lt;/li&gt;
&lt;li&gt;如何去打造项目经验中的亮点呢？&lt;/li&gt;
&lt;li&gt;怎么写简历上的自我评价才诚恳？&lt;/li&gt;
&lt;li&gt;针对不同的&lt;code&gt;JD&lt;/code&gt;该如何微调简历？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在就不再多说废话啦，让我们直接开始吧！&lt;/p&gt;
&lt;h3&gt;一、怎样写好简历上的专业技能&lt;/h3&gt;
&lt;p&gt;作为技术从业者的我们，在简历上表露自身的技术能力，这自然是必不可少的一项，毕竟这可是咱们吃饭的看家本领！那在简历上描述个人技术时，到底是该吹牛呢，还是谦虚啊？吹牛怕被面试官吊打，谦虚又怕别人看不上，这可怎么办？没关系，下面我们就逐步聊聊简历的专业技能该如何写。&lt;/p&gt;
&lt;h4&gt;遵循六条基本原则&lt;/h4&gt;
&lt;p&gt;在描述简历上的专业技能时，首先得遵循下述六条原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;描述技术栈形容的词汇需保持一致；&lt;/li&gt;
&lt;li&gt;各项标点符号要统一（包括符号的输入法）；&lt;/li&gt;
&lt;li&gt;包含英文的技术关键字，遵循驼峰命名法；&lt;/li&gt;
&lt;li&gt;对技术按属性分类分项，条条罗列更清晰；&lt;/li&gt;
&lt;li&gt;对掌握的技术按热度、掌握度排序；&lt;/li&gt;
&lt;li&gt;“精通”要慎用，要确定自己能驾驭再用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来会先展开讲讲这六条原则，然后再讲解如何对其进行优化，从而突出自己的技术优势。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一条，形容技术的词汇要统一&lt;/strong&gt;。这是指对于所有技术的掌握程度，应该使用相同性质的形容词汇进行描述。例如，最常用、也是最经典的一组形容词汇：&lt;strong&gt;了解、熟悉、熟练掌握、精通&lt;/strong&gt;，这分别代表技术的四个掌握层次。简历上描述技术能力时，通常要使用同一组词去描述，不能出现下述这种情况：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;熟悉 XXX、专注 XXX、精通 XXX、善于 XXX……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;虽然上述这种用不同表达词的描述手法也可以，但最好还是用同一性质的形容词汇（至少看起来不要脱离一个词系），这样能直接把你对技术的掌握程度反馈给面试官，能让面试官清楚你对各项技术的掌握度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二条，标点符号统一&lt;/strong&gt;。主要指中英符号一致、结尾符号一致，这是为了保证简历的美观程度。毕竟中、英模式下的各个符号，相对都有所差异，如果不统一的情况下，显然会影响简历的整洁性，例如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506290826905.awebp&quot; alt=&quot;反例&quot;&gt;&lt;/p&gt;
&lt;p&gt;这样的简历看上去有些潦草感，&lt;code&gt;HR&lt;/code&gt;筛选简历时，也会注重整洁性问题的。为此，在描述专业技能时，一定要统一标点符号，如：所有符号都为中文符号，每一项都以&lt;code&gt;。&lt;/code&gt;句号结尾。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三条，英文单词的开头字母大写，不同单词间遵循驼峰命名法&lt;/strong&gt;。作为&lt;code&gt;IT&lt;/code&gt;开发人员，掌握的专业技能中，难免会有许多技术关键字是英文，因此要注意这些关键字的大小写。以&lt;code&gt;Java&lt;/code&gt;为例，&lt;code&gt;SpringMVC&lt;/code&gt;不能写成&lt;code&gt;springmvc、SPRINGMVC、sPrINgMvC&lt;/code&gt;等形式，毕竟这样写严重影响美感，并且还显得自身不够专业（同时单词也不能打错，字母顺序也不能打错，注意多加检查）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四条，对技术按属性分类分项&lt;/strong&gt;。有些小伙伴为了图方便，描述专业技能时会将其写成一段话。但其实要想让简历更直观、更便于阅读，专业技能最好分成多项去描述。不过分项时要注意，最好按&lt;code&gt;属性&lt;/code&gt;做分类，而不是随意罗列。&lt;/p&gt;
&lt;p&gt;以全栈开发举例，假设通过如下方式描述：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;精通 Nginx、Vue、CSS、Spring、MQ、JVM、React、gRPC……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;你觉得合理吗？并不合理，毕竟这些技术都不属一个领域！最好的做法是按照属性的不同，将每个属性单开一项进行描述，如前端分一项、Java 分一项、中间件分一项……&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第五条，技术栈按热度、掌握度排序&lt;/strong&gt;。面试官看专业技能这一栏的目的是啥？想要看出你会什么，并判断你与空缺岗位所需的技术栈是否匹配。为此，在写专业技能时，一定要按技术热度进行排序，热度越高代表需求更高，匹配度自然也越高，因此将热度高的技术栈放在前面，更便于招聘方“检索”信息。除开热度外，也可以适当地将自己比较擅长的技术放前面，比如“精通”的优先级应该要高于“了解”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第六条，“精通”要慎用！&lt;/strong&gt; 很多人在写专业技能时，都存在一个疑惑：“我这项技术到底是写了解，还是熟悉、熟练掌握、精通呢？”大家很难把握这个度，因此往往会遵循下面这个原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;听过写了解，学过写熟悉，用过写熟练掌握，研究过底层写精通。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样做有没有问题呢？其实前三项问题不大，&lt;strong&gt;但“精通”这个词要慎用&lt;/strong&gt;！因为只要你敢写“精通”，那后果绝对是会引来面试官的狂轰滥炸。为此，如果你对某项技术有绝对自信，有过全面且深入性的研究，能 Hold 住吹出去的牛，那简历上就放心大胆地吹。反之，尽量谦虚为上，不要去写“精通”。&lt;/p&gt;
&lt;p&gt;遵守上述六条原则撰写简历上的专业技能，最后能得到一份“初稿”，接着可以再建立在“初稿”的基础之上，对其不断进行优化与改进即可。&lt;/p&gt;
&lt;h4&gt;正常人如何美化自己的专业技能？&lt;/h4&gt;
&lt;p&gt;对技术栈的描述，许多人都停留在用“了解、熟悉、熟练掌握、精通”这套词，然后就没有然后了……&lt;/p&gt;
&lt;p&gt;举个尤为常见的例子，如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;熟练掌握&lt;code&gt;AAA&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;熟悉&lt;code&gt;BBB&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;了解&lt;code&gt;CCC&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;学习过&lt;code&gt;DDD&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;有过&lt;code&gt;XXX&lt;/code&gt;经验。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;这样可以吗？当然可以，但你没有真正表达出对技术的掌握度，比如你写熟悉&lt;code&gt;BBB&lt;/code&gt;，但你到底熟悉到什么程度呢？看你简历的人无从得知，假设你是一位面试官，想要基于简历进行提问时，又该如何提问？你不清楚，只能按自己的推断去提问。&lt;/p&gt;
&lt;p&gt;也正是由于上述原因，所以在描述技术栈可以稍加&lt;strong&gt;美化&lt;/strong&gt;，如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原文：精通&lt;code&gt;Spring&lt;/code&gt;框架。&lt;/li&gt;
&lt;li&gt;美化：精通&lt;code&gt;Spring&lt;/code&gt;框架，曾阅读过&lt;code&gt;IOC、AOP、MVC&lt;/code&gt;、事务机制的源码。&lt;/li&gt;
&lt;li&gt;原文：熟悉&lt;code&gt;Nginx&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;美化：熟悉&lt;code&gt;Nginx&lt;/code&gt;代理技术，能熟练运用&lt;code&gt;Nginx&lt;/code&gt;搭建服务的热备集群。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是给面试官一个方向，看到你的这项技术栈之后，能明确知道你具体对哪方面有过深入研究，至少能在面试中，给予对方一个提问的方向，而并不是让对方盲目提问。简单来说就是：&lt;strong&gt;对于自己掌握的技术栈，不要再用一个单调的词汇形容，而是加上一些修饰语去引导&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;经验丰富者又该如何优化专业技能？&lt;/h4&gt;
&lt;p&gt;前面所说的方法都只适用于“初、中、高级”水平的人，随着工作年限的不断增长，个人的技术栈也在不断丰富，所以一些工作多年的技术人，所掌握的技术栈十分多，再按照前面的方式就容易写出长篇大论。我见过的简历中，甚至见过专业技能写了三十多项的牛人，但这样去写，不如换成下面这种方式（以后端为例）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;十年&lt;code&gt;IT&lt;/code&gt;开发经验，六年系统架构经验，具备丰富的大型项目处理经验；&lt;/li&gt;
&lt;li&gt;精通大流量、高并发、海量数据处理，擅于构建高吞吐低延迟架构；&lt;/li&gt;
&lt;li&gt;精通常用开源框架，如&lt;code&gt;Spring&lt;/code&gt;体系框架，阅读过大部分框架底层源码；&lt;/li&gt;
&lt;li&gt;精通分布式/微服务架构，熟知分布式架构各难点排除及解决方案；&lt;/li&gt;
&lt;li&gt;精通关系型数据库、非关系型数据库，擅于搭建大流量系统存储中心；&lt;/li&gt;
&lt;li&gt;精通&lt;code&gt;Shell&lt;/code&gt;脚本语言编写，具备搭建可持续化自动部署/监控中台经验；&lt;/li&gt;
&lt;li&gt;精通中间件技术、线上问题排除、系统故障分析、应用性能优化手段；&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看上述这段专业技能描述，虽说没有指明具体的技术栈，但常人看到这段描述就能感受出：&lt;strong&gt;是个大佬&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;Why？因为这段描述是提炼过的内容，不再拘泥于某个技术细节，更多的是在突出自己的优势，所以这也是每位资深技术人应该要掌握的能力，即：&lt;strong&gt;学会提炼自己的专业技能，重点突出自身的优势，而并非长篇大论。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不过话说回来，干到资深水准的小伙伴，基本上也无需通过投简历的方式找工作。毕竟自身工作经验丰富，所以人脉关系并不差，换工作更多是被挖，或者走内推、猎头的途径。&lt;/p&gt;
&lt;h3&gt;二、项目经验不应该写成流水账&lt;/h3&gt;
&lt;p&gt;聊完了专业技能如何撰写后，接着再来说说项目经历，项目经历估计是大家头疼的一栏，很多时候不知道如何去描述项目，为此，这里先给出一个 &lt;strong&gt;&lt;code&gt;通用模板&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基本信息：项目名称（项目开发周期）。&lt;/li&gt;
&lt;li&gt;技术架构：项目开发中所使用的技术栈。&lt;/li&gt;
&lt;li&gt;项目背景：如果是自研项目，说明项目的背景。&lt;/li&gt;
&lt;li&gt;项目描述：大概形容一下这个项目是干嘛的。&lt;/li&gt;
&lt;li&gt;个人职责：说清自己在项目中做的事情。&lt;/li&gt;
&lt;li&gt;技术描述：罗列一些自己在项目中用到的技术亮点（可以与个人职责合二为一）。&lt;/li&gt;
&lt;li&gt;个人收获：大概讲讲做完这个项目给自己带来的成长（可选项，中级以上建议不写）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大家在描述项目经验时，都可以按照这个模板往里套，给个简单的示例，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506290855127.awebp&quot; alt=&quot;项目示例&quot;&gt;&lt;/p&gt;
&lt;p&gt;按模板去套自己的项目，你的项目经历就能较为全面地体现出来。但对于年限较长的小伙伴而言，因为工作的时间不算短，所以接手过的项目不在少数，请牢记：&lt;strong&gt;千万不要把项目经历写成流水账&lt;/strong&gt;！&lt;/p&gt;
&lt;h4&gt;项目经历怎么写才更吸引人？&lt;/h4&gt;
&lt;p&gt;不要写成流水账是啥意思呢？就是简历无需写太多的项目，我的建议是来&lt;code&gt;4&lt;/code&gt;个左右就够了，而这几个项目中，至少前两个一定要选比较有吸引力的，即技术面试官感兴趣的！但问题又来了：什么样的项目算比较有吸引力的呢？&lt;/p&gt;
&lt;p&gt;具备吸引力的项目主要有如下几类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;知名度比较高&lt;/strong&gt;，例如淘宝、京东（这里是举例，稍微有点名气也行）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户量比较大&lt;/strong&gt;，用户基数大代表流量大，对性能、技术要求会更高，知名度也不会低；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;技术难度比较大&lt;/strong&gt;，用到了较复杂或较新的技术开发的项目，如直播、金融类型项目等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;描述项目经历时，从接手过的项目中，选几个符合上述条件的即可，千万别把所有项目都写上去！因为这样干，会造成简历篇幅过长，同时还容易写成流水账，看着虽然多，但没有核心，所以写项目经历讲究：&lt;strong&gt;浓缩的才是精华&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但如果你从业时间较短，接手过的项目并不多，则可以把做过的项目都写上去。同时，如果项目都算不上很出色，你也可以用一点小手段，也就是所谓的“包装大法”。找一两个你认为比较出色的项目，写在你的简历上也行，但这样的做的前提是：&lt;strong&gt;你对“包装”的项目，必须得像自己做过的一样熟悉&lt;/strong&gt;，如果无法吃透包装的项目，自然在面试中很容易露出~~鸡~~马脚。&lt;/p&gt;
&lt;h4&gt;平凡的项目如何制造亮点？&lt;/h4&gt;
&lt;p&gt;相信诸位都有一个苦恼：“平时工作就是打螺丝，简历上的项目该如何写出不一样的感觉啊？”&lt;/p&gt;
&lt;p&gt;这是导致大家项目经历看着很平凡的罪魁祸首，很多人往往就是因为不知如何优化项目经历，所以只能按事实陈述，导致项目经历一点也不突出。&lt;/p&gt;
&lt;p&gt;下面就来教大家优化项目的方法。&lt;/p&gt;
&lt;p&gt;优化项目主要依靠两条准则，&lt;strong&gt;第一条是通过语言美化，第二条则是塑造技术亮点。&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;1. 善用语言去美化&lt;/h5&gt;
&lt;p&gt;先来说一则小故事：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个小和尚问方丈：“师父，我念经的时候可以抽烟吗？” 方丈怒道：“当然不行！”
另一个小和尚也问这个方丈：“师父，我吸烟的时候可以念经吗？” 方丈的回答是:“自然可以。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从这个故事中大家能明显感受到语言的魅力，同样的一件事情，用不同方式去表达，事情的核心并没有改变，但得到的结果却完全不同。这个道理很容易懂，但却很少有人能真正用好它。怎样才叫用好这个道理呢？例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A：我要教人搞传销诈骗！&lt;/li&gt;
&lt;li&gt;B：现在传销诈骗日益猖獗，为了防止大家上当，我将以犯罪人的视角宣传反诈骗！&lt;/li&gt;
&lt;li&gt;A：小李兼职摆摊卖炒饭，昨天赚了&lt;code&gt;15&lt;/code&gt;块，今天赚了&lt;code&gt;60&lt;/code&gt;块。&lt;/li&gt;
&lt;li&gt;B：小李兼职摆摊卖炒饭，今日相较昨日，收益环比增长&lt;code&gt;300%&lt;/code&gt;！&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面两个例子，大家是不是有种熟悉感？日常生活中有着许多类似的案例，但具体有哪些就不指出了。这里重点是在强调：&lt;strong&gt;一件相同的事情在不同的语言修饰下，产生的结果自然不同&lt;/strong&gt;！想要给自己简历上的项目做美化，首先就得利用这个原则去落实。&lt;/p&gt;
&lt;p&gt;但搞技术的人里面，大部分都缺乏这项能力，因为大家更偏向于理工科，文学功底有所欠缺，想利用这个准则用来美化项目，会存在些许难度，因此这里给出两个示例参考。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原文：两三个人负责一个项目，自己负责写代码、在开发周期内交付项目。&lt;/li&gt;
&lt;li&gt;美化：推动项目正常进度，参与需求分析与系统架构设计，负责项目核心模块的开发工作。&lt;/li&gt;
&lt;li&gt;原文：把写好的项目丢到服务器上，以后出问题了再负责改一下 Bug。&lt;/li&gt;
&lt;li&gt;美化：主导项目的上线部署工作，跟进线上实际运行状况，及时排查与解决线上故障。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些示例中，事情的本质有改变吗？其实没有，归根结底说的还是同一件事，但当换了一种表达方式后，给人的感觉完全不同。比如第一个示例中，按照正常的写法去形容，给人的感觉就是个做业务开发的螺丝仔，但换了一个说法之后，你便化身成了团队主程、核心骨干，少了你肯定不行（&lt;code&gt;2～3&lt;/code&gt;个人负责开发的项目，少了你的确不行）。&lt;/p&gt;
&lt;p&gt;OK，上述内容便是优化项目的第一个技巧，也就是把同样的事情，用听起来更加牛逼的方式表达出来。但这里要牢记：&lt;strong&gt;适当塑造确实可以提升竞争力，但塑造时也千万不要过度&lt;/strong&gt;。比如你在飞机上打了两颗螺丝，结果写成参与了整架飞机的制造，甚至对外宣称你造了架飞机，这可以吗？不行，因为实际考察时，稍微一问就露馅了，那什么叫适当地塑造呢？可以把“给飞机打了几颗螺丝”改为“参与了飞机零部件的制造”。&lt;/p&gt;
&lt;p&gt;简历的项目经历中，这个技巧可以用在项目描述、技术描述、职责描述等各方面。好比项目职责描述中，我见过的许多简历，往往是下面这样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;负责开发&lt;code&gt;XXX&lt;/code&gt;模块，实现了&lt;code&gt;AAA、BBB&lt;/code&gt;功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样写虽然可以，但会显得有点平平无奇，那如何优化呢？套下面的模板：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;负责开发&lt;code&gt;XXX&lt;/code&gt;模块，利用&lt;code&gt;XXX&lt;/code&gt;等技术，解决了&lt;code&gt;XXX&lt;/code&gt;问题，达到了&lt;code&gt;XXX&lt;/code&gt;效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有小伙伴会说，我不会套啊，能不能给个例子啊？那就来一个吧（以后端举例）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;负责&lt;code&gt;XX&lt;/code&gt;模块开发，利用&lt;code&gt;MQ、Redis&lt;/code&gt;中间件，解决了系统并发吞吐低的问题，经实测由&lt;code&gt;500QPS&lt;/code&gt;提升至&lt;code&gt;4000QPS&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样看起来是不是好多了？所以在写项目经历时，这个美化技巧一定要利用好。&lt;/p&gt;
&lt;h5&gt;2. 学会塑造技术亮点&lt;/h5&gt;
&lt;p&gt;接着再聊聊第二个优化技巧：&lt;strong&gt;制造技术亮点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为啥要制造技术亮点？因为技术面试官在看项目时，重点只关注三方面。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;项目的背景：规模大不大、名气大不大、技术难度高不高、业务复不复杂……&lt;/li&gt;
&lt;li&gt;你是什么角色：你在项目中是负责边角料开发的螺丝仔，还是核心开发，或者负责人……&lt;/li&gt;
&lt;li&gt;项目中的技术亮点：项目中有没有比较难的问题，会用到令人眼前一亮的技术或方案……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前两点在前面讲过了，现在把目光放在最后一点，这点对于大家来说，似乎是个不小的问题，我天天就是打螺丝，哪儿来的什么技术亮点啊？利用&lt;code&gt;CV&lt;/code&gt;大法日码一万行算不算亮点？&lt;/p&gt;
&lt;p&gt;由于工作的局限性，似乎项目中很难出现技术亮点是不？这点我能理解，不过大可不必担忧，既然你做项目时没有亮点，那你在平时瞎逛时，有没有遇到过令你眼前一亮的技术解决方案、疑难排查呢？如果有，就请把它写在你的简历上。&lt;/p&gt;
&lt;p&gt;这样做合适吗？当然没问题，但前提也需要你能吃透写的亮点，因为你既然写了，那么面试中&lt;code&gt;80%&lt;/code&gt;几率会被问到，如果&lt;code&gt;Hold&lt;/code&gt;不住自己写的亮点，面试结果自然可想而知！&lt;/p&gt;
&lt;p&gt;那有哪些属于技术亮点呢？以后端举例（其他岗位我不是很熟悉，大家根据自己情况来定）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分布式系统中需要传递一个全局唯一的 ID，用于串联分布式系统中请求的链路日志记录。
&lt;ul&gt;
&lt;li&gt;亮点 1：如何确保全局唯一？可以延伸分布式&lt;code&gt;ID&lt;/code&gt;生成策略，如拓展到雪花算法。&lt;/li&gt;
&lt;li&gt;亮点 2：并发情况下，如何保证不同请求的&lt;code&gt;ID&lt;/code&gt;不会冲突？可以延伸到&lt;code&gt;ThreadLocal&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;项目中某个接口调用后响应速度缓慢，每次用户访问时需要等待很久，如何做的优化？
&lt;ul&gt;
&lt;li&gt;亮点 1：线上排查手段，如何精准定位到响应缓慢的接口、造成缓慢的原因……&lt;/li&gt;
&lt;li&gt;亮点 2：性能优化手段，如何优化了响应时间？拓展到&lt;code&gt;MQ&lt;/code&gt;中间件、多线程、缓存……&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总之，所谓的技术亮点，就是写一些&lt;strong&gt;比较难、吸引人的问题&lt;/strong&gt;，这样面试官在看的时候，自然会对你解决问题的方案感兴趣，因此你的简历吸引力会更大。并且在面试过程中，也可以大概率引导面试官的提问。&lt;/p&gt;
&lt;p&gt;综上，我们可以总结出项目经历描述与优化的要点，其实重点就是做好三方面：&lt;strong&gt;选好适合的项目、美化好项目经历、突出技术亮点&lt;/strong&gt;。这三个是技术面试官会关注的点，所以针对简历的项目优化，围绕着这核心的三点展开即可。&lt;/p&gt;
&lt;p&gt;最后，额外说明一点，如果你所负责的项目是面向&lt;code&gt;C&lt;/code&gt;端的业务，可以适当贴出演示地址，这样有助于让招聘方充分了解到你的项目，尤其是针对同业务类型的招聘方，看到你的实际项目时，会对你更加满意。&lt;/p&gt;
&lt;h3&gt;三、简历上其他容易忽略的技巧&lt;/h3&gt;
&lt;h4&gt;认真对待自我评价&lt;/h4&gt;
&lt;p&gt;至此，简历上会出现的每项信息，都给出了编写时的建议及优化技巧，但前面忽视了一项内容，也就是“自我评价”这一栏，大家在写这栏时，基本上全靠在网上抄。正因如此，很多简历的自我评价都会出现下述这些信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506290856231.awebp&quot; alt=&quot;常规的自我评价&quot;&gt;&lt;/p&gt;
&lt;p&gt;我的建议是最好别这么写，而是结合个人的实际情况，诚恳地写出一段自我评价！因为有些 HR 在看简历时，会优先查看你对自己的评价，所以写的时候，可以优先考虑写上自己的优势，如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;平时热衷于技术研究与分享，&lt;code&gt;XX&lt;/code&gt;签约作者、&lt;code&gt;XX&lt;/code&gt;博客专家、&lt;code&gt;XX&lt;/code&gt;畅销书原作者、&lt;code&gt;XX&lt;/code&gt;技术核心贡献者……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;N&lt;/code&gt;年服务端、后端开发经验，&lt;code&gt;N&lt;/code&gt;年大型项目架构经验，主导过十余个大型项目研发及落地，多个项目用户规模达到千万级、全站单日并发达到百万级……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上述这段自我评价中，就能够将你自己的优势完美体现出来，但如果没有优势的话，一段发自内心的诚恳评价，也能给人留下良好的印象。不过对于技术人来说，这段评价的影响也并不大，有句话叫做“&lt;strong&gt;无过便是功&lt;/strong&gt;”，能写出一段有优势最好，如果文笔功夫实在欠缺，从网上复制一段也&lt;code&gt;OK&lt;/code&gt;。但千万不要自作聪明，在自我评价中大肆吹嘘、夸奖自己，否则只能适得其反。&lt;/p&gt;
&lt;h4&gt;不要一份简历打天下&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;JD&lt;/code&gt;（全称&lt;code&gt;Job Description&lt;/code&gt;）是指职位描述，即你在招聘网站上看到的招聘需求。&lt;/p&gt;
&lt;p&gt;在求职的过程中，当你看到一个&lt;code&gt;JD&lt;/code&gt;很满意，薪资待遇、福利、技术栈、业务等方面都特别合适，那针对这样的&lt;code&gt;JD&lt;/code&gt;就要额外珍惜，别抱着“一份简历打天下”的想法。&lt;/p&gt;
&lt;p&gt;面对自己满意的招聘，尤其是工作年限较长的伙伴，可以了解下招聘方的业务范围，适当把简历的项目调整成与招聘方业务更匹配的。同时再参考&lt;code&gt;JD&lt;/code&gt;上的技术要求，调整一下自己的专业技能顺序。&lt;/p&gt;
&lt;p&gt;为此，大家在写好一份通用简历的前提下，也要做好随时微调的准备，匹配度越高的简历，会更受招聘方的欢迎。对于招聘方而言，想要招到一个业务经验、技术能力十分匹配的人才并不容易，如果你能够通过微调达到对方满意的标准，简历上项目的业务属性、个人的技术栈，都与招聘方相匹配时，那自然成功几率会更高。&lt;/p&gt;
&lt;h4&gt;写好简历后要做的三两事&lt;/h4&gt;
&lt;p&gt;当你按照文中所说，撰写并优化好了简历之后，首先记得多复查两遍，看看整份简历是否可以再精简一些。如果可以，请再次对简历动刀，毕竟写简历是个修修补补的过程，多番打磨后才能得到更好的成品。同时，复查也能解决不细心带来的后患，例如错别字、英文单词的字母顺序反了、某些内容重复……这类问题。&lt;/p&gt;
&lt;p&gt;确保简历内容完全正确，并且足够精简时，可以把你的简历发给同为技术人的几位朋友、同学看看，如果你的朋友看了之后，觉得你的简历比他写得要好，那相对来说你的简历就很不错啦！同时，朋友看你简历的过程，相当于他人帮你复查了一次，也相当于别人给你筛选了一次，说不定他们还能给出一些额外的调整建议。&lt;/p&gt;
&lt;p&gt;最后，简历成品出来之后，一定不要忘了&lt;strong&gt;导成&lt;code&gt;PDF&lt;/code&gt;格式&lt;/strong&gt;噢！如果是&lt;code&gt;.doc、.md&lt;/code&gt;或其他格式，当你投递简历的时候很容易出现打不开，或者打开出现乱码、排版混乱的问题，而转换成&lt;code&gt;.pdf&lt;/code&gt;格式后自然就不存在这些问题。&lt;/p&gt;
&lt;h2&gt;自我练习篇：自我介绍、项目介绍该怎么说面试官才会听？&lt;/h2&gt;
&lt;p&gt;自我介绍、项目介绍，这属于技术面试必不可少的两个环节，但往往许多小伙伴，要么不会表达，刚开始介绍没多久就结束了，或者开始介绍后，就不知道该如何停下来，又或者不知怎样介绍得更好，做到扬长避短……&lt;/p&gt;
&lt;p&gt;正因上述一些情况，所以大家会遇到下面两个困扰：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;面试中的&lt;strong&gt;自我介绍&lt;/strong&gt;，怎么说才合理？&lt;/li&gt;
&lt;li&gt;面试中的&lt;strong&gt;项目介绍&lt;/strong&gt;，怎么说才更合适？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;想要做好这两点，需要经过充分准备与一定练习，整个过程需要一点时间，也需要克服一些困难，具体怎么做呢？下面我们一同来探讨探讨吧～&lt;/p&gt;
&lt;h3&gt;一、自我介绍该怎么说才合理？&lt;/h3&gt;
&lt;p&gt;自我介绍是许多面试的第一问。当你与面试官初次见面时，通常都会让你先做个简单的自我介绍。估计有些人会犯嘀咕：“你是没长眼吗？简历上都写着还叫我介绍？”&lt;/p&gt;
&lt;p&gt;简历上有求职者的大致信息，这点我们懂，其实面试官也懂，但为何还要让人做自我介绍呢？原因如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;面试官想对你有个初步的了解，毕竟你们也许是第一次见面，之前对你不熟悉；&lt;/li&gt;
&lt;li&gt;面试官想从这里判断你的语言组织能力、沟通能力怎么样，以及性格是否内向等；&lt;/li&gt;
&lt;li&gt;虽然简历上有你的大致信息，但面试官需要时间浏览，自我介绍则起到缓冲作用；&lt;/li&gt;
&lt;li&gt;看你自我介绍时的语言表述，和简历信息是否一致，以此来推断简历信息的真实性；&lt;/li&gt;
&lt;li&gt;面试官想要通过你做自我介绍，来打破四目对视的尴尬场景，起到缓解气氛的作用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于上述几点因素，所以自我介绍才成了面试中常有的环节。不过也并非所有面试都会叫你做自我介绍，尤其是技术面试官，很多时候没那么讲究，可能上来就直接聊技术。&lt;/p&gt;
&lt;p&gt;但有可能不需要做自我介绍，不代表咱们不需要准备，那自我介绍怎么说才合适呢？我们一起来分析下这个话题。&lt;/p&gt;
&lt;h4&gt;自我介绍的模板和注意点&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;问候语 + 姓名 + 年龄 + 毕业院校 + 工作年限 + 目标岗位 + 专业技能 + 上家经历 + 工作职责 + 结束语&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;将自己的信息套入这个模板中，就能够得到一个自我介绍的初稿。但是，有以下&lt;code&gt;三点&lt;/code&gt;要切记。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一点，注意扬长避短&lt;/strong&gt;。如果自己的学历是专科或非统招学历，可以适当省去毕业院校，毕竟这是你的短处。反之，如果你拥有名校背景，则可以在自我介绍中重点突出。其他方面，例如项目经历、工作履历、个人荣誉……亦是同理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二点，注意控制时长，不要太短也不要太长&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这里分享之前我做面试官的两个经历：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;经历一：我让候选人做个自我介绍，结果一分钟没到就完了，当时我连简历都没看完。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;经历二：我让候选人做自我介绍，我趁机快速浏览一下简历，结果候选人嘴不带停，从基本信息一直介绍到了项目经历，讲了十多分钟还在描述项目细节……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由上面案例可得知，自我介绍也需要一定的技巧，太短的话可能面试官连简历都没看完，太长的话则会变成面试官一直等着你，无论哪种情况都容易给人留下不好的印象。为此，一定要学会控制自我介绍的时长。&lt;/p&gt;
&lt;p&gt;下面结合前面给出的模板，给出一个自我介绍的例子：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;面试官你好，我叫竹子爱熊猫，今年&lt;code&gt;29&lt;/code&gt;岁，&lt;code&gt;17&lt;/code&gt;年于清华大学硕士毕业，至今从事&lt;code&gt;Java&lt;/code&gt;开发已有六年时间，今天是因为在&lt;code&gt;Boss&lt;/code&gt;上看到了贵公司的招聘，所以过来应聘&lt;code&gt;Java&lt;/code&gt;架构师一职。&lt;/p&gt;
&lt;p&gt;平时个人比较热爱技术，主要擅长&lt;code&gt;XXX、XXX...&lt;/code&gt;等方面，对&lt;code&gt;XXX、XXX...&lt;/code&gt;都有过深入研究（这里最好简短，不要把所有掌握的技能都讲一遍）。&lt;/p&gt;
&lt;p&gt;毕业以来曾先后就职于&lt;code&gt;公司1、公司2&lt;/code&gt;等多家企业，上份工作是在&lt;code&gt;XXX&lt;/code&gt;担任技术总监一职，在职期间内曾主导&lt;code&gt;项目1、项目2、项目3&lt;/code&gt;等多个项目的研发工作，平时主要负责&lt;code&gt;职责1、职责2....&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;以上大致就是我个人的基本介绍，如果你有其他需要了解的，我这边可以再做补充。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;又或者：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你好，我叫竹子爱熊猫，今年&lt;code&gt;22&lt;/code&gt;岁，因为我是培训出身，所以参加工作的年份比较早，&lt;code&gt;20xx&lt;/code&gt;年的时候就出来上班了，到现在已经工作了有三年时间，技术栈这块，个人比较擅长&lt;code&gt;......&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在我工作的三年以来，一直在&lt;code&gt;XXX&lt;/code&gt;公司做&lt;code&gt;Java&lt;/code&gt;开发，在职期间内，曾参与过&lt;code&gt;项目1、项目2、项目3&lt;/code&gt;等项目的设计与研发工作，平时自己主要是负责&lt;code&gt;职责1、职责2....&lt;/code&gt;（如果自己感觉较短，这里也可以补充一个最近做过的项目）。&lt;/p&gt;
&lt;p&gt;今天主要是看到了贵公司在&lt;code&gt;Boss&lt;/code&gt;上的招聘，仔细阅读招聘需求后，发现自身能力与贵公司的需求比较匹配，同时之前自己也做过&lt;code&gt;XX&lt;/code&gt;方面的业务，所以过来面试&lt;code&gt;Java&lt;/code&gt;开发一职。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在给出的两个自我介绍示例中，虽然不是很长，但却涵盖了个人基本信息、工作年限、从哪来的、来干嘛的、之前在哪些公司待过、在上家公司是做啥的、上家公司做过哪些项目等内容，整段介绍看起来简短，但已经将面试官想要知道的所有信息都做了介绍（案例仅供参考，如若有更好的方式可以结合一下，不必完全套入）。&lt;/p&gt;
&lt;p&gt;同时，第一点提到的“扬长避短”要充分发挥好！比如第一个示例中，高学历+名校背景，显然是候选人的优势，因此可以在自我介绍中重点突出；而后者因为是培训出身，所以学历这块可以适当跳过。&lt;/p&gt;
&lt;p&gt;另外，&lt;strong&gt;第三个切记点是：在自我介绍的最后，一定要记得加上结束语！&lt;/strong&gt; 这是许多小伙伴会遗漏的点，但加上它之后效果会很好：一方面可以给自我介绍收场，避免叨叨絮絮个没完，不知道如何停下介绍的尴尬场景；另一方面还能给面试官留下提问的“衔接口”，也就是最后那句“如果你有其他需要了解的，我这边可以再做补充”，面试官就可以接话提问。&lt;/p&gt;
&lt;h4&gt;自我练习的方式&lt;/h4&gt;
&lt;p&gt;将自身的情况套入给出的模板中，能够得到一个最基本的初稿，可以先调整优化一下，接着可以背两遍，熟悉稿子之后再多加练习。&lt;/p&gt;
&lt;p&gt;练习的方式主要有三种。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对镜练习：自己正对着镜子，然后盯着自己的双眼，开始模拟做自我介绍。&lt;/li&gt;
&lt;li&gt;录音练习：通过手机自带的录音软件，或者用通信软件语音功能，录制一段自我介绍。&lt;/li&gt;
&lt;li&gt;录像练习：利用手机的相机录像功能，调整好角度之后录制一段自我介绍。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看到这三种练习方法，相信许多小伙伴会有些排斥，心里可能会想着：“我都已经把稿子写好了，面试直接按稿子说就好了呀，干嘛还要这么去练？”如果你的内心也有这个想法，其实很正常，但请一定要去尝试练习！&lt;/p&gt;
&lt;p&gt;为什么一定要去练习呢？心里默念难道不可以吗？&lt;/p&gt;
&lt;p&gt;如果你是个社牛，练不练习都无关紧要，毕竟性格本身就很外向，在面试中自然可以做到侃侃而谈。但如若性格偏内向或者性格普通，就一定要多加练习！这个练习不仅仅是为了做好自我介绍，而且还可以改善面试中整体的个人表现。&lt;/p&gt;
&lt;h5&gt;对镜练习&lt;/h5&gt;
&lt;p&gt;对镜练习时，先盯着自己的双眼，或者盯着自己的眉心，然后再开始练习自我介绍，并且不是练习一遍就完事，一定要多次练习，至少也要在&lt;code&gt;10&lt;/code&gt;次以上，最好做到&lt;code&gt;30&lt;/code&gt;次以上！Why？自我介绍练习个三五遍就滚瓜烂熟了，至于练这么多遍吗？&lt;/p&gt;
&lt;p&gt;咱们不能只看表象，其实这里是在&lt;strong&gt;练习眼神&lt;/strong&gt;！我见过许多人在回答问题时，都会出现眼神飘忽的现象，比如当我一直盯着他时，最多三秒，就会避开与我的眼神接触，这显然是不自信的表现。甚至有些候选人，在面试时整个眼神摇摆不定，喜欢东瞟西看，这更会给人留下不好的印象。&lt;/p&gt;
&lt;p&gt;而对镜练习就能很好地改善眼神问题，当你习惯与自己对视时，你的眼神也会逐渐坚定，经过多次练习后，在以后真正的面试场景中，也会下意识地盯着面试官的眼睛回答问题。直视他人是一种尊重他人的表现，也是一种自信的表现，更能给人留下一个好印象。&lt;/p&gt;
&lt;p&gt;不知大家身边是否有入伍多年的“兵哥哥”，当你跟他们待在一起时，你会发现他们的气场完全不一样。当然，我们就算对镜练习两年半，也不一定能够达到军人的气质，但至少能够改善原本眼神上的一些小毛病，起码能习惯与人对视，不至于面试时怯场，从而表现得很糟糕。&lt;/p&gt;
&lt;h5&gt;录音练习&lt;/h5&gt;
&lt;p&gt;对镜练习好眼神之后，接着是录音练习，因为当你在说话时，是无法感受到自己的谈吐好坏，就算说话语气词、口头禅很多，在事后也不会有所察觉。这件事相信大家都有所体会，比如身边有些朋友或领导，说话时带很多语气词或口头禅，听的人都能感受到，但他自身却觉察不到。录音练习主要解决的就是这类问题。&lt;/p&gt;
&lt;p&gt;当准备好自我介绍的稿子后，可以把自我介绍录下来，但要记住：无论是对镜练习、录音练习还是后面的录像练习，&lt;strong&gt;就算发挥不好，也要坚持下去&lt;/strong&gt;！许多小伙伴在做一件事情时，如果没做好的第一反应就是重做，比如跟朋友发语音，说着说着出现一点卡壳时，通常会取消重新说一遍，但在练习时千万不要这么做！&lt;/p&gt;
&lt;p&gt;录制好一段自我介绍的音频后，接着你要放大声音去听。当你前几次听自己的录音时，可能会感觉有点尴尬，迫切地想关掉不听，这是十分常见的情况，请不要在意，&lt;strong&gt;认真去找录音中自己认为不足的地方，并且加以改善后再次录制，直到自己感觉没问题为止&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;不过自我介绍在录音练习时，往往无法反映出自己说话的问题，因为自我介绍经过前面的练习后，在你内心已经滚瓜烂熟了，所以录制时会更加自然一点。为了充分反映出自身的问题，可以&lt;strong&gt;预设一个主题自由发挥&lt;/strong&gt;，比如“以猫为主题讲个故事、谈谈你对&lt;code&gt;XXX&lt;/code&gt;技术的看法……”，只要不是事先准备的都行，单个录音至少要保持在&lt;code&gt;10&lt;/code&gt;分钟以上。&lt;/p&gt;
&lt;p&gt;这样做的好处在于：一方面能充分找出自己说话时的问题，另一方面还能锻炼自己的临场发挥能力，就算是在胡言乱语也没关系，当你录制好之后再去听的时候，就能明显感觉到问题所在。比如找找自己说话的过程中，是否经常出现“嗯、啊、吗、额、可能”等语气或停顿，如果有，就换个主题录音练习，但在下次练习的时候，脑海里强迫自己注意减少这些语气词和停顿。&lt;/p&gt;
&lt;p&gt;通常经过&lt;code&gt;5~8&lt;/code&gt;个主题的切换后，你说话的小毛病能得到不小改善，最主要的是你能习惯临场发挥，从而能做到在面试中侃侃而谈。这也是为什么有些人一开口时，总能如滔滔江水般连绵不绝的原因，难道他们与生俱来就有这个能力吗？并不是，都是靠锻炼出来的，大部分人最开始都是支支吾吾，经历多了之后才逐渐变得能说会道。&lt;/p&gt;
&lt;h5&gt;录像练习&lt;/h5&gt;
&lt;p&gt;对镜练习能改善眼神气质问题，录音练习能改善谈吐与增强临场发挥能力，而录像练习的作用是干嘛呢？许多小伙伴似乎感觉录像和录音的差距不大呀？为什么还要多此一举呢？实则不然，录像练习的最大好处在于&lt;strong&gt;改善小动作&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;小动作这个情况，许多人一紧张或尴尬时就会出现，比如摸鼻子、撩头发、扭脖子、舔嘴唇、两个手相互触碰、手掌从大腿搓到膝盖……总之人在紧张时，就容易出现各种肢体小动作。与口头禅、语气词的状况类似，这些小动作也是潜意识下做出的反应，当事人也很难觉察，录像练习的作用，就是解决这类问题。&lt;/p&gt;
&lt;p&gt;同样随机找个主题，然后准备录制视频练习，但私下一人练习很难产生紧张、尴尬的情绪，所以在录制视频时，你还需要通过“想象”来辅助。比如，你目前录制一个自我介绍的视频，但在自己做自我介绍的时候，脑海里面去想一些紧张的场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目前在考试你正打算作弊，目前你站在几百人的大会台上发言……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;或者想象一些尴尬的场景也行，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在人来人往的广场上跳舞，在许多人吃饭的食堂里大声唱歌……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面列出的一些场景是不是想象着就紧张？听起来就很尴尬？你在录像练习时，需要的就是这种情绪来辅助，只有当你处于这种情绪之下，才能把潜意识中的那些小动作“唤醒”，这种情况下录制的视频，才真正能反映出你不好的习惯。&lt;/p&gt;
&lt;p&gt;录制好之后，当自己去看视频的时候，同样会感觉很尴尬，此时要抱着“鸡蛋里面挑骨头”的心态去找问题，将发现的不足记录下来，然后加以改善，最后就能控制好自己在紧张/尴尬情绪下的表现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对镜练习、录音练习、录像练习&lt;/strong&gt;，针对于求职前的自我练习就讲到这里啦，我并不确定有多少人会去落实这些，或许没人去这么做，又或很多人跟着方法做，这我都无从得知。但我真心希望大家可以去尝试练练，虽然听起来这么做会有点尴尬，但当你真正尝试后，真正改善掉自身不足时，你得到的将是一个全新的自己，这些练习带来的好处不仅仅是满足面试，更能让你在以后的日子中长久受益。&lt;/p&gt;
&lt;h3&gt;二、面试时如何介绍项目？&lt;/h3&gt;
&lt;p&gt;技术人的社招面试而言，更多会围绕着项目展开话题，因此项目介绍成为了面试必然存在的环节。一场技术面试也许可能没有自我介绍，但绝不会少了项目介绍，所以除开要准备自我介绍外，准备项目介绍也必不可缺。&lt;/p&gt;
&lt;p&gt;但有些人在介绍项目时，会遇到两个问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不会说&lt;/strong&gt;：不知道如何描述自己的项目，以及自己在项目中的职责。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不会停&lt;/strong&gt;：一旦开始介绍之后，从整体描述讲到实现细节，不知道如何收场。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是我个人经验中，在候选人身上看到的两个问题，归根结底就是不会去形容项目，所以才会导致这些现象出现。那项目介绍又该遵循什么样的原则呢？如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;面试官没有指定项目的情况下，优先介绍自己最熟悉、最好的项目；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;以简历上写的作为基础，但千万不要介绍得和简历写的一模一样；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;以讲故事的方式去形容自己的项目，才能更加吸引面试官的注意力；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;针对项目中的技术亮点和个人职责，多运用&lt;code&gt;STAR&lt;/code&gt;法则去进行阐述。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述四点是项目介绍时要遵守的原则，其中第一点很容易理解就不多说了。第二点则是许多人常犯的错误，简历上怎么写的就怎么介绍，但请大家一定要杜绝这种情况出现，毕竟面试官让你说，主要是想听点不一样的，而并不是让你把简历上的内容重复一遍。&lt;/p&gt;
&lt;p&gt;前面提到的四点原则中，第一、二点比较容易理解，但第三、四点如何运用起来呢？待会儿会用&lt;code&gt;社区买菜&lt;/code&gt;来举例说明，但在此之前，会&lt;code&gt;先讲一下项目介绍的通用模板&lt;/code&gt;，&lt;code&gt;然后基于给出的示例来讲述第三、四条原则&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;项目介绍的模板与注意点&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;基本信息 + 项目背景 + 项目描述 + 技术架构 + 个人职责 + 技术亮点 + 结束语&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个模板咋套进去呢？结合&lt;code&gt;社区买菜&lt;/code&gt;案例，套进去给个示例：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;项目背景&lt;/strong&gt;：我最近做过的一个项目叫&lt;code&gt;XX&lt;/code&gt;买菜，因为随着互联网时代的进步，人们购物愈发依赖于网购平台，相较于传统的门店、商场购物，网上购物更加便捷。但当我们想要在这些平台上购买日常需要的家庭食材时，如蔬菜水果、肉禽水产之类的，由于快递的周期普遍在三天左右，所以到货时基本都不怎么新鲜。为此，既要如网购般方便、又要保证新鲜程度的需求就产生了，这也是我们做这个项目的初衷。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基本信息&lt;/strong&gt;：为了尽快将这个项目落地，当时组建了一个&lt;code&gt;XX&lt;/code&gt;人的研发团队，从需求分析到初版上线，整个过程大致耗费了六个月。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;项目描述&lt;/strong&gt;：&lt;code&gt;XX&lt;/code&gt;买菜是一个依托于社区的买菜平台，用户通过网上下单，平台通过线下专员配送的方式，结合本地仓库、专属配送链、社区提货点等机制，从而实现了当天买、隔日达的需求，由于是依托社区作为提货点，所以十分便于用户提货。并且结合冷链配送、社区保鲜保证了食材的新鲜度，相较于传统的线下市场、超市买菜，线上买菜更加便捷、可挑选的种类也更多，同时价格上更实惠，也能保障足够新鲜……&lt;/p&gt;
&lt;p&gt;结合业务需求和分布式架构思想，我们将其拆分成了&lt;code&gt;XX1、XX2....&lt;/code&gt;等多个子系统，&lt;code&gt;XX1&lt;/code&gt;子系统主要负责....、&lt;code&gt;XX2&lt;/code&gt;主要负责....（简单概述）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;技术架构&lt;/strong&gt;：考虑到系统的可用性及拓展性，核心服务都采用了弹性集群部署，同时也为了保障程序性能，项目中也引入了&lt;code&gt;XX、XX&lt;/code&gt;等中间件，而整个项目的技术架构为&lt;code&gt;......&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;个人职责&lt;/strong&gt;：我在项目中主要负责&lt;code&gt;....&lt;/code&gt;等工作（最好不要只说编码，可以拓展到推进项目、跨部门对接....）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;技术亮点&lt;/strong&gt;：在项目的开发过程中，其实我们也遇到并解决过过很多难点问题，例如并发情况下单机锁失效问题、服务故障的无感切换、基于全局&lt;code&gt;ID&lt;/code&gt;实现分布式链路追踪、线上突发故障的定位排查等等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结束语&lt;/strong&gt;：以上就是我最近一个项目的大致情况，如果您有其他想了解的，可以随时问我，我这边可以再做补充。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上述这个项目介绍的方式，首先能说明项目的基本情况，也能说明为什么要做这个项目、项目大概是干嘛的、里面用了什么技术、自己在里面做了什么事、项目中有没有遇到什么难点问题……基本也将面试官想知道的说出来了。经过大致介绍后，面试官也能根据他感兴趣的方向进行提问。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PS：这仅是一个通用的模板，如果你有更好的方式去介绍自己的项目，也不一定要套用这个模板，这里只是给出案例当作参考，如果你不会做项目介绍时，则可以套入模板提前准备。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但不管你是否要套模板，项目介绍时也要注意以下两个点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一是&lt;strong&gt;控制时长&lt;/strong&gt;。千万不要超过五分钟，太长就显得太过啰嗦，面试官听完之后抓不住你要表达的主题。&lt;/li&gt;
&lt;li&gt;二是&lt;strong&gt;加结束语&lt;/strong&gt;。我见过许多候选人都没有这个习惯，自我介绍也好，项目介绍也罢，说着说着就戛然而止了，我都不知道候选人到底是已经说完了，还是想缓口气接着说，所以加上结束语后，更便于面试官接话。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;运用讲故事手法和 STAR 法则&lt;/h4&gt;
&lt;p&gt;前面提到的&lt;code&gt;第三点原则&lt;/code&gt;，是&lt;strong&gt;多用讲故事的手法介绍项目&lt;/strong&gt;。相信大家在前面给出的示例中能感受出来，在示例的一开始，并未直接进行枯燥的项目介绍，而是以讲故事的形式，先讲述了项目的产生背景和平台描述，从而能让面试官更有兴趣听下去。为了能让大家产生对比感，下面也贴一个反例：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我最近做过的项目叫&lt;code&gt;XXX&lt;/code&gt;，里面被分成了&lt;code&gt;N&lt;/code&gt;大模块，&lt;code&gt;XX1&lt;/code&gt;模块是用来……&lt;code&gt;XX2&lt;/code&gt;模块是负责……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这样去介绍项目虽然精简，但不免有种乏味感在内，缺失了一定的铺垫，很难让面试官代入进去，尤其当你介绍的时间一长，面试官甚至到后面都不会听了，而是等着你说完就开始提问……所以，讲故事的手法主要是为了引起面试官兴趣，至少别让别人听起来就感觉很枯燥。&lt;/p&gt;
&lt;p&gt;接着聊聊起初说到的&lt;code&gt;第四点&lt;/code&gt;，&lt;strong&gt;个人职责和技术亮点多运用&lt;code&gt;STAR&lt;/code&gt;法则&lt;/strong&gt;。通常我建议个人职责和技术亮点结合在一起去说，这样有助于消除职责介绍时的枯燥感，因为很多小伙伴在讲个人职责时，更多的停留在业务的介绍，例如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我主要负责&lt;code&gt;XXX...&lt;/code&gt;模块的开发，&lt;code&gt;XX1&lt;/code&gt;模块是用来&lt;code&gt;...&lt;/code&gt;，里面主要又&lt;code&gt;N&lt;/code&gt;个功能，功能&lt;code&gt;1...&lt;/code&gt;、功能&lt;code&gt;2...&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这样去介绍，时间一长，同样会带来很重的枯燥感，所以适当地控制业务介绍，掺入一些技术亮点的穿插效果会更佳，例如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我主要负责&lt;code&gt;XX...&lt;/code&gt;模块的开发，&lt;code&gt;X1&lt;/code&gt;模块是用来&lt;code&gt;...&lt;/code&gt;，里面包含了&lt;code&gt;x1、x2...&lt;/code&gt;等核心功能，&lt;code&gt;XX2&lt;/code&gt;模块&lt;code&gt;...&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在我做订单模块的开发过程中，由于晚上十一点会有新的菜品库存补货，此时订单量会剧增造成系统并发过高，因而导致系统访问较为缓慢，为了解决这个问题，我用到了&lt;code&gt;XXX...&lt;/code&gt;等技术与&lt;code&gt;XXX&lt;/code&gt;方案，经过实测后，业务高峰期的&lt;code&gt;1.5s&lt;/code&gt;延迟降到了&lt;code&gt;140ms&lt;/code&gt;左右。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这样去介绍个人职责是不是比前面好多啦？既避免了过多业务介绍带来的枯燥，又为后续面试官的发问留下了话题，所以一定要多结合技术去做职责介绍，并且运用上&lt;code&gt;STAR&lt;/code&gt;法则，&lt;code&gt;因为什么出现了这个问题、怎么处理的问题、处理之后得到的成效&lt;/code&gt;（但只建议展开&lt;code&gt;1~2&lt;/code&gt;个亮点细说，其他的亮点可以轻略带过，太多反而是副作用）。&lt;/p&gt;
&lt;p&gt;OK，到这里就是如何做好项目介绍的全部内容啦，在大家准备项目介绍时，最好也是把它写出来，写出之后可以再次微整，整体描述要显得精简，同时也可以结合提到的原则稍加美化。当项目介绍调整好之后，就不用再做自我介绍时的那些练习了，但也记得在心中默念几次，加深印象之后才能表达得更为自然。&lt;/p&gt;
&lt;h2&gt;未完待续...&lt;/h2&gt;</content:encoded><h:img src="/_astro/20250810-eteaOJ.BsHL1kX0.png"/><enclosure url="/_astro/20250810-eteaOJ.BsHL1kX0.png"/></item><item><title>GDB: The GNU Project Debugger</title><link>https://coooredump.github.io/blog/system-architecture/debugging-with-gdb</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/debugging-with-gdb</guid><description>GDB 官方手册</description><pubDate>Sun, 13 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;官方文档：&lt;a href=&quot;https://sourceware.org/gdb/documentation/&quot;&gt;GDB: The GNU Project Debugger&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;官方提供的 GDB Document 所展示的 gdb 调试全过程示例如下链接【&lt;strong&gt;已阅&lt;/strong&gt;】：https://sourceware.org/gdb/current/onlinedocs/gdb/Sample-Session.html#Sample-Session
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gdb m4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set width 70&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;break m4_changequote&lt;/code&gt; / &lt;code&gt;b m4_changequote&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run&lt;/code&gt; / &lt;code&gt;r&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt; / &lt;code&gt;next&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; / &lt;code&gt;step&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;backtrace&lt;/code&gt; / &lt;code&gt;bt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p lquote&lt;/code&gt; / &lt;code&gt;print lquote&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list&lt;/code&gt; / &lt;code&gt;l&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p len_lquote=strlen(lquote)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c&lt;/code&gt; / &lt;code&gt;continue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ctrl-d&lt;/code&gt; / &lt;code&gt;quit&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1. &lt;code&gt;gdb&lt;/code&gt; 启动 gdb&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 直接启动 gdb
$ gdb
# 启动 gdb 的同时加载一个要调试的 [可执行文件]
# 该 test 文件在编译的过程中必须要加 -g 选项, 把调试信息加到可执行文件中: gcc -g test.c -o test
$ gdb test
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. &lt;code&gt;quit&lt;/code&gt;/&lt;code&gt;q&lt;/code&gt; 退出 gdb&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ quit
$ q
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. &lt;code&gt;file&lt;/code&gt; 命令加载程序&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ file [可执行文件]

(gdb) file test
Reading symbols from test...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. &lt;code&gt;list&lt;/code&gt;/&lt;code&gt;l&lt;/code&gt; 命令显示源代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;list` 命令可以列出可执行文件的源代码的一部分，简写为 `l
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;该命令既可不带参数：&lt;code&gt;list&lt;/code&gt; 命令将显示 10 行代码，第一次从首行开始显示，第二次从上次显示的末行的下一行开始显示，以此类推&lt;/li&gt;
&lt;li&gt;也可带 1 个参数：&lt;code&gt;list n&lt;/code&gt; 命令显示的是第 n 行的前 5 行和后 4 行代码&lt;/li&gt;
&lt;li&gt;或者带 2 个参数：&lt;code&gt;list n1, n2&lt;/code&gt; 命令显示的是 n1—n2 行之间的源代码内容&lt;/li&gt;
&lt;li&gt;还可以显示某函数附近的源代码内容：&lt;code&gt;list funcname&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) list
1       #include &amp;#x3C;iostream&gt;
2       #include &amp;#x3C;fstream&gt;
3       #include &amp;#x3C;vector&gt;
4
5       using namespace std;
6
7       int main()
8       {
9         ofstream outfile;
10        outfile.open(&quot;./results/MD_trace_results.txt&quot;, ios::out | ios::app);
(gdb) l
11        if (!outfile.is_open())
12        {
13          cout &amp;#x3C;&amp;#x3C; &quot;failed&quot; &amp;#x3C;&amp;#x3C; endl;
14          exit(1);
15        }
16
17        cout &amp;#x3C;&amp;#x3C; &quot;succeeded&quot; &amp;#x3C;&amp;#x3C; endl;
18
19        return 1;
20      }
(gdb) l
Line number 21 out of range; open_file.cpp has 20 lines.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. &lt;code&gt;run&lt;/code&gt;/&lt;code&gt;r&lt;/code&gt; 命令运行程序&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;run&lt;/code&gt; / &lt;code&gt;r&lt;/code&gt; 可以在 gdb 中运行调试中的程序，&lt;strong&gt;该命令可以跟一个或多个参数，作为运行程序的命令行参数&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) run 1 2 3
`/home/wyk/straid/code/open_file&apos; has changed; re-reading symbols.
Starting program: /home/wyk/straid/code/open_file 1 2 3
argc = 4
argv[0]: /home/wyk/straid/code/open_file
argv[1]: 1
argv[2]: 2
argv[3]: 3
succeeded
[Inferior 1 (process 145429) exited with code 01]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;show args&lt;/code&gt; 命令显示传给该程序的参数列表：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) show args
Argument list to give program being debugged when it is started is &quot;1 2 3&quot;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果重新运行 &lt;code&gt;run&lt;/code&gt; 则会将上次的命令行重新参数传给该程序。&lt;/p&gt;
&lt;p&gt;如果要改变传递给程序的参数，可使用 &lt;code&gt;set args&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) set args 4 5 6
(gdb) run
Starting program: /home/wyk/straid/code/open_file 4 5 6
argc = 4
argv[0]: /home/wyk/straid/code/open_file
argv[1]: 4
argv[2]: 5
argv[3]: 6
succeeded
[Inferior 1 (process 145554) exited with code 01]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. &lt;code&gt;break&lt;/code&gt;/&lt;code&gt;b&lt;/code&gt; 命令设置断点&lt;/h2&gt;
&lt;p&gt;程序执行到断点时将被挂起，有以下几种方式设置断点：&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;break` 命令也有简写形式 `b
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;(1) 根据行号设置断点 &lt;code&gt;break &amp;#x3C;linenum&gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) break 10
Breakpoint 1 at 0x5555555552d5: file open_file.cpp, line 10.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置好断点后启动程序，会停在断点位置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) run
Starting program: /home/wyk/straid/code/open_file 4 5 6

Breakpoint 1, main (argc=4, argv=0x7fffffffe178) at open_file.cpp:10
10        cout &amp;#x3C;&amp;#x3C; &quot;argc = &quot; &amp;#x3C;&amp;#x3C; argc &amp;#x3C;&amp;#x3C; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(2) 根据函数名设置断点 &lt;code&gt;break &amp;#x3C;funcname&gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) break main
Breakpoint 2 at 0x5555555552a9: file open_file.cpp, line 8.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(3) 执行&lt;strong&gt;非当前源文件&lt;/strong&gt;的某行或某函数时停止执行（为非当前源文件设置断点）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) break filename:linenum
# or
(gdb) break filename:funcname
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(4) 根据条件停止执行程序&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) break linenum if expr
# or
(gdb) break funcname if expr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⭐清除断点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clear &amp;#x3C;source-line&gt;&lt;/code&gt;：清除&lt;strong&gt;源文件&lt;/strong&gt;某一行的所有断点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete &amp;#x3C;breakpoint-id&gt;&lt;/code&gt;：删除 &lt;code&gt;info b&lt;/code&gt; 中对应 ID 的断点&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00005555555553ad in main(int, char**) at open_file.cpp:17
2       breakpoint     keep y   0x00005555555553e3 in main(int, char**) at open_file.cpp:19
3       breakpoint     keep y   0x00005555555552d5 in main(int, char**) at open_file.cpp:10
(gdb) clear 17  # 清除源文件 line 17 位置的断点
(gdb) info b
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x00005555555553e3 in main(int, char**) at open_file.cpp:19
3       breakpoint     keep y   0x00005555555552d5 in main(int, char**) at open_file.cpp:10
(gdb) delete 2  # 清除 Num=2 的断点
(gdb) info b
Num     Type           Disp Enb Address            What
3       breakpoint     keep y   0x00005555555552d5 in main(int, char**) at open_file.cpp:10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. 在不退出/中断 gdb 的情况下使用 shell 命令：&lt;code&gt;!&amp;#x3C;command&gt;&lt;/code&gt; 或 &lt;code&gt;shell &amp;#x3C;command&gt;&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# don&apos;t work
(gdb) echo $PATH
# works
(gdb) shell echo $PATH
/home/wyk/.vscode-server/bin/8fa188b2b301d36553cbc9ce1b0a146ccb93351f/bin/remote-cli:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
(gdb) !ls -al $PWD
total 108
drwxrwx--- 8 wyk  wyk   4096 Dec 17 17:36 .
drwxrwxr-x 6 wyk  wyk   4096 Dec  7 08:57 ..
drwxr-xr-x 2 root root  4096 Dec  7 12:43 bin
drwxrwxr-x 7 wyk  wyk   4096 Dec  7 12:43 include
-rwxrwxrwx 1 wyk  wyk    278 Nov 22 03:04 install_depends.sh
-rwxrwxr-x 1 wyk  wyk   3589 Dec  7 09:12 Makefile
drwxr-xr-x 2 root root  4096 Dec  7 12:43 obj
-rwxrwxr-x 1 wyk  wyk  41016 Dec 17 17:36 open_file
-rw-rw-r-- 1 wyk  wyk    535 Dec 17 17:36 open_file.cpp
-rw-rw-r-- 1 wyk  wyk   5964 Nov 22 03:04 README.md
drwxrwxr-x 2 wyk  wyk   4096 Dec  7 10:38 results
-rwxrwxrwx 1 wyk  wyk    880 Dec 12 15:08 run_bench.sh
-rwxrwxrwx 1 wyk  wyk    486 Dec  7 10:37 run_tracemd.sh
-rwxrwxrwx 1 wyk  wyk    397 Dec  7 09:27 run_tracest.sh
drwxrwxr-x 2 wyk  wyk   4096 Nov 22 03:04 src
drwxrwxr-x 2 wyk  wyk   4096 Nov 22 03:04 Traces
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为在 GDB 中不常使用 &lt;code&gt;shell&lt;/code&gt; 命令，所以需要 &lt;code&gt;shell&lt;/code&gt; 和 &lt;code&gt;!&lt;/code&gt; 的限制，而经常在开发环境中使用 &lt;code&gt;make&lt;/code&gt; 命令，所以无需使用以上符号即可调用 &lt;code&gt;make&lt;/code&gt; 命令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# make 命令可直接调用
(gdb) make -j4

# sudo 命令：还是得使用 ! 或者 shell
(gdb) !sudo make -j10
[sudo] password for wyk: 
g++ -I. -I./include -I./include/Bitmap -I./include/concurrentqueue -I./src -Wp,-MT,obj/define.o -Wp,-MMD,obj/define.o.d -g -std=c++2a -Wall -Wno-unused-variable -Wno-unused-but-set-variable -Wno-sign-compare -Wno-comment -O3  -c -o obj/define.o ./include/define.cc
g++ -I. -I./include -I./include/Bitmap -I./include/concurrentqueue -I./src -Wp,-MT,obj/ecEncoder.o -Wp,-MMD,obj/ecEncoder.o.d -g -std=c++2a -Wall -Wno-unused-variable -Wno-unused-but-set-variable -Wno-sign-compare -Wno-comment -O3  -c -o obj/ecEncoder.o ./include/ecEncoder.cc
g++ -I. -I./include -I./include/Bitmap -I./include/concurrentqueue -I./src -Wp,-MT,obj/metadata.o -Wp,-MMD,obj/metadata.o.d -g -std=c++2a -Wall -Wno-unused-variable -Wno-unused-but-set-variable -Wno-sign-compare -Wno-comment -O3  -c -o obj/metadata.o ./include/metadata.cc
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@ 管道命令 &lt;code&gt;|&lt;/code&gt;, 可用 &lt;code&gt;pipe&lt;/code&gt; 将 gdb 中的命令与 shell 命令结合使用 [ &lt;code&gt;|&lt;/code&gt; 原本是用于 shell 与 shell 之间的管道命令 ]&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 不生效
(gdb) show args | wc -l
Argument list to give program being debugged when it is started is &quot;1 2 3&quot;.
# 使用 pipe 即可打通 gdb 与 shell 之间的传输
(gdb) pipe show args | wc -l
1
(gdb) pipe p argv | wc -l
1
# | 原本用于 shell 与 shell 之间的命令传输
(gdb) !ls -al | wc -l
17
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. &lt;code&gt;s&lt;/code&gt; 命令 == step「单步进入」&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) help s
Step program until it reaches a different source line.
Usage: step [N]
Argument N means step N times (or till program stops for another reason).
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;9. &lt;code&gt;finish&lt;/code&gt; 命令「单步跳出」&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) help finish
Execute until selected stack frame returns.
Usage: finish
Upon return, the value returned is printed and put in the value history.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10. &lt;code&gt;n&lt;/code&gt; 命令 == next「单步跳过」&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) help n
Step program, proceeding through subroutine calls.
Usage: next [N]
Unlike &quot;step&quot;, if the current source line calls a subroutine,
this command does not enter the subroutine, but instead steps over
the call, in effect treating it as a single source line.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;11. &lt;code&gt;c&lt;/code&gt; 命令 == continue「跳到下一个断点」&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) help c
Continue program being debugged, after signal or breakpoint.
Usage: continue [N]
If proceeding from breakpoint, a number N may be used as an argument,
which means to set the ignore count of that breakpoint to N - 1 (so that
the breakpoint won&apos;t break until the Nth time it is reached).

If non-stop mode is enabled, continue only the current thread,
otherwise all the threads in the program are continued.  To 
continue all stopped threads in non-stop mode, use the -a option.
Specifying -a and an ignore count simultaneously is an error.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;12. &lt;code&gt;return n&lt;/code&gt; 命令直接跳过当前函数后面的语句并直接返回 n，该 n 值是自定义的返回值&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) return 6
Make main(int, char**) return now? (y or n) y
#0  __libc_start_main (main=0x5555555552a9 &amp;#x3C;main(int, char**)&gt;, argc=4, argv=0x7fffffffe178, 
     init=&amp;#x3C;optimized out&gt;, fini=&amp;#x3C;optimized out&gt;, rtld_fini=&amp;#x3C;optimized out&gt;, stack_end=0x7fffffffe168)
     at ../csu/libc-start.c:342
342     ../csu/libc-start.c: No such file or directory.
(gdb) n
[Inferior 1 (process 148790) exited with code 06]
(gdb) n
The program is not being run.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;13. &lt;code&gt;print var&lt;/code&gt;/&lt;code&gt;p var&lt;/code&gt; 命令查看 var 值&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(gdb) print argv
$1 = (char **) 0x7fffffffe178
(gdb) print argc
$2 = 4
(gdb) print *argv[0]
$3 = 47 &apos;/&apos;
(gdb) print argv[0]
$4 = 0x7fffffffe42e &quot;/home/wyk/straid/code/open_file&quot;
(gdb) print argv[1]
$5 = 0x7fffffffe44e &quot;1&quot;
(gdb) print argv[2]
$6 = 0x7fffffffe450 &quot;2&quot;
(gdb) print argv[3]
$7 = 0x7fffffffe452 &quot;3&quot;
(gdb) print argv[4]
$8 = 0x0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;14. &lt;code&gt;backtrace&lt;/code&gt; / &lt;code&gt;bt&lt;/code&gt; 与 &lt;code&gt;frame&lt;/code&gt;、&lt;code&gt;up&lt;/code&gt;、&lt;code&gt;down&lt;/code&gt;、&lt;code&gt;info&lt;/code&gt; 搭配食用：查看函数调用栈的最佳命令｜快速定位 bug 位置&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;立瀚教学 get 到 backtrace，转载链接：&lt;a href=&quot;https://doc.embedfire.com/linux/imx6/base/zh/latest/linux_debug/backtrace.html&quot;&gt;gdb调试之函数调用栈——backtrace&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;更基础的 gdb 内容：&lt;a href=&quot;https://doc.embedfire.com/linux/imx6/base/zh/latest/linux_debug/gdb_use.html&quot;&gt;GDB调试利器&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bt&lt;/code&gt; ：bt是 &lt;code&gt;backtrace&lt;/code&gt; 指令的缩写，显示所有的函数调用栈的信息，栈中的每个函数都被分配了一个编号，最近被调用的函数在 0 号帧中（栈顶），并且每个帧占用一行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bt n&lt;/code&gt; ：显示函数调用栈从栈顶算起的 n 帧信息（n 表示一个正整数）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bt -n&lt;/code&gt; ：显示函数调用栈从栈底算起的 n 帧信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bt full&lt;/code&gt; ：显示栈中所有信息如：函数参数，本地变量等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bt full n&lt;/code&gt; ：显示函数调用栈从栈顶算起的 n 帧的所有信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bt full -n&lt;/code&gt; ：显示函数调用栈从栈底算起的 n 帧的所有信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面的 &lt;code&gt;bt&lt;/code&gt; 指令主要是查看栈的信息，而每一帧都会有详细的信息，这些函数调用信息帧包括：调用函数的地方，函数的参数等。如果想查看栈中某一帧的信息，首先要做的是切换当前栈。这时候需用用到 &lt;code&gt;frame&lt;/code&gt; 指令（缩写形式为 &lt;code&gt;f&lt;/code&gt;）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;f n&lt;/code&gt; / &lt;code&gt;frame n&lt;/code&gt;: 它的功能是切换到编号为 n 的栈帧（n 表示一个正整数），并显示相关信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了使用 frame 指令切换栈帧外，还可以使用 up 和 down 指令。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;down n&lt;/code&gt; ：表示往栈顶方向下移 n 层（n 表示一个正整数，默认值为 1）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;up n&lt;/code&gt; ：表示往栈底方向上移 n 层。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;info 指令是一个很强大的指令，使用它可以查看各种变量的值，如果我们希望看到详细的函数调用信息帧的信息，如：函数地址、调用函数的地址、被调用函数的地址、当前函数由哪种编程语言编写、函数参数地址及形参值、局部变量的地址、当前桢中存储的寄存器等，可以使用以下指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;info frame&lt;/code&gt; ： 指令的缩写形式为 &lt;code&gt;i f&lt;/code&gt; ，查看函数调用帧的所有信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;info args&lt;/code&gt; ：查看函数变量的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;info locals&lt;/code&gt; ：查看本地变量的信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;15. 了解一下 GDB 的语法规则|注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用 gdb 调试的前提是在编译命令中添加 &lt;code&gt;-g&lt;/code&gt; 参数，因为有些编译器是无法同时处理 &lt;code&gt;-g&lt;/code&gt; 和 &lt;code&gt;-O&lt;/code&gt; 选项，所以无法调试带有调试信息 (-g) 的优化 (-O) 可执行文件！&lt;/li&gt;
&lt;li&gt;gdb 是单行输入，由 &lt;code&gt;&amp;#x3C;命令&gt;&lt;/code&gt; 跟着 &lt;code&gt;&amp;#x3C;参数&gt;&lt;/code&gt;，取决于命令，比如 &lt;code&gt;step 5&lt;/code&gt; 表示连续执行 5 次 &lt;code&gt;step&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对于缩写无歧义的 gdb 命令，通常可以截断使用；如果有些以相同字母开头可能造成歧义的命令，可以使用 &lt;code&gt;help&lt;/code&gt; 命令来判别该缩写命令是属于哪一条具体的命令，比如 &lt;code&gt;help s&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;直接按下「回车」会重复上一步命令，但是对于某些可能带来麻烦的命令不会生效，比如 &lt;code&gt;run&lt;/code&gt;；对于 &lt;code&gt;list&lt;/code&gt; 和 &lt;code&gt;x&lt;/code&gt; 命令，按下回车会构造新的参数来重复命令，这样方便扫描资源和内存（连续按下 &lt;code&gt;list&lt;/code&gt; 会往下不断展示 10 行代码） ，&lt;code&gt;ctrl+o&lt;/code&gt; 同 &lt;code&gt;Enter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#&lt;/code&gt; 表示注释，同 shell 脚本&lt;/li&gt;
&lt;li&gt;gdb 使用 &lt;code&gt;Tab&lt;/code&gt; 按钮也可「补全命令」&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 盘点一些 gdb 系统命令 [Useful]
(gdb) help
(gdb) help &amp;#x3C;command&gt;
(gdb) complete &amp;#x3C;alphabet&gt;   # 列出以 alphabet 开头的所有命令, 比如 complete sh: sharedlibrary shell show
(gdb) show
(gdb) info
(gdb) set
# Here are several miscellaneous show subcommands, all of which are exceptional in lacking corresponding set commands:
(gdb) show version
(gdb) show copying
(gdb) info copying
(gdb) show warranty
(gdb) info warranty
(gdb) show configuration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⭐更多内容（未完待续）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inferior 可以同时调试多个程序&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;info inferiors&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inferior &amp;#x3C;infno&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add-inferior&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clone-inferior&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remove-inferiors &amp;#x3C;infno&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kill inferiors &amp;#x3C;infno&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;threads 可以调试多线程&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;info threads&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkpoint 可以保留快照，搭配 restart &amp;#x3C;checkpoint-id&gt; 回到快照点&lt;/code&gt;，当你接近错误点时，可以保留快照，如果因为走得太远导致错过关键语句，无需重新启动程序，直接跳回上一个快照点 checkpoint 即可
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;checkpoint&lt;/code&gt;: 在此处留下快照点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;info checkpoints&lt;/code&gt;: 查看所有快照点信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;restart &amp;#x3C;checkpoint-id&gt;&lt;/code&gt;: 回到指定快照点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete checkpoint &amp;#x3C;checkpoint-id&gt;&lt;/code&gt;: 删除指定快照点&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;watchpoint: 当观察的表达式变化时，立刻停下&lt;/li&gt;
&lt;li&gt;catchpoint: 当某个事件触发时，立刻停下&lt;/li&gt;
&lt;li&gt;breakpoint: 断点——毋庸置疑
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;b &amp;#x3C;linenum&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b &amp;#x3C;funcname&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;info breakpoints&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;save breakpoints &amp;#x3C;filename&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;disable breakpoints&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enable breakpoints&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clear &amp;#x3C;source-line&gt;&lt;/code&gt;：清除&lt;strong&gt;源文件&lt;/strong&gt;某一行的所有断点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete &amp;#x3C;breakpoint-id&gt;&lt;/code&gt;：删除 &lt;code&gt;info b&lt;/code&gt; 中对应 ID 的断点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;step
&lt;ul&gt;
&lt;li&gt;step 是单步程序源代码&lt;/li&gt;
&lt;li&gt;stepi 是单步机器指令&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;next
&lt;ul&gt;
&lt;li&gt;next 是单步程序源代码&lt;/li&gt;
&lt;li&gt;nexti 是单步机器指令&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;finish&lt;/li&gt;
&lt;li&gt;until&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/202504140009551.D8ZdltPL.jpeg"/><enclosure url="/_astro/202504140009551.D8ZdltPL.jpeg"/></item><item><title>分布式系统｜共识算法 Paxos</title><link>https://coooredump.github.io/blog/system-architecture/distributed-systems-paxos</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/distributed-systems-paxos</guid><description>Paxos 算法是由 Leslie Lamport 在 1990 年代提出的一种基于消息传递共识算法。在讨论分布式算法时，Paxos 几乎是一个绕不开的话题。在过去的几十年中，它已经成为分布式共识的象征，许多流行的共识算法都是基于 Paxos 进行改进的，比如 Fast Paxos、Raft、ZAB 等协议。虽然 Paxos 算法可以认为是一些共识算法的基础，但是其本身也相对较复杂，理解起来有一定的难度。</description><pubDate>Sun, 13 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;分布式系统 · 协调与协定（XMU Curriculum）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本章为「2024 春季课程」分布式系统的部分重要内容，仅作归纳，便于理清框架。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ L6-时间同步与全局状态&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时钟同步&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;物理时钟同步问题（机器本地时间）
&lt;ul&gt;
&lt;li&gt;NTP 网络时间协议：使用中心化的全局时间同步来保证各节点的时间统一&lt;/li&gt;
&lt;li&gt;Berkeley 算法：服务器主动定期询问每台机器的时间，服务器基于客户的回答计算出平均值，告知它们拨快或者拨慢时间 —— 主动式服务（与 Cristian 算法中的被动式时间服务器相反）&lt;/li&gt;
&lt;li&gt;Cristian 算法：一台机器设为时间服务器，其他的每台机器周期性地向时间服务器发送请求消息以获得当前标准时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;逻辑时钟（时钟的内部一致性）
&lt;ul&gt;
&lt;li&gt;Lamport 算法：为了同步逻辑时钟，Lamport 定义了一个二元 关系，称作“先发生”的关系...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局状态&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;全局一致性快照&lt;/li&gt;
&lt;li&gt;Chandy-Lamport 算法：引入「分布式快照」概念
&lt;ul&gt;
&lt;li&gt;Initiating a snapshot&lt;/li&gt;
&lt;li&gt;Propagating a snapshot&lt;/li&gt;
&lt;li&gt;Terminating a snapshot&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ L7-协调与协定&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分布式互斥&lt;/strong&gt; Ricart-Agrawala 算法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;选举算法&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bully&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;拜占庭与共识问题&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拜占庭&lt;/li&gt;
&lt;li&gt;共识算法
&lt;ul&gt;
&lt;li&gt;Paxos&lt;/li&gt;
&lt;li&gt;Raft&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ L8-并发控制与分布式事务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原子事务&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;ACID 特性&lt;/li&gt;
&lt;li&gt;事务实现
&lt;ul&gt;
&lt;li&gt;私有工作空间与影子更新&lt;/li&gt;
&lt;li&gt;写前日志&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;分为单层事务、嵌套事务、分布式事务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发控制&lt;/strong&gt;：当多个事务在不同的进程（在不同的处理机上）中同时执行时，需要一些机制以保证它们互不干扰，这种机制称为并发控制算法
&lt;ul&gt;
&lt;li&gt;两阶段封锁协议（2PL）：恰好在需要或不再需要锁时去请求或释放锁；可能会死锁&lt;/li&gt;
&lt;li&gt;乐观的并发控制：做自己想做的，有问题出现再说（避免了死锁，允许最大的并行度；有时可能会失效，这时所有的事务都必须退回重新运行一遍）&lt;/li&gt;
&lt;li&gt;时间戳：每个文件带有对它操作的最后一个提交事务的读时间戳、写时间戳&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分布式事务&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;CAP 理论：一致性、可用性、分区容错性&lt;/li&gt;
&lt;li&gt;BASE 理论：基本可用、软状态、最终一致性&lt;/li&gt;
&lt;li&gt;分布式事务解决方案
&lt;ul&gt;
&lt;li&gt;CP 方式：强一致性，弱可用&lt;/li&gt;
&lt;li&gt;AP 方式：高可用，但弱一致&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;基于这两种思想，延伸出了很多分布式事务解决方案（2PC、3PC、TCC 等）
&lt;ul&gt;
&lt;li&gt;两阶段提交协议（2PC）
&lt;ul&gt;
&lt;li&gt;准备阶段 &lt;code&gt;Prepare&lt;/code&gt;：取得一致决定&lt;/li&gt;
&lt;li&gt;执行阶段 &lt;code&gt;Commit/Rollback&lt;/code&gt;：执行命令（提交或废弃）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;三阶段提交协议（3PC）：在协调者和参与者中都引入超时机制，并且把两阶段提交协议的第一个阶段分成了两步
&lt;ul&gt;
&lt;li&gt;CanCommit：与 2PC 的 Prepare 阶段类似&lt;/li&gt;
&lt;li&gt;PreCommit：协调者将通知事务参与者准备提交或取消事务，写本地的 redo 和 undo 日志，但不提交&lt;/li&gt;
&lt;li&gt;DoCommit：提交或回滚，如果无法及时收到来自协调者的信息之后，他会默认执行提交，不会一直锁定资源处于阻塞状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TCC：解决 2PC 中的资源锁定和阻塞问题，减少资源锁定时间
&lt;ul&gt;
&lt;li&gt;Try：资源的检测和预留&lt;/li&gt;
&lt;li&gt;Confirm：执行的业务操作提交，要求 Try 成功，Confirm 一定要成功&lt;/li&gt;
&lt;li&gt;Cancel：预留资源释放&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 分布式互斥&lt;/h3&gt;
&lt;h4&gt;1.1 集中式算法（仿照单机）&lt;/h4&gt;
&lt;p&gt;选一个进程作为协调者，进程若要进入临界区，它向协调者发送请求消息，协调者负责处理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点: 易实现、通信量少 (请求-许可-释放)&lt;/li&gt;
&lt;li&gt;缺点: 单点故障、瓶颈、无法辨认服务器崩溃&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.2 分布式互斥算法（Ricart-Agrawala 算法）&lt;/h4&gt;
&lt;p&gt;Lamport 提出了一种分布式互斥算法，Ricart 等对它作了进一步的改进。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;步骤 1：当一个进程想进入一个临界区时 ，它构造一个消息，其中包含它要进入的 &lt;code&gt;&amp;#x3C;临界区的名字、它的进程号、当前时间&gt;&lt;/code&gt;，然后它将该消息发送给所有其他的进程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;步骤 2：当一个进程接收到来自另一个进程的请求消息时，它根据自己与消息中的临界区相关的状态来决定它要采取的动作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若接收者不在临界区也不想进入临界区，它就向发送者发送一个 &lt;code&gt;ok&lt;/code&gt; 消息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若接收者已经在临界区中，它不进行应答，而是将该请求放入队列中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果接收者想进入临界区但尚未进入时，它将对收到的消息的时间戳与包含在它发送给其余进程的消息中的时间戳（本身的时间戳）进行比较，时间戳最早的那个进程获胜。如果收到的消息的时间戳比较早，那么接收者向发送者发回一个 &lt;code&gt;ok&lt;/code&gt; 消息。如 果它本身的时间戳比较早，那么接收者将收到的请求放入队列中，并且不发送任何消息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;步骤 3：在发送了请求进入临界区的请求消息后，进程进行等待，直到其他所有进程都发回了允许进入消息为止，一旦请求进程得到了所有进程的允许，它就可以进入临界区了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;步骤 4：当它退出临界区时，它向其队列中的所有进程发送 &lt;code&gt;ok&lt;/code&gt; 消息，并将它们从队列中删除。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;n 点失败&lt;/li&gt;
&lt;li&gt;n 点瓶颈&lt;/li&gt;
&lt;li&gt;2(n-1) 个消息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;改进方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超时重发&lt;/li&gt;
&lt;li&gt;组通信&lt;/li&gt;
&lt;li&gt;简单多数同意&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.3 令牌环算法&lt;/h4&gt;
&lt;p&gt;进程没有固定顺序，可以用软件的方法构造出一个逻辑环，环中为每个进程都分配了一个位置（如按网络地址），不管按什么方式分配，每个进程要知道谁在它的下一个位置上。进程从它邻近的进程得到令牌后，检查自己是否要进入临界区。如果自己要进入临界区，那么它就进入临界区，做它要做的工作，然后离开临界区。在该进程退出临界区后，它沿着环继续传递令牌。&lt;/p&gt;
&lt;h3&gt;2. 选举算法&lt;/h3&gt;
&lt;p&gt;许多分布式算法需要一个进程充当协调者，发起者，排序者或其他特定的角色。&lt;/p&gt;
&lt;h4&gt;2.1 欺负算法（Bully）&lt;/h4&gt;
&lt;p&gt;当一个进程 P 发现协调者不再响应请求时，它发起选举。进程 P 选举过程如下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;P 向所有号码比它大的进程发送选举 (Election) 消息&lt;/li&gt;
&lt;li&gt;若无人响应，P 获胜成为协调者&lt;/li&gt;
&lt;li&gt;若有号码比它大的进程响应，响应者接管，P 的工作完成&lt;/li&gt;
&lt;li&gt;由于总是号码最大的进程取胜，因而将该算法命名为欺负算法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h3&gt;3. 拜占庭问题与共识算法&lt;/h3&gt;
&lt;h4&gt;3.1 拜占庭将军&lt;/h4&gt;
&lt;p&gt;Lamport 在 1982 年发表的论文《&lt;a href=&quot;https://lamport.azurewebsites.net/pubs/byz.pdf&quot;&gt;The Byzantine Generals Problem&lt;/a&gt;》描述了拜占庭将军投票问题，借以映射分布式系统中计算机通信容错问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Wikipedia&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一组拜占庭将军分别各率领一支军队共同围困一座城市。为了简化问题，将各支军队的行动策略限定为进攻或撤离两种。因为部分军队进攻部分军队撤离可能会造成灾难性后果，因此各位将军必须通过投票来达成一致策略，即所有军队一起进攻或所有军队一起撤离。因为各位将军分处城市不同方向，他们只能通过信使互相联系。在投票过程中每位将军都将自己投票给进攻还是撤退的信息通过信使分别通知其他所有将军，这样一来每位将军根据自己的投票和其他所有将军送来的信息就可以知道共同的投票结果而决定行动策略。&lt;/p&gt;
&lt;p&gt;投票系统的问题在于，军队中可能存在叛徒和敌军的间谍，左右将军们的决定又扰乱整体军队的秩序。这时候在已知有成员可能不可靠的情况下，其他忠诚的将军如何在不受叛徒和间谍影响的情况下意见达成一致，这就是&lt;strong&gt;拜占庭将军问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;拜占庭将军问题提供了对「分布式共识」问题的一种情景化描述：&lt;/p&gt;
&lt;p&gt; 拜占庭将军：即分布式系统的服务节点&lt;/p&gt;
&lt;p&gt; 忠诚的将军：即分布式系统正常的服务节点&lt;/p&gt;
&lt;p&gt; 叛变的将军：出现故障并发送误导信息的服务节点&lt;/p&gt;
&lt;p&gt; 信使被杀：通信故障导致信息丢失&lt;/p&gt;
&lt;p&gt; 信使被间谍替换：服务节点进行网络通信过程中信息被黑客攻击，通信存在劫持以及信息伪造&lt;/p&gt;
&lt;p&gt;1998 年，Lamport 发表了名为《&lt;a href=&quot;https://lamport.azurewebsites.net/pubs/lamport-paxos.pdf&quot;&gt;The Part-Time Parliament&lt;/a&gt;》 的论文， 这是 Paxos 算法第一次公开发发布，为网络异常情况下分布式系统如何保证数据一致性提供了一个解决思路。注意 Paxos 算法是有特定前提的，即先不考虑拜占庭将军问题（消息篡改）的情况。&lt;/p&gt;
&lt;p&gt;《The Part-Time Parliament》中使用了大量的数学证明，考虑到大多数人理解起来比较困难，Lamport 于 2001 年发表了另一篇论文《&lt;a href=&quot;https://lamport.azurewebsites.net/pubs/paxos-simple.pdf&quot;&gt;Paxos Made Simple&lt;/a&gt;》，使用了逻辑推导来论述 Paxos 算法。&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;共识理论&lt;/h2&gt;
&lt;h3&gt;1. 什么是共识&lt;/h3&gt;
&lt;p&gt;「共识」是指在分布式系统中，多个节点对某个数据值或决策达成一致意见。&lt;/p&gt;
&lt;p&gt;用数学术语来描述一下分布式系统的共识问题：在一个包含 n 个实例的分布式系统中，集合 &lt;code&gt;{0,1,2,…,n−1}&lt;/code&gt; 表示这些实例。每个实例 &lt;code&gt;i&lt;/code&gt; 拥有一个初始值 &lt;code&gt;vi&lt;/code&gt;。这些实例之间可以相互通信。系通过某种算法，使得即使部分实例出现故障，&lt;strong&gt;系统中的所有非故障实例仍能达成一致&lt;/strong&gt;，选择出一个不可更改的最终决定值 &lt;code&gt;vf&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;共识算法三个性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;终止性（Termination）：所有非故障的实例最终都会确定某一个值并终止算法执行。换句话说，算法不会无限期地执行下去，实例们都会在有限的时间内达成决议。&lt;/li&gt;
&lt;li&gt;一致性（Consistency）：所有非故障的实例最终选择的值 &lt;code&gt;vf&lt;/code&gt; 必须是相同的。&lt;/li&gt;
&lt;li&gt;完整性（Integrity）：如果所有节点提议相同的值，那么该值一定会被最终选定为共识结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些性质确保了在分布式系统中，即使面临部分节点故障或网络延迟等问题，系统仍然能够达成一个一致的决策，避免数据不一致的问题。&lt;/p&gt;
&lt;h3&gt;2. 一致性和共识&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;一致性&lt;/strong&gt;：指的是在分布式系统中，多个节点在执行一系列操作后，通过遵循某些协议，确保它们对外部呈现的数据和状态保持一致。简单来说，就是确保所有节点上的数据是完全同步的，并且在某个提案（Proposal）上达成共识。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;共识&lt;/strong&gt;：指的是多个节点在分布式系统中就某个状态或决定达成一致的过程。&lt;/p&gt;
&lt;p&gt;✅ 换句话说，&lt;strong&gt;一致性强调的是最终的状态是否一致，而共识则是实现这种一致性的手段&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;3. 为什么要达成共识&lt;/h3&gt;
&lt;p&gt;在分布式系统中，多个节点（如服务器、进程等）共同协作来完成任务，对外感觉就像是一个单机的服务。由于是分布式的环境，一定会存在网络问题，时钟问题，以及节点故障问题，这些问题都将导致系统出现各种各样的问题，其可靠性得不到保证。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;选主（Leader Election）&lt;/strong&gt;：在需要一个主节点进行写操作的分布式数据库中，系统必须确保在所有节点之间达成共识，选出一个主节点。如果网络出现故障，可能会导致出现多个主节点的情况，这样会引发数据冲突和一致性问题。共识算法能够保证在这种情况下，系统能够选出唯一的主节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原子提交（Atomic Commit）&lt;/strong&gt;：在涉及多个节点或分区的分布式事务中，所有节点必须一致地决定是提交还是回滚事务，以保证事务的原子性。如果事务在某些节点上成功，而在其他节点上失败，系统就需要使用共识机制来确保所有节点做出统一的决定，要么全部提交，要么全部回滚。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可见，共识是分布式系统正常运转的最基本保障，&lt;strong&gt;只有在共识的帮助下，分布式系统才能保证一致性&lt;/strong&gt;，像单一节点一样工作，对外提供可靠的服务。&lt;/p&gt;
&lt;h3&gt;4. 共识算法&lt;/h3&gt;
&lt;p&gt;「共识算法」也叫「一致性协议算法」，是用于在分布式系统中让多个独立的节点就某个决策或状态达成一致的一类算法。分布式系统中的每个节点都可能会因为网络分区、节点故障、延迟等问题，导致它们之间无法完全同步状态。因此，共识算法的目标是在这种不确定性和潜在的故障情况下，确保所有非故障节点最终能够就某个值或操作达成一致。&lt;/p&gt;
&lt;p&gt;共识算法通常具备以下几个关键特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;终止性（Termination）&lt;/strong&gt;：所有非故障的实例最终都会确定某一个值并终止算法执行。换句话说，算法不会无限期地执行下去，实例们都会在有限的时间内达成决议。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性（Consistency）&lt;/strong&gt;：所有非故障的实例最终选择的值 &lt;code&gt;vf&lt;/code&gt; 必须是相同的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完整性（Integrity）&lt;/strong&gt;：如果所有节点提议相同的值，那么该值一定会被最终选定为共识结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.1 常见的共识算法&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Paxos：一种经典的共识算法，被认为是分布式一致性问题的基础解决方案&lt;/li&gt;
&lt;li&gt;Raft：一种更易于理解和实现的共识算法，解决了 Paxos 的复杂性问题&lt;/li&gt;
&lt;li&gt;ZAB (ZooKeeper Atomic Broadcast protocol)：ZooKeeper 使用的原子广播协议，确保分布式系统中的状态一致性&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Paxos 算法&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;「共识算法」本质就是「一致性算法」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;相关链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lamport.azurewebsites.net/pubs/lamport-paxos.pdf&quot;&gt;The Part-Time Parliament&lt;/a&gt; (Paxos 算法)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lamport.azurewebsites.net/pubs/paxos-simple.pdf&quot;&gt;Paxos Made Simple&lt;/a&gt; (Paxos 推导)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/u013201439/article/details/81285271&quot;&gt;Paxos 算法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/linbingdong/p/6253479.html&quot;&gt;Paxos 算法原理及推导&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/column/paxos&quot;&gt;[知乎专栏] Paxos、Raft 分布式一致性最佳实践&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Paxos 算法是由 Leslie Lamport 在 1990 年代提出的一种&lt;strong&gt;基于消息传递共识算法&lt;/strong&gt;。在讨论分布式算法时，Paxos 几乎是一个绕不开的话题。在过去的几十年中，它已经成为分布式共识的象征，许多流行的共识算法都是基于 Paxos 进行改进的，比如 Fast Paxos、Raft、ZAB 等协议。虽然 Paxos 算法可以认为是一些共识算法的基础，但是其本身也相对较复杂，理解起来有一定的难度。&lt;/p&gt;
&lt;p&gt;Paxos 算法包括两个主要部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Basic Paxos 算法&lt;/strong&gt;：分布式系统中的多个节点如何就&lt;strong&gt;某个数据值&lt;/strong&gt;达成共识&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-Paxos 算法&lt;/strong&gt;：Multi-Paxos 是在 Basic Paxos 的基础上扩展而来，用于在分布式系统中就&lt;strong&gt;一系列的值&lt;/strong&gt;达成共识&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1. Basic Paxos&lt;/h3&gt;
&lt;p&gt;在正式介绍 Basic Paxos 算法之前，首先来看一个问题。&lt;/p&gt;
&lt;p&gt;假设有一个分布式的 key-value 存储集群，有 A，B，C 三个节点，对外提供只读服务。也就是说，key-value 键值对一旦被创建，就不能再被修改。&lt;/p&gt;
&lt;p&gt;假设此时，有两个客户端 client1 和 client2 同时发起创建键值对的请求，且创建的键值对 key 都为&quot;Name&quot;，client1 试图创建 “Name：张三”，client2 试图创建“Name：李四”，在这种情况下，这个分布式集群如何达成共识，实现在 A，B，C 各个节点上，Key 为&quot;Name&quot;的值是一致的呢。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410180647446.png&quot; alt=&quot;image-20241018064712350&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里其实就需要一种共识机制，保证集群中的 A，B，C 三个节点能达成一致，每个节点写入一样的值，Paxos 算法就能保证这样一种共识。&lt;/p&gt;
&lt;h4&gt;1.1 Paxos 涉及的概念&lt;/h4&gt;
&lt;p&gt;Basic Paxos 的工作机制主要分为三个角色和两个主要阶段。&lt;/p&gt;
&lt;h5&gt;1.1.1 提案（Proposal）&lt;/h5&gt;
&lt;p&gt;提案指的是需要在多个节点之间达成一致的某个值或操作，提案是由提案编号（n）和提案的值（v）组成的，可以表示为 &lt;code&gt;[n, v]&lt;/code&gt;。每个提案的提案编号是唯一的。&lt;/p&gt;
&lt;h6&gt;1.1.1.1 提案编号&lt;/h6&gt;
&lt;p&gt;提案编号一般不是由 Paxos 算法生成的，而是由外部传入的。所以不同的业务场景可以按照自身业务需求，自定义提案编号的生成逻辑，只需要保证提案编号全局唯一并且单调递增即可。&lt;/p&gt;
&lt;p&gt;比如在只有一个 Proposer 的环境中可以简单地使用&lt;strong&gt;自增 ID&lt;/strong&gt; 或&lt;strong&gt;时间戳&lt;/strong&gt;作为提案编号。例如，使用时间戳 1693702932000。在有两个 Proposer 的环境中，可以为不同提议者分配不同的编号序列。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;例如，一个提议者使用奇数（1, 3, 5...），另一个提议者使用偶数（2, 4, 6...）。在有多个 Proposer 的环境中，可以为每个 Proposer 分配一个固定的 ServerId，并将自增序号或时间戳与 ServerId 组合，生成唯一的提案编号。例如，&lt;code&gt;1.1&lt;/code&gt; 或者 &lt;code&gt;1693702932000.1&lt;/code&gt; 表示 Proposer1 生成的第一个提案编号。每个 Proposer 在发起 Prepare 请求后如果没有得到超半数响应时，会更新自己的提案号，再重新发起新一轮的 Prepare 请求。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h6&gt;1.1.1.2 提案值&lt;/h6&gt;
&lt;p&gt;在 Paxos 算法中，提案值的具体内容也是根据实际业务需求来定义的。提案值可以是数值、字符串、命令（cmd），甚至是一些操作。比如在分布式数据库的场景中，可以将数据的插入、更新、删除操作等作为提案值。这种灵活性允许 Paxos 算法适应各种不同的应用场景。&lt;/p&gt;
&lt;h5&gt;1.1.2 三个角色&lt;/h5&gt;
&lt;p&gt;在 Paxos 算法中，角色分为提议者（Proposer）、接受者（Acceptor）和学习者（Learner），它们的关系如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提议者（Proposer）：处理客户端请求，主动发起提案（proposal）给所有的接受者（Acceptor），提议者的角色通常由集群中的某些节点担任&lt;/li&gt;
&lt;li&gt;接受者（Acceptor）：被动接受提案，对提案进行投票，并存储已经接受的值，返回投票结果给 Proposer 以及发送通知给 Learner&lt;/li&gt;
&lt;li&gt;学习者（Learner）：不参与提案和投票，只被动接收提案结果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个节点，既可以是提议者，也可以是接受者&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181216718.png&quot; alt=&quot;image-20241018121610341&quot;&gt;&lt;/p&gt;
&lt;p&gt;在实际的分布式业务场景中，一个服务器节点或进程可以同时扮演其中的一种或几种角色，而且在分布式环境中往往同时存在多个 Proposer、多个 Acceptor 和多个 Learner。&lt;/p&gt;
&lt;h5&gt;1.1.3 两个阶段&lt;/h5&gt;
&lt;h6&gt;1.1.3.1 准备阶段（prepare）&lt;/h6&gt;
&lt;p&gt;&lt;strong&gt;提议者（Proposer）&lt;/strong&gt;：生成一个唯一的提案编号 n，并向所有的接受者（Acceptor）发送一个准备请求（Prepare Request），请求内容是编号 n，&lt;strong&gt;注意在准备阶段请求只会包含请求编号，而不会包含提案值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接受者（Acceptor）&lt;/strong&gt;：在收到准备请求后，接受者（Acceptor）会做出如下承诺：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果接受者（Acceptor）收到过比此次提案编号n更大的准备请求，将丢弃这次准备请求，不作响应&lt;/li&gt;
&lt;li&gt;否则：
&lt;ol&gt;
&lt;li&gt;接受者（Acceptor）承诺不再通过编号&lt;strong&gt;小于等于 n&lt;/strong&gt; 的提案的准备（Prepare）请求&lt;/li&gt;
&lt;li&gt;接受者（Acceptor）承诺不再通过编号&lt;strong&gt;小于 n&lt;/strong&gt; 的提案的接收（Accept）请求，也就是不再通过编号小于 n 的提案&lt;/li&gt;
&lt;li&gt;如果接受者（Acceptor）已经通过某一提案，则承诺在准备请求的响应中返回&lt;strong&gt;已经通过的最大编号的提案内容，即提案值&lt;/strong&gt;。如果没有通过任何提案，则在 prepare 请求的响应中返回空值，即尚无提案&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从 prepare 流程可知：集群中的每个 Acceptor 会存储自己当前已接受（Accept）的最大提案编号和提案值。&lt;/p&gt;
&lt;h6&gt;1.1.3.2 接受阶段（accept）&lt;/h6&gt;
&lt;p&gt;&lt;strong&gt;提议者&lt;/strong&gt;（Proposer）：在收到大多数接受者（Acceptor）的准备响应后，提议者将正式发起一个带有提案编号 n 和提案值 v 的接受请求 &lt;code&gt;[n, v]&lt;/code&gt; 给所有接受者。&lt;/p&gt;
&lt;p&gt;⚠️ 注意：这里提议者（Proposer）设置提案值 v 有一定的规则：&lt;strong&gt;如果在准备（prepare）请求的响应中，部分 acceptor 已经批准过的提案值，则 v 为 prepare 请求的响应中编号最大的提案值，否则可以由 proposer 任意指定&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接受者&lt;/strong&gt;（Acceptor）：接受者（Acceptor）会根据准备阶段的响应情况作出如下承诺：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果此时接受者没有通过编号大于 n 的准备请求，则会批准通过提案 &lt;code&gt;[n, v]&lt;/code&gt;，并返回已通过的编号最大的提案（也就是 n）&lt;/li&gt;
&lt;li&gt;如果此时接受者已经通过了编号大于 n 的准备请求，则会拒绝提案 &lt;code&gt;[n, v]&lt;/code&gt;，并返回已通过的编号最大的提案（大于 n 的编号，比如 m）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;提议者（Proposer）会统计收到的 accept 请求的响应，&lt;strong&gt;如果响应中的编号等于自己发出的编号，则认为该 acceptor 批准通过了该提案&lt;/strong&gt;。如果存在大多数 acceptor 批准了该提案，则认为该提案已达成共识，即该提案被通过。如果没有大多数 acceptor 批准该提案，则重新回到 prepare 阶段进行协商。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需要注意的是：在准备请求中，proposer 只会发提案编号 n。而 accept 请求，proposer 会发送提案编号和提案值，也就是 &lt;code&gt;[n, v]&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;1.1.4 算法流程&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181329833.png&quot; alt=&quot;image-20241018132934738&quot;&gt;&lt;/p&gt;
&lt;p&gt;先明确几个变量的意思：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;minProposal&lt;/code&gt;：当前 acceptor 在 prepare 请求中通过的最大提案编号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;acceptedProposal&lt;/code&gt;：当前 acceptor 在 accept 请求中通过的最大提案编号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;acceptedValue&lt;/code&gt;：当前 acceptor 在 accept 请求中通过的最大提案编号的提案值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Acceptor 需要持久化存储 minProposal、acceptedProposal、acceptedValue 这 3 个值&lt;/p&gt;
&lt;p&gt;算法流程如下：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一阶段：Prepare 阶段&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为提案生成一个全局唯一且递增的提案编号 &lt;code&gt;n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Proposer 会向所有 Acceptor 节点发送一个包含提案编号 &lt;code&gt;n&lt;/code&gt; 的 准备请求（&lt;code&gt;Prepare(n)&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;当 Acceptor 接收到准备请求（&lt;code&gt;Prepare(n)&lt;/code&gt;）时，会将 &lt;code&gt;n&lt;/code&gt; 与其已知的最小提案编号 &lt;code&gt;minProposal&lt;/code&gt; 进行比较，如果 &lt;code&gt;n&lt;/code&gt; &gt; &lt;code&gt;minProposal&lt;/code&gt;，则更新 &lt;code&gt;minProposal&lt;/code&gt; 为 &lt;code&gt;n&lt;/code&gt;，并返回其当前已经接受的提案编号 &lt;code&gt;acceptedProposal&lt;/code&gt; 和对应的值 &lt;code&gt;acceptedValue&lt;/code&gt; 给 Proposer，如果 &lt;code&gt;n&lt;/code&gt; 小于或等于 &lt;code&gt;minProposal&lt;/code&gt;，则该请求将被拒绝，不作处理&lt;/li&gt;
&lt;li&gt;Proposer 接收到大多数 （&lt;strong&gt;过半&lt;/strong&gt;）Acceptor 的响应后，如果发现有 Acceptor 返回了 &lt;code&gt;acceptedValue&lt;/code&gt;，那么 Proposer 将选择所有响应中编号最大的 &lt;code&gt;acceptedProposal&lt;/code&gt; 对应的 &lt;code&gt;acceptedValue&lt;/code&gt; 作为本次提案的值。&lt;/li&gt;
&lt;li&gt;如果所有 Acceptor 都没有返回 &lt;code&gt;acceptedValue&lt;/code&gt;，Proposer 可以自由设置本次提案的值&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;第二阶段：accept 阶段&lt;/strong&gt;&lt;/p&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;在确定提案的值后，Proposer 将向所有 Acceptor 节点广播接收请求（&lt;code&gt;Accept(n, value)&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;Acceptor 接收到 &lt;code&gt;Accept(n, value)&lt;/code&gt; 请求后，再次比较 &lt;code&gt;n&lt;/code&gt; 与其当前的 &lt;code&gt;minProposal&lt;/code&gt;，如果 &lt;code&gt;n&lt;/code&gt; &gt;= &lt;code&gt;minProposal&lt;/code&gt;，则 Acceptor 更新 &lt;code&gt;minProposal&lt;/code&gt; 和 &lt;code&gt;acceptedProposal&lt;/code&gt; 为 n，并将 value 设置为 &lt;code&gt;acceptedValue&lt;/code&gt;，然后持久化该提案并返回确认。如果 &lt;code&gt;n&lt;/code&gt; &amp;#x3C; &lt;code&gt;minProposal&lt;/code&gt;，则该请求将被拒绝，并返回当前的 &lt;code&gt;minProposal&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Proposer 接收到大多数 Acceptor 的确认后，若发现有返回值 result（minProposal） &gt; n，表示有更新的提议，跳转到步骤 1，否则认为当前提案已经达成一致&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;1.1.5 最终值选定&lt;/h5&gt;
&lt;p&gt;Acceptor 在每次同意新的提案值后，会将结果同步给 Learner。Learner 通过汇总各个 Acceptor 的反馈，判断是否已获得多数同意（超过半数）。如果达成共识，即获得了多数同意，Learner 会向所有 Acceptor 和 Proposer 发送广播消息，并结束提案。在实际应用中，Learner 通常由多个节点组成，每个 Learner 都需要接收到最新的投票结果。对于 Learner 的实现，Lamport 在其论文中提供了两种方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;主从 Learner 架构&lt;/strong&gt;：选定一个 Learner 作为主节点，专门接收投票结果（Accepted 消息），其他 Learner 节点作为备份节点。主节点接收数据后再将其同步到其他 Learner 节点。这种方案的缺点在于可能产生单点故障问题，如果主节点宕机，将无法获取投票结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分布式 Learner 同步&lt;/strong&gt;：Acceptor 同意提案后，将结果同步到所有 Learner 节点，然后每个 Learner 节点再将结果广播给其他 Learner 节点。尽管这种方式避免了单点故障，但由于涉及多次消息传递，效率相对较低。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;1.1.6 算法模拟&lt;/h5&gt;
&lt;p&gt;还是以开篇的例子来进行分析，在实际应用中，通常提议者（Proposer）是集群中的某些节点，接收客户端请求，将其封装成提案（proposal）。这里为了方便演示，将 Client1 和 Client2 看作提议者，并不会影响 Paxos 算法的本质。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;准备阶段（prepare）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设 Client1 的提案编号是 1，Client2 的提案编号是 6，Client1 和 Client2 分别向所有的接受者发送准备请求&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181337574.png&quot; alt=&quot;image-20241018133708424&quot;&gt;&lt;/p&gt;
&lt;p&gt;紧接着，节点 A 和节点 B 收先到提案者 Client1 的准备请求，编号为 1，节点 C 先收到提案者 Client2 的准备请求，提案编号为 6&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181338377.png&quot; alt=&quot;image-20241018133802307&quot;&gt;&lt;/p&gt;
&lt;p&gt;分析各个节点在接收到第一个准备请求的处理过程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点A、B：由于之前没有通过任何提案，所以节点 A 和节点 B 都将返回“尚无提案”的准备提案请求响应，并承诺，后续不再响应编号小于 1 的准备请求，也不会通过编号小于 1 的提案&lt;/li&gt;
&lt;li&gt;节点 C：由于之前没有通过任何提案，所以节点 A 和节点 B 都将返回“尚无提案”的准备提案请求响应，并承诺，后续不再响应编号小于 6 的准备请求，也不会通过编号小于 6 的提案&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181338417.png&quot; alt=&quot;image-20241018133850340&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来，节点 A 和节 B 收到提议者 Client2 发出的编号为 6 的提案，而节点 C 会收到提议者 Client1 发出的编号为 1 的提案&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点 A、B：此时收到的准备请求提案编号为 6，大于之前响应的准备请求的提案编号 1，并且节点 A 和节点 B 都还未通过（accept）任何提案，所以均返回“尚无提案”的准备提案请求响应，并承诺，后续不再响应编号小于 6 的准备请求，也不会通过编号小于 6 的提案&lt;/li&gt;
&lt;li&gt;节点 C：由于节点 C 此时接收到的提案编号 1 小于之前响应的准备请求的提案编号 6，所以丢弃该准备请求，不作响应&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181339227.png&quot; alt=&quot;image-20241018133944150&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接收阶段（accept）&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;Client1 和 Client2 收到大多数节点的准备响应之后，开始发送接收请求（accept）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Client1&lt;/strong&gt;：Client1 在接收到大多数的接受者（节点 A，B）的准备响应之后，会根据响应中的提案编号最大的提案值来设置接受请求的值。由于节点 A, B 均返回“尚无提案”，即提案值为空，所以 Client1 会自己设置一个提案值“张三”，把自己的提议值 “张三”作为该提案的值，发送接受请求 &lt;code&gt;[1, “张三”]&lt;/code&gt; 给 A, B, C 三个 acceptor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client2&lt;/strong&gt;：同理 Client2 在接收到大多数接受者的准备响应后，也会根据响应中的提案编号最大的提案的来设置接受请求的值。由于节点 A, B, C 均返回“尚无提案”，即提案值为空，所以 Client2 会自己设置一个提案值 “李四”，把自己的提议值 “李四” 作为该提案的值，发送接受请求 &lt;code&gt;[6, “李四”]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181342674.png&quot; alt=&quot;image-20241018134240604&quot;&gt;&lt;/p&gt;
&lt;p&gt;节点 A，B，C 收到 Client1 和 Client2 的接受请求之后&lt;/p&gt;
&lt;p&gt;由前面的准备阶段响应可知，节点 A，B，C 承诺可以通过的最小提案编号为 6，而此时节点 A，B，C 接收到的 Client1 发出的接受请求为 &lt;code&gt;[1, “张三”]&lt;/code&gt;，提案编号 1 小于承诺的提案编号 6，所以 &lt;code&gt;[1, “张三”]&lt;/code&gt; 被拒绝，并向 Client1 返回当前 accepter 在准备请求中通过的最大提案编号 6&lt;/p&gt;
&lt;p&gt;节点 A，B，C 收到 Client2 发出的接受请求为 &lt;code&gt;[6, “李四”]&lt;/code&gt;，提案编号 6 不小于承诺的提案编号 6，所以提案 &lt;code&gt;[6, “李四”]&lt;/code&gt; 被通过，节点 A，B，C 达成了共识，接收 Name 的值为 “李四”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181343324.png&quot; alt=&quot;image-20241018134338212&quot;&gt;&lt;/p&gt;
&lt;p&gt;假设集群中还有学习者，在接受者通过了某个提案后，会通知所有学习者。一旦学习者确认多数接受者都同意了这个提案，它们也会同意并采纳提案的值。&lt;/p&gt;
&lt;h6&gt;1.1.6.1 接受者存在已通过提案的情况&lt;/h6&gt;
&lt;p&gt;在上述例子中，在准备阶段和接受阶段均不存在已通过提案的情况，准备阶段接受者的请求响应都是“尚无提案”，假设有节点已经通过了提案点又是什么场景呢？想象出这样一个场景：&lt;/p&gt;
&lt;p&gt;假设节点 A，B 已经通过了 &lt;code&gt;[6, “李四”]&lt;/code&gt; 提案，而节点 C 尚未通过任何提案，此时，新增一个提议者 Client3，Client3 的提案为 &lt;code&gt;[8，“王五”]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Client3 向节点 A，B，C 发送提案编号为 8 的准备请求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181353400.png&quot; alt=&quot;image-20241018135343311&quot;&gt;&lt;/p&gt;
&lt;p&gt;节点 A 和 B 将接收 Client3 的准备请求，由于节点 A 和 B 已经通过了编号为 &lt;code&gt;[6, “李四”]&lt;/code&gt; 的提案，所以它们在准备响应中会包含这个提案的详细信息。而节点 C 因为之前没有通过任何提案，因此它返回的是‘尚无提案’的准备响应。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181355899.png&quot; alt=&quot;image-20241018135504824&quot;&gt;&lt;/p&gt;
&lt;p&gt;在接收到来自节点 A、B、C 的准备响应后，Client3 随即向这些节点发送接受请求。特别要注意的是，&lt;strong&gt;Client3 会根据响应中提案编号最大的提案值来确定接受请求的值（1.1.3.2 小节的注意事项）&lt;/strong&gt;。由于准备响应中包含了提案 &lt;code&gt;[6, “李四”]&lt;/code&gt;，因此 Client3 将接受请求的提案编号设为 8，提案值设置为“李四”，即客户端 3 发送的接受请求为 &lt;code&gt;[8, “李四”]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181355046.png&quot; alt=&quot;image-20241018135546912&quot;&gt;&lt;/p&gt;
&lt;p&gt;节点 A, B, C 接收到提议者 Client3 的接受请求，由于提案编号 8 不小于三个节点承诺可以通过的最小提案编号 6，因此均通过提案 &lt;code&gt;[8, “李四”]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181356395.png&quot; alt=&quot;image-20241018135614277&quot;&gt;&lt;/p&gt;
&lt;h5&gt;1.1.7 活锁问题&lt;/h5&gt;
&lt;p&gt;先看一个例子，这里 Server1 和 Server4 既作为 Proposer，又作为 Acceptor，Server1 即 Proposer1，Server4 即 Proposer4。所有的 server 都是 acceptor。P1.1 表示 Proposer1 发起的编号为 1 的 prepare 请求，P2.4 表示 Proposer4 发起的编号为 2 的 prepare 请求，以此类推。A1.1 X 表示 Proposer1 发起的 accept 请求，提案编号为 1，提案值为 X，A2.4 Y 表示 processe4 发起的 accept 请求，提案编号为 2，提案值为 Y，以此类推。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410181356609.png&quot; alt=&quot;image-20241018135658553&quot;&gt;&lt;/p&gt;
&lt;p&gt;client1 向 server1 即 Proposer1 发起写入 X 的请求，client2 向 server4 即 Proposer4 发起写入 Y 的请求。&lt;/p&gt;
&lt;p&gt;随着时间线从左往右看，server3 即 acceptor3 先收到 proposer1 发起的 prepare 请求 P1.1，由于没有通过任何提案，所以尚无提案，承诺不通过提案编号小于 1 的提案，随后 acceptor3 收到了 proposer4 发起的 prepare 请求 P2.4，由于此次天的编号 2 大于之前承诺的提案编号 1，所以向 proposer4 返回，承诺不再通过提案编号小于 2 的提案。&lt;/p&gt;
&lt;p&gt;紧接着 acceptor3 收到 proposer1 发起的 accept 请求 A1.1 X，由于之前承诺了不再通过提案编号小于 2 的提案，而此次收到的 accept 提案编号为 1，所以拒绝。proposer1 发起的 accept 提案被拒绝了，所以它加大编号，又发起了 P3.1 的 prepare 请求，此次提案编号为 3，大于 acceptor3 承诺的最大提案编号 2，所以做出响应，回应承诺不再通过提案编号小于 3 的提案，随后 proposer4 发来 accept 请求 A2.4 Y，此时由于提案编号为 2，小于 acceptor3 刚刚承诺的最大提案编号 3，所以这个 accept 请求也会被决绝。proposer4 又加大 prepare 的提案编号，如此循环往复.....&lt;/p&gt;
&lt;p&gt;一直通过多个 Proposer 的 prepare 请求，但是不能通过 accept 请求，导致一直没有提案通过，这样就形成了活锁。&lt;/p&gt;
&lt;h6&gt;1.1.7.1 活锁的定义&lt;/h6&gt;
&lt;p&gt;在多个提议者同时提出提案时，由于出现竞争，这几个提议者不断的更新提案编号，发起新的提案，导致一直没有 accept 请求被通过，导致提案一直不能通过，而陷入这样的死循环，就是活锁问题。&lt;/p&gt;
&lt;h6&gt;1.1.7.2 活锁的解决方案&lt;/h6&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;随机延迟重试&lt;/strong&gt;：当一个 Proposer（提议者）发现支持它的 Acceptor（接受者）数量小于半数时，Proposer 并不会立即更新编号并再次发起提案，而是会随机延迟一小段时间。这样做的目的是为了错开多个 Proposer 之间的冲突。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设置 Proposer 的 Leader&lt;/strong&gt;：可以在系统中选举一个 Proposer 作为 Leader，让这个 Leader 负责发起所有的提案。其他 Proposer 不再主动提案，只在需要时响应 Leader 的请求。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. Multi-Paxos&lt;/h3&gt;
&lt;p&gt;Basic Paxos 算法虽然能一定程度解决分布式系统的共识问题，但是存在很多的局限性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它只能对一个值形成决议&lt;/li&gt;
&lt;li&gt;提案的最终确定至少需要两次网络来回，在高并发情况下可能需要更多的网络来回，因此性能低下&lt;/li&gt;
&lt;li&gt;当存在多个 Proposer 的时候，极端情况下甚至会形成活锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此 Basic Paxos 算法几乎只是用来做理论研究，并不直接应用在工程实践中。&lt;/p&gt;
&lt;p&gt;有没有更好的算法策略能够有效解决 Basic Paxos 算法带来的这些问题呢？基于这种目的随即出现了 Multi-Paxos 算法&lt;/p&gt;
&lt;h4&gt;2.1 Multi-Paxos 算法概念&lt;/h4&gt;
&lt;p&gt;Multi-Paxos 算法其实是 Lamport 提出的一种思想，而并非具体的算法。可以认为，Multi-Paxos 算法是一类算法的总称，这类算法都基于 Multi-Paxos 思想，实现了&lt;strong&gt;一系列值共识&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;2.2 Multi-Paxos 思想&lt;/h4&gt;
&lt;p&gt;总的来说，multi-paxos 思想基于 basic-paxos 算法做了两点改进：&lt;/p&gt;
&lt;h5&gt;2.2.1 领导者选举&lt;/h5&gt;
&lt;p&gt;在所有 Proposers 中选举出一个 Leader，让这个 Leader 唯一地提交提案（Proposal）给 Acceptors 进行表决。这样一来，就没有多个 Proposer 之间的竞争，从而解决了活锁问题。在只有一个 Leader 提交提案的情况下，Prepare 阶段可以被跳过，从而将原本的两阶段过程简化为一阶段，从而显著提高了系统的效率。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410180635466.png&quot; alt=&quot;image-20241018063551863&quot;&gt;&lt;/p&gt;
&lt;h5&gt;2.2.2 优化 Basic Paxos 执行过程&lt;/h5&gt;
&lt;p&gt;准备阶段的意义在于让 Proposer 通过发送 Prepare 请求来了解各个 Acceptor 节点上已通过的提案，但有了领导者（Leader）后，只有领导者才可发送提议，领导者独自负责提出提案，所以领导者能够保证提案的最新性和唯一性。因此，领导者的提案就已经是最新的了。所以不再需要通过准备阶段来发现之前被大多数节点通过的提案。领导者可以直接跳过准备阶段，进入接受阶段（Accept Phase），从而减少不必要的通信开销和 RPC 调用次数。&lt;/p&gt;
&lt;h3&gt;3. Paxos 面试题&lt;/h3&gt;
&lt;h4&gt;3.1 Prepare 与 Accept 阶段工作过程差不多，为什么需要 Prepare 过程呢，Paxos 算法在设计之初为什么不直接进行 Accept 阶段呢&lt;/h4&gt;
&lt;p&gt;因为在 basic proxos 算法中，有多个 proposer 可以发起提案，一个 acceptor 可能通过不同的 proposer 发起的提案，prepare 请求的主要作用就是获取各个 acceptor 节点上已通过的最新提案，保证最新性和唯一性。&lt;/p&gt;
&lt;h4&gt;3.2 提案编号可以怎么生成&lt;/h4&gt;
&lt;p&gt;在只有一个 Processer 的环境中可以简单地使用自增 ID 或时间戳作为提案编号。例如，使用时间戳 &lt;code&gt;1693702932000&lt;/code&gt;。在有两个 Processer 的环境中，可以为不同提议者分配不同的编号序列。例如，一个提议者使用奇数（1, 3, 5...），另一个提议者使用偶数（2, 4, 6...）。在有多个 Processer 的环境中，可以为每个 Processer 分配一个固定的 ServerId，并将自增序号或时间戳与 ServerId 组合，生成唯一的提案编号。例如，1.1 或者 1693702932000.1 表示 Processer1 生成的第一个提案编号&lt;/p&gt;
&lt;h4&gt;3.3 Paxos 协议的活锁问题怎么解决&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;随机延迟重试&lt;/strong&gt;：当 Proposer 接收到响应，发现支持它的 Acceptor 小于半数时，不立即更新编号发起重试，而是随机延迟一小段时间，来错开彼此的冲突&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设置 Proposer 的 Leader&lt;/strong&gt;：可以在系统中选举一个 Proposer 作为 Leader，让这个 Leader 负责发起所有的提案，其他 Proposer 不再主动提案，只在需要时响应 Leader 的请求，等同于 Multi-Paxos 算法&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/202504140127607.P8IOcI_1.png"/><enclosure url="/_astro/202504140127607.P8IOcI_1.png"/></item><item><title>分布式系统｜共识算法 Raft</title><link>https://coooredump.github.io/blog/system-architecture/distributed-systems-raft</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/distributed-systems-raft</guid><description>Raft 算法是一类基于日志复制的分布式共识算法，由于 Raft 算法易于理解和实现，在提出后，迅速获得了广泛关注，并成为了分布式系统中实际应用最广泛的一致性算法之一。目前，已经有十多种语言的 Raft 算法实现框架，比较有代表性的有 etcd、Consul，CockroachDB 等。</description><pubDate>Sun, 13 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Raft 算法&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Raft 动图演示：http://www.kailing.pub/raft/index.html&lt;/p&gt;
&lt;p&gt;更多链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/654074439&quot;&gt;分布式系统一致性模型与共识算法：Raft 详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/681152253&quot;&gt;深度解析 Raft 分布式一致性协议&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410231725869.png&quot; alt=&quot;image-20241023172554484&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1. Raft 背景&lt;/h3&gt;
&lt;p&gt;Paxos 算法虽然理论上能够解决分布式的共识问题，但是其过于复杂，难以理解。实现 Paxos 算法的开源软件很少，比较有代表性的是 Google 的 Chubby。&lt;/p&gt;
&lt;p&gt;正是由于 Paxos 算法的复杂性和实现难度，使得其在实际工程中的应用受到了限制。然而，分布式系统的发展迫切需要一种既高效又易于实现的分布式一致性算法。在这种背景下，Raft 算法应运而生，成为一种更具实用性和可读性的替代方案。&lt;/p&gt;
&lt;p&gt;Raft 算法由斯坦福大学的 Diego Ongaro 和 John Ousterhout 在 2013 年发表的《In Search of an Understandable Consensus Algorithm》中提出。&lt;strong&gt;Raft 算法是一类基于日志复制的分布式共识算法&lt;/strong&gt;，由于 Raft 算法易于理解和实现，在提出后，迅速获得了广泛关注，并成为了分布式系统中实际应用最广泛的一致性算法之一。目前，已经有十多种语言的 Raft 算法实现框架，比较有代表性的有 &lt;strong&gt;etcd、Consul，CockroachDB&lt;/strong&gt; 等。&lt;/p&gt;
&lt;p&gt;所以说&lt;strong&gt;掌握了 Raft 算法，就能比较轻松地处理绝大部分的一致性场景和需求&lt;/strong&gt;，本文的大纲如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410190220881.png&quot; alt=&quot;image-20241019022013692&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2. Raft 算法优化思路&lt;/h3&gt;
&lt;p&gt;Raft 算法为了达到易于理解的目的，主要做了两件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问题分解：将分布式共识问题拆分成主节点选举、日志复制、安全点，以及成员变更 4 个独立子问题逐一进行解决&lt;/li&gt;
&lt;li&gt;状态简化：通过减少算法中需要考虑的状态数，使得算法更加清晰和易于理解&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 领导者选举（Leader Election）&lt;/h3&gt;
&lt;h4&gt;3.1 Raft 角色&lt;/h4&gt;
&lt;p&gt;在一个 Raft 集群中，每个节点有以下三种状态，一般也称这三种状态的节点为 Raft 集群的三种角色&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;领导者（Leader）
&lt;ol&gt;
&lt;li&gt;处理客户端请求&lt;/li&gt;
&lt;li&gt;管理和同步日志&lt;/li&gt;
&lt;li&gt;定期向 Follower 发送心跳（Heartbeat）信号，以表明自己仍然存活，并防止 Follower 发起选举&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;跟随者（Follower）
&lt;ol&gt;
&lt;li&gt;被动地响应 Leader 的日志同步请求&lt;/li&gt;
&lt;li&gt;响应 Candidate 发起的邀票请求&lt;/li&gt;
&lt;li&gt;把客户端打到 Follower 的请求转发给 Leader&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;候选者（Candidate）
&lt;ol&gt;
&lt;li&gt;在集群刚启动或 Leader 宕机时，Follower 节点可以转换为 Candidate 并发起选举&lt;/li&gt;
&lt;li&gt;当 Follower 长时间未接收到 Leader 的心跳信号时，会认为 Leader 可能已经失效，从而将自己状态转换为 Candidate 并发起选举。Candidate 向其他节点请求选票，若获得超过半数节点的投票，它就会成为新的 Leader&lt;/li&gt;
&lt;li&gt;如果选举胜出，Candidate 转变为 Leader；否则，如果有其他节点当选为 Leader，Candidate 会返回到 Follower 状态&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;节点状态转换如下图所示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191024114.png&quot; alt=&quot;image-20241019102437957&quot;&gt;&lt;/p&gt;
&lt;p&gt;从上图可以看出，在集群启动时，所有的节点都处于 Follower 状态，如果在一段时间内，没有收到来自 Leader 的心跳信息，则 Follower 将切换成 Candidate，然后发起投票，如果该 Candidate 收到了多数（超过半数）的 Follower 的投票（包含该 Candidate 自己投给自己的一票），则该 Candidate 将切换成 Leader。在选举过程中，如果该 Candidate 发现有其他节点有比自己更新（即日志条目的任期号更高），它会自动放弃选举，并重新切回 Follower。&lt;/p&gt;
&lt;p&gt;一句话概括：系统中最多只有一个 Leader，如果在某一段时间内没有 Leader，Follower 会通过选举投票的方式选出 Leader。Leader 会不停的给 Follower 发送心跳信息，保证自己的存活状态。如果 Leader 挂掉，Follower 会切换成 Candidate 发起投票，重新选出 Leader。&lt;/p&gt;
&lt;h4&gt;3.2 任期&lt;/h4&gt;
&lt;p&gt;任期是一个整数，用于标识 raft 集群的一个时间段。这个跟国家行政相似，比如上一个五年是领导人人 xx 任期的五年，这个五年是领导人 yy 的任期，可以简单的理解为“朝代”，用递增的整数表示。那对应到 raft 集群中，就可以简单的理解为，任期是某个节点处于 leader 的一个时间段。但这里需要明确一点，可能某些情况下，因为选票的分流，在选举期间内没有成功选出 Leader，则会进入下一个任期。
任期示意图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191047831.png&quot; alt=&quot;image-20241019104758716&quot;&gt;&lt;/p&gt;
&lt;p&gt;任期包含两个阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选举阶段&lt;/li&gt;
&lt;li&gt;已选举出 Leader 的阶段&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同上面所说，任期也可能只包含选举阶段，没有 Leader，比如图中的任期 3，这种情况下，会立即进入到下一个任期，开始新的选举。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;任期具有如下特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个节点都会保存当前的任期（即一个标识时间段的整数），并随着集群状态的变化进行更新&lt;/li&gt;
&lt;li&gt;Follower 等待 Leader 的心跳超时后，会推举自己为 Candidate 发起投票，此时会将当前任期编号加 1。比如当前该 Follower 保存的任期为 1，在推举自己为候选人邀票时，会将任期编号增加到 2&lt;/li&gt;
&lt;li&gt;当一个节点发现自己保存的任期编号比另一个节点的任期编号小，它会主动更新自己的任期编号到最新的较大的任期编号，比如节点 A 当前的任期编号是 1，当收到来自节点 B 的请求投票 的 RPC 消息时，因为消息中包含了节点 B 的任期编号，且编号为 2，那么节点 A 将把自己的任期编号更新为 2&lt;/li&gt;
&lt;li&gt;如果一个节点接收到一个比自己任期编号小的 RPC 请求，该节点会立即拒绝这个 RPC 请求（无论是投票请求还是日志追加请求）。这是因为这个请求的任期编号已经过时，代表着发出请求的节点拥有的是旧任期，不再被视为合法的领导者或候选者&lt;/li&gt;
&lt;li&gt;如果一个 Leader 或者是 Candidate 发现自己的任期编号比其他节点小，该节点会立即退回到 Follower，假设由于网络分区错误，集群中出现了两个 Leader，LeaderA 任期编号为 4，LeaderB 任期编号为 5，当网络分区错误恢复后，LeaderA 收到了来自 LeaderB 的心跳信息，LeaderA 将回退为 Follower，接收 LeaderB 成为 Leader&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3.3 随机超时&lt;/h4&gt;
&lt;p&gt;Raft 算法中的随机超时有以下两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Follower 等待 Leader 心跳信息的超时间隔是随机的&lt;/strong&gt;：这里的随机化的超时机制可以防止多个 Follower 同时转换为 Candidate，减少选举冲突&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Candidate 等待选举结果的超时间隔是随机的&lt;/strong&gt;：当一个节点转换为 Candidate 并发起选举后，会等待其他节点的投票结果，这个等待时间是随机的。如果在这个等待的时间段内，没有获得大多数选票，将再次随机设置一个等待时间，发起新一轮投票。这里的随机化的超时机制降低了多个 Candidate 同时发起选举的可能性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总的来说，在 Raft 算法中，随机超时机制是一个关键设计，保证在大多数情况下只有一个节点发起选举，避免多 Candidate 选举带来的性能问题&lt;/p&gt;
&lt;h4&gt;3.4 通信方式&lt;/h4&gt;
&lt;p&gt;在 Raft 算法中，节点之间通过远程调用（RPC）进行通信，主要涉及以下三种类型的 RPC：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;投票 RPC&lt;/strong&gt;：由 Candidate 节点在选举过程中向 Follower 发出&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;附加日志条目 RPC&lt;/strong&gt;：Leader 节点在日志复制过程中将日志条目发送给其他 Follower 节点，同时也起到维持心跳的作用，确保 Leader 的存活状态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快照 RPC&lt;/strong&gt;：当 Follower 的日志落后 Leader 太多时，Leader 会发送 Snapshot RPC 请求，通过快照的方式帮助 Follower 快速同步日志&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3.5 选举流程&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191055081.png&quot; alt=&quot;image-20241019105555033&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示：Follower 在收到 Leader 心跳信息超时后，会推选自己为 Candidate，将自身的任期+1，然后发起选举，如果在设置的时间内收到了多数选票，将晋升为新的 Leader，如果没有获得足够多的选票，收到 Leader 的心跳包，则 Candidate 恢复成 Follower 角色。&lt;/p&gt;
&lt;h5&gt;3.5.1 选举详细流程下面以三个节点的集群来演示下选举的详细流程&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始状态&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;初始时，每个节点的角色都是 Follower，任期 Term 为 0（假设任期编号从 0 开始），每个节点都设置了一个随机超时时间（节点 A：100ms，节点 B：120ms，节点 C：160ms），如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191154329.png&quot; alt=&quot;image-20241019115401253&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;发起投票&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于节点 A 的随机超时时间是设置的最小的，为 100ms，所以在 A 在 100ms 后倒计时结束被唤醒，成为 Candidate，并为自己发起投票，此时将自己的任期编号加 1，变为 1。先投自己一票，然后向其他的 Follower 发起投票 RPC 请求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191154843.png&quot; alt=&quot;image-20241019115443774&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;响应投票&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Follower 节点 B 和 C 收到 Candidate 节点 A 的投票请求后，会做如下处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果自身已经在任期编号为 1 的投票请求中投过票了，则会忽略该投票请求&lt;/li&gt;
&lt;li&gt;否则，将自己的选票投给 Candidate，也就是节点 A，并将自身保存的任期编号设置为 1，然后重置随机超时时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设 B 和 C 都没有在任期编号为 1 的投票请求中投过票，此时都将选票投给 A，并设置自身的任期编号为 1，然后重置随机超时时间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191155090.png&quot; alt=&quot;image-20241019115524037&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;&lt;strong&gt;结束投票&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191449637.png&quot; alt=&quot;image-20241019144905549&quot;&gt;&lt;/p&gt;
&lt;h5&gt;3.5.2 多 Candidate 选举&lt;/h5&gt;
&lt;p&gt;从上面的选举过程可知，每个节点都会设置一个随机超时时间，这样可以降低了多个节点在同一时刻被唤醒成为 Candidate 的概率。但是也只是能降低概率，并且由于系统可能存在网络延迟，所以仍然无法完全避免多个 Follower 同时成为 Candidate 发起投票，假设这里有两个 Follower（A 和 B）同时被唤醒，转换为 Candidate 发起投票：&lt;/p&gt;
&lt;p&gt;假设 A 和 B 设置的随机超时时间都是 120ms，在 A 和 B 节点被同时唤醒之后，会各自为自己投上一票，然后开始向其他节点发送投票请求，假设节点 C 先收到 A 的投票请求，之后再收到 B 的投票请求，那样 C 将会把选票投给 A，最终节点 A 获得两票（包含自己一票）成为 Leader，而节点 B 只会获得一张选票（自己的一票），则会回退成 Follower.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191457084.png&quot; alt=&quot;image-20241019145739011&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191457963.png&quot; alt=&quot;image-20241019145725893&quot;&gt;&lt;/p&gt;
&lt;h5&gt;3.5.3 平票问题&lt;/h5&gt;
&lt;p&gt;一般在集群中，节点的个数都会选择奇数个，很重要的一点就是防止两个 Candidate 同时发起选票获得相同票数，导致系统内无法选出 Leader 的情况。但是由于系统可能出现故障，导致某个节点故障之后，依然可能会存在这个问题。假设集群中的节点是出现了偶数个，结果又会怎样呢？&lt;/p&gt;
&lt;p&gt;假设集群中有 A，B，C，D 四个节点，其中 A，B 两个节点设置的随机超时时间都是 120ms。现在假设 A，B 被同时唤醒了，向其他节点发送投票请求。节点 A 和 B 在同一任期内竞选领导者时，由于每个节点在同一个任期内只能投票一次，A 和 B 都已经投了自己的票，因此不会再给对方投票。然后节点 C 把票投给 A，节点 D 把票投给 B，这样节点 A 和 B 都获得了两张选票，&lt;strong&gt;出现了平票的情况，这种情况下是不会有 Leader 被选出来的，所有节点会恢复成 Follower 状态，重新设置随机超时时间，准备下一轮的选举，虽然会有下一次轮的选举，直到选出新的 Leader，但是在这个过程中，集群都是处于不可用状态，所以选举的轮次越多，集群不可用状态越久，因此要尽量避免平票问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;节点 A 和节点 B 回退为 Follower 之后，重新设置随机超时时间，节点 A 50ms，节点 B 100ms，等待超时时间开启新一轮的选举：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191458849.png&quot; alt=&quot;image-20241019145808794&quot;&gt;&lt;/p&gt;
&lt;h5&gt;3.5.4 脑裂问题&lt;/h5&gt;
&lt;p&gt;前面所说的情况是在集群完全正常的情况下，一个集群中只会存在一个 Leader。&lt;strong&gt;假设在一个集群内发发生了网络分区&lt;/strong&gt;，形成了两个分区，选举情况将会怎样进行呢？&lt;/p&gt;
&lt;p&gt;这里以 5 个节点的集群为例，集群节点 A,B,C,D,E，节点 A 为集群 Leader。假设发生了网络分区，[A,B,C] 为一个分区，节点 [D,E] 为一个分区，由于 A 本身就是原集群的 Leader，所以 [A,B,C] 分区内还是按照以前的集群模式 A 为 Leader，向 B，C 节点发送心跳。而 [D,E] 由于发生了网络分区，收不到 A 节点的心跳信息了，假设 D 节点设置的随机超时时间较短，那么到时间后，会成为 Candidate，发起投票，成为 [D,E] 这个分区的 Leader，由于经过了一轮选举，那么 [D,E] 这个分区的任期将会比 [A,B,C] 分区的任期大 1。这个时候在整个集群 [A,B,C,D,E] 中就有了两个 Leader A 和 D，这就是“脑裂问题”。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;脑裂问题如何解决&lt;/strong&gt;？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实在网络恢复后，虽然有了两个 Leader，Leader 都会向其他的节点发送心跳信息，这里 A 和 C 会互相收到对方发送的心跳信号，但是在 A 节点收到 C 节点发送的心跳之后，会发现携带的任期比自身保存的任期要大，所以 A 节点会退成 Follower，集群会再次恢复成只有一个 Leader 的状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191458593.png&quot; alt=&quot;image-20241019145832505&quot;&gt;&lt;/p&gt;
&lt;h3&gt;4. 日志复制（Log Replication）&lt;/h3&gt;
&lt;p&gt;Raft 算法中第二个很重要的字问题就是日志复制，日志复制（Log Replication）是保证整个集群中的所有节点（follower）一致地存储相同状态的核心机制。它的主要目标是通过将客户端提交的指令（通常是状态变化操作）复制到每个节点的日志中，确保所有节点都达成一致的状态，即一致性。&lt;/p&gt;
&lt;h4&gt;4.1 日志&lt;/h4&gt;
&lt;p&gt;在 raft 日志其实是一种数据格式，主要用于存储客户端的一些列操作指令，日志由三部分组成，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;索引值（Log index）&lt;/li&gt;
&lt;li&gt;任期编号（Term）&lt;/li&gt;
&lt;li&gt;指令（Command）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191610677.png&quot; alt=&quot;image-20241019161047597&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;索引值：日志条目对应的索引值，用来标识第几条日志，是一个连续单调递增的整数&lt;/li&gt;
&lt;li&gt;任期编号：创建这条日志条目的 Leader 的任期编号&lt;/li&gt;
&lt;li&gt;指令：客户端发起请求需要执行的指令，例如指令 &lt;code&gt;X &amp;#x3C;- 2&lt;/code&gt; 表示将 X 变量赋值为 2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一条日志也叫日志项，从上图可以看出，在一个 Leader 的任期内，可以有多个日志项，比如任期 1 内有 3 个日志项，任期 3 内有 4 个日志项。&lt;/p&gt;
&lt;p&gt;日志还对应有两个状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;committed&lt;/code&gt;（已提交）：针对的是日志，对应于某个日志项被成功复制到集群的大多数节点之后，这个日志项就处于 committed 状态，比如索引值 1-7 所对应的日志项均处于 committed 状态，因为他们都被复制到了大多数节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;applied&lt;/code&gt; （已应用）：针对的是状态机即节点，&lt;strong&gt;节点要将日志真正应用到状态机&lt;/strong&gt;，即真正改变了节点上对应变量的值&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.2 日志复制过程&lt;/h4&gt;
&lt;p&gt;集群中只有 Leader 会跟客户端交互，接收客户端的指令，而这些指令除了要在客户端执行以外，还需要通过日志复制的方式讲这些指令复制到各个 Follower 节点，以保证集群的一致性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191616211.png&quot; alt=&quot;image-20241019161611127&quot;&gt;&lt;/p&gt;
&lt;p&gt;日志复制的过程可以总结如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Leader 接收到客户端请求，请求中的指令创建一个新的日志项，然后&lt;strong&gt;追加&lt;/strong&gt;（append）到当前本地日志中（此时 Leader 中该日志项的状态为 &lt;code&gt;uncommitted&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;Leader 通过日志复制 RPC 请求将该日志项复制到其他 Follower 节点（此时在各个 Follower 中该日志项的状态为 &lt;code&gt;uncommitted&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;当 Leader 确认将日志项成功复制到大多数节点后，Leader 会将该日志项标记为 &lt;code&gt;committed&lt;/code&gt;，&lt;strong&gt;之后 Leader 会将该日志项应用到自己的状态机，即真正执行指令，修改对应的值&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Leader 将执行结果返回给客户端&lt;/li&gt;
&lt;li&gt;Leader &lt;strong&gt;通过心跳或新的日志复制请求&lt;/strong&gt;将提交了该日志项的状态同步给 Follower，如果 Follower 发现 Leader 已提交了该日志项，而自己还没将该日志项应用 &lt;code&gt;apply&lt;/code&gt; 至状态机，则会将该日志项应用至自己的状态机中&lt;/li&gt;
&lt;li&gt;如果 Follower 节点出现宕机或者由于网络丢包，Leader 会通过不断重试发送日志复制请求来确保日志条目最终复制到 Follower 上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看出，在日志复制过程中，只要有半数以上的处于正常工作的状态，整个系统就可用，假如在复制日志的过程中，出现了节点宕机、进程中断等问题，可能导致日志不一致，这种情况会怎么处理呢？&lt;/p&gt;
&lt;h4&gt;4.3 日志一致性&lt;/h4&gt;
&lt;p&gt;从前面的日志复制过程可以看出，在日志复制过程中，只要有半数以上的处于正常工作的状态，整个系统就可用，假如在复制日志的过程中，出现了&lt;strong&gt;节点宕机&lt;/strong&gt;、&lt;strong&gt;进程中断&lt;/strong&gt;等问题，可能导致日志不一致，这种情况会怎么处理，怎么来保证各个节点日志的一致性呢？&lt;/p&gt;
&lt;p&gt;首先看一下 Raft 日志的特点，Raft 日志具体如下两个特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不同日志中的两个日志项有相同的「任期编号」和「索引值」，那么这两个日志项一定有相同的指令&lt;/li&gt;
&lt;li&gt;如果不同日志中的两个日志项有相同的「&lt;strong&gt;任期编号&lt;/strong&gt;」和「&lt;strong&gt;索引值&lt;/strong&gt;」，&lt;strong&gt;那么这两个日志项之前的所有日志项也全部都相同&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过这两个特征其实可以看出，只要同步到位的日志都是一致的，在 Raft 算法中，&lt;strong&gt;其实是以领导者日志为准来实现日志的一致性的&lt;/strong&gt;，主要包括两个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Leader 通过日志复制 RPC 请求的&lt;strong&gt;一致性检查&lt;/strong&gt;，找到 Follower 节点上与自己&lt;strong&gt;具有相同日志项的最大索引值&lt;/strong&gt;（在该索引值之前的日志项，Leader 和 Follower 是一致的，之后不一致）&lt;/li&gt;
&lt;li&gt;Leader 强制 Follower 更新不一致日志条目，Leader 强制 Follower 将该索引值之后的所有日志项删除，并将 Leader 该索引值之后的所有日志项同步给 Follower&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;4.3.1 一致性检查&lt;/h5&gt;
&lt;p&gt;Leader 为了找到 Follower 节点上与自己具有相同日志项的最大索引值，每次日志复制请求除了发送该日志项之外，还要发送一些额外信息，这里引入两个新的概念：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;prevLogIndex&lt;/code&gt;：Leader 当前需要复制的日志项的前一个日志项的索引值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prevLogTerm&lt;/code&gt;：Leader 当前需要复制的日志项的前一个日志项的任期编号&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如下图，假设 Leader 当前需要将索引值为7的日志项发送复制到 Follower，则 &lt;code&gt;prevLogIndex&lt;/code&gt; 为 6，&lt;code&gt;prevLogTerm&lt;/code&gt; 为 3&lt;/p&gt;
&lt;p&gt;下面以一个具体的例子来看：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410191622200.png&quot; alt=&quot;image-20241019162239119&quot;&gt;&lt;/p&gt;
&lt;p&gt;当前 Leader 的最大日志项索引为 10，假设当前 Leader 需要将 10 号日志项复制给 Follower，步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Leader 将索引值为 10 的日志项通过日志复制 RPC 请求发送给 Follower，同时还会发送该日志项的 prevLogIndex（9）和 prevLogTerm（3），Follower 收到消息后，判断自己没有索引值为 9 的日志，因此拒绝更新日志并向 Leader 失败信息&lt;/li&gt;
&lt;li&gt;Leader 收到 Follower 的失败响应后，将日志项的索引值减 1，接着发送索引值为 9 的日志项并且携带 prevLogIndex（8）和 prevLogTerm（3）给 Follower，Followe 发现自己索引值为 8 的日志项中任期为 4，指令为 &lt;code&gt;N &amp;#x3C;- 5&lt;/code&gt;，和 Leader 发过来的日志项不一样 ，再次拒绝更新，向 Leader 响应失败&lt;/li&gt;
&lt;li&gt;直至需要复制索引值为 7 的日志项时，Follower 发现同步过来的 &lt;code&gt;prevLogIndex&lt;/code&gt; 为 6，&lt;code&gt;prevLogTerm&lt;/code&gt; 为 3，与自己在索引值为 6 的的日志条目相同（任期也是 3），则接收该日志复制 RPC 请求&lt;/li&gt;
&lt;li&gt;Leader 收到跟 Follower 的成功响应后，Leader 通过日志复制 RPC 消息，强制 Follower 复制并更新覆盖索引值为 7 及之后的内容。保证 Follower 与 Leader 的日志状态一致&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5. 安全性&lt;/h3&gt;
&lt;p&gt;前面分析了 Raft 算法是如何进行 Leader 选举以及日志复制的，但是这套机制还不能够完全保证每个节点都会严格按照相同的顺序 &lt;code&gt;apply&lt;/code&gt; 日志，这就可能造成各个节点的状态机不一致。&lt;/p&gt;
&lt;p&gt;假设有如下场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Leader 将某些日志项复制到了大多数节点上，在 &lt;code&gt;commit&lt;/code&gt; 后发生了宕机&lt;/li&gt;
&lt;li&gt;某个 Follower 尚未被复制这些日志项，但是在 Leader 挂了之后，进行的选举中，该 Follower 成为了 Leader&lt;/li&gt;
&lt;li&gt;这个新的 Leader 又同步并提交了一些新的日志，这些日志覆盖掉了其它节点上的上一任提交的日志&lt;/li&gt;
&lt;li&gt;各个节点在进行 &lt;code&gt;apply&lt;/code&gt; 时可能应用了不同的日志序列，导致出现不一致&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以要想保证各个节点状态机一致性，&lt;strong&gt;光有 Leader 选举和日志复制策略还是不够的&lt;/strong&gt;，还要有一些额外的措施，这就是本小节要讨论的安全性限制策略。&lt;/p&gt;
&lt;h4&gt;5.1 对选举的限制&lt;/h4&gt;
&lt;p&gt;回顾上述场景，为什么会出现日志被错误地覆盖，导致不一致。根本问题其实就是在第二部，一个落后的 Follower（还没被复制上一任 Leader 的最新日志）就当选了新的 Leader。那么他接下来的操作肯定会以自己的日志为准，导致集群中其他节点的日志被覆盖掉。所以这个 Candidate 来竞选 Leader 其实是不合格的，&lt;strong&gt;Candidate 必须有足够的资格才能当选 leader&lt;/strong&gt;，所以在 Candidate 发起选举投票的时候，可以加一个条件限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;⚠️&lt;strong&gt;每个 Candidate 发起投票 RPC 请求时必须在请求体中包含自己本地日志最新的任期编号（&lt;code&gt;term&lt;/code&gt;）和索引值（&lt;code&gt;index&lt;/code&gt;）当 Follower 收到 Candidate 的投票请求时，如果发现该 Candidate 的日志还没有自己的新，则拒绝投票给该 Candidate&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🌟在加了这个条件之后，再结合上本身 Candidate 就必须赢得集群大多数节点的投票才会成为 Leader，同时一条日志只有复制到了大多数节点才能被 commit，所以 Leader 就一定拥有所有 committed 日志。也就是说：Follower 不可能比 leader 多出一些 committed 日志。&lt;/p&gt;
&lt;p&gt;比较日志新旧的策略也很简单：&lt;code&gt;(term, index)&lt;/code&gt; 比较，先比较 term， term 更大的日志更新，term 相同的话，index 大的日志更新。&lt;/p&gt;
&lt;h4&gt;5.2 对提交的限制&lt;/h4&gt;
&lt;p&gt;单独的对选举加一定限制还不能保证日志的正确性，不正确的提交（commit）同样会带来问题。回顾一下 commit 的作用：&lt;/p&gt;
&lt;p&gt;当 leader 得知某条日志项被成功复制到集群的大多数节点后，就可以进行 commit，表明该日志项可以被 apply 生效到状态机，committed（已提交） 日志项一定最终会被状态机 apply。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是不正确的 commit 也可能带来日志覆盖的问题&lt;/strong&gt;，考虑如下场景：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410201721586.png&quot; alt=&quot;image-20241020172124036&quot;&gt;&lt;/p&gt;
&lt;p&gt;图中的方框内的数字表示该日志项的任期 term，对应坐上面一栏的数字表示该日志项的索引值 index，一条日志项用（&lt;code&gt;term&lt;/code&gt;，&lt;code&gt;index&lt;/code&gt;）表示，从左到右随着时间集群状变更如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;阶段 a&lt;/strong&gt;：S1 是 leader，收到请求后将日志项(2, 2) 只复制给了 S2，尚未复制给 S3，S4，S5&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阶段 b&lt;/strong&gt;：S1 宕机，S5 选举获取了 S3、S4、S5 三票，当选任期（term）为 3 的 leader，收到客户端请求后保存了 日志项（3，2），尚未复制给任何节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阶段 c&lt;/strong&gt;：S5 宕机，S1 恢复，S1 重新当选 term 为 4 的 leader，继续将日志项 (2, 2) 复制给了 S3，已经满足大多数节点（S1，S2，S3），于是 S1 将该日志项 commit&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阶段 d&lt;/strong&gt;：S1 又宕机，S5 恢复，S5 选举获得了 S2、S3、S4 三票，重新当选 ，将 日志项(3, 2) 复制给了所有节点并 commit。注意，此时发生了日志覆盖错误，已经 committed 的 日志项(2, 2) 被 (3, 2) 覆盖了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了避免这个错误，需要在日志的提交阶段也加一个限制：&lt;/p&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Leader 只允许 commit 包含当前任期 (term) 的日志&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;假设有了这个限制，再来模拟一下上述场景，在加了这个限制后，其实上述阶段的阶段 c 就出错了，阶段 c 虽然 S1 恢复当选了 term4 的 Leader，但是其并不能直接将日志项（2，2）commit，因为 S1 当前的日志为（4，3），必须等到（4，3）成功复制后才能 commit。&lt;/p&gt;
&lt;p&gt;一旦有了这个限制，在阶段 c 就只存两种情况了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;日志项（2，2）始终没有被 commit，这样 S5 在阶段 d 将其覆盖就是安全的&lt;/li&gt;
&lt;li&gt;日志项（2，2）连同（4，3）一起被成功 commit，这样的话，&lt;strong&gt;在阶段 d，S5 就无法成功当选 Leader（对选举的限制，当 Follower 收到 Candidate 的投票请求时，如果发现该 Candidate 的日志还没有自己的新，则拒绝投票给该 Candidate）&lt;/strong&gt;，就不存在上述问题了&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6. 节点变更问题&lt;/h3&gt;
&lt;p&gt;集群中的节点数量并不是恒定不变的，比如随着业务的发展，集群需要扩容或者是缩容，那么就需要适当的增加或者是减少机器节点，又或者是某些几点出现了故障，需要变更机器等等，都需要变更集群的节点数量。Raft 算法如何处理集群成员节点变更的问题呢？&lt;/p&gt;
&lt;h4&gt;6.1 配置&lt;/h4&gt;
&lt;p&gt;在介绍节点变更过程之间，需要先明确一个概念：配置（configuration）&lt;/p&gt;
&lt;p&gt;在 Raft 算法中，使用用配置来表示集群的节点集合，比如某个集群由 A、B、C 三个节点构成，那么集群的配置就是 &lt;code&gt;[A, B, C]&lt;/code&gt;，在稳定的状态下，所有节点的配置都相同。从这里就可以知道，每个节点是通过这个配置信息来获取集群状态的，比如在选举，日志同步过程中，集群中有哪几个 Follower，Leader 需要向哪几个节点发送 RPC 通信都需要通过配置来获取。&lt;/p&gt;
&lt;h4&gt;6.2 节点变更可能带来的问题&lt;/h4&gt;
&lt;p&gt;集群中节点的变更很有可能给集群的一致性带来影响，主要是会影响集群的多数派。我们知道在 Raft 中很多场合都需要多数派的支持，比如在投票中，只有当一个节点收到多数派投票（超过半数）才会成为 Leader，在日志同步中，只有当 Leader 确认将日志项成功复制到多数派（超过半数）节点后，会将该日志项标记为 committed，类似的场景还有很多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而集群节点的变更最主要的就是会影响到多数派&lt;/strong&gt;，比如在一个三个节点的集群中原本只要 2 个节点就可以达到多数派，假设现在往集群新增两个节点，则需要三个节点才能达到多数派。&lt;/p&gt;
&lt;p&gt;在 Raft 集群中，同样是由 Leader 节点负责同步集群的配置信息，当集群中出现节点变更，几乎不能保证所有节点同时进行配置的变更，由于网络先后等因素导致一部分节点配置已变更，另一部分没有变更在所难免，所以就会导致集群中部分节点使用的新的配置信息 &lt;code&gt;C_new&lt;/code&gt;，而有的节点使用老的配置信息 &lt;code&gt;C_old&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;假设有如下场景中，原来集群有三个节点 &lt;code&gt;[server1,server2,server3]&lt;/code&gt;，现在向集群新增了两个节点 server4 和 server5.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410201732382.png&quot; alt=&quot;image-20241020173239227&quot;&gt;&lt;/p&gt;
&lt;p&gt;当处于画框的时间点时，假如此时出发了选举，server1 和 server2 用的是旧的配置文件 &lt;code&gt;C_old&lt;/code&gt;，因此他们会从节点 &lt;code&gt;[server1,server2]&lt;/code&gt; 中选出 Leader；而节点 server3，server4 和 server5 已经是新的配置文件 &lt;code&gt;C_new&lt;/code&gt; 了，他们会从节点 &lt;code&gt;[server3,server4,server5]&lt;/code&gt; 中选出新的 Leader。此时集群就可能会有两个 Leader，&lt;strong&gt;出现脑裂问题&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;6.3 节点变更策略&lt;/h4&gt;
&lt;p&gt;前面分析了在集群变更的时候很可能导致集群的一致性出现问题，那又没有什么策略可以解决这个问题呢？主要有以下这几种解决方案。&lt;/p&gt;
&lt;h5&gt;6.3.1 串行更新&lt;/h5&gt;
&lt;p&gt;这种方法就是先将集群原来所有节点关闭，更新其配置后，再启动新的集群，显然这种方法很安全，可以保证集群始终只有一个 Leader，但是这种方法会导致每次成员变更时都需要关闭集群，导致集群无法对外提供服务，对于高可用的业务场景显然不适用。&lt;/p&gt;
&lt;h5&gt;6.3.2 单节点变更&lt;/h5&gt;
&lt;p&gt;每一次集群的变动只能新增或者删除一个节点，假设集群需要变更多个节点，那么需要分多个步骤来完成，每次只变更一个节点。比如原集群有 3 个节点，先需要扩容到 5 个节点，那么需要分两步，第一步扩充到 4 个节点，再由 4 个节点增加到 5 个节点，所以单节点变更法也叫单步成员变更法。&lt;/p&gt;
&lt;p&gt;详细步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端向 Leader 提交一个集群成员变更请求，请求的内容新增或者删除节点，以及服务节点的地址信息&lt;/li&gt;
&lt;li&gt;Leader 在收到请求之后，向本地日志中追加一条配置信息志，其中包含了新的集群配置信息 C_new，之后，这个新的配置信息会随着 RPC 请求（AppendEntries）同步给所有的 Follower 节点。&lt;strong&gt;注意：配置信息日志被添加到日志中是立即生效（不需要 commit 之后再生效）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当配置信息日志被复制到新的配置信息 C_new 所标识的所有节点的多数派节点后，就 commit 该日志&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;提交配置日志的作用&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;日志提交之后，才可以响应客户端，完成集群节点变更&lt;/li&gt;
&lt;li&gt;标志着本轮节点变更已结束，可以开始下一轮的节点变更&lt;/li&gt;
&lt;li&gt;如果集群中有删除节点，那么提交日志之后，被删除的节点可以关机了&lt;/li&gt;
&lt;/ol&gt;
&lt;h6&gt;6.3.2.1 单步成员变更法为什么可以解决集群节点变更带来的脑裂问题呢？&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;这里可以枚举出奇偶节点情况下，新增或者删除节点的情况&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410201738455.png&quot; alt=&quot;image-20241020173822354&quot;&gt;&lt;/p&gt;
&lt;p&gt;从上图可以看出，不管原集群节点数是奇数还是偶数，也不管是在原集群上新增一个节点还是删除一个节点，在集群的节点数变更之后，原集群的多数派和新集群的多数派一定存在交集，那么再同一个任期内，原集群 C_old 和新集群 C_new 中交集的那一个节点只会进行一次投票，要么投票给 C_old，要么投票给 C_new，这样就避免可同一任期出现在两个 Leader 的现象。&lt;/p&gt;
&lt;p&gt;需要注意的是：单节点变更法虽然简单，很好理解，但是也有其缺陷，这种方式在串行化的方式下可以保证一个集群只能有一个 Leader，&lt;strong&gt;但是并发执行单节点变更，可能会出现一次单节点变更还没完成，新一次单节点变更已经执行，导致集群出现脑裂问题&lt;/strong&gt;，这里不过多阐述，感兴趣的话可以去看 &lt;a href=&quot;https://web.stanford.edu/~ouster/cgi-bin/papers/OngaroPhD.pdf&quot;&gt;Raft 论文&lt;/a&gt;。&lt;/p&gt;
&lt;h5&gt;6.3.3 两阶段切换集群成员配置&lt;/h5&gt;
&lt;p&gt;虽然 Raft 论文中认为单步变更是更简单的办法，但节点变更有一定的问题，但是现在主流的实现都使用了 Joint Consensus（联合共识）算法来完成集群变更，也就是小标题所说的两阶段切换集群成员配置。&lt;/p&gt;
&lt;p&gt;具体流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;阶段一&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;客户端将新配置 &lt;code&gt;C_new&lt;/code&gt; 发送给 Leader，Leader 取旧配置 &lt;code&gt;C_old&lt;/code&gt; 和新配置 &lt;code&gt;C_new&lt;/code&gt; 的并集（称为联合配置（表示为 &lt;code&gt;C_old,new&lt;/code&gt;））并立即 apply 即生效&lt;/li&gt;
&lt;li&gt;Leader 将配置 &lt;code&gt;C_old,new&lt;/code&gt; 包装成日志通过 AppendEntries 请求复制到 Follower 节点&lt;/li&gt;
&lt;li&gt;Follower 收到 &lt;code&gt;C_old,new&lt;/code&gt; 后立即生效，立刻应用该配置作为当前节点的配置，当 &lt;code&gt;C_old,new&lt;/code&gt; 的大多数节点（&lt;strong&gt;即 &lt;code&gt;C_old&lt;/code&gt; 的大多数节点和 &lt;code&gt;C_new&lt;/code&gt; 的大多数节点&lt;/strong&gt;）都切换后，leader 将 commit 该日志&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阶段二&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;紧接着 Leader 将新配置 &lt;code&gt;C_new&lt;/code&gt; 包装成日志通过 AppendEntries 请求复制到 Follower 节点&lt;/li&gt;
&lt;li&gt;Follower 收到 &lt;code&gt;C_new&lt;/code&gt; 后立即生效，如果此时发现自己不在 &lt;code&gt;C_new&lt;/code&gt; 列表，则主动退出集群&lt;/li&gt;
&lt;li&gt;Leader 确认 &lt;code&gt;C_new&lt;/code&gt; 的大多数节点都切换成功后，给客户端发送执行成功的响应&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;几个概念详细解释一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C_old,new&lt;/strong&gt;：比如 C_old 为 &lt;code&gt;[A, B, C]&lt;/code&gt;，C_new 为 &lt;code&gt;[B, C, D]&lt;/code&gt;，那么 &lt;code&gt;C_old,new&lt;/code&gt; 就为他们的并集 &lt;code&gt;[A, B, C, D]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C_old,new 的大多数节点&lt;/strong&gt;：是指 C_old 中的大多数和 C_new 中的大多数，如下表所示，第一行因为 C，D 节点还没有被复制到日志，导致 C_new 的多数派不能达成，所以该日志不能被 commit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410201744969.png&quot; alt=&quot;image-20241020174404877&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410201744293.png&quot; alt=&quot;image-20241020174420174&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图展示了用两阶段提交方法集群节点变更过程中的几个过渡期：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚线：表示已经创建但尚未 commit 的成员配置日志&lt;/li&gt;
&lt;li&gt;实线：表示 committed 的成员配置日志&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在每一个时期，每一个任期下都不可能出现两个 Leader.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原因如下&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;阶段一：C_old,new 日志尚未 commit&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在这个阶段，集群中节点可能处于旧配置 C_old 下，也有可能处于联合配置 C_old,new 下，但无论这两种情况的哪一种，只要原 leader 发生宕机，新 leader 都必须得到旧配置 C_old 下大多数节点的投票，所以不会出现两个 Leader&lt;/li&gt;
&lt;li&gt;再次强调一下：C_old 节点发起选举需要 C_old 的大多数，C_old,new 发起选举需要 C_old 和 C_new 两者的大多数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;阶段二： C_old,new 已经 commit，C_new 下发之前&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在这个阶段，C_old,new 已经被 commit，表示联合配置 C_old,new 已经被应用到了集群的大多数节点上（C_old 的大多数节点和 C-new 的大多数节点），因此当 leader 宕机时，新选出的 leader 一定是已经拥有 C_old,new 的节点，否则票数通不过，所以不可能出现两个 leader&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;阶段三： C_new 已经下发，但尚未 commit&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在这个阶段，集群中可能有三种节点，集群中节点可能处于旧配置 C_old 下，也有可能处于联合配置 C_old,new 下，还有可能处于新配置 C_new 下，但由于已经经历了阶段 2，因此 C_old 节点不可能再成为 leader。而无论是 C_old,new 还是 C_new 节点发起选举，都需要经过大多数 C_new 节点的同意，因此也不可能出现两个 leader&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;阶段四：C_new 已经 commit&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在这个阶段，C_new 已经被 commit，因此只有 C_new 节点可以得到大多数选票成为 leader，所以也不会出现两个 Leader，至此，集群已经安全地完成了这轮变更，可以继续开启下一轮变更了&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7. 小结&lt;/h3&gt;
&lt;p&gt;Raft 算法将共识问题分解成了多个相对独立的子问题，从而简化了共识的实现。其主要流程包括领导者选举以及日志复制，集群先选举出 leader，然后 leader 负责复制、提交日志。当然为了在任何异常情况下系统不出错，还需要满足一定的安全性，以及需要对 Leader Election，Log Replication 两个子问题加一些限制条件。最后集群都是动态变化的，所以 Raft 算法也应用了单节点变更以及联合共识机制来保证集群节点安全的变更。&lt;/p&gt;</content:encoded><h:img src="/_astro/202504140127607.P8IOcI_1.png"/><enclosure url="/_astro/202504140127607.P8IOcI_1.png"/></item><item><title>分布式系统｜理论基础</title><link>https://coooredump.github.io/blog/system-architecture/distributed-systems-theoretical-foundations</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/distributed-systems-theoretical-foundations</guid><description>由于分布式系统中的程序是部署在多个节点上的，各个节点通过网络通信。一旦有网络通信，就会有网络的可靠性问题，延迟问题，以及各个节点的故障问题等等。可能出现有的节点能正常工作，而有的节点挂掉，导致有的请求达到正常节点就能正常处理，而打到故障节点又会失败。又或者因为网络故障，导致有的写操作在部分节点成功，而在另一些节点失败，总之可能存在种种状态不一致的情况，这些问题的存在影响着系统在高性能，高可用等方面的设计。</description><pubDate>Sun, 13 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;分布式系统概念&lt;/h2&gt;
&lt;p&gt;单机就不说了，分辨一下「集群」和「分布式」的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;集群是相同应用的多备份部署，同一份代码，功能相同，相当于多台单机，有效负载均衡，不过耦合度太高。&lt;/li&gt;
&lt;li&gt;分布式则是将业务拆成了多个不同的子业务，每个子业务有单独的服务部署，这些子业务构成一个完整的系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;熟悉了分布式的概念，这里需要强调一下「分布式」和「微服务」的关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;微服务是一种软件设计理念，它是面向服务的，将大的系统或者是服务，拆分成一组小的且相互独立的服务。这些小的服务都是都是独立运行的，并且每个小服务有自己的数据库、业务逻辑，服务之间通过网络进行通信和协作。这组小服务一起协作对外提供服务，共同完成一个系统的任务。&lt;/li&gt;
&lt;li&gt;微服务其实是一种特定的分布式系统架构风格，分布式是一种系统架构的范畴，而微服务是分布式系统的一种具体实现方式，微服务将应用程序拆分成小的、自治的服务单元，通过网络进行通信和协作来完成系统任务。分布式有很多种实现方式，微服务并不是它唯一的实现方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;分布式理论基础&lt;/h2&gt;
&lt;p&gt;由于分布式系统中的程序是部署在多个节点上的，各个节点通过网络通信。一旦有网络通信，就会有网络的可靠性问题，延迟问题，以及各个节点的故障问题等等。可能出现有的节点能正常工作，而有的节点挂掉，导致有的请求达到正常节点就能正常处理，而打到故障节点又会失败。又或者因为网络故障，导致有的写操作在部分节点成功，而在另一些节点失败，总之可能存在种种状态不一致的情况，这些问题的存在影响着系统在高性能，高可用等方面的设计。而随着分布式的发展，人们总结出了一套分布式设计理论，这套理论对于后来分布式系统设计有着指导意义，这就是大名鼎鼎的 CAP 理论。&lt;/p&gt;
&lt;h3&gt;CAP 原理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;更多：&lt;a href=&quot;https://ruanyifeng.com/blog/2018/07/cap.html&quot;&gt;阮一峰｜CAP 定理&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1. CAP 理论由来&lt;/h4&gt;
&lt;p&gt;Lynch 教授提出了一个影响深远的概念，如在一个不稳定（&lt;strong&gt;要么消息错乱，要么消息丢失&lt;/strong&gt;）的网络环境里（&lt;strong&gt;异构模型&lt;/strong&gt;），想始终保持数据一致是不可能的，这个概念为后来 CAP 理论的发展奠定了重要基础。&lt;/p&gt;
&lt;p&gt;在 CAP 理论出来之前，并没有一个明确的方向性的指导。所以在设计实现分布式系统时会显得很混乱。可能在分布式系统中，不同的子模块有着不同的设计标准，对同一个系统而言，这显然不是很合理&lt;/p&gt;
&lt;p&gt;比如在一个由两个模块构成的分布式系统中，这两个模块 A 和 B 之间能够互相通信&lt;/p&gt;
&lt;p&gt;A 模块的设计原则是：A 发送请求给其他模块，如果节点间出现了故障，会选择不断的重试，一直等到节点通信恢复。可以理解为提供高质量服务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410122204115.png&quot; alt=&quot;image-20241012220449009&quot;&gt;&lt;/p&gt;
&lt;p&gt;B 模块的设计原则是：B 发送请求给其他模块，如果节点间出现了故障，会选择直接断开，并记下当前状态，等待后续处理，可以理解为提供高可用服务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410122205604.png&quot; alt=&quot;image-20241012220500548&quot;&gt;&lt;/p&gt;
&lt;p&gt;显然，系统中如果出现了通信故障，A 往 B 发请求，会出现不断重试，而 B 往 A 发请求，则会出现直接断开的情况，整个系统会很混乱。&lt;/p&gt;
&lt;p&gt;所以，IT界的研究者们长期以来一直在探索指导原则来指导分布式系统的设计，在 2000 年 Eric Brewer 教授在 PODC 会议上首次提出了 CAP 理论。然而，当时这一理论还没有被正式证明，所以只能称为 CAP 猜想。&lt;/p&gt;
&lt;p&gt;这个猜想一经提出，就在业界引起了极大的关注，因为它为分布式系统设计提供了一个简单而有力的框架。到了 2002 年，Seth Gilbert 和 Nancy Lynch 通过理论证明了 CAP 猜想的正确性，从而使 CAP 理论正式确立，并成为分布式系统设计中的重要理论基础之一。&lt;/p&gt;
&lt;h4&gt;2. CAP 理论&lt;/h4&gt;
&lt;p&gt;在一个分布式系统中，一致性（&lt;strong&gt;Consistency&lt;/strong&gt;）、可用性（&lt;strong&gt;Availability&lt;/strong&gt;）和分区容错性（&lt;strong&gt;Partition Tolerance&lt;/strong&gt;）这三个目标无法同时满足，最多只能同时满足其中的两个。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410122205749.png&quot; alt=&quot;image-20241012220515676&quot;&gt;&lt;/p&gt;
&lt;h5&gt;2.1 数据一致性（C）&lt;/h5&gt;
&lt;p&gt;「数据一致性」是指在分布式系统中的所有节点，在同一时间点上拥有相同的数据副本。比如某个节点数据有更新，那么其他的节点数据要跟着更新，要求所有的读请求都必须读到这个新数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写操作（增、删、改）：数据变更&lt;/li&gt;
&lt;li&gt;读操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当数据服务有多个节点的时候，当数据服务接收到写请求时，怎样判定数据服务上所有的节点都一起发生了变更呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当然有的分布式系统由不同的策略配置，比如 MySQL 的主从集群，当配置为&lt;strong&gt;全同步&lt;/strong&gt;时，即每次写数据只有当主库把数据都同步到从库后，才会返回成功，这样返回写成功后，称之为&lt;strong&gt;数据一致性改变&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果不是这种配置策略，比如&lt;strong&gt;半同步&lt;/strong&gt;，只有部分从库同步完了数据，也会返回成功，那么从从库读数据的话，可能就会读取不到新值，就不能称之为数据一致性改变。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设「一主两从」的 MySQL 集群系统，在网络通信正常情况下，如果经过一次写请求后，两个从库节点都发生了数据变化。然后，读请求把这些变化后的数据都读取到了，我们就把这次数据修改称为数据发生了一致性改变。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410122214247.png&quot; alt=&quot;image-20241012221442123&quot;&gt;&lt;/p&gt;
&lt;p&gt;假设系统内部发生了通信问题，主库和从库 2 之间的通信发生了故障，而主库和从库 1 之间的通信正常，那么写入操作成功之后，从库 1 可以读取到 V1 版本数据，而从库 2 无法读取到 V1 版本数据，地区到的还是旧数据。此时，系统中的节点就没有发生一致性改变。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410122215134.png&quot; alt=&quot;image-20241012221537030&quot;&gt;&lt;/p&gt;
&lt;h5&gt;2.2 可用性（A）&lt;/h5&gt;
&lt;p&gt;「可用性」是指系统要处于 100% 的可用状态，对于每一个请求，正常节点要&lt;strong&gt;在合理的时间给出正确的响应&lt;/strong&gt;，即系统能够正常提供服务。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须在合理时间内给出响应，时间根据业务要求来定。&lt;/li&gt;
&lt;li&gt;只要正常的节点都能做出响应（无论数据是否最新）：
&lt;ul&gt;
&lt;li&gt;如果系统内的某个节点或者是某些节点宕机了，但是其他的正常节点可以在合理的时间内做出响应。&lt;/li&gt;
&lt;li&gt;节点正常，但是节点上的数据有问题，比如不是最新数据，如果有请求达到这个节点上了，依然不能拒绝请求，要正常返回这个旧数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2.3 分区容错性（P）&lt;/h5&gt;
&lt;p&gt;「分区容错性」是指分布式系统能够在&lt;strong&gt;网络分区&lt;/strong&gt;的情况下继续运行对外提供服务，即系统能够在节点之间进行通信的网络出现故障或延迟的情况下，保证系统的正常运行。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;网络分区&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;网络分区只在分布式集群中，在分布式系统中，多个节点之间的网络本来是连通的，但是由于某些故障导致节点之间网络不通了，形成不同的子集，子集中节点之间网络互通，而子集与子集之间网络不通，整个网络就形成了几块区域，就是网络分区。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410122223041.png&quot; alt=&quot;image-20241012222321914&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图中，G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息，G2 可能无法收到。系统设计的时候，必须考虑到这种情况。&lt;/p&gt;
&lt;p&gt;一般来说，分区容错无法避免，因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们，剩下的 C 和 A 无法同时做到。&lt;/p&gt;
&lt;h4&gt;3. CAP 为什么不能同时满足&lt;/h4&gt;
&lt;p&gt;这个问题看似是论证一致性、可用性、分区容忍性为什么只能选择其中的两个同时满足，其实并非如此。因为在分布式系统中&lt;strong&gt;分区容错性（Partition-tolerance ）&lt;/strong&gt; 是&lt;strong&gt;不得不选择&lt;/strong&gt;的。假设不考虑分区容错性，那就相当于把数据只存放在一个节点上，因为数据依旧集中在一个地方，一旦这个节点出现故障，整个系统毫无疑问会随之瘫痪，这在实际的生产环境中通常是不可接受的，而且数据集中在一个地方，这本身也与分布式相矛盾。&lt;/p&gt;
&lt;p&gt;✅ 在分布式系统中，分区容错性（ P ）是一定要满足的，剩下的一致性（ C ）和可用性（ A ）只能满足其一。因此，分布式架构不可能选择 CA 架构，只能选择 CP 架构 或者 AP 架构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202410131348725.png&quot; alt=&quot;image-20241013134846229&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于存在分区容错性，所以存在 server2 写入 X=2 失败，而 server1 写入 X=2 成功，那么此时 client2 和 client3 就会读取到不一样的值，client2 读取到 X=2，而 client3 读取到 X=1，此时系统可用，但是违背了一致性，保证了 A，但是违背了 C，属于 &lt;strong&gt;AP 架构&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;假设此时要保持 X 值的一致性，server1 和 server2 的 Write 操作必须同时成功，系统必须等待 server2 也写入成功，在失败的这段时间里，系统是不可用状态，这样的话就保证了一致性 C，但是也降低了系统的可用性，违背了 A，此时属于 &lt;strong&gt;CP 架构&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由此可见，在分布式系统中，无法同时满足 CAP 定律中的 “一致性”、“可用性” 和 “分区容错性” 三者。&lt;/p&gt;
&lt;h4&gt;4. CAP 如何选择&lt;/h4&gt;
&lt;p&gt;任何方案选型都要根据业务场景出发，看业务场景适合哪种，就选哪种，一般而言：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CP 使用场景&lt;/strong&gt;：比较典型的 CP 系统是分布式数据库，数据的一致性是最基本的要求。在极端情况时，优先保证数据的强一致性，代价就是放弃系统的可用性。例如，类似 Redis、HBase 这种分布式存储系统，以及 ZooKeeper 这种分布式协调系统。另一个常见场景就是金融领域，为了资金安全一般需要确保强一致性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AP 使用场景&lt;/strong&gt;：AP 则是适应于目前大多数对于用户体验要求高的互联网应用场景，比如社交媒体，内容分发业务，像微博、Instagram。用户量大，主机众多，分布式部署，而且集群的规模越来越大，节点故障、网络故障时有发生，要保证系统的可用性，保障 AP 放弃 CP 是常见的一种做法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5. CAP 的不足&lt;/h4&gt;
&lt;p&gt;CAP 最大的不足其实也就是一致性（Consistency）、可用性（Availability）的强取舍问题。&lt;/p&gt;
&lt;p&gt;在实际应用中，&lt;strong&gt;一致性和可用性并不只是简单的二选一问题，而是取决于各自的优先级&lt;/strong&gt;。当我们强调一致性时，并不意味着系统的可用性会完全丧失。比如，在 Zookeeper 中，只有在主节点出现问题时，系统才可能会出现短暂的不可用状态，但在其他时间，系统通过各种方式来保证其可用性。同样，强调可用性时，通常也会采用技术手段来确保数据最终能够保持一致性。&lt;strong&gt;CAP 定理并没有详细说明这些细节&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;✅ 其实简单来说，就是在设计分布式系统时，对于一致性和可用性希望有一些折中的手段，而不是选择了其中一个特性，就要完全舍弃另一个特性。&lt;/p&gt;
&lt;h3&gt;BASE 原理&lt;/h3&gt;
&lt;p&gt;正因为 CAP 理论存在一些局限性，eBay 的架构师 Dan Pritchett 基于他在大规模分布式系统中的实践经验，总结出了 BASE 理论。BASE 理论是对 CAP 理论的进一步扩展，其核心思想是，即便无法实现强一致性（Strong Consistency），应用程序也可以通过适当的方法达到&lt;strong&gt;最终一致性&lt;/strong&gt;（Eventual Consistency）。&lt;/p&gt;
&lt;p&gt;BASE 理论是一个更具工程实践意义的理论，它弥补了 CAP 理论过于抽象的问题，同时也为 AP 系统提供了整体的工程实践思想。目前 BASE 理论已经成为分布式系统中的核心理论之一。&lt;/p&gt;
&lt;h4&gt;1. 数据一致性&lt;/h4&gt;
&lt;p&gt;BASE 理论对数据一致性做了一些更细致的分类，从而对 CAP 理论作了进一步的扩展。数据一致性大致可以分为以下几类：&lt;/p&gt;
&lt;h5&gt;1.1 强一致性&lt;/h5&gt;
&lt;p&gt;数据更新操作完成之后，数据立即生效，后续的&lt;strong&gt;所有访问&lt;/strong&gt;当中都能得到最新的结果。&lt;/p&gt;
&lt;h5&gt;1.2 弱一致性&lt;/h5&gt;
&lt;p&gt;数据更新操作完成之后，不要求立即可以读到最新写入的值，能容忍在更新发生之后，&lt;strong&gt;部分情况&lt;/strong&gt;下无法访问到新数据的情况。&lt;/p&gt;
&lt;h5&gt;1.3 最终一致性&lt;/h5&gt;
&lt;p&gt;数据更新操作完成之后，能容忍更新后一段时间内无法访问到最新数据，不需要实时保证系统数据的强一致性，但是经过一段时间的同步之后，最终可以达到一个一致的状态。所以，最终一致性可以看作是弱一致性的一个特例。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而在 CAP 理论中的 C 指的是强一致性&lt;/strong&gt;，所以要分析什么样的系统适合 AP，什么样的系统适合 CP，其实就是在强一致性和可用性之间做权衡，根据业务情况，看那些业务能够容忍最终一致性，而很在乎用户体验，这样的业务就适合 AP，而对于强一致性要求高的业务则适合用 CP。&lt;/p&gt;
&lt;h4&gt;2. BASE&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;BA 基本可用（Basically Available）：系统在遇到不可预知的故障时，允许损失部分可用性，就是说即使系统不能完全正常工作，但是&lt;strong&gt;仍然有部分功能可用&lt;/strong&gt;。换句话说，但至少能够提供部分服务。例如，响应时间比平时长或者某些功能暂时不可用。
&lt;ul&gt;
&lt;li&gt;响应性能变弱：比如正常情况下请求响应为 0.3s，但是出现了某个故障之后，虽然还能正常响应，但是响应时长变为了 2s&lt;/li&gt;
&lt;li&gt;系统功能有损：比如商城双十一活动时，评论模块出现故障，但不会影响交易、商品等核心模块的流程使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;S 软状态（Soft state）：指允许系统中的数据&lt;strong&gt;存在中间状态&lt;/strong&gt;，这种状态可能是不一致的，但是这种中间状态的存在不会影响系统的整体可用性，因为数据在不同节点之间的同步可能存在延迟。&lt;/li&gt;
&lt;li&gt;E 最终一致性（Eventual Consistency）：系统不要求数据实时地达到一致性，而是允许数据在一段时间后最终达到一致状态。这意味着在经过一定的时间之后，所有副本的数据会最终到达一致的状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;既然在分布式系统中分区容错性我们无法回避，而根据 CAP 理论，我们要么选择 AP 模型，要么选择 CP 模型。但是在很多的场景中，尤其是对可用性要求高的场景往往既需要可用性，又想保证一致性，这里就出现了矛盾。所以这里可以在选择可用性的同时，弱化强一致性，但是并不是永远放弃一致性，在分区故障恢复后，根据各自的业务特点，经过一段时间系统应该达到最终一致性。而&lt;strong&gt;系统中一部分不一致时，系统仍需要保持系统整体“主要可用”，也就是基本可用&lt;/strong&gt;。其实 Base 理论可以理解为：分区容错性 + 基本可用性 + 最终一致性；其实就是对 CAP 理论的一种延伸。&lt;/p&gt;</content:encoded><h:img src="/_astro/202504140127607.P8IOcI_1.png"/><enclosure url="/_astro/202504140127607.P8IOcI_1.png"/></item><item><title>动图轻松理解 Self-Attention</title><link>https://coooredump.github.io/blog/ai-infra/animated-pictures-to-easily-understand-self-attention</link><guid isPermaLink="true">https://coooredump.github.io/blog/ai-infra/animated-pictures-to-easily-understand-self-attention</guid><description>Self-Attention 是 Transformer 中最核心的思想，我们在阅读 Transformer 论文的过程中，最难理解的可能就是自注意力机制实现的过程和繁杂的公式。</description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504140113572.jpg&quot; alt=&quot;动图轻松理解Self-Attention(自注意力机制)&quot;&gt;&lt;/p&gt;
&lt;p&gt;Self-Attention 是 Transformer 中最核心的思想。我们在阅读 Transformer 论文的过程中，最难理解的可能就是自注意力机制实现的过程和繁杂的公式。本文在 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//sh-tsang.medium.com/brief-review-on-the-relationship-between-self-attention-and-convolutional-layers-509a75230478&quot;&gt;Illustrated: Self-Attention&lt;/a&gt; 这篇文章的基础上，加上了自己对 Self-Attention 的理解，力求通俗易懂。希望大家批评指正。&lt;/p&gt;
&lt;h2&gt;1. Self-Attention 是什么？&lt;/h2&gt;
&lt;p&gt;我们再来讲解一个重要的概念，即 &lt;strong&gt;query&lt;/strong&gt;、&lt;strong&gt;key&lt;/strong&gt; 和 &lt;strong&gt;value&lt;/strong&gt;。这三个词翻译成中文就是查询、键、值，看到这中文的意思，还是迷迷糊糊的。我们来举个例子：小明想在 b 站搜索深度学习，他把深度学习四个字输入到搜索栏，按下搜索键。搜索引擎就会将他的查询 query 映射到数据库中相关的标签 key，如吴恩达、神经网络等等，然后向小明展示最匹配的结果 value。&lt;/p&gt;
&lt;p&gt;最后我们来说说 Self-Attention。和 Attention 类似，他们都是一种注意力机制。不同的是 &lt;strong&gt;Attention 是 source 对 target，输入的 source 和输出的 target 内容不同&lt;/strong&gt;。例如英译中，输入英文，输出中文。&lt;strong&gt;而 Self-Attention 是 source 对 source，是 source 内部元素之间或者 target 内部元素之间发生的 Attention 机制&lt;/strong&gt;，也可以理解为 Target=Source 这种特殊情况下的注意力机制。&lt;/p&gt;
&lt;p&gt;下面我们通过一个简单的例子，来了解 Self-Attention 的计算步骤。&lt;/p&gt;
&lt;h2&gt;2. 计算步骤&lt;/h2&gt;
&lt;h3&gt;2.1 定义 input&lt;/h3&gt;
&lt;p&gt;在进行 Self - Attention 之前，我们首先定义 3 个 1×4 的 input。 pytorch 代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import torch
x = [
    [1, 0, 1, 0],  # input 1
    [0, 2, 0, 2],  # input 2
    [1, 1, 1, 1]   # input 3
    ]
x = torch.tensor(x, dtype=torch.float32)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 初始化权重&lt;/h3&gt;
&lt;p&gt;每个 input 和三个权重矩阵分别相乘会得到三个新的矩阵，分别是 key(橙色)，query(红色)，value(紫色)。我们已经令 input 的 shape 为 1×4，key、query、value 的 shape 为 1×3，因此可以推出与 input 相乘的权重矩阵的 shape 为 4×3。 代码如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这三个不同的权重矩阵（$W_Q、W_K、W_V$）是通过神经网络模型的训练过程自动学习而来的。在自注意力机制中，这些矩阵是模型参数的一部分，它们的初值通常是随机初始化的。然后，通过训练数据和反向传播算法，模型会逐渐调整这些矩阵的值，以最小化预测误差（比如分类任务中的交叉熵损失）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;w_key = [
  [0, 0, 1],
  [1, 1, 0],
  [0, 1, 0],
  [1, 1, 0]
]  
w_query = [
  [1, 0, 1],
  [1, 0, 0],
  [0, 0, 1],
  [0, 1, 1]
]
w_value = [
  [0, 2, 0],
  [0, 3, 0],
  [1, 0, 3],
  [1, 1, 0]
]
w_key = torch.tensor(w_key, dtype=torch.float32)
w_query = torch.tensor(w_query, dtype=torch.float32)
w_value = torch.tensor(w_value, dtype=torch.float32)

print(&quot;Weights for key: \n&quot;, w_key)
print(&quot;Weights for query: \n&quot;, w_query)
print(&quot;Weights for value: \n&quot;, w_value)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 计算 key, query 和 value&lt;/h3&gt;
&lt;p&gt;现在我们计算 key, query 和 value 矩阵的值，计算的过程也很简单，运用矩阵乘法即可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;key = input * w_key;&lt;/li&gt;
&lt;li&gt;query = input * w_query;&lt;/li&gt;
&lt;li&gt;value = input * w_value;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;keys = x @ w_key
querys = x @ w_query
values = x @ w_value

print(&quot;Keys: \n&quot;, keys)
# tensor([[0., 1., 1.],
#         [4., 4., 0.],
#         [2., 3., 1.]])

print(&quot;Querys: \n&quot;, querys)
# tensor([[1., 0., 2.],
#         [2., 2., 2.],
#         [2., 1., 3.]])
print(&quot;Values: \n&quot;, values)
# tensor([[1., 2., 3.],
#         [2., 8., 0.],
#         [2., 6., 3.]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403301514309.gif&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2.4 计算 attention scores&lt;/h3&gt;
&lt;p&gt;例如：为了获得 input1 的注意力分数 (attention scores)，我们将 input1 的 query(红色)与 input1、2、3 的 key(橙色) 的转置分别作点积，得到 3 个 attention scores(蓝色)。 同理，我们也可以得到 input2 和 input3 的 attention scores。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;attn_scores = querys @ keys.T
print(attn_scores)

# tensor([[ 2.,  4.,  4.],  # attention scores from Query 1
#         [ 4., 16., 12.],  # attention scores from Query 2
#         [ 4., 12., 10.]]) # attention scores from Query 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403301515895.gif&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2.5 对 attention scores 作 softmax&lt;/h3&gt;
&lt;p&gt;上一步得到了 attention scores 矩阵后，我们对 attention scores 矩阵作 softmax 计算。softmax 的作用为归一化，使得其中各项相加后为 1。这样做的好处是凸显矩阵中最大的值并抑制远低于最大值的其他分量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from torch.nn.functional import softmax

attn_scores_softmax = softmax(attn_scores, dim=-1)
print(attn_scores_softmax)
# tensor([[6.3379e-02, 4.6831e-01, 4.6831e-01],
#         [6.0337e-06, 9.8201e-01, 1.7986e-02],
#         [2.9539e-04, 8.8054e-01, 1.1917e-01]])

attn_scores_softmax = [
  [0.0, 0.5, 0.5],
  [0.0, 1.0, 0.0],
  [0.0, 0.9, 0.1]
]
attn_scores_softmax = torch.tensor(attn_scores_softmax)
print(attn_scores_softmax)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403301515966.gif&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2.6 将 attention scores 与 values 相乘&lt;/h3&gt;
&lt;p&gt;每个 score(蓝色)乘以其对应的 value(紫色)得到 3 个 alignment vectors(黄色)。在本教程中，我们将它们称为 weighted values(加权值)。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;weighted_values = values[:,None] * attn_scores_softmax.T[:,:,None]
print(weighted_values)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403301519982.gif&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2.7 对 weighted values 求和得到 output&lt;/h3&gt;
&lt;p&gt;从图中可以看出，每个 input 生成 3 个 weighed values(黄色)，我们将这 3 个 weighted values 相加，得到 output(深绿)。图中一共有 3 个 input，所以最终生成 3 个 output。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;outputs = weighted_values.sum(dim=0)
print(outputs)

# tensor([[2.0000, 7.0000, 1.5000],  # Output 1
#         [2.0000, 8.0000, 0.0000],  # Output 2
#         [2.0000, 7.8000, 0.3000]]) # Output 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403301518558.gif&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3. 回到论文&lt;/h2&gt;
&lt;p&gt;我们在 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//proceedings.neurips.cc/paper_files/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf&quot;&gt;Attention is all you need&lt;/a&gt; 这篇论文中，可以看到这样一个公式：&lt;/p&gt;
&lt;p&gt;$$
Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V
$$
其实，这个公式就是描述了我们上面计算的过程。我们首先将 Query 与 Key 的转置作点积，然后将结果除以 $\sqrt{d_k}$ ，再作 softmax 计算，最后将计算的结果与 Value 作矩阵乘法得到 output。&lt;/p&gt;
&lt;p&gt;这里有一个点，就是为什么要除以 $\sqrt{d_k}$，$d_k$ 表示的是词向量的维度。我们除以 $\sqrt{d_k}$ 是为了防止 $QK^T$ 值过大，导致 softmax 计算时上溢出 (overflow)。其次，使用 $d_k$ 可以使 $QK^T$ 的结果满足期望为 0，方差为 1 的分布。&lt;/p&gt;
&lt;h2&gt;4. 为什么这样计算？&lt;/h2&gt;
&lt;p&gt;最后的问题是，为什么要像公式那样计算呢？&lt;/p&gt;
&lt;p&gt;我们先从 $QK^T$ 看起，从几何角度看，点积是两个向量的长度与它们夹角余弦的积。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果两向量夹角为 90°，那么结果为 0，代表两个向量线性无关。&lt;/li&gt;
&lt;li&gt;如果两个向量夹角越小，两向量在方向上相关性也越强，结果也越大。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;点积反映了两个向量在方向上的相似度，结果越大越相似。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403301524780.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;对 $QK^T$ 进行相似度的计算后，再使用 softmax 归一化。最后将归一化的结果与 $V$ 作乘法，&lt;strong&gt;计算的结果就是输入经过注意力机制加权求和之后的表示&lt;/strong&gt;。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ChatGPT 是如何处理文字输入的？</title><link>https://coooredump.github.io/blog/ai-infra/how-does-chatgpt-handle-text-input</link><guid isPermaLink="true">https://coooredump.github.io/blog/ai-infra/how-does-chatgpt-handle-text-input</guid><description>在 ChatGPT 计算处理完之后，也需要将结果再做逆转换，形成文字形式，反馈给用户。这种转换包括 Tokenizer 和 Embedding，本文要介绍这两个模块。</description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我们清楚了 ChatGPT 模型的输入和输出，实际上就是将文字输入 ChatGPT 模型当中，然后再让模型预测出文字，本质上就是一个“文字接龙”式的&lt;strong&gt;语言模型&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而文字在进入 ChatGPT 模型之前，需要先经过一个转换，形成另外一种数据形式。在 ChatGPT 计算处理完之后，也需要将结果再做逆转换，形成文字形式，反馈给用户。这种转换包括两个步骤，Tokenizer 和 Embedding。本文主要介绍这两个模块。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403300027011.awebp&quot; alt=&quot;4-1.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Tokenizer&lt;/h2&gt;
&lt;p&gt;ChatGPT 官方目前已经开始对服务收费了，收费方式主要是计算用户使用的 token 数，数量越多，收费越高。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;例如，用户提问了一条文本，文字（带标点和各种特殊符号）共有 50 个字符，但耗费了 30 个 token，ChatGPT 根据输入生成一条回答，总计 200 个 token，逆转换为文字总共 300 个字，那么用户一共消费的 token 数就是 30+200=230 个。那什么是 token 呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;token 是任何 NLP 神经网络 模型接收用户输入的最小粒度。&lt;/strong&gt; token 本身就是一些字符的组合，如英文单词&lt;code&gt;#cat&lt;/code&gt;、中文词汇&lt;code&gt;鞋子&lt;/code&gt;、英文词缀&lt;code&gt;ly&lt;/code&gt;、中文汉字&lt;code&gt;珂&lt;/code&gt;等，都可以看作是一个 token。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;将用户输入的文本转换为 token 序列的过程就叫做 Tokenizer&lt;/strong&gt;。它包含两部分，一部分是从文字转换为 token（设置在进入 ChatGPT 之前），另一部分是将 token 转换为文字，也就是逆转换（设置在 ChatGPT 模型输出之后）。&lt;/p&gt;
&lt;h3&gt;Tokenizer 算法 BPE 执行流程&lt;/h3&gt;
&lt;p&gt;Tokenizer 目前最流行的实现方法是 &lt;strong&gt;字符对编码&lt;/strong&gt; &lt;strong&gt;BPE（Byte Pair Encoding） 算法&lt;/strong&gt;，它也是 ChatGPT 采用的算法。BPE 算法是根据一份 token 词表（Vocabulary），将输入的文本拆解成若干个 token。其中，每一个 token 都存在于词表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403300036542.awebp&quot; alt=&quot;4-2.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;具体以如下一条输入模型的文本为例：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The newest car has a lower price and the lowest fuel.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这条文本中，包含了 53 个字符（包含字母、空格和标点，以及任何键盘可以打出的特殊符号，均输入 ChatGPT 中）。&lt;/p&gt;
&lt;p&gt;一般地，模型训练所使用的词表中 token 数量大约从几万~几十万不等。假设 BPE 算法已经生成一个 &lt;strong&gt;Token 词表（Vocabulary）&lt;/strong&gt; ，其部分词表 token 内容如下：&lt;/p&gt;
&lt;p&gt;| #low      | est    | #new | er   | #the  | #car |
| --------- | ------ | ---- | ---- | ----- | ---- |
| #and      | #fuel  | #a   | #has | #have | and  |
| #thailand | #price | #dog | #old | #most | ...  |&lt;/p&gt;
&lt;p&gt;BPE 算法就是根据上述 token 词表对文本进行匹配，从文本中拆分出存在于词表中的 token，将文本转换成如下的形式：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The newest car has a lower price and the lowest fuel.&lt;/p&gt;
&lt;p&gt;==&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;#The&lt;/code&gt;, &lt;code&gt;#new&lt;/code&gt;, &lt;code&gt;est&lt;/code&gt;, &lt;code&gt;#car&lt;/code&gt;, &lt;code&gt;#has&lt;/code&gt;, &lt;code&gt;#a&lt;/code&gt;, &lt;code&gt;#low&lt;/code&gt;, &lt;code&gt;er&lt;/code&gt;, &lt;code&gt;#price&lt;/code&gt;, &lt;code&gt;#and&lt;/code&gt;, &lt;code&gt;#the&lt;/code&gt;, &lt;code&gt;#low&lt;/code&gt;, &lt;code&gt;est&lt;/code&gt;, &lt;code&gt;#fuel&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在这条例子中，文本被拆分成 15 个 token。由于英文单词是以空格形式进行分割的，因此，每一个单词的首字母都添加&lt;code&gt;#&lt;/code&gt;为单词起始的标识，它可以理解为一个空格，&lt;strong&gt;不加&lt;code&gt;#&lt;/code&gt;的token表示无法独立成词&lt;/strong&gt;。一些单词被拆分成若干部分，如&lt;code&gt;newest&lt;/code&gt;被拆分成两部分，&lt;code&gt;#new&lt;/code&gt; 和 &lt;code&gt;est&lt;/code&gt;。然后，模型就接收这样的 token 数据做进一步处理计算。&lt;/p&gt;
&lt;p&gt;从上面的例子中，我们可以看出，token 中一般都是以非常&lt;strong&gt;高频&lt;/strong&gt;的字符组合构成的，而且这些 token 往往具备一定的语义。例如，&lt;code&gt;newest&lt;/code&gt;被拆解为&lt;code&gt;#new&lt;/code&gt; 和 &lt;code&gt;est&lt;/code&gt;，前半部分是单词词根，后半部分是英文形容词最高级。&lt;/p&gt;
&lt;p&gt;同样地，ChatGPT 模型在回答用户问题，输出答案时，也是首先输出 token 序列，再将 token 序列反转为正常的自然语言文本，这个操作叫做 &lt;strong&gt;De-tokenization&lt;/strong&gt;。它与 Tokenization 是完全&lt;strong&gt;互逆&lt;/strong&gt;的操作。读者可以尝试把上面的 token 序列合并成完整的文本句子。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于中文而言，常用汉字大约为 7000 个左右，而且文本之间不存在空格。因此，也可以采用上述的算法来完成，唯一的区别就是中文的 token 的开头不添加 &lt;code&gt;#&lt;/code&gt; 符号。一些极为常见的中文单词可能合并为一个 token，如&lt;code&gt;我们&lt;/code&gt;，而考虑到词频，绝大多数中文依然以单字独立成 token。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Byte-level BPE 算法&lt;/h3&gt;
&lt;p&gt;之前介绍的 BPE 算法是基于&lt;strong&gt;字符&lt;/strong&gt;的，除此之外，还有一种基于&lt;strong&gt;字节&lt;/strong&gt;的 BPE 算法（Byte-level BPE）。这种方法，主要是为了克服基于字符的 token 词表，由于各种特殊字符数量太庞大导致效果变差。&lt;/p&gt;
&lt;p&gt;除了我们常用的中文外，ChatGPT 可以随意操作英文、日文、韩文、法文等至少二十多种文字。这些语言的文字和符号更是多种多样，有英文拉丁字母&lt;code&gt;ABCDabcd&lt;/code&gt;，中文汉字&lt;code&gt;千百花鸟风月&lt;/code&gt;，西里尔字母&lt;code&gt;БГД&lt;/code&gt;，日语假名&lt;code&gt;ピンイ&lt;/code&gt;，当然也包括很多 emoji 特殊符号&lt;code&gt;💁👌🎍😍&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所有的字符在计算机中都是以 &lt;strong&gt;Unicode&lt;/strong&gt; 编码格式实现的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Unicode 编码&lt;/strong&gt;是一种用于计算机表示全球范围内各种语言文字字符的标准编码方式，&lt;strong&gt;它为世界上所有的字符都分配了一个唯一的数字编号&lt;/strong&gt;，解决不同国家和地区使用不同语言文字、字符集的问题。 Unicode 编码采用 16 进制表示，每个字符都有一个唯一的码点，例如汉字“&lt;strong&gt;中&lt;/strong&gt;”在 Unicode 编码中的码点是U+4E2D，其中 U+ 表示 Unicode 编码，4E2D 是该字符的 16 进制码点。若以 UTF-8 编码为例，汉字“中”被转换为 &lt;strong&gt;3 个字节（byte）&lt;/strong&gt; 的二进制数据：11100100 10111000 10101101。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Unicode 常用字符目前总量大约有十多万，如果直接基于字符形式，构造 token 词表的话，那么词表可能会变得非常庞大，达到几十万。过于庞大的词表会对 ChatGPT 模型产生很强的不确定性因素，让模型难以训练。&lt;/p&gt;
&lt;p&gt;因此，Byte-level BPE 算法应运而生。这种算法的执行步骤和上述的 BPE 算法完全一致，唯一的区别在于，&lt;strong&gt;BPE 算法直接操作 Unicode 字符，而 Byte-level BPE 算法把文本的字节作为直接操作的对象&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于操作的基本单位是字节而不是字符，因此即使是复杂的、多样化的文字系统（如中文、日文等），其基础元素也仅仅是256个可能的字节值。这相较于直接基于字符的BPE算法，其可能需要包含成千上万个不同的字符，词表大小显著减少。小型词表意味着模型训练和推理时的内存和存储需求大大降低。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如，在 BPE 算法中，&lt;code&gt;中&lt;/code&gt; 字被当作一个字符进行 token 匹配。而在 Byte-level 算法中，它被当作 3 个字符进行匹配（因其 Unicode 占用 3 个字节）。而英文字母如 &lt;code&gt;p&lt;/code&gt; 则在两种算法中，都被当作一个字符处理，因为字母的 Unicode 编码只占用一个字节。所有的字节个数全部加起来不过 256 （即一个字节所表示的符号个数 $1\ Byte = 8\ bit = 2^8$）个，这对模型训练是一个巨大的利好。&lt;/p&gt;
&lt;p&gt;Byte-level BPE 算法的代码链接：&lt;a href=&quot;https://github.com/dongrixinyu/JioNLP/blob/master/jionlp/algorithm/bpe/encoder_decoder.py&quot;&gt;Byte-level BPE&lt;/a&gt;，感兴趣的可以阅读一下。&lt;/p&gt;
&lt;h3&gt;BPE 的词表是如何训练得到的？&lt;/h3&gt;
&lt;p&gt;BPE 的词表主要是根据训练文本语料统计得到的，训练的语料数量越大，得到的 BPE 词表越准确，越具有词根语义。&lt;/p&gt;
&lt;p&gt;假设根据一份语料数据，我们可以统计得到如下&lt;strong&gt;词汇&lt;/strong&gt;和其对应出现的次数。&lt;/p&gt;
&lt;p&gt;| #lowest   | 7    | #lower | 4    | #newest | 5    | #older | 5    | #newer  | 4    |
| --------- | ---- | ------ | ---- | ------- | ---- | ------ | ---- | ------- | ---- |
| #and      | 10   | #fuel  | 4    | #a      | 14   | #has   | 4    | #oldest | 5    |
| #thailand | 3    | #price | 6    | #new    | 7    | #old   | 6    | ...     | ...  |&lt;/p&gt;
&lt;p&gt;以上均为文本中存在的完整的词汇。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接下来，我们可以按字母进行统计，得到频率最高的字符对为标红的 “es”，共计出现 17 次。我们单独把 “es” 提出来，&lt;strong&gt;并把语料中的所有 “es” 看作一个整体&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;再重复上面的过程，&lt;strong&gt;可以发现“est”可以看作是“es” 和 “t” 的结合体&lt;/strong&gt;，总计也出现 17 次。因此，可以把“est” 看作一个整体，放入词表，&lt;strong&gt;并把语料中所有的 “est” 看作一个整体&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;再重复上面的过程，可以发现，“#a” 的频率仅次于 “est”，为 14 次。因此，把 “#a” 放入词表中。&lt;/li&gt;
&lt;li&gt;再重复，可以把 “er” 这个字符对提取出来。&lt;/li&gt;
&lt;li&gt;以此类推，我们可以逐渐将高频的字符对提取出来，不断放入词表中。&lt;/li&gt;
&lt;li&gt;当放入词表中的 token 数达到了预定的最大数 N 时（一般从几万到几十万不等），得到最终的词表，即可用于 BPE 算法的执行流程，拆分每一条文本为若干 token。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Tokenizer 的好处&lt;/h3&gt;
&lt;h4&gt;克服长尾效应 OOV&lt;/h4&gt;
&lt;p&gt;在英文单词中，最常出现的 5000 个单词占据了实际使用量的 90%。而那些&lt;strong&gt;极低频&lt;/strong&gt;的单词数量极多，但总共加起来的实际使用量也不超过 2%。这就是自然语言的&lt;strong&gt;长尾效应&lt;/strong&gt; &lt;strong&gt;，&lt;/strong&gt; 这种现象也出现在其它语言中。&lt;/p&gt;
&lt;p&gt;直接把极低频的单词和字符当作 token，本身意味着数据量的缺乏，会导致它有可能不在词表中（Out Of Vocabulary，OOV），对 NLP 模型的性能产生很大的影响。因此，引入 Tokenizer，采用 BPE 算法可以避免低频词作为 token。&lt;/p&gt;
&lt;p&gt;例如，根据上述训练例子得到的词表，&lt;code&gt;#strangest&lt;/code&gt; 这个词在训练语料中词频较低，可能不出现在 token 词表中，但 “&lt;code&gt;#strang&lt;/code&gt;” 和 “&lt;code&gt;est&lt;/code&gt;” 一定以较高的频率出现在 token 词表中。&lt;/p&gt;
&lt;h4&gt;多语言支持&lt;/h4&gt;
&lt;p&gt;在早期，NLP 神经网络模型功能十分单一，且仅支持某一种语言。一个针对英文的文本分类模型，并不能支持中文的文本分类。而 BPE 算法，包括 Byte-level BPE 算法的设计，使得一份词表中包含了多种语言的字符，支持模型的多语言处理功能。&lt;/p&gt;
&lt;h2&gt;词嵌入｜Embedding&lt;/h2&gt;
&lt;p&gt;ChatGPT 的输入文字转换为 token 之后，还需要将 token 再转换为张量，这个过程叫做词嵌入（ &lt;strong&gt;Embedding&lt;/strong&gt;），同时 embedding 也指被转换后得到的张量本身。&lt;/p&gt;
&lt;p&gt;在神经网络中，&lt;strong&gt;张量&lt;/strong&gt;（Tensor）是指多维数组，它可以存储和处理大量的数据，是神经网络中最基本的数据结构。张量一般都以浮点数（小数的一种计算机表示形式）作为元素进行填充。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;例如，$a=[[1.034,0.932,−0.347],[0.023,−1.025,0.256]]$ 就是一个 $(2,3)$ 形状的张量，是一个多维数组。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而&lt;strong&gt;向量（vector）&lt;/strong&gt;，就是高中数学中的概念，一般就可以看作是一维张量。&lt;/p&gt;
&lt;p&gt;ChatGPT 从功能上看，是一个语言模型，但从结构上看，它是一个多层的、复杂的神经网络模型，每一层的神经网络都在进行浮点数张量（Tensor）的数字计算，而 ChatGPT 的输入是文字符号，token 也是文字符号。因此，&lt;strong&gt;token 需要先转换为浮点数字&lt;/strong&gt;，再进入模型中进行计算。将用户输入的 token 转换为浮点数张量的过程，就叫做&lt;strong&gt;词嵌入（Embedding）&lt;/strong&gt; 。当模型将结果计算完，也要将最终的浮点数转换为具体的 token，作为输出。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403300933980.awebp&quot; alt=&quot;4-3.png&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;#The&lt;/code&gt;, &lt;code&gt;#new&lt;/code&gt;, &lt;code&gt;est&lt;/code&gt;, &lt;code&gt;#car&lt;/code&gt;, &lt;code&gt;#has&lt;/code&gt;, &lt;code&gt;#a&lt;/code&gt;, &lt;code&gt;#low&lt;/code&gt;, &lt;code&gt;er&lt;/code&gt;, &lt;code&gt;#price&lt;/code&gt;, &lt;code&gt;#and&lt;/code&gt;, &lt;code&gt;#the&lt;/code&gt;, &lt;code&gt;#low&lt;/code&gt;, &lt;code&gt;est&lt;/code&gt;, &lt;code&gt;#fuel&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;仍以上述句子为例，假设 token 词表（Vocabulary）的数量总共为 N，每一个 token 都用一个 M 维的浮点数张量表示，其中&lt;strong&gt;每一个 token 都对应了一行张量&lt;/strong&gt;，即该 token 的 embedding 表示。&lt;/p&gt;
&lt;p&gt;例如，&lt;code&gt;#price&lt;/code&gt; 这个 token 对应的 embedding 是一个 M 维向量：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$#price→ [0.103,0.034,0.129,−0.219,−0.156,...,0.0284,−0.172]$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这组数据就可以传入 ChatGPT 模型中，做模型的训练和使用。所有的词表组成了一个 $N × M$ 维度的张量，如下图左侧方阵。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403300937866.awebp&quot; alt=&quot;4-4.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;根据例子中的前四个 token，我们可以将其对应的 embedding 抽取出来，按 token 的顺序排列成一组 $N_{输入token数}×M$  的张量，这组张量即可输入 ChatGPT 进行操作，图中白色部分表示词表中的词汇未匹配到 token 序列。换句话说，它完成了由 token 到其对应张量的映射。&lt;/p&gt;
&lt;p&gt;在实际模型当中，一次性输入给模型的 token 数量 $N_{输入token数}$ 并不是无限大的，例如，在 ChatGPT 的 &lt;code&gt;gpt-3.5-turbo&lt;/code&gt; 版本中，最大的输入 token 数量为 4097 个，超出这个范围则会被模型自动截断。&lt;/p&gt;
&lt;p&gt;在自然语言中，&lt;strong&gt;文字的顺序是非常重要的&lt;/strong&gt;，“我喜欢你”，和 “你喜欢我” 表达的含义是完全不同的。所以，ChatGPT 考虑到模型的每个 token 相互之间的顺序不能改变，需要明确地在输入端标识出每个 token 的位置张量（&lt;strong&gt;Position Embedding&lt;/strong&gt;），其大小和 token 的 embedding 是一致的。两者以如下形式融合起来：$embedding_{input}=UW_e+W_p$。&lt;/p&gt;
&lt;p&gt;其中，$W_e$ 是 token embedding 矩阵，$W_p$ 是 position embedding 矩阵。而其中的 $U$ 是一个&lt;strong&gt;上下文矩阵&lt;/strong&gt;。根据第 3 节的语言模型原理，模型在建模时有上下文限制，针对当前的一个 token，模型只能关注该 token 之前的 $k$ 个 token。因此，$U=(u_{-k},...,u_{-2},u_{-1})$，它是一个单位矩阵。&lt;/p&gt;
&lt;p&gt;假设 token 数量小于模型可接收的最大数量，那么，上述公式可以退化为：$embedding_{input}=W_e+W_p$。&lt;/p&gt;
&lt;p&gt;由此，即可输入 ChatGPT 模型进行计算。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403300945970.awebp&quot; alt=&quot;4-5.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;第 1 节中提到，ChatGPT 是有多轮对话能力的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403300946471.awebp&quot; alt=&quot;4-6.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;在模型中，需要从输入端将输入1（Q1）、输出1（A1）、输入2（Q2）等部分信息区分出来。这几个部分信息分别叫做一个 segment，其中每一个 segment 都包含了多个 token，它们共享了同一个 segment embedding。具体方式如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403300947899.awebp&quot; alt=&quot;4-7.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图中做了假设：Q1、A1、Q2 分别包含了 4 个 token。当然，在实际输入中，每个 segment 包含的 token 数都是可以灵活变化的；上面对话的轮数仅有两轮，而实际输入中，对话轮数可以非常多，形如 Q1、A1、Q2、A2、...、Qn， 只要所有 segment 对应的 token 总数加起来不超过模型允许的最大 token 数即可。&lt;/p&gt;
&lt;p&gt;因此，输入给 ChatGPT 的 embedding可以表示为如下公式：
$$
embedding=embedding_{segment}+embedding_{position}+embedding_{token}
$$&lt;/p&gt;
&lt;h3&gt;Embedding 的好处&lt;/h3&gt;
&lt;p&gt;最早的时候，NLP 是直接处理文本字符串，没有 Embedding 这个操作的。Embedding 这个操作最早是由 &lt;strong&gt;word2vec 模型&lt;/strong&gt;提出并实施的，GPT 系列模型，包括 ChatGPT 已将此操作作为了固定默认步骤。&lt;/p&gt;
&lt;h4&gt;Embedding 方便接入大规模神经网络&lt;/h4&gt;
&lt;p&gt;我们在第 2 节中论述了，AI 想要有较高水平的智能，其模型规模必然比较大，参数量众多。在机器学习领域，神经网络模型是最容易扩展其模型规模的。我们会在第 8 节讲解神经网络相关的概念。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果没有 Embedding 操作，那么 NLP 领域依然停留在直接处理字符的层面上，模型的规模扩展难度较大。embedding 将文字对应的 token 转换为抽象的固定维度的张量，标志着 NLP 迈入了深度神经网络的大门。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;Embedding 抽象了 token 的语义&lt;/h4&gt;
&lt;p&gt;当我们训练好 ChatGPT 这个模型之后，假设我们抽取出如下 token 对应的 embedding 向量：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;#price&lt;/code&gt;（价格），&lt;code&gt;#cost&lt;/code&gt;（开销），&lt;code&gt;#trunk&lt;/code&gt;（卡车），&lt;code&gt;#texi&lt;/code&gt;（出租车）&lt;/p&gt;
&lt;p&gt;其对应的均为 M 维 embedding 向量。计算两个向量相似度的方式主要采用余弦距离，则一定有：
$$
cos(price,cost)&gt;cos(price,truck) \
cos(truck,texi)&gt;cos(cost,texi)
$$
其含义为，price 和 cost 在 embedding 上的相似度，要大于 price 和 truck 的相似度，这符合人们的语言直觉。可以得出结论，在自然语言中，语义相近的两个词汇，其 embedding 向量之间的数学意义上的距离更相近。&lt;/p&gt;
&lt;p&gt;换句话说，&lt;strong&gt;Embedding&lt;/strong&gt; 建立了自然语言的语义与数学之间的关联关系。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Tokenizer 将模型输入的文字转换为 token 序列。&lt;/li&gt;
&lt;li&gt;ChatGPT 使用了 BPE 算法实现 Tokenizer。&lt;/li&gt;
&lt;li&gt;Embedding 将 token 序列映射为张量矩阵，方便模型进行张量矩阵运算。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ChatGPT 的灵魂：Attention 注意力机制</title><link>https://coooredump.github.io/blog/ai-infra/the-soul-of-chatgpt-attention-mechanism</link><guid isPermaLink="true">https://coooredump.github.io/blog/ai-infra/the-soul-of-chatgpt-attention-mechanism</guid><description>OpenAI 的 GPT 系列模型，包括其它科技公司研发的各种最先进的 NLP 模型，甚至图像处理模型，广泛采用了 Attention 注意力机制进行建模，它可谓是当前 NLP 神经网络的灵魂机制。</description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Paper&lt;/strong&gt;：&lt;a href=&quot;https://proceedings.neurips.cc/paper_files/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf&quot;&gt;Attention is All You Need !&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recommended Post&lt;/strong&gt;：&lt;a href=&quot;https://zhuanlan.zhihu.com/p/619154409&quot;&gt;Self-Attention Principle&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;OpenAI 的 GPT 系列模型，包括其它科技公司研发的各种最先进的 NLP 模型，甚至图像处理模型，广泛采用了 Attention 注意力机制进行建模，它可谓是当前 NLP 神经网络的灵魂机制。&lt;/p&gt;
&lt;h2&gt;注意力机制的思想&lt;/h2&gt;
&lt;p&gt;相信大家在学生时期，都被家长或老师提点过：“听课的时候注意力集中点！不要东张西望！” 这里就用到了注意力机制。这句话的含义是，学生应当把注意力集中在接收课堂知识上，而不是放在无关的信息上。&lt;/p&gt;
&lt;p&gt;注意力机制的思想实际上广泛应用在各个方面，它可以抽象为如下形式：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个智能体&lt;/strong&gt;（人或 AI 模型）&lt;strong&gt;从接收到的大量信息&lt;/strong&gt;（文本、图像、音频）&lt;strong&gt;中，剔除不重要、不相关的信息，重点关注与自身密切相关的信息&lt;/strong&gt;。其核心在于收缩关注的信息范围，实现信息的压缩。&lt;/p&gt;
&lt;p&gt;根据第 3 节的介绍，在 NLP 中，ChatGPT 语言模型建模实际上是寻找输入文本的&lt;strong&gt;上下文关联&lt;/strong&gt;关系。例如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;例2：请补全这条语句：&lt;strong&gt;掘金&lt;/strong&gt;社区是一个便捷的技术交流______&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在这条文本中，想要补全最终的语句，应当参考前文的信息，而前文总共 14 个字，对空格处影响最大的是&lt;code&gt;掘金&lt;/code&gt;两个字，而像形容词&lt;code&gt;便捷的&lt;/code&gt;，系词&lt;code&gt;是一个&lt;/code&gt;都不是最关键的影响因素。换句话说，我们应当设计一种注意力机制，让模型能够在输出空格字符的时候，最大限度地注意到&lt;code&gt;掘金&lt;/code&gt;两个字。&lt;/p&gt;
&lt;h2&gt;注意力机制的建模&lt;/h2&gt;
&lt;h3&gt;建立权重模式&lt;/h3&gt;
&lt;p&gt;根据第 4 节的介绍，在 NLP 模型中，自然语言是以 token 形式排列输入模型中的。如下图所示，绿色的每一列都是对应的一个 token 的 embedding 向量表示，假设每一个 token 的 embedding 具有 7 维，总共 14 个 token 共同组成一个 embedding 矩阵。我们的模型设计思路，是模型应当能够在输出&lt;code&gt;平台&lt;/code&gt;的&lt;code&gt;平&lt;/code&gt; 字时，更加关注到&lt;code&gt;掘金&lt;/code&gt;二字。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403301348771.awebp&quot; alt=&quot;5-1.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;让模型更加关注到&lt;code&gt;掘金&lt;/code&gt;两个字，实际上可以认为，给&lt;code&gt;掘金&lt;/code&gt;两个字对应的 token embedding 赋予更大的&lt;strong&gt;权重&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在神经网络模型中，所有的操作均为矩阵操作，所有的特征均为向量形式。设 $e_i$ 表示第 $i$ 个 token 的 embedding 表示，在例子中，它是一个 7 维的向量，$w_i$ 是第 $i$ 个 token 对应的权重值，它是一个标量值。那么，可以对所有的 token embedding 做一个加权：
$$
h=\sum_ie_iw_i
$$
这里，$h$ 是一个加权后的结果，它也是一个 7 维的向量。它的本质含义，是从各个 token 不同的 embedding 中，按重要程度（权重值 $w_i$）做加和，权重值高的 $e_i$ ，对后续操作影响大，权重值低的 $e_i$ ，对后续操作影响小。这就产生了一种更加注意权重高的 $e_i$ 的效果。&lt;/p&gt;
&lt;h3&gt;softmax 函数&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;定义：softmax 函数是在机器学习和深度学习中广泛使用的一个非常重要的函数，尤其是在处理分类问题时。它可以将一个含任意实数的K维向量压缩（映射）成另一个 K 维实向量，其中每个元素的范围都在 $(0, 1)$ 之间，并且所有元素的和为 1。因此，softmax 函数的输出可以被视为一个概率分布。&lt;/p&gt;
&lt;p&gt;公式：$Softmax(z_i)=\frac{e^{z_i}}{\sum^K_{j=1}e^z j}$&lt;/p&gt;
&lt;p&gt;作用和特点：softmax 函数的输出可以解释为一个概率分布，每个元素的值代表了对应类别的概率。&lt;strong&gt;使用指数函数确保所有的输出都是正的，并且可以放大输入向量中的差异&lt;/strong&gt;。&lt;strong&gt;softmax 函数配合交叉熵损失函数，常用于训练阶段的梯度下降优化&lt;/strong&gt;。这种组合可以给出明确的梯度信息，有利于模型通过学习数据来调整参数，以提高分类的准确性。&lt;/p&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多类分类问题&lt;/strong&gt;：在神经网络的最后一层，用于将神经元的输出转换为预测每个类别的概率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语言模型&lt;/strong&gt;：在处理自然语言处理任务，如文本分类或机器翻译时，softmax 被用来预测下一个词的概率分布。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增强学习&lt;/strong&gt;：在某些策略梯度方法中，softmax 用于从一组可能的动作中选择动作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面的加权计算中，$w_i$ 是一个标量值，假设模型经过训练后，针对上述例子的 token 计算得到：$w=(w_1,w_2,\ldots,w_i,w_{14})=(2.1,1.3,\ldots,-1.2,-0.4)$&lt;/p&gt;
&lt;p&gt;其中的数值有正有负，前两个 token 对应的权重标量值较大，说明对后续操作的影响大。若直接进行加权，这不符合人们的一般认知。&lt;strong&gt;一般来说，权重占比以概率形式表示，概率值应当大于 0，小于 1，且所有分量的加和等于 1&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;例如：今年国内的 GDP 占比中，第一产业占比 14.6%，第二产业占比 35.2%，第三产业占比 50.2%。&lt;/p&gt;
&lt;p&gt;这是一个典型的概率分布示例，其总和为1，三产占比最高，权重最大，影响经济的程度最大。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此，我们应当将 $w$ 转换为概率占比形式，主要采用 &lt;code&gt;softmax&lt;/code&gt; 方法：
$$
α_i=\frac{exp(w_i)}{\sum_i exp(w_i)}
$$
其中，$exp$ 是指数计算，$α_i$ 表示第 $i$ 个 token 的 embedding 对应的权重，其值介于 0~1 之间。计算上例，假设：
$$
w=(2.1,1.3,0.1,0,−0.2,−1.3,0.5,0.2,−0.8,0,0.1,−0.7,−1.2,−0.4)
$$
那么，第 1 个 token 的权重：
$$
α_1=\frac{exp(2.1)}{\sum exp(w_i)} = \frac{8.166}{8.166+3.669+1.105+1+\cdots+0.301+0.67}=\frac{8.166}{21.858}=0.374
$$&lt;/p&gt;
&lt;p&gt;此外，其它若干 token 的权重为：
$$
α_2=\frac{exp(1.3)}{\sum exp(w_i)} = \frac{3.669}{8.166+3.669+1.105+1+\cdots+0.301+0.67}=\frac{3.669}{21.858}=0.167
$$&lt;/p&gt;
&lt;p&gt;$$
α_6=\frac{exp(-1.3)}{\sum exp(w_i)} = \frac{0.272}{8.166+3.669+1.105+1+\cdots+0.301+0.67}=\frac{0.272}{21.858}=0.012
$$&lt;/p&gt;
&lt;p&gt;第 1、2 个 token 对应&lt;code&gt;掘金&lt;/code&gt; 两个字，分别占权重 37.4% 和 16.7%，占比较高，第 6 个字是&lt;code&gt;一&lt;/code&gt;，它的 token 对应的权重仅 1.2%，占比较低。这说明，在这个模型中更加注重了&lt;code&gt;掘金&lt;/code&gt; 的权重，方便后续模型输出正确的字符 token。&lt;/p&gt;
&lt;p&gt;这个例子说明了 softmax 算法的一些特性：&lt;/p&gt;
&lt;p&gt;1、softmax 可以将一维向量，输出形成概率分布的形式；&lt;/p&gt;
&lt;p&gt;2、softmax 利用指数函数，会更加着重值更高的元素，使要关注的元素更加突出；&lt;/p&gt;
&lt;p&gt;3、相应地，由于指数函数特性，它也可以尽力压低不需关注的元素的权重。&lt;/p&gt;
&lt;p&gt;以公式形式表示，计算模型的所有 embedding 加权后的权重：
$$
h=\sum_i e_i α_i
$$
Softmax 函数在神经网络模型中十分常用，&lt;strong&gt;除了应用在注意力机制计算外，softmax 还可以完美契合交叉熵损失函数&lt;/strong&gt;（将在第 8 节中介绍）。&lt;/p&gt;
&lt;h3&gt;自注意力机制 Self-Attention&lt;/h3&gt;
&lt;p&gt;前文讲述了，利用权重向量 $w$ 就可以找到模型要关注的重点内容。那么，$w$ 从哪来呢？值如何计算出来？&lt;/p&gt;
&lt;p&gt;计算权重，不同的模型、不同的 NLP 任务都有不同的形式。这项技术经过多年的发展，最终趋向于&lt;strong&gt;自注意力机制（Self-Attention）&lt;/strong&gt; ，这也是 ChatGPT 所采用的形式。&lt;/p&gt;
&lt;p&gt;$w$ 是一个权重向量，其长度（维度）与 token 的个数相同，其中的每一项是标量值。我们知道，神经网络模型中都是以向量、矩阵等构成的张量作为计算基础的。因此，想要计算得到一个标量值，最简单的形式就是&lt;strong&gt;向量点积&lt;/strong&gt;，我们需要想办法找到两个向量。&lt;/p&gt;
&lt;p&gt;假设针对第 $i$ 个 token，有两个向量 $q_i$ 和 $k_i$，两者具有相同的维度，其点积可以得到一个标量值：$w_i=q_i k_i$&lt;/p&gt;
&lt;p&gt;依然以前述句子为例。在补全句子时，&lt;code&gt;掘金&lt;/code&gt;对要空格处填写的字符，影响最大。而“&lt;code&gt;掘金&lt;/code&gt;对要填写什么字符，影响最大”这一认知，依然是我们阅读这个句子本身得到的。换句话说，&lt;strong&gt;$w_i$ 权重的信息来源，依然是原句子本身（Self）&lt;/strong&gt;，这就是自注意力命名的原因。&lt;/p&gt;
&lt;p&gt;因此，我们可以直接把每个 token 的 embedding 分别当作向量 $q_i$ 和 $k_i$ 进行计算。计算过程如下图所示。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;更详细的解析请见：&lt;a href=&quot;https://zhuanlan.zhihu.com/p/619154409&quot;&gt;动图轻松理解 Selft-Attention（自注意力机制）&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403301412760.awebp&quot; alt=&quot;5-2.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;当我们计算第 $i$ 个 token的 $w_i$ 值时，以第 1 个 token &lt;code&gt;掘&lt;/code&gt;字为例，应当先观察整条文本的 token，将所有的 token 都和 &lt;code&gt;掘&lt;/code&gt; token 做计算，这实际上就是在比较 &lt;code&gt;掘&lt;/code&gt; token 和其它 token 的关联关系。&lt;/p&gt;
&lt;p&gt;在上图中，$q_i$ 含义为 &lt;strong&gt;query&lt;/strong&gt;（中文含义为&lt;strong&gt;查询向量&lt;/strong&gt;），和 $k_i$ 的含义为 &lt;strong&gt;key&lt;/strong&gt;（中文含义为&lt;strong&gt;钥匙向量&lt;/strong&gt;）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为何起名 query 和 key？&lt;/p&gt;
&lt;p&gt;query 和 key 这两个词，最初是计算机搜索引擎中提出的概念，在 AI 领域中，被引申到注意力机制上。&lt;/p&gt;
&lt;p&gt;例如，当我们在 Google 搜索引擎中查询搜索“成都有什么好吃的？”时，搜索引擎会按匹配程度给出若干回答。&lt;/p&gt;
&lt;p&gt;其中，用户的搜索问句，就被称为 query，而每一条匹配到的结果，都包含和 query 的关联性，也就是各自的 key。通过 query 和 每一条 key 的匹配，就可以得到问答搜索结果的匹配程度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;回到上图中，当计算 &lt;code&gt;掘&lt;/code&gt; token 的输出向量时，即利用其 query 向量，分别和每一个 token 的 key 向量做点积，并对这个点积 score 做归一化（图中的 2.645 实际上是 $\sqrt7$，即 query 向量的维度 $\sqrt{d_q}$）。由此，就得到了熟悉的 $w$ 权重向量，进而执行 softmax，就得到了一个概率分布 $\alpha$ ，由此可以计算出 &lt;code&gt;掘&lt;/code&gt; token 的输出向量。&lt;/p&gt;
&lt;p&gt;为什么要除以 $sqrt{d_q}$ ？神经网络模型的训练过程是采用&lt;strong&gt;梯度下降法&lt;/strong&gt;来完成的。这里具体细节不展开，你可以参考第 8 节的内容。&lt;/p&gt;
&lt;p&gt;梯度下降非常像一个人从山顶上走到山脚下。放在神经网络的训练中，一个良好的训练过程，是人能够顺利找到下山的路，平稳下来。而若遇到一段很长的平路，没有向下的坡路，则说明模型训练遇到了阻碍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403301445080.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;既然模型需要做梯度下降，则必须保证模型在做参数运算过程中，梯度值是存在且较大的。而 softmax 函数很容易造成梯度消失，消失原因在于，输入 softmax 函数的值过大。&lt;/p&gt;
&lt;p&gt;在上述例子中，&lt;strong&gt;$q_i k_i$ 乘积过大，会导致模型训练过程中的梯度消失，进而模型训练失败&lt;/strong&gt;。因此，为了限制这个乘积值，需要除以这两个向量的维度根号值，确保数值在稳定的范围内。&lt;/p&gt;
&lt;p&gt;计算后续每个 token 位置的输出向量均同理。若想计算最后一个输出位（即空格处，第 15 个 token 要填的字）的向量，计算方法也和上面同理。&lt;/p&gt;
&lt;p&gt;上述计算方式是一步一步地以向量表示形式展开的。若以矩阵形式做公式计算，则可以表示为：
$$
Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_q}})V
$$
其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$Q=(q_1,q_2,\ldots,q_i,\ldots,q_{d_q})$&lt;/li&gt;
&lt;li&gt;$K=(k_1,k_2,\ldots,k_i,\ldots,k_{d_k})$&lt;/li&gt;
&lt;li&gt;$V=(v_1,v_2,\ldots,v_i,\ldots,v_{d_v})$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，$QK^T$ 就是点积的矩阵形式，最右侧的 $V$ 可以理解为 token embedding 组成的矩阵。$softmax(*)V$ 实际上就是前述各个 embedding 的注意力权重 $\sum_i e_i \alpha_i$ 的矩阵表示形式，$e_i$ 表示第 $i$ 个 token 的 embedding 表示，$\alpha_i$ 是注意力权重概率值。&lt;/p&gt;
&lt;p&gt;还需要补充说明的是，我们在讲解注意力机制的计算过程中，默认了 $q_i, k_i, v_i$ 都是 token embedding 本身，在实际的模型中却并不是这样。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;第 2 节中，我们提到了 ChatGPT 模型为了体现强大的模型拟合能力，具备较为高级的智能，其模型参数量是随着规模逐渐增大的。到了 ChatGPT 的基础模型 GPT3.5，模型训练的参数规模已经达到了 1750 亿。&lt;/p&gt;
&lt;p&gt;神经网络模型之所以很容易契合模型膨胀扩张这一需求。其关键特点在于可以加参数。&lt;/p&gt;
&lt;p&gt;在 $q_i、k_i、v_i$ 这里，也可以扩充参数，提升模型的拟合能力。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;具体来讲，&lt;strong&gt;$Q、K、V$ 三个模型矩阵都是由 token embedding 矩阵做一个矩阵变换得到的&lt;/strong&gt;。其维度也可以和 embedding 的维度不同。具体形式如下图所示。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体过程参见「&lt;em&gt;动图轻松理解 Self-Attention.md&lt;/em&gt;」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403301448854.awebp&quot; alt=&quot;5-3.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;在图中，$W_Q、W_K、W_V$ 均为模型参数，它们都是神经网络模型扩展的可训练参数。其具体计算方法为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$q_i=W_Q e_i$&lt;/li&gt;
&lt;li&gt;$k_i=W_K e_i$&lt;/li&gt;
&lt;li&gt;$v_i=W_V e_i$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要明确的是，&lt;strong&gt;$Q$ 和 $K$ 的维度必须是相同的&lt;/strong&gt;，这样才能保证可执行点积运算。而 $V$ 的维度则可以灵活多变，图中特意以 4 维和 7 维强调这一点。但一般来讲，三者和 token embedding 的维度保持一致即可。&lt;/p&gt;
&lt;p&gt;如果我们把注意力机制的输入输出当作一个黑盒，我们可以观察其输入、输出为如下形式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202403301448706.awebp&quot; alt=&quot;5-4.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;经过了一层自注意力机制之后，模型的输入和输出结构是相仿的，每一个 token 和其上下文经过对比之后，又生成了一个对应的融合了上下文信息的向量表示。&lt;/p&gt;
&lt;p&gt;因此，&lt;strong&gt;我们完全可以多多堆叠自注意力层，不断地让模型学习上下文联系&lt;/strong&gt;。ChatGPT 也确实是这么干的，这将在第 6、7 节中展开讲。&lt;/p&gt;
&lt;h2&gt;注意力机制的好处&lt;/h2&gt;
&lt;p&gt;通过本节的介绍，我们已经非常清楚，自然语言中自注意力机制是如何将每个 token 的上下文融合起来的，这是目前深度学习模型最流行的操作。&lt;/p&gt;
&lt;p&gt;正如例句中，空格处要填写的内容，位于句子的末尾，而信息相关的 &lt;code&gt;掘金&lt;/code&gt; 二字则位于句首，&lt;strong&gt;中间跨越了很多个 token&lt;/strong&gt;，寻找两者之间的关联关系，注意力机制非常擅长。技术上讲，这叫注意力机制擅长计算长文本依赖。&lt;/p&gt;
&lt;p&gt;而在过去，神经网络里主要使用循环神经网络（RNN）模型结构来处理。RNN 的网络结构可以使用如下公式来解释：$e_i=h(e_{i-1},W_{RNN})$。&lt;/p&gt;
&lt;p&gt;其中，$h(*)$ 是 RNN 的计算函数，$e_i$ 是第 $i$ 个 token 的网络内的 embedding 表示，$W_{RNN}$ 是模型参数。它表示了第 $i$ 个 token 必然和其相邻的 第 $i-1$ 个 token 相关联，而&lt;strong&gt;无法跨 token&lt;/strong&gt;。在处理例句的注意力时，局限性很强。&lt;/p&gt;
&lt;p&gt;另外，深度学习中模型的计算量超级大，为了让 ChatGPT 模型能够快速输出结果，就需要采用&lt;strong&gt;并行计算&lt;/strong&gt;的方式。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;所谓并行计算，就是一种朴素的加速思想，一个工作，一个人干需要10天，那么找10个人来，一天时间干完。在程序里，主要就是采用多核 GPU 来计算。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意力机制相比 RNN，更加方便模型在工程上实施并行加速计算。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;注意力机制的本质是从大量信息中剔除杂质、无关信息，保留感兴趣的信息。&lt;/li&gt;
&lt;li&gt;注意力机制在 NLP 领域的应用主要是 &lt;strong&gt;自注意力 Self-Attention&lt;/strong&gt; 形式，它是神经网络具备充分拟合能力的灵魂。&lt;/li&gt;
&lt;li&gt;在第 1 节中，我们提到了，Transformer 是构成 ChatGPT 这座房子的砖块和钢筋，而自注意力机制则是构成 Transformer 的核心要素。下一节，我们就来介绍 Transformer 结构和原理。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>C++ STL 与常见语法糖</title><link>https://coooredump.github.io/blog/cpp/cpp-stl</link><guid isPermaLink="true">https://coooredump.github.io/blog/cpp/cpp-stl</guid><description>C++ STL 语法大全</description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;刷题常见语法&lt;/h2&gt;
&lt;h3&gt;function｜&lt;code&gt;auto&amp;#x26;&amp;#x26; dfs&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;1️⃣ 关于 [&lt;a href=&quot;https://leetcode.cn/problems/subtree-of-another-tree/&quot;&gt;572. 另一棵树的子树&lt;/a&gt;] 代码中的 &lt;code&gt;auto dfs = [&amp;#x26;](auto&amp;#x26;&amp;#x26; dfs, TreeNode* node) -&gt; pair&amp;#x3C;int, bool&gt; {}&lt;/code&gt; 传递 &lt;code&gt;auto&amp;#x26;&amp;#x26; dfs&lt;/code&gt; 的解释。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 572. 另一棵树的子树
class Solution {
public:
    int getHeight(TreeNode* root) {
        if(root == nullptr)
            return 0;
        int left_h = getHeight(root-&gt;left);
        int right_h = getHeight(root-&gt;right);
        return max(left_h, right_h) + 1;
    }

    bool isSameTree(TreeNode* p, TreeNode* q) {
        if(p == nullptr || q == nullptr) {
            return p == q;
        }
        return p-&gt;val == q-&gt;val &amp;#x26;&amp;#x26; isSameTree(p-&gt;left, q-&gt;left) &amp;#x26;&amp;#x26; isSameTree(p-&gt;right, q-&gt;right);
    }

    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
        int hs = getHeight(subRoot);
        
        auto dfs = [&amp;#x26;](auto&amp;#x26;&amp;#x26; dfs, TreeNode* node) -&gt; pair&amp;#x3C;int, bool&gt; {
            if(node == nullptr)
                return {0, false};
            auto [left_h, left_found] = dfs(dfs, node-&gt;left);
            auto [right_h, right_found] = dfs(dfs, node-&gt;right);
            if(left_found || right_found)
                return {0, true};
            int node_h = max(left_h, right_h) + 1;
            return {node_h, node_h == hs &amp;#x26;&amp;#x26; isSameTree(node, subRoot)};
        };

        return dfs(dfs, root).second;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🔥 为什么需要在调用 dfs 传递 dfs 自身：为了能够递归调用 &lt;code&gt;dfs&lt;/code&gt;，我们需要在 lambda 的内部将其传递给自己。&lt;strong&gt;C++ 的 lambda 本身是不能直接递归的，因为它只是一个匿名函数对象，不知道如何调用自身&lt;/strong&gt;。因此，我们显式地将 &lt;code&gt;dfs&lt;/code&gt; 作为参数传递给它自身，以便在递归时能正确调用。&lt;/p&gt;
&lt;p&gt;2️⃣ 如果要简化调用方式（即不重复传入 &lt;code&gt;dfs&lt;/code&gt;），那可以这样定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto dfs = [&amp;#x26;](this auto&amp;#x26;&amp;#x26; dfs, TreeNode* node) -&gt; pair&amp;#x3C;int, bool&gt; {
    ...
}
// call
dfs(root);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3️⃣ 当然也可以使用 &lt;code&gt;function&amp;#x3C;pair&amp;#x3C;int, bool&gt;(TreeNode*)&gt;dfs = [&amp;#x26;](TreeNode* node) -&gt; pair&amp;#x3C;int, bool&gt; {};&lt;/code&gt; 这样就只需要传一个参数，但是也显得代码有些臃肿，看个人习惯，我比较习惯直接用 &lt;code&gt;function&lt;/code&gt; 而不是 &lt;code&gt;auto&lt;/code&gt;，也可以是：&lt;code&gt;auto&amp;#x26;&amp;#x26; dfs = [&amp;#x26;](auto&amp;#x26;&amp;#x26; dfs, TreeNode* node) -&gt; pair&amp;#x3C;int, bool&gt; {};&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;-&gt;&lt;/code&gt;，&lt;code&gt;.&lt;/code&gt; 常见用处&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;.&lt;/code&gt; 是&lt;strong&gt;结构体&lt;/strong&gt;的成员运算符。用于通过对象或引用直接访问对象的成员。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct MyStruct {
    int x;
    void func() { /* ... */ }
};

MyStruct obj;
obj.x = 10;   // 使用 . 来访问成员变量
obj.func();   // 使用 . 来调用成员函数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-&gt;&lt;/code&gt; 是&lt;strong&gt;指针&lt;/strong&gt;指向其成员的运算符。组合操作符，相当于先解引用指针，再通过 &lt;code&gt;.&lt;/code&gt; 访问成员。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;MyStruct* ptr = &amp;#x26;obj;
ptr-&gt;x = 20;   // 使用 -&gt; 来访问成员变量
ptr-&gt;func();   // 使用 -&gt; 来调用成员函数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其它用法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pair&amp;#x3C;int, int&gt; p&lt;/code&gt; 使用 &lt;code&gt;p.first&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 对象
std::pair&amp;#x3C;int, int&gt; p = {1, 2};
p.first = 10;
// 指针
std::pair&amp;#x3C;int, int&gt;* pPtr = &amp;#x26;p;
pPtr-&gt;first = 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;迭代器通常是一个对象（例如容器的迭代器），&lt;strong&gt;但它通常表现为类似指针的行为&lt;/strong&gt;，所以迭代器 iterator 使用 &lt;code&gt;-&gt;&lt;/code&gt; 来解引用访问元素&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 迭代器是对象
it.operator++();  // 假设 it 是一个迭代器对象，可以直接调用成员函数

// 但是访问元素需要使用 -&gt; 解引用
auto it = mp.begin();
it-&gt;first; 	// 访问 std::map 或 std::unordered_map 中的 pair 成员
// *it 解引用迭代器后通常指向容器中的元素，-&gt; 是 (*it).member 的简写
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;关于 for 在 &lt;code&gt;map&lt;/code&gt; 与 &lt;code&gt;pair&lt;/code&gt; 中的遍历&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;unordered_map&amp;#x3C;int, int&gt; mp{{1, 2},
                           {3, 4}};
// iterator
auto it = mp.begin();
cout &amp;#x3C;&amp;#x3C; it-&gt;first &amp;#x3C;&amp;#x3C; &quot;: &quot; &amp;#x3C;&amp;#x3C; it-&gt;second &amp;#x3C;&amp;#x3C; std::endl;

// unordered_map
for (auto &amp;#x26;p: mp) {
    std::cout &amp;#x3C;&amp;#x3C; p.first &amp;#x3C;&amp;#x3C; &quot;: &quot; &amp;#x3C;&amp;#x3C; p.second &amp;#x3C;&amp;#x3C; std::endl;
}

vector&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; vec{{1,  2},
                           {11, 22}};
// pair
for (auto &amp;#x26;p: vec) {
    std::cout &amp;#x3C;&amp;#x3C; p.first &amp;#x3C;&amp;#x3C; &quot;: &quot; &amp;#x3C;&amp;#x3C; p.second &amp;#x3C;&amp;#x3C; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常见数据结构数值大小&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;size_t&lt;/code&gt;：是一种在 C 和 C++ 中常见的无符号整数类型，它用于表示某个对象或类型在内存中的大小，通常由 &lt;code&gt;sizeof&lt;/code&gt; 操作符返回。&lt;code&gt;size_t&lt;/code&gt; 的定义在标准头文件 &lt;code&gt;&amp;#x3C;stddef.h&gt;&lt;/code&gt;（C 中）或 &lt;code&gt;&amp;#x3C;cstddef&gt;&lt;/code&gt;（C++ 中）中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无符号类型&lt;/strong&gt;：&lt;code&gt;size_t&lt;/code&gt; 是无符号整数，这意味着它不能表示负值（始终 &gt;= 0）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平台相关&lt;/strong&gt;：&lt;code&gt;size_t&lt;/code&gt; 的具体大小依赖于平台的架构。在32位系统上，&lt;code&gt;size_t&lt;/code&gt; 通常为 4 字节（32 位）；在64位系统上，它通常为 8 字节（64 位）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;典型用途&lt;/strong&gt;：&lt;code&gt;size_t&lt;/code&gt; 主要用于数组索引、内存大小计算、&lt;code&gt;malloc&lt;/code&gt; 和 &lt;code&gt;calloc&lt;/code&gt; 等内存分配函数的返回值。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 像 STL 的容器 size() 函数返回值是 size_t，而 size_t 是一个与机器相关（32bit，64bit）的无符号整数类型，
size_t size() const;

// 死循环
for (size_t i = N; i &gt;= 0; --i) {
	// todo
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其他常见数据结构类型&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;| 数据类型      | 字节大小               | 数值范围                                  |
| ------------- | ---------------------- | ----------------------------------------- |
| char          | 1                      | &lt;code&gt;-128&lt;/code&gt; 到 &lt;code&gt;127&lt;/code&gt; 或 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;255&lt;/code&gt;（无符号） |
| signed char   | 1                      | &lt;code&gt;-128&lt;/code&gt; 到 &lt;code&gt;127&lt;/code&gt;                           |
| unsigned char | 1                      | &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;255&lt;/code&gt;                              |
| short         | 2                      | $-2^{15}$ 到 $2^{15}-1$                   |
| int           | 4                      | $-2^{31}$ 到 $2^{31}-1$                   |
| long          | 4 或 8                 | ...                                       |
| long long     | 8                      | $-2^{63}$ 到 $2^{63}-1$                   |
| wchar_t       | 2 或 4                 | ...                                       |
| char16_t      | 2                      | 0 到 $2^{16}-1$                           |
| char32_t      | 4                      | 0 到 $2^{32}-1$                           |
| size_t        | 4 或 8（根据机器而定） | 0 到 $2^{32}-1$ 或者 0 到 $2^{64}-1$      |&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;atoi()&lt;/code&gt;, &lt;code&gt;stoi()&lt;/code&gt;, &lt;code&gt;iota()&lt;/code&gt;, &lt;code&gt;itoa()&lt;/code&gt; 与 &lt;code&gt;c_str()&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;c_str()&lt;/code&gt;：将 &lt;code&gt;string&lt;/code&gt; 转为 &lt;code&gt;char*&lt;/code&gt;，即把 C++ 字符串对象转换为 C 风格的字符串（以 \0 结尾的字符数组）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// const char* c_str() const noexcept;
std::string str = &quot;hello&quot;;
const char* cstr = str.c_str();  // cstr 现在为 &quot;hello&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;itoa()&lt;/code&gt;：&lt;strong&gt;Integer to ASCII&lt;/strong&gt;；将整数转换为 C 风格的字符串（即以 \0 结尾的字符数组），返回指向转换后字符串的指针，即参数 str 的地址&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;iota()&lt;/code&gt;：&lt;strong&gt;Incremental Fill&lt;/strong&gt;；将指定范围内的每个元素赋值为一个递增的值，通常用于生成序列&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;itoa()&lt;/code&gt; 为 C 库函数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// char* itoa(int value, char* str, int base);
char buffer[20];
itoa(12345, buffer, 10);  // 将整数 12345 转换为字符串 &quot;12345&quot;

// void iota(ForwardIt first, ForwardIt last, T value);
std::vector&amp;#x3C;int&gt; vec(5);
std::iota(vec.begin(), vec.end(), 10);  // vec 现在包含 {10, 11, 12, 13, 14}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;atoi()&lt;/code&gt;：&lt;strong&gt;ASCII to Integer&lt;/strong&gt;；将 &lt;code&gt;char*&lt;/code&gt; 转为 &lt;code&gt;int&lt;/code&gt;，因此对于一个字符串 str 我们必须调用 &lt;code&gt;c_str()&lt;/code&gt; 的方法把这个 &lt;code&gt;string&lt;/code&gt; 转换成 &lt;code&gt;const char*&lt;/code&gt; 类型；不会做范围检查，如果超出范围的话，超出上界，则输出上界，超出下界，则输出下界。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;atoi()&lt;/code&gt; 为 C 库函数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// int atoi(const char* str);
const char* str = &quot;123&quot;;
int num = atoi(str);  // num 现在为 123
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;stoi()&lt;/code&gt;：&lt;strong&gt;String to Int&lt;/strong&gt;；将 &lt;code&gt;string&lt;/code&gt; 转为 &lt;code&gt;int&lt;/code&gt;，且会做范围检查，默认范围是在 int 的范围内的，如果超出范围的话则会 runtime error.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;stof()&lt;/code&gt;：&lt;strong&gt;String to Float&lt;/strong&gt;；将 &lt;code&gt;string&lt;/code&gt; 转为 &lt;code&gt;float&lt;/code&gt;，同上&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;to_string()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// int stoi(const std::string&amp;#x26; str, size_t* pos = 0, int base = 10);
std::string str1 = &quot;456&quot;;
int num1 = stoi(str1);  // num 现在为 456

// float stof(const std::string&amp;#x26; str, size_t* pos = 0);
std::string str2 = &quot;3.14&quot;;
float num2 = stof(str2);  // num 现在为 3.14f
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;*max_element() 与 *min_element()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;std::max_element()&lt;/code&gt; 和 &lt;code&gt;std::min_element()&lt;/code&gt; 分别是取「最大值」和「最小值」的函数&lt;/p&gt;
&lt;p&gt;C++ 20 也可以使用 &lt;code&gt;ranges::max_element()&lt;/code&gt; 与 &lt;code&gt;ranges::min_element()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::vector&amp;#x3C;int&gt; v{3, 1, -4, 1, 5, 9};

// ForwardIt max_element( ForwardIt first, ForwardIt last );
int mx_elem = *max_element(v.begin(), v.end());

// ForwardIt min_element( ForwardIt first, ForwardIt last );
int mn_elem = *min_element(v.begin(), v.end());

// 自定义 compare 函数
auto it = std::max_element(v.begin(), v.end(), [](int a, int b)
    {
        return std::abs(a) &amp;#x3C; std::abs(b);
    });
std::cout &amp;#x3C;&amp;#x3C; *it &amp;#x3C;&amp;#x3C; std::endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;__builtin_popcount(nums[i])&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;__builtin_popcount(nums[i])&lt;/code&gt; 是 GCC 和 Clang 编译器中的内置函数，用于计算一个整数中二进制位为 1 的数量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 3011. 判断一个数组是否可以变为有序
class Solution {
public:
    bool canSortArray(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        // __builtin_popcount(num): GCC 和 Clang 编译器中的内置函数，用于计算一个整数中二进制位为 1 的数量
        int n = nums.size();
        int pre_max = 0;
        for (int i = 0; i &amp;#x3C; n;) {
            int mx = 0;
            int ones = __builtin_popcount(nums[i]);
            while (i &amp;#x3C; n &amp;#x26;&amp;#x26; __builtin_popcount(nums[i]) == ones) {
                if (nums[i] &amp;#x3C; pre_max) {
                    return false;
                }
                mx = max(mx, nums[i++]);
            }
            pre_max = mx;
        }
        return true;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;resize() 知多少&lt;/h3&gt;
&lt;p&gt;&lt;code&gt; ans.resize(n, avg)&lt;/code&gt; 多出的空间用 avg 补全，数组变短则直接截断。&lt;/p&gt;
&lt;h3&gt;ranges::nth_element()&lt;/h3&gt;
&lt;p&gt;相关例题：&lt;a href=&quot;https://leetcode.cn/problems/find-kth-largest-xor-coordinate-value/&quot;&gt;1738. 找出第 K 大的异或坐标值&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ranges::nth_element(vec, vec.end() - k)&lt;/code&gt; 求取第 k 大的数并放在对应位置，左边比其小，右边比其大，但顺序不保证。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 1738. 找出第 K 大的异或坐标值
class Solution {
public:
    int kthLargestValue(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; matrix, int k) {
        int m = matrix.size(), n = matrix[0].size();
        vector&amp;#x3C;int&gt; ans;
        vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; s(m + 1, vector&amp;#x3C;int&gt;(n + 1));
        for (int i = 0; i &amp;#x3C; m; i++) {
            for (int j = 0; j &amp;#x3C; n; j++) {
                s[i + 1][j + 1] = s[i][j] ^ s[i + 1][j] ^ s[i][j + 1] ^ matrix[i][j];
                ans.push_back(s[i + 1][j + 1]);
            }
        }
        ranges::nth_element(ans, ans.end() - k);
        return ans[ans.size() - k];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;lower_bound() 与 upper_bound()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;lower_bound()&lt;/code&gt; 和 &lt;code&gt;upper_bound()&lt;/code&gt; 都是利用「&lt;strong&gt;二分查找&lt;/strong&gt;」方法在排序数组中进行查找。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lower_bound(nums.begin(), nums.end(), num)&lt;/code&gt;：找第一个&lt;strong&gt;大于或等于&lt;/strong&gt; num 的数字&lt;/li&gt;
&lt;li&gt;&lt;code&gt;upperer_bound(nums.begin(), nums.end(), num)&lt;/code&gt;：找第一个&lt;strong&gt;大于&lt;/strong&gt; num 的数字&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 2529. 正整数和负整数的最大计数
class Solution {
public:
    int maximumCount(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        int neg = ranges::lower_bound(nums, 0) - nums.begin();
        int pos = nums.end() - ranges::upper_bound(nums, 0);
        return max(neg, pos);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等价于 &lt;code&gt;ranges::equal_range()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Solution {
public:
    int maximumCount(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        auto [left, right] = ranges::equal_range(nums, 0);
        int neg = left - nums.begin();
        int pos = nums.end() - right;
        return max(neg, pos);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;std::move()&lt;/code&gt; 与右值表达式&lt;/h3&gt;
&lt;p&gt;推荐阅读：&lt;a href=&quot;https://zhuanlan.zhihu.com/p/335994370&quot;&gt;一文读懂 C++ 右值引用和 &lt;strong&gt;std::move&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 什么是右值和左值？&lt;/h4&gt;
&lt;p&gt;在 C++ 中，表达式的值可以分为两种类型：&lt;strong&gt;左值（lvalue）&lt;/strong&gt; 和 &lt;strong&gt;右值（rvalue）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;左值（lvalue）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;左值表示一个具有持久存储（可以取地址）的对象，通常是变量或可以出现在赋值操作符左边的表达式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如：变量、数组元素、对象的成员等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int x = 10;
x = 20; // x 是一个左值，可以赋值。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;右值（rvalue）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;右值表示一个临时值或不持久存储的值，通常是表达式的结果，如常量、临时对象、运算结果等。右值通常无法取地址，也不能在赋值操作符的左边使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int y = 10 + 20; // 10 + 20 是一个右值，只是一个计算结果。
int z = 30;      // 30 是一个右值。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 右值引用（rvalue reference）&lt;/h4&gt;
&lt;p&gt;右值引用是 C++11 引入的一种新特性，它允许你通过引用操作右值。它的语法是在类型后面加上 &lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt;，例如：&lt;code&gt;int&amp;#x26;&amp;#x26;&lt;/code&gt; 表示一个右值引用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;右值引用的用途&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;移动语义&lt;/strong&gt;：&lt;strong&gt;允许通过移动（而不是复制）资源来优化性能&lt;/strong&gt;，特别是对于涉及大量数据的对象（如大数组、字符串等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完美转发&lt;/strong&gt;：在模板编程中使用，允许将函数参数完美转发给另一个函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;右值引用的示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int x = 10;
int&amp;#x26;&amp;#x26; rvalueRef = 10; // 10 是右值，rvalueRef 是一个右值引用。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. &lt;code&gt;std::move()&lt;/code&gt; 的作用&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;std::move()&lt;/code&gt; 是 C++ 标准库中的一个函数模板，&lt;strong&gt;用于将一个左值显式地转换为右值引用&lt;/strong&gt;。这种转换允许开发者以右值引用的方式来处理本应是左值的对象，&lt;strong&gt;从而触发移动语义&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;为什么需要 &lt;code&gt;std::move()&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;移动语义&lt;/strong&gt;：在实现对象移动时（例如在 &lt;code&gt;std::vector&lt;/code&gt; 中），你可以通过 &lt;code&gt;std::move()&lt;/code&gt; 将资源从一个对象“&lt;strong&gt;移动&lt;/strong&gt;”到另一个对象，而不是复制它们。&lt;strong&gt;这样可以避免不必要的资源分配和释放&lt;/strong&gt;，极大提高性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免复制&lt;/strong&gt;：通过 &lt;code&gt;std::move()&lt;/code&gt;，你可以明确告诉编译器，你不再需要某个对象的值，所以可以将其资源移动到另一个对象中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;std::move()&lt;/code&gt; 的示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;
#include &amp;#x3C;utility&gt; // std::move

int main() {
    std::string str = &quot;Hello, World!&quot;;
    std::string newStr = std::move(str); // str 的内容被移动到 newStr 中。

    std::cout &amp;#x3C;&amp;#x3C; &quot;str: &quot; &amp;#x3C;&amp;#x3C; str &amp;#x3C;&amp;#x3C; std::endl;    // str 现在可能为空。
    std::cout &amp;#x3C;&amp;#x3C; &quot;newStr: &quot; &amp;#x3C;&amp;#x3C; newStr &amp;#x3C;&amp;#x3C; std::endl; // newStr 拥有 &quot;Hello, World!&quot;。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;输出&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-makefile&quot;&gt;str: 
newStr: Hello, World!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，&lt;code&gt;std::move(str)&lt;/code&gt; 将 &lt;code&gt;str&lt;/code&gt; 转换为一个右值引用，这样 &lt;code&gt;newStr&lt;/code&gt; 可以接管 &lt;code&gt;str&lt;/code&gt; 的资源，而不需要复制字符串的内容。&lt;code&gt;str&lt;/code&gt; 在移动后，其内部资源（如字符串数据）被转移到 &lt;code&gt;newStr&lt;/code&gt;，因此 &lt;code&gt;str&lt;/code&gt; 变为空或处于未定义状态。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 移动语义与对象的生命周期&lt;/h4&gt;
&lt;p&gt;当对象的资源被移动后，原对象通常会被清空或置于一种安全的“空”状态。你不应再依赖或使用已移动的对象，除非明确知道它处于什么状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;移动构造函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当一个对象被移动时，通常会调用它的移动构造函数。移动构造函数接受一个右值引用，并将资源从旧对象移动到新对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class MyClass {
public:
    MyClass() : data(new int[100]) {}
    ~MyClass() { delete[] data; }

    // 移动构造函数
    MyClass(MyClass&amp;#x26;&amp;#x26; other) noexcept : data(other.data) {
        other.data = nullptr; // 旧对象的指针设为 nullptr，表示资源已被转移。
    }

private:
    int* data;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5. &lt;code&gt;std::move()&lt;/code&gt; 与右值引用的通俗理解&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;右值引用&lt;/strong&gt;：你可以把右值引用看作是一个能够接管“临时对象”所有权的“特殊指针”，它可以直接操作这些临时对象而不会额外复制数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::move()&lt;/code&gt;&lt;/strong&gt;：它并不是真正“移动”了什么东西，而是“转换”了一个左值，使其变得可以被视为右值（临时对象），这样你就可以使用右值引用来操作它。其背后是为了优化性能，减少不必要的资源复制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;move 总结&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;右值引用&lt;/strong&gt;（&lt;code&gt;int&amp;#x26;&amp;#x26;&lt;/code&gt;）允许你捕获和操作临时对象，从而实现更高效的资源管理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::move()&lt;/code&gt;&lt;/strong&gt; 是一种显式的类型转换工具，将左值转换为右值引用，告诉编译器可以“安全地”移动这个对象的资源。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;std::move()&lt;/code&gt; 和右值引用，可以避免不必要的复制操作，优化代码的执行效率，尤其是在处理大量数据的对象时。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;C++ ranges 包&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;该包位于 &lt;code&gt;#include &amp;#x3C;algorithm&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;algorithm&gt;

std::vector&amp;#x3C;int&gt; vec = {1, 2, 3};

ranges::sort(vec);

ranges::for_each(vec, [](int&amp;#x26; n) { n *= 2; });

ranges::find(vec, 1);

int cnt = ranges::count_if(vec, [&amp;#x26;](int x) { return x % 2 == 0; });

// 原地去重: 移除范围内的重复连续元素， unique() 返回去重后最大值的指针位置
auto it = std::ranges::unique(vec);

//constexpr const T&amp;#x26; max( const T&amp;#x26; a, const T&amp;#x26; b, Comp comp = {}, Proj proj = {} );
int mx = ranges::max(1, 9999);
vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; nums;
//constexpr T max( std::initializer_list&amp;#x3C;T&gt; r, Comp comp = {}, Proj proj = {} );
int max_end = ranges::max(nums, {}, [](const auto&amp;#x26; a) { return a[1]; })[1];

// fill 快速填充
int min_d[26];
ranges::fill(min_d, INT_MAX);

ranges::lower_bound(nums, 0);

ranges::upper_bound(nums, 0);

ranges::reverse(vec);

auto [left, right] = ranges::equal_range(nums, 0);

ranges::nth_element(ans, ans.end() - k);

bool isEmpty = ranges::empty(vec);

auto size = ranges::size(vec);

auto begin = std::ranges::begin(vec);
auto end = std::ranges::end(vec);

auto dist = std::ranges::distance(vec.begin() + 1, vec.end() - 1);
auto distance = std::ranges::distance(vec);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关于 &lt;code&gt;static auto x = []()&lt;/code&gt; 的用法解析&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐阅读：&lt;a href=&quot;https://blog.csdn.net/weixin_42658928/article/details/82974658&quot;&gt;&lt;code&gt;关于 static auto x = []() 的用法解析&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;static const auto _ = []() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    return nullptr;
}();

class Solution {
public:
    // 官方题解
    bool canThreePartsEqualSum(vector&amp;#x3C;int&gt;&amp;#x26; arr) {
        int s = accumulate(arr.begin(), arr.end(), 0);
        if(s % 3 != 0)
            return false;
        int target = s / 3;
        int n = arr.size(), i = 0, cur = 0;
        while(i &amp;#x3C; n) {
            cur += arr[i];
            if(cur == target)
                break;
            ++i;
        }
        if(cur != target)
            return false;
        int j = i + 1;
        // 满足最后两个数组非空
        while(j + 1 &amp;#x3C; n) {
            cur += arr[j];
            if(cur == target * 2)
                return true;
            ++j;
        }
        return false;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;ios::sync_with_stdio(false)&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;: 这行代码用于解除 C++ 标准流（cin, cout, 等）与 C 标准流（scanf, printf, 等）的同步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原因&lt;/strong&gt;: 在 C++ 中，cin 和 cout 默认与 scanf 和 printf 这类 C 的输入输出流同步，以确保它们可以混合使用而不会出现顺序问题。然而，这种同步会带来性能开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果&lt;/strong&gt;: 将 ios::sync_with_stdio(false) 设为 false 后，cin 和 cout 不再与 C 标准流同步，因此可以提高输入输出的效率，但这也意味着你不应再混用 C 和 C++ 的输入输出函数，否则可能会导致未定义行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;code&gt;cin.tie(nullptr)&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;: cin.tie(nullptr) 用于解除 cin 和 cout 之间的绑定关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原因&lt;/strong&gt;: 默认情况下，cin 和 cout 是绑定在一起的，这意味着在每次使用 cin 进行输入操作前，cout 会自动刷新缓冲区，以确保输入输出顺序的正确性。然而，这种绑定关系在处理大量数据时会带来性能损失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果&lt;/strong&gt;: 将 cin 的绑定关系设为 nullptr 后，cout 不会自动刷新缓冲区，从而提高了程序的运行效率。但这意味着你需要手动刷新输出缓冲区（通过 cout.flush() 或 endl 等方式），以确保输出顺序正确。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;这两行代码通常用于竞赛编程或需要快速处理大量输入输出的程序中，能够显著提高输入输出的性能。然而，需要注意的是，它们可能会改变输入输出行为的某些细节，因此在启用这些优化时应确保不混用 C 和 C++ 的输入输出函数，并在需要时手动刷新输出缓冲区。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;#include&amp;#x3C;bits/stdc++.h&gt;&lt;/code&gt; 指令，您可以轻松将大多数标准 C++ 头文件包含在代码中（面试中的 ACM 模式经常使用该标准库函数）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bits/stdc++.h&lt;/code&gt; 是 GCC 专用的头文件，&lt;strong&gt;在使用 Clang 的 MacOS 上默认不可用&lt;/strong&gt;。要修复“文件未找到”错误，您可以创建自己的 &lt;code&gt;stdc++.h&lt;/code&gt; 文件并将其放在 Clang 可以找到的目录中。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;类型转换 &lt;code&gt;static_cast&amp;#x3C;int&gt;(val)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;C++ 提供了几种不同的类型转换操作符，每种都有特定的用途和使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;static_cast：基本数据类型之间的转换（如 int 转 float）&lt;/li&gt;
&lt;li&gt;dynamic_cast：用于处理包含多态的类层次结构的类型转换&lt;/li&gt;
&lt;li&gt;const_cast：修改变量的 const 或 volatile 修饰符，允许去除或增加这些修饰符&lt;/li&gt;
&lt;li&gt;reinterpret_cast：最危险的类型转换操作符，主要用于在指针或引用之间进行非常规的、非安全的类型转换&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;push() 与 emplace()&lt;/h3&gt;
&lt;p&gt;对于 map 也好，pair 也好，或者是对象，emplace 都能直接构造。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::map&amp;#x3C;int, std::string&gt; myMap;

// 使用 insert 和 std::make_pair
myMap.insert(std::make_pair(1, &quot;one&quot;));

// 使用 emplace，直接传入构造 pair 的参数
myMap.emplace(2, &quot;two&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C/C++ &lt;code&gt;__builtin&lt;/code&gt; 超实用位运算库函数&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;__builtin_ctz()&lt;/code&gt; / &lt;code&gt;__builtin_ctzll()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;返回括号内数字的&lt;strong&gt;二进制&lt;/strong&gt;表示数&lt;strong&gt;末尾 0 的个数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int main() {
    // 输出 3: 8 = 1000, 末尾 3 个 0
    cout &amp;#x3C;&amp;#x3C; __builtin_ctz(8) &amp;#x3C;&amp;#x3C; endl ;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;__builtin_clz()&lt;/code&gt; / &lt;code&gt;__builtin_clzll()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;返回括号内数字的&lt;strong&gt;二进制&lt;/strong&gt;表示数&lt;strong&gt;前导 0 的个数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int main() {
    // 输出 28:
    // 8 = 0000 0000 0000 0000 0000 0000 0000 1000
    // 整型(int)为 32 位, 有 28 个前导 0
    // 如果换成 __builtin_clzll 则输出 60
    cout &amp;#x3C;&amp;#x3C; __builtin_clz(8) &amp;#x3C;&amp;#x3C; endl ;
    return 0 ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;__builtin_popcount()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;返回括号内数字的&lt;strong&gt;二进制表示数 1 的个数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int main() {
    // 输出 4: 15 = 1111, 1 的个数有 4 个
    cout &amp;#x3C;&amp;#x3C; __builtin_popcount(15) &amp;#x3C;&amp;#x3C; endl ;
    return 0 ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;__builtin_parity()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;判断括号中数字的二进制表示数 1 的个数的奇偶数（偶数返回 0，奇数返回 1）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++/h&gt;
using namespace std;

int main() {
    // 输出 0: 15 = 1111, 1 的个数为 4 (偶数个)
    cout &amp;#x3C;&amp;#x3C; __builtin_parity(15) &amp;#x3C;&amp;#x3C; endl ;
    return 0 ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;__builtin_ffs()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;返回括号中数字的二进制表示数的最后一个 1 在第几位（从后往前算）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int main() {
    // 输出 4: 8 = 1000, 最后一个 1 在第四位
    cout &amp;#x3C;&amp;#x3C; __builtin_ffs(8) &amp;#x3C;&amp;#x3C; Lendl ;
    return 0 ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;STL&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐阅读：https://www.apiref.com/cpp-zh/cpp/container.html&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;容器库是类模板与算法的汇集，允许程序员简单地访问常见数据结构，例如队列、链表和栈。&lt;/p&gt;
&lt;p&gt;有三类容器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;顺序容器&lt;/li&gt;
&lt;li&gt;关联容器&lt;/li&gt;
&lt;li&gt;无序关联容器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;容器管理为其元素分配的存储空间，并提供&lt;strong&gt;直接或间接地通过迭代器&lt;/strong&gt;（拥有类似指针属性的对象）访问它们的函数。&lt;/p&gt;
&lt;p&gt;C++ 标准模板库（STL）提供了一系列强大的容器，这些容器封装了常见的数据结构，支持各种操作和算法。&lt;/p&gt;
&lt;p&gt;一旦 STL 容器中有 &lt;code&gt;find()&lt;/code&gt; 方法，那么判断方法一般是 &lt;code&gt;stl.find(val) != stl.end()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;⚠️ find 函数只有在「关联容器」和「string」中存在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前者返回迭代器 iterator&lt;/li&gt;
&lt;li&gt;后者 If no matches were found, the function returns &lt;a href=&quot;http://www.cplusplus.com/string::npos&quot;&gt;string::npos&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202409030615939.png&quot; alt=&quot;image-20240903061556605&quot;&gt;&lt;/p&gt;
&lt;h3&gt;顺序容器&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;顺序容器实现能按&lt;strong&gt;顺序访问&lt;/strong&gt;的数据结构&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;array&lt;/code&gt;：静态的连续数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vector&lt;/code&gt;：动态的连续数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deque&lt;/code&gt;：双端队列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forward_list&lt;/code&gt;：单链表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list&lt;/code&gt;：双链表&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;关联容器&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;关联容器实现能快速查找「 $O(log n)$ 复杂度」的数据结构&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;set&lt;/code&gt;：&lt;strong&gt;唯一&lt;/strong&gt;键的集合，按照键排序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt;：键值对的集合，按照键排序，键是&lt;strong&gt;唯一&lt;/strong&gt;的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;multiset&lt;/code&gt;：键的集合，按照键排序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;multimap&lt;/code&gt;：键值对的集合，按照键排序&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;无序关联容器&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;无序关联容器提供能快速查找「均摊 $O(1)$，最坏情况 $O(n)$ 复杂度」的无序（&lt;strong&gt;哈希&lt;/strong&gt;）数据结构&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unordered_set&lt;/code&gt;：&lt;strong&gt;唯一&lt;/strong&gt;键的集合，按照键生成&lt;strong&gt;散列&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unordered_map&lt;/code&gt;：键值对的集合，按照键生成&lt;strong&gt;散列&lt;/strong&gt;，键是&lt;strong&gt;唯一&lt;/strong&gt;的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unordered_multiset&lt;/code&gt;：键的集合，按照键生成散列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unordered_multimap&lt;/code&gt;：键值对的集合，按照键生成散列&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;容器适配器&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;容器适配器&lt;strong&gt;提供顺序容器的不同接口&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stack&lt;/code&gt;：适配一个容器以提供栈（LIFO 数据结构）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queue&lt;/code&gt;：适配一个容器以提供队列（FIFO 数据结构）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;迭代器非法化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;只读方法决不非法化迭代器或引用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改容器内容的方法可能非法化迭代器和/或引用&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;

using namespace std;

template&amp;#x3C;typename T&gt;
std::ostream &amp;#x26;operator&amp;#x3C;&amp;#x3C;(std::ostream &amp;#x26;s, const vector&amp;#x3C;T&gt; &amp;#x26;v) {
    s.put(&apos;[&apos;);
    char comma[3] = {&apos;\0&apos;, &apos; &apos;, &apos;\0&apos;};
    for (const auto &amp;#x26;e: v) {
        s &amp;#x3C;&amp;#x3C; comma &amp;#x3C;&amp;#x3C; e;
        comma[0] = &apos;,&apos;;
    }
    return s &amp;#x3C;&amp;#x3C; &apos;]&apos;;
}

int main() {
    vector&amp;#x3C;int&gt; vec{1, 2, 3, 4, 5, 6};
    auto it = vec.begin();
    vec.insert(it, 1);      // success
    vec.insert(it, 666);    // fail: 迭代器非法化(因为在上一步后容器扩容)
    cout &amp;#x3C;&amp;#x3C; vec.capacity() &amp;#x3C;&amp;#x3C; endl;    // 扩容: 重新分配内存空间

    it = vec.begin();   // 重新指向新内存地址
    vec.insert(it, 666);    // success
    vec.insert(it, 666);    // success
    vec.insert(it, 666);    // success
    cout &amp;#x3C;&amp;#x3C; vec &amp;#x3C;&amp;#x3C; endl;


    auto i = vec.begin();
    vec.erase(i);   // success
    vec.erase(i);   // success

    i = vec.end() - 1;
    vec.erase(i);   // success
    vec.erase(i);   // fail: 迭代器非法化
    cout &amp;#x3C;&amp;#x3C; vec &amp;#x3C;&amp;#x3C; endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;迭代器非法化总结表格：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503061015924.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里的&lt;strong&gt;插入&lt;/strong&gt;指代任何添加一或多个元素到容器的方法：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因为插入过程可能会导致&lt;strong&gt;容器扩容&lt;/strong&gt;（&lt;code&gt;capacity&lt;/code&gt; 变化），扩容后会释放原有的空间，而迭代器依然指向原有空间中的位置，此时该迭代器变成野指针，导致无法访问！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;insert&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push_back&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push_front&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operator[]&lt;/code&gt; 也算，因为也存在插入的可能性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而&lt;strong&gt;擦除&lt;/strong&gt;指代任何从容器移除一或多个元素的方法：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;擦除后迭代器失效是因为删除的是最后一个有效元素，然后删除后该&lt;strong&gt;迭代器指向容器的非有效位置&lt;/strong&gt;！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;http://tva1.sinaimg.cn/large/006V2BYXly1h1a8q194rxj30hb04gt94.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;erase&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop_back&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop_front&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clear&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;尾后迭代器需要特别留意。通常像指向未被擦除元素的正常迭代器一般非法化此迭代器。&lt;/p&gt;
&lt;p&gt;故 &lt;code&gt;std::set::end&lt;/code&gt; 决不被非法化，&lt;code&gt;std::unordered_set::end&lt;/code&gt; 仅在重哈希时被非法化，&lt;code&gt;std::vector::end&lt;/code&gt; 始终被非法化（因为它始终出现在被修改元素后），以此类推。&lt;/p&gt;
&lt;h3&gt;STL 成员函数&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;成员函数一览表&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503061015186.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503061015983.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;bitset｜位图&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;std::bitset&lt;/code&gt; 也称「位图」，非常常用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template &amp;#x3C;size_t N&gt; 
class bitset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;bitset 的初始化方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::bitset&amp;#x3C;N&gt; bitset1; // 创建一个长度为 N 的 bitset，所有位都被初始化为 0
std::bitset&amp;#x3C;N&gt; bitset2(value); // 使用二进制整数 value 初始化一个长度为 N 的 bitset
std::bitset&amp;#x3C;32&gt; bitset21(0xffff);          // bits 0 ... 15 are set to 1; 16 ... 31 are 0
std::bitset&amp;#x3C;128&gt; bitset22(0xffff);         // bits 32 through 127 initialized to zero
 
std::bitset&amp;#x3C;N&gt; bitset3(string); // 使用二进制字符串 string 初始化一个长度为 N 的 bitset
string str(&quot;1111111000000011001101&quot;);
std::bitset&amp;#x3C;N&gt; bitset31(str);    //用整个字符串来初始化bitset
std::bitset&amp;#x3C;32&gt; bitset32(str, 5, 4); // 4 bits starting at str[5], 1100
std::bitset&amp;#x3C;32&gt; bitset33(str, str.size() - 4);     // use last 4 characters
 
std::bitset&amp;#x3C;N&gt; bitset4(bitset); // 使用另一个 bitset 初始化一个长度为 N 的 bitset
std::bitset&amp;#x3C;n&gt; bitset5(bitset4, pos, n);	  //bitset5是bitset4中从位置pos开始的n个位的副本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法：&lt;/p&gt;
&lt;p&gt;| 方法         | 功能                                           |
| ------------ | ---------------------------------------------- |
| b.any()      | b 中是否存在置为 1 的二进制位？                |
| b.none()     | b 中不存在置为 1 的二进制位？                  |
| b.count()    | b 中置为 1 的二进制位的个数                    |
| b.size()     | b 中二进制位的个数                             |
| b[pos]       | 访问 b 中在 pos 处的二进制位                   |
| b.test(pos)  | b 中在 pos 处的二进制是否为 1？                |
| b.set()      | 把 b 中所有二进制位都置为 1                    |
| b.set(pos)   | 把 b 中在 pos 处的二进制位置为 1               |
| b.reset()    | 把 b 中所有二进制位都置为 0                    |
| b.reset(pos) | 把 b 中在 pos 处的二进制位置为 0               |
| b.flip()     | 把 b 中所有二进制位取反                        |
| b.flip(pos)  | 把 b 中在 pos 处的二进制位取反                 |
| b.to_ulong() | 用 b 中同样的二进制位返回一个 unsigned long 值 |
| &lt;code&gt;os &amp;#x3C;&amp;#x3C; b&lt;/code&gt;      | 把 b 中的位集输出到 os 流                      |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::bitset&amp;#x3C;4&gt; b1(&quot;1100&quot;);
size_t count = b1.count(); // count set bits
size_t size = b1.size();   // get number of bits
bool bit = b1.test(2);     // test bit at position 2
bool any = b1.any();       // check if any bit is set
bool none = b1.none();     // check if no bit is set
b1.flip();                 // flip all bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些函数使得 &lt;code&gt;std::bitset&lt;/code&gt; 成为&lt;strong&gt;处理位级别数据&lt;/strong&gt;的强大工具。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::bitset&lt;/code&gt; 还支持「位操作符」、「位移操作符」和「比较操作符」：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::bitset&amp;#x3C;4&gt; b1(&quot;1100&quot;);
std::bitset&amp;#x3C;4&gt; b2(&quot;1010&quot;);
std::bitset&amp;#x3C;4&gt; b3 = b1 &amp;#x26; b2; // bitwise AND
std::bitset&amp;#x3C;4&gt; b4 = b1 | b2; // bitwise OR
std::bitset&amp;#x3C;4&gt; b5 = b1 ^ b2; // bitwise XOR
std::bitset&amp;#x3C;4&gt; b6 = ~b1;     // bitwise NOT
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::bitset&amp;#x3C;4&gt; b1(&quot;1100&quot;);
std::bitset&amp;#x3C;4&gt; b2 = b1 &amp;#x3C;&amp;#x3C; 1; // left shift
std::bitset&amp;#x3C;4&gt; b3 = b1 &gt;&gt; 1; // right shift
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::bitset&amp;#x3C;4&gt; b1(&quot;1100&quot;);
std::bitset&amp;#x3C;4&gt; b2(&quot;1010&quot;);
bool equal = (b1 == b2); // compare bitsets
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;string&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;str[i]&lt;/code&gt; 取到的是 &lt;code&gt;char&lt;/code&gt; 字符&lt;/p&gt;
&lt;p&gt;&lt;code&gt;c_str()&lt;/code&gt;：const CharT* c_str() const;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;string 可以利用以下几个方法模拟 stack&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;push_back()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pop_back()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;erase()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;append()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;length()&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find()&lt;/code&gt;：finds the first occurrence of the given substring&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iomanip&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;

int main()
{
    std::string::size_type n;
    std::string const s = &quot;This is a string&quot;; /*
                             ^  ^  ^
                             1  2  3          */
 
    // search from beginning of string
    n = s.find(&quot;is&quot;);
 
    // search from position 5
    n = s.find(&quot;is&quot;, 5);
 
    // find a single character
    n = s.find(&apos;a&apos;);
 
    // find a single character
    n = s.find(&apos;q&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rfind()&lt;/code&gt;：find the last occurrence of a substring&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;string::npos&lt;/code&gt;：一般和 &lt;code&gt;find()&lt;/code&gt; 搭配使用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// string search functions return npos if nothing is found
std::string s = &quot;test&quot;;
if (s.find(&apos;a&apos;) == s.npos)
    std::cout &amp;#x3C;&amp;#x3C; &quot;no &apos;a&apos; in &apos;test&apos;\n&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;查找，包括前面 &lt;code&gt;find&lt;/code&gt; 与 &lt;code&gt;rfind&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;find_first_of()&lt;/code&gt;：size_type find_first_of( const basic_string&amp;#x26; &lt;code&gt;str&lt;/code&gt;, size_type &lt;code&gt;pos&lt;/code&gt; = 0 ) const;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;find_first_not_of()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;find_last_of()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;find_last_not_of()&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;operations&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;starts_with()&lt;/code&gt; [C++ 20]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ends_with()&lt;/code&gt; [C++ 20]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;contains()&lt;/code&gt; [C++ 23]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;substr(size_type position = 0, size_type count = npos)&lt;/code&gt;：获取子串&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;int/float 转为 string&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;to_string&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;to_wstring&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;string 转为数值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stoi()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stof()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stod()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stol()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stoll()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;array&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;array&lt;/code&gt; 是一个固定大小的数组封装容器。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::array&amp;#x3C;int, 5&gt; arr = {1, 2, 3, 4, 5};

// at(size_t index)
int value1 = arr.at(2);  	// value = 3
int value2 = arr[2];  		// value = 3

int first = arr.front();  	// first = 1
int last = arr.back();  	// last = 5

int* p = arr.data();  // p 是指向 arr 首元素的指针

// size_t 一般用作 size() 的接收参数
size_t size = arr.size();  // size = 5

std::array&amp;#x3C;int, 0&gt; ar;  // 定义一个空的 std::array
bool isEmpty = ar.empty();  // isEmpty = true

std::array&amp;#x3C;int, 3&gt; arr1 = {1, 2, 3};
std::array&amp;#x3C;int, 3&gt; arr2 = {4, 5, 6};
arr1.swap(arr2);  // arr1 现在是 {4, 5, 6}, arr2 现在是 {1, 2, 3}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;vector&lt;/h3&gt;
&lt;p&gt;动态数组，支持随机访问，内存是连续的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常用方法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push_back(value)&lt;/code&gt;：在末尾添加元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop_back()&lt;/code&gt;：删除末尾元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insert(vec.begin(), value)&lt;/code&gt;：在指定位置插入元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(vec.begin())&lt;/code&gt;：删除指定位置的元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clear()&lt;/code&gt;：清空容器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resize(n)&lt;/code&gt;：调整容器大小。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;at(index)&lt;/code&gt;：访问指定位置的元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;front()&lt;/code&gt;：访问第一个元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;back()&lt;/code&gt;：访问最后一个元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty()&lt;/code&gt;：检查容器是否为空。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt;：返回容器中元素的数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;capacity()&lt;/code&gt;：返回容器的容量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reserve(n)&lt;/code&gt;：预留至少 n 个元素的空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;deque&lt;/h3&gt;
&lt;p&gt;双端队列，支持在两端快速插入和删除。内存可能不连续，支持常数时间的随机访问。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push_back(value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push_front(value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop_back()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop_front()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;front()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;back()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(position)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insert(position, value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;at(index)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;set [有序]&lt;/h3&gt;
&lt;p&gt;集合，存储唯一元素，&lt;strong&gt;元素按顺序排列&lt;/strong&gt;，底层通常是&lt;strong&gt;红黑树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;插入、删除、查找的时间复杂度为 $O(log n)$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;insert(value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;emplace(value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(position)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find(value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count(value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contains(value)&lt;/code&gt; [C++ 20]&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;multiset [有序]&lt;/h3&gt;
&lt;p&gt;允许存储&lt;strong&gt;重复元素&lt;/strong&gt;的集合，元素按顺序排列，底层通常是&lt;strong&gt;红黑树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;插入、删除、查找的时间复杂度为 $O(log n)$。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;滑动窗口 + &lt;code&gt;multiset&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 1438. 绝对差不超过限制的最长连续子数组
class Solution {
public:
    int longestSubarray(vector&amp;#x3C;int&gt;&amp;#x26; nums, int limit) {
        multiset&amp;#x3C;int&gt; ordered_set;
        int left = 0, ans = 0;
        for (int right = 0; right &amp;#x3C; nums.size(); right++) {
            ordered_set.insert(nums[right]);
            while (*ordered_set.rbegin() - *ordered_set.begin() &gt; limit) {
                ordered_set.erase(ordered_set.find(nums[left]));
                left++;
            }
            ans = max(ans, right - left + 1);
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;insert(value)&lt;/code&gt;：插入元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(position)&lt;/code&gt;：删除指定位置的元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(value)&lt;/code&gt;：删除与指定值相等的元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find(value)&lt;/code&gt;：查找指定值的元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count(value)&lt;/code&gt;：统计与指定值相等的元素数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clear()&lt;/code&gt;：清空容器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty()&lt;/code&gt;：检查容器是否为空。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt;：返回容器中元素的数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;map [有序]&lt;/h3&gt;
&lt;p&gt;键值对集合，按键排序，键唯一，底层通常是&lt;strong&gt;红黑树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;插入、删除、查找的时间复杂度为 $O(log n)$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;insert({key, value})&lt;/code&gt;：插入键值对。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(position)&lt;/code&gt;：删除指定位置的键值对。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(key)&lt;/code&gt;：删除指定键的键值对。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find(key)&lt;/code&gt;：查找指定键的键值对。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count(key)&lt;/code&gt;：统计指定键的键值对数量（返回 0 或 1）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clear()&lt;/code&gt;：清空容器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty()&lt;/code&gt;：检查容器是否为空。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt;：返回容器中元素的数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;at(key)&lt;/code&gt;：访问指定键的值（若键不存在则抛出异常）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operator[](key)&lt;/code&gt;：访问或插入指定键的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;multimap [有序]&lt;/h3&gt;
&lt;p&gt;允许重复键的键值对集合，按键排序，底层通常是&lt;strong&gt;红黑树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;插入、删除、查找的时间复杂度为 $O(log n)$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;insert({key, value})&lt;/code&gt;：插入键值对。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(position)&lt;/code&gt;：删除指定位置的键值对。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(key)&lt;/code&gt;：删除指定键的键值对。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find(key)&lt;/code&gt;：查找指定键的键值对。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count(key)&lt;/code&gt;：统计指定键的键值对数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clear()&lt;/code&gt;：清空容器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty()&lt;/code&gt;：检查容器是否为空。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt;：返回容器中元素的数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;unordered_map&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;无序容器使用哈希表实现，元素无序排列，插入和查找操作平均时间复杂度为 O(1)。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;键值对集合，键唯一，&lt;strong&gt;元素无序排列&lt;/strong&gt;，&lt;strong&gt;底层为哈希表&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;插入、删除、查找的平均时间复杂度为 $O(1)$。&lt;/p&gt;
&lt;h3&gt;unordered_set&lt;/h3&gt;
&lt;p&gt;集合，存储唯一元素，&lt;strong&gt;元素无序排列&lt;/strong&gt;，&lt;strong&gt;底层为哈希表&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;插入、删除、查找的平均时间复杂度为 $O(1)$。&lt;/p&gt;
&lt;h3&gt;unordered_multimap&lt;/h3&gt;
&lt;p&gt;键值对集合，键唯一，&lt;strong&gt;元素无序排列&lt;/strong&gt;，&lt;strong&gt;底层为哈希表&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;插入、删除、查找的平均时间复杂度为 $O(1)$。&lt;/p&gt;
&lt;h3&gt;unordered_multiset&lt;/h3&gt;
&lt;p&gt;允许存储重复元素的集合，&lt;strong&gt;元素无序排列&lt;/strong&gt;，&lt;strong&gt;底层为哈希表&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;插入、删除、查找的平均时间复杂度为 $O(1)$。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;std::pair&amp;#x3C;int, int&gt;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;将两个值组合在一起，常用于关联容器（如 &lt;code&gt;std::map&lt;/code&gt;）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;p.first&lt;/code&gt;：第一个元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p.second&lt;/code&gt;：第二个元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;make_pair(val_1, val_2)&lt;/code&gt;：创建一个 &lt;code&gt;pair&lt;/code&gt; 对象&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;stack&lt;/h3&gt;
&lt;p&gt;后进先出（LIFO）的栈，默认使用 &lt;code&gt;std::deque&lt;/code&gt; 作为底层容器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push(value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;top()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;queue&lt;/h3&gt;
&lt;p&gt;先进先出（FIFO）的队列，默认使用 &lt;code&gt;std::deque&lt;/code&gt; 作为底层容器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;queue&lt;/code&gt; 为单端队列，&lt;code&gt;deque&lt;/code&gt; 为双端队列&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push(value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;front()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;back()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;priority_queue&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;top&lt;/code&gt; 访问队头元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty&lt;/code&gt; 队列是否为空&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt; 返回队列内元素个数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push&lt;/code&gt; / &lt;code&gt;emplace&lt;/code&gt; 插入元素到队尾（后者为 in-place insert）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop&lt;/code&gt; 弹出队头元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;swap&lt;/code&gt; 交换内容&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const auto data = {1, 8, 5, 6, 3, 4, 0, 9, 7, 2};

// 大顶堆(默认)
priority_queue&amp;#x3C;int&gt; max_priority_queue;
// priority_queue&amp;#x3C;int, vector&amp;#x3C;int&gt;, less&amp;#x3C;&gt;&gt; max_priority_queue;

// 小顶堆
priority_queue&amp;#x3C;int, vector&amp;#x3C;int&gt;, greater&amp;#x3C;int&gt;&gt;
        min_priority_queue(data.begin(), data.end());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 &lt;code&gt;swap&lt;/code&gt; 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main()
{
    std::vector&amp;#x3C;std::string&gt; v1{&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;4&quot;},
                             v2{&quot;Ɐ&quot;,&quot;B&quot;,&quot;Ɔ&quot;,&quot;D&quot;,&quot;Ǝ&quot;};
 
    std::priority_queue s1(std::less&amp;#x3C;&gt;(), std::move(v1));
    std::priority_queue s2(std::less&amp;#x3C;&gt;(), std::move(v2));
 
    print(&quot;s1&quot;, s1);
    print(&quot;s2&quot;, s2);
 
    s1.swap(s2);
 
    print(&quot;s1&quot;, s1);
    print(&quot;s2&quot;, s2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 [4]: 4 3 2 1
s2 [5]: Ǝ D Ɔ B Ɐ
s1 [5]: Ǝ D Ɔ B Ɐ
s2 [4]: 4 3 2 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;iterator&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;begin()&lt;/code&gt; 与 &lt;code&gt;end()&lt;/code&gt;：返回指向数组&lt;strong&gt;开头&lt;/strong&gt;和&lt;strong&gt;末尾之后一个位置&lt;/strong&gt;的迭代器&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cbegin()&lt;/code&gt; 与 &lt;code&gt;cend()&lt;/code&gt;：返回指向数组开头和末尾之后一个位置的&lt;strong&gt;常量&lt;/strong&gt;迭代器&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rbegin()&lt;/code&gt; 与 &lt;code&gt;rend()&lt;/code&gt;：返回指向&lt;strong&gt;数组末尾&lt;/strong&gt;和&lt;strong&gt;开头之前一个位置&lt;/strong&gt;的&lt;strong&gt;反向&lt;/strong&gt;迭代器&lt;/p&gt;
&lt;p&gt;&lt;code&gt;crbegin()&lt;/code&gt; 与 &lt;code&gt;crend()&lt;/code&gt;：返回指向数组末尾和开头之前一个位置的&lt;strong&gt;常量反向迭代器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常量迭代器只读&lt;/strong&gt;，不可以修改对应的元素值。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Modern C++</title><link>https://coooredump.github.io/blog/cpp/cpp-wiki</link><guid isPermaLink="true">https://coooredump.github.io/blog/cpp/cpp-wiki</guid><description>Modern C++ Lecture</description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Link: &lt;a href=&quot;https://www.youtube.com/playlist?list=PLgnQpQtFTOGRM59sr3nSL8BmeMZR9GCIA&quot;&gt;Modern C++ (Lecture &amp;#x26; Tutorials, 2020, Vizzo &amp;#x26; Stachniss) - University of Bonn&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;CMake｜Build System｜library&lt;/h2&gt;
&lt;p&gt;✅ &lt;a href=&quot;https://www.youtube.com/watch?v=2xlk4bSPG38&amp;#x26;list=PLgnQpQtFTOGRM59sr3nSL8BmeMZR9GCIA&amp;#x26;index=2&quot;&gt;Modern C++: The Basics (Lecture 0, I. Vizzo, 2020)&lt;/a&gt;：简单的 Linux 和 Cpp 历史和教程&lt;/p&gt;
&lt;p&gt;✅ &lt;a href=&quot;https://www.youtube.com/watch?v=9mZw6Rwz1vg&amp;#x26;list=PLgnQpQtFTOGRM59sr3nSL8BmeMZR9GCIA&amp;#x26;index=5&quot;&gt;Modern C++: Build and Tools (Lecture 1, I. Vizzo, 2020)&lt;/a&gt;：学习了 cpp build system 整个流程（包括使用 cmake 生成 makefile 以及 CMAKE 的语法）、静态库/动态库（&lt;code&gt;lib*.a&lt;/code&gt; 与 &lt;code&gt;lib*.so&lt;/code&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 可以自己写 library 然后自己追加
# compile modules
c++ -std=c++17 -c tools.cpp -o tools.o
# organize modules into libraries
# &quot;ar rcs libname.a module.o module.o ...&quot;
ar rcs libtools.a tools.o &amp;#x3C;other_modules&gt;
# link libraries when building code
c++ -std=c++17 main.cpp -L . -ltools -o main
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# Use CMake to simplify the build
# CMakeLists.txt
cmake_minimum_required(VERSION 3.1)
project(first_project)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS &quot;-Wall&quot;)

# 这个命令可以将构建系统的整个脚本过程输出到当前目录下
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)	# important!
# tell cmake where to look for *.hpp *.h files
include_directories(include/)

# create library &quot;libtools&quot;
add_library(tools src/tools.cpp)	# create libtools.a

# add executable main
add_executable(main src/main.cpp)	# main.o

# tell the linker to bind these objects together
target_link_libraries(main tools)	# ./main
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Build a CMake project (Build process)
cd &amp;#x3C;project_folder&gt;
mkdir build &amp;#x26;&amp;#x26; cd build
cmake ..
make
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;strtok｜stringstream&lt;/h2&gt;
&lt;p&gt;✅ &lt;a href=&quot;https://www.youtube.com/watch?v=0Jqwxr7vER4&amp;#x26;list=PLgnQpQtFTOGRM59sr3nSL8BmeMZR9GCIA&amp;#x26;index=7&quot;&gt;Modern C++: Core C++ (Lecture 2, I. Vizzo, 2020)&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iomanip&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;sstream&gt;

using namespace std;

int main() {
  stringstream filename(&quot;00205.txt&quot;);

  int num = 0;
  string ext;

  // Split the string stream using simple syntax
  // 而不是使用 strtok 来分割字符串
  filename &gt;&gt; num &gt;&gt; ext;

  cout &amp;#x3C;&amp;#x3C; num &amp;#x3C;&amp;#x3C; endl;
  cout &amp;#x3C;&amp;#x3C; ext &amp;#x3C;&amp;#x3C; endl;

  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;int main(int argc, char const *argv[]);&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;argc&lt;/code&gt; defines number of input parameters&lt;/p&gt;
&lt;p&gt;&lt;code&gt;argv&lt;/code&gt; is an array of string parameters&lt;/p&gt;
&lt;p&gt;By &lt;strong&gt;default&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;argc == 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;argv == &quot;&amp;#x3C;binary_path&gt;&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;C++ Functions&lt;/h2&gt;
&lt;p&gt;✅ &lt;a href=&quot;https://www.youtube.com/watch?v=b1VdTKNEbrk&amp;#x26;list=PLgnQpQtFTOGRM59sr3nSL8BmeMZR9GCIA&amp;#x26;index=8&quot;&gt;Modern C++: C++ Functions (Lecture 3, I. Vizzo, 2020)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;C++ 17 可以像 Python 返回多个类型一样，返回多种值了 —— &lt;code&gt;tuple&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;tuple&gt;
using namespace std;

auto Foo() {
  return make_tuple(&quot;Super Variable&quot;, 19);
}

int main() {
  auto [name, age] = Foo();
  cout &amp;#x3C;&amp;#x3C; name &amp;#x3C;&amp;#x3C; &quot; is &quot; &amp;#x3C;&amp;#x3C; age &amp;#x3C;&amp;#x3C; &quot; years old.&quot; &amp;#x3C;&amp;#x3C; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;WARNING&lt;/strong&gt;: Never return reference to locally variables!!!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;

int&amp;#x26; MultiplyBy10(int num) {   // retval is created
  int retval = 0;
  retval = 10 * num;
  cout &amp;#x3C;&amp;#x3C; &quot;retval is &quot; &amp;#x3C;&amp;#x3C; retval &amp;#x3C;&amp;#x3C; endl;   // 加上这行代码后，g++ -O3 test.cpp -o test 就可以正常工作；反之则输出乱数
  return retval;
} // retval is destroyed, it&apos;s not accesisble anymore

int main() {
  int out = MultiplyBy10(5);
  cout &amp;#x3C;&amp;#x3C; out &amp;#x3C;&amp;#x3C; endl;
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回了一个指向已经被销毁的内存位置的引用，访问这个引用会导致未定义的行为。&lt;code&gt;g++ -O3 test.cpp -o test&lt;/code&gt; 则会输出错误的值，不加 &lt;code&gt;-O3&lt;/code&gt; 则正常，或者说在方法中加个 &lt;code&gt;cout&lt;/code&gt; 输出则也正常。&lt;/p&gt;
&lt;p&gt;Static：发生在「编译」时&lt;/p&gt;
&lt;p&gt;Non-Static：发生在「运行」时&lt;/p&gt;
&lt;h3&gt;inline function&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;function&lt;/code&gt; calls are expensive...&lt;/li&gt;
&lt;li&gt;If the function is rather small, you could help the compiler.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inline&lt;/code&gt; is a &lt;strong&gt;hint&lt;/strong&gt; to the compiler
&lt;ul&gt;
&lt;li&gt;should attempt to generate code for a call.&lt;/li&gt;
&lt;li&gt;rather than a function call.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;总结：当函数足够小的时候，可以用 &lt;code&gt;inline&lt;/code&gt; 来内联函数，帮助编译器为你优化它 —— 即当函数调用时，直接将代码放（替换）到对应位置，而不是调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Check it out: https://godbolt.org/z/EGd6aG&lt;/p&gt;
&lt;h2&gt;Good C++ Practices&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Break up complicated computations into meaningful chunks and name them.&lt;/li&gt;
&lt;li&gt;[ ] Keep the length of functions small enough.&lt;/li&gt;
&lt;li&gt;[ ] Avoid unecessary comments.&lt;/li&gt;
&lt;li&gt;[ ] One function shouldl achieve &lt;strong&gt;ONE&lt;/strong&gt; task.&lt;/li&gt;
&lt;li&gt;[ ] If you can&apos;t pick a short name, then &lt;strong&gt;split&lt;/strong&gt; functionallity.&lt;/li&gt;
&lt;li&gt;[ ] Avoid &lt;strong&gt;macros&lt;/strong&gt;: If you must use ig, use ugly names with lots of capital letters.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;C++ Namespace&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Helps avoiding name conflicts&lt;/li&gt;
&lt;li&gt;Group the project into logical modules&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Avoid &lt;code&gt;using namespace &amp;#x3C;name&gt;&lt;/code&gt;&lt;/strong&gt;：好比写算法时总是使用 &lt;code&gt;using namespace std;&lt;/code&gt;，这是需要避免的！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;cmath&gt;
#include &amp;#x3C;iostream&gt;

// Avoiding!!!
using namespace std;  // std namespace is used

// Self-defined function power shadows the std::pow
double pow(double x, int exp) {
  double res = 1.0;
  for(int i = 0; i &amp;#x3C; exp; i++) {
    res *= x;
  }
  return res;
}

int main() {
  cout &amp;#x3C;&amp;#x3C; &quot;2.0 ^ 2 = &quot; &amp;#x3C;&amp;#x3C; pow(2.0, 2) &amp;#x3C;&amp;#x3C; endl;
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;C++ STL Library&lt;/h2&gt;
&lt;p&gt;✅ &lt;a href=&quot;https://www.youtube.com/watch?v=T2ZwqdwHRxg&amp;#x26;list=PLgnQpQtFTOGRM59sr3nSL8BmeMZR9GCIA&amp;#x26;index=10&quot;&gt;Modern C++: The C++ STL Library (Lecture 4, I. Vizzo, 2020)&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Size of container (C vs. CPP)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int data[6];
size_t data_size = sizeof(data) / sizeof(data[0]);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::array&amp;#x3C;int, 6&gt; data_{};
cout &amp;#x3C;&amp;#x3C; data_.size() &amp;#x3C;&amp;#x3C; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Clear elements (C vs. CPP)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char letters[5] = {&apos;a&apos;, &apos;e&apos;, &apos;i&apos;, &apos;o&apos;, &apos;u&apos;};
memset(letters, 0, sizeof(letters));
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::string letters_{&quot;aeiou&quot;};
letters_.clear();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Iterating over maps&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;New in C++17&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::map&amp;#x3C;char, int&gt; _dict{{&apos;a&apos;, 17}, {&apos;b&apos;, 3}};
for(const auto&amp;#x26; [key, val] : _dict) {
    cout &amp;#x3C;&amp;#x3C; key &amp;#x3C;&amp;#x3C; val;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++ Iterators&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501181508116.png&quot; alt=&quot;image-20250118150754948&quot;&gt;&lt;/p&gt;
&lt;h3&gt;C++ Algorithm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sort(v.begin(), v.end())&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find(v.begin(), v.end(), val)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fill(v.begin(), v.end(), -1)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count(v.begin(), v.end(), val)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count_if(v.begin(), v.end(), [](int x) {return x % 3 == 0;})&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for_each(v.begin(), v.end(), [](const int&amp;#x26; n) {cout &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; n;})&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt; rotate(v.begin(), v.begin() + 2, v.end())&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transform(v.begin(), v.end(), [](char ch) { return std::toupper(ch); })&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;accumulate(v.begin(), v.end(), 0)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min_element(v.begin(), v.end())&lt;/code&gt;: &lt;code&gt;*min_element(v.begin(), v.end())&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto [min, max] = minmax_element(v.begin(), v.end())&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;C++ Utilities&lt;/h2&gt;
&lt;p&gt;✅ &lt;a href=&quot;https://www.youtube.com/watch?v=8IHAwmbUuKU&amp;#x26;list=PLgnQpQtFTOGRM59sr3nSL8BmeMZR9GCIA&amp;#x26;index=11&quot;&gt;Modern C++: I/O Files, Intro to Classes (Lecture 5, I. Vizzo, 2020)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;C++ includes a variety of utility libraries that provide functionality ranging from bit-counting to partial function application.&lt;/p&gt;
&lt;p&gt;These libraries can be broadly divided into two groups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;language support libraries&lt;/li&gt;
&lt;li&gt;general-purpose libraries&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Language support&lt;/h3&gt;
&lt;p&gt;Type support（&lt;code&gt;std::size_t&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;Dynamic memory management（&lt;code&gt;std::shared_ptr&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;Error handling（&lt;code&gt;std::exception&lt;/code&gt;，&lt;code&gt;assert&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;Initializer list（&lt;code&gt;std::vector{1, 2}&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;Much more ...&lt;/p&gt;
&lt;h3&gt;General-purpose libraries&lt;/h3&gt;
&lt;p&gt;Program utilities（&lt;code&gt;std::abort&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;Date and Time（&lt;code&gt;std::chronologically::duration&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;Optional, variant and any（&lt;code&gt;std::variant&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;Pairs and tuples（&lt;code&gt;std::tuple&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;Swap, forward and move（&lt;code&gt;std::move&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;Hash support（&lt;code&gt;std::hash&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;Formatting library (coming in C++20)&lt;/p&gt;
&lt;p&gt;Much more ...&lt;/p&gt;
&lt;h3&gt;Much more utilities&lt;/h3&gt;
&lt;p&gt;Just spend some time looking around: https://en.cppreference.com/w/cpp/utility&lt;/p&gt;
&lt;h2&gt;Error handling&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;跳过，不建议使用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We can &lt;code&gt;throw&lt;/code&gt; an exception if there is an error.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::exception&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;To use exceptions: &lt;code&gt;#include &amp;#x3C;stdexcept&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;An exception can be &quot;caught&quot; at any point of the program (&lt;code&gt;try - catch&lt;/code&gt;) and even  &quot;thrown&quot; further (&lt;code&gt;throw&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;The constructor of an exception receives a string error message as a parameter.&lt;/p&gt;
&lt;p&gt;This string can be called through a member function &lt;code&gt;what()&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Intuition&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Only used for &quot;exceptional behavior&quot;&lt;/p&gt;
&lt;p&gt;Often misused: e.g. wrong parameter should not lead to an exception.&lt;/p&gt;
&lt;p&gt;🔥 &lt;strong&gt;GOOGLE—STYLE&lt;/strong&gt;: Don&apos;t use exceptions.&lt;/p&gt;
&lt;p&gt;Link: https://en.cppreference.com/w/cpp/error&lt;/p&gt;
&lt;h2&gt;I/O Library&lt;/h2&gt;
&lt;h3&gt;read/write file&lt;/h3&gt;
&lt;p&gt;Use streams from STL&lt;/p&gt;
&lt;p&gt;Syntax similar to &lt;code&gt;cerr&lt;/code&gt;, &lt;code&gt;cout&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;fstream&gt;

using std::string;
using Mode = std::ios_base::openmode;

// input
std::ifstream f_in(string&amp;#x26; filename, Mode mode);

// output
std::ofstream f_out(string&amp;#x26; filename, Mode mode);

// in_output
std::fstream f_in_out(string&amp;#x26; filename, Mode mode);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are many modes under which a file can be opened.&lt;/p&gt;
&lt;p&gt;| Mode             | Meaning                     |
| ---------------- | --------------------------- |
| ios_base::app    | append output               |
| ios_base::ate    | seek to EOF when opened     |
| ios_base::binary | open file in binary mode    |
| ios_base::in     | open file for reading       |
| ios_base::out    | open file for writing       |
| ios_base::trunc  | overwrite the existing file |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;fstream&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;

using namespace std;

int main() {
  int i;
  double a, b;
  string s;
  ifstream in(&quot;test_cols.txt&quot;, ios_base::in);
  while (in &gt;&gt; i &gt;&gt; a &gt;&gt; s &gt;&gt; b) {
    // ...
  }
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iomanip&gt;
#include &amp;#x3C;fstream&gt;
using namespace std;
int main() {
    string filename = &quot;out.txt&quot;;
    ofstream outfile(filename);
    if(!outfile.is_open()) {
        return EXIT_FAILURE;
    }
    double a = 1.23456789;
    outfile &amp;#x3C;&amp;#x3C; fixed &amp;#x3C;&amp;#x3C; setprecision(20) &amp;#x3C;&amp;#x3C; a &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;directory_iterator&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;filesystem&gt;
#include &amp;#x3C;fstream&gt;
#include &amp;#x3C;iostream&gt;

namespace fs = std::filesystem;

int main() {
  fs::create_directories(&quot;sandbox/a/b&quot;);
  std::ofstream(&quot;sandbox/file1.txt&quot;);
  std::ofstream(&quot;sandbox/file2.txt&quot;);
  for (auto&amp;#x26; p : fs::directory_iterator(&quot;sandbox&quot;)) {
    std::cout &amp;#x3C;&amp;#x3C; p.path() &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
  }
  fs::remove_all(&quot;sandbox&quot;);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 必须指定 c++ 17 才可以编译
g++ -std=c++17 test.cpp -o test
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&quot;sandbox/a&quot;
&quot;sandbox/file1.txt&quot;
&quot;sandbox/file2.txt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;filesystem&gt;
#include &amp;#x3C;fstream&gt;
#include &amp;#x3C;iostream&gt;

namespace fs = std::filesystem;

int main() {
  std::cout &amp;#x3C;&amp;#x3C; fs::path(&quot;/foo/bar/txt&quot;).filename() &amp;#x3C;&amp;#x3C; &apos;\n&apos;
            &amp;#x3C;&amp;#x3C; fs::path(&quot;/foo/.bar&quot;).filename() &amp;#x3C;&amp;#x3C; &apos;\n&apos;
            &amp;#x3C;&amp;#x3C; fs::path(&quot;/foo/bar/&quot;).filename() &amp;#x3C;&amp;#x3C; &apos;\n&apos;
            &amp;#x3C;&amp;#x3C; fs::path(&quot;/foo/.&quot;).filename() &amp;#x3C;&amp;#x3C; &apos;\n&apos;
            &amp;#x3C;&amp;#x3C; fs::path(&quot;/foo/..&quot;).filename() &amp;#x3C;&amp;#x3C; &apos;\n&apos;;
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 必须指定 c++ 17 才可以编译
g++ -std=c++17 test.cpp -o test
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&quot;txt&quot;
&quot;.bar&quot;
&quot;&quot;
&quot;.&quot;
&quot;..&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;filesystem&gt;
#include &amp;#x3C;fstream&gt;
#include &amp;#x3C;iostream&gt;

namespace fs = std::filesystem;

int main() {
  std::cout &amp;#x3C;&amp;#x3C; fs::path(&quot;/foo/bar.txt&quot;).extension() &amp;#x3C;&amp;#x3C; &apos;\n&apos;
            &amp;#x3C;&amp;#x3C; fs::path(&quot;/foo/bar.&quot;).extension() &amp;#x3C;&amp;#x3C; &apos;\n&apos;
            &amp;#x3C;&amp;#x3C; fs::path(&quot;/foo/bar.png&quot;).extension() &amp;#x3C;&amp;#x3C; &apos;\n&apos;
            &amp;#x3C;&amp;#x3C; fs::path(&quot;/foo/.&quot;).extension() &amp;#x3C;&amp;#x3C; &apos;\n&apos;
            &amp;#x3C;&amp;#x3C; fs::path(&quot;/foo/..&quot;).extension() &amp;#x3C;&amp;#x3C; &apos;\n&apos;;
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;g++ -std=c++17 test.cpp -o test
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&quot;.txt&quot;
&quot;.&quot;
&quot;.png&quot;
&quot;&quot;
&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const fs::path&amp;#x26; p = &quot;...&quot;;
fs::(exists(p));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;C++ Classes&lt;/h2&gt;
&lt;p&gt;class glossary&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Class Definition&lt;/li&gt;
&lt;li&gt;Class Implementation&lt;/li&gt;
&lt;li&gt;Class data members&lt;/li&gt;
&lt;li&gt;Class Member functions&lt;/li&gt;
&lt;li&gt;Class Constructors&lt;/li&gt;
&lt;li&gt;Class Destructor&lt;/li&gt;
&lt;li&gt;Class setters&lt;/li&gt;
&lt;li&gt;Class getters&lt;/li&gt;
&lt;li&gt;Class operators&lt;/li&gt;
&lt;li&gt;Class static members&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By default everything is &lt;code&gt;private&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Access members with a &quot;.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GOOGLE—STYLE&lt;/strong&gt;: All data must be &lt;code&gt;private&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;What about structs?&lt;/h2&gt;
&lt;p&gt;Definition starts with the keyword &lt;code&gt;struct&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct ExampleStruct {
    Type value;
    Type value;
    Type value;
    // No functions!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;struct&lt;/code&gt; is a &lt;code&gt;class&lt;/code&gt; where everything is &lt;code&gt;public&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;🔥 &lt;strong&gt;GOOGLE—STYLE&lt;/strong&gt;: Use &lt;code&gt;struct&lt;/code&gt; as a simple data container, if it needs a function it should be a &lt;code&gt;class&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;🔥 Always initialize structs using braced initialization, such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct namedInt {
	int num;
    std::string name;
};

namedInt var{1, std::string{&quot;hello&quot;}};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Const correctness&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;const&lt;/code&gt; after function states that this function &lt;strong&gt;does not change the object&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Mark all functions that &lt;strong&gt;should not&lt;/strong&gt; change the state of the object as &lt;code&gt;const&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Ensures that we can pass objects by a &lt;code&gt;const&lt;/code&gt; reference and still call their functions.&lt;/p&gt;
&lt;p&gt;Substantially reduces number of errors.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Typical const error&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;

using namespace std;

class Student {
 public:
  Student(string name) : name_(name) {}

  // This function *might* change the object
  const string&amp;#x26; getName() { return name_; }
  // correct: const string&amp;#x26; getName() const { return name_; }

 private:
  string name_;
};

void Print(const Student&amp;#x26; student) { cout &amp;#x3C;&amp;#x3C; student.getName() &amp;#x3C;&amp;#x3C; endl; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不在 getName() 后面加上 &lt;code&gt;const&lt;/code&gt;，那么编译器不能保证你不会改变它，除了 setter，一般都需要加上 const吗，这能很大程度上避免错误。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;std::Move()&lt;/code&gt; semantics&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;c++11 引入 move()&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所有权转移，用于将左值显示转换为右值，允许通过移动（而不是复制）资源来优化性能。&lt;/p&gt;
&lt;h2&gt;Smart pointers&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Smart pointers wrap a raw pointer into a class and manage its lifetime (&lt;strong&gt;RAII&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Smart opinters are all about ownership&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only use them with heap memory!&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#include &amp;#x3C;memory&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;C++ 11 Smart pointers types&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::auto_ptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;🔥 &lt;code&gt;std::unique_ptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;🔥 &lt;code&gt;std::shared_ptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::weak_ptr&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;因为使用的是 unique，不能直接用 = 复制，但是可以转移所有权，那就使用 &lt;code&gt;move()&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501181829054.png&quot; alt=&quot;image-20250118182925534&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Markdown 数学公式指导手册</title><link>https://coooredump.github.io/blog/markdown/markdown-mathjax-basic-tutorial-and-quick-reference</link><guid isPermaLink="true">https://coooredump.github.io/blog/markdown/markdown-mathjax-basic-tutorial-and-quick-reference</guid><description>MathJax 速查手册</description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;用 Katex 改写数学公式，发现右括号接下标，不能正常解析，&lt;code&gt;)&lt;/code&gt;改成 &lt;code&gt;\rparen&lt;/code&gt;, &lt;code&gt;]&lt;/code&gt;改成 &lt;code&gt;\rbrack&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;一、公式使用参考&lt;/h2&gt;
&lt;h3&gt;1．如何插入公式&lt;/h3&gt;
&lt;p&gt;LaTex 的数学公式有两种：行中公式和独立公式。行中公式放在文中与其它文字混编，独立公式单独成行。&lt;/p&gt;
&lt;p&gt;行中公式示例: &lt;code&gt;$\sum_{i=0}^n i^2 = \frac{(n^2+n)(2n+1)}{6}$&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;独立公式示例：&lt;code&gt;$$\sum_{i=0}^n i^2 = \frac{(n^2+n)(2n+1)}{6}$$&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=0}^n i^2 = \frac{(n^2+n)(2n+1)}{6}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自动编号的公式可以用如下方法表示：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若需要手动编号，参见 &lt;a href=&quot;https://freeopen.github.io/mathjax/#14-da-gua-hao-he-xing-biao-de-shi-yong&quot;&gt;大括号和行标的使用&lt;/a&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{equation}
数学公式
\label{eq:当前公式名}
\end{equation}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自动编号后的公式可在全文任意处使用 &lt;code&gt;\eqref{eq:公式名}&lt;/code&gt; 语句引用。&lt;/p&gt;
&lt;h3&gt;2．如何输入上下标&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;^&lt;/code&gt; 表示上标,&lt;code&gt;_&lt;/code&gt; 表示下标。如果上下标的内容多于一个字符，需要用 &lt;code&gt;{}&lt;/code&gt; 将这些内容括成一个整体。上下标可以嵌套，也可以同时使用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$x^{y^z}=(1+{\rm e}^x)^{-2xy^w} $$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
x^{y^z}=(1+{\rm e}^x)^{-2xy^w}
$$&lt;/p&gt;
&lt;h3&gt;3．如何输入括号和分隔符&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;()&lt;/code&gt;、&lt;code&gt;[]&lt;/code&gt; 和 &lt;code&gt;|&lt;/code&gt; 表示符号本身，使用 &lt;code&gt;\{\}&lt;/code&gt; 来表示 &lt;code&gt;{}&lt;/code&gt; 。当要显示大号的括号或分隔符时，要用 &lt;code&gt;\left&lt;/code&gt; 和 &lt;code&gt;\right&lt;/code&gt; 命令。&lt;/p&gt;
&lt;p&gt;一些特殊的括号：&lt;/p&gt;
&lt;p&gt;| 输入 | 显示 | 输入 | 显示 |
| --- | --- | --- | --- |
| \langle | $\langle$ | \rangle | $\rangle$ |
| \lceil | $\lceil$ | \rceil | $\rceil$ |
| \lfloor | $\lfloor$ | \rfloor | $\rfloor$ |
| \lbrace | $\lbrace$ | \rbrace | $\rbrace$ |
| \lvert | $\lvert$ | \rvert | $\rvert$ |
| \lVert | $\lVert$ | \rVert | $\rVert$ |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$ f(x,y,z) = 3y^2z \left( 3+\frac{7x+5}{1+y^2} \right) $$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
f(x,y,z) = 3y^2z \left( 3+\frac{7x+5}{1+y^2} \right)
$$&lt;/p&gt;
&lt;p&gt;有时候要用 &lt;code&gt;\left.&lt;/code&gt; 或 &lt;code&gt;\right.&lt;/code&gt; 进行匹配而不显示本身。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$\left. \frac{{\rm d}u}{{\rm d}x} \right| _{x=0}$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\left. \frac{{\rm d}u}{{\rm d}x} \right|_{x=0}
$$&lt;/p&gt;
&lt;h3&gt;4．如何输入分数&lt;/h3&gt;
&lt;p&gt;通常使用 &lt;code&gt;\frac {分子} {分母}&lt;/code&gt; 命令产生一个分数，分数可嵌套。 便捷情况可直接输入 &lt;code&gt;\frac{a}{b}&lt;/code&gt; 来快速生成一个 。 如果分式很复杂，亦可使用 &lt;code&gt;分子 \over 分母&lt;/code&gt; 命令，此时分数仅有一层。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$\frac{a-1}{b-1} \quad and \quad {a+1\over b+1}$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\frac{a-1}{b-1} \quad and \quad {a+1\over b+1}
$$&lt;/p&gt;
&lt;h3&gt;5．如何输入开方&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;\sqrt [根指数，省略时为2] {被开方数}&lt;/code&gt; 命令输入开方。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$\sqrt{2} \quad and \quad \sqrt[n]{3}$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\sqrt{2} \quad and \quad \sqrt[n]{3}
$$&lt;/p&gt;
&lt;h3&gt;6．如何输入省略号&lt;/h3&gt;
&lt;p&gt;数学公式中常见的省略号有两种，&lt;code&gt;\ldots&lt;/code&gt; 表示与文本底线对齐的省略号，&lt;code&gt;\cdots&lt;/code&gt; 表示与文本中线对齐的省略号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$f(x_1,x_2,\ldots ,x_n) = x_1^2 + x_2^2 + \cdots + x_n^2$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
f(x_1,x_2,\ldots ,x_n) = x_1^2 + x_2^2 + \cdots + x_n^2
$$&lt;/p&gt;
&lt;h3&gt;7．如何输入矢量&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;\vec{矢量}&lt;/code&gt; 来自动产生一个矢量。也可以使用 &lt;code&gt;\overrightarrow&lt;/code&gt; 等命令自定义字母上方的符号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$ \vec{a} \cdot \vec{b}=0 $$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\vec{a} \cdot \vec{b}=0
$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$ \overleftarrow{xy} \quad and \quad \overleftrightarrow{xy} \quad and \quad \overrightarrow{xy} $$
$$ xy \text{ with arrows:} \quad \overleftarrow{xy} \\; \mid \\; \overleftrightarrow{xy} \\; \mid \\; \overrightarrow{xy} $$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\overleftarrow{xy} \quad and \quad \overleftrightarrow{xy} \quad and \quad \overrightarrow{xy} \
xy \text{ with arrows:} \quad \overleftarrow{xy} ; \mid ; \overleftrightarrow{xy} ; \mid ; \overrightarrow{xy}
$$&lt;/p&gt;
&lt;h3&gt;8．如何输入积分&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;\int_积分下限^积分上限 {被积表达式}&lt;/code&gt; 来输入一个积分。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$\int_0^1 {x^2} \,{\rm d}x$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\int_0^1 {x^2} ,{\rm d}x
$$&lt;/p&gt;
&lt;p&gt;本例中 &lt;code&gt;\,&lt;/code&gt; 和 &lt;code&gt;{\rm d}&lt;/code&gt; 部分可省略，但建议加入，能使式子更美观。&lt;/p&gt;
&lt;h3&gt;9．如何输入极限运算&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;\lim_{变量 \to 表达式}&lt;/code&gt; 表达式 来输入一个极限。如有需求，可以更改 &lt;code&gt;\to&lt;/code&gt; 符号至任意符号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$ \lim_{n \to +\infty} \frac{1}{n(n+1)} \quad and \quad \lim_{x\leftarrow{示例}} \frac{1}{n(n+1)} $$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\lim_{n \to +\infty} \frac{1}{n(n+1)} \quad and \quad \lim_{x\leftarrow{示例}} \frac{1}{n(n+1)}
$$&lt;/p&gt;
&lt;h3&gt;10．如何输入累加、累乘运算&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;\sum_{下标表达式}^{上标表达式} {累加表达式}&lt;/code&gt; 来输入一个累加。 与之类似，使用 &lt;code&gt;\prod \bigcup \bigcap&lt;/code&gt; 来分别输入累乘、并集和交集。 此类符号在行内显示时上下标表达式将会移至右上角和右下角。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$ sum_{i=1}^n \frac{1}{i^2} \quad and \quad \prod_{i=1}^n \frac{1}{i^2} \quad and \quad \bigcup_{i=1}^{2} R $$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\sum_{i=1}^n \frac{1}{i^2} \quad and \quad \prod_{i=1}^n \frac{1}{i^2} \quad and \quad \bigcup_{i=1}^{2} R
$$&lt;/p&gt;
&lt;h3&gt;11．如何输入希腊字母&lt;/h3&gt;
&lt;p&gt;输入 &lt;code&gt;\小写希腊字母英文全称&lt;/code&gt; 和 &lt;code&gt;\首字母大写希腊字母英文全称&lt;/code&gt; 来分别输入小写和大写希腊字母。 对于大写希腊字母与现有字母相同的，直接输入大写字母即可。&lt;/p&gt;
&lt;p&gt;| 输入 | 显示 | 输入 | 显示 | 输入 | 显示 | 输入 | 显示 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| \alpha | $\alpha$ | A | $A$ | \beta | $\beta$ | B | $B$ |
| \gamma | $\gamma$ | \Gamma | $\Gamma$ | \delta | $\delta$ | \Delta | $\Delta$ |
| \epsilon | $\epsilon$ | E | $E$ | \zeta | $\zeta$ | Z | $Z$ |
| \eta | $\eta$ | H | $H$ | \theta | $\theta$ | \Theta | $\Theta$ |
| \iota | $\iota$ | I | $I$ | \kappa | $\kappa$ | K | $K$ |
| \lambda | $\lambda$ | \Lambda | $\Lambda$ | \mu | $\mu$ | M | $M$ |
| \nu | $\nu$ | N | $N$ | \xi | $\xi$ | \Xi | $\Xi$ |
| o | $o$ | O | $O$ | \pi | $\pi$ | \Pi | $\Pi$ |
| \rho | $\rho$ | P | $P$ | \sigma | $\sigma$ | \Sigma | $\Sigma$ |
| \tau | $\tau$ | T | $T$ | \upsilon | $\upsilon$ | \Upsilon | $\Upsilon$ |
| \phi | $\phi$ | \Phi | $\Phi$ | \chi | $\chi$ | X | $X$ |
| \psi | $\psi$ | \Psi | $\Psi$ | \omega | $\omega$ | \Omega | $\Omega$ |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;部分字母有变量专用形式，以 &lt;code&gt;\var&lt;/code&gt; 开头。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 小写 | 大写 | 变量形式 | 显示 |
| --- | --- | --- | --- |
| \epsilon | E | \varepsilon | $\epsilon$|$E$|$\varepsilon$ |
| \theta | \Theta | \vartheta | $\theta$|$\Theta$|$\vartheta$ |
| \rho | P | \varrho | $\rho$|$P$|$\varrho$ |
| \sigma | \Sigma | \varsigma | $\sigma$|$\Sigma$|$\varsigma$ |
| \phi | \Phi | \varphi | $\phi$|$\Phi$|$\varphi$ |&lt;/p&gt;
&lt;h3&gt;12．如何输入其它特殊字符&lt;/h3&gt;
&lt;p&gt;若需要显示更大或更小的字符，在符号前插入 &lt;code&gt;\large&lt;/code&gt; 或 &lt;code&gt;\small&lt;/code&gt; 命令。&lt;/p&gt;
&lt;p&gt;若找不到需要的符号，使用 &lt;a href=&quot;http://detexify.kirelabs.org/classify.html&quot;&gt;Detexify&lt;/a&gt; 来画出想要的符号。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(1) 关系运算符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 输入 | 显示 | 输入 | 显示 | 输入 | 显示 | 输入 | 显示 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| \pm | $\pm$ | \times | $\times$ | \div | $\div$ | \mid | $\mid$ |
| \nmid | $\nmid$ | \cdot | $\cdot$ | \circ | $\circ$ | \ast | $\ast$ |
| \bigodot | $\bigodot$ | \bigotimes | $\bigotimes$ | \bigoplus | $\bigoplus$ | \leq | $\leq$ |
| \geq | $\geq$ | \neq | $\neq$ | \approx | $\approx$ | \equiv | $\equiv$ |
| \sum | $\sum$ | \prod | $\prod$ | \coprod | $\coprod$ | \backslash | $\backslash$ |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(2) 集合运算符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 输入 | 显示 | 输入 | 显示 | 输入 | 显示 |
| --- | --- | --- | --- | --- | --- |
| \emptyset | $\emptyset$ | \in | $\in$ | \notin | $\notin$ |
| \subset | $\subset$ | \supset | $\supset$ | \subseteq | $\subseteq$ |
| \supseteq | $\supseteq$ | \bigcap | $\bigcap$ | \bigcup | $\bigcup$ |
| \bigvee | $\bigvee$ | \bigwedge | $\bigwedge$ | \biguplus | $\biguplus$ |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(3) 对数运算符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 输入 | 显示 | 输入 | 显示 | 输入 | 显示 |
| --- | --- | --- | --- | --- | --- |
| \log | $\log$ | \lg | $\lg$ | \ln | $\ln$ |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(4) 三角运算符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 输入 | 显示 | 输入 | 显示 | 输入 | 显示 |
| --- | --- | --- | --- | --- | --- |
| 30^\circ | $30^\circ$ | \bot | $\bot$ | \angle A | $\angle A$ |
| \sin | $\sin$ | \cos | $\cos$ | \tan | $\tan$ |
| \csc | $\csc$ | \sec | $\sec$ | \cot | $\cot$ |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(5) 微积分运算符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 输入 | 显示 | 输入 | 显示 | 输入 | 显示 |
| --- | --- | --- | --- | --- | --- |
| \int | $\int$ | \iint | $\iint$ | \iiint | $\iiint$ |
| \partial | $\partial$ | \oint | $\oint$ | \prime | $\prime$ |
| \lim | $\lim$ | \infty | $\infty$ | \nabla | $\nabla$ |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(6) 逻辑运算符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 输入 | 显示 | 输入 | 显示 |
| --- | --- | --- | --- |
| \because | $\because$ | \therefore | $\therefore$ |
| \forall | $\forall$ | \exists | $\exists$ |
| \not\subset | $\not\subset$ | \not&amp;#x3C; | $\not&amp;#x3C;$ |
| \not&gt; | $\not&gt;$ | \not= | $\not=$ |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(7) 戴帽符号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 输入 | 显示 | 输入 | 显示 |
| --- | --- | --- | --- |
| \hat{xy} | $\hat{xy}$ | \widehat{xyz} | $\widehat{xyz}$ |
| \tilde{xy} | $\tilde{xy}$ | \widetilde{xyz} | $\widetilde{xyz}$ |
| \check{x} | $\check{x}$ | \breve{y} | $\breve{y}$ |
| \grave{x} | $\grave{x}$ | \acute{y} | $\acute{y}$ |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(8) 连线符号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 输入 | 显示 |
| --- | --- |
| \fbox{a+b+c+d} | $\fbox{a+b+c+d}$ |
| \overleftarrow{a+b+c+d} | $\overleftarrow{a+b+c+d}$ |
| \overrightarrow{a+b+c+d} | $\overrightarrow{a+b+c+d}$ |
| \overleftrightarrow{a+b+c+d} | $\overleftrightarrow{a+b+c+d}$ |
| \underleftarrow{a+b+c+d} | $\underleftarrow{a+b+c+d}$ |
| \underrightarrow{a+b+c+d} | $\underrightarrow{a+b+c+d}$ |
| \underleftrightarrow{a+b+c+d} | $\underleftrightarrow{a+b+c+d}$ |
| \overline{a+b+c+d} | $\overline{a+b+c+d}$ |
| \underline{a+b+c+d} | $\underline{a+b+c+d}$ |
| \overbrace{a+b+c+d}^{Sample} | $\overbrace{a+b+c+d}^{Sample}$ |
| \underbrace{a+b+c+d}_{Sample} | $\underbrace{a+b+c+d}&lt;em&gt;{Sample}$ |
| \overbrace{a+\underbrace{b+c}_{1.0}+d}^{2.0} | $\overbrace{a+\underbrace{b+c}&lt;/em&gt;{1.0}+d}^{2.0}$ |
| \underbrace{a\cdot a\cdots a}_{b\text{ times}} | $\underbrace{a\cdot a\cdots a}_{b\text{ times}}$ |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(9) 箭头符号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 输入 | 显示 | 输入 | 显示 |
| --- | --- | --- | --- |
| \uparrow | $\uparrow$ | \Uparrow | $\Uparrow$ |
| \downarrow | $\downarrow$ | \Downarrow | $\Downarrow$ |
| \leftarrow | $\leftarrow$ | \Leftarrow | $\Leftarrow$ |
| \rightarrow or \to | $\to$ | \Rightarrow | $\Rightarrow$ |
| \leftrightarrow | $\leftrightarrow$ | \Leftrightarrow | $\Leftrightarrow$ |
| \longleftarrow | $\longleftarrow$ | \Longleftarrow or \impliedby | $\Longleftarrow$ |
| \longrightarrow | $\longrightarrow$ | \Longrightarrow or \implies | $\Longrightarrow$ |
| \longleftrightarrow | $\longleftrightarrow$ | \Longleftrightarrow or \iff |   $\iff$ |
| \mapsto | $\mapsto$ | \underrightarrow{1℃/min} | $\underrightarrow{1℃/min}$ |&lt;/p&gt;
&lt;h3&gt;13．如何进行字体转换&lt;/h3&gt;
&lt;p&gt;若要对公式的某一部分字符进行字体转换，可以用 &lt;code&gt;{\字体 {需转换的部分字符}}&lt;/code&gt; 命令，其中 &lt;code&gt;\字体&lt;/code&gt; 部分可以参照下表选择合适的字体。一般情况下，公式默认为意大利体 。&lt;/p&gt;
&lt;p&gt;示例中 &lt;strong&gt;全部大写&lt;/strong&gt; 的字体仅大写可用。&lt;/p&gt;
&lt;p&gt;| 输入 | 说明 | 显示 | 输入 | 说明 | 显示 |
| --- | --- | --- | --- | --- | --- |
| \rm | 罗马体 | $\rm{Sample}$ | \cal | 花体 | $\cal{SAMPLE}$ |
| \it | 意大利体 | $\it{Sample}$ | \Bbb | 黑板粗体 | $\Bbb{SAMPLE}$ |
| \bf | 粗体 | $\rm{Sample}$ | \mathit | 数字斜体 | $\mathit{SAMPLE}$ |
| \sf | 等线体 | $\sf{Sample}$ | \mathscr | 手写体 | $\mathscr{SAMPLE}$ |
| \tt | 打字机体 | $\tt{Sample}$ |  |  |  |
| \frak | 旧德式体 | $\frak{Sample}$ |  |  |  |&lt;/p&gt;
&lt;p&gt;转换字体十分常用，例如在积分中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{array}{c | c}
\mathrm{Bad} &amp;#x26; \mathrm{Better} \\\\
\hline \\\\
\int_0^1 x^2 dx &amp;#x26; \int_0^1 x^2 \\,{\rm d}x
\end{array}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{array}{c | c} \mathrm{Bad} &amp;#x26; \mathrm{Better} \ \hline \ \int_0^1 x^2 dx &amp;#x26; \int_0^1 x^2 ,{\rm d}x \end{array}
$$&lt;/p&gt;
&lt;p&gt;注意比较两个式子间 &lt;code&gt;dx&lt;/code&gt; 的不同。 使用 &lt;code&gt;\operatorname&lt;/code&gt; 命令也可以达到相同的效果，详见 &lt;a href=&quot;https://freeopen.github.io/mathjax/#15-qi-ta-ming-ling&quot;&gt;定义新的符号 &lt;code&gt;\operatorname&lt;/code&gt;&lt;/a&gt; 。&lt;/p&gt;
&lt;h3&gt;14．大括号和行标的使用&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;\left&lt;/code&gt; 和 &lt;code&gt;\right&lt;/code&gt; 来创建自动匹配高度的 (圆括号)，[方括号] 和 {花括号} 。 在每个公式末尾前使用 &lt;code&gt;\tag{行标}&lt;/code&gt; 来实现行标。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
f\left(
	\left[
		\frac{
			1+\left\\{x,y\right\\}
        }{
            \left(
            \frac xy + \frac yx
            \right)
            (u+1)
        }+a
    \right]^{3/2}
\right)
\tag {行标}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
f\left( \left[  \frac{ 1+\left{x,y\right} }{ \left( \frac xy + \frac yx \right) (u+1) }+a \right]^{3/2} \right) \tag {行标}
$$&lt;/p&gt;
&lt;p&gt;如果你需要在不同的行显示对应括号，可以在每一行对应处使用 &lt;code&gt;\left.&lt;/code&gt; 或 &lt;code&gt;\right.&lt;/code&gt; 来放一个“影子“括号：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{aligned}
a=&amp;#x26;\left(1+2+3+ \cdots \right. \\\\
&amp;#x26; \cdots+ \left. \infty-2+\infty-1+\infty\right)
\end{aligned}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{aligned} a=&amp;#x26;\left(1+2+3+  \cdots \right. \ &amp;#x26; \cdots+ \left. \infty-2+\infty-1+\infty\right) \end{aligned}
$$&lt;/p&gt;
&lt;h3&gt;15．其它命令&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;(1) 定义新的符号 &lt;code&gt;\operatorname&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查询 &lt;a href=&quot;http://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference/15077#15077&quot;&gt;关于此命令的定义&lt;/a&gt; 和 &lt;a href=&quot;http://meta.math.stackexchange.com/search?q=operatorname&quot;&gt;关于此命令的讨论&lt;/a&gt; 来进一步了解此命令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$ \operatorname{Symbol} A $$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\operatorname{Symbol} A
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(2) 添加注释文字 &lt;code&gt;\text&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 &lt;code&gt;\text {文字}&lt;/code&gt; 中仍可以使用 &lt;code&gt;$公式$&lt;/code&gt; 插入其它公式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$ f(n)= \begin{cases} n/2, &amp;#x26; \text {if $n$ is even} \\\\ 3n+1, &amp;#x26; \text{if $n$ is odd} \end{cases} $$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
f(n)= \begin{cases} n/2, &amp;#x26; \text {if $n$ is even} \ 3n+1, &amp;#x26; \text{if $n$ is odd} \end{cases}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(3) 在字符间加入空格&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有四种宽度的空格可以使用： &lt;code&gt;\,&lt;/code&gt;、&lt;code&gt;\;&lt;/code&gt;、&lt;code&gt;\quad&lt;/code&gt; 和 &lt;code&gt;\qquad&lt;/code&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$ a \, b \mid a \; b \mid a \quad b \mid a \qquad b $$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
a , b \mid a ; b \mid a \quad b \mid a \qquad b
$$&lt;/p&gt;
&lt;p&gt;当然，使用 &lt;code&gt;\text {n个空格}&lt;/code&gt; 也可以达到同样效果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(4) 更改文字颜色&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 &lt;code&gt;\color{颜色}{文字}&lt;/code&gt; 来更改特定的文字颜色。 更改文字颜色需要浏览器支持，如果浏览器不知道你所需的颜色，那么文字将被渲染为黑色。&lt;/p&gt;
&lt;p&gt;对于较旧的浏览器（HTML4与CSS2），支持的颜色较少; 对于较新的浏览器（HTML5与CSS3），额外的124种颜色将被支持 输入 &lt;code&gt;\color {#rgb} {text}&lt;/code&gt; 来自定义更多的颜色，其中 &lt;code&gt;#rgb&lt;/code&gt; 的 &lt;code&gt;r g b&lt;/code&gt; 可输入 &lt;code&gt;0-9&lt;/code&gt; 和 &lt;code&gt;a-f&lt;/code&gt; 来表示红色、绿色和蓝色的纯度（饱和度）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;\begin{array}{|rrrrrrrr|}
\hline
\verb+#000+ &amp;#x26;amp; \color{#000}{text} &amp;#x26;amp; \verb+#005+ &amp;#x26;amp; \color{#005}{text} &amp;#x26;amp; \verb+#00A+ &amp;#x26;amp; \color{#00A}{text} &amp;#x26;amp; \verb+#00F+ &amp;#x26;amp; \color{#00F}{text}  \\\\
\verb+#500+ &amp;#x26;amp; \color{#500}{text} &amp;#x26;amp; \verb+#505+ &amp;#x26;amp; \color{#505}{text} &amp;#x26;amp; \verb+#50A+ &amp;#x26;amp; \color{#50A}{text} &amp;#x26;amp; \verb+#50F+ &amp;#x26;amp; \color{#50F}{text}  \\\\
\verb+#A00+ &amp;#x26;amp; \color{#A00}{text} &amp;#x26;amp; \verb+#A05+ &amp;#x26;amp; \color{#A05}{text} &amp;#x26;amp; \verb+#A0A+ &amp;#x26;amp; \color{#A0A}{text} &amp;#x26;amp; \verb+#A0F+ &amp;#x26;amp; \color{#A0F}{text}  \\\\
\verb+#F00+ &amp;#x26;amp; \color{#F00}{text} &amp;#x26;amp; \verb+#F05+ &amp;#x26;amp; \color{#F05}{text} &amp;#x26;amp; \verb+#F0A+ &amp;#x26;amp; \color{#F0A}{text} &amp;#x26;amp; \verb+#F0F+ &amp;#x26;amp; \color{#F0F}{text}  \\\\
\hline
\verb+#080+ &amp;#x26;amp; \color{#080}{text} &amp;#x26;amp; \verb+#085+ &amp;#x26;amp; \color{#085}{text} &amp;#x26;amp; \verb+#08A+ &amp;#x26;amp; \color{#08A}{text} &amp;#x26;amp; \verb+#08F+ &amp;#x26;amp; \color{#08F}{text}  \\\\
\verb+#580+ &amp;#x26;amp; \color{#580}{text} &amp;#x26;amp; \verb+#585+ &amp;#x26;amp; \color{#585}{text} &amp;#x26;amp; \verb+#58A+ &amp;#x26;amp; \color{#58A}{text} &amp;#x26;amp; \verb+#58F+ &amp;#x26;amp; \color{#58F}{text}  \\\\
\verb+#A80+ &amp;#x26;amp; \color{#A80}{text} &amp;#x26;amp; \verb+#A85+ &amp;#x26;amp; \color{#A85}{text} &amp;#x26;amp; \verb+#A8A+ &amp;#x26;amp; \color{#A8A}{text} &amp;#x26;amp; \verb+#A8F+ &amp;#x26;amp; \color{#A8F}{text}  \\\\
\verb+#F80+ &amp;#x26;amp; \color{#F80}{text} &amp;#x26;amp; \verb+#F85+ &amp;#x26;amp; \color{#F85}{text} &amp;#x26;amp; \verb+#F8A+ &amp;#x26;amp; \color{#F8A}{text} &amp;#x26;amp; \verb+#F8F+ &amp;#x26;amp; \color{#F8F}{text}  \\\\
\hline
\verb+#0F0+ &amp;#x26;amp; \color{#0F0}{text} &amp;#x26;amp; \verb+#0F5+ &amp;#x26;amp; \color{#0F5}{text} &amp;#x26;amp; \verb+#0FA+ &amp;#x26;amp; \color{#0FA}{text} &amp;#x26;amp; \verb+#0FF+ &amp;#x26;amp; \color{#0FF}{text}  \\\\
\verb+#5F0+ &amp;#x26;amp; \color{#5F0}{text} &amp;#x26;amp; \verb+#5F5+ &amp;#x26;amp; \color{#5F5}{text} &amp;#x26;amp; \verb+#5FA+ &amp;#x26;amp; \color{#5FA}{text} &amp;#x26;amp; \verb+#5FF+ &amp;#x26;amp; \color{#5FF}{text}  \\\\
\verb+#AF0+ &amp;#x26;amp; \color{#AF0}{text} &amp;#x26;amp; \verb+#AF5+ &amp;#x26;amp; \color{#AF5}{text} &amp;#x26;amp; \verb+#AFA+ &amp;#x26;amp; \color{#AFA}{text} &amp;#x26;amp; \verb+#AFF+ &amp;#x26;amp; \color{#AFF}{text}  \\\\
\verb+#FF0+ &amp;#x26;amp; \color{#FF0}{text} &amp;#x26;amp; \verb+#FF5+ &amp;#x26;amp; \color{#FF5}{text} &amp;#x26;amp; \verb+#FFA+ &amp;#x26;amp; \color{#FFA}{text} &amp;#x26;amp; \verb+#FFF+ &amp;#x26;amp; \color{#FFF}{text}  \\\\
\hline
\end{array}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{array}{|rrrrrrrr|} \hline \verb+#000+ &amp;#x26; \color{#000}{text} &amp;#x26; \verb+#005+ &amp;#x26; \color{#005}{text} &amp;#x26; \verb+#00A+ &amp;#x26; \color{#00A}{text} &amp;#x26; \verb+#00F+ &amp;#x26; \color{#00F}{text}  \ \verb+#500+ &amp;#x26; \color{#500}{text} &amp;#x26; \verb+#505+ &amp;#x26; \color{#505}{text} &amp;#x26; \verb+#50A+ &amp;#x26; \color{#50A}{text} &amp;#x26; \verb+#50F+ &amp;#x26; \color{#50F}{text}  \ \verb+#A00+ &amp;#x26; \color{#A00}{text} &amp;#x26; \verb+#A05+ &amp;#x26; \color{#A05}{text} &amp;#x26; \verb+#A0A+ &amp;#x26; \color{#A0A}{text} &amp;#x26; \verb+#A0F+ &amp;#x26; \color{#A0F}{text}  \ \verb+#F00+ &amp;#x26; \color{#F00}{text} &amp;#x26; \verb+#F05+ &amp;#x26; \color{#F05}{text} &amp;#x26; \verb+#F0A+ &amp;#x26; \color{#F0A}{text} &amp;#x26; \verb+#F0F+ &amp;#x26; \color{#F0F}{text}  \ \hline \verb+#080+ &amp;#x26; \color{#080}{text} &amp;#x26; \verb+#085+ &amp;#x26; \color{#085}{text} &amp;#x26; \verb+#08A+ &amp;#x26; \color{#08A}{text} &amp;#x26; \verb+#08F+ &amp;#x26; \color{#08F}{text}  \ \verb+#580+ &amp;#x26; \color{#580}{text} &amp;#x26; \verb+#585+ &amp;#x26; \color{#585}{text} &amp;#x26; \verb+#58A+ &amp;#x26; \color{#58A}{text} &amp;#x26; \verb+#58F+ &amp;#x26; \color{#58F}{text}  \ \verb+#A80+ &amp;#x26; \color{#A80}{text} &amp;#x26; \verb+#A85+ &amp;#x26; \color{#A85}{text} &amp;#x26; \verb+#A8A+ &amp;#x26; \color{#A8A}{text} &amp;#x26; \verb+#A8F+ &amp;#x26; \color{#A8F}{text}  \ \verb+#F80+ &amp;#x26; \color{#F80}{text} &amp;#x26; \verb+#F85+ &amp;#x26; \color{#F85}{text} &amp;#x26; \verb+#F8A+ &amp;#x26; \color{#F8A}{text} &amp;#x26; \verb+#F8F+ &amp;#x26; \color{#F8F}{text}  \ \hline \verb+#0F0+ &amp;#x26; \color{#0F0}{text} &amp;#x26; \verb+#0F5+ &amp;#x26; \color{#0F5}{text} &amp;#x26; \verb+#0FA+ &amp;#x26; \color{#0FA}{text} &amp;#x26; \verb+#0FF+ &amp;#x26; \color{#0FF}{text}  \ \verb+#5F0+ &amp;#x26; \color{#5F0}{text} &amp;#x26; \verb+#5F5+ &amp;#x26; \color{#5F5}{text} &amp;#x26; \verb+#5FA+ &amp;#x26; \color{#5FA}{text} &amp;#x26; \verb+#5FF+ &amp;#x26; \color{#5FF}{text}  \ \verb+#AF0+ &amp;#x26; \color{#AF0}{text} &amp;#x26; \verb+#AF5+ &amp;#x26; \color{#AF5}{text} &amp;#x26; \verb+#AFA+ &amp;#x26; \color{#AFA}{text} &amp;#x26; \verb+#AFF+ &amp;#x26; \color{#AFF}{text}  \ \verb+#FF0+ &amp;#x26; \color{#FF0}{text} &amp;#x26; \verb+#FF5+ &amp;#x26; \color{#FF5}{text} &amp;#x26; \verb+#FFA+ &amp;#x26; \color{#FFA}{text} &amp;#x26; \verb+#FFF+ &amp;#x26; \color{#FFF}{text}  \ \hline \end{array}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(5) 添加删除线&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用删除线功能必须声明 &lt;code&gt;$$&lt;/code&gt; 符号。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(注意: katex 不需要&lt;code&gt;\\require{cancel}&lt;/code&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在公式内使用 &lt;code&gt;\require{cancel}&lt;/code&gt; 来允许 &lt;em&gt;片段删除线&lt;/em&gt; 的显示。 声明片段删除线后，使用 &lt;code&gt;\cancel{字符}&lt;/code&gt;、&lt;code&gt;\bcancel{字符}&lt;/code&gt;、&lt;code&gt;\xcancel{字符}&lt;/code&gt; 和 &lt;code&gt;\cancelto{字符}&lt;/code&gt; 来实现各种片段删除线效果。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;\begin{array}{rl}
\verb|y+\cancel{x}| &amp;#x26;amp; y+\cancel{x}\\\\
\verb|\cancel{y+x}| &amp;#x26;amp; \cancel{y+x}\\\\
\verb|y+\bcancel{x}| &amp;#x26;amp; y+\bcancel{x}\\\\
\verb|y+\xcancel{x}| &amp;#x26;amp; y+\xcancel{x}\\\\
\verb|y+\cancelto{0}{x}| &amp;#x26;amp; y+\cancelto{0}{x}\\\\ (katex 不支持)
\verb+\frac{1\cancel9}{\cancel95} = \frac15+&amp;#x26;amp; \frac{1\cancel9}{\cancel95} = \frac15 \\
\end{array}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{array}{rl} \verb|y+\cancel{x}| &amp;#x26; y+\cancel{x}\ \verb|\cancel{y+x}| &amp;#x26; \cancel{y+x}\ \verb|y+\bcancel{x}| &amp;#x26; y+\bcancel{x}\ \verb|y+\xcancel{x}| &amp;#x26; y+\xcancel{x}\ \verb+\frac{1\cancel9}{\cancel95} = \frac15+ &amp;#x26; \frac{1\cancel9}{\cancel95} = \frac15 \ \end{array}
$$&lt;/p&gt;
&lt;h2&gt;二、矩阵使用参考&lt;/h2&gt;
&lt;h3&gt;1．如何输入无框矩阵&lt;/h3&gt;
&lt;p&gt;在开头使用 &lt;code&gt;begin{matrix}&lt;/code&gt;，在结尾使用 &lt;code&gt;end{matrix}&lt;/code&gt;，在中间插入矩阵元素，每个元素之间插入 &lt;code&gt;&amp;#x26;&lt;/code&gt; ，并在每行结尾处使用 \ 。 使用矩阵时必须声明 &lt;code&gt;$&lt;/code&gt; 或 &lt;code&gt;$$&lt;/code&gt; 符号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
    \begin{matrix}
    1 &amp;#x26; x &amp;#x26; x^2 \\\\
    1 &amp;#x26; y &amp;#x26; y^2 \\\\
    1 &amp;#x26; z &amp;#x26; z^2 \\\\
    \end{matrix}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{matrix} 1 &amp;#x26; x &amp;#x26; x^2 \ 1 &amp;#x26; y &amp;#x26; y^2 \ 1 &amp;#x26; z &amp;#x26; z^2 \ \end{matrix}
$$&lt;/p&gt;
&lt;h3&gt;2．如何输入带边框的矩阵&lt;/h3&gt;
&lt;p&gt;在开头将 &lt;code&gt;matrix&lt;/code&gt; 替换为 &lt;code&gt;pmatrix bmatrix Bmatrix vmatrix Vmatrix&lt;/code&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$ \begin{matrix} 1 &amp;#x26; 2 \\\\ 3 &amp;#x26; 4 \\\\ \end{matrix} $
$ \begin{pmatrix} 1 &amp;#x26; 2 \\\\ 3 &amp;#x26; 4 \\\\ \end{pmatrix} $
$ \begin{bmatrix} 1 &amp;#x26; 2 \\\\ 3 &amp;#x26; 4 \\\\ \end{bmatrix} $
$ \begin{Bmatrix} 1 &amp;#x26; 2 \\\\ 3 &amp;#x26; 4 \\\\ \end{Bmatrix} $
$ \begin{vmatrix} 1 &amp;#x26; 2 \\\\ 3 &amp;#x26; 4 \\\\ \end{vmatrix} $
$ \begin{Vmatrix} 1 &amp;#x26; 2 \\\\ 3 &amp;#x26; 4 \\\\ \end{Vmatrix} $
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{matrix} 1 &amp;#x26; 2 \\ 3 &amp;#x26; 4 \\ \end{matrix} \
\begin{pmatrix} 1 &amp;#x26; 2 \\ 3 &amp;#x26; 4 \\ \end{pmatrix} \
\begin{bmatrix} 1 &amp;#x26; 2 \\ 3 &amp;#x26; 4 \\ \end{bmatrix} \
\begin{Bmatrix} 1 &amp;#x26; 2 \\ 3 &amp;#x26; 4 \\ \end{Bmatrix} \
\begin{vmatrix} 1 &amp;#x26; 2 \\ 3 &amp;#x26; 4 \\ \end{vmatrix} \
\begin{Vmatrix} 1 &amp;#x26; 2 \\ 3 &amp;#x26; 4 \\ \end{Vmatrix} \
$$&lt;/p&gt;
&lt;h3&gt;3．如何输入带省略符号的矩阵&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;\cdots&lt;/code&gt; , &lt;code&gt;\ddots&lt;/code&gt; , &lt;code&gt;\vdots&lt;/code&gt; 来输入省略符号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
        \begin{pmatrix}
        1 &amp;#x26; a_1 &amp;#x26; a_1^2 &amp;#x26; \cdots &amp;#x26; a_1^n \\\\
        1 &amp;#x26; a_2 &amp;#x26; a_2^2 &amp;#x26; \cdots &amp;#x26; a_2^n \\\\
        \vdots &amp;#x26; \vdots &amp;#x26; \vdots &amp;#x26; \ddots &amp;#x26; \vdots \\\\
        1 &amp;#x26; a_m &amp;#x26; a_m^2 &amp;#x26; \cdots &amp;#x26; a_m^n \\\\
        \end{pmatrix}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{pmatrix} 1 &amp;#x26; a_1 &amp;#x26; a_1^2 &amp;#x26; \cdots &amp;#x26; a_1^n \ 1 &amp;#x26; a_2 &amp;#x26; a_2^2 &amp;#x26; \cdots &amp;#x26; a_2^n \ \vdots &amp;#x26; \vdots &amp;#x26; \vdots &amp;#x26; \ddots &amp;#x26; \vdots \ 1 &amp;#x26; a_m &amp;#x26; a_m^2 &amp;#x26; \cdots &amp;#x26; a_m^n \ \end{pmatrix}
$$&lt;/p&gt;
&lt;h3&gt;4．如何输入带分割符号的矩阵&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
        \left[
            \begin{array}{cc|c}
              1&amp;#x26;2&amp;#x26;3\\\\
              4&amp;#x26;5&amp;#x26;6
            \end{array}
        \right]
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;cc|c&lt;/code&gt; 代表在一个三列矩阵中的第二和第三列之间插入分割线。&lt;/p&gt;
&lt;p&gt;$$
\left[ \begin{array}{cc|c} 1&amp;#x26;2&amp;#x26;3\ 4&amp;#x26;5&amp;#x26;6 \end{array} \right]
$$&lt;/p&gt;
&lt;h3&gt;5．如何输入行中矩阵&lt;/h3&gt;
&lt;p&gt;若想在一行内显示矩阵，使用&lt;code&gt;\bigl(\begin{smallmatrix} ... \end{smallmatrix}\bigr)&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;这是一个行中矩阵的示例 $\bigl( \begin{smallmatrix} a &amp;#x26; b \\\\ c &amp;#x26; d \end{smallmatrix} \bigr)$ 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个行中矩阵的示例 $\bigl(\begin{smallmatrix} a &amp;#x26; b \ c &amp;#x26; d \end{smallmatrix}\bigr)$&lt;/p&gt;
&lt;h2&gt;三、方程式序列使用参考&lt;/h2&gt;
&lt;h3&gt;1．如何输入一个方程式序列&lt;/h3&gt;
&lt;p&gt;人们经常想要一列整齐且居中的方程式序列。使用 &lt;code&gt;\begin{align}…\end{align}&lt;/code&gt; 来创造一列方程式，其中在每行结尾处使用 &lt;code&gt;\\&lt;/code&gt; 。 使用方程式序列无需声明公式符号 &lt;code&gt;$&lt;/code&gt; 或 &lt;code&gt;$$&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;请注意 &lt;code&gt;{align}&lt;/code&gt; 语句是 &lt;strong&gt;自动编号&lt;/strong&gt; 的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\left\\{ 
    \begin{array}{c}
        a_1x+b_1y+c_1z &amp;#x26;=d_1 \\\\ 
        a_2x+b_2y+c_2z &amp;#x26;=d_2 \\\\ 
        a_3x+b_3y+c_3z &amp;#x26;=d_3 \\\\
    \end{array}
\right. 
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\left{  \begin{array}{c} a_1x+b_1y+c_1z &amp;#x26;=d_1 \  a_2x+b_2y+c_2z &amp;#x26;=d_2 \  a_3x+b_3y+c_3z &amp;#x26;=d_3 \ \end{array} \right.
$$&lt;/p&gt;
&lt;h3&gt;2．在一个方程式序列的每一行中注明原因&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;{align}&lt;/code&gt; 中灵活组合 &lt;code&gt;\text&lt;/code&gt; 和 &lt;code&gt;\tag&lt;/code&gt; 语句。&lt;code&gt;\tag&lt;/code&gt; 语句编号优先级高于自动编号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{align}
   v + w &amp;#x26; = 0  &amp;#x26;\text{Given} \tag 1\\\\
   -w &amp;#x26; = -w + 0 &amp;#x26; \text{additive identity} \tag 2\\\\
   -w + 0 &amp;#x26; = -w + (v + w) &amp;#x26; \text{equations $(1)$ and $(2)$} \tag 3
\end{align}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{align}
v + w &amp;#x26; = 0  &amp;#x26;\text{Given} \tag 1\\
-w &amp;#x26; = -w + 0 &amp;#x26; \text{additive identity} \tag 2\\
-w + 0 &amp;#x26; = -w + (v + w) &amp;#x26; \text{equations $(1)$ and $(2)$} \tag 3
\end{align}
$$&lt;/p&gt;
&lt;p&gt;本例中第一、第二行的自动编号被 &lt;code&gt;\tag&lt;/code&gt; 语句覆盖，第三行的编号为自动编号。&lt;/p&gt;
&lt;h2&gt;四、条件表达式使用参考&lt;/h2&gt;
&lt;h3&gt;1．如何输入一个条件表达式&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;begin{cases}&lt;/code&gt; 来创造一组条件表达式，在每一行条件中插入 &lt;code&gt;&amp;#x26;&lt;/code&gt; 来指定需要对齐的内容，并在每一行结尾处使用 &lt;code&gt;\\&lt;/code&gt;，以 &lt;code&gt;end{cases}&lt;/code&gt; 结束。 条件表达式无需声明 &lt;code&gt;$&lt;/code&gt; 或 &lt;code&gt;$$&lt;/code&gt; 符号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
        f(n) =
        \begin{cases}
        n/2,  &amp;#x26; \text{if $n$ is even} \\\\
        3n+1, &amp;#x26; \text{if $n$ is odd}
        \end{cases}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
f(n) =
\begin{cases}
n/2,  &amp;#x26; \text{if $n$ is even} \\
3n+1, &amp;#x26; \text{if $n$ is odd}
\end{cases}
$$&lt;/p&gt;
&lt;h3&gt;2．如何输入一个左侧对齐的条件表达式&lt;/h3&gt;
&lt;p&gt;若想让文字在 左侧对齐显示 ，则有如下方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
        \left.
        \begin{array}{l}
        \text{if $n$ is even:}&amp;#x26;n/2\\\\
        \text{if $n$ is odd:}&amp;#x26;3n+1
        \end{array}
        \right\\}
        =f(n)
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\left. \begin{array}{l} \text{if $n$ is even:}&amp;#x26;n/2\ \text{if $n$ is odd:}&amp;#x26;3n+1 \end{array} \right} =f(n)
$$&lt;/p&gt;
&lt;h3&gt;3．如何使条件表达式适配行高&lt;/h3&gt;
&lt;p&gt;在一些情况下，条件表达式中某些行的行高为非标准高度，此时使用 &lt;code&gt;\\[2ex]&lt;/code&gt; 语句代替该行末尾的 &lt;code&gt;\\&lt;/code&gt; 来让编辑器适配。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不适配[2ex]&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
f(n) = 
\begin{cases}
\frac{n}{2},  &amp;#x26; \text{if $n$ is even} \\\\
3n+1, &amp;#x26; \text{if $n$ is odd}
\end{cases}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
f(n) =  \begin{cases} \frac{n}{2},  &amp;#x26; \text{if $n$ is even} \ 3n+1, &amp;#x26; \text{if $n$ is odd} \end{cases}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;适配[2ex]&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
f(n) = 
\begin{cases}
\frac{n}{2},  &amp;#x26; \text{if $n$ is even} \\\\[2ex]
3n+1, &amp;#x26; \text{if $n$ is odd}
\end{cases}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
f(n) =  \begin{cases} \frac{n}{2},  &amp;#x26; \text{if $n$ is even} \[2ex] 3n+1, &amp;#x26; \text{if $n$ is odd} \end{cases}
$$&lt;/p&gt;
&lt;p&gt;一个 &lt;code&gt;[ex]&lt;/code&gt; 指一个 “X-Height”，即&lt;code&gt;x&lt;/code&gt;字母高度。可以根据情况指定多个 &lt;code&gt;[ex]&lt;/code&gt;，如 &lt;code&gt;[3ex]&lt;/code&gt;、&lt;code&gt;[4ex]&lt;/code&gt; 等。 其实可以在任何地方使用 &lt;code&gt;\\[2ex]&lt;/code&gt; 语句，只要你觉得合适。&lt;/p&gt;
&lt;h2&gt;五、数组与表格使用参考&lt;/h2&gt;
&lt;h3&gt;1．如何输入一个数组或表格&lt;/h3&gt;
&lt;p&gt;通常，一个格式化后的表格比单纯的文字或排版后的文字更具有可读性。数组和表格均以 &lt;code&gt;begin{array}&lt;/code&gt; 开头，并在其后定义列数及每一列的文本对齐属性，&lt;code&gt;c l r&lt;/code&gt; 分别代表居中、左对齐及右对齐。若需要插入垂直分割线，在定义式中插入 &lt;code&gt;|&lt;/code&gt; ，若要插入水平分割线，在下一行输入前插入 &lt;code&gt;\hline&lt;/code&gt; 。与矩阵相似，每行元素间均须要插入 &lt;code&gt;&amp;#x26;&lt;/code&gt; ，每行元素以 &lt;code&gt;\\&lt;/code&gt; 结尾，最后以 &lt;code&gt;end{array}&lt;/code&gt; 结束数组。 使用单个数组或表格时无需声明 &lt;code&gt;$&lt;/code&gt; 或 &lt;code&gt;$$&lt;/code&gt; 符号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{array}{c|lcr}
n &amp;#x26; \text{左对齐} &amp;#x26; \text{居中对齐} &amp;#x26; \text{右对齐} \\\\
\hline
1 &amp;#x26; 0.24 &amp;#x26; 1 &amp;#x26; 125 \\\\
2 &amp;#x26; -1 &amp;#x26; 189 &amp;#x26; -8 \\\\
3 &amp;#x26; -20 &amp;#x26; 2000 &amp;#x26; 1+10i
\end{array}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{array}{c|lcr} n &amp;#x26; \text{左对齐} &amp;#x26; \text{居中对齐} &amp;#x26; \text{右对齐} \ \hline 1 &amp;#x26; 0.24 &amp;#x26; 1 &amp;#x26; 125 \ 2 &amp;#x26; -1 &amp;#x26; 189 &amp;#x26; -8 \ 3 &amp;#x26; -20 &amp;#x26; 2000 &amp;#x26; 1+10i  \end{array}
$$&lt;/p&gt;
&lt;h3&gt;2．如何输入一个嵌套的数组或表格&lt;/h3&gt;
&lt;p&gt;多个数组/表格可&lt;strong&gt;互相嵌套&lt;/strong&gt; 并组成一组数组/一组表格。 使用嵌套前必须声明 &lt;code&gt;$$&lt;/code&gt; 符号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
% outer vertical array of arrays 外层垂直表格
\begin{array}{c}
    % inner horizontal array of arrays 内层水平表格
    \begin{array}{cc}
        % inner array of minimum values 内层&quot;最小值&quot;数组
        \begin{array}{c|cccc}
        \text{min} &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 3\\\\
        \hline
        0 &amp;#x26; 0 &amp;#x26; 0 &amp;#x26; 0 &amp;#x26; 0\\\\
        1 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 1 &amp;#x26; 1\\\\
        2 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 2\\\\
        3 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 3
        \end{array}
    &amp;#x26;
        % inner array of maximum values 内层&quot;最大值&quot;数组
        \begin{array}{c|cccc}
        \text{max}&amp;#x26;0&amp;#x26;1&amp;#x26;2&amp;#x26;3\\\\
        \hline
        0 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 3\\\\
        1 &amp;#x26; 1 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 3\\\\
        2 &amp;#x26; 2 &amp;#x26; 2 &amp;#x26; 2 &amp;#x26; 3\\\\
        3 &amp;#x26; 3 &amp;#x26; 3 &amp;#x26; 3 &amp;#x26; 3
        \end{array}
    \end{array}
    % 内层第一行表格组结束
    \\\\
    % inner array of delta values 内层第二行Delta值数组
        \begin{array}{c|cccc}
        \Delta&amp;#x26;0&amp;#x26;1&amp;#x26;2&amp;#x26;3\\\\
        \hline
        0 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 3\\\\
        1 &amp;#x26; 1 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2\\\\
        2 &amp;#x26; 2 &amp;#x26; 1 &amp;#x26; 0 &amp;#x26; 1\\\\
        3 &amp;#x26; 3 &amp;#x26; 2 &amp;#x26; 1 &amp;#x26; 0
        \end{array}
        % 内层第二行表格组结束
\end{array}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
% outer vertical array of arrays 外层垂直表格
\begin{array}{c}
% inner horizontal array of arrays 内层水平表格
\begin{array}{cc}
% inner array of minimum values 内层&quot;最小值&quot;数组
\begin{array}{c|cccc}
\text{min} &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 3\\
\hline
0 &amp;#x26; 0 &amp;#x26; 0 &amp;#x26; 0 &amp;#x26; 0\\
1 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 1 &amp;#x26; 1\\
2 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 2\\
3 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 3
\end{array}
&amp;#x26;
% inner array of maximum values 内层&quot;最大值&quot;数组
\begin{array}{c|cccc}
\text{max}&amp;#x26;0&amp;#x26;1&amp;#x26;2&amp;#x26;3\\
\hline
0 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 3\\
1 &amp;#x26; 1 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 3\\
2 &amp;#x26; 2 &amp;#x26; 2 &amp;#x26; 2 &amp;#x26; 3\\
3 &amp;#x26; 3 &amp;#x26; 3 &amp;#x26; 3 &amp;#x26; 3
\end{array}
\end{array}
% 内层第一行表格组结束
\\
% inner array of delta values 内层第二行Delta值数组
\begin{array}{c|cccc}
\Delta&amp;#x26;0&amp;#x26;1&amp;#x26;2&amp;#x26;3\\
\hline
0 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2 &amp;#x26; 3\\
1 &amp;#x26; 1 &amp;#x26; 0 &amp;#x26; 1 &amp;#x26; 2\\
2 &amp;#x26; 2 &amp;#x26; 1 &amp;#x26; 0 &amp;#x26; 1\\
3 &amp;#x26; 3 &amp;#x26; 2 &amp;#x26; 1 &amp;#x26; 0
\end{array}
% 内层第二行表格组结束
\end{array}
$$&lt;/p&gt;
&lt;h3&gt;3．如何输入一个方程组&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;\begin{array}…\end{array}&lt;/code&gt; 和 &lt;code&gt;\left\{…\right.&lt;/code&gt; 来创建一个方程组。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\left\\{ 
    \begin{array}{c}
    a_1x+b_1y+c_1z=d_1 \\\\
    a_2x+b_2y+c_2z=d_2 \\\\ 
    a_3x+b_3y+c_3z=d_3
    \end{array}
\right. 
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\left{  \begin{array}{c} a_1x+b_1y+c_1z=d_1 \ a_2x+b_2y+c_2z=d_2 \  a_3x+b_3y+c_3z=d_3 \ \end{array} \right.
$$&lt;/p&gt;
&lt;p&gt;或者使用条件表达式组 &lt;code&gt;\begin{cases}…\end{cases}&lt;/code&gt; 来实现相同效果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{cases}
a_1x+b_1y+c_1z=d_1 \\\\
a_2x+b_2y+c_2z=d_2 \\\\ 
a_3x+b_3y+c_3z=d_3
\end{cases}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{cases} a_1x+b_1y+c_1z=d_1 \ a_2x+b_2y+c_2z=d_2 \  a_3x+b_3y+c_3z=d_3 \end{cases}
$$&lt;/p&gt;
&lt;h2&gt;六、连分数使用参考&lt;/h2&gt;
&lt;h3&gt;1．如何输入一个连分式&lt;/h3&gt;
&lt;p&gt;就像输入分式时使用 &lt;code&gt;\frac&lt;/code&gt; 一样，使用 &lt;code&gt;\cfrac&lt;/code&gt; 来创建一个连分数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
x = a_0 + \cfrac{1^2}{a_1
          + \cfrac{2^2}{a_2
          + \cfrac{3^2}{a_3 + \cfrac{4^4}{a_4 + \cdots}}}}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
x = a_0 + \cfrac{1^2}{a_1 + \cfrac{2^2}{a_2 + \cfrac{3^2}{a_3 + \cfrac{4^4}{a_4 + \cdots}}}}
$$&lt;/p&gt;
&lt;p&gt;不要使用普通的 &lt;code&gt;\frac&lt;/code&gt; 或 &lt;code&gt;\over&lt;/code&gt; 来创建，否则会看起来 &lt;strong&gt;很恶心&lt;/strong&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
x = a_0 + \frac{1^2}{a_1
          + \frac{2^2}{a_2
          + \frac{3^2}{a_3 + \frac{4^4}{a_4 + \cdots}}}}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
x = a_0 + \frac{1^2}{a_1 + \frac{2^2}{a_2 + \frac{3^2}{a_3 + \frac{4^4}{a_4 + \cdots}}}}
$$&lt;/p&gt;
&lt;p&gt;当然，你可以使用 &lt;code&gt;\frac&lt;/code&gt; 来表达连分数的 &lt;strong&gt;紧缩记法&lt;/strong&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
x = a_0 + \frac{1^2}{a_1+}
          \frac{2^2}{a_2+}
          \frac{3^2}{a_3 +} \frac{4^4}{a_4 +} \cdots
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
x = a_0 + \frac{1^2}{a_1+} \frac{2^2}{a_2+} \frac{3^2}{a_3 +} \frac{4^4}{a_4 +} \cdots
$$&lt;/p&gt;
&lt;p&gt;连分数通常都太大以至于不易排版，所以建议在连分数前后声明 &lt;code&gt;$$&lt;/code&gt; 符号，或使用像 &lt;code&gt;[a0;a1,a2,a3,…]&lt;/code&gt; 一样的紧缩记法。&lt;/p&gt;
&lt;h2&gt;七、交换图表使用参考&lt;/h2&gt;
&lt;h3&gt;1．如何输入一个交换图表&lt;/h3&gt;
&lt;p&gt;使用一行 &lt;code&gt;\require{AMScd}&lt;/code&gt; 语句来允许交换图表的显示。 声明交换图表后，语法与矩阵相似，在开头使用 &lt;code&gt;begin{CD}&lt;/code&gt;，在结尾使用 &lt;code&gt;end{CD}&lt;/code&gt;，在中间插入图表元素，每个元素之间插入 &lt;code&gt;&amp;#x26;&lt;/code&gt; ，并在每行结尾处使用&lt;code&gt;\\&lt;/code&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{CD}
    A  @&gt;a&gt;&gt;  B \\\\
    @VbVV   @VVcV \\\\
    C  @&gt;&gt;d&gt;  D
\end{CD}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{CD} A  @&gt;a&gt;&gt; B \ @VbVV  @VVcV \ C  @&gt;&gt;d&gt;  D \end{CD}
$$&lt;/p&gt;
&lt;p&gt;其中，&lt;code&gt;@&gt;&gt;&gt;&lt;/code&gt; 代表右箭头、&lt;code&gt;@&amp;#x3C;&amp;#x3C;&amp;#x3C;&lt;/code&gt; 代表左箭头、&lt;code&gt;@VVV&lt;/code&gt; 代表下箭头、&lt;code&gt;@AAA&lt;/code&gt; 代表上箭头、&lt;code&gt;@=&lt;/code&gt; 代表水平双实线、&lt;code&gt;@|&lt;/code&gt; 代表竖直双实线、&lt;code&gt;@.&lt;/code&gt;代表没有箭头。 在 &lt;code&gt;@&gt;&gt;&gt;&lt;/code&gt; 的 &lt;code&gt;&gt;&gt;&gt;&lt;/code&gt; 之间任意插入文字即代表该箭头的注释文字。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{CD}
    A @&gt;&gt;&gt; B @&gt;{\text{very long label}}&gt;&gt; C \\\\
    @. @AAA @| \\\\
    D @= E @&amp;#x3C;&amp;#x3C;&amp;#x3C; F
\end{CD}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{CD} A @&gt;&gt;&gt; B @&gt;{\text{very long label}}&gt;&gt; C \ @. @AAA @| \ D @= E @&amp;#x3C;&amp;#x3C;&amp;#x3C; F \end{CD}
$$&lt;/p&gt;
&lt;p&gt;在本例中， “very long label“自动延长了它所在箭头以及对应箭头的长度。&lt;/p&gt;
&lt;h2&gt;八、一些特殊的注意事项&lt;/h2&gt;
&lt;p&gt;有些小窍门会让数学公式显得更好看，强迫症和完美主义者会喜欢下面的内容。&lt;/p&gt;
&lt;h3&gt;1. 某些分数的显示问题&lt;/h3&gt;
&lt;p&gt;在以&lt;code&gt;e&lt;/code&gt;为底的指数函数、极限和积分中尽量不要使用 &lt;code&gt;\frac&lt;/code&gt; 符号：它会使整段函数看起来很怪，而且可能产生歧义。也正是因此它在专业数学排版中几乎从不出现。 横着写这些分式，中间使用斜线间隔 &lt;code&gt;/&lt;/code&gt; （用斜线代替分数线）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{array}{c|c}
\mathrm{Bad} &amp;#x26; \mathrm{Better} \\\\
\hline \\\\
e^{i\frac{\pi}2} \quad e^{\frac{i\pi}2}&amp;#x26; e^{i\pi/2} \\\\
\int_{-\frac\pi2}^\frac\pi2 \sin x\,dx &amp;#x26; \int_{-\pi/2}^{\pi/2}\sin x\,dx \\\\
\end{array}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{array}{c|c} \mathrm{Bad} &amp;#x26; \mathrm{Better} \ \hline \ e^{i\frac{\pi}2} \quad e^{\frac{i\pi}2}&amp;#x26; e^{i\pi/2} \ \int_{-\frac\pi2}^\frac\pi2 \sin x,dx &amp;#x26; \int_{-\pi/2}^{\pi/2}\sin x,dx \ \end{array}
$$&lt;/p&gt;
&lt;h3&gt;2. 留出合理的间隔&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;|&lt;/code&gt; 符号在被当作分隔符时会产生过于狭窄的间隔，因此在需要分隔时最好使用 &lt;code&gt;\mid&lt;/code&gt; 来代替它。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{array}{c|c}
\mathrm{Bad} &amp;#x26; \mathrm{Better} \\\\
\hline \\\\
\{x|x^2\in\Bbb Z\} &amp;#x26; \{x\mid x^2\in\Bbb Z\} \\\\
\end{array}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$\begin{array}{c|c} \mathrm{Bad} &amp;#x26; \mathrm{Better} \ \hline \ {x|x^2\in\Bbb Z} &amp;#x26; {x\mid x^2\in\Bbb Z} \ \end{array}$$&lt;/p&gt;
&lt;h3&gt;3. 多重积分符号的显示&lt;/h3&gt;
&lt;p&gt;使用多重积分符号时，不要多次使用 &lt;code&gt;\int&lt;/code&gt; 来声明，直接使用 &lt;code&gt;\iint&lt;/code&gt; 来表示 二重积分 ，使用 &lt;code&gt;\iiint&lt;/code&gt; 来表示 三重积分 等。对于无限次积分，可以用 &lt;code&gt;\int \cdots \int&lt;/code&gt; 表示。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{array}{c|c}
\mathrm{Bad} &amp;#x26; \mathrm{Better} \\\\
\hline \\\\
\int\int_S f(x)\,dy\,dx &amp;#x26; \iint_S f(x)\,dy\,dx \\\\
\int\int\int_V f(x)\,dz\,dy\,dx &amp;#x26; \iiint_V f(x)\,dz\,dy\,dx
\end{array}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$\begin{array}{c|c} \mathrm{Bad} &amp;#x26; \mathrm{Better} \ \hline \ \int\int_S f(x),dy,dx &amp;#x26; \iint_S f(x),dy,dx \ \int\int\int_V f(x),dz,dy,dx &amp;#x26; \iiint_V f(x),dz,dy,dx \end{array}$$&lt;/p&gt;
&lt;h3&gt;4. 多个微分符号的显示&lt;/h3&gt;
&lt;p&gt;在微分符号前加入 &lt;code&gt;\&lt;/code&gt;, 来插入一个小的间隔空隙；没有 &lt;code&gt;\&lt;/code&gt;, 符号的话， 将会把不同的微分符号堆在一起。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;$$
\begin{array}{c|c}
\mathrm{Bad} &amp;#x26; \mathrm{Better} \\\\
\hline \\\\
\iiint_V f(x){\rm d}z {\rm d}y {\rm d}x &amp;#x26; \iiint_V f(x)\,{\rm d}z\,{\rm d}y\,{\rm d}x
\end{array}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$$
\begin{array}{c|c} \mathrm{Bad} &amp;#x26; \mathrm{Better} \ \hline \ \iiint_V f(x){\rm d}z {\rm d}y {\rm d}x &amp;#x26; \iiint_V f(x),{\rm d}z,{\rm d}y,{\rm d}x \end{array}
$$&lt;/p&gt;
&lt;p&gt;感谢您花费时间阅读这份指导手册，本手册内容可能有疏漏之处，欢迎更改指正。&lt;/p&gt;</content:encoded><h:img src="/_astro/202504140008452.4guH10pP.png"/><enclosure url="/_astro/202504140008452.4guH10pP.png"/></item><item><title>深入浅出 Docker 技术</title><link>https://coooredump.github.io/blog/productivity-tool/docker-intro</link><guid isPermaLink="true">https://coooredump.github.io/blog/productivity-tool/docker-intro</guid><description>Docker 是一个开源的应用容器引擎，让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中，然后发布到任何流行的 Linux 或 Windows 操作系统的机器上，也可以实现虚拟化。容器是完全使用沙箱机制，相互之间不会有任何接口。</description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Docker 是什么&lt;/h2&gt;
&lt;p&gt;Docker 是一个开源的应用容器引擎，让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中，然后发布到任何流行的 Linux 或 Windows 操作系统的机器上，也可以实现虚拟化。容器是完全使用沙箱机制，相互之间不会有任何接口。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fec2e706f9dc4206b2ffe7024fd24a69~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;E506BFAF-560F-4AA3-95C6-98C604EC69B2_1_201_a.jpeg&quot;&gt;&lt;/p&gt;
&lt;h3&gt;容器技术&lt;/h3&gt;
&lt;p&gt;容器使软件应用程序与操作系统脱钩，从而为用户提供了一个干净而最小的Linux环境，同时在一个或多个隔离的“容器”中运行其他所有内容。容器的目的是启动一组有限的应用程序或服务（通常称为微服务），并使它们在独立的沙盒环境中运行。&lt;/p&gt;
&lt;p&gt;这种隔离可防止在给定容器中运行的进程监视或影响在另一个容器中运行的进程。同样，这些容器化服务不会影响或干扰主机。能够将分散在多个物理服务器上的许多服务整合为一个的想法，是数据中心选择采用该技术的众多原因之一。&lt;/p&gt;
&lt;h3&gt;容器VS虚拟机&lt;/h3&gt;
&lt;p&gt;首先我们来看下面一张图，下面是虚拟机的实现方式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7ff0caabe0254cfeb1e92a2eeb03eb53~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;1630839530493_61cb27a5d0f2149a41488d8c9f932bf0.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;虚拟机实现资源隔离的方法是利用在主操作系统上运行独立的从操作系统，上图所示，在最底层运行的是我们所熟知的服务器，通常服务器上会运行一个主操作系统.&lt;/p&gt;
&lt;p&gt;虚拟机的Guest OS即为虚拟机安装的操作系统，它是一个完整操作系统内核；虚拟机的Hypervisor层可以简单理解为一个硬件虚拟化平台，他负责协调宿主机上的硬件资源分配与管理.一个比较经典的虚拟机软件就是Parallels Desktop.&lt;/p&gt;
&lt;p&gt;下面再来看看Docker的实现方式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b4f72eaa2add4217b442c3ea653a442c~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;1630839542476_a5f81adc1406b44da7941fa87b179b61.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Docker容器技术的实现要比虚拟机技术的实现减少一层，由于Docker不需要Hypervisor实现硬件资源虚拟化，&lt;strong&gt;运行在Docker容器上的程序直接使用的都是实际物理机的硬件资源&lt;/strong&gt;。因此在CPU、内存利用率上Docker将会在效率上有优势.&lt;/p&gt;
&lt;p&gt;最后做一个简单的比较.&lt;/p&gt;
&lt;p&gt;| 特性 | 容器 | 虚拟机 |
| --- | --- | --- |
| 启动 | 秒级 | 分钟级 |
| 硬盘使用 | 一般为MB | 一般为GB |
| 性能 | 接近原生 | 弱于 |
| 系统支持量 | 单机支持上千个容器 | 一般是几十个 |&lt;/p&gt;
&lt;h2&gt;Docker 的使用&lt;/h2&gt;
&lt;p&gt;可能有部分读者朋友们没有直接的使用过docker.我们先来举一个例子.在以前没有docker的情况下，假使我们想在linux环境下运行一个mysql，可能我们先要下载压缩包，解压，编译，设置等等流程，十分复杂.&lt;/p&gt;
&lt;p&gt;但是当我们使用docker运行mysql时，只需要先运行&lt;code&gt;docker pull mysql&lt;/code&gt;就会自动下载最新的mysql镜像.&lt;/p&gt;
&lt;p&gt;再运行&lt;code&gt;docker run mysql&lt;/code&gt;以后，就可以直接运行，如果你想再运行一份mysql实例，只需要再运行一次&lt;code&gt;docker run&lt;/code&gt;的命令，并设置新的端口就能运行，这么一看是不是感觉docker是不是非常好用.&lt;/p&gt;
&lt;p&gt;docker的功能还远远不止这些，接下来让我们一起走进docker，看看docker是怎么实现的容器技术.&lt;/p&gt;
&lt;h2&gt;Docker 基础概念&lt;/h2&gt;
&lt;h3&gt;运行流程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc2fac06427d4799b40017fb410e0de2~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;流程.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;在Docker运行的流程图中，我们可以简单的把image理解为可执行程序，Container就是运行起来的进程。Registry就是代码管理平台.&lt;/p&gt;
&lt;p&gt;那么写程序需要源代码，那么“写”image的&quot;源代码&quot;就是dockerfile，docker就是&quot;编译器&quot;。&lt;/p&gt;
&lt;p&gt;因此我们只需要在dockerfile中指定需要哪些程序、依赖什么样的配置，之后把dockerfile交给“编译器”docker进行“编译”，生成&quot;可执行程序&quot;image，之后就可以运行这个image了，image运行起来后就是&lt;code&gt;Docker container&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Image&lt;/code&gt;(镜像)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Docker 镜像是一个特殊的文件系统，除了提供容器运行时所需的程序、库、资源、配置等文件外，还包含了一些为运行时准备的一些配置参数（如匿名卷、环境变量、用户等）。&lt;/strong&gt; 镜像不包含任何动态数据，其内容在构建之后也不会被改变。&lt;/p&gt;
&lt;p&gt;首先来看一个比较简单的例子:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7b23cc443a5842b8b87ce4ad8cd7c958~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;1625490629341_9e19801dc41a18fb83bf93c00a5db13f.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图是一个由debian系统作为基础镜像的简历样例，可以看到中间层就是基础的镜像，我们并没有对镜像进行任何定制化的操作，运行起来后就生成了一个容器，容器才是可写的对象.&lt;/p&gt;
&lt;p&gt;对于Linux而言，内核启动后，会挂载root文件系统为其提供用户空间支持。而Docker镜像（Image），就相当于是一个root文件系统。&lt;/p&gt;
&lt;p&gt;当然，Docker能实现的功能远不止如此，下面我们再来看看如何使用DockerFile构建一个定制化镜像:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM debian
RUN apt-get install emacs
RUN apt-get install apache2
CMD [&quot;/bin/bash&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5706b485db3640a4a34fde506411dece~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;1625490629343_a52602982fa713b466af861a0644f458.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Docker设计时，充分利用&lt;strong&gt;Union FS&lt;/strong&gt;的技术，将其设计为&lt;strong&gt;分层存储的架构&lt;/strong&gt;。镜像实际是由多层文件系统联合组成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;镜像构建时，会一层层构建，前一层是后一层的基础。每一层构建完就不会再发生改变，后一层上的任何改变只发生在自己这一层。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层，然后进一步添加新的层，以定制自己所需的内容，构建新的镜像。&lt;/p&gt;
&lt;p&gt;但是在构建镜像时也要格外的注意，比如，删除前一层文件的操作，实际不是真的删除前一层的文件，而是仅在当前层标记为该文件已删除。在最终容器运行的时候，虽然不会看到这个文件，但是实际上该文件会一直跟随镜像。所以在构建镜像时，每一层尽量只包含该层需要添加的东西，任何额外的东西应该在该层构建结束前清理掉。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Container&lt;/code&gt;(容器)&lt;/h3&gt;
&lt;p&gt;容器 &lt;code&gt;(container)&lt;/code&gt; 的定义和镜像 &lt;code&gt;(image)&lt;/code&gt; 几乎一模一样，唯一区别在于容器的最上面那一层是可读可写的。&lt;/p&gt;
&lt;p&gt;广义上我们可以将容器理解为，容器 = 镜像 + 读写层。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Repository&lt;/code&gt;(仓库)&lt;/h3&gt;
&lt;p&gt;镜像构建完成后，可以很容易的在当前宿主上运行，但是如何在其他服务器上运行这个镜像，那么我们就需要一个集中存放镜像文件的场所.这时就引出了&lt;code&gt;Docker Repository&lt;/code&gt;的概念.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Docker Registry&lt;/code&gt; (仓库注册服务器)是一个集中的存储、分发镜像的服务。&lt;code&gt;Docker&lt;/code&gt; 仓库的概念跟 &lt;code&gt;Git&lt;/code&gt; 类似，注册服务器可以理解为 &lt;code&gt;GitHub&lt;/code&gt; 这样的托管服务。实际上，一个 &lt;code&gt;Docker Registry&lt;/code&gt; 中可以包含多个仓库 &lt;code&gt;(Repository)&lt;/code&gt; ，每个仓库可以包含多个标签 &lt;code&gt;(Tag)&lt;/code&gt;，每个标签对应着一个镜像。所以说，镜像仓库是 &lt;code&gt;Docker&lt;/code&gt; 用来集中存放镜像文件的地方类似于我们之前常用的代码仓库。&lt;/p&gt;
&lt;p&gt;通常，&lt;strong&gt;一个仓库会包含同一个软件不同版本的镜像&lt;/strong&gt;，而&lt;strong&gt;标签就常用于对应该软件的各个版本&lt;/strong&gt; 。我们可以通过&lt;code&gt;&amp;#x3C;仓库名&gt;:&amp;#x3C;标签&gt;&lt;/code&gt;的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签，将以 &lt;code&gt;latest&lt;/code&gt; 作为默认标签.。&lt;/p&gt;
&lt;p&gt;仓库又可以分为两种形式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public&lt;/code&gt;(公有仓库)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private&lt;/code&gt;(私有仓库)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Docker Registry&lt;/code&gt; 公有仓库是开放给用户使用、允许用户管理镜像的 &lt;code&gt;Registry&lt;/code&gt; 服务。一般这类公开服务允许用户免费上传、下载公开的镜像，并可能提供收费服务供用户管理私有镜像。&lt;/p&gt;
&lt;p&gt;除了使用公开服务外，用户还可以在本地搭建私有 &lt;code&gt;Docker Registry&lt;/code&gt; 。&lt;code&gt;Docker&lt;/code&gt; 官方提供了 &lt;code&gt;Docker Registry&lt;/code&gt;镜像，可以直接使用做为私有 &lt;code&gt;Registry&lt;/code&gt; 服务。当用户创建了自己的镜像之后就可以使用 &lt;code&gt;push&lt;/code&gt; 命令将它上传到公有或者私有仓库，这样下次在另外一台机器上使用这个镜像时候，只需要从仓库上 &lt;code&gt;pull&lt;/code&gt; 下来就可以了。&lt;/p&gt;
&lt;h2&gt;Docker 架构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7613ee72cd344c279cd80819eb274283~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;流程图.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;Docker是一个C/S模式的架构，后端是一个松耦合架构，模块各司其职。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户的所有命令通过&lt;code&gt;Docker Client&lt;/code&gt;与&lt;code&gt;Docker Daemon&lt;/code&gt;建立通信，并发送请求给后者。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Docker Daemon&lt;/code&gt;作为Docker架构中的主体部分，首先提供Server的功能使其可以接受&lt;code&gt;Docker Client&lt;/code&gt;的请求；&lt;/li&gt;
&lt;li&gt;Engine执行Docker内部的一系列工作，每一项工作都是以一个Job的形式的存在。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在之前的基础概念中，我们已经了解了Registry，Container等概念.接下来就是一些Docker运行过程中的组件介绍.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Docker Client&lt;/code&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Docker Client&lt;/code&gt;是和&lt;code&gt;Docker Daemon&lt;/code&gt;建立通信的客户端。用户使用docker命令后，&lt;code&gt;Docker Client&lt;/code&gt;负责解析对应的命令以及参数，并向&lt;code&gt;Docker Daemon&lt;/code&gt;服务端发起请求.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Docker Client&lt;/code&gt;可以通过以下三种方式和&lt;code&gt;Docker Daemon&lt;/code&gt;建立通信：
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;tcp://host:port&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unix://path_to_socket&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fd://socketfd&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Docker Client&lt;/code&gt;发送容器管理请求后，由&lt;code&gt;Docker Daemon&lt;/code&gt;接受并处理请求，当&lt;code&gt;Docker Client&lt;/code&gt;接收到返回的请求相应并简单处理后，&lt;code&gt;Docker Client&lt;/code&gt;一次完整的生命周期就结束了。一次完整的请求：发送请求→处理请求→返回结果，与传统的C/S架构请求流程一致.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;code&gt;Docker Daemon&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/068f78bad06f4007a4b469f20ef22840~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Docker Daemon是docker的守护进程，也是docker运行时的核心.分别有两个部分组成.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Docker Server
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Docker Server&lt;/code&gt;相当于C/S架构的服务端。功能为接受并调度分发Docker Client发送的请求。接受请求后，Server通过路由与分发调度，找到相应的Handler来执行请求。&lt;/li&gt;
&lt;li&gt;在Server的服务过程中，Server在listener上接受&lt;code&gt;Docker Client&lt;/code&gt;的访问请求，并创建一个全新的goroutine来服务该请求。在goroutine中，首先读取请求内容，然后做解析工作，接着找到相应的路由项，随后调用相应的Handler来处理该请求，最后Handler处理完请求之后回复该请求。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Engine
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Engine&lt;/code&gt;是Docker架构中的运行引擎，通过执行&lt;code&gt;Job&lt;/code&gt;的方式来管理所有的容器与镜像。&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;Engine&lt;/code&gt;数据结构的设计与实现过程中，有一个handler对象。该handler对象存储的都是关于众多特定job的handler处理访问。举例说明，&lt;code&gt;Engine&lt;/code&gt;的handler对象中有一项为：&lt;code&gt;{“create”: daemon.ContainerCreate，}&lt;/code&gt;，则说明当名为&quot;create&quot;的job在运行时，执行的是&lt;code&gt;daemon.ContainerCreate的handler&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Job
&lt;ol&gt;
&lt;li&gt;一个&lt;code&gt;Job&lt;/code&gt;可以认为是Docker架构中&lt;code&gt;Engine&lt;/code&gt;内部最基本的工作执行单元。Docker可以做的每一项工作，都可以抽象为一个&lt;code&gt;Job&lt;/code&gt;。无论是镜像的下载，容器的运行停止等等。&lt;code&gt;Docker Server&lt;/code&gt;的运行过程实际也是一个&lt;code&gt;Job&lt;/code&gt;，名为&lt;code&gt;serveapi&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Job&lt;/code&gt;的概念与Unix中进程相仿。在Unix进程中，对每个进程都有名称，参数，环境变量，标准的输入输出，错误处理，返回状态等，在Docker的&lt;code&gt;Job&lt;/code&gt;也都存在.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;code&gt;Graph&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05a4bcafff83445dbe388a191f27f0e5~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Graph中管理着所有本地已经下载的镜像.其中Graph DB中记录了所有镜像之间的依赖关系.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Driver&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;通过Driver驱动，Docker可以实现对Docker容器运行环境的定制，定制的维度主要有网络环境、存储方式以及容器执行方式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/06f87e1cb82e4abda4d59fc8d772b549~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Docker Driver的实现可以分为以下三类驱动：graphdriver、networkdriver和execdriver。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;graphdriver&lt;/code&gt;&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;graphdriver主要用于完成容器镜像的管理，包括存储与获取。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eebbca3b2f5a43f3ad447d55d6b32489~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;graphdriver主要用于容器镜像的管理:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;负责从Docker Registry下载镜像并进行存储，当用户下载指定的容器镜像时，graphdriver将容器镜像分层存储在本地的指定目录下.&lt;/li&gt;
&lt;li&gt;负责从本地镜像存储目录中获取指定的容器镜像，并按特定规则为容器准备rootfs；&lt;/li&gt;
&lt;li&gt;负责管理通过指定Dockerfile构建的全新镜像。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;code&gt;networkdriver&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0dc8ecdea7a4b9390287c0354d2f3b7~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;networkdriver的作用是完成Docker容器网络环境的配置，其中包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Docker Daemon启动时为Docker环境创建网桥；&lt;/li&gt;
&lt;li&gt;Docker容器创建前为其分配相应的网络接口资源；&lt;/li&gt;
&lt;li&gt;Docker容器分配IP、端口并与宿主机做NAT端口映射，设置容器防火墙策略等&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;code&gt;execdriver&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a9cbd51d2c8d4104b76a7d1778d22fec~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;execdriver作为Docker容器的执行驱动，负责创建容器运行时的命名空间，负责管理容器资源使用的统计与限制，负责容器内部进程的真正运行等&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在Docker 0.9.0版本之前，只支持使用Linux的LXC驱动进行容器管理，在0.9.0版本之后默认使用native驱动实现，native驱动是docker项目下一个全新的子项目，去除了外部依赖.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;code&gt;libcontainer&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4e979e7578dc4e39b0d1f2093be8be49~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;ibcontainer是Docker架构中一个使用Go语言设计实现的库，设计初衷是希望该库可以不依靠任何依赖，直接访问内核中与容器相关的系统调用。&lt;/p&gt;
&lt;p&gt;正是由于libcontainer的存在，才使得docker可以不需要依赖LXC或者其他包就可以完成对防火墙，namespaces等的操作。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Docker Container&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Docker Container（Docker容器）是Docker架构中服务交付的最终体现形式。Docker通过DockerDaemon的管理，libcontainer的执行，最终创建Docker容器。Docker容器作为一个交付单位，功能类似于传统意义上的虚拟机（Virtual Machine），具备资源受限、环境与外界隔离的特点。&lt;/p&gt;
&lt;h3&gt;流程梳理&lt;/h3&gt;
&lt;p&gt;看到这里，我相信各位读者朋友们应该已经对Docker基础架构有了一个大概得认知了，让我们以docker run为例子回顾一下 &lt;code&gt;Docker&lt;/code&gt; 各个组件是如何协作的。&lt;/p&gt;
&lt;p&gt;假设我们要运行一条: &lt;code&gt;docker run -p 3306:3306 --name mysql -d mysql&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;容器启动过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Docker&lt;/code&gt; 客户端执行 &lt;code&gt;docker run&lt;/code&gt; 命令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Docker daemon&lt;/code&gt; 通过&lt;code&gt;graghdriver&lt;/code&gt;去&lt;code&gt;Gragh&lt;/code&gt;中拉取最新的 &lt;code&gt;mysql&lt;/code&gt; 镜像&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Docker daemon&lt;/code&gt; 通过&lt;code&gt;networkdriver&lt;/code&gt;建立端口映射&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Docker daemon&lt;/code&gt; 通过&lt;code&gt;execdriver&lt;/code&gt;启动容器&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Docker 核心技术实现&lt;/h2&gt;
&lt;p&gt;在了解了这么多Docker实现以后，我们可能还会有最后一些疑问，Docker是如何实现资源隔离的.&lt;/p&gt;
&lt;p&gt;在日常使用 Linux 或者 macOS 时，我们并没有运行多个完全分离的服务器的需要，但是如果我们在服务器上启动了多个服务，这些服务其实会相互影响的，每一个服务都能看到其他服务的进程，也可以访问宿主机器上的任意文件，这是很多时候我们都不愿意看到的，我们更希望运行在同一台机器上的不同服务能做到&lt;strong&gt;完全隔离&lt;/strong&gt;，就像运行在多台不同的机器上一样。&lt;/p&gt;
&lt;p&gt;Docker实现容器之间的&lt;strong&gt;完全隔离&lt;/strong&gt;一共使用到了三大技术:&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Namespaces&lt;/code&gt;(命名空间)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;命名空间 (namespaces) 是 Linux 为我们提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法.&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;进程隔离&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;Docker Deamon&lt;/code&gt;启动的初期，会通过&lt;code&gt;setNamespaces&lt;/code&gt;函数去创建一个新的命名空间.在创建命名空间使用的&lt;code&gt;clone&lt;/code&gt;函数中传入&lt;code&gt;CLONE_NEWPID&lt;/code&gt;参数就完成容器对宿主机之间的进程隔离.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/19f1fa31d914462babe10c652cfb42df~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h4&gt;文件资源隔离&lt;/h4&gt;
&lt;p&gt;在创建命名空间使用的&lt;code&gt;clone&lt;/code&gt;函数中传入&lt;code&gt;CLONE_NEWNS&lt;/code&gt;参数，子进程即可得到父进程挂载的拷贝.&lt;/p&gt;
&lt;p&gt;当容器创建时，容器需要一个自己的rootfs来实现与别的容器文件资源隔离，所以当Docker创建容器时，会将容器需要的目录进行挂载，并改变容器能访问的根目录，将容器之间的文件系统隔离.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;改变容器能够访问个文件目录的根节点，libcontaine 提供的了&lt;code&gt;pivot_root&lt;/code&gt; 或者 &lt;code&gt;chroot&lt;/code&gt; 函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;网络隔离&lt;/h4&gt;
&lt;p&gt;当 Docker 启动时，会自动在主机上创建一个 docker0 虚拟网桥，实际上是 Linux 的一个 bridge，可以理解为一个软件交换机。它会在挂载到它的网口之间进行转发。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/af358f873ec44f69b322e27c6a6a3761~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;当创建一个 Docker 容器的时候，同时会创建了一对veth pair接口。这对接口一端在容器内，即是容器内部的eth0；&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;veth pair是成对出现的一种虚拟网络设备接口，一端连着网络协议栈，一端彼此相连，彼此联通的这端数据互通。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;http://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/149f27f80f5745a6966846b523b7ba50~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;另一端在本地并被挂载到 docker0 网桥，名称以 veth 开头。通过这种方式，主机可以跟容器通信，容器之间也可以相互通信。Docker就创建了在主机和所有容器之间一个虚拟共享网络。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Control Groups&lt;/code&gt;(控制组)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Control Groups（简称 CGroups）能够隔离宿主机器上的物理资源，CGroup提供以下这些功能.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;限制进程组可以使用的资源数量（Resource limiting ）。比如：memory子系统可以为进程组设定一个memory使用上限，一旦进程组使用的内存达到限额再申请内存，就会触发OOM（out of memory）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程组的优先级控制（Prioritization ）。比如：可以使用cpu子系统为某个进程组分配特定cpu share。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记录进程组使用的资源数量（Accounting ）。比如：可以使用cpuacct子系统记录某个进程组使用的cpu时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程组隔离（Isolation）。比如：使用ns子系统可以使不同的进程组使用不同的&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fbaike.baidu.com%2Fitem%2Fnamespace&quot; title=&quot;https://baike.baidu.com/item/namespace&quot;&gt;namespace&lt;/a&gt;，以达到隔离的目的，不同的进程组有各自的进程、网络、文件系统挂载空间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程组控制（Control）。比如：使用&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fbaike.baidu.com%2Fitem%2Ffreezer&quot; title=&quot;https://baike.baidu.com/item/freezer&quot;&gt;freezer&lt;/a&gt;子系统可以将进程组挂起和恢复&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;物理资源隔离&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;CGroup通过多个子系统来控制系统资源的分配.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们可以使用&lt;code&gt;lssubsys -m&lt;/code&gt;查看当前系统下CGroup对应的子系统目录.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cpuset&lt;/code&gt; &lt;code&gt;/sys/fs/cgroup/cpuset&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cpu，cpuacct&lt;/code&gt; &lt;code&gt;/sys/fs/cgroup/cpu，cpuacct&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blkio&lt;/code&gt; &lt;code&gt;/sys/fs/cgroup/blkio&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory&lt;/code&gt; &lt;code&gt;/sys/fs/cgroup/memory&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;devices&lt;/code&gt; &lt;code&gt;/sys/fs/cgroup/devices&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;freezer&lt;/code&gt; &lt;code&gt;/sys/fs/cgroup/freezer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;net_cls，net_prio&lt;/code&gt; &lt;code&gt;/sys/fs/cgroup/net_cls，net_prio&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;perf_event&lt;/code&gt; &lt;code&gt;/sys/fs/cgroup/perf_event&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pids&lt;/code&gt; &lt;code&gt;/sys/fs/cgroup/pids&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在宿主机内，首先当Docker启动时，会在上述的所有子系统下创建docker文件夹.&lt;/p&gt;
&lt;p&gt;当Docker创建容器时，会在docker文件夹下的task子目录下创建pid对应的新文件对容器资源进行分配以及管控.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d1738b491e1542ed8714637df711e7e2~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;UnionFS&lt;/code&gt;(联合文件系统)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Union文件系统（UnionFS）是一种分层、轻量级并且高性能的文件系统，它支持对文件系统的修改作为一次提交来一层层的叠加，同时可以将不同目录挂载到同一个虚拟文件系统下&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9ce9911333b744a1a56f289579e911cb~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?&quot; alt=&quot;2017-11-30-docker-filesystems.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;在Docker中，每一个镜像层都是建立在另一个镜像层之上的，同时所有的镜像层都是只读的，只有每个容器最顶层的容器层才可以被用户直接读写，所以需要一个文件系统对所有的文件进行管理.&lt;/p&gt;
&lt;h4&gt;镜像管理&lt;/h4&gt;
&lt;p&gt;在Docker中目前使用了多种文件系统对镜像进行管理，包括当前主流的&lt;code&gt;overlay2&lt;/code&gt;，&lt;code&gt;aufs&lt;/code&gt;等等.&lt;/p&gt;
&lt;p&gt;不同的存储驱动在存储镜像和容器文件时也有着完全不同的实现，有兴趣的读者可以在 Docker 的官方文档&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fdocs.docker.com%2Fstorage%2Fstoragedriver%2Fselect-storage-driver%2F&quot; title=&quot;https://docs.docker.com/storage/storagedriver/select-storage-driver/&quot;&gt;Docker storage drivers&lt;/a&gt; 中找到相应的内容。&lt;/p&gt;
&lt;h2&gt;Docker 常用命令&lt;/h2&gt;
&lt;p&gt;🐳 &lt;a href=&quot;https://www.bilibili.com/video/BV1MR4y1Q738/?spm_id_from=333.999.0.0&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;Docker 概念，工作流和实践 - 入门必懂&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Dockerfile&lt;/code&gt;｜&lt;code&gt;.dockerignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker build .&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker images&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker tag &amp;#x3C;IMAGE_ID&gt; &amp;#x3C;USER_NAEM/IMAGE_NAME&gt;:&amp;#x3C;TAG&gt;&lt;/code&gt;，比如 &lt;code&gt;docker tag e6f dansoncutnodejs:v1.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker login&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker push &amp;#x3C;USER_NAEM/IMAGE_NAME&gt;:&amp;#x3C;TAG&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker build -t eggpain-image .&lt;/code&gt;：构建的时候就指定镜像名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker rmi -f dansoncut/nodejs:v1.0&lt;/code&gt;：后面那串其实就是镜像名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker run -d &amp;#x3C;IMAGE_NAME&gt;&lt;/code&gt;：-d 是 detached-mode 让容器不占用当前的命令行窗口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker run -d -p &amp;#x3C;HOST_PORT:CONTAINER_PORT&gt; --name &amp;#x3C;CONTAINER_NAME&gt; &amp;#x3C;IMAGE_NAME&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker ps&lt;/code&gt;、&lt;code&gt;docker ps -a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker stop &amp;#x3C;CONTAINER_NAME&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker rm -f &amp;#x3C;CONTAINER_ID&gt;/&amp;#x3C;CONTAINER_NAME&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker exec -it &amp;#x3C;CONTAINER_NAME&gt; /bin/bash&lt;/code&gt;：以终端交互的方式进入容器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504132304030.png&quot; alt=&quot;image-20240422195743085&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Docker 网络模式&lt;/h2&gt;
&lt;p&gt;🐳 &lt;a href=&quot;https://www.bilibili.com/video/BV1Aj411r71b/?spm_id_from=333.999.0.0&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;Docker 网络模式 Linux&lt;/a&gt;【Bridge | Host | None】&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo docker network ls&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Bridge：&lt;strong&gt;如果没有指定容器的网络，默认加入的都是「默认 Bridge」网络&lt;/strong&gt;，自定义 Bridge 网络可以提供自动 DNS 解析（默认 Bridge 不会自动为容器间进行 DNS 解析），不同 Bridge 网络还可以实现更好的隔离，所以非常适合单个宿主里运行多个容器。真因为起到隔离作用，容器里的数据要出来需要地址转化，即 NAT，性能会被拉低。&lt;/li&gt;
&lt;li&gt;Host：Host 网络就不需要地址转换了，即不需要端口映射（直接指定 &lt;code&gt;--network host&lt;/code&gt;），因为容器直接和宿主合体了，适合高网络性能的场景，但存在安全问题；Host 网络没有自己的 IP 地址，&lt;strong&gt;在单个容器需要处理大量端口的时候，就不需要像 Bridge 网络那样逐个端口进行关联&lt;/strong&gt;，但是如果要多个容器用同一个端口会出现端口冲突（该模式仅能在 Linux 主机上实现）&lt;/li&gt;
&lt;li&gt;None：不能联网，适合进行备份，有些需要网络隔离的一次性操作也非常好用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo docker network create -d bridge &amp;#x3C;BRIDGE_NAME&gt;&lt;/code&gt;：自定义 Bridge 网络，创建的网络可以通过 &lt;code&gt;sudo docker network ls&lt;/code&gt; 查看；默认 Bridge 网络范围是 &lt;code&gt;172.17.0.0/16&lt;/code&gt;，网关是 &lt;code&gt;172.17.0.1&lt;/code&gt;；而新创建的网络则是 &lt;code&gt;172.18.0.0/16&lt;/code&gt;，网关为 &lt;code&gt;172.18.0.1&lt;/code&gt;。此后会出现一个类似 &lt;code&gt;docker0&lt;/code&gt; 的接口。&lt;/li&gt;
&lt;li&gt;Docker 安装后会出现一个 &lt;code&gt;docker 0&lt;/code&gt; 的端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo docker run -d --name naihe1 --hostname naihe1 --network naihe-bridge &amp;#x3C;IMAGE&gt;&lt;/code&gt;：&lt;strong&gt;创建一个容器加入到自定义的 Bridge 网络&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo docker run -d --name egg --network host &amp;#x3C;IMAGE&gt;&lt;/code&gt;：&lt;strong&gt;创建一个 Host 网络下的容器&lt;/strong&gt;，此时就不需要端口关联了，因为该容器和宿主共用所有网络和端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo docker run -it --network none &amp;#x3C;IMAGE&gt;&lt;/code&gt;：&lt;strong&gt;none 网络，无法联网&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo docker network rm &amp;#x3C;NETWORK_NAME&gt;&lt;/code&gt;：删除网络（Bridge 网络可以有多个，Host 网络只能有一个），注意，默认 bridge、host、none 网络都不能删除；该命令仅针对自定义 Bridge 网络。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202404222109674.png&quot; alt=&quot;image-20240422203844488&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202504132305798.png&quot; alt=&quot;image-20240422204313870&quot;&gt;&lt;/p&gt;
&lt;h2&gt;dockerfile 与 docker-compose 通俗解释&lt;/h2&gt;
&lt;p&gt;先简单理解 docker 的使用过程，它分为&lt;strong&gt;镜像构建&lt;/strong&gt;与&lt;strong&gt;容器启动&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;镜像构建：即创建一个镜像，它包含安装运行所需的环境、程序代码等。这个创建过程就是使用 &lt;em&gt;dockerfile&lt;/em&gt; 来完成的。&lt;/li&gt;
&lt;li&gt;容器启动：容器最终运行起来是通过拉取构建好的镜像，通过一系列运行指令（如端口映射、外部数据挂载、环境变量等）来启动服务的。针对单个容器，这可以通过 &lt;code&gt;docker run&lt;/code&gt; 来运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而如果涉及多个容器的运行（如服务编排）就可以通过 &lt;code&gt;docker-compose&lt;/code&gt; 来实现，它可以轻松的将多个容器作为 service 来运行（当然也可仅运行其中的某个），并且提供了 scale (服务扩容) 的功能。&lt;/p&gt;
&lt;p&gt;🙄简单总结：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;dockerfile: 构建镜像；&lt;/li&gt;
&lt;li&gt;docker run: 启动容器；&lt;/li&gt;
&lt;li&gt;docker-compose: 启动服务；&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;从头开始：dockerfile、docker run、docker-compose 存在的必要性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假如你不用 docker ，搭建 wordpress 怎么弄？先找台 server ，假设其 OS 为 Ubuntu ，然后按照文档一步步敲命令，写配置，对吧？&lt;/p&gt;
&lt;p&gt;用 docker 呢？ 随便找台 server ，不管什么操作系统，只要支持 docker 就行， &lt;code&gt;docker run ubuntu&lt;/code&gt;, docker 会从官方源里拉取最新的 Ubuntu 镜像，可以认为你开了个 Ubuntu 虚拟机，然后一步步安装，跟上面一样。&lt;/p&gt;
&lt;p&gt;但是这样安装有个显著的缺点，一旦 container 被删，你做的工作就都没了。当然可以用 &lt;code&gt;docker commit&lt;/code&gt; 来保存成镜像，这样就可以复用了。&lt;/p&gt;
&lt;p&gt;但是镜像一般比较大，而且只分享镜像的话，别人也不知道你这镜像到底包含什么，这些问题都不利于分享和复用。&lt;/p&gt;
&lt;p&gt;一个直观的解决方案就是，写个脚本把安装过程全部记录下来，这样再次安装的时候，执行脚本就行了。 Dockerfile 就是这样的脚本，它记录了一个镜像的制作过程。&lt;/p&gt;
&lt;p&gt;有了 Dockerfile, 只要执行 &lt;code&gt;docker build .&lt;/code&gt; 就能制作镜像，而且 Dockerfile 就是文本文件，修改也很方便。&lt;/p&gt;
&lt;p&gt;现在有了 wordpress 的镜像，只需要 &lt;code&gt;docker run&lt;/code&gt; 就把 wordpress 启动起来了。&lt;/p&gt;
&lt;p&gt;如果仅仅是 wordpress 这也就够了。但是很多时候，需要多个镜像合作才能启动一个服务，比如前端要有 nginx ， 数据库 mysql, 邮件服务等等，当然你可以把所有这些都弄到一个镜像里去，但这样做就无法复用了。&lt;/p&gt;
&lt;p&gt;更常见的是，nginx, mysql, smtp 都分别是个镜像，然后这些镜像合作，共同服务一个项目。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker-compose&lt;/code&gt; 就是解决这个问题的。你的项目需要哪些镜像，每个镜像怎么配置，要挂载哪些 &lt;code&gt;volume&lt;/code&gt;, 等等信息都包含在 &lt;em&gt;docker-compose.yml&lt;/em&gt; 里。&lt;/p&gt;
&lt;p&gt;要启动服务，只需要 &lt;code&gt;docker-compose up&lt;/code&gt; 就行，停止也只需要 &lt;code&gt;docker-compse stop/down&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;简而言之， Dockerfile 记录单个镜像的构建过程， docker-compse.yml 记录一个项目（project, 一般是多个镜像）的构建过程。&lt;/p&gt;
&lt;p&gt;你说有些教程用了 &lt;em&gt;&lt;strong&gt;dockerfile&lt;/strong&gt;&lt;/em&gt; + &lt;em&gt;&lt;strong&gt;docker-compose&lt;/strong&gt;&lt;/em&gt;, 是因为 &lt;em&gt;&lt;strong&gt;docker-compose.yml&lt;/strong&gt;&lt;/em&gt; 本身没有镜像构建的信息，如果镜像是从 &lt;em&gt;docker registry&lt;/em&gt; 拉取下来的，那么 Dockerfile 就不需要；如果镜像是需要 build 的，那就需要提供 Dockerfile.&lt;/p&gt;
&lt;p&gt;docker-compose是编排容器的。例如，你有一个 php 镜像，一个 mysql 镜像，一个 nginx 镜像。如果没有 docker-compose，那么每次启动的时候，你需要敲各个容器的启动参数，环境变量，容器命名，指定不同容器的链接参数等等一系列的操作，相当繁琐。而用了 docker-composer 之后，你就可以把这些命令一次性写在 &lt;em&gt;docker-composer.yml&lt;/em&gt; 文件中，以后每次启动这一整个环境（含 3 个容器）的时候，你只要敲一个 &lt;code&gt;docker-composer up&lt;/code&gt; 命令就ok了。&lt;/p&gt;
&lt;p&gt;dockerfile 的作用是从无到有的构建镜像。它包含安装运行所需的环境、程序代码等。这个创建过程就是使用 dockerfile 来完成的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt;：为 &lt;code&gt;docker build&lt;/code&gt; 命令准备的，用于建立一个独立的 image ，在 docker-compose 里也可以用来实时 build。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;docker-compose.yml&lt;/strong&gt;：为 &lt;code&gt;docker-compose&lt;/code&gt; 准备的脚本，可以同时管理多个 container ，包括他们之间的关系、用官方 image 还是自己 build 、各种网络端口定义、储存空间定义等。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;看到这里相信各位读者朋友们对 Docker 已经有了更为深刻的理解.&lt;/p&gt;
&lt;p&gt;由于 Docker 更新至今，代码库太过庞大，也只能从低版本的 Docker 源码以及大佬们的 Docker 技术文章中窥其一二，如果有感兴趣的朋友也可以相互交流.&lt;/p&gt;
&lt;h2&gt;参考文献&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;《Docker源码分析》(孙宏亮)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fdraveness.me%2Fdocker%2F&quot; title=&quot;https://draveness.me/docker/&quot;&gt;Docker 核心技术与实现原理 - 面向信仰编程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Ftech.bytedance.net%2Farticles%2F6872293753253134344%23heading1&quot; title=&quot;https://tech.bytedance.net/articles/6872293753253134344#heading1&quot;&gt;docker容器技术介绍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fqq_43413788%2Farticle%2Fdetails%2F108162920&quot; title=&quot;https://blog.csdn.net/qq_43413788/article/details/108162920&quot;&gt;Docker基础镜像是什么？都有哪些？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fusstmiracle%2Farticle%2Fdetails%2F111178532&quot; title=&quot;https://blog.csdn.net/usstmiracle/article/details/111178532&quot;&gt;虚拟化技术介绍 &amp;#x26; hypervisor简介&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Ftech.bytedance.net%2Farticles%2F6872293753253134344%23heading1&quot; title=&quot;https://tech.bytedance.net/articles/6872293753253134344#heading1&quot;&gt;docker容器技术介绍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fdeng624796905%2Farticle%2Fdetails%2F86493330%3Fops_request_misc%3D%25257B%252522request%25255Fid%252522%25253A%252522166107576616782246456566%252522%25252C%252522scm%252522%25253A%25252220140713.130102334..%252522%25257D%26request_id%3D166107576616782246456566%26biz_id%3D0%26utm_medium%3Ddistribute.pc_search_result.none-task-blog-2~blog~top_positive~default-2-86493330-null-null.nonecase%26utm_term%3Ddocker%26spm%3D1018.2226.3001.4450&quot; title=&quot;https://blog.csdn.net/deng624796905/article/details/86493330?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166107576616782246456566%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&amp;#x26;request_id=166107576616782246456566&amp;#x26;biz_id=0&amp;#x26;utm_medium=distribute.pc_search_result.none-task-blog-2~blog~top_positive~default-2-86493330-null-null.nonecase&amp;#x26;utm_term=docker&amp;#x26;spm=1018.2226.3001.4450&quot;&gt;这可能是最为详细的Docker入门吐血总结&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fcbmljs%2Farticle%2Fdetails%2F123353108&quot; title=&quot;https://blog.csdn.net/cbmljs/article/details/123353108&quot;&gt;Docker架构简介&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fqq_33934427%2Farticle%2Fdetails%2F120487243%3Fspm%3D1001.2101.3001.6650.1%26utm_medium%3Ddistribute.pc_relevant.none-task-blog-2%257Edefault%257ECTRLIST%257ERate-1-120487243-blog-111178532.pc_relevant_default%26depth_1-utm_source%3Ddistribute.pc_relevant.none-task-blog-2%257Edefault%257ECTRLIST%257ERate-1-120487243-blog-111178532.pc_relevant_default%26utm_relevant_index%3D2&quot; title=&quot;https://blog.csdn.net/qq_33934427/article/details/120487243?spm=1001.2101.3001.6650.1&amp;#x26;utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-120487243-blog-111178532.pc_relevant_default&amp;#x26;depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-120487243-blog-111178532.pc_relevant_default&amp;#x26;utm_relevant_index=2&quot;&gt;VM VS Container 浅谈Hpervisor虚拟化技术和容器技术&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fwww.huweihuang.com%2Farticle%2Fdocker%2Fdocker-commands-principle%2F&quot; title=&quot;https://www.huweihuang.com/article/docker/docker-commands-principle/&quot;&gt;Docker常用命令原理图&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fweixin_35470511%2Farticle%2Fdetails%2F116889363&quot; title=&quot;https://blog.csdn.net/weixin_35470511/article/details/116889363&quot;&gt;Linux镜像run起来，六、Docker run 运行镜像&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/202311141756070.hDBvTaXK.jpg"/><enclosure url="/_astro/202311141756070.hDBvTaXK.jpg"/></item><item><title>Git 个人工作流分享</title><link>https://coooredump.github.io/blog/productivity-tool/git-workflow</link><guid isPermaLink="true">https://coooredump.github.io/blog/productivity-tool/git-workflow</guid><description>有关 git 的常用指令</description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;查看本地分支与远程分支的对应关系&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 查看远程分支与本地分支的对应关系
$ git branch -vv
# 查看所有分支（远程 + 本地）
$ git branch -a
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;新建远程分支 | &lt;code&gt;push&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git checkout &amp;#x3C;local-branch&gt;
# 本地分支和远程分支的名字可不一样，但一般是同名分支 | 关联后 (--set-upstream) 可直接 push
# git push origin master 这个master指的是远程分支，因为本地分支默认为当前分支
$ git push origin &amp;#x3C;local-branch&gt;:&amp;#x3C;remote-branch&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tip: &lt;code&gt;push&lt;/code&gt; 的时候只会上传当前的 &lt;code&gt;branch&lt;/code&gt; 的指向，并不会把本地的 &lt;code&gt;HEAD&lt;/code&gt; 的指向也一起上传到远程仓库。事实上，远程仓库的 &lt;code&gt;HEAD&lt;/code&gt; 是永远指向它的默认分支（即 master，如果不修改它的名称的话），并会随着默认分支的移动而移动的。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;--set-upstream&lt;/code&gt; 设置远程分支与本地分支关联&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 关联远程仓库的 master 分支与本地的 master 分支，该方法在 push 中可设置，如果要直接绑定分支但不 push 呢，见下面命令行
$ git push --set-upstream origin master:master
# 通过 branch 命令直接设置
$ git branch --set-upstream-to=origin/feature2 feature2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305221418898.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;拉取远程分支 | &lt;code&gt;pull&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 说明：关联后 (--set-upstream) 可直接 push
# git pull origin master 这个 master 指的是远程分支，因为本地分支默认为当前分支
$ git pull origin &amp;#x3C;remote-branch&gt;:&amp;#x3C;local-branch&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;根据远程分支上创建本地分支&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 只有远程分支的时候，根据远程分支创建本地分支即可
$ git checkout -b &amp;#x3C;local-branch&gt; origin/&amp;#x3C;remote-branch&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;删除远程分支&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 说明：git push origin &amp;#x3C;local-branch&gt;/&amp;#x3C;remote-branch&gt;
# Method1: 推送空分支到远程分支 remote-branch，相当于删除远程分支
$ git push origin :&amp;#x3C;remote-branch&gt;
# Method2: 直接删除远程分支
$ git push origin --delete &amp;#x3C;remote-branch&gt;
# Tip: 删除本地分支
$ git branch -d &amp;#x3C;local-branch&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;区分 &lt;code&gt;git reset&lt;/code&gt; &amp;#x26; &lt;code&gt;git checkout&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;⭐Reference: https://blog.csdn.net/longintchar/article/details/82314102&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305290945794.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git reset&lt;/code&gt; 会带着当前指向的 branch 一起向前推进&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git checkout&lt;/code&gt; 只会修改当前 HEAD 的指向，可先用 checkout 跳转到某次提交处，然后创建某个分支 feature（此时的 HEAD 并未指向 feature1，而是仅仅处于同一处 commit），最后再使用 checkout 切换到该 feature 分支上（即 HEAD 指向 feature 分支）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;checkout&lt;/code&gt; 签出动作会将 HEAD 与 branch 分离开来，它有一个专门的参数用于让 HEAD 与 branch 脱离且不移动 HEAD 的用法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git checkout --detach
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/30/1600acce7b90b009~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;git checkout --detach&quot;&gt;&lt;/p&gt;
&lt;h2&gt;git 各式后悔药&lt;/h2&gt;
&lt;p&gt;git 管理仓库时，往往需要撤销某些操作/提交/暂存内容。&lt;/p&gt;
&lt;h3&gt;1. 撤销工作区的文件修改&lt;/h3&gt;
&lt;p&gt;如果工作区的某个文件被改乱了，但还没有执行 &lt;code&gt;git add&lt;/code&gt;，可以用 &lt;code&gt;git checkout&lt;/code&gt; 命令&lt;strong&gt;找回本次修改之前的文件&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git checkout -- [filename]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⭐它的原理是先找暂存区，如果该文件有暂存的版本，则恢复该版本，否则恢复上一次提交的版本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：工作区的文件变化一旦被撤销，就无法找回了。&lt;/p&gt;
&lt;h3&gt;2. 从暂存区撤销文件&lt;/h3&gt;
&lt;p&gt;如果不小心把一个文件添加到暂存区，可以用下面的命令撤销。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git rm --cached [filename]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的命令不影响已经提交的内容。&lt;/p&gt;
&lt;h3&gt;3. 替换上一次提交信息&lt;/h3&gt;
&lt;p&gt;💔情况一：&lt;strong&gt;提交以后，发现提交信息写错了&lt;/strong&gt;，这时可以使用 &lt;code&gt;git commit&lt;/code&gt; 命令的 &lt;code&gt;--amend&lt;/code&gt; 参数，可以修改上一次的提交信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git commit --amend -m &quot;Fixes bug #42&quot;
# git commit --amend 也可
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的原理是产生一个新的提交对象，替换掉上一次提交产生的提交对象。&lt;/p&gt;
&lt;p&gt;💔情况二：&lt;strong&gt;提交之后，发现提交的文件需要修改&lt;/strong&gt;！这时先修改好工作区，然后再执行 &lt;code&gt;add&lt;/code&gt; 后执行 &lt;code&gt;git commit --amend&lt;/code&gt;，因为&lt;strong&gt;这时暂存区有发生变化的文件，会一起提交到仓库。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git commit --amend -m &quot;append info&quot;
# git commit --amend 也可
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，&lt;code&gt;--amend&lt;/code&gt; 不仅可以修改提交信息，还可以整个把上一次提交替换掉。&lt;/p&gt;
&lt;h3&gt;4. 撤销某次提交 | 但需新增来覆盖提交&lt;/h3&gt;
&lt;p&gt;一种常见的场景是，提交代码以后，你突然意识到这个提交有问题，应该撤销掉，这时执行下面的命令就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git revert HEAD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305251012520.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;上面命令的原理是，在当前提交后面，⭐&lt;strong&gt;新增一次提交(commit+1)，抵消掉上一次提交导致的所有变化(workspace&amp;#x26;stage change)&lt;/strong&gt;。它&lt;strong&gt;不会改变过去的历史&lt;/strong&gt;，所以是首选方式，&lt;strong&gt;没有任何丢失代码的风险&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git revert&lt;/code&gt; 命令只能抵消上一个提交，如果想抵消多个提交，必须在命令行依次指定这些提交。比如，抵消前两个提交，要像下面这样写。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git revert [倒数第一个提交] [倒数第二个提交]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git revert&lt;/code&gt; 命令还有两个参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--no-edit&lt;/code&gt;：执行时不打开默认编辑器，直接使用 Git 自动生成的提交信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--no-commit&lt;/code&gt;：&lt;strong&gt;只抵消暂存区(stage)和工作区的文件变化，不产生新的提交&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 丢弃提交 | 回溯&lt;/h3&gt;
&lt;p&gt;如果希望以前的提交在历史中彻底消失，而不是被抵消掉，可以使用&lt;code&gt;git reset&lt;/code&gt;命令，丢弃掉某个提交之后的所有提交。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git reset [last good SHA]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git reset&lt;/code&gt;的原理是，让最新提交的指针回到以前某个时点，该时点之后的提交都从历史中消失。&lt;/p&gt;
&lt;p&gt;默认情况下，&lt;code&gt;git reset&lt;/code&gt;不改变工作区的文件（但会改变暂存区），&lt;code&gt;--hard&lt;/code&gt; 参数可以让工作区里面的文件也回到以前的状态。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git reset --hard [last good SHA]
# 或者
$ git reset --hard HEAD^1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;执行 &lt;code&gt;git reset&lt;/code&gt; 命令之后，如果想找回那些丢弃掉的提交，可以使用 &lt;code&gt;git reflog&lt;/code&gt; 命令&lt;/strong&gt;，具体做法参考&lt;a href=&quot;https://github.blog/2015-06-08-how-to-undo-almost-anything-with-git/#redo-after-undo-local&quot;&gt;这里&lt;/a&gt;。不过，这种做法有&lt;strong&gt;时效性&lt;/strong&gt;，时间长了可能找不回来。&lt;/p&gt;
&lt;h3&gt;6. 撤销当前分支的变化&lt;/h3&gt;
&lt;p&gt;你在当前 error 分支上做了几次提交，突然发现&lt;strong&gt;放错了分支&lt;/strong&gt;，这几个提交本应该放到 master 分支。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 将 error 分支上的&amp;#x3C;最新一次&gt;提交转移到 master 分支
$ git checkout master
$ git cherry-pick error

# 将 error 分支上的多次提交转移到 master 分支，注意：SHA1 比 SHA2 来得早！
$ git checkout master
$ git cherry-pick [SHA1] [SHA2]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;目标：将 error 分支上的最新两次 commit 转移到 master 分支&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305251129240.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;先执行 &lt;code&gt;git checkout master&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;再执行 &lt;code&gt;git cherry-pick [SHA1] [SHA2]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;有冲突解决冲突，没冲突即可完成&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305251130555.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;OK！error 分支想要 reset 就 reset~&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305251133726.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;合并分支 | &lt;code&gt;merge&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;由于现在 Git 仓库处于冲突待解决的中间状态（已执行 &lt;code&gt;merge&lt;/code&gt; 操作），所以如果你最终决定放弃这次 &lt;code&gt;merge&lt;/code&gt;，也需要执行一次 &lt;code&gt;merge --abort&lt;/code&gt; 来手动取消它。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 回到 merge 前的状态
$ git merge --abort
# 在 master 分支上合并 feature 分支
$ git checkout master
$ git merge feature
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;主流工作流 Feature Branching&lt;/h2&gt;
&lt;p&gt;这种工作流的核心内容可以总结为两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任何新的功能（feature）或 bug 修复全都新建一个 &lt;code&gt;branch&lt;/code&gt; 来写；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;branch&lt;/code&gt; 写完后，合并到 &lt;code&gt;master&lt;/code&gt;，然后删掉这个 &lt;code&gt;branch&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/21/15fde6edbfe362c4~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;Feature Branching&quot;&gt;&lt;/p&gt;
&lt;p&gt;这就是这种工作流最基本的模型。&lt;/p&gt;
&lt;p&gt;从上面的动图来看，这种工作流似乎没什么特别之处。但实质上，Feature Branching 这种工作流，为团队开发时两个关键的问题提供了解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码分享&lt;/li&gt;
&lt;li&gt;一人多任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 代码分享&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 本地电脑
$ git checkout -b books
$ git push origin books
# 同事电脑
$ git pull
$ git checkout books
$ git checkout master
$ git pull	# merge 之前 pull 一下，让 master 更新到和远程仓库同步
$ git merge books
$ git push
# 删除本地 books 分支，删除远程 books 分支
$ git branch -d books
$ git push origin -d books
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;借助 GitHub 的 Pull Request 简化 Feature Branching 工作流&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;事实上，上面所说的这个流程，还可以利用 &lt;strong&gt;Pull Request&lt;/strong&gt; 来进一步简化。&lt;/p&gt;
&lt;p&gt;Pull Request 并不是 Git 的内容，而是一些 Git 仓库服务提供方（例如 GitHub）所提供的一种便捷功能，它可以让团队的成员方便地讨论一个 &lt;code&gt;branch&lt;/code&gt; ，并在讨论结束后一键合并这个 &lt;code&gt;branch&lt;/code&gt; 到 &lt;code&gt;master&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;同样是把写好的 &lt;code&gt;branch&lt;/code&gt; 给同事看，使用 Pull Request 的话你可以这样做：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把 &lt;code&gt;branch&lt;/code&gt; &lt;code&gt;push&lt;/code&gt; 到中央仓库；&lt;/li&gt;
&lt;li&gt;在中央仓库处创建一个 Pull Request。以 GitHub 为例：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305230950674.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后你的同事就可以在 GitHub 上看到你创建的 Pull Request 了。他们可以在 GitHub 的这个页面查看你的 &lt;code&gt;commit&lt;/code&gt;s，也可以给你评论表示赞同或提意见，你接下来也可以根据他们的意见把新的 &lt;code&gt;commit&lt;/code&gt;s &lt;code&gt;push&lt;/code&gt; 上来，这也页面会随着你新的 &lt;code&gt;push&lt;/code&gt; 而展示出最新的 &lt;code&gt;commits&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在讨论结束以后，你们一致认为这个 &lt;code&gt;branch&lt;/code&gt; 可以合并了，你只需要点一下页面中那个绿色的 &quot;Merge pull request&quot; 按钮，GitHub 就会自动地在中央仓库帮你把 &lt;code&gt;branch&lt;/code&gt; 合并到 &lt;code&gt;master&lt;/code&gt; 了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305230951662.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后你只要在本地 &lt;code&gt;pull&lt;/code&gt; 一下，把最新的内容拉到你的电脑上，这件事情就算完成了。&lt;/p&gt;
&lt;p&gt;另外，GitHub 还设计了一个贴心的 &quot;Delete branch&quot; 按钮，方便你在合并之后一键删除 &lt;code&gt;branch&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;完整的例子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305231003034.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305231004086.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305231004038.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305231005995.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 拉取 feature2 分支
$ git pull origin feature2:feature2
$ git branch --set-upstream-to=origin/feature2 feature2
$ git branch -vv
  feature1 c16362b [origin/feature1] Merge branch &apos;master&apos; into feature1
* feature2 db3024c [origin/feature2] create branch feature2
  master   55c2952 [origin/master: behind 2] Merge pull request
$ git checkout master
# master 分支也要 pull
$ git pull origin master:master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305231015784.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2. 一人多任务&lt;/h3&gt;
&lt;p&gt;你正在认真写着代码，忽然同事过来跟你说：「内个……你这个功能先放一放吧，我们最新讨论出要做另一个更重要的功能，你来做一下吧。」&lt;/p&gt;
&lt;p&gt;如果你是在独立的 &lt;code&gt;branch&lt;/code&gt; 上做事，切换任务是很简单的。你只要稍微把目前未提交的代码简单收尾一下，然后做一个带有「未完成」标记的提交（例如，在提交信息里标上「TODO」），然后回到 &lt;code&gt;master&lt;/code&gt; 去创建一个新的 &lt;code&gt;branch&lt;/code&gt; 就好了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 切换回 master 主分支！！！因为需要从主分支上创建分支
$ git checkout master
$ git checkout -b new_feature
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查改历史改动记录&lt;/h2&gt;
&lt;h3&gt;git log&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 在 .bashrc 中设置 git-log 别名，可图形化输出 git log 的信息
$ alias git-log=&apos;git log --pretty=oneline --all --graph --abbrev-commit&apos;
$ git-log
# 查看历史改动信息
$ git log
# 查看详细的历史记录，可以看到每个 commit 的具体改动细节（-p 是 --patch 的缩写）
$ git log -p
# 查看简要统计，只想大致看一下改动内容
$ git log --stat
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git show&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 查看某个具体的 commit: $ git show 查看当前 commit
$ git show &amp;#x3C;SHA-1&gt;
$ git show &amp;#x3C;SHA-1&gt; &amp;#x3C;filename&gt;	# 具体到某个指定文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git diff&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;比对本地工作目录与暂存区的内容&lt;/strong&gt;（即显示未使用 &lt;code&gt;git add&lt;/code&gt; 加入暂存区的内容与暂存区的内容的不同之处）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git diff
diff --git a/README.md b/README.md
index 4b572c1..0f8ceb4 100644
--- a/README.md
+++ b/README.md
@@ -5,3 +5,4 @@
 4. `git branch feature1` &amp;#x26;&amp;#x26; `git checkout feature1` &amp;#x26;&amp;#x26; `git checkout master` &amp;#x26;&amp;#x26; `git merge feature1` (feature1)
 5. master branch (feature branching, solve conflict by `pull request`)
 6. feature2
+7. 哈哈哈哈
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;比对暂存区与上一条提交的内容&lt;/strong&gt;（即显示 &lt;code&gt;git add&lt;/code&gt; 后的内容与上次 &lt;code&gt;commit&lt;/code&gt; 之间内容的不同之处）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 二者命令完全等价
$ git diff --staged
$ git diff --cached
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;比对工作目录和上一条提交的内容&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;git diff HEAD&lt;/code&gt; 可以显示工作目录和上一条提交之间的不同，它是上面这二者的内容相加。换句话说，这条指令可以让你看到「如果你现在把所有文件都 &lt;code&gt;add&lt;/code&gt; 然后 &lt;code&gt;git commit&lt;/code&gt;，你将会提交什么」（不过需要注意，没有被 Git 记录在案的文件（即从来没有被 add 过 的文件，untracked files 并不会显示出来。为什么？因为对 Git 来说它并不存在啊）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305231055267.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git diff HEAD
# 也可 git diff &amp;#x3C;SHA-1&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实质上，如果你把 &lt;code&gt;HEAD&lt;/code&gt; 换成别的 &lt;code&gt;commit&lt;/code&gt;，也可以显示当前工作目录和这条 &lt;code&gt;commit&lt;/code&gt; 的区别。&lt;/p&gt;
&lt;h2&gt;不喜欢 merge 的分叉，那就用 rebase 吧&lt;/h2&gt;
&lt;p&gt;rebase —— 变基？！&lt;/p&gt;
&lt;p&gt;其实这个翻译还是比较准确的。&lt;code&gt;rebase&lt;/code&gt; 的意思是，给你的 &lt;code&gt;commit&lt;/code&gt; 序列重新设置基础点（也就是父 &lt;code&gt;commit&lt;/code&gt;）。展开来说就是，把你指定的 &lt;code&gt;commit&lt;/code&gt; 以及它所在的 &lt;code&gt;commit&lt;/code&gt; 串，以指定的目标 &lt;code&gt;commit&lt;/code&gt; 为基础，依次重新提交一次。例如下面这个 &lt;code&gt;merge&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# merge 过来；rebase 过去
$ git merge branch1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;master: 1-2-3-4-&lt;strong&gt;7&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;branch1: 1-2-5-6-&lt;strong&gt;7&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/21/15fdea7b6646a1f3~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果把 &lt;code&gt;merge&lt;/code&gt; 换成 &lt;code&gt;rebase&lt;/code&gt;，可以这样操作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git checkout branch1
# merge 过来；rebase 过去
$ git rebase master
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;master: 1-2-3-4-&lt;strong&gt;7-8&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;branch1: 1-2-&lt;strong&gt;5-6&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;master 上的 7、8 是 branch1 上的 5、6 rebase 过去的~&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/30/1600abd620a8e28c~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看出，通过 &lt;code&gt;rebase&lt;/code&gt;，&lt;code&gt;5&lt;/code&gt; 和 &lt;code&gt;6&lt;/code&gt; 两条 &lt;code&gt;commit&lt;/code&gt;s 把基础点从 &lt;code&gt;2&lt;/code&gt; 换成了 &lt;code&gt;4&lt;/code&gt; 。通过这样的方式，就让本来分叉了的提交历史重新回到了一条线。这种「重新设置基础点」的操作，就是 &lt;code&gt;rebase&lt;/code&gt; 的含义。&lt;/p&gt;
&lt;p&gt;另外，在 &lt;code&gt;rebase&lt;/code&gt; 之后，记得切回 &lt;code&gt;master&lt;/code&gt; 再 &lt;code&gt;merge&lt;/code&gt; 一下，把 &lt;code&gt;master&lt;/code&gt; 移到最新的 &lt;code&gt;commit&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git checkout master
$ git merge branch1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;master/branch1: 1-2-3-4-7-8&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/12/2/160149e054fe485c~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么要从 &lt;code&gt;branch1&lt;/code&gt; 来 &lt;code&gt;rebase&lt;/code&gt;，然后再切回 &lt;code&gt;master&lt;/code&gt; 再 &lt;code&gt;merge&lt;/code&gt; 一下这么麻烦，而不是直接在 &lt;code&gt;master&lt;/code&gt; 上执行 &lt;code&gt;rebase&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;从图中可以看出，&lt;code&gt;rebase&lt;/code&gt; 后的 &lt;code&gt;commit&lt;/code&gt; 虽然内容和 &lt;code&gt;rebase&lt;/code&gt; 之前相同，但它们已经是不同的 &lt;code&gt;commits&lt;/code&gt; 了。如果直接从 &lt;code&gt;master&lt;/code&gt; 执行 &lt;code&gt;rebase&lt;/code&gt; 的话，就会是下面这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/12/2/16014b5a6919c0b7~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;这就导致 &lt;code&gt;master&lt;/code&gt; 上之前的两个最新 &lt;code&gt;commit&lt;/code&gt; 被剔除了。如果这两个 &lt;code&gt;commit&lt;/code&gt; 之前已经在中央仓库存在，这就会导致没法 &lt;code&gt;push&lt;/code&gt; 了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/12/2/16014bc64d4337f8~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以，为了避免和远端仓库发生冲突，一般不要从 &lt;code&gt;master&lt;/code&gt; 向其他 &lt;code&gt;branch&lt;/code&gt; 执行 &lt;code&gt;rebase&lt;/code&gt; 操作。而如果是 &lt;code&gt;master&lt;/code&gt; 以外的 &lt;code&gt;branch&lt;/code&gt; 之间的 &lt;code&gt;rebase&lt;/code&gt;（比如 &lt;code&gt;branch1&lt;/code&gt; 和 &lt;code&gt;branch2&lt;/code&gt; 之间)，就不必这么多费一步，直接 &lt;code&gt;rebase&lt;/code&gt; 就好。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;⭐以上情况是不发生冲突的 &lt;code&gt;rebase&lt;/code&gt;，如果发生冲突了，那么就需要先解决冲突再执行如下命令：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;以下展示另外一个例子&lt;/strong&gt;！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 1.修改冲突文件
# 2.git add. &amp;#x26; git commit -m &quot;fix conflict&quot;
# 3.rebase continue
$ git rebase --continue
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;初始状态&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305250955548.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;git checkout rebase-branch&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git rebase master&lt;/code&gt;：发生 conflict 在 &lt;strong&gt;&quot;step 3&quot;&lt;/strong&gt;！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305250949354.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305250956107.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;解决冲突后，再执行 &lt;code&gt;add&lt;/code&gt; 与 &lt;code&gt;commit&lt;/code&gt;（⭐甚至这一步都不需要 &lt;code&gt;commit&lt;/code&gt;，在 &lt;code&gt;add&lt;/code&gt; 之后即可）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305251000282.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;git rebase --continue&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305251001570.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;git checkout master&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git merge rebase-branch&lt;/code&gt;：相当于 fast-forward！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305251003603.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;⭐总结：需要说明的是，&lt;code&gt;rebase&lt;/code&gt; 是站在需要被 &lt;code&gt;rebase&lt;/code&gt; 的 &lt;code&gt;commit&lt;/code&gt; 上进行操作，这点和 &lt;code&gt;merge&lt;/code&gt; 是不同的（相反）。&lt;/p&gt;
&lt;h2&gt;如何修复倒数第 2 个 commit | 交互式 rebase&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;commit --amend&lt;/code&gt; 可以修复最新 &lt;code&gt;commit&lt;/code&gt; 的错误，但如果是倒数第二个 &lt;code&gt;commit&lt;/code&gt; 写错了，怎么办？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果不是最新的 &lt;code&gt;commit&lt;/code&gt; 写错，就不能用 &lt;code&gt;commit --amend&lt;/code&gt; 来修复了，而是要用 &lt;code&gt;rebase&lt;/code&gt;。不过需要给 &lt;code&gt;rebase&lt;/code&gt; 也加一个参数：&lt;code&gt;-i&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rebase -i&lt;/code&gt; 是 &lt;code&gt;rebase --interactive&lt;/code&gt; 的缩写形式，意为「交互式 rebase」。&lt;/p&gt;
&lt;p&gt;所谓「交互式 rebase」，就是在 &lt;code&gt;rebase&lt;/code&gt; 的操作执行之前，你可以指定要 &lt;code&gt;rebase&lt;/code&gt; 的 &lt;code&gt;commit&lt;/code&gt; 链中的每一个 &lt;code&gt;commit&lt;/code&gt; 是否需要进一步修改。&lt;/p&gt;
&lt;p&gt;那么你就可以利用这个特点，进行一次「原地 rebase」。&lt;/p&gt;
&lt;p&gt;例如你是在写错了 &lt;code&gt;commit&lt;/code&gt; 之后，又提交了一次才发现之前写错了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/22/15fdf5fd00a27f45~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在再用 &lt;code&gt;commit --amend&lt;/code&gt; 已经晚了，但可以用 &lt;code&gt;rebase -i&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git rebase -i HEAD^^
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：在 Git 中，有两个「偏移符号」： &lt;code&gt;^&lt;/code&gt; 和 &lt;code&gt;~&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;^&lt;/code&gt; 的用法：在 &lt;code&gt;commit&lt;/code&gt; 的后面加一个或多个 &lt;code&gt;^&lt;/code&gt; 号，可以把 &lt;code&gt;commit&lt;/code&gt; 往回偏移，偏移的数量是 &lt;code&gt;^&lt;/code&gt; 的数量。例如：&lt;code&gt;master^&lt;/code&gt; 表示 &lt;code&gt;master&lt;/code&gt; 指向的 &lt;code&gt;commit&lt;/code&gt; 之前的那个 &lt;code&gt;commit&lt;/code&gt;； &lt;code&gt;HEAD^^&lt;/code&gt; 表示 &lt;code&gt;HEAD&lt;/code&gt; 所指向的 &lt;code&gt;commit&lt;/code&gt; 往前数两个 &lt;code&gt;commit&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~&lt;/code&gt; 的用法：在 &lt;code&gt;commit&lt;/code&gt; 的后面加上 &lt;code&gt;~&lt;/code&gt; 号和一个数，可以把 &lt;code&gt;commit&lt;/code&gt; 往回偏移，偏移的数量是 &lt;code&gt;~&lt;/code&gt; 号后面的数。例如：&lt;code&gt;HEAD~5&lt;/code&gt; 表示 &lt;code&gt;HEAD&lt;/code&gt; 指向的 &lt;code&gt;commit&lt;/code&gt;往前数 5 个 &lt;code&gt;commit&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面这行代码表示，把当前 &lt;code&gt;commit&lt;/code&gt; （ &lt;code&gt;HEAD&lt;/code&gt; 所指向的 &lt;code&gt;commit&lt;/code&gt;） &lt;code&gt;rebase&lt;/code&gt; 到 &lt;code&gt;HEAD&lt;/code&gt; 之前 2 个的 &lt;code&gt;commit&lt;/code&gt; 上：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/22/15fdf5fd00522381~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果没有 &lt;code&gt;-i&lt;/code&gt; 参数的话，这种「原地 rebase」相当于空操作，会直接结束。而在加了 &lt;code&gt;-i&lt;/code&gt; 后，就会跳到一个新的界面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/22/15fdf5fd04f46d6e~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;把 &lt;code&gt;pick&lt;/code&gt; 修改成 &lt;code&gt;edit&lt;/code&gt; 后，就可以退出编辑界面了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/22/15fdf5fd007159fa~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图的提示信息说明，&lt;code&gt;rebase&lt;/code&gt; 过程已经停在了第二个 &lt;code&gt;commit&lt;/code&gt; 的位置，那么现在你就可以去修改你想修改的内容了。&lt;/p&gt;
&lt;p&gt;修改完成之后，和上节里的方法一样，用 &lt;code&gt;commit --amend&lt;/code&gt; 来把修正应用到当前最新的 &lt;code&gt;commit&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git add 笑声
$ git commit --amend
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/22/15fdf5fd04de0d40~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;在修复完成之后，就可以用 &lt;code&gt;rebase --continue&lt;/code&gt; 来继续 &lt;code&gt;rebase&lt;/code&gt; 过程，把后面的 &lt;code&gt;commit&lt;/code&gt; 直接应用上去。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git rebase --continue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/22/15fdf5fd54455c29~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后，这次交互式 &lt;code&gt;rebase&lt;/code&gt; 的过程就完美结束了，你的那个倒数第二个写错的 &lt;code&gt;commit&lt;/code&gt; 就也被修正了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/22/15fdf5fd4e7d5257~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;实质上，交互式 &lt;code&gt;rebase&lt;/code&gt; 并不是必须应用在「原地 rebase」上来修改写错的 &lt;code&gt;commit&lt;/code&gt; ，这只不过是它最常见的用法。你同样也可以把它用在分叉的 &lt;code&gt;commit&lt;/code&gt; 上，不过这个你就可以自己去研究一下了。&lt;/p&gt;
&lt;h2&gt;丢弃倒数第二个提交 | 强大的 rebase&lt;/h2&gt;
&lt;p&gt;如果要丢弃刚提交的 &lt;code&gt;commit&lt;/code&gt;，那只需要执行 &lt;code&gt;git reset --hard HEAD^&lt;/code&gt; 即可，那如果是倒二个呢？&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;git rebase -i&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git rebase -i HEAD^^
# 上文是修改 pick 为 edit；而该操作只需要将 pick 当行删除即可
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;git rebase --onto&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;除了用交互式 &lt;code&gt;rebase&lt;/code&gt; ，你还可以用 &lt;code&gt;rebase --onto&lt;/code&gt; 来更简便地撤销提交。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rebase&lt;/code&gt; 加上 &lt;code&gt;--onto&lt;/code&gt; 选项之后，可以指定 &lt;code&gt;rebase&lt;/code&gt; 的「起点」。一般的 &lt;code&gt;rebase&lt;/code&gt;，告诉 Git 的是「我要把当前 &lt;code&gt;commit&lt;/code&gt; 以及它之前的 &lt;code&gt;commit&lt;/code&gt;s 重新提交到目标 &lt;code&gt;commit&lt;/code&gt; 上去，这其中，&lt;code&gt;rebase&lt;/code&gt; 的「起点」是自动判定的：选取当前 &lt;code&gt;commit&lt;/code&gt; 和目标 &lt;code&gt;commit&lt;/code&gt; 在历史上的交叉点作为起点。&lt;/p&gt;
&lt;p&gt;例如下面这种情况：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/22/15fe24400508e3c8~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果在这里执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git rebase &amp;#x3C;第3个commit&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么 Git 会自动选取 &lt;code&gt;3&lt;/code&gt; 和 &lt;code&gt;5&lt;/code&gt; 的历史交叉点 &lt;code&gt;2&lt;/code&gt; 作为 &lt;code&gt;rebase&lt;/code&gt; 的起点，依次将 &lt;code&gt;4&lt;/code&gt; 和 &lt;code&gt;5&lt;/code&gt; 重新提交到 &lt;code&gt;3&lt;/code&gt; 的路径上去。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;--onto&lt;/code&gt; 参数，就可以额外给 rebase 指定它的起点。例如同样以上图为例，如果我只想把 &lt;code&gt;5&lt;/code&gt; 提交到 &lt;code&gt;3&lt;/code&gt; 上，不想附带上 &lt;code&gt;4&lt;/code&gt;，那么我可以执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git rebase --onto &amp;#x3C;第3个commit&gt; &amp;#x3C;第4个commit&gt; branch1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--onto&lt;/code&gt; 参数后面有三个附加参数：目标 &lt;code&gt;commit&lt;/code&gt;、起点 &lt;code&gt;commit&lt;/code&gt;（注意：rebase 的时候会把起点排除在外）、终点 &lt;code&gt;commit&lt;/code&gt;。所以上面这行指令就会从 &lt;code&gt;4&lt;/code&gt; 往下数，拿到 &lt;code&gt;branch1&lt;/code&gt; 所指向的 &lt;code&gt;5&lt;/code&gt;，然后把 &lt;code&gt;5&lt;/code&gt; 重新提交到 &lt;code&gt;3&lt;/code&gt; 上去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/22/15fe24400d7d73d0~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git rebase --onto HEAD^^ HEAD^ branch1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这行代码的意思是：以倒数第二个 &lt;code&gt;commit&lt;/code&gt; 为起点（起点不包含在 &lt;code&gt;rebase&lt;/code&gt; 序列里哟），&lt;code&gt;branch1&lt;/code&gt; 为终点，&lt;code&gt;rebase&lt;/code&gt; 到倒数第三个 &lt;code&gt;commit&lt;/code&gt; 上。&lt;/p&gt;
&lt;p&gt;也就是这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/22/15fe243fce5804fd~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;reset 的本质 | 参数解析&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;reset&lt;/code&gt; 的三种参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;--hard&lt;/code&gt;：重置位置的同时，&lt;strong&gt;清空工作目录的所有改动&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--soft&lt;/code&gt;：重置位置的同时，&lt;strong&gt;保留工作目录和暂存区的内容&lt;/strong&gt;，并把重置 &lt;code&gt;HEAD&lt;/code&gt; 的位置所导致的新的文件差异放进暂存区。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--mixed&lt;/code&gt;（默认 &lt;code&gt;git reset&lt;/code&gt;）：重置位置的同时，&lt;strong&gt;保留工作目录的内容，并清空暂存区&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;checkout 的本质 | 除了切换分支还可签出某个提交&lt;/h2&gt;
&lt;p&gt;不过实质上，&lt;code&gt;checkout&lt;/code&gt; 并不止可以切换 &lt;code&gt;branch&lt;/code&gt;。&lt;code&gt;checkout&lt;/code&gt; 本质上的功能其实是：签出（ checkout ）指定的 &lt;code&gt;commit&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;直接上案例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git checkout HEAD^^
$ git checkout master~5
$ git checkout &amp;#x3C;SHA&gt;
$ git checkout &amp;#x3C;SHA&gt;^^^
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，如果你留心的话可能会发现，在 &lt;code&gt;git status&lt;/code&gt; 的提示语中，Git 会告诉你可以用 &lt;code&gt;checkout -- 文件名&lt;/code&gt; 的格式，通过「签出」的方式来撤销指定文件的修改：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;即撤销工作目录下的修改&lt;/strong&gt;（此时未添加到暂存区）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/11/22/15fe34cc387ba541~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Emergency！放下你手上的工作 | stash 临时存放工作目录变动&lt;/h2&gt;
&lt;p&gt;&quot;stash&quot; 这个词，和它意思比较接近的中文翻译是「藏匿」，是「把东西放在一个秘密的地方以备未来使用」的意思。在 Git 中，&lt;code&gt;stash&lt;/code&gt; 指令可以帮你把工作目录的内容全部放在你本地的一个独立的地方，它不会被提交，也不会被删除，你把东西放起来之后就可以去做你的临时工作了，做完以后再来取走，就可以继续之前手头的事了。&lt;/p&gt;
&lt;p&gt;具体说来，&lt;code&gt;stash&lt;/code&gt; 的用法很简单。当你手头有一件临时工作要做，需要把工作目录暂时清理干净，那么你可以：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git stash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就这么简单，你的工作目录的改动就被清空了，所有改动都被存了起来。&lt;/p&gt;
&lt;p&gt;然后你就可以从你当前的工作分支切到 &lt;code&gt;master&lt;/code&gt; 去给你的同事打包了……&lt;/p&gt;
&lt;p&gt;打完包，切回你的分支，然后：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git stash pop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你之前存储的东西就都回来了。很方便吧！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：没有被 track 的文件（即从来没有被 add 过的文件不会被 stash 起来，因为 Git 会忽略它们。如果想把这些文件也一起 stash，可以加上 &lt;code&gt;-u&lt;/code&gt; 参数，它是 &lt;code&gt;--include-untracked&lt;/code&gt; 的简写。就像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git stash -u
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;从暂存区撤回工作目录 | restore&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;use &lt;code&gt;git restore --staged &amp;#x3C;file&gt;&lt;/code&gt; to unstage&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果已经将文件添加到暂存区，然后想要撤销暂存区中该文件的内容（打回工作目录），则使用以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git restore --staged &amp;#x3C;file&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;tip：如果文件在工作目录下修改过但未添加到暂存区，则通过前文提到的 &lt;code&gt;git checkout -- &amp;#x3C;file&gt;&lt;/code&gt; 来撤销该修改。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;找回丢失的 branch | reflog&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;reflog&lt;/code&gt; 是 &quot;reference log&quot; 的缩写，使用它可以查看 Git 仓库中的引用的移动记录。如果不指定引用，它会显示 &lt;code&gt;HEAD&lt;/code&gt; 的移动记录。假如你误删了 &lt;code&gt;branch1&lt;/code&gt; 这个 &lt;code&gt;branch&lt;/code&gt;，那么你可以查看一下 &lt;code&gt;HEAD&lt;/code&gt; 的移动历史：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git reflog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305291053387.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;从图中可以看出，&lt;code&gt;HEAD&lt;/code&gt; 的最后一次移动行为是「从 &lt;code&gt;branch1&lt;/code&gt; 移动到 &lt;code&gt;master&lt;/code&gt;」。而在这之后，&lt;code&gt;branch1&lt;/code&gt; 就被删除了。所以它之前的那个 &lt;code&gt;commit&lt;/code&gt; 就是 &lt;code&gt;branch1&lt;/code&gt; 被删除之前的位置了，也就是第二行的 &lt;code&gt;c08de9a&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以现在就可以切换回 &lt;code&gt;c08de9a&lt;/code&gt;，然后重新创建 &lt;code&gt;branch1&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git checkout -b branch1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，你刚删除的 &lt;code&gt;branch1&lt;/code&gt; 就找回来了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：不再被引用直接或间接指向的 &lt;code&gt;commit&lt;/code&gt;s 会在一定时间后被 Git 回收，所以使用 &lt;code&gt;reflog&lt;/code&gt; 来找回删除的 &lt;code&gt;branch&lt;/code&gt; 的操作一定要及时，不然有可能会由于 &lt;code&gt;commit&lt;/code&gt; 被回收而再也找不回来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;reflog&lt;/code&gt; 默认查看 &lt;code&gt;HEAD&lt;/code&gt; 的移动历史，除此之外，也可以手动加上分支名称查看其他分支的引用移动历史，例如 &lt;code&gt;master&lt;/code&gt; 分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git reflog master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202305291056201.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;不可移动的 branch | tag&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tag&lt;/code&gt; 是一个和 &lt;code&gt;branch&lt;/code&gt; 非常相似的概念，它和 &lt;code&gt;branch&lt;/code&gt; 最大的区别是：&lt;code&gt;tag&lt;/code&gt; 不能移动。所以在很多团队中，&lt;code&gt;tag&lt;/code&gt; 被用来在关键版本处打标记用。&lt;/p&gt;
&lt;p&gt;更多关于 &lt;code&gt;tag&lt;/code&gt;：&lt;a href=&quot;https://git-scm.com/docs/git-tag&quot;&gt;git-scm.com/docs/git-ta…&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Git Flow：复杂又高效的工作流&lt;/h2&gt;
&lt;p&gt;除了前面讲到的 &quot;Feature Branching&quot;，还有一个也很流行的工作流：Git Flow。Git Flow 的机制非常完善，很适合大型团队的代码管理。不过由于它的概念比较复杂（虽然难度并不高），所以并不适合新手直接学习，而更适合在不断的自我研究中逐渐熟悉，或者在团队合作中慢慢掌握。基于这个原因，我最终也没有在这本小册里讲 Git Flow，但我推荐你自己在有空的时候了解一下它。&lt;/p&gt;</content:encoded><h:img src="/_astro/202311150150548.Ddg3RA3X.jpg"/><enclosure url="/_astro/202311150150548.Ddg3RA3X.jpg"/></item><item><title>2025.04.12 饿了么笔试题</title><link>https://coooredump.github.io/blog/recruitment/20250412-eleme</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/20250412-eleme</guid><description>饿了么 20250412 笔试解析</description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;题解链接：https://mp.weixin.qq.com/s/uWZalLLGSpPh8HxLeUtQOA&lt;/p&gt;
&lt;p&gt;测评链接：https://niumacode.com/training/68&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 小红的小苯题&lt;/h2&gt;
&lt;p&gt;小红拿到了一个正整数 x，小苯希望小红找到一个正整数 y，满足 x ⊕ y 既是 x 的因子，也是 y 的因子，你能帮帮小红吗?&lt;/p&gt;
&lt;p&gt;在这里，⊕ 表示位运算中的按位异或操作。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果存在解，请输出一个正整数 y (1 ≤ y ≤ $10^{18}$)，代表询问的答案。如果无解，请输出 -1。&lt;/p&gt;
&lt;p&gt;如果存在多个解决方案，您可以输出任意一个，系统会自动判定是否正确。注意，自测运行功能可能因此返回错误结果，请自行检查答案正确性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解释：对样例 1，由于 6⊕4 = 2，而 2 同时是 4 和 6 两个数字的因数。&lt;/p&gt;
&lt;p&gt;⚠️ 注意，本题答案不唯一。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：脑筋急转弯&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为 1 是所有数字的因子，所以直接让 &lt;code&gt;y=x^1&lt;/code&gt;，凑出 1 即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;

int main() {
    long x;
    cin &gt;&gt; x;

    if (x == 1) {
        cout &amp;#x3C;&amp;#x3C; -1 &amp;#x3C;&amp;#x3C; endl;
    } else {
        long y = x ^ 1;
        cout &amp;#x3C;&amp;#x3C; y &amp;#x3C;&amp;#x3C; endl;
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 音乐队列&lt;/h2&gt;
&lt;p&gt;小红的播放器里一共有 n 首歌待放，歌单里第 $i$ 首歌的长度为 $a_i$ 秒。&lt;/p&gt;
&lt;p&gt;小红会按顺序播放歌单里的歌，如果当前歌放完了，会自动插放下一首，两首歌曲之间没有缓冲的时间。&lt;/p&gt;
&lt;p&gt;第 $i$ 首歌曲的等待时长为 $a_1 + … + a_{i-1}$ 秒，第一首歌曲等待时间为 0 秒。&lt;/p&gt;
&lt;p&gt;小红想知道，如果她选择 $k$ 首歌移除播放队列，那么播放队列中剩下的歌曲的等待时长之和最小能达到多少?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一行输入两个整数 $n(1≤n ≤5000;0≤k≤n)$ 表示歌单里歌曲的数量、需要移除的歌曲数量&lt;/li&gt;
&lt;li&gt;第二行输入 $n$ 个整数，表示每首歌的长度，其中 $ 1 ≤ a_i ≤ 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;输出一个整数，表示插放队列中剩下的歌曲的等待时长之和的最小值。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;3 1
1 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;3 0
1 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：贪心算法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;目标转换： 最小化移除 $k$ 首歌后的总等待时间，等价于最大化移除这 k 首歌所能减少的总等待时间。&lt;/li&gt;
&lt;li&gt;计算单次移除收益： 对于原始列表中的每一首歌 $a_j$，计算如果只移除它，会使原始总等待时间减少多少。这个减少量 $R_j$ 等于 $a_j$ 原本的等待时间加上 $a_j$ 对其后面所有歌曲等待时间的贡献（即 $a_j$ 的长度乘以它后面歌曲的数量）。&lt;/li&gt;
&lt;li&gt;贪心选择： 既然要最大化总减少量，并且移除每首歌的“收益” $R_j$ 是基于原始列表计算的，可以独立看待。因此，采用贪心策略：计算所有歌曲的 $R_j$，然后选择 $R_j$ 值最大的 $k$ 首歌进行移除。&lt;/li&gt;
&lt;li&gt;计算结果：确定要移除的 $k$ 首歌的原始索引。构建一个只包含剩下 $n-k$ 首歌的新列表（保持它们的原始相对顺序）。直接计算这个新列表的总等待时间。&lt;strong&gt;核心思想是贪心，优先移除那些能最大程度降低总等待时间的歌曲&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;algorithm&gt;
#include &amp;#x3C;unordered_set&gt;
using namespace std;

// Class to store reduction value and original index
struct ReductionInfo {
    long long reduction;
    int originalIndex; // 1-based index

    ReductionInfo(long long r, int idx) : reduction(r), originalIndex(idx) {}
};

int main() {
    int n, k;
    cin &gt;&gt; n &gt;&gt; k;

    vector&amp;#x3C;int&gt; a(n);
    for (int i = 0; i &amp;#x3C; n; i++) {
        cin &gt;&gt; a[i];
    }

    if (k == n) {
        cout &amp;#x3C;&amp;#x3C; 0 &amp;#x3C;&amp;#x3C; endl; // If all songs are removed, waiting time is 0
        return 0;
    }
    if (k == 0) {
        // Calculate original waiting time directly if k=0
        long long totalWait = 0;
        long long currentPrefixSum = 0;
        for (int i = 0; i &amp;#x3C; n; i++) {
            totalWait += currentPrefixSum;
            currentPrefixSum += a[i];
        }
        cout &amp;#x3C;&amp;#x3C; totalWait &amp;#x3C;&amp;#x3C; endl;
        return 0;
    }

    // Calculate prefix sums
    vector&amp;#x3C;long long&gt; prefixSum(n + 1, 0);
    for (int i = 0; i &amp;#x3C; n; i++) {
        prefixSum[i + 1] = prefixSum[i] + a[i];
    }

    // Calculate reduction for each song
    vector&amp;#x3C;ReductionInfo&gt; reductions;
    for (int j = 1; j &amp;#x3C;= n; j++) {
        // R_j = P[j-1] + (n-j) * a[j-1]
        long long rj = prefixSum[j - 1] + (long long)(n - j) * a[j - 1];
        reductions.emplace_back(rj, j);
    }

    // Sort reductions in descending order
    sort(reductions.begin(), reductions.end(), [](const ReductionInfo&amp;#x26; r1, const ReductionInfo&amp;#x26; r2) {
        // Sort by reduction descending. If reductions are equal, order doesn&apos;t matter much,
        // but consistent sorting is good (e.g., by index ascending).
        if (r2.reduction != r1.reduction) {
            return r2.reduction &amp;#x3C; r1.reduction; // Descending reduction
        }
        return r1.originalIndex &amp;#x3C; r2.originalIndex; // Ascending index as tie-breaker
    });

    // Identify indices to remove
    unordered_set&amp;#x3C;int&gt; removedIndices;
    for (int i = 0; i &amp;#x3C; k; i++) {
        removedIndices.insert(reductions[i].originalIndex);
    }

    // Build the new list of songs (kept songs)
    vector&amp;#x3C;int&gt; keptSongs;
    for (int i = 0; i &amp;#x3C; n; i++) {
        if (removedIndices.find(i + 1) == removedIndices.end()) { // Check using 1-based index
            keptSongs.push_back(a[i]);
        }
    }

    // Calculate the total waiting time for the kept songs
    long long finalTotalWait = 0;
    long long currentPrefixSumNew = 0;
    for (int songLength : keptSongs) {
        finalTotalWait += currentPrefixSumNew;
        currentPrefixSumNew += songLength;
    }

    cout &amp;#x3C;&amp;#x3C; finalTotalWait &amp;#x3C;&amp;#x3C; endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 小红的加权三色数&lt;/h2&gt;
&lt;p&gt;小红得到一棵树，该树有 $n$ 个节点。每个节点具有三种颜色之一，分别为 R、G 与 B。每个节点还拥有一个正整数权值，用 $w_i$ 表示第 $i$ 个节点的权值。&lt;/p&gt;
&lt;p&gt;小红需要选择一个节点作为根节点。选定后，该根节点的颜色保持不变，不能被修改。对于其他非根节点，小红可以进行修改操作。每次修改操作的规则为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择一个非根节点，将以该节点为根的子树内所有节点的颜色统一修改为目标颜色，目标颜色为根节点的初始颜色
在一次修改中，该操作的代价为被修改子树内所有节点权值之和。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;小红希望通过合理选择根节点并规划修改方案，使得最终全树所有节点均为根节点的颜色，同时使总代价最小。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;名词解释&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;子树：对于树中的一个节点，其与所有后代节点构成的树称为该节点的子树。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一行输入一个整数 $(1 ≤ n ≤ 5*10^5)$，代表树的节点数量。&lt;/li&gt;
&lt;li&gt;第二行输入一个长度为 $n$ 的字符串，该字符串仅由字符 R、G、B 组成，其中第 $i$ 个字符表示第 $i$ 个节点的初始颜色。&lt;/li&gt;
&lt;li&gt;第三行输入 $n$ 个正整数 $w_1,w_2,...,w_n(1≤w_i≤10^9)$，表示各节点的权值。&lt;/li&gt;
&lt;li&gt;随后输入 $n-1$ 行，每行包会两个整数 $u$ 和 $v(1≤u,v≦n,u≠y)$，表示节点 $u$ 与节点 $v$ 之间存在一条边。保证给定的 $n$ 个节点构成一棵树，即任意两个节点之问存在且仅存在一条简单路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出一个整数，代表使全树所有节点顿色统一为根节点初始颜色所需的最小总代价。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;3
RBB
1 2 3
1 2
2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若选择节点 2 或节点 3 作为根节点，则目标颜色为 B。&lt;/li&gt;
&lt;li&gt;以节点 2 为根时，仅需修改与其相连的非根节点（如节点 1），修改操作使节点 1 变为 B，代价为 1。&lt;/li&gt;
&lt;li&gt;同理，以节点 3 为根时，方案类似，总代价亦为 1。&lt;/li&gt;
&lt;li&gt;故最小总代价为 1。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：换根 DP（reroot）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;题目要求尝试以每个节点 $r$ 为根，计算将整棵树染成 $r$ 初始颜色的最小代价，并求所有根选择中的最小代价。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;暴力不可行&lt;/strong&gt;： 对每个节点作为根都独立计算一次代价（需要 DFS 确定子树和计算代价）复杂度为 $O(N^2)$，太慢。&lt;/li&gt;
&lt;li&gt;核心技术：&lt;strong&gt;换根 DP (Rerooting DP)&lt;/strong&gt;。 &lt;strong&gt;这种技术适用于需要计算以每个节点为根时的某个树上属性的问题，能将复杂度优化到 $O(N)$&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;第一遍 DFS（向下）：任选一个节点（如 0）作为临时根。 计算每个节点的子树权重和 &lt;code&gt;subtreeSum[u]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;计算 DP 状态 &lt;code&gt;dpDown[u][C]&lt;/code&gt;：表示在以 0 为根时，假设 u 的父节点颜色已经是目标色 C，将 u 的子树完全染成颜色 C 所需的最小代价。这个计算依赖于其子节点的 &lt;code&gt;dpDown&lt;/code&gt; 值和 &lt;code&gt;subtreeSum&lt;/code&gt; 值。&lt;/li&gt;
&lt;li&gt;第二遍 DFS（&lt;strong&gt;向上/换根&lt;/strong&gt;）：计算最终 DP 状态 &lt;code&gt;finalCost[u][C]&lt;/code&gt;：表示如果以 u 为真正的根，将整棵树染成目标色 C 的最小总代价。&lt;/li&gt;
&lt;li&gt;这个计算利用 &lt;code&gt;dpDown[u][C]&lt;/code&gt;（处理 u 子树部分的代价）和其父节点 p 的 &lt;code&gt;finalCost[p][C]&lt;/code&gt; 以及 &lt;code&gt;dpDown&lt;/code&gt; 值（推导出处理树上其他部分的代价）。&lt;/li&gt;
&lt;li&gt;统计答案：遍历所有节点 r (0 到 N-1)。获取节点 r 的初始颜色 &lt;code&gt;initial_color[r]&lt;/code&gt;。该节点作为根时的最小代价为 &lt;code&gt;finalCost[r][initial_color[r]]&lt;/code&gt;。在所有 r 的这个代价中取最小值，即为最终答案。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;algorithm&gt;
#include &amp;#x3C;climits&gt;
using namespace std;

vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; adj;
vector&amp;#x3C;char&gt; colors; // 0-based node index
vector&amp;#x3C;int&gt; weights; // 0-based node index
vector&amp;#x3C;long long&gt; subtreeSum; // S[u]: sum of weights in subtree rooted at u
vector&amp;#x3C;vector&amp;#x3C;long long&gt;&gt; dpDown; // dpDown[u][color_idx]: cost for subtree u if parent imposes target color
vector&amp;#x3C;vector&amp;#x3C;long long&gt;&gt; finalCost; // finalCost[u][color_idx]: total cost if u is root and target color is color_idx
long long totalWeightSum;
const int R_IDX = 0;
const int G_IDX = 1;
const int B_IDX = 2;
int n;

// Helper to map color char to index 0, 1, 2
int colorToIndex(char c) {
    if (c == &apos;R&apos;) return R_IDX;
    if (c == &apos;G&apos;) return G_IDX;
    return B_IDX; // &apos;B&apos;
}

// DFS1: Calculate subtree sums, starting from node 0
void dfs_sum(int u, int p) {
    subtreeSum[u] = weights[u];
    for (int v : adj[u]) {
        if (v != p) {
            dfs_sum(v, u);
            subtreeSum[u] += subtreeSum[v];
        }
    }
}

void dfs_dp_down(int u, int p) {
    for (int v : adj[u]) {
        if (v != p) {
            dfs_dp_down(v, u); // Calculate for child first
            int vColorIdx = colorToIndex(colors[v]);
            long long sv = subtreeSum[v]; // Subtree sum of child v

            for (int targetColorIdx = 0; targetColorIdx &amp;#x3C; 3; targetColorIdx++) {
                if (vColorIdx == targetColorIdx) {
                    // If child color matches target, add child&apos;s dpDown cost for that target
                    dpDown[u][targetColorIdx] += dpDown[v][targetColorIdx];
                } else {
                    // If child color differs, must modify child&apos;s subtree, cost is S[v]
                    dpDown[u][targetColorIdx] += sv;
                }
            }
        }
    }
}

// Helper to get contribution of child x&apos;s subtree when target is C, from parent&apos;s view
long long getContribution(int x, int targetColorIdx) {
    int xColorIdx = colorToIndex(colors[x]);
    if (xColorIdx == targetColorIdx) {
        // If x matches target, contribution is the cost calculated below x for that target
        return dpDown[x][targetColorIdx];
    } else {
        // If x doesn&apos;t match target, the whole subtree x must be modified, cost is S[x]
        return subtreeSum[x];
    }
}

// DFS3: Rerooting to calculate finalCost[u][C] = cost if u is root and target is C
void dfs_reroot(int u, int p) {
    for (int v : adj[u]) {
        if (v != p) {
            // Calculate cost contribution from the &apos;parent&apos; branch (everything except v&apos;s subtree)
            for (int targetColorIdx = 0; targetColorIdx &amp;#x3C; 3; targetColorIdx++) {
                long long costParentBranch; // Cost of the tree excluding v&apos;s subtree, assuming targetColorIdx

                long long contributionFromV = getContribution(v, targetColorIdx);
                // Weight sum of the parent branch (everything except v&apos;s subtree)
                long long sumExcludingVSubtree = totalWeightSum - subtreeSum[v];

                int uColorIdx = colorToIndex(colors[u]);
                if (uColorIdx == targetColorIdx) {
                    // If u matches target, the cost from parent branch is its calculated cost,
                    // which is the total cost from u&apos;s perspective minus v&apos;s contribution.
                    costParentBranch = finalCost[u][targetColorIdx] - contributionFromV;
                } else {
                    // If u doesn&apos;t match target, from v&apos;s perspective, u must be modified.
                    // The cost incurred by this modification is the sum of weights in that branch.
                    costParentBranch = sumExcludingVSubtree;
                }
                // Total cost for v = (cost below v) + (cost from parent branch)
                finalCost[v][targetColorIdx] = dpDown[v][targetColorIdx] + costParentBranch;
            }
            // Recurse into child v
            dfs_reroot(v, u);
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin &gt;&gt; n;

    colors.resize(n);
    for (int i = 0; i &amp;#x3C; n; ++i) {
        cin &gt;&gt; colors[i];
    }

    weights.resize(n);
    totalWeightSum = 0;
    for (int i = 0; i &amp;#x3C; n; ++i) {
        cin &gt;&gt; weights[i];
        totalWeightSum += weights[i];
    }

    adj.resize(n);
    for (int i = 0; i &amp;#x3C; n - 1; ++i) {
        int u, v;
        cin &gt;&gt; u &gt;&gt; v;
        --u; --v; // Convert to 0-based
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    subtreeSum.resize(n);
    dpDown.assign(n, vector&amp;#x3C;long long&gt;(3, 0));
    finalCost.assign(n, vector&amp;#x3C;long long&gt;(3, 0));

    // Step 1: Calculate subtree sums (rooted arbitrarily at 0)
    dfs_sum(0, -1);

    // Step 2: Calculate dpDown (cost from below, relative to root 0)
    dfs_dp_down(0, -1);

    // Step 3: Rerooting DP
    // Initialize root&apos;s finalCost (cost if 0 is root = cost below 0)
    for (int c = 0; c &amp;#x3C; 3; ++c) {
        finalCost[0][c] = dpDown[0][c];
    }
    // Start rerooting DFS from root 0 to calculate finalCost for all nodes
    dfs_reroot(0, -1);

    // Step 4: Find minimum cost
    long long minTotalCost = LLONG_MAX;
    for (int r = 0; r &amp;#x3C; n; ++r) {
        int targetColorIdx = colorToIndex(colors[r]); // Target color is initial color of root r
        minTotalCost = min(minTotalCost, finalCost[r][targetColorIdx]);
    }

    cout &amp;#x3C;&amp;#x3C; minTotalCost &amp;#x3C;&amp;#x3C; endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/20250810-4Ol2Ne.BahNDzt7.avif"/><enclosure url="/_astro/20250810-4Ol2Ne.BahNDzt7.avif"/></item><item><title>2025.04.09</title><link>https://coooredump.github.io/blog/journal/2025-04-09</link><guid isPermaLink="true">https://coooredump.github.io/blog/journal/2025-04-09</guid><description>找暑期实习哪有不疯的</description><pubDate>Wed, 09 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;陆续一个月了，腾讯、阿里云、淘天、高德、拼多多、字节、美团... 能投的都投了。&lt;/p&gt;
&lt;p&gt;项目：要么是让介绍项目难点时讨论下，要么就是基于项目去引申（相当于场景题和设计题），时间多的话，可以自己借助 GPT 和有些技术团队的博客/沙龙去提升下项目。比如缓存一致性问题，遍地都是 Cache Aside 或者延时双删，可以调研下有没有其他成熟些的方案。&lt;/p&gt;
&lt;p&gt;八股：少部分是八股盛宴，大多数情况问的都是些常规八股，偶尔是很细的八股 + 深挖。有的面试甚至没问八股。有些面试官会在你不了解某个八股的时候，会引导着思考。比如解决某个问题，我提到了可能要用本地缓存，面试官问我了解本地缓存不，不了解，然后就会问你觉得本地缓存该如何设计，有哪些功能。&lt;/p&gt;
&lt;p&gt;手撕：简单题 + Hot 100 原题。遇到过多线程题：顺序打印 ABC、多线程计算数组和（future_task 最好会用），一次 sql 题（简单的 sql 还是最好掌握，如果面试官出了个简单的，写不出来就很尴尬了）&lt;/p&gt;
&lt;p&gt;其他类型的题：大文件，小内存，排序/&lt;a href=&quot;https://mp.weixin.qq.com/s/y1RKaocicNyz5dkZ7iimcg&quot;&gt;去重&lt;/a&gt;/统计次数等，这部分就看个人知识储备和思考能力了。&lt;/p&gt;
&lt;p&gt;建议：多刷面经，可以快速积累场景题、设计题、多线程题，而且能快速 get 到高频八股，比如 oom、cpu 使用率高、&lt;a href=&quot;https://mp.weixin.qq.com/s/Jnimeyjifl6QH3HLC7ptUg&quot;&gt;慢查询治理&lt;/a&gt;...&lt;/p&gt;
&lt;p&gt;找暑期哪有不疯的，运气也是非常重要（特指面试官）。&lt;/p&gt;
&lt;p&gt;最后祝各位早日 oc（也祝我能 oc），无需过度焦虑，才四月初，正是发力期，过段时间一堆鸽穿的。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>2025.04.05 美团笔试题</title><link>https://coooredump.github.io/blog/recruitment/20250405-meituan</link><guid isPermaLink="true">https://coooredump.github.io/blog/recruitment/20250405-meituan</guid><description>美团 20250405 笔试解析</description><pubDate>Sat, 05 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;题解链接：https://mp.weixin.qq.com/s/v5MeHD9ui8lPxRIoe0wD3Q&lt;/p&gt;
&lt;p&gt;测评链接：https://oj.niumacode.com/training/59&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 整数转变&lt;/h2&gt;
&lt;p&gt;开始有一个整数 b，你需要经过若干次操作，使 n 的值不小于 m 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作一：将 $n$ 更改为 $2 * n$ ，花费为 $w_2$&lt;/li&gt;
&lt;li&gt;操作二：将 $n$ 更改为 $3 * n$ ，花费为 $w_3$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请输出需要的最小花费。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;输入第一行一个整数 $T$，代表接下来有 $T$ 组测试数据。接下来 $T$ 行，每行有 4 个整数 $n,m,w_2,w_3$&lt;/li&gt;
&lt;li&gt;$1&amp;#x3C; T &amp;#x3C; 10000$&lt;/li&gt;
&lt;li&gt;$1&amp;#x3C; n,m &amp;#x3C; 2^{31}-1$&lt;/li&gt;
&lt;li&gt;$1&amp;#x3C; w_2,w_3 &amp;#x3C; 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;对于每组测试数据，输出一行，一个整数代表最小花费&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;4
1 6 2 3
4 32 3 4
2 1 1 2
1 2147483647 1 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;5
8
0
31
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：动态规划（记忆化搜索 + 哈希表 memo）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;记忆化搜索，&lt;strong&gt;这个题不可以使用迭代的动态规划来完成，会超时&lt;/strong&gt;，记忆化搜索可以跳过非常多不必要考虑的状态（因为使用了 &lt;code&gt;unordered_map&lt;/code&gt; 而非 &lt;code&gt;vector&lt;/code&gt;，省去了逐个遍历的时间）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;

using namespace std;

unordered_map&amp;#x3C;long long, long long&gt; memo;

long long dfs(long long i, long long m, long long w2, long long w3) {
    if(i &gt;= m)
		return 0;
    if(memo.find(i) != memo.end())
       	return memo[i];
    return memo[i] = min(dfs(i * 2, m, w2, w3) + w2, dfs(i * 3, m, w2, w3) + w3);
}

int main() {
    int T;
    cin &gt;&gt; T;
    while(T--) {
        int n, m, w2, w3;
        cin &gt;&gt; n &gt;&gt; m &gt;&gt; w2 &gt;&gt; w3;
        memo.clear();
        cout &amp;#x3C;&amp;#x3C; dfs(n, m, w2, w3) &amp;#x3C;&amp;#x3C; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 漂亮数&lt;/h2&gt;
&lt;p&gt;我们定义一个漂亮数是这样的数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;该数为正整数&lt;/li&gt;
&lt;li&gt;设该数为 x，存在一个质数 p 使得 &lt;code&gt;x mod p = 0&lt;/code&gt; 且 &lt;code&gt;p * p &gt;= x&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;给你一个正整数 n，你能否求出有多少漂亮数小于等于 n？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;输入一行一个正整数 $n(1 &amp;#x3C; n &amp;#x3C; 5 * 10^6)$&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;输出一行一个正整数，代表小于等于 n 的漂亮数的个数。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解释：10 以内除了 1 和 8 都是漂亮数&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：数论&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;筛质数&lt;/li&gt;
&lt;li&gt;基于最小质因数，递推计算每个数的最大质因数。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;algorithm&gt;
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin &gt;&gt; n;

    vector&amp;#x3C;int&gt; min_prime(n + 1, 0);  // 最小质因数数组
    vector&amp;#x3C;int&gt; primes;

    // 欧拉筛法预处理最小质因数
    for (int i = 2; i &amp;#x3C;= n; ++i) {
        if (min_prime[i] == 0) {
            min_prime[i] = i;
            primes.push_back(i);
        }
        for (size_t j = 0; j &amp;#x3C; primes.size(); ++j) {
            int p = primes[j];
            if (p &gt; min_prime[i] || i * p &gt; n) {
                break;
            }
            min_prime[i * p] = p;
        }
    }

    int count = 0;
    vector&amp;#x3C;int&gt; max_prime(n + 1, 0);  // 最大质因数数组

    for (int x = 2; x &amp;#x3C;= n; ++x) {
        if (min_prime[x] == x) {  // x是质数
            max_prime[x] = x;
            count++;
        } else {  // x是合数
            int p = min_prime[x];
            int m = x / p;
            max_prime[x] = max(p, max_prime[m]);
            if (1LL * max_prime[x] * max_prime[x] &gt;= x) {
                count++;
            }
        }
    }

    cout &amp;#x3C;&amp;#x3C; count &amp;#x3C;&amp;#x3C; endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 最长路径&lt;/h2&gt;
&lt;p&gt;游游很喜欢树，这一天他在研究树上的路径，他遇到了一个难题，现在邀请你帮助他解决该问题。&lt;/p&gt;
&lt;p&gt;在一棵树上，两个点并不一定能确定一条链，但是可以找到一条经过这两个点最长的一条链。&lt;/p&gt;
&lt;p&gt;你有一棵 n 个点的树，树上每条边都有一个权值，定义一条简单路径的长度为这条简单路径上的边权和，对于给定的两个点 x, y，你需要回答在树上经过这两个点的最长简单路径是多少。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;树上的路径&lt;/strong&gt;：从节点 u 到节点 v 的简单路径定义为从节点 u 出发，以节点 v 为终点，随意在树上走，不经过重复的点和边走出来的序列。可以证明，在树上，任意两个节点间有且仅有一条简单路径。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一行两个数 $n, m (1&amp;#x3C; n, m &amp;#x3C; 10^5)$&lt;/li&gt;
&lt;li&gt;接下来 $n - 1$ 行，每行 3 个数 $u_i, v_i, d_i (1 &amp;#x3C; u_i, v_i &amp;#x3C; n, 1 &amp;#x3C; d_i &amp;#x3C; 10^9)$，表示树的第 $i$ 条边&lt;/li&gt;
&lt;li&gt;接下来 $m$ 行，每行 2 个数 $x, y$，表示一次询问。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;输出描述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;共 m 行，每行一个整数 ans，表示你的答案&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;示例 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;4 4
1 2 1
1 3 2
1 4 1
2 1
4 3
1 4
2 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;3
3
3
2
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;代码：LCA（最近公共祖先）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;路径由三部分组成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;起点 a 到 x&lt;/strong&gt;：选一个能离 x 最远的点 a（不能往 y 方向走）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;x 到 y&lt;/strong&gt;：这是唯一的固定路径&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;y 到终点 b&lt;/strong&gt;：选一个能离 y 最远的点 b（不能往 x 方向走）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总长度就是这三段距离的和。核心是找到 a 和 b 这两个“最远端点”。&lt;/p&gt;
&lt;p&gt;为了快速计算，需要提前准备三个重要信息：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;向下最长路径（子树方向）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用深度优先搜索（DFS）从根节点出发，记录每个节点 u 的两个值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;down1[u]&lt;/code&gt;：u 向下走到子树的最长路径（比如 u→ 子节点 v → ... → 叶子）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;down2[u]&lt;/code&gt;：次长路径（必须走不同子节点）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同时记录 &lt;code&gt;down1_child[u]&lt;/code&gt; 表示哪个子节点贡献了最长路径&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;向上最长路径（父节点方向）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第二次 DFS 计算 &lt;code&gt;up[u]&lt;/code&gt;，表示从u向上走（经过父节点）的最长路径。这里要考虑两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果父节点的最长路径经过 u → 只能用父节点的次长路径&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;否则 → 可以用父节点的最长路径&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比如父节点 p 的最长路径是 p→q，而 u 是 p 的子节点但不是 q，则 &lt;code&gt;up[u] = up[p] + 边权&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LCA（最近公共祖先）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用倍增法预处理每个节点的 $2^k$ 级祖先，快速计算 x 和 y 的距离：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dist(x,y) = 到根的距离x + 到根的距离y - 2 * 到根的距离(lca(x,y))&lt;/code&gt;：这能快速得到 x 到 y 的路径长度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于每个查询 $(x,y)$，分情况处理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;x 和 y 是同一个点：最长路径要么是向下走两条分支（down1 + down2），要么是向上走再向下（up + down1）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;x 和 y 不同&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分三步计算：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;确定路径方向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;找到 x 到 y 路径上的邻居节点 nx（如果 y 在 x 的子树里，nx 是 x 的子节点；否则是 x 的父节点）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同理找到 y 的邻居 ny&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 &lt;code&gt;max_dist_x&lt;/code&gt;（不经过 nx 的最远距离）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 nx 是父节点 → 只能向下走，取 &lt;code&gt;down1[x]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 nx 是子节点 → 比较向上（&lt;code&gt;up[x]&lt;/code&gt;）和向其他子节点的路径（down1 或 down2）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同样计算 &lt;code&gt;max_dist_y&lt;/code&gt;，最终总长度是这三部分的和&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;unordered_map&gt;
#include &amp;#x3C;algorithm&gt;
#include &amp;#x3C;cmath&gt;
using namespace std;

const int MAX_N = 200005;
const int MAX_LOG_N = 20;

unordered_map&amp;#x3C;int, vector&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt;&gt; adj;
int parent[MAX_LOG_N][MAX_N];
int dist_to_parent_pow2[MAX_LOG_N][MAX_N];
int depth[MAX_N];
int dist_from_root[MAX_N];
int actual_parent[MAX_N];
int edge_weight_to_parent[MAX_N];
int down1[MAX_N], down2[MAX_N], down1_child[MAX_N], up[MAX_N];
vector&amp;#x3C;int&gt; results;

void add_edge(int u, int v, int d) {
    adj[u].emplace_back(v, d);
    adj[v].emplace_back(u, d);
}

void dfs1(int u, int p, int d, int current_dist) {
    depth[u] = d;
    dist_from_root[u] = current_dist;
    parent[0][u] = p;
    actual_parent[u] = p;

    int max1 = 0, max2 = 0, child1 = 0;

    for (auto&amp;#x26; [v, weight] : adj[u]) {
        if (v != p) {
            edge_weight_to_parent[v] = weight;
            dist_to_parent_pow2[0][v] = weight;
            
            dfs1(v, u, d + 1, current_dist + weight);
            
            int current_down_path = down1[v] + weight;
            if (current_down_path &gt;= max1) {
                max2 = max1;
                max1 = current_down_path;
                child1 = v;
            } else if (current_down_path &gt; max2) {
                max2 = current_down_path;
            }
        }
    }

    down1[u] = max1;
    down2[u] = max2;
    down1_child[u] = child1;
}

void dfs2(int u, int p) {
    int p_node = actual_parent[u];
    
    if (p_node != 0) {
        int weight_up = edge_weight_to_parent[u];
        int down_from_parent_not_via_u;
        
        if (down1_child[p_node] == u) {
            down_from_parent_not_via_u = down2[p_node];
        } else {
            down_from_parent_not_via_u = down1[p_node];
        }
        
        up[u] = weight_up + max(up[p_node], down_from_parent_not_via_u);
    }

    for (auto&amp;#x26; [v, weight] : adj[u]) {
        if (v != p) {
            dfs2(v, u);
        }
    }
}

int get_lca(int u, int v) {
    if (depth[u] &amp;#x3C; depth[v]) swap(u, v);
    
    for (int k = MAX_LOG_N - 1; k &gt;= 0; --k) {
        if (depth[u] - (1 &amp;#x3C;&amp;#x3C; k) &gt;= depth[v]) {
            u = parent[k][u];
        }
    }
    
    if (u == v) return u;
    
    for (int k = MAX_LOG_N - 1; k &gt;= 0; --k) {
        if (parent[k][u] != parent[k][v]) {
            u = parent[k][u];
            v = parent[k][v];
        }
    }
    
    return parent[0][u];
}

int get_dist(int u, int v) {
    int l = get_lca(u, v);
    return dist_from_root[u] + dist_from_root[v] - 2 * dist_from_root[l];
}

int get_ancestor(int u, int k) {
    int node = u;
    for (int i = 0; i &amp;#x3C; MAX_LOG_N; ++i) {
        if ((k &gt;&gt; i) &amp;#x26; 1) {
            node = parent[i][node];
            if (node == 0) break;
        }
    }
    return node;
}

void solve() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n, m;
    cin &gt;&gt; n &gt;&gt; m;

    for (int i = 0; i &amp;#x3C; n - 1; ++i) {
        int u, v, d;
        cin &gt;&gt; u &gt;&gt; v &gt;&gt; d;
        add_edge(u, v, d);
    }

    int MAX_LOG_N = (n - 1) == 0 ? 1 : 32 - __builtin_clz(n - 1);
    
    dfs1(1, 0, 0, 0);

    for (int k = 1; k &amp;#x3C; MAX_LOG_N; ++k) {
        for (int i = 1; i &amp;#x3C;= n; ++i) {
            parent[k][i] = parent[k-1][parent[k-1][i]];
            if (parent[k-1][i] != 0) {
                dist_to_parent_pow2[k][i] = dist_to_parent_pow2[k-1][i] + dist_to_parent_pow2[k-1][parent[k-1][i]];
            }
        }
    }

    dfs2(1, 0);

    for (int i = 0; i &amp;#x3C; m; ++i) {
        int x, y;
        cin &gt;&gt; x &gt;&gt; y;

        if (x == y) {
            int ans = max(down1[x] + down2[x], up[x] + down1[x]);
            results.push_back(ans);
            continue;
        }

        int l = get_lca(x, y);
        int dist_xy = get_dist(x, y);

        int nx = 0;
        if (l == x) {
            int target_depth = depth[x] + 1;
            int steps_up = depth[y] - target_depth;
            nx = get_ancestor(y, steps_up);
        } else {
            nx = actual_parent[x];
        }

        int ny = 0;
        if (l == y) {
            int target_depth = depth[y] + 1;
            int steps_up = depth[x] - target_depth;
            ny = get_ancestor(x, steps_up);
        } else {
            ny = actual_parent[y];
        }

        int max_dist_x;
        if (nx == actual_parent[x]) {
            max_dist_x = down1[x];
        } else {
            if (nx == down1_child[x]) {
                max_dist_x = max(up[x], down2[x]);
            } else {
                max_dist_x = max(up[x], down1[x]);
            }
        }

        int max_dist_y;
        if (ny == actual_parent[y]) {
            max_dist_y = down1[y];
        } else {
            if (ny == down1_child[y]) {
                max_dist_y = max(up[y], down2[y]);
            } else {
                max_dist_y = max(up[y], down1[y]);
            }
        }

        int ans = max_dist_x + dist_xy + max_dist_y;
        results.push_back(ans);
    }

    for (int res : results) {
        cout &amp;#x3C;&amp;#x3C; res &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    }
}

int main() {
    solve();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/20250810-eRw5cH.ErTVHpQY.png"/><enclosure url="/_astro/20250810-eRw5cH.ErTVHpQY.png"/></item><item><title>分布式系统中如何保证崩溃一致性？</title><link>https://coooredump.github.io/blog/system-architecture/how-to-ensure-crash-consistency-in-distributed-systems</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/how-to-ensure-crash-consistency-in-distributed-systems</guid><description>崩溃一致性是指系统在发生崩溃（例如服务器宕机、进程异常退出或断电）后，能够确保持久化的数据仍然处于一致的有效状态。也就是说，无论何时发生崩溃，系统存储上的数据要么保持崩溃前的完整更新，要么回退到崩溃前的稳定状态，不会出现部分更新导致的数据不完整或损坏。</description><pubDate>Wed, 02 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;面试官：你如何在系统中保证崩溃一致性？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;崩溃一致性的定义和重要性&lt;/h2&gt;
&lt;p&gt;崩溃一致性是指系统在发生崩溃（例如服务器宕机、进程异常退出或断电）后，能够确保持久化的数据仍然处于&lt;strong&gt;一致的有效状态&lt;/strong&gt;。也就是说，无论何时发生崩溃，系统存储上的数据要么保持&lt;strong&gt;崩溃前的完整更新&lt;/strong&gt;，要么回退到&lt;strong&gt;崩溃前的稳定状态&lt;/strong&gt;，不会出现部分更新导致的数据不完整或损坏。例如，在文件系统或数据库中，如果一次操作需要更新多个位置，崩溃一致性要求不能出现只更新了一部分就崩溃的情况，否则会造成数据结构的不一致。通过保证崩溃一致性，系统在重启恢复后可以正确地继续运行，数据不会因中途崩溃而处于混乱状态。这在分布式存储、数据库和后端服务中至关重要，直接关系到数据可靠性和系统健壮性。&lt;/p&gt;
&lt;h2&gt;常见的崩溃一致性保障方案&lt;/h2&gt;
&lt;p&gt;在分布式系统和后端架构设计中，有多种机制用来保证崩溃一致性。实际设计中通常根据场景组合运用这些方案：&lt;/p&gt;
&lt;h3&gt;写前日志（Write-Ahead Logging，WAL）&lt;/h3&gt;
&lt;p&gt;先将将要进行的更新记录到日志（预写日志）并持久化，再执行实际的数据更新。这样如果系统在更新过程中崩溃，重启时可以通过日志&lt;strong&gt;重放或回滚&lt;/strong&gt;确保数据一致 (存储系统的崩溃一致性问题 (Crash Consistency))。WAL 广泛应用于数据库和文件系统（如 MySQL InnoDB 的 redo log，PostgreSQL 的 WAL，Ext4 文件系统的 journal 模式）。举例来说，在数据库事务中，先写事务日志并将其 fsync 到磁盘，再更新数据页；若中途崩溃，重启时根据日志完成未完事务或撤销部分更新，保证数据一致不丢失。&lt;/p&gt;
&lt;h3&gt;两阶段提交（2PC）/ 三阶段提交（3PC）&lt;/h3&gt;
&lt;p&gt;这是分布式事务的经典协议，用于确保跨多个节点或资源的原子性提交。一致性通过一个协调者让所有参与者都准备就绪再统一提交。&lt;strong&gt;两阶段提交&lt;/strong&gt; (prepare/commit) 确保所有节点要么都提交事务，要么都回滚，中间任一节点崩溃都不会导致部分节点提交。例如，在订单系统中，订单服务和支付服务需要同时更新，各服务在准备阶段预留资源，只有当所有服务都准备成功后才正式提交扣款和订单确认，否则全部撤销。2PC 的缺点是协调者崩溃或网络分区时会阻塞（出现&lt;strong&gt;脑裂&lt;/strong&gt;风险）。&lt;strong&gt;三阶段提交&lt;/strong&gt;在 2PC 基础上增加预提交阶段（CanCommit、PreCommit、DoCommit），减少单点故障导致的阻塞，但通信开销更大，且仍要求可靠网络条件。实际中原生 3PC 较少直接使用，更常用改进的一致性协议或结合其他机制来避免单点问题。&lt;/p&gt;
&lt;h3&gt;分布式一致性协议（如 Paxos / Raft 等）&lt;/h3&gt;
&lt;p&gt;这些是一致性算法，用于在&lt;strong&gt;多副本场景&lt;/strong&gt;下保证数据副本之间的状态一致，进而保障崩溃后的系统一致性。Paxos 和 Raft 通过多数派投票&lt;strong&gt;达成共识&lt;/strong&gt;，确保一旦某个操作被多数节点提交，整个集群最终都应用该操作，即使部分节点崩溃也不影响全局一致结果。它们通常用于实现复制日志和领导者选举。例如，Raft 协议通过由 Leader 将日志条目复制到 Follower，并要求超过半数节点写入确认后再认为提交成功，这样即使一个节点崩溃，已提交日志仍然存在于其他节点中，系统可以选举新 Leader 继续提供服务。很多分布式 KV 存储（如 Etcd、Consul）和分布式数据库利用 Raft/Paxos 保证在崩溃或网络异常情况下数据不会不一致。此外，基于这些共识协议可以构建分布式事务：例如 Google Spanner 将 2PC 与 Paxos 结合，在每个分片组内用 Paxos 保证副本一致性，并用 2PC 跨分片提交全局事务，从而同时实现强一致和高可用的事务处理。&lt;/p&gt;
&lt;h3&gt;本地持久化与 fsync&lt;/h3&gt;
&lt;p&gt;无论采用哪种高级协议，底层的&lt;strong&gt;持久化正确性&lt;/strong&gt;都是基础。为了保证崩溃时数据已真正写入稳定存储，需要使用&lt;code&gt;fsync&lt;/code&gt;或类似手段将数据刷盘。操作系统和硬件常有缓存，如果只写入内存缓冲区而不刷新到磁盘，崩溃会导致已写入的数据丢失。通过在关键步骤后调用 fsync 确保数据写入磁盘，可以提供&lt;strong&gt;持久化的崩溃一致性&lt;/strong&gt;。例如，消息队列在写入消息日志后会调用 fsync（或根据配置定期刷盘）才确认消息“已持久化”，这样 Broker 宕机重启后，日志里的消息仍然可用，不会丢失或损坏。需要注意频繁 fsync 会影响性能，因此实际系统常结合&lt;strong&gt;批量写入&lt;/strong&gt;或后台刷盘策略，在保证一定一致性的前提下平衡性能（例如 MySQL 的&lt;code&gt;innodb_flush_log_at_trx_commit&lt;/code&gt;参数决定每次事务提交是否立即 fsync 日志）。&lt;/p&gt;
&lt;h3&gt;幂等性设计与重试机制&lt;/h3&gt;
&lt;p&gt;在分布式系统中，&lt;strong&gt;失败重试&lt;/strong&gt;是常见现象（例如网络超时后客户端重发请求，或者服务重启后重新处理消息）。为了保证在崩溃和重试场景下状态不乱，要求关键操作具有&lt;strong&gt;幂等性&lt;/strong&gt;——同一操作执行一次和多次的效果相同，不会因重复执行产生副作用差异。例如，支付接口需要设计为幂等，以避免由于请求超时重复扣款；又如订单创建接口应防止创建重复订单。这通常通过在业务层引入唯一请求 ID 或事务 ID 来实现，每次操作前先检查是否已处理过该 ID，已处理则跳过，未处理才执行并记录结果。幂等设计配合重试机制，使系统在部分操作失败或崩溃恢复后可以安全地&lt;strong&gt;重放操作&lt;/strong&gt;，达到“至少一次”投递但效果如“只执行一次”的一致性结果。例如，消息队列消费者在处理消息时，如果进程突然崩溃，下次重启后消息会被重新投递，此时消费者处理逻辑如果具备幂等（例如根据消息唯一键检查数据库中是否已应用），就能保证不会因重复消费导致数据错误。&lt;/p&gt;
&lt;h2&gt;实际项目经验与应用案例&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;面试中展示崩溃一致性的理解，最好结合自身经历或熟悉的系统案例，说明如何运用了上述机制：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;分布式 KV 存储系统&lt;/strong&gt;：举例来说，在设计分布式 Key-Value 存储时，会使用 &lt;strong&gt;WAL 日志 + 多副本复制&lt;/strong&gt;来保证崩溃一致性。每次写操作先写入本地 WAL 并持久化，然后通过一致性协议（如 Raft）将该操作复制到其他节点。在我参与的一个存储引擎项目中，我们采用 Raft 保证各副本日志一致，同时每台节点本地使用 WAL 和定期快照。一次写入只有当多数节点写入日志成功并且主节点的日志 fsync 完成后才确认成功返回。这样，即便某个节点宕机，其恢复时可以通过重放本地 WAL 和从其他副本同步缺失日志将数据恢复到崩溃前的&lt;strong&gt;一致状态&lt;/strong&gt;。这种方案在实践中确保了无单点故障的数据可靠性——如 etcd、ZooKeeper 也使用类似思路，保证配置数据在节点崩溃后依然一致。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;订单和支付事务系统&lt;/strong&gt;：在电商订单系统中，经常需要保证诸如“扣库存”和“扣款”这两个不同服务的操作要么都成功要么都不执行。我的经验是可以采用&lt;strong&gt;分布式事务&lt;/strong&gt;或&lt;strong&gt;事务补偿机制&lt;/strong&gt;来解决。例如，我们尝试过使用两阶段提交：订单服务作为协调者，通知库存服务和支付服务预留资源（准备阶段），如果都成功则发出提交指令，各自将最终状态持久化；如果中途任一失败则发出回滚指令撤销之前的操作。这保证了服务崩溃或网络异常时不会出现“扣了款但订单未创建”这类不一致结果。另外一种实践是使用 &lt;strong&gt;Saga（补偿事务）模式&lt;/strong&gt;配合幂等设计，各服务先本地完成操作，如果后续步骤失败则通过调用补偿动作（如退款、加回库存）来最终达成一致。无论哪种方案，都需要在&lt;strong&gt;实际落地&lt;/strong&gt;时注意持久化（写数据库事务日志或业务状态日志）以及操作的幂等性。例如，我们会为每个订单生成唯一事务 ID，在整个事务链路中传递，崩溃恢复后根据事务日志判断需要补偿还是继续未完成的步骤。通过这样的设计，订单系统在面临部分服务宕机重启时，仍然可以保证数据的最终一致性和正确性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;消息队列系统&lt;/strong&gt;：以分布式消息队列为例（如 Kafka 或 RabbitMQ），崩溃一致性体现在&lt;strong&gt;不丢消息且不重复乱序&lt;/strong&gt;。实际项目中，我部署过的 Kafka 集群采用&lt;strong&gt;持久化日志+副本同步&lt;/strong&gt;：生产者发送的消息先写入领导者节点的磁盘日志（Kafka 提供多种 acks 级别确保消息写入持久化），Leader 将消息复制给 Follower 节点，等至少一个 Follower 也写入成功后再确认给生产者。每条消息有偏移量，消费者处理时按偏移顺序提交位移。若 Broker 崩溃，Zookeeper/Raft 协调下会选出新的 Leader 继续未完成的日志追加，消费者可以从上次提交的偏移继续消费，保证顺序和一致性。而对于消费者重复消费的情况，我们在消费端通过&lt;strong&gt;幂等处理&lt;/strong&gt;解决：例如消息携带唯一 ID，消费逻辑更新数据库前先检查记录是否已处理该 ID，以避免因消费者崩溃重启后重复消费导致的数据重复。通过这些措施，消息队列系统实现了&lt;strong&gt;崩溃后恢复不丢消息&lt;/strong&gt;，并使得任何重复消息的影响可控，从而在分布式部署下依然保持数据一致可靠。&lt;/p&gt;
&lt;h2&gt;面试回答思路和结构化表达建议&lt;/h2&gt;
&lt;p&gt;在技术面试中回答“如何保证崩溃一致性”这类问题时，可以按照有条理的结构来表达，确保面试官能清晰理解你的思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;问题背景简介&lt;/strong&gt;：首先简要说明什么是崩溃一致性，以及在什么背景下需要考虑它。可以点出崩溃场景（宕机、断电等）会导致部分写入丢失或数据结构不完整，从而引出需要机制保证一致性的重要性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可选方案对比&lt;/strong&gt;：接下来概述解决崩溃一致性的常见方案。可以按照从单机到分布式逐步展开，例如先介绍本地级别的方案（&lt;code&gt;WAL&lt;/code&gt; 日志、&lt;code&gt;Copy-on-Write&lt;/code&gt;、&lt;code&gt;fsync&lt;/code&gt; 刷盘等），再说明分布式场景的方案（&lt;code&gt;2PC&lt;/code&gt;/&lt;code&gt;3PC&lt;/code&gt; 分布式提交、&lt;code&gt;Paxos&lt;/code&gt;/&lt;code&gt;Raft&lt;/code&gt; 一致性算法、幂等重试等）
&lt;ul&gt;
&lt;li&gt;WAL 适合单节点多步骤原子更新&lt;/li&gt;
&lt;li&gt;2PC 适合跨服务事务但有阻塞问题&lt;/li&gt;
&lt;li&gt;Raft 适合多副本一致&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实际方案选择及理由&lt;/strong&gt;：然后重点描述你在实际项目中用到了哪些机制来保证崩溃一致性，以及为什么选择这些方案。&lt;strong&gt;结合具体案例&lt;/strong&gt;谈更有说服力，比如说明“在某项目中我们遇到 XX 一致性要求，选择了 YY 方案，因为…”。阐述方案如何落地实施（比如如何实现日志持久化、如何协调多服务提交）。这一部分体现你将理论用于实践的能力，例如提到性能考量（为什么选 WAL 而不是每次直接写入，以及 WAL 带来的性能提升与一致性保障）、可靠性需求（为何采用 Raft 保证副本一致）等决策因素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;遇到的问题与优化&lt;/strong&gt;：最后补充说明在实现崩溃一致性过程中遇到的挑战以及采取的优化措施。这显示你对细节和系统影响有深入理解。比如，你可以提到&lt;strong&gt;性能瓶颈和解决&lt;/strong&gt;：“由于频繁 fsync 影响吞吐，我们采用了批量提交来优化磁盘 IO”。或者&lt;strong&gt;复杂性问题&lt;/strong&gt;：“实现 2PC 时遇到了超时和协调难题，通过引入超时重试和事务日志监控来保证一致性”。再比如&lt;strong&gt;边缘情况处理&lt;/strong&gt;：“考虑到网络分区导致的脑裂，我们引入了仲裁机制/心跳检测来处理”。通过讲述如何发现问题、解决问题，表现出你对崩溃一致性机制有实战经验和思考。结束时可以强调经过这些努力，系统成功保证了在各种异常情况下数据的一致可靠。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;推荐参考文献&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;《数据密集型应用系统设计（Designing Data-Intensive Applications）》&lt;/li&gt;
&lt;li&gt;《操作系统原理与实现（Operating System Principle and Implementation）》&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/202504021525623.DuoFilCh.png"/><enclosure url="/_astro/202504021525623.DuoFilCh.png"/></item><item><title>Linux Kernel I/O</title><link>https://coooredump.github.io/blog/system-architecture/linux-kernel-io-path</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/linux-kernel-io-path</guid><description>针对腾讯 CSIG 一面的问题做一个总结，包括读写操作在整个内核中的 I/O 请求链路，页缓存、零拷贝技术，以及用户态 I/O 和系统调用的优化。</description><pubDate>Sun, 30 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前置知识｜页缓存 &amp;#x26; 零拷贝&lt;/h2&gt;
&lt;h4&gt;零拷贝技术（Zero-Copy）&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;概念&lt;/strong&gt;：在传统的数据传输过程中（比如读取文件发送给网络），数据通常会在&lt;strong&gt;内核空间和用户空间之间多次拷贝&lt;/strong&gt;。零拷贝的目标是&lt;strong&gt;尽量减少或避免数据在内核空间与用户空间之间的拷贝操作&lt;/strong&gt;，从而提升性能。&lt;/p&gt;
&lt;p&gt;如果服务端要提供文件传输的功能，我们能想到的最简单的方式是：将磁盘上的文件读取出来，然后通过网络协议发送给客户端。&lt;/p&gt;
&lt;p&gt;传统 I/O 的工作方式是，数据读取和写入是从用户空间到内核空间来回复制，而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。&lt;/p&gt;
&lt;p&gt;代码通常如下，一般会需要两个系统调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;read(file, tmp_buf, len);
write(socket, tmp_buf, len);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503300027722.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;首先，期间共&lt;strong&gt;发生了 4 次用户态与内核态的上下文切换&lt;/strong&gt;，因为发生了两次系统调用，一次是 &lt;code&gt;read()&lt;/code&gt; ，一次是 &lt;code&gt;write()&lt;/code&gt;，每次系统调用都得先从用户态切换到内核态，等内核完成任务后，再从内核态切换回用户态。&lt;/p&gt;
&lt;p&gt;上下文切换到成本并不小，一次切换需要耗时几十纳秒到几微秒，虽然时间看上去很短，但是在高并发的场景下，这类时间容易被累积和放大，从而影响系统的性能。&lt;/p&gt;
&lt;p&gt;其次，还&lt;strong&gt;发生了 4 次数据拷贝&lt;/strong&gt;，其中两次是 DMA 的拷贝，另外两次则是通过 CPU 拷贝的，下面说一下这个过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;第一次拷贝&lt;/code&gt;，把磁盘上的数据拷贝到操作系统内核的缓冲区里，这个拷贝的过程是通过 DMA 搬运的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;第二次拷贝&lt;/code&gt;，把内核缓冲区的数据拷贝到用户的缓冲区里，于是我们应用程序就可以使用这部分数据了，这个拷贝到过程是由 CPU 完成的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;第三次拷贝&lt;/code&gt;，把刚才拷贝到用户的缓冲区里的数据，再拷贝到内核的 socket 的缓冲区里，这个过程依然还是由 CPU 搬运的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;第四次拷贝&lt;/code&gt;，把内核的 socket 缓冲区里的数据，拷贝到网卡的缓冲区里，这个过程又是由 DMA 搬运的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种简单又传统的文件传输方式，存在冗余的上文切换和数据拷贝，在高并发系统里是非常糟糕的，多了很多不必要的开销，会严重影响系统性能。&lt;/p&gt;
&lt;p&gt;所以，&lt;strong&gt;要想提高文件传输的性能，就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;从 Linux 2.1 版本开始，Linux 引入了 &lt;code&gt;sendfile&lt;/code&gt; 来简化操作。文件通过 &lt;code&gt;sendfile()&lt;/code&gt; 直接发送到 socket，不经过用户空间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;sys/socket.h&gt;
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的前两个参数分别是目的端和源端的文件描述符，后面两个参数是源端的偏移量和复制数据的长度，返回值是实际复制数据的长度。&lt;/p&gt;
&lt;p&gt;首先，它可以替代前面的 &lt;code&gt;read()&lt;/code&gt; 和 &lt;code&gt;write()&lt;/code&gt; 这两个系统调用，这样就可以减少一次系统调用，也就减少了 2 次上下文切换的开销。其次，该系统调用，可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里，不再拷贝到用户态，这样就只有 2 次上下文切换，和 3 次数据拷贝。如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503300031322.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h4&gt;多路复用技术（I/O Multiplexing）&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;概念&lt;/strong&gt;：允许单个线程同时监听多个 I/O 事件（如 &lt;code&gt;socket&lt;/code&gt; 连接），只在有事件发生时才进行处理，&lt;strong&gt;避免阻塞等待每个 I/O&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常见接口&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;select&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;poll&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;epoll&lt;/code&gt;（Linux 高效实现）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;用途&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;高并发服务器（如 Nginx、Redis）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络编程中高效的 I/O 模型&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;优势&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;少量线程处理大量连接&lt;/li&gt;
&lt;li&gt;减少线程切换开销&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;页缓存（Page Cache）技术&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;位于「程序内存分布」中的内核空间&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;概念&lt;/strong&gt;：&lt;strong&gt;操作系统将磁盘中的数据缓存在内存中&lt;/strong&gt;，以加快文件读写速度。缓存的单位是“页”（通常是 4KB）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加快文件读取（命中缓存时无需访问磁盘）&lt;/li&gt;
&lt;li&gt;写文件时先写到缓存，再异步刷到磁盘（提高写入性能）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;相关命令&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sync&lt;/code&gt;：强制把缓存写入磁盘&lt;/li&gt;
&lt;li&gt;&lt;code&gt;drop_caches&lt;/code&gt;：清除缓存，用于测试或释放内存&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Linux 内核 I/O 链路一览图&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503290106690.png&quot; alt=&quot;Linux 的I/O栈&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Linux I/O 存储栈下的读写流程&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503292030540.png&quot; alt=&quot;image-20250329203049399&quot;&gt;&lt;/p&gt;
&lt;p&gt;应用程序通过系统调用访问文件（无论是块设备文件，还是各种文件系统中的文件）。可以通过 &lt;code&gt;open&lt;/code&gt; 系统调用，也可以通过 &lt;code&gt;memory map&lt;/code&gt; 的方式调用来打开文件。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;mmap 与 read/write 的区别可以参考文章：&lt;a href=&quot;https://wu-yikun.github.io/post/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/mmap-vs-read-write/&quot;&gt;mmap 与 read/write 对比&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Linux 内核收到系统调用的软中断，通过参数检查后，会调用虚拟文件系统（Virtual File System，VFS），虚拟文件系统会根据信息把相应的处理交给具体的文件系统，如 ext2/3/4 等文件系统，接着相应的文件 I/O 命令会转化成 &lt;code&gt;bio&lt;/code&gt; 命令进入通用块设备层，把针对文件的基于 offset 的读/写转化成基于逻辑区块地址（Logical Block Address，LBA）的读/写，并最终翻译成每个设备对应的可识别的地址，通过 Linux 的设备驱动对物理设备，如硬盘驱动器（Harddisk Drive，HDD）或固态硬盘进行相关的读/写。&lt;/p&gt;
&lt;p&gt;用户态文件系统的管理。Linux 文件系统的实现都是在内核进行的，但是用户态也有一些管理机制可以对块设备文件进行相应的管理。例如，使用 &lt;code&gt;parted&lt;/code&gt; 命令进行分区管理，使用 &lt;code&gt;mkfs&lt;/code&gt; 工具进行文件系统的管理，使用逻辑卷管理器（Logical Volume Manager，LVM）命令把一个或多个磁盘的分区进行逻辑上的集合，然后对磁盘上的空间进行动态管理。&lt;/p&gt;
&lt;p&gt;当然在用户态也有一些用户态文件系统的实现，但是一般这样的系统性能不是太高，因为文件系统最终是建立在实际的物理存储设备上的，且这些物理设备的驱动是在内核态实现的。那么即使文件系统放在用户态，I/O 的读和写也还是需要放到内核态去完成的。除非相应的设备驱动也被放到用户态，形成一套完整的用户态 I/O 栈的解决方案，就可以降低 I/O 栈的深度。另外采用一些无锁化的并行机制，就可以提高 I/O 的性能。例如，由英特尔开源的 SPDK（Storage Performance Development Kit）软件库，就可以利用用户态的 NVMe SSD（Non-Volatile Memory express）驱动，从而加速那些使用 NVMe SSD 的应用，如 iSCSI Target 或 NVMe-oF Target 等。&lt;/p&gt;
&lt;p&gt;linux IO 存储栈分为 7 层:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VFS 虚拟文件层: 在各个具体的文件系统上建立一个抽象层，屏蔽不同文件系统的差异。&lt;/li&gt;
&lt;li&gt;PageCache 层: 为了缓解内核与磁盘速度的巨大差异。&lt;/li&gt;
&lt;li&gt;映射层 Mapping Layer: 内核必须从块设备上读取数据，Mapping layer 要确定在物理设备上的位置。&lt;/li&gt;
&lt;li&gt;通用块设备层: 通用块层处理来自系统其他组件发出的块设备请求，包含了块设备操作的一些通用函数和数据结构。&lt;/li&gt;
&lt;li&gt;I/O 调度层： IO 调度层主要是为了减少磁盘 IO 的次数，增大磁盘整体的吞吐量，队列中多个 bio 进行排序和合并。&lt;/li&gt;
&lt;li&gt;块设备驱动层: 每一类设备都有其驱动程序，负责设备的读写。&lt;/li&gt;
&lt;li&gt;物理设备层: 物理设备层有 HDD、SATA SSD、NVMe SSD 等物理设备。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PageCache 层 —— 两种策略&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;write back: 写入 PageCache 便返回，不等数据落盘。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;write through: 同步等待数据落盘。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;程序的内存分布&lt;/strong&gt;，其中包括内核空间（Page Cache 在内存中）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503290041746.png&quot; alt=&quot;image-20240725233029022&quot;&gt;&lt;/p&gt;
&lt;h3&gt;读流程&lt;/h3&gt;
&lt;p&gt;下面以一次文件读取操作为例，完整详细描述一次 I/O 请求处理链路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚拟文件系统层（VFS，也是 Linux 一切皆文件的底层原因）&lt;/li&gt;
&lt;li&gt;文件系统（Ext2/3/4、NFS、Btrfs、xfs）&lt;/li&gt;
&lt;li&gt;通用块设备层（bio、request）&lt;/li&gt;
&lt;li&gt;I/O 调度器（CFQ、Deadline、noop、BFQ）&lt;/li&gt;
&lt;li&gt;设备驱动（块设备/字符设备）&lt;/li&gt;
&lt;li&gt;设备控制器（如 NVMe/SCSI 控制器）&lt;/li&gt;
&lt;li&gt;中断处理（IRQ 处理流程）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1️⃣ 用户进程调用标准库函数&lt;/h4&gt;
&lt;p&gt;用户进程发起&lt;strong&gt;系统调用&lt;/strong&gt; &lt;code&gt;read(fd, buf, count)&lt;/code&gt;，系统陷入内核态。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// glibc 中封装的 read() 最终触发 syscall
ssize_t read(int fd, void *buf, size_t count);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内核获取调用参数，内核深入调用 &lt;code&gt;sys_read&lt;/code&gt;，检查文件描述符的有效性并获取内核文件结构体，通过软中断（x86 上是 &lt;code&gt;syscall&lt;/code&gt; 指令）进入内核态：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 内核入口点（x86_64 架构）
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
    return ksys_read(fd, buf, count);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2️⃣ 虚拟文件系统 VFS&lt;/h4&gt;
&lt;p&gt;内核通过文件描述符定位 &lt;code&gt;struct file *&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
    struct fd f = fdget_pos(fd);          // 从 fd 表中找到 struct file
    ...
    return vfs_read(f.file, buf, count, &amp;#x26;f.file-&gt;f_pos);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;VFS 提供统一的文件接口，不管是 ext4、xfs 还是设备文件，统一调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    if (file-&gt;f_op-&gt;read)
        return file-&gt;f_op-&gt;read(file, buf, count, pos);  // legacy
    else if (file-&gt;f_op-&gt;read_iter)
        return call_read_iter(file, buf, count, pos);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3️⃣ 文件系统层&lt;/h4&gt;
&lt;p&gt;假设文件位于 ext4 文件系统，对应操作函数为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// ext4_file_operations
.read_iter = ext4_file_read_iter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这最终会调用 &lt;code&gt;generic_file_read_iter()&lt;/code&gt;，进入&lt;strong&gt;页缓存&lt;/strong&gt;机制（page cache）。&lt;/p&gt;
&lt;p&gt;页缓存流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在页缓存中查找页是否命中&lt;/li&gt;
&lt;li&gt;命中则拷贝回用户态（零拷贝优化）&lt;/li&gt;
&lt;li&gt;未命中则触发 page cache miss → &lt;strong&gt;发起读取请求到块设备&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// mm/filemap.c
filemap_get_pages() → 调用 readpage(s) → 提交 bio 到块层
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4️⃣ 通用块设备层&lt;/h4&gt;
&lt;p&gt;页缓存 page miss 会创建对应的 &lt;code&gt;bio&lt;/code&gt; 结构体表示一次 I/O 请求并提交 &lt;code&gt;submit_bio()&lt;/code&gt;，然后对 bio 进行进一步封装，形成更底层的 &lt;code&gt;request&lt;/code&gt; 结构传入 I/O 调度器队列。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503300058333.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;✅ request 是 I/O 调度的最小单位，多个 bio 访问存储器件上相邻的区域数据并且是同种类型的（读/写），则会被合并到一个 request 中，所以一个 request 可能包含多个 bio。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;bio structure&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503300056158.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;submit_io&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503300054947.png&quot; alt=&quot;submit_bio&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;submit_bio(bio); // fs 层构造 bio，提交到底层设备
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;块层主要组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;bio&lt;/strong&gt;：描述一次 I/O 操作（起始扇区、长度、数据缓冲区等）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;request_queue&lt;/strong&gt;：设备对应的请求队列&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I/O scheduler&lt;/strong&gt;：调度多个 bio 的先后顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5️⃣ I/O 调度器&lt;/h4&gt;
&lt;p&gt;I/O 调度器决定请求的先后顺序，进行排序和合并优化，以此优化磁盘/硬盘访问。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;blk_mq_sched_insert_request()
blk_mq_run_hw_queue()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见调度器如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CFQ（完全公平队列）&lt;/li&gt;
&lt;li&gt;Deadline&lt;/li&gt;
&lt;li&gt;noop&lt;/li&gt;
&lt;li&gt;BFQ（新版本）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;6️⃣ 块设备驱动&lt;/h4&gt;
&lt;p&gt;调度器处理完后，接着将 request 分发给具体驱动，调用对应的驱动操作函数（例如 NVMe 驱动）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SATA 磁盘驱动 (&lt;code&gt;ahci.ko&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;NVMe 驱动 (&lt;code&gt;nvme.ko&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// nvme driver: drivers/nvme/host/nvme-core.c
nvme_queue_rq() → 调用控制器提交命令
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;驱动程序将 &lt;code&gt;request&lt;/code&gt; 翻译成硬件控制器能理解的指令集&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于磁盘：ATA/SCSI 命令&lt;/li&gt;
&lt;li&gt;对于 NVMe：NVMe 命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;驱动程序将命令写入设备控制器寄存器，具体一些是控制器的 &lt;strong&gt;Submission Queue&lt;/strong&gt;，并触发 DMA（直接内存访问）数据传输。&lt;/p&gt;
&lt;h4&gt;7️⃣ 设备控制器/硬件接口层&lt;/h4&gt;
&lt;p&gt;硬件控制器接收到 SQ 中的命令，解析命令 &lt;code&gt;read&lt;/code&gt; 并读取数据到 DMA 缓存，通过 DMA 把数据传输到内存中的内核缓冲区（Page Cache），DMA 数据传输完成后，写入 &lt;strong&gt;Completion Queue&lt;/strong&gt;，&lt;strong&gt;硬件设备发起硬件中断通知 CPU 操作完成&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;8️⃣ 中断处理流程&lt;/h4&gt;
&lt;p&gt;CPU 响应硬件中断，内核执行相应中断处理程序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确认硬件完成状态；&lt;/li&gt;
&lt;li&gt;根据 CQ 的完成项来找到并标记 &lt;code&gt;request&lt;/code&gt;/&lt;code&gt;bio&lt;/code&gt; 已经完成；&lt;/li&gt;
&lt;li&gt;唤醒被该 &lt;code&gt;read&lt;/code&gt; 阻塞的上层等待进程（在 &lt;code&gt;wait_on_page_locked()&lt;/code&gt; 阻塞）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;9️⃣ bio 回传用户空间&lt;/h4&gt;
&lt;p&gt;唤醒进程后，内核从内核缓冲区（Page Cache）将数据拷贝到用户空间缓冲区。&lt;/p&gt;
&lt;p&gt;系统调用返回，用户进程继续执行。&lt;/p&gt;
&lt;h3&gt;写流程&lt;/h3&gt;
&lt;p&gt;write()—&gt;sys_write()—&gt;vfs_write()—&gt;通用块层—&gt;IO 调度层—&gt;块设备驱动层—&gt;块设备&lt;/p&gt;
&lt;h3&gt;mmap 与 read/write&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;顺便复习下 mmap 与 read/write 的区别：&lt;a href=&quot;https://wu-yikun.github.io/post/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/mmap-vs-read-write/&quot;&gt;mmap 与 read/write 对比&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;传统的 file I/O 中 read 系统调用首先从磁盘拷贝数据到 kernel，然后再把数据从 kernel 拷贝到用户定义的 buffer 中。&lt;/p&gt;
&lt;p&gt;而 mmap 直接由内核操刀，mmap 返回的指针指向映射内存的起始位置，然后可以像操作内存一样操作文件，而且如果是用 read/write 将 buffer 写回 page cache 意味着整个文件都要与磁盘同步（即使这个文件只有个别 page 被修改了），而 mmap 的同步粒度是 page，可以根据 page 数据结构的 dirty 位来决定是否需要与 disk 同步。这是 mmap 比 read 高效的主要原因。对于那种频繁读写同一个文件的程序更是如此。&lt;/p&gt;
&lt;h2&gt;面试题：如何不使用库函数完成对底层设备的读写？&lt;/h2&gt;
&lt;h3&gt;用户态 I/O&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;参考《操作系统 原理与实现》13.4.2 小节 —— 陈海波&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;无论是基于系统调用还是 I/O 库接口，应用程序默认操作的设备对象都是操作系统提供的&lt;strong&gt;逻辑设备&lt;/strong&gt;。用户态和内核态之间需要大量的拷贝，造成性能下降，有没有办法不使用库函数来达到直接对&lt;strong&gt;底层物理设备&lt;/strong&gt;的读写呢？&lt;/p&gt;
&lt;p&gt;以网络 I/O 为例，一种直观的思路是允许防火墙软件直接操作网卡的 DMA 缓冲区（应用程序直接操作设备控制器的 DMA 缓冲区），为了实现这一目标，Intel 联合其他网卡制造商共同开发了一套高性能的用户态网络 I/O 框架 —— 数据平面开发套件（DPDK）。&lt;/p&gt;
&lt;p&gt;DPDK 在设计上采用&lt;strong&gt;旁路内核&lt;/strong&gt;的设计，即网络包的收发处理基本不需要 Linux 内核的参与，DPDK 的设计思路如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;其底层原理同样适用于其他物理设备&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;用户空间驱动&lt;/h4&gt;
&lt;p&gt;为了能在用户态同网卡设备进行交互，DPDK 需要在用户态直接执行网卡驱动代码。做法就是将设备寄存器直接映射到应用自身的进程地址空间中，进而让 DPDK 的用户态驱动通过 MMIO 操作设备。&lt;/p&gt;
&lt;p&gt;正如操作系统为应用程序的开发提供统一设备文件系统的和 I/O 使用接口一样，&lt;strong&gt;Linux 提供了用户态驱动开发框架，即 UIO (Userspace I/O)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Linux 将 UIO 设备抽象为路径为 &lt;code&gt;/dev/uio&lt;/code&gt; [x] 的设备文件。应用程序通过打开 UIO 设备文件获取设备的 I/O 空间和中断信息，同时自行决定如何操作和响应设备。&lt;/p&gt;
&lt;p&gt;注意：UIO 用户驱动通常不使用 &lt;code&gt;write&lt;/code&gt; 接口，而是从 &lt;em&gt;uio_driver.h&lt;/em&gt; 中已经经过 &lt;code&gt;mmap&lt;/code&gt; 处理的内存区间直接与设备交换数据。&lt;/p&gt;
&lt;p&gt;更多详细内容可以查看 Linux Kernel 中的 &lt;code&gt;include/linux/uio_driver.h&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;系统调用的优化&lt;/h2&gt;
&lt;p&gt;系统调用作为应用程序调用操作系统的入口，其性能也非常重要。然而不同于传统的函数调用，系统调用的过程复用了异常机制，因此不可避免地需要执行特权级切换、上下文保存等操作，导致其时延比普通函数调用高1～2 个数量级。对于需要频繁进行系统调用的应用来说，这是很大的性能开销。&lt;/p&gt;
&lt;p&gt;Q：那么要怎样绕过费时的异常处理机制来实现系统调用呢？&lt;/p&gt;
&lt;p&gt;A：可以通过在用户态和内核态之间共享一小块内存的方式，在应用与内核之间创建一条新的通道。&lt;/p&gt;
&lt;h3&gt;1️⃣ 方法 1：共享内核只读数据给应用&lt;/h3&gt;
&lt;p&gt;内核将一部分数据通过只读的形式共享给应用，允许应用直接读取。&lt;/p&gt;
&lt;p&gt;这种方法的缺点在于：如果系统调用需要修改内核中的变量，或者在运行过程中需要读取更多内核数据，该方法就不适用了。&lt;/p&gt;
&lt;h3&gt;2️⃣ 方法 2：允许应用以 “向内存页写入请求” 发起系统调用&lt;/h3&gt;
&lt;p&gt;第二种方法就是允许应用（用户态）以 “向内存页写入请求” 的方式发起系统调用，并通过&lt;strong&gt;轮询&lt;/strong&gt;来等待系统调用完成；内核（内核态）同样通过轮询来等待用户的请求，然后执行系统调用，并将系统调用的返回值写入同一块内存页以表示完成。&lt;/p&gt;
&lt;p&gt;但应用和内核怎么同时轮询呢？&lt;/p&gt;
&lt;p&gt;这个设计的关键点在于：&lt;strong&gt;让内核独占一个 CPU 核心&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个核心一直在内核态运行，而其他 CPU 核心则一直在用户态运行。这样从系统整体来看，对于任何一个 CPU 核心都不会发生从用户态到内核态的切换，大大降低系统调用的时延。在应用将请求写入内存页后的下一个时钟周期，处于轮询状态的内核立即可以读到这个请求，并开始运行处理函数；同样，当内核将返回结果写入内存页后，在另一个 CPU 核心处于轮询状态的应用立即可以读到结果并继续运行。&lt;/p&gt;
&lt;p&gt;不过这种方式存在两个缺点：&lt;/p&gt;
&lt;p&gt;第一个缺点在于，如果有多个应用同时发起请求，内核需要一个个顺序处理，则时延可能会比原来更长，因为没能充分使用多核。解决方法也很直接：让多个 CPU 核心同时运行在内核态并轮询用户的请求，当内核忙不过来时，占用的核心多一些，反之少一些（&lt;strong&gt;根据系统负载动态调整内核占用的 CPU 核心数&lt;/strong&gt;）。&lt;/p&gt;
&lt;p&gt;第二个缺点在于，如果整个系统只有一个 CPU 核心怎么办？可以将轮询改为批处理。当 CPU 运行在用户态时，应用程序一次发起多个系统调用请求，同样将请求和参数写入共享内存页。然后 CPU 切换到内核态，内核一次性将所有系统调用处理完，把结果写入共享内存页，再切换回用户态运行。由于特权级的切换次数变少了，所以整体吞吐率提升了。&lt;/p&gt;</content:encoded><h:img src="/_astro/202501222240708.iCDyqeox.png"/><enclosure url="/_astro/202501222240708.iCDyqeox.png"/></item><item><title>DuckDB 的 Adaptive Radix Tree 源码分析</title><link>https://coooredump.github.io/blog/system-architecture/duckdbs-adaptive-radix-tree-source-code</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/duckdbs-adaptive-radix-tree-source-code</guid><description>ART 索引是由 Viktor Leis, Alfons Kemper, Thomas Neumann 等人提出，它相比于 B+ 树的主要区别在于 B+ 树是面向磁盘的，而 ART 则是面向内存的，即 ART 索引是需要全部加载到内存中。</description><pubDate>Fri, 14 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;DuckDB 不同于其他数据库，并没有使用 B+ 树作为主要索引结构，而是使用 ART (Adaptive Radix Tree) 作为它内部的主要索引结构，本文介绍这一索引。&lt;/p&gt;
&lt;h2&gt;ART (Adaptive Radix Tree)&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://db.in.tum.de/~leis/papers/ART.pdf&quot;&gt;ART&lt;/a&gt; 索引是由 Viktor Leis, Alfons Kemper, Thomas Neumann 等人提出，它相比于 B+ 树的主要区别在于 &lt;strong&gt;B+ 树是面向磁盘的，而 ART 则是面向内存的&lt;/strong&gt;，即 ART 索引是需要全部加载到内存中的。DuckDB 之所以选择这个索引有以下几方面的考虑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;随着内存越来越大，并且价格也越来越便宜，我们可以使用纯内存的索引，从而避免磁盘 I/O，提升性能&lt;/li&gt;
&lt;li&gt;ART 索引可以很大程度上节省空间&lt;/li&gt;
&lt;li&gt;ART 索引支持范围查询&lt;/li&gt;
&lt;li&gt;ART 索引有着较高的性能&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;后续本文会先介绍 ART 这一数据结构，然后配合着 DuckDB 的代码描述 ART 是如何实现的。&lt;/p&gt;
&lt;h2&gt;Trie 数据结构&lt;/h2&gt;
&lt;p&gt;在讲 ART 索引之前，我们先看一下 Trie 树🌲&lt;/p&gt;
&lt;p&gt;如果你不知道 Trie 树，可以参考 Wikipedia：https://en.wikipedia.org/wiki/Trie&lt;/p&gt;
&lt;p&gt;✅ LeetCode 上也有「&lt;a href=&quot;https://leetcode.cn/problems/implement-trie-prefix-tree/description/?envType=study-plan-v2&amp;#x26;envId=top-100-liked&quot;&gt;206. 实现 Trie（前缀树）&lt;/a&gt;」算法题可供参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请你实现 Trie 类：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Trie()&lt;/code&gt; 初始化前缀树对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void insert(String word)&lt;/code&gt; 向前缀树中插入字符串 &lt;code&gt;word&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean search(String word)&lt;/code&gt; 如果字符串 &lt;code&gt;word&lt;/code&gt; 在前缀树中，返回 &lt;code&gt;true&lt;/code&gt;（即，在检索之前已经插入）；否则，返回 &lt;code&gt;false&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean startsWith(String prefix)&lt;/code&gt; 如果之前已经插入的字符串 &lt;code&gt;word&lt;/code&gt; 的前缀之一为 &lt;code&gt;prefix&lt;/code&gt; ，返回 &lt;code&gt;true&lt;/code&gt; ；否则，返回 &lt;code&gt;false&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;假设字符串里面只有 a 和 b 两种字符。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;insert&lt;/code&gt;：例如插入字符串 aab，相当于生成了一条移动方向为「左-左-右」的路径。标记最后一个节点为终止节点。再插入字符串 aabb，相当于生成了一条移动方向为「左-左-右-右」的路径。标记最后一个节点为终止节点。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;search&lt;/code&gt;：例如查找字符串 aab，相当于查找二叉树中是否存在一条移动方向为「左-左-右」的路径，且最后一个节点是终止节点。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;startsWith&lt;/code&gt;：例如查找前缀 aa，相当于查找二叉树中是否存在一条移动方向为「左-左」的路径，无其他要求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503140324466.png&quot; alt=&quot;lc208.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;推广到 26 种字母，其实就是一棵 26 叉树，对于 26 叉树的每个节点，可以用哈希表，或者长为 26 的数组来存储子节点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Node {
    Node* son[26]{};
    bool end = false;
};

class Trie {
private:
    Node* root = new Node();

    int find(string word) {
        Node* cur = root;
        for (char c : word) {
            c -= &apos;a&apos;;
            if (cur-&gt;son[c] == nullptr) {
                return 0; // not found
            }
            cur = cur-&gt;son[c];
        }
        return cur-&gt;end ? 2 : 1;
    }

public:
    Trie() {}

    void insert(string word) {
        Node* cur = root;
        for (char c : word) {
            c -= &apos;a&apos;;
            if (cur-&gt;son[c] == nullptr) {
                cur-&gt;son[c] = new Node();
            }
            cur = cur-&gt;son[c];
        }
        cur-&gt;end = true;
    }

    bool search(string word) {
        return find(word) == 2;
    }

    bool startsWith(string prefix) {
        return find(prefix) != 0;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503140324242.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可以看到 Trie 树在检索时的优点是，它的检索时间仅与最长的字符串长度有关，而与存储的字符数量无关，这一特性在数据量极大的情况下十分优秀，但是它的缺点是浪费空间，即&lt;strong&gt;每个内部节点都需要保存固定数量的指针，即使它仅有极少的子节点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如图中的 root 节点，尽管它只有三个子节点，但是它仍然需要保存指向 &lt;code&gt;a,b,c,d,e...&lt;/code&gt; 的空指针，这十分浪费空间，其次 Trie 树仅支持保存字符串。&lt;/p&gt;
&lt;p&gt;ART 则是在 Trie 树的基础上，解决了它缺点的同时，保留了它的优点，下面来介绍 ART 索引。&lt;/p&gt;
&lt;p&gt;对于一个索引而言，我们希望它有以下两个特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查询速度快&lt;/li&gt;
&lt;li&gt;空间占用小&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是如果我们使用 Trie 树做索引（ART 是 Trie 的一个变种），我们就要面临取舍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果内部节点拥有的最大子节点越多（空间占据越多），那么它的高度也越低（速度越快）&lt;/li&gt;
&lt;li&gt;如果内部节点拥有的最大子节点越少（空间占据越少），那么它的高度也越高（速度越慢）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503140334359.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;ART 树选择每个内部节点的大小为 8bit（子节点的数量为 256），刚好是一个 &lt;code&gt;Byte&lt;/code&gt;。这样的好处是&lt;strong&gt;免去了内存对齐的问题，同时在空间与速度上取得了一个较好的平衡&lt;/strong&gt;，我们称内部节点所占据的位宽为 &lt;code&gt;span&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;尽管如此，面对稀疏的数据时，每个节点有 256 个子节点仍然会浪费空间，为了解决这个问题，ART 将内部节点进一步细分为以下四类，我们分别来对其进行介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node 4&lt;/li&gt;
&lt;li&gt;Node 16&lt;/li&gt;
&lt;li&gt;Node 48&lt;/li&gt;
&lt;li&gt;Node 256&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Node 4&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503140354857.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;从图中可以看出，Node 4 分为两个部分，一个是 &lt;code&gt;key&lt;/code&gt; 数组，一个是 &lt;code&gt;child&lt;/code&gt; 数组。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;key&lt;/code&gt; 数组存放 key 的部分内容（也就是 key 的一个 Byte）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;child&lt;/code&gt; 数组则是保存对应的子节点的指针&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意，我们为了可以范围查询，key 数组要求顺序存储。&lt;/p&gt;
&lt;h3&gt;Node 16&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503140353397.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;Node 16 和 Node 4 几乎一样，区别只是从 4 个 &lt;code&gt;slot&lt;/code&gt; 变成 16 个 &lt;code&gt;slot&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;Node 48&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503140401476.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;Node 48 和之前介绍的 Node 一样也是分为 &lt;code&gt;key&lt;/code&gt; 数组和 &lt;code&gt;child&lt;/code&gt; 数组，区别在于 Node 48 的 &lt;code&gt;key&lt;/code&gt; 数组长度为 256，且&lt;strong&gt;内部 &lt;code&gt;key slot&lt;/code&gt; 存放的是指针，指向对应子节点在 child 数组中的位置&lt;/strong&gt;，这样我们就无需通过遍历找到对应的数组，而是可以直接通过 key 的二进制值作为下标直接定位到对应的 &lt;code&gt;key slot&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;实际查询仅需要 &lt;code&gt;child_array[key_array[key]]&lt;/code&gt; 即可。&lt;/p&gt;
&lt;h3&gt;Node 256&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503140401135.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Node 256 就是「Trie 树」原始的内部节点表示形式，仅需要一个数组&lt;/strong&gt;，数组的下标即为 key，&lt;strong&gt;数组中存放的就是子节点的指针&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;各种不同类型的 Node 可以相互转换，如果子节点数量超过限制容量就向上转换，如果节点数量相较于限制容量太小就向下转换。&lt;/p&gt;
&lt;h3&gt;Leaf&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ART 中的叶节点存放的就是 Key 对应的 Value 值&lt;/strong&gt;，ART 的叶节点可以采用三种形式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单独有一个叶节点类型专门保存 Value&lt;/li&gt;
&lt;li&gt;和中间节点保持一致的类型，唯一区别则是 child 数组不保存指针而是保存值&lt;/li&gt;
&lt;li&gt;如果值足够小可以通过位操作和指针一起保存，那么我们可以将值直接存放在内部节点中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;🔥 &lt;strong&gt;DuckDB 采用的是第一种方式&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Optimization&lt;/h3&gt;
&lt;p&gt;在解决了 ART 的空间问题，我们希望可以&lt;strong&gt;进一步优化查询速度&lt;/strong&gt;，即减少树的高度。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;论文链接：https://db.in.tum.de/~leis/papers/ART.pdf&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;论文中有两种方式，但实际上我们可以通过一种简单的做法同时获得这两种优化，&lt;strong&gt;每个节点加上 Prefix 标识&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;1. Lazy Expansion&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503140409919.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;其实这个优化相当简单，我们只需 &lt;code&gt;Leaf&lt;/code&gt; 可以保存多个 byte 即可，这样子对于多个只有一个子节点的路径来说，我们可以将其都保存在 &lt;code&gt;Leaf&lt;/code&gt; 中，从而减少树的高度。&lt;/p&gt;
&lt;h4&gt;2. Path Compression&lt;/h4&gt;
&lt;p&gt;这个优化和 Lazy Expansion 类似，我们只需让 &lt;code&gt;内部节点&lt;/code&gt; 可以保存多个 byte 即可。即如果内部节点有相同的前缀，我们可以将其保存在 Prefix 中，&lt;code&gt;key&lt;/code&gt; 数组仅仅只对 key 不同的部分作区分。这样也可以有效地减少树的高度。&lt;/p&gt;
&lt;p&gt;如果这里没看懂也没关系，后续我会分析 DuckDB 的代码，那样会更加清晰。&lt;/p&gt;
&lt;h3&gt;数据转换&lt;/h3&gt;
&lt;p&gt;对于 ART 来说，前面介绍的都是对于字符串类型 &lt;code&gt;string&lt;/code&gt;，如果作为一个广泛使用的索引，也需要支持不同类型的数据。而 ART 索引实际上是把 &lt;code&gt;Key&lt;/code&gt; 作为数据流进行处理的，也就是说如果想要通过 ART 进行范围搜索，我们需要让 &lt;code&gt;Key&lt;/code&gt; 保持一个性质，&lt;strong&gt;即二进制的大小与该类型的语义大小相同&lt;/strong&gt;：
$$
memcmp(binary(x),binary(y)) &amp;#x3C; 0 \Leftrightarrow x &amp;#x3C; y \
memcmp(binary(x),binary(y)) = 0 \Leftrightarrow x = y \
memcmp(binary(x),binary(y)) &gt; 0 \Leftrightarrow x &gt; y
$$
因此我们需要对某些数字进行转换。&lt;/p&gt;
&lt;h4&gt;unsigned int&lt;/h4&gt;
&lt;p&gt;无需转化，已经满足需求。&lt;/p&gt;
&lt;h4&gt;signed int&lt;/h4&gt;
&lt;p&gt;将符号为 &lt;code&gt;flip&lt;/code&gt; 即可。&lt;/p&gt;
&lt;h4&gt;float&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;static inline uint32_t EncodeFloat(float x) {
	uint64_t buff;

	//! zero
	if (x == 0) {
		buff = 0;
		buff |= (1u &amp;#x3C;&amp;#x3C; 31);
		return buff;
	}
	// nan
	if (Value::IsNan(x)) {
		return UINT_MAX;
	}
	//! infinity
	if (x &gt; FLT_MAX) {
		return UINT_MAX - 1;
	}
	//! -infinity
	if (x &amp;#x3C; -FLT_MAX) {
		return 0;
	}
	buff = Load&amp;#x3C;uint32_t&gt;(const_data_ptr_cast(&amp;#x26;x));
	if ((buff &amp;#x26; (1u &amp;#x3C;&amp;#x3C; 31)) == 0) { //! +0 and positive numbers
		buff |= (1u &amp;#x3C;&amp;#x3C; 31);
	} else {          //! negative numbers
		buff = ~buff; //! complement 1
	}

	return buff;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;char&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Unicode_collation_algorithm&quot;&gt;UCA 算法&lt;/a&gt;已经做出了定义。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Unicode Collation Algorithm&lt;/strong&gt; (UCA) 是 Unicode 规定的如何比较两个字符串大小的算法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;null&lt;/h4&gt;
&lt;p&gt;可以将该值设置为比最大位数仍多 1 位。&lt;/p&gt;
&lt;h4&gt;compound keys&lt;/h4&gt;
&lt;p&gt;按照其包含的基本类型进行拼接即可。&lt;/p&gt;
&lt;h2&gt;DuckDB 源码分析&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;DuckDB 代码链接：https://github.com/duckdb/duckdb/tree/main/src/execution/index/art&lt;/p&gt;
&lt;p&gt;其他参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;论文
&lt;ul&gt;
&lt;li&gt;《The Adaptive Radix Tree: ARTful Indexing for Main-Memory Databases》&lt;/li&gt;
&lt;li&gt;《SMART: A High-Performance Adaptive Radix Tree for Disaggregated Memory》&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;相关代码
&lt;ul&gt;
&lt;li&gt;[&lt;code&gt;cpp&lt;/code&gt;] ART：https://github.com/rafaelkallis/adaptive-radix-tree&lt;/li&gt;
&lt;li&gt;[&lt;code&gt;golang&lt;/code&gt;] ART：https://github.com/plar/go-adaptive-radix-tree&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;这一章通过阅读 DuckDB 的源码，来看一下 ART 索引的实现。&lt;/p&gt;
&lt;p&gt;ART 索引的相关实现都在 &lt;code&gt;art.cpp&lt;/code&gt; 和 &lt;code&gt;art.hpp&lt;/code&gt;，我们主要关注 &lt;code&gt;Insert&lt;/code&gt; 和 &lt;code&gt;Find&lt;/code&gt;，其他函数留给读者自行了解。&lt;/p&gt;
&lt;h3&gt;Insert&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool ART::Insert(Node &amp;#x26;node, const ARTKey &amp;#x26;key, idx_t depth, const row_t &amp;#x26;row_id) {

	if (!node.IsSet()) {
		// node is currently empty, create a leaf here with the key
		Leaf::New(*this, node, key, depth, row_id);
		return true;
	}

	if (node.DecodeARTNodeType() == NType::LEAF) {

		// add a row ID to a leaf, if they have the same key
		auto &amp;#x26;leaf = Leaf::Get(*this, node);
		auto mismatch_position = leaf.prefix.KeyMismatchPosition(*this, key, depth);

		// identical equal
		if (mismatch_position == leaf.prefix.count &amp;#x26;&amp;#x26; depth + leaf.prefix.count == key.len) {
			return InsertToLeaf(node, row_id);
		}

		// example:
		// prefix : hello
		// key[depth] : heel;
		// mismatch_position = 2
		// replace leaf with Node4 and store both leaves in it
		auto old_node = node;
		auto &amp;#x26;new_n4 = Node4::New(*this, node);

		// new prefix
		// new_n4&apos;s prefix is he
		new_n4.prefix.Initialize(*this, key, depth, mismatch_position);

		// old_node&apos;s prefix change to llo
		auto key_byte = old_node.GetPrefix(*this).Reduce(*this, mismatch_position);

		// add child
		Node4::InsertChild(*this, node, key_byte, old_node);

		Node leaf_node;
		Leaf::New(*this, leaf_node, key, depth + mismatch_position + 1, row_id);
		// add child
		Node4::InsertChild(*this, node, key[depth + mismatch_position], leaf_node);

		return true;
	}

	// handle prefix of inner node
	auto &amp;#x26;old_node_prefix = node.GetPrefix(*this);
	if (old_node_prefix.count) {

		auto mismatch_position = old_node_prefix.KeyMismatchPosition(*this, key, depth);
		if (mismatch_position != old_node_prefix.count) {

			// prefix differs, create new node
			auto old_node = node;
			auto &amp;#x26;new_n4 = Node4::New(*this, node);
			new_n4.prefix.Initialize(*this, key, depth, mismatch_position);

			auto key_byte = old_node_prefix.Reduce(*this, mismatch_position);
			Node4::InsertChild(*this, node, key_byte, old_node);

			Node leaf_node;
			Leaf::New(*this, leaf_node, key, depth + mismatch_position + 1, row_id);
			Node4::InsertChild(*this, node, key[depth + mismatch_position], leaf_node);

			return true;
		}
		depth += node.GetPrefix(*this).count;
	}

	// recurse
	D_ASSERT(depth &amp;#x3C; key.len);
	auto child = node.GetChild(*this, key[depth]);
	if (child) {
		bool success = Insert(*child, key, depth + 1, row_id);
		node.ReplaceChild(*this, key[depth], *child);
		return success;
	}

	// insert at position
	Node leaf_node;
	Leaf::New(*this, leaf_node, key, depth + 1, row_id);
	Node::InsertChild(*this, node, key[depth], leaf_node);
	return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;node&lt;/code&gt; 即为当前要进行插入的节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key&lt;/code&gt; 即为要插入的 key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;depth&lt;/code&gt;：即当前已经处理到 key 的第几个 byte，举个例子，key 为 &lt;code&gt;hello&lt;/code&gt;，depth 为 3，那么说明 &lt;code&gt;he&lt;/code&gt; 已经保存在了 node 的祖先节点中，我们当前要处理的是 &lt;code&gt;l&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;row_id&lt;/code&gt; 即为 key 对应的 value 值&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool ART::Insert(Node &amp;#x26;node, const ARTKey &amp;#x26;key, idx_t depth, const row_t &amp;#x26;row_id) {
	if (!node.IsSet()) {
		// node is currently empty, create a leaf here with the key
		Leaf::New(*this, node, key, depth, row_id);
		return true;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果当前节点为空，那么直接设置该节点为叶节点，并且将 &lt;code&gt;row_id&lt;/code&gt; 进行保存，注意这里我们会使用 &lt;code&gt;lazy-expansion&lt;/code&gt;，即将 key 剩余未处理的字符全部保存在叶节点中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool ART::Insert(Node &amp;#x26;node, const ARTKey &amp;#x26;key, idx_t depth, const row_t &amp;#x26;row_id) {

	// .... skip
	if (node.DecodeARTNodeType() == NType::LEAF) {

		// add a row ID to a leaf, if they have the same key
		auto &amp;#x26;leaf = Leaf::Get(*this, node);
		auto mismatch_position = leaf.prefix.KeyMismatchPosition(*this, key, depth);

		// identical equal
		if (mismatch_position == leaf.prefix.count &amp;#x26;&amp;#x26; depth + leaf.prefix.count == key.len) {
			return InsertToLeaf(node, row_id);
		}

		// example:
		// prefix : hello
		// key[depth] : heel;
		// mismatch_position = 2
		// replace leaf with Node4 and store both leaves in it
		auto old_node = node;
		auto &amp;#x26;new_n4 = Node4::New(*this, node);

		// new prefix
		// new_n4&apos;s prefix is he
		new_n4.prefix.Initialize(*this, key, depth, mismatch_position);

		// old_node&apos;s prefix change to llo
		auto key_byte = old_node.GetPrefix(*this).Reduce(*this, mismatch_position);

		// add child
		Node4::InsertChild(*this, node, key_byte, old_node);

		Node leaf_node;
		Leaf::New(*this, leaf_node, key, depth + mismatch_position + 1, row_id);
		// add child
		Node4::InsertChild(*this, node, key[depth + mismatch_position], leaf_node);

		return true;
	}
	
	//skip....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果当前遇到的是叶节点，同时 key 完全相同，那么我们可以直接将 &lt;code&gt;row_id&lt;/code&gt; 插入叶节点中。不然的话，我们需要将叶节点变成内部节点，同时将不同的部分作为该内部节点的叶节点，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503140429649.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool ART::Insert(Node &amp;#x26;node, const ARTKey &amp;#x26;key, idx_t depth, const row_t &amp;#x26;row_id) {

	// skip ....
	// handle prefix of inner node
	auto &amp;#x26;old_node_prefix = node.GetPrefix(*this);
	if (old_node_prefix.count) {

		auto mismatch_position = old_node_prefix.KeyMismatchPosition(*this, key, depth);
		if (mismatch_position != old_node_prefix.count) {

			// prefix differs, create new node
			auto old_node = node;
			auto &amp;#x26;new_n4 = Node4::New(*this, node);
			new_n4.prefix.Initialize(*this, key, depth, mismatch_position);

			auto key_byte = old_node_prefix.Reduce(*this, mismatch_position);
			Node4::InsertChild(*this, node, key_byte, old_node);

			Node leaf_node;
			Leaf::New(*this, leaf_node, key, depth + mismatch_position + 1, row_id);
			Node4::InsertChild(*this, node, key[depth + mismatch_position], leaf_node);

			return true;
		}
		depth += node.GetPrefix(*this).count;
	}

	// recurse
	D_ASSERT(depth &amp;#x3C; key.len);
	auto child = node.GetChild(*this, key[depth]);
	if (child) {
		bool success = Insert(*child, key, depth + 1, row_id);
		node.ReplaceChild(*this, key[depth], *child);
		return success;
	}

	// insert at position
	Node leaf_node;
	Leaf::New(*this, leaf_node, key, depth + 1, row_id);
	Node::InsertChild(*this, node, key[depth], leaf_node);
	return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是内部节点，那我们需要讨论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果前缀完全相同，即 &quot;hello&quot; 和 &quot;hellopxxx&quot;。那么我们仅需要找出子节点进行插入即可&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503140432273.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;如果前缀有不同之处，即 &quot;hello&quot; 和 &quot;helopxxx&quot;。那么我们需要创建一个新的节点，并将两个节点作为子节点进行插入&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202503140434646.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到我们只需要在内部节点和叶节点中支持存储多个字符后，便天然支持上述的优化方案。&lt;/p&gt;
&lt;h3&gt;Find&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Node ART::Lookup(Node node, const ARTKey &amp;#x26;key, idx_t depth) {

	while (node.IsSet()) {
		if (node.DecodeARTNodeType() == NType::LEAF) {
			auto &amp;#x26;leaf = Leaf::Get(*this, node);

			// check if leaf contains key
			for (idx_t i = 0; i &amp;#x3C; leaf.prefix.count; i++) {
				if (leaf.prefix.GetByte(*this, i) != key[i + depth]) {
					return Node();
				}
			}
			return node;
		}
		auto &amp;#x26;node_prefix = node.GetPrefix(*this);
		if (node_prefix.count) {
			for (idx_t pos = 0; pos &amp;#x3C; node_prefix.count; pos++) {
				if (key[depth + pos] != node_prefix.GetByte(*this, pos)) {
					// prefix mismatch, subtree of node does not contain key
					return Node();
				}
			}
			depth += node_prefix.count;
		}

		// prefix matches key, but no child at byte, does not contain key
		auto child = node.GetChild(*this, key[depth]);
		if (!child) {
			return Node();
		}

		// recurse into child
		node = *child;
		D_ASSERT(node.IsSet());
		depth++;
	}

	return Node();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查找的代码相对来说比较简单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查找到了 &lt;code&gt;Leaf&lt;/code&gt; 节点，检查 &lt;code&gt;Prefix&lt;/code&gt; 是否匹配，如果不匹配说明 Key 不存在，若匹配直接返回该叶节点即可&lt;/li&gt;
&lt;li&gt;查找到了 &lt;code&gt;内部节点&lt;/code&gt;，检查 &lt;code&gt;Prefix&lt;/code&gt; 是否匹配，如果不匹配说明 Key 不存在，若匹配则继续搜索对应的子节点&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;本文介绍了 DuckDB 的 ART 索引，可以看到尽管 ART 索引的树会比 B+ 树更高，因此如果是面向磁盘的情况下，B+ 树会比 ART 索引优势更大，但是如果是内存索引的情况下，ART 索引更加紧凑，同时它的渐进时间复杂度仅与 key 的长度有关，&lt;strong&gt;可能也更加 Cache friendly&lt;/strong&gt;？它的节点相较于 B+ 树更加的小，可以更多的保存在 Cache 中。从论文中的实验来看，它的性能会比 B+ 树更好。相较于 &lt;code&gt;Hash table&lt;/code&gt;，它支持范围查询。基于此，DuckDB 将 ART 索引作为其的主要索引。&lt;/p&gt;</content:encoded><h:img src="/_astro/202503140508596.ZrYnuBnn.png"/><enclosure url="/_astro/202503140508596.ZrYnuBnn.png"/></item><item><title>未来世界的幸存者</title><link>https://coooredump.github.io/blog/future-survivor/the-future-world</link><guid isPermaLink="true">https://coooredump.github.io/blog/future-survivor/the-future-world</guid><description>生活就像投资品一样，是存在均值回归的，溢价终究会被时间抹平。</description><pubDate>Mon, 03 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;彻底了解你自己&lt;/h2&gt;
&lt;p&gt;所有关于成长的书籍都告诉我们要扬长避短，做自己最擅长的事，但是了解自己长短这件事情并不容易。&lt;/p&gt;
&lt;p&gt;你可能试过很多方法，比如心理学的各种人格与性格分类，甚至玄学中的星座、属相等等。你会从这些工具中得到各种各样结果，但不管你被归类为冷静理性的 intj，还是敏感细腻的 infp；是潇洒随意的射手座，还是完美主义的处女座。这些结论除了为你的自恋找到依据，或者为自我厌恶的部分做心理按摩以外，就只能增加些谈资，并没有什么实用价值，你的生活和事业并没有因为你知道这些而变好。&lt;/p&gt;
&lt;p&gt;所以你需要一个科学的工具，这个工具叫回馈分析法。人类这种生物是没有多少自由意志的，所有的决策都被潜意识所影响。&lt;/p&gt;
&lt;p&gt;卡尔·荣格曾说过，除非你意识到自己的潜意识，否则它会一直影响着你的生活，然后你说那是命运。&lt;/p&gt;
&lt;p&gt;所以你必须扒开你的潜意识看看你的底色，而窥探潜意识最好的方式，就是用回馈分析法观察自己的决策。&lt;/p&gt;
&lt;p&gt;你的潜意识如何影响你的人生，真实的你自己是什么样子，答案藏在你做的每一个决策里。&lt;/p&gt;
&lt;p&gt;具体操作是：在你准备做一件事情之前，记录下你对结果/效果的期望，在事情完成之后，将实际的结果/效果与你的预期进行比较。&lt;/p&gt;
&lt;p&gt;比如下面这样。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;决策：我决定远离有毒的同事关系，跳槽到新公司重新开始。&lt;/li&gt;
&lt;li&gt;预期结果：新公司的同事关系一定会很和睦，至少会和平相处。&lt;/li&gt;
&lt;li&gt;实际结果：新公司的同事一样很坏，他们都针对我。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你记录的越多，你就对自己越了解，你会发现成功和失败的原因都极其类似，有些事情你很擅长，有些事情你就是死活也做不到。&lt;/p&gt;
&lt;p&gt;你是怎么想的和怎么说的都不重要，重要的是怎么做的，你的决策和行为方式才是你最真实人格的体现，你所有的长项和缺陷都在其中一览无遗。&lt;/p&gt;
&lt;p&gt;当然，你也不必花费数年的时间来记录自己的决策和结果，回忆过去所做的对人生有重大影响的决策，也可以达到同样的效果。&lt;/p&gt;
&lt;p&gt;从这些记录中，你可以分析出你应该做什么，避免做什么，你最强的欲望和最深的恐惧都一目了然。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;然后改变其中能改变的，接受并避免那些不能改变的，在以后的人生中充分发挥你的长项，而不是在缺陷里死循环，白白浪费时间。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;等价交换&lt;/h2&gt;
&lt;p&gt;你目前所拥有的一切都是来自你自身价值的交换，你的工作来自于你的技能；你的生意来自你掌握的资源；你的友情来自你所能提供的情绪价值；你的爱情来自物质和情绪价值的双重交换。&lt;/p&gt;
&lt;p&gt;人在年轻的时候总是过于自我，觉得自己 “配得上这世间所有美好”，所以一旦不被领导赏识，难免会觉得 “怀才不遇”。你爱的人不爱你，你会觉得对方 “瞎了眼”，永远不会反思自己配不配，因为你觉得爱情不应该用物质来衡量，可是你试图用 “有趣的灵魂” 去交换别人的财富、地位、美貌这些硬通货，又高尚到哪里去了呢？&lt;/p&gt;
&lt;p&gt;短期内你的价值可能会被高估或者被低估，&lt;strong&gt;但生活就像投资品一样，是存在均值回归的，溢价终究会被时间抹平&lt;/strong&gt;，最终每个人都只能过上自身实力能够匹配的生活。&lt;/p&gt;
&lt;p&gt;所以你想要什么，必需要先掌握能够与之交换的筹码。&lt;/p&gt;
&lt;p&gt;那么你有什么呢？&lt;/p&gt;
&lt;p&gt;如果你是一个普通人，那么你对世界的认知无非来自知乎、微博以及你平凡父母的言传身教；你的知识来自智商正常就能读的学校（毕业后大部分都还了回去）；你掌握的工作技能，任何一个应届生培训半个月就能胜任。&lt;/p&gt;
&lt;p&gt;你想要怒放的生命，你想要不平凡的人生，又或者你没有别的愿望，只想和多个异性.....（季羡林先生语，此处省略）&lt;/p&gt;
&lt;p&gt;就凭这些能交换到你想要的生活吗？&lt;/p&gt;
&lt;p&gt;所以你必须要增加你的筹码，如果你不名一文，那就应该用你的勤奋，去交换技能和知识，然后和这个世界做交易，在每一步交易中都获得自己想要的筹码，或者是钱，或者是人情，或者是信息。当你积攒的筹码足够多，最后你就可以给你的目标一个 “无法拒绝的条件”，过上你想要的人生。&lt;/p&gt;
&lt;p&gt;这是一条极为艰苦的道路，爽片里的主角一旦下定决心奋斗，几个转场镜头，主角就脱胎换骨，走向人生巅峰。可是真实的奋斗不是这样，真实世界的奋斗是血与火的拼杀，是未知带来的恐惧，是无数个不眠之夜，你需要忍受焦虑和未知的煎熬。在这个过程中你会不断杀死 “自我”，从一个文艺小清新，变成一个 “油腻的生意人”，但这是一条正确的路。&lt;/p&gt;
&lt;h2&gt;你要有一门安身立命的手艺&lt;/h2&gt;
&lt;p&gt;你必须要精通一门手艺，通过这门手艺你可以连接更高阶层的人，用帮助他们的方式种下友谊的种子，积累跨越阶层的人脉，从他们那里获取有价值的信息。&lt;/p&gt;
&lt;p&gt;这门手艺将是你&lt;strong&gt;人生的下限&lt;/strong&gt;，保证你过上至少中等水平的生活。这非常重要，假如你生活在社会底层，为了生活不得不精打细算，那么你将会因为生存资源匮乏养成 “稀缺头脑模式”，没有任何多余 “带宽” 来考虑如何学习提升，这叫贫穷的陷阱。陷入其中的人会因为缺乏改变现状的心力而长期贫困，甚至出现贫困的代际传递，他们的子女大概率也会继续贫困。&lt;/p&gt;
&lt;p&gt;在学习手艺的过程中你还可以掌握把事情做成的方法论，不要小看这件事的价值，&lt;strong&gt;成功乃成功之母&lt;/strong&gt;，这个社会大多数人都浑浑噩噩，没有体验过真正成功的滋味，多少都有点习得性无助，当你体验过为一件事专注到废寝忘食，最终把它做成，你将脱胎换骨。&lt;/p&gt;
&lt;p&gt;得益于互联网的发展，我们学习一门手艺不需要像古时候一样磕头拜师（如果你看过白鹿原就知道那时候学手艺这事儿有多难），现今社会需要的大部分技术都可以从网络上学习，网上有丰富详细的各种教程，你可以学习任何你想学的东西，互联网时代不会埋没人才，也不会亏待勤奋的人，假如你是的话。&lt;/p&gt;
&lt;h2&gt;生理决定了你的精神状态&lt;/h2&gt;
&lt;p&gt;你有没有想过，为什么有的人精力充沛，遇到紧急工作能连续熬夜，在巨大压力下，还能保持充足睡眠，该吃就吃，该喝就喝。而你整天精神萎靡，早上起不来，晚上睡不着，浑身散发负能量，遇难则退，“太难了”、“做不到” 是你的口头禅，饿、困、穷是你的日常抱怨内容。&lt;/p&gt;
&lt;p&gt;有人觉得这是性格问题，但其实本质是个生理问题，人体分泌的各种神经递质会影响我们的情绪，而情绪又会外化为我们的各种日常行为。&lt;/p&gt;
&lt;p&gt;大体上我们的情绪主要被三种神经递质影响：&lt;/p&gt;
&lt;h3&gt;多巴胺&lt;/h3&gt;
&lt;p&gt;多巴胺应该是最为大众熟知的神经递质，多巴胺不生产快乐，它是一种 “承诺你这么做就能够获得快乐” 的物质，是人的心理动力源泉。没有了它就没有做任何事情的内在驱动力。&lt;/p&gt;
&lt;p&gt;人们之所以会在周五工作效率特别高，是因为知道完成工作，就可以在周末愉快的玩耍。如果多巴胺分泌水平较低，就会缺乏这种奖赏机制下产生的动力，明明知道很多事情「很重要」「必须做」，但就是提不起精神，一再拖延。&lt;/p&gt;
&lt;h3&gt;血清素&lt;/h3&gt;
&lt;p&gt;血清素最核心的作用是保持情绪和心情的稳定性。比如，别人对你发脾气，你心态很好，不容易生气。或者遇到挫折时抗压能力强，有点类似成功学说的 “逆商”。&lt;/p&gt;
&lt;p&gt;另外，血清素还能抑制「厌烦」情绪的产生。有实验表明，当分泌血清素的神经元被激活，实验参与者会表现出更高的耐心和积极性。哪怕接连给他们制造困难，也不会失控和不耐烦。&lt;/p&gt;
&lt;p&gt;同样的，当大脑缺乏血清素的时候，很容易导致情绪不稳定，感到抑郁、厌烦、悲观，觉得世界一片灰暗，动不动就发脾气烦躁，看什么都不顺眼。&lt;/p&gt;
&lt;h3&gt;皮质醇&lt;/h3&gt;
&lt;p&gt;皮质醇又叫 “压力荷尔蒙”，听起来是一种不好的激素，实际上大有益处。人在运动锻炼和比赛之前身体会自然分泌皮质醇，可以短暂的改善记忆力和提高疼痛阈值。人体能进化并保留出皮质醇这种激素，是自然选择的结果。一个面对猛兽的原始人，不管他是选择战斗还是逃跑，适当的压力都会提升他的警惕性与战斗能力，生存概率也就会因此提高。也就是说，今天的人类都是那些能产生压力情绪的原始人的后代。&lt;/p&gt;
&lt;p&gt;但是皮质醇过高则又变成了一件坏事，它会让我们过度悲观，导致什么也不想干，食欲大增，吃饱了还想吃。&lt;/p&gt;
&lt;p&gt;最关键的，皮质醇高还会导致「延迟满足」能力变弱，所有让人进步的事情，都需要先苦后甜，你想英语流利，就必须忍受背单词的枯燥。但是皮质醇过高，会让人没办法集中精力做需要静下心才能做的事情，只想要当下的满足。&lt;/p&gt;
&lt;p&gt;如果你多巴胺和血清素分泌水平低，皮质醇分泌水平高，你就会变成一个负能量黑洞或者高敏感人格，日日陷在心理内耗中不能自拔，学习、工作、事业就不要想了。&lt;/p&gt;
&lt;h3&gt;饮食｜冥想｜运动&lt;/h3&gt;
&lt;p&gt;那么怎么改善这些神经递质的分泌水平呢？有三种经过科学检验的方法：饮食、冥想、运动。&lt;/p&gt;
&lt;p&gt;1️⃣ 先说饮食，有些食物对改善神经递质的分泌有一定作用，但是我不建议用饮食调理，因为如果不能想吃什么就吃什么，人的情绪会受很大影响，饮食的调理功效会被对冲掉，所以饮食的作用可能不会太明显。&lt;/p&gt;
&lt;p&gt;2️⃣ 其次是冥想，有研究表明，有规律的进行冥想可以直接影响大脑中关键神经递质的水平，能有效提高自控力、钝感力，促进睡眠。而且冥想的效果立竿见影，没有冥想经历的人，尝试一次冥想就能体验到久违的内心平静，对驱散焦躁情绪帮助极大。&lt;/p&gt;
&lt;p&gt;3️⃣ 最后是运动，这是我比较推荐的一种方法，运动对提高多巴胺和血清素，降低皮质醇的作用非常显著，这和我们的印象大体上是一致的，运动能力好，身体素质好的人，通常元气满满，更乐观开朗。而身体素质差或者干脆就是亚健康的人抗压能力弱，遇到问题会更多抱怨，没有耐心，甚至脾气也更差。&lt;/p&gt;
&lt;p&gt;当然冥想和运动都需要长期坚持，这对本身就充满负能量的人来说非常难。但是人生若不做成几件抓心挠肝的难事，基本不可能有阶级跃迁的机会。何况还是这种只要坚持就有效果，因果关系极为清晰的事。&lt;/p&gt;
&lt;h2&gt;完美主义害死人&lt;/h2&gt;
&lt;p&gt;让一个完美主义者不出手的理由有很多，天气和心情都可以是原因，&lt;strong&gt;他们害怕失败，希望一出手就是完美状态&lt;/strong&gt;。但这几乎是做不到的，很多事情都是在与现实的碰撞中一步一步完善的，不可能事先在脑海中想清楚每一个细节。&lt;/p&gt;
&lt;p&gt;软件设计领域有一个著名的 “MVP” 原则，既最小可行产品，可以把它理解为一个软件的最简单版本，细节虽然不完善，甚至粗糙，但它是可以运行的，可以满足用户最核心的需求。把它快速推向市场，接受用户的检验，验证这个软件是否有市场，确定以后再快速迭代，加大投入。这样做可以用最小的成本，快速验证自己的创意，如果一开始就追求大而全，要么失了先机，让别人抢占市场，要么就是闭门造车，做的东西完全没人用，浪费时间和金钱。&lt;/p&gt;
&lt;p&gt;很多人在学习上也是完美主义者，喜欢死抠细节，不搞明白绝不进入下一章节的学习。在学习上死磕精神是对的，但是对于初学者来说，过于抠细节往往会钻牛角尖，导致学习过程举步维艰。不是说细节不重要，而是初学阶段的细节问题，要么在以后的章节会讲到，要么就是初学者不了解基本概念，思考了一个不是问题的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于一个想要做点事儿的人来说，接纳一定程度的未知、混乱、瑕疵是常态，路是一步步走出来的&lt;/strong&gt;。你需要在 “战争中学习战争”，让自己的想法和技能接受别人的检验和反馈。很多功成名就的人在成功以后都自夸当初如何运筹帷幄、高瞻远瞩，但实际上哪个不是一路如履薄冰，小心翼翼才走到今天。&lt;/p&gt;
&lt;h2&gt;你需要做一个副业&lt;/h2&gt;
&lt;p&gt;这几年很多人都在尝试副业，但是我让你做的副业不是利用一项技能接私活，而是独立运营一门小生意。它可以是在朋友圈卖水果；在二手平台卖旧货；做外贸 soho；利用独立站做 drop shipping；做自媒体博主（文字、音频、视频，你对哪个感兴趣就做哪一种类型）。&lt;/p&gt;
&lt;p&gt;做这些小生意你有百分之一的可能赚到超过你工资的钱，百分之十的可能赚点零花钱，更可能是亏几顿 KFC，但这些都不重要，重要的是培养商业能力和保持市场敏感度。&lt;strong&gt;你会学习如何引流获客，什么是投入产出比，什么是供应链&lt;/strong&gt;。&lt;strong&gt;当你以一个商人的视角看待这个世界时，你会更加客观，你没有得到你想要的东西，只会是你错了，因为市场永远不可能出错，它只会给正确对待的它的人以回报&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;读一百本商业书籍，在脑中设计一万个商业模式，不如自己亲自运营一个小生意，你只有身在其中才能保持对市场的敏感，在下一波浪潮到来时及时抓住机会，站上浪潮之巅。&lt;/p&gt;
&lt;p&gt;做站长失败的伊光旭抓住了微博崛起的机会，运营出冷笑话精选等一批微博大号；在论坛博客时代就活跃的人，抓住了公众号的红利期，赚得盆满钵溢；在阿里巴巴把流量日益向大卖家倾斜时，一些活不下去的小卖家抓住了拼夕夕的机会；近几年火爆的跨境电商，从业者大多以前就在做传统外贸。&lt;/p&gt;
&lt;p&gt;🌀&lt;strong&gt;没有人是横空出现的，你必须先进场&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;得到贵人帮助&lt;/h2&gt;
&lt;p&gt;如果你研究过一些大佬的生平，你就会发现在大佬崛起的关键节点，往往都有贵人相扶。大佬得到的也许并不是直接的帮助，可能就是一个建议或者信息，但大佬从此就一飞冲天。&lt;/p&gt;
&lt;p&gt;那么什么样的人才能得到贵人相助呢？&lt;/p&gt;
&lt;p&gt;首先你要有平均线以上的能力，这是别人给你机会的基础。一个团队中什么样的人会得到提拔呢？不一定是能力最好的人，但肯定不是能力最差的人。&lt;/p&gt;
&lt;p&gt;其次，你要靠谱，这个品质非常重要，甚至比能力还重要。对一个领导来说，“可预期” 这件事非常重要，能力最好的人未必是可预期的，他可能心高气傲，受不了委屈随时撂挑子不干。如果你能做到 “凡事有交代，件件有着落，事事有回音”，成为领导心目中靠谱的人，那么领导就更有可能成为帮助你的贵人。&lt;/p&gt;
&lt;p&gt;第三，你要真诚，懂得感恩（起码要表现的如此），因为没有人愿意帮助一个白眼狼。&lt;/p&gt;
&lt;p&gt;除此以外，还有一种人必然会得到贵人相助，那就是真正的 “人中龙凤”，如果你惊才绝艳，才华突破天际，里里外外都表现出 “非池中物” 的品质，别人知道你迟早要出头，打压你也无济于事，就会给你提供帮助，提前在你这里攒下人情。&lt;/p&gt;
&lt;p&gt;比如拼夕夕的创始人黄峥。在浙大期间黄峥就表现出了极客的品质，他热衷在网上发布技术文章，是学校的风云人物。网易的创始人丁磊有次遇到一个技术难题，在网上看到了黄峥的文章，于是联系到黄峥，在他的帮助下顺利解决了问题。丁磊非常希望黄峥能够加入网易，但是当时黄峥正要去美国留学，丁磊便介绍黄峥给退休在美国做投资的步步高创始人段永平认识，两人很快成为忘年之交。&lt;/p&gt;
&lt;p&gt;段永平是黄峥重要的人生导师和贵人。黄峥在美国硕士毕业以后获得两个 offer，一个来自科技巨头微软，一个来自创立不久但发展迅速的谷歌。段永平建议他去当时规模还不大的谷歌。后来谷歌上市，黄峥持有的期权立刻让他实现财务自由。在谷歌工作两年后，黄峥回国创业，段永平又成为了他的投资人。&lt;/p&gt;
&lt;p&gt;像黄峥这样的人万中无一，对于普通人来说，把自己修炼到具备水平以上的能力，并展现自己的靠谱和真诚，是获得贵人帮助的最好方式。&lt;/p&gt;</content:encoded><h:img src="/_astro/202502030017184.DkI9UnaY.jpeg"/><enclosure url="/_astro/202502030017184.DkI9UnaY.jpeg"/></item><item><title>「2024 年终总结」世界指向任何我想去的地方</title><link>https://coooredump.github.io/blog/yearly-review/2024-the-world-points-to-wherever-i-want-to-go</link><guid isPermaLink="true">https://coooredump.github.io/blog/yearly-review/2024-the-world-points-to-wherever-i-want-to-go</guid><description>我最初写年终总结的初衷，仅仅是为了如果有一天 remake 了，能在互联网多留一些痕迹，所以当阅读人数从几十人到几万人，我就会很开心，这意味着世界上又多了一些看到了我痕迹的人。我写的这种不算小说，所以我写的很慢，一个作者哪能写尽世上的所有人呢，写来写去，写的还是自己和自己身边的人，无论孤独还是野心，都是自己人生某个侧面的写照，这是我的局限与浅薄，但也是我的真诚。</description><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我总是习惯在跨年的时节，去回望过去的人生旅途，在无数次相遇和离别中，找回内心的平静。&lt;/p&gt;
&lt;p&gt;今年真的要过去了，我始终觉得元旦不算新年，但真等到了除夕，又似乎和平时没有什么区别。2024 年仿佛一直都在路上，一直都在疲于奔命，社会节奏本就匆忙，互联网更是。为生活奔波的时候，没精力和大爷聊天，也没心情抬头看朝霞日落，连电梯上升的加速度都压得人腿软。&lt;/p&gt;
&lt;p&gt;我总是期待着也祈祷着自己在路上，哪怕是周末了，我也希望自己可以从头忙到尾，兼顾学业、家庭、朋友，找时间读一本好书，听一场音乐会，看一场电影。似乎什么好事我都不愿意错过，好像身在一家自助餐厅，每样菜都很好吃，我都想尝试。&lt;strong&gt;我也确实很喜欢吃自助，但其实很快就吃饱了，已经不再是当年了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对我个人来说，没有任何一件事情，像生病和生病造成的影响让我成熟得更快，它明确指出生命中到底什么最重要，我或许现在还没有找到，但一定不是屏幕上冗长的代码和无用的八股。&lt;/p&gt;
&lt;p&gt;我最初写年终总结的初衷，仅仅是为了如果有一天 remake 了，能在互联网多留一些痕迹，所以当阅读人数从几十人到几万人，我就会很开心，这意味着世界上又多了一些看到了我痕迹的人。我写的这种不算小说，所以我写的很慢，一个作者哪能写尽世上的所有人呢，写来写去，写的还是自己和自己身边的人，无论孤独还是野心，都是自己人生某个侧面的写照，&lt;strong&gt;这是我的局限与浅薄，但也是我的真诚&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;#01 我的学生时代&lt;/h2&gt;
&lt;p&gt;春节那几天恰逢初高中学校的校友日，顺便约上朋友和老师一起回去看看，那六年的时间还历历在目。&lt;/p&gt;
&lt;p&gt;我在高中的时候有人嫉妒我，我想大概是因为和哪个女生传过绯闻，被当成了情敌，我在本科的时候有人嫉妒我，我想是因为我当年是个卷王，对于卷王，大多数人都是恨得咬牙切齿然后一边痛骂一边怨恨为什么这个人不是自己，特别是人前显圣的时候，因为我也在心里骂过那个台上的卷王，这俩我都能理解。我不理解的是，怎么读了研究生，仍然会有人嫉妒我，嫉妒这样的一个卷了好些年后的衰仔。&lt;/p&gt;
&lt;p&gt;有次看到个热搜是，一百万回到高三，问你愿不愿意，我其实挺想去的，可惜没钱，后来看了评论区才发现，原来是给我一百万。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;图 1～图 3 分别是：小学、初中、高中&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501090056373.JPG&quot; alt=&quot;IMG_2163&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;替我的化学老师带孩子哈哈哈，非常可爱&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501090119766.jpg&quot; alt=&quot;IMG_2165&quot;&gt;&lt;/p&gt;
&lt;p&gt;说实话我并不知道我为什么会疯狂的怀念学生时代，有时候一个画面，一段音乐，都可以勾起思绪，惆怅得不行，听说人喜欢怀旧是因为现实里过得并不好，我也不知道这是不是对的。那天我坐在曾经我们下课后聚众闲聊的走道边，脑海一路开始回想过去读书的日子，那时候阳光不是很大，暖暖的，温和的照在眼前，隔着树叶斑驳的影子在墙上晃动。也许我生来就是比较喜欢念旧的，不管过去有多好多差，总会去怀念那一段青涩的日子。怀念那个 12 人间的老旧寝室，晚上此起彼伏的鼾声；怀念偷偷看着喜欢的女孩子，然后她也会偶尔回头看自己一眼；怀念留下来默默的打扫卫生，然后听着广播里的不知名歌曲；怀念在图书馆用 MP3 听周杰伦的歌；怀念那条通往教学楼和寝室的小路；怀念那家经常去的奶茶店；怀念那辆通往学校的公交车...&lt;/p&gt;
&lt;p&gt;在十七八岁或者更早的某天，我们像无数个往常一样和朋友说再见，很多年之后才意识到那是最后一见，却连那天的天气都记不起来了。相比之下，能认真告别就已经足够幸运了。&lt;/p&gt;
&lt;p&gt;那天我拍了很多照片存手机里，好像这样就能留住我的学生时代一样。&lt;/p&gt;
&lt;p&gt;可是人是会变的，大家都走了，没有人留在原地，我只是往过去多看了几眼。&lt;/p&gt;
&lt;p&gt;很多年后，我还是会想起高考后的那个盛夏，大学，军训，正午，不知道哪里来的马蜂嗡嗡的穿过迷彩服的人群，人群快速的散开又聚合，在阴凉地看着未来的舍友和同学军训，呆呆的幻想着美好的大学生活。这时候还没有计算机，没有 Java，没有绩点，没有保研，也没有什么实习，什么秋招，什么天南海北的漂泊，聚散离合。&lt;/p&gt;
&lt;p&gt;看到那些曾与我度过美好时光的人都已渐渐淡出我的人生时，我的心情 be like：🥺&lt;/p&gt;
&lt;h2&gt;#02 白日梦想家&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;分享一些我在 2024 年看过的文学作品，不限文体。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们活在这样复杂的世界里，被其中如同圆周率一样从不重复也亳无规则的事情拉扯着，朝世界尽头盲目地跋涉而去。我们就是这样生活在如同圆周率般复杂而变化莫测的世界里，慢慢地度过了自己的人生，而文学把生命剥出新的层次，让人看见新的可能。它让我获得日常琐碎中感受不到的快乐，甚至是痛苦，但每感受一次，我就多活了一次。时间多珍贵啊，能多活一次，就要多活一次。活着，认真笃定地生活，要清醒，要思考，而不是长久地单薄地发笑。&lt;/p&gt;
&lt;h3&gt;0x01. 世界只有一个真相&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501090236298.JPG&quot; alt=&quot;IMG_2166&quot;&gt;&lt;/p&gt;
&lt;p&gt;黑塞的《悉达多》尽了最大的努力来缓解我们人生的空虚感，尤其是当你一无所有的时候，你依然可以拥有欲望和目标。&lt;/p&gt;
&lt;p&gt;世界只有一个真相：原来我期待的圆满的人生，不会到来，换句话说，每一个当下就是圆满。所谓的我，就是过去一切体验的总和。我是我接触过的人、碰到过的物、感受过的情爱、迷失过的痛苦。所有的一切，才有此刻的我，少一点都不是。&lt;/p&gt;
&lt;p&gt;我听便灵魂与肉体的安排，去经历罪孽，追逐肉欲和财富，去贪慕虚荣，以陷入最羞耻的绝望，以学会放弃挣扎，学会热爱世界。我不再将这个世界与我所期待的，塑造的圆满世界比照，而是接受这个世界，爱它，属于它。&lt;/p&gt;
&lt;h3&gt;0x02. 你想活出怎样的人生&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501221543903.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;四月份，和朋友一同去看了宫崎骏这部收官之作《你想活出怎样的人生》，是日本作家吉野源三郎的著作《你想活出怎样的人生》的同名电影。&lt;/p&gt;
&lt;p&gt;在虚无主义无孔不入的时代，清醒观察思考，努力认真生活，无论时代如何困难、残酷，请始终作为一个“人”而活着 —— 也许这就是 83 岁的宫崎骏与内心的矛盾与伤痛纠葛一生后，想要告诉大家的活法。&lt;/p&gt;
&lt;h3&gt;0x03. 死亡不是终点，遗忘才是&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501090300627.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;强烈建议大家都去看看《寻梦环游记》，一部具有带有亡灵色彩与死亡教育的动画片。&lt;/p&gt;
&lt;p&gt;人的一生，要死去三次：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次，当你的心跳停止，呼吸消逝，你在生物学上被宣告了死亡。&lt;/li&gt;
&lt;li&gt;第二次，当你下葬，人们穿着黑衣出席你的葬礼，他们宣告，你在这个社会上不复存在，你悄然离去。&lt;/li&gt;
&lt;li&gt;第三次，是这个世界上最后一个记得你的人，把你忘记。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这部电影不只是教会了我们应该如何在亲情与梦想之间做选择、如何去面对死亡，它更是一堂教育课，教会了我们如何成长，让我们明白活着的意义。我们是人类的一分子，而人类是充满激情的。医学、法律、商业、科技，这些都是崇高的追求，足以支撑人的一生。但音乐、诗歌、梦想、情感，这些才是我们活着的意义。&lt;/p&gt;
&lt;h3&gt;0x04. 自由，就是拥有被讨厌的勇气&lt;/h3&gt;
&lt;p&gt;《被讨厌的勇气》一书中的阿德勒心理学其实就是个体心理学，把人当作一个独立的个体，也就是所谓的课题分离。我们要清楚的知道哪些是自己的课题，哪些是别人的课题，没必要为别人的期待而活，珍惜更多当下的时刻，去体验更多的事物，相见什么人就去见，去认识，去告别，做好自己的课题，被别人讨厌也没关系。人生是不断与理想的自己进行比较，不要把人生理解为一条线，而要理解成点的连续。人生就像是在每一个瞬间不停旋转起舞的连续的刹那。暮然四顾时常常会惊觉：已经来到这里了吗？&lt;/p&gt;
&lt;p&gt;在人际关系中面对困难时，应该先考虑 “倾听更大共同体的声音”，阿德勒用了一个更容易理解的比喻，当下的痛楚，如果比作杯中的风暴，那你应该跳出杯子，在太平洋这阵风暴完全不值一提，如果此刻难过，就用更宏大的视角去看待现在的困境。在读万卷书行万里路的过程中，找到更大的世界，去稀释当下的痛苦，当世界的分母变得足够大，痛苦的分子就无关紧要了。&lt;/p&gt;
&lt;h3&gt;0x05. 巴音布鲁克没有海&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501090415076.png&quot; alt=&quot;image-20250109041548848&quot;&gt;&lt;/p&gt;
&lt;p&gt;属实没想到《飞驰人生》还有续集，不管是第一部还是第二部，都给了我极佳的视觉震撼和观感体验。&lt;/p&gt;
&lt;p&gt;影片中最击中我内心的一段对话：&lt;/p&gt;
&lt;p&gt;​	&quot;我们努力了就一定有机会&quot;&lt;/p&gt;
&lt;p&gt;​	&quot;不是的，我努力过无数次了，但我明白，机会只存在于其中的一两次&quot;&lt;/p&gt;
&lt;p&gt;巴音布鲁克没有海，只有无尽的热爱，把你的全部奉献给你所热爱的一切。&lt;/p&gt;
&lt;h3&gt;0x06. 花束般的恋爱&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501090422600.jpeg&quot; alt=&quot;7A3EB4B4-5D93-410F-9553-8C319786A738_1_101_o&quot;&gt;&lt;/p&gt;
&lt;p&gt;不得不说，这部电影拍得很细腻，我在看的时候已经把我自己完全带入这个男主，我的心情随着麦跌宕起伏，体会到了学生时代恋爱的青涩，也感受到了成年后的爱情和生活多么不容易。&lt;/p&gt;
&lt;p&gt;我也懂得了人生重要的一课：&lt;strong&gt;不要挽留所有要离开的人&lt;/strong&gt;。你应该优先考虑你的神经系统，当远离某人可以让你的精神放松下来，给你带来平静时，你就知道是时候放弃这段关系了。&lt;/p&gt;
&lt;h3&gt;0x07. 等待戈多&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501090459634.png&quot; alt=&quot;image-20250109045925536&quot;&gt;&lt;/p&gt;
&lt;p&gt;《等待戈多》是爱尔兰现代主义剧作家塞缪尔·贝克特的两幕悲喜剧，1953 年首演。这是一个关于等待的故事，人生中那些名为等待的消磨，那些名为绝望的希望。戈多是你打不通的电话，是公交站未到的公车，是明天，是时间，是你现实生活中所有不可期的一切。&lt;/p&gt;
&lt;p&gt;人生就是一场漫无目的的等待，生活是一个希望渺茫的困局，人是自己的救世主，等待戈多就是等待自己。&lt;/p&gt;
&lt;p&gt;虚无的人生，荒诞的世界，每个人都在等待戈多。&lt;/p&gt;
&lt;h2&gt;#03 寻找人生的 25 号底片&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092225356.jpeg&quot; alt=&quot;B85E0F20-8687-4AD0-A6C0-38BC378E3107_4_5005_c&quot;&gt;&lt;/p&gt;
&lt;p&gt;中学的时候喜欢看意林之类的杂志，里面的作者用乱七八糟的理由跑去旅游，然后说 “阻碍你脚步的永远只有逃离的勇气和对生活的热爱”。&lt;/p&gt;
&lt;p&gt;我觉得太对了，可惜 12306 付款方式里没有勇气和热爱，不知道是不是下了盗版。&lt;/p&gt;
&lt;h3&gt;Jan. 泉州&lt;/h3&gt;
&lt;p&gt;年初和小伙伴们从厦门打车去泉州玩了一遭，刚好碰到大师在免费写春联，排了一晚上终于拿到了哈哈哈&lt;/p&gt;
&lt;p&gt;晚上租了一个四人麻将房，锋哥（大师兄）的胜率高达 90%+，被虐惨了&lt;/p&gt;
&lt;p&gt;有我这个地道的泉州人带路，狠狠地逛了西街、关岳庙、五店市，后面还深度体验了下蟳埔村的簪花（指偷拍🫤&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092244722.JPG&quot; alt=&quot;IMG_2168&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;清源山&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092303188.JPG&quot; alt=&quot;IMG_2169&quot;&gt;&lt;/p&gt;
&lt;h3&gt;May. 漳州&lt;/h3&gt;
&lt;p&gt;五一：南靖土楼 + 长汀古城&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092318410.JPG&quot; alt=&quot;IMG_2171&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092317520.JPG&quot; alt=&quot;IMG_2170&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092319547.JPG&quot; alt=&quot;IMG_2172&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Jun. 杭州&lt;/h3&gt;
&lt;p&gt;六月，实验室一起去杭州开会，也算故地重游了。&lt;/p&gt;
&lt;p&gt;参加学术会议最深的感受就是：全场四五个小时表现最具专业性的是摄影师。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092331350.JPG&quot; alt=&quot;IMG_2173&quot;&gt;&lt;/p&gt;
&lt;p&gt;除了开会，还抽空去灵隐寺、京杭大运河、西湖等各地游玩，体验了人生第一次剧本杀。此外还参观了浙大玉泉校区，和浙大的某实验室深入交流了一番，收获挺多的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092339001.JPG&quot; alt=&quot;IMG_2175&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092338967.JPG&quot; alt=&quot;IMG_2174&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Jun. 西安&lt;/h3&gt;
&lt;p&gt;六月底前往西安 · OPPO 公司结项，开启了长达两个月的实习。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;记录人生第一次坐飞机 ✈️&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202245848.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100126191.JPG&quot; alt=&quot;IMG_2176&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;西安不愧是「碳水之都」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100138863.JPG&quot; alt=&quot;IMG_2180&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;夕阳无限好&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100149952.JPG&quot; alt=&quot;IMG_2181&quot;&gt;&lt;/p&gt;
&lt;p&gt;公司的健身房有点小，但是挺赞的，因为基本只有我在用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100215393.JPG&quot; alt=&quot;IMG_2182&quot;&gt;&lt;/p&gt;
&lt;p&gt;实习期间看到个帖子，说 xx 公司自己一入职就给自己花了 5w：笔记本 + 显示屏 + 一堆软件。下面有老哥锐评，你怎么不说公司还替你建了好几个亿的写字楼，非常真实。在 OPPO 实习的这段时间，只能用公司的电脑，而公司内电脑权限很小，你甚至不能下 QQ 和微信，也不能访问公网云盘，有时候觉得，&lt;strong&gt;不是给人配了个电脑，而是给电脑配了个人，人走了是耗材，电脑回到公司循环使用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我之前一直用的 Windows，后来转向了 Mac，有朋友问我，Windows 和 Mac 怎么选，这个问题很难回答，就像大一暑期的时候问我 C++ 和 Java 这两门课应该怎么选择一样。之前觉得，用 Windows 让我还记得我是个学生，记得大一开学刚买第一个笔记本时的激动，而 Mac 给我的回忆只有极强的续航和轻便，以便我在不同的会议室奔波。&lt;/p&gt;
&lt;p&gt;这两个月除了开不完的会议以及赶不完的进度，还投了一篇论文，好在是中了，会议需要作者 12 月到武汉开会，可惜我导不给报销，推掉了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092349247.png&quot; alt=&quot;image-20250109234907153&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;会议邀请函&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100157812.png&quot; alt=&quot;image-20250110015709725&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;奇險天下第一山 · 華山&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100224155.JPG&quot; alt=&quot;IMG_2185&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;落花时节又逢君 · 西安交大&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100214048.JPG&quot; alt=&quot;IMG_2184&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Jul. 成都&lt;/h3&gt;
&lt;p&gt;七月去成都找师兄玩！虽说刚毕业 1 个月，但还是迫不及待来见一面～&lt;/p&gt;
&lt;p&gt;这三天自驾游去「川西 · 毕棚沟」游玩，在成都街头走走，到熊猫谷看看国宝 🐼&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100242570.JPG&quot; alt=&quot;IMG_2189&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100245109.JPG&quot; alt=&quot;IMG_2187&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;成都美食真的顶哇！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100243573.JPG&quot; alt=&quot;IMG_2186&quot;&gt;&lt;/p&gt;
&lt;p&gt;在成都的最后一天，恰逢附近有梵高展！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100243620.JPG&quot; alt=&quot;IMG_2190&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Jul.  银川&lt;/h3&gt;
&lt;p&gt;抓住七月的尾巴，自己一人前往银川三天，其中一天跟了一个小团一日游：中卫 + 沙坡头 + 腾格里沙漠 + 66 号公路。&lt;/p&gt;
&lt;p&gt;银川即是 “归雁入胡天” 的古来边关，更是 “大漠孤烟直” 的具像化，览山公园的风景让我着迷，我走过沙漠，骑骆驼 🐫，吹过旷野的风，这种无拘无束，自由掌握人生的感觉真美好。&lt;/p&gt;
&lt;p&gt;生活的底片从来不是遥远的白日梦，而是热爱生活的自己。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100249871.JPG&quot; alt=&quot;IMG_2191&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Oct. 福州&lt;/h3&gt;
&lt;p&gt;国庆节回了一趟福州，见了许久未见的朋友，每一刻都弥足珍贵。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;约了本科两位亦师亦友的教授闲聊&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100315939.JPG&quot; alt=&quot;IMG_2192&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;感谢好朋友们的热情款待，顺带蹭了俩老师一顿饭哈哈哈&lt;/p&gt;
&lt;p&gt;同时郑重的感谢我的好朋友思萍，来福州这段时间麻烦她不少&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100315786.JPG&quot; alt=&quot;IMG_2193&quot;&gt;&lt;/p&gt;
&lt;p&gt;这次从福州回来，买了一等票，我之前觉得坐一等座是很不划算的行为，直到这时候我才明白，人为什么会去坐一等座，很简单：高铁上坐商务座的是有钱的，坐二等座是有票的，所以坐一等座的是没票而且没钱的。&lt;/p&gt;
&lt;p&gt;每每返回福州，总会感伤和怀念那段时光和同学们。有一些东西要靠消失才能证明它的珍贵，如果这是无法返航的日子，那我祝你们一路向前，桥都坚固，隧道都光明，如果不能，那就祝你们曾经的理想能足够支撑当下的生活，等到未来偶然的一天回到这里，再聚的时候，我们能轻轻释怀所有的冷雨，微笑着轻描淡写地说：不过些许风霜罢了。&lt;/p&gt;
&lt;h3&gt;Oct. 东山岛&lt;/h3&gt;
&lt;p&gt;计划了几月之久的东山岛旅游，工作日的短暂逃离💨&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100338356.JPG&quot; alt=&quot;IMG_2198&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100338119.JPG&quot; alt=&quot;IMG_2200&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说好的一起看日出呢，就我醒了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100340548.PNG&quot; alt=&quot;IMG_2194&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Nov. 广州&lt;/h3&gt;
&lt;p&gt;又是参会的一周，做不了学术大佬，就做学术蝗虫！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CCF China Storage&apos;2024｜又见程老师！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100402666.JPG&quot; alt=&quot;IMG_2210&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;📷&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100403226.JPG&quot; alt=&quot;IMG_2208&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100401275.JPG&quot; alt=&quot;IMG_2207&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;广式早茶&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100403520.JPG&quot; alt=&quot;IMG_2209&quot;&gt;&lt;/p&gt;
&lt;h2&gt;#04 被审判的二十余岁人生&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100848428.jpeg&quot; alt=&quot;A240C727-BF1F-4B17-8417-CD66F2475DC1_4_5005_c&quot;&gt;&lt;/p&gt;
&lt;p&gt;我总觉得，学生时代在大四那年就已经结束了，读研只是一场用来对前四年脱敏的短暂回光返照。每一根网线都在传递着焦虑的信息，只能靠逃避来抵制对未来的恐惧和怀疑。&lt;/p&gt;
&lt;p&gt;有一次学长和我闲聊，问我最近学习怎样，我说我没学。他斟酌着措辞跟我隐晦的暗示：“额，有空闲时间不学习我会有内疚感的，你没有吗？”&lt;/p&gt;
&lt;p&gt;我沉默了一会，然后不得不痛苦的承认了一个事实，是的，这可能就是我现阶段痛苦的根源。原来我傲慢的讨厌这讨厌那，讨厌世界，讨厌你们，我最讨厌的是我自己，那个傲慢又懒惰的自己。最痛苦莫过于清醒的摆烂。呵，不过话说回来，都是二十来岁，谁还没点儿理想啊。海边绚烂的烟花，路上美丽的夕阳，湖面晴朗的天空，益海楼前火烧的晚霞，落日，晚风，鲜花。这些浪漫与美好毋庸置疑装点了我的青春，但绝不能够成为我青春的全部意义，我要的是热血万丈，我要去的是英雄梦想，我要看的是天地奇观，我要拼尽全力拿到众望所归的成就，大大松一口气地听着别人的称赞，然后再谦虚的说，运气好而已。&lt;/p&gt;
&lt;p&gt;有时候，我也会为一些无用的事情落泪，理想主义者的无畏坚持，改革者的魄力，权衡利弊者的仗义直言，已知悲剧结局的少年志气，或者仅仅是次元有壁。无病呻吟久了，有朝一日真得病了，我才突然发现，那些所谓挫折好像也就那么回事，太阳底下没有新鲜事。不过好像真到了需要验证某个成果的年纪了，生活热衷于打断理想主义者的脊梁，但永远有人撞破阻碍着理想主义者的屏障，也永远有人是理想主义者。看到被迫 gap 的同龄人困境，听到别人对他们或怜悯或嗤笑的高高在上的评价，我根本没办法庆幸自己逃过一劫。气愤和痛苦，夹杂着铺天盖地的焦虑将我包围，不是作壁上观，实属无奈，谁都一样。走在向上的钢丝上，越往上走，一旦踩空只会坠落得更加难看，泻水置平地，各自东西南北流。人们以为自己在追求幸福，但向下的自由不是自由，人类区别于一支芦苇，正是在于思维流动的光辉。&lt;/p&gt;
&lt;p&gt;这一年，过得自信心全无，读研就像是某种无形的寄生虫，对人的侵蚀是无声无息、不温不火的，每天都透支一点点，一年后就消耗掉你所有的热情和活力。这一年我也终于理解了为什么工作的人越来越少发朋友圈，真的上纲上线倒也没有，但以前发是坦坦荡荡的分享快乐，现在发是如履薄冰，感觉像是在吸引炮火，真的丧失了很大一部分的自由。我也开始理解为什么有人会沉溺酒精、尼古丁、短视频，因为日子过到一定程度，有时候真的需要一点麻醉剂来饮鸩止渴。也许这时候有人又会说：年轻人就是吃不了苦、无病呻吟。辛苦就是辛苦，各有各的辛苦，并不是不如某某辛苦，就没有资格说出来。自嘲是为了不麻木，重视自己的感受。这一代的年轻人并非软弱，即使说了这么多丧气话，大家也一直在努力生活。只不过满腹牢骚者也可以把工作干得很好，胆怯者也可以颤颤巍巍的成为英雄。我开始意识到，解构才是对个体的真正尊重，人生无数次阅历的积攒，就只为在某个重要的时刻给予我们面对的勇气。&lt;/p&gt;
&lt;p&gt;无所谓，每个人都有自己的南墙要撞！&lt;/p&gt;
&lt;p&gt;这一年，看着身边二十啷当岁的朋友。有的啤酒肚了，头发开始掉了，被女孩甩了。如果问我，生活会不会将当初的少年打趴，我不知道，但生活一定会将当初的少年打散。再聚首，再问起当年的少年意气，多数人都会不好意思地笑笑，然后说，那时候不懂事。而喝多的时候，就全成了让自己觉得矫情的眼泪。往往这个时候，我想不出来什么安慰的话。我确实没有什么话想讲，只觉得胸口像是春夏的雷雨天前那样发闷。人的心态会经历很多个奇妙的转变，这种转变甚至都不需要什么道理和铺垫，就好像你本来好好地在路上走着，然后一下子和路边的陌生人一起举着手，大声欢呼或者低着头默哀一样。&lt;/p&gt;
&lt;p&gt;我们终于活到了小时候最羡慕的年纪，却没有成为小时候最想成为的人。毕业之后的我们都在害怕自己没出息，害怕自己买不起房子车子，遇不到喜欢自己的人，每次过年回家面对什么时候结婚，什么时候要孩子这种话题，其实更害怕的是面对许久不见的父母。我们都是普通的孩子，我们都想让自己的爸爸妈妈更自豪一些，更幸福一些。可我们自己的幸福都还像个石子，在生活的湖面上打着水漂。这么多年上学上过来，努力地学习，考试，可到最后才发现，普通的孩子只是聚光灯下的基石。想想这些年，我有很多以为近在咫尺的时候，我努力地抓啊抓，可就是什么都抓不到。再多的书也比不上导师满意的 PPT 和恰到好处的谄媚。我一边和自己的妈妈保证说，以后我一定会有出息的，一边成晚成晚的睡不着觉。这样的场景太多了，多到我觉得每一场跌宕起伏的人生经历总是会有个这样毫不意外的结局。&lt;/p&gt;
&lt;p&gt;爬到山顶的时候，跑向海边的时候，以为我不再是我的时候。&lt;/p&gt;
&lt;p&gt;我总以为山顶的石头不一样，升起的太阳不一样。&lt;/p&gt;
&lt;p&gt;我总以为海边吹的风不一样，尽头的那边不一样。&lt;/p&gt;
&lt;p&gt;我以为我不再是我，我爱她，她也爱我。&lt;/p&gt;
&lt;p&gt;可惜，山还是山，海还是海。&lt;/p&gt;
&lt;p&gt;我拥有很多人情绪崩溃的瞬间，他们有的在我身边，有的靠互联网奇缘。我没有和其中的任何一个人有过合照，有一起吃过一顿饭，没有听到过他们的声音，也没有听太多他们的故事。我只是短暂地让他们靠了一下岸。&lt;/p&gt;
&lt;p&gt;而当生活的节奏反复无常，日历上的时间不断反转，我总是会在人生的重要时刻丧失无穷无尽的仪式感。我常觉得所有人都是被上了锁的自由花，偶尔被阳光照耀的时候会觉得自己配的上很多东西。像是野马找到了河边的水，牵牛爬上了缺角的屋檐。我很难分辨压在自己身上的到底是挡住眼睛的石头，还是粘住了后背的五指山。任何一种喘不过来气的定义都被他人掌控，我带着面罩，别人掐着氧气管。等到自己坚持不住的那一刻，很难说是呼吸的缺失，还是自己早就病入膏肓。&lt;/p&gt;
&lt;p&gt;好多事情想不明白，只能先活着，看以后能不能想明白了。&lt;/p&gt;
&lt;p&gt;人人都得活着，所以人人都得藏着。我偶尔就在这样沉默的冰河之下，悄悄探出头来，感受下有温度的太阳。然后再沉下去，告诉别人海面之上的故事。&lt;/p&gt;
&lt;h2&gt;#05 人生南北多歧路&lt;/h2&gt;
&lt;p&gt;网上冲浪看到一个很有意思的话题：人一辈子，最重要的到底是什么？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原回答如下，感触颇深&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;三岁那年，我紧握着手中的棒棒糖，坚定的认为那最重要。&lt;/p&gt;
&lt;p&gt;五岁那年，我花了整整一个下午，逮住那只蜻蜓，那一刻，它好像是最重要的。&lt;/p&gt;
&lt;p&gt;七岁那年，我看着同桌手中的奖状，带着羡慕和一点点嫉妒，觉得那也许是最重要的。&lt;/p&gt;
&lt;p&gt;九岁那年，仰躺在树荫下，阳光斑驳的洒在脸上，一个悠闲的暑假于我而言是如此重要。&lt;/p&gt;
&lt;p&gt;十三岁那年，我意识到，重点高中的录取通知书对我的人生很重要。&lt;/p&gt;
&lt;p&gt;十六岁那年，坐在教室里，微风穿堂，盯着前排姑娘的马尾出了神，忽然觉得就这样一直下去也不错。&lt;/p&gt;
&lt;p&gt;十八岁那年，我日夜苦读，求神拜佛，只为一张大学录取通知书。&lt;/p&gt;
&lt;p&gt;二十二岁那年，告别校园，懵懂的踏进所谓社会，一份工作又成了最重要的。&lt;/p&gt;
&lt;p&gt;二十四岁那年，迎来了我的婚礼，我看着满堂宾客和我的新娘，她当然不是我十六岁时的那个姑娘，心中只觉得有些遗憾，不过那一刻，我的新娘就成为了我最重要的人。&lt;/p&gt;
&lt;p&gt;二十五岁那年，我和狐朋狗友推杯换盏，吹牛打屁，不谙世事的年纪，只觉得面子最重要。&lt;/p&gt;
&lt;p&gt;二十六岁那年，我焦急的等在产房外，啼哭声打破了宁静，我知道，更重要的来了。&lt;/p&gt;
&lt;p&gt;三十三岁那年，被房贷和车贷搞的焦头烂额的我觉得，钱可太重要了。&lt;/p&gt;
&lt;p&gt;三十八岁那年，一生强硬的爸爸开始征求我的意见，那一刻我猛然意识到，他终于是老了。&lt;/p&gt;
&lt;p&gt;还是三十八岁那年，妈妈再没有训斥过我，而是不厌其烦的念叨，还带着些小心翼翼，我知道，她也会老的。&lt;/p&gt;
&lt;p&gt;又是三十八岁那年，儿子不再黏我，他有了自己的伙伴的生活，我知道，此后的一辈子，他只会不停的远离我。&lt;/p&gt;
&lt;p&gt;那年，我恍然，可能时光才是这世上最重要的吧。&lt;/p&gt;
&lt;p&gt;四十岁那年，看着乱七八糟的体检报告，我才想起，我从来没觉得自己重要。&lt;/p&gt;
&lt;p&gt;四十五岁那年，浑浑噩噩度过了半生，挺着啤酒肚在工位摸鱼的时候，回想起年少的梦想，从未觉得梦想如此重要。&lt;/p&gt;
&lt;p&gt;五十岁那年，看着儿子和一个还不错的姑娘步入婚姻殿堂，我眯着眼看着台上的儿子，不知道新娘是不是他十六岁时爱上的那个姑娘。但还是觉得儿子的幸福比我的幸福更重要。&lt;/p&gt;
&lt;p&gt;五十五岁那年，我气喘吁吁的跟在孙子屁股后面，生怕他摔跤，那一刻，我从未给予孙子远大的希冀，他平安快乐便是最重要的。&lt;/p&gt;
&lt;p&gt;六十岁那年，我将父母葬在了一起，年纪大了，很多事也便看开了许多，我没有流泪，只觉得，爸爸的责骂和母亲的絮叨在那一刻无比重要。&lt;/p&gt;
&lt;p&gt;七十岁那年，妻子终是先走一步，儿子儿媳事业有成，孙子在外地读大学，我只能无所事事的在大街上闲逛，莫名觉得，妻子可比那广场舞的老太太重要的多。&lt;/p&gt;
&lt;p&gt;七十五岁那年，在医院里，医生让我出去，单独留下我儿子的时候，我明白时间不多了，趁着这功夫我给孙子打了个电话，我想告诉他，如果你在十六岁的时候爱上过一个姑娘，可千万要握紧，就像握紧三岁那年手中的棒棒糖。思来想去，又觉得多少有些为老不尊，电话接通后，只说了一句爷爷想你了，有空来看看我。医生宽慰我问题不大，我笑着告诉医生，人生没有大问题，其实把日子过下去是最重要的。&lt;/p&gt;
&lt;p&gt;七十六岁那年，孙子回来看我了，让他看到我奄奄一息的样子心里多少还有点别扭，儿子儿媳守在床边，泣不成声，我没有多余的精力思考什么最重要了，我只想着后事从简，儿子儿媳年纪也不小了，身体遭不住，孙子刚刚参加工作不久，请假不好请，别给领导留下坏印象。&lt;/p&gt;
&lt;p&gt;正想着，不知哪里吹来一阵风，迷了我的眼，睁开眼，爸爸妈妈牵着手，脸上挂着我最熟悉的笑容，他们都是年轻的样子，张开双臂示意我抱抱，我好想他们啊，所以我毫不犹豫跳下床，向他们飞奔而去，奔跑中，我变成了六十岁的样子，五十岁的样子，四十岁的样子，三十岁的样子，直到变成三岁的样子，他们终于又能抱起我了，我向他们点点头，他们也笑着点头，带着我转身离开。我回望一眼儿子儿媳和孙子，他们抱着七十六岁的我，嚎啕大哭，虽然不舍，不过没关系，我知道他们依然可以过的很好。&lt;/p&gt;
&lt;p&gt;所以，什么最重要？什么都重要，但又不是非有不可。初中的亲戚小孩问我寒假作业不写有什么后果吗。我说有的，比如你会拥有一个没有作业的寒假。当然，你也会失去一些什么，比如寒假作业。&lt;/p&gt;
&lt;p&gt;你曾经认为最重要的，总有失去的那天。&lt;/p&gt;
&lt;p&gt;时间改变了太多东西了。&lt;/p&gt;
&lt;p&gt;遗憾才是人生的常态。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;年少的标志之一，就是对人生的旷野感到恐惧。&lt;/p&gt;
&lt;p&gt;这不稀奇，当然也不值得去忧虑，因为它来源于一种与现实之间轻飘飘的，遥远的疏离。&lt;/p&gt;
&lt;p&gt;我完全无法想象当我的生命趋于麻木，当我困守在一间窄窄的房子里，开始为钱发起了愁，为生计开始了奔波，那是怎样一副光景。&lt;/p&gt;
&lt;p&gt;而这样的一生到底有什么意义呢。&lt;/p&gt;
&lt;p&gt;不要恐惧，不要害怕你的自由和浪漫会失去。&lt;/p&gt;
&lt;p&gt;我说了人生就如同一片旷野，当那漫长且潮湿的雨季来临以后，你就会发现浅小的水洼成为了汪洋，萤火虫成了海上的月亮，蝉也化作挥动着翅膀的鲸。&lt;/p&gt;
&lt;p&gt;去赋予生活意义，也赋予生活爱和有趣。&lt;/p&gt;
&lt;p&gt;一个人思虑太多，就会失去做人的乐趣，一直往前走吧，会有你想要的答案的。&lt;/p&gt;
&lt;p&gt;年轻嘛，未妨惆怅是清狂，但我知道，人不能永远过着打球、唱歌、夜宵这样放纵的日子，一方面，岁月无情，另一方面，美丽的东西往往也太过单薄，成为一个好的人就是要有一种对于世界的开放性、一种信任自己难以控制的无常事物的能力... 这种生活的根基就在于愿意被暴露在世界中，就在于更像一株植物，而不是一颗宝石。&lt;/p&gt;
&lt;p&gt;现代人格外接受不了不确定性，于是热衷于相信存在一套稳定的秩序，费尽千辛万苦地找一个尽可能高的位置把自己放进去，高塔当然辉煌，高的位置当然可以很好的安放一个人，可是转头看，凡有日月星辰照耀之地，又何处不可寄此一生？天地广阔，四季春秋，等年华老去，明天的我们也许会去找一些更厚重的东西来承担更长久的人生，比如去祖国的宏大叙事里做一枚齿轮，不管你信不信，朋友，对于齿轮来说，运转不是致命的，生锈才是。&lt;/p&gt;
&lt;p&gt;所以，困住你脚步的到底是什么？&lt;/p&gt;
&lt;p&gt;我的意思是，&lt;strong&gt;当你意识到生命只有一次的时候，你的第二次生命就开始了&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;#06 这江湖没什么好的，也就酒还行&lt;/h2&gt;
&lt;p&gt;似乎本科时候也没那么难熬，可能是因为当时在乎的人都在身边吧，我是个贼念旧的人，这点其实不好，也不适合互联网。&lt;/p&gt;
&lt;p&gt;2019 年以前腾讯放弃了电商，默认了自己没有电商基因、京东在 3C 电子做着防守、字节跳动正准备发布一款叫 “抖音” 的音乐社交软件、那家叫拼多多的初创公司还在农村里乱转，卖点可怜的水果拼团生意，那会的耀子还和华子绑在一起，大家还觉得是个低端品牌，至于比亚迪，绝大多数人连简历都不曾投递过，当然，从 2020 年开始一切都变了。&lt;/p&gt;
&lt;p&gt;正如公司所信仰的拥抱变化一样，其实我们也拥抱了不少变化，比如还没出生的时候就遇到了计划生育，刚出生赶上了非典，到了喝奶粉的年纪遇到了三鹿毒奶粉，没安分几年又来了多种版本的禽流感，小学爱吃校门口大锅炸的辣条，后来发现用的全是地沟油，玩游戏的时候赶上了防沉迷，好不容易混进大学被疫情封了三年，毕业了发现大学生比狗都多，想吃外卖了到了发现是国潮包装。&lt;/p&gt;
&lt;p&gt;我们这一代人绝大多数都看过考试周破防，对我来说是大二的暑期，我喜欢去买一瓶热带风味冰红茶，这是我本科过得最开心的一个暑假之一了，下学期的课很少很少，然后要保研啦，保研去哪里不知道呢，反正有很多很多的可能性，我到时候一定要选一个喜欢的学校，喜欢的城市，冰红茶的瓶盖上有时会写着再来一瓶，我总是不着急兑换，因为觉得来日方长，未来有一天我收拾东西提桶跑路的时候发现了当年的瓶盖，才意识到好像我的青春只剩下了谢谢惠顾。&lt;/p&gt;
&lt;p&gt;忘了在哪里看到的鸡汤：“不要谩骂以前的自己，TA 当时一个人站在雾里也很迷茫”。&lt;/p&gt;
&lt;p&gt;但我从不骂，因为我他丫的现在还在雾里。&lt;/p&gt;
&lt;p&gt;政治老师曾说过：“这个选项没有错，只是它不符合题意”，我想，在这个时代，我们也是。&lt;/p&gt;
&lt;p&gt;我一直是个很悲观的人，也没什么安全感，我走在一直变强的路上，我现在有能力屏蔽掉许多坎坷，但终有一天，我会遇到迈不过去的门槛，压抑不了的负面情绪。我始终会想，这时候，谁还会在我身边，我希望大家都在，但这个显然太过天真了，我现在给不出答案，但其实与人的每一次相遇我都在想，TA 会怎么做。我厌恶欺骗，相对于欺骗别人，我更厌恶被别人欺骗。所以我是个很慢热的人，我的很多值得交心的朋友，在我决定敞开心扉的时候，早就不在我身边了。&lt;/p&gt;
&lt;p&gt;在此声明一下哥们不是回避型人格，哥们是逃难型人格，每次感觉到别人一丁点冷落，哥们都会屁滚尿流地收拾包袱跑路。&lt;/p&gt;
&lt;p&gt;我这人从很小的时候就争强好胜，我一直以为是我自己赢了，直到有一天看着镜子，才知道自己输了。在我最美好的时候，我最在意的人都不在我身边。从认识我到对我失望，到底需要多长时间，我每认识一个朋友，就会在心里想一遍这个问题。如果几年前你问我会不会害怕失去而逃避未来的相遇，我会回答是的，但现在不会是。因为我已经失去过很多了，不再差这点了。&lt;/p&gt;
&lt;p&gt;以我的能力和意志力，永远不会迎来把过往的错误和懊悔全部弥补的那一天，但以我的记忆力和心态，一定有毫无内疚把他们全部抛之脑后的那一天。人的烦恼就是记性太好，如果可以把所有事都忘掉，以后每一天都是个新开始，你说多好。&lt;strong&gt;因为我总是很擅长把一个可以花精力解决的问题，变成一个需要时间接受的事实&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;学计算机的总有种错觉，仿佛自己穷举出所有的可能性，然后选取一个不会太差的决定，也有时会觉得，很多事靠卷或是努力可以解决，这几年发现并不是。朋友问我的人生是贪心还是动态规划，我说我只会暴力，但我很喜欢 KMP 算法：一个人能走多远不取决于在顺境中能走多快，而在于他在逆境中能否找到曾经的自己。&lt;/p&gt;
&lt;p&gt;有时候会觉得，命运不公平。人和人是不一样的，有的人生下来就和开了挂一样，有钱开朗，家庭温馨，一路上会遇到很多有趣的人，很多朋友，老了跟人吹牛都有很多故事可讲，成为别人眼中酷酷的老头或者老婆婆，对这样的人来说，每件往事都很珍贵，但也都没那么珍贵，就像她可能会在意这周的演唱会，会激动的好几天睡不着抢票，做规划，要是抢不到也没有什么大不了，她可以飞到另一个城市去看下周的演唱会，但有些人不一样，他们一辈子就呆在一片很小很小的地方，一共认识不了几个人，没有几个人会真正在乎他，也没有几件他真正在乎的事。&lt;/p&gt;
&lt;p&gt;有时候又会觉得，命运挺公平，至少过程，是独一无二的，是有开心的时刻的。&lt;/p&gt;
&lt;p&gt;我始终觉得，人非草木，人活一世，更多的只是活几个瞬间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;反正这个世界挺没意思的，要是没了我那几个朋友，就更没意思了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我很重感情，这是优点也是弱点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我由衷感谢那些还陪在我身边的朋友们。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;于道各努力，千里自同风&lt;/strong&gt;！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;还有许多朋友遗失了合照和相片，找了一晚上实在没找到 😭&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100546655.JPG&quot; alt=&quot;IMG_2219&quot;&gt;&lt;/p&gt;
&lt;h2&gt;#07 UNICEF｜联合国儿童基金会&lt;/h2&gt;
&lt;p&gt;这一年，也可以尽自己所能做一些慈善活动，为什么会有这个想法呢。我想借用中科院黄国平博士的一段话：人情冷暖，生离死别，固然让人痛苦与无奈，而贫穷则可能让人失去希望。我的理想不伟大，只愿年过半百，归来仍是少年，希望还有机会重新认识这个世界，不辜负这一生吃过的苦。&lt;strong&gt;最后如果还能做出点让别人生活更美好的事，那这辈子就赚了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100557369.JPG&quot; alt=&quot;IMG_2225&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100558383.JPG&quot; alt=&quot;IMG_2226&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;大爱壁纸&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100558208.JPG&quot; alt=&quot;IMG_2224&quot;&gt;&lt;/p&gt;
&lt;h2&gt;#08 超级全满贯｜Fan Z.D.&lt;/h2&gt;
&lt;p&gt;从小学一年级我就开始打乒乓球了，打了 12 年的球，至今已经二十余年的间隔了。&lt;/p&gt;
&lt;p&gt;今年奖励自己新买了一副乒乓球拍，作为球迷也见证了樊振东的夺冠之路，心满意足。&lt;/p&gt;
&lt;p&gt;奥运会男单那两场比赛我已经看了无数次，但是我还是会选择点进来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1j5rbYHExm/?spm_id_from=333.999.0.0&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;七局死斗，一个人就是千军万马；巴黎奥运会：樊振东 VS 张本智和&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV14JtoeeEqC/?spm_id_from=333.999.0.0&amp;#x26;vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;巴黎奥运会：樊振东 4-1 莫雷高德，实现超级全满贯伟业&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100603774.jpg&quot; alt=&quot;IMG_2227&quot;&gt;&lt;/p&gt;
&lt;p&gt;我非常喜欢的几位乒乓球运动员：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;张继科：抢班夺朝，这么多年只有张继科一人做到了，真正意义上的天赋型选手&lt;/li&gt;
&lt;li&gt;马龙：GOAT 无需多言&lt;/li&gt;
&lt;li&gt;樊振东：16 岁横空出世，天降紫薇星，17 岁成为男乒史上最年轻世界冠军，三剑客时代后我唯一爱看的选手&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;简单介绍下东哥的职业生涯&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2013 年樊振东横空出世，在队内选拔赛中原本并没有获得参赛资格，而是赛后教练组动用机动名额给了樊振东，这才让他第一次参加巴黎世乒赛，但是在第三轮比赛中就输给了张继科，此时樊振东年仅 16 岁。&lt;/p&gt;
&lt;p&gt;2015 年樊振东再次参赛，成功打进苏州世乒赛半决赛，尽管此刻的他技术进步明显，但遇到了巅峰发胶龙，还是被 4 : 1 带走，止步半决赛。&lt;/p&gt;
&lt;p&gt;之后樊振东又打进世界杯决赛与马龙会师，没想到被马龙横扫夺冠。&lt;/p&gt;
&lt;p&gt;2016 年里约奥运会樊振东当时世界排名第二，却最终以 P 卡参赛，那一年是龙队横扫张继科夺冠，划时代的决赛。&lt;/p&gt;
&lt;p&gt;2016 年樊振东成功打进世界杯决赛，并且以 4 : 1 的大比分战胜许昕，夺得首个三大赛男子单打冠军。&lt;/p&gt;
&lt;p&gt;2017 年杜塞尔多夫世乒赛，樊振东首次打入决赛，但是以两分之差遗憾输给龙队，赛后也成为了他的意难平。&lt;/p&gt;
&lt;p&gt;2018 年世界杯在马龙出局的情况下，樊振东顶住压力战胜波尔再夺世界杯冠军。&lt;/p&gt;
&lt;p&gt;2019 年布达佩斯世乒赛，此时樊振东世界排名第一，却爆冷输给梁靖崑。&lt;/p&gt;
&lt;p&gt;2019 年成都世界杯，马龙意外被张本智和打败无缘决赛，而后樊振东在主场战胜了张本智和蝉联冠军。&lt;/p&gt;
&lt;p&gt;2020 年世界杯决赛，樊振东时隔五年决赛再遇马龙，决胜局以两分之差险胜马龙成为世界杯四冠王！&lt;/p&gt;
&lt;p&gt;2021 年东京奥运会男单决赛，樊振东首次奥运单打便以 4 : 2 的比分遗憾输给马龙摘银。&lt;/p&gt;
&lt;p&gt;2021 年休斯顿世乒赛樊振东再一次打进决赛，这也是罕见的决赛外战，樊振东不出意外 4 : 0 横扫小莫拿下首个世乒赛冠军。&lt;/p&gt;
&lt;p&gt;2023 年德班世乒赛决赛，樊振东战胜王楚钦蝉联世乒赛单打冠军，再一次捧起圣伯莱杯。&lt;/p&gt;
&lt;p&gt;2024 年巴黎奥运会，樊振东的 last dance 表明了自己的决心，并成功击败小莫拿下自己生涯的首个奥运单打冠军。&lt;/p&gt;
&lt;p&gt;从 13 年看小胖崭露头角，到乒坛 11 年的角逐，他乒乓球生涯的大部分时间，都和巅峰张继科以及巅峰马龙重叠，一拍一拍从三剑客的时代打出自己的时代。三大赛单打拥有四个世界杯冠军，两个世乒赛冠军，一个奥运会冠军，三大赛团体冠军更是数不胜数。&lt;/p&gt;
&lt;p&gt;解说口中的樊振东是普通人一生的缩影，其实不是的，他是天才，但是大满贯这条路他走了十一年，但未来不止于此，一个时代的到来，它不会轻易落下帷幕！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Fan Z.D.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100618843.JPG&quot; alt=&quot;IMG_2228&quot;&gt;&lt;/p&gt;
&lt;h2&gt;#09 2024 东成西就&lt;/h2&gt;
&lt;p&gt;一年前的我，略带期待地向未来发出诘问，一字一句：“一年后的你，生活有答案了吗？”&lt;/p&gt;
&lt;p&gt;人类常常如此，有时候连自己的问题是什么都没弄清楚，就敢执着地去索要答案。&lt;/p&gt;
&lt;p&gt;于是我开始复盘，在飞驰的时光里，努力筛选一些拿得出手的成绩。不得不说，每当复盘的时候，数据还是挺能安慰到人的，如非为了申请或吹嘘，我大概很少会主动想起所谓加身的荣誉，这些其实只占珍贵时光中的很少一部分。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GitHub&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100632651.PNG&quot; alt=&quot;cc4dd32424e80c07499ed1b99fe06c0e&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;LeetCode&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100636387.PNG&quot; alt=&quot;784a6a2a9da56d241f2efc0f93725b6a&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Apple Music&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100654570.JPG&quot; alt=&quot;IMG_2235&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;集满 Jay Chou 所有专辑&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202506281105405.png&quot; alt=&quot;Jay Chou&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Netease Cloud Music&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100640237.JPG&quot; alt=&quot;IMG_2230&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;高德地图&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100640281.JPG&quot; alt=&quot;IMG_2229&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;知乎&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100639500.PNG&quot; alt=&quot;IMG_2068&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;专利&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100642490.png&quot; alt=&quot;image-20250110064217352&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;花各有期&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100624365.png&quot; alt=&quot;image-20250110062428251&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;手机相册里的 2024&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100541518.PNG&quot; alt=&quot;IMG_2195 2&quot;&gt;&lt;/p&gt;
&lt;h2&gt;#10 你看，每个人都在闪闪发光&lt;/h2&gt;
&lt;p&gt;点开了很久没看的朋友圈，当初想的是打着防止被骚扰的理由换账号，实际上是接受不了和身边朋友的落差，很多人去名校读研，或者留学，每个学校都赫赫有名，许多人也都有自己的归宿。&lt;/p&gt;
&lt;p&gt;“你看，每个人都在闪闪发光。“&lt;/p&gt;
&lt;p&gt;我对自己说。就像烟花一样，我好喜欢烟花，恨不得在每个句子里都上一颗火星，这样整篇文章就看起来熠熠生辉。&lt;/p&gt;
&lt;p&gt;我知道，应该接受过去、享受当下、期待未来，&lt;/p&gt;
&lt;p&gt;可我永远只会后悔过去、浪费当下、焦虑未来。&lt;/p&gt;
&lt;p&gt;我们总是把自己未经历或者已经经历过的时光称作最美好的时光，我突然开始怀疑，其实从来没有哪段时光是美好的，所有时光都是痛苦的，所谓美好，只是大脑编出来的一个美妙幻梦，让我们在尘世间有所寄托。我开始怀疑，会不会所有对过去的怀念，全部来自于不用真的再去经历那些事情，本质上只是旁观者的幸灾乐祸。我想不是的，起码过年放烟花的日子，是实实在在的开心的。不论春晚有多无聊，当主持人的倒数淹没在鞭炮声里，火树银花凌空绽放，整座城市隆隆作响，震颤从天空传到大地，极尽所能地宣告我们又活过一年，捂住耳朵又不舍得错过满天绚烂，仰起头，眼底惊喜和火光交相辉映。&lt;/p&gt;
&lt;p&gt;这一年我的心态也转变了不少，以前我总是想着追寻那个所谓的成功，所谓的万事求全，但回过头来你会发现和伙伴一起被现实打得满地找牙的过程，似乎才是最美妙的。我很喜欢我朋友和我说过的一句话，他说巅峰的快乐总是短暂的，你知道人生最美好的感受是什么吗？虚惊一场，对吧！最美好的感觉就在这一张一弛之间。&lt;/p&gt;
&lt;p&gt;当你看尽人类所有的历史，悲欢离合，英雄小人，爱恨情仇，都只在这么一颗小小的蓝色星球之上，你会不会有这种感觉，最终我们都会化为尘土，那为什么要做所谓的好事，还有难的事呢？如果这个宇宙的结局是注定、是消亡、是归于死寂，那或许更说明，过程才是最重要的，这是我们能够改变的唯一的事。&lt;/p&gt;
&lt;p&gt;2024 年在漫长的折磨中，带着无尽苦涩，终究也是结束了，有人欣喜雀跃，有人遗憾痛悔，更多人在平淡艰巨的日子里，朝着更大的光荣彼岸无声努力着。&lt;/p&gt;
&lt;p&gt;如果未来变得更糟，也没关系，我们有的是时间去变成更好的人。&lt;/p&gt;
&lt;p&gt;关于离别，是制造羁绊需要承担的掉眼泪的风险。&lt;/p&gt;
&lt;p&gt;关于未来，是做该做的事并承担它的事与愿违。&lt;/p&gt;
&lt;p&gt;新的一年里，我只希望趁着自己还有劲儿，多笑一笑，百年需笑三万六千场，一日一笑，此生快哉！&lt;/p&gt;
&lt;p&gt;今天这个日落真的很完美，我就在此刻为这篇文章画上句号，尽情享受这一刻。&lt;/p&gt;
&lt;p&gt;我们明年再见👋&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501101000682.JPG&quot; alt=&quot;IMG_2236&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202501200717503.y-ClD3lm.jpg"/><enclosure url="/_astro/202501200717503.y-ClD3lm.jpg"/></item><item><title>A Study of Linux File System Evolution</title><link>https://coooredump.github.io/blog/system-architecture/a-study-of-linux-file-system-evolution</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/a-study-of-linux-file-system-evolution</guid><description>为了使不同的文件系统共存， Linux 内核在用户层与具体文件 系统之前增加了虚拟文件系统中间层，它对复杂的系统进行抽象化，对用户提供了统一的文件操作接口。无论是 ext2/3/4、FAT32、NTFS 存储的文件，还是 /proc、/sys 提供 的信息还是硬件设备，无论内容是在本地还是网络上，都使用一样的 open、read、write 来访问，使得 “一切皆文件” 的理念被实现，这也正是软件中间层的魅力。</description><pubDate>Mon, 30 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Virtual File System (VFS)&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202412271620668.png&quot; alt=&quot;VFS&quot;&gt;上图解构如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应用层指用户编写的程序，如我们的 &lt;code&gt;hello.c&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;GNU C 库（&lt;em&gt;glibc&lt;/em&gt;）即 C 语言标准库，例如在编译器章节介绍的 &lt;em&gt;libc.so.6&lt;/em&gt; 文件，它 包含了 &lt;code&gt;printf&lt;/code&gt;、&lt;code&gt;malloc&lt;/code&gt;，以及本章使用的 &lt;code&gt;fopen&lt;/code&gt;、&lt;code&gt;fread&lt;/code&gt;、&lt;code&gt;fwrite&lt;/code&gt; 等文件操作函数&lt;/li&gt;
&lt;li&gt;用户程序和 &lt;strong&gt;glibc&lt;/strong&gt; 库都是属于用户空间的，本质都是用户程序&lt;/li&gt;
&lt;li&gt;应用层的程序和 glibc 可能会调用到 “系统调用层（SCI）” 的函数，这些函数 是 Linux 内核对外提供的函数接口，用户通过这些函数向系统申请操作。例如，C 库 的 &lt;code&gt;printf&lt;/code&gt; 函数使用了系统的 &lt;code&gt;vsprintf&lt;/code&gt; 和 &lt;code&gt;write&lt;/code&gt; 函数，C 库的 &lt;code&gt;fopen&lt;/code&gt;、&lt;code&gt;fread&lt;/code&gt;、&lt;code&gt;fwrite&lt;/code&gt; 分别 调用了系统的 &lt;code&gt;open&lt;/code&gt;、&lt;code&gt;read&lt;/code&gt;、&lt;code&gt;write&lt;/code&gt; 函数，具体可以阅读 glibc 的源码了解。&lt;/li&gt;
&lt;li&gt;由于文件系统种类非常多，跟文件操作相关的 &lt;code&gt;open&lt;/code&gt;、&lt;code&gt;read&lt;/code&gt;、&lt;code&gt;write&lt;/code&gt; 等函数经过虚 拟文件系统层，再访问具体的文件系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总的来说，为了使不同的文件系统共存， Linux 内核在用户层与具体文件 系统之前增加了虚拟文件系统中间层，它对复杂的系统进行抽象化，对用户提供了统一的文件操作接口。无论是 &lt;strong&gt;ext2/3/4&lt;/strong&gt;、&lt;strong&gt;FAT32&lt;/strong&gt;、&lt;strong&gt;NTFS&lt;/strong&gt; 存储的文件，还是 /proc、/sys 提供 的信息还是硬件设备，无论内容是在本地还是网络上，都使用一样的 open、read、write 来访问，使得 “一切皆文件” 的理念被实现，这也正是软件中间层的魅力。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202412292343673.jpg&quot; alt=&quot;详细讲解，Linux内核——文件系统（建议收藏）&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Linux System Calls&lt;/h2&gt;
&lt;p&gt;从上图可了解到，系统调用（System Call）是操作系统提供给用 户程序调用的一组“特殊”函数接口 API，文件操作就是其中一种类型。实际 上，Linux 提供的系统调用包含以下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程控制：如 fork、clone、exit 、setpriority 等创建、中止、设置进程优先级的操作。&lt;/li&gt;
&lt;li&gt;文件系统控制：如 open、read、write 等对文件的打开、读取、写入操作。&lt;/li&gt;
&lt;li&gt;系统控制：如 reboot、stime、init_module 等重启、调整系统时间、初始化模块的系统操作。&lt;/li&gt;
&lt;li&gt;内存管理：如 mlock、mremap 等内存页上锁重、映射虚拟内存操作。&lt;/li&gt;
&lt;li&gt;网络管理：如 sethostname、gethostname 设置或获取本主机名操作。&lt;/li&gt;
&lt;li&gt;socket 控制：如 socket、bind、send 等进行 TCP、UDP 的网络通讯操作。&lt;/li&gt;
&lt;li&gt;用户管理：如 setuid、getuid 等设置或获取用户 ID 的操作。&lt;/li&gt;
&lt;li&gt;进程间通信：包含信号量、管道、共享内存等操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从逻辑上来说，系统调用可被看成是一个 Linux 内核与用户空间程序交互的中间人，它把用户进程的请求传达给内核，待内核把请求处理完毕后再将处理结果送回给用户空间。它的存在就是为了对用户空间与内核空间进行隔离，要求用户通过给定的方式访问系统资源，从 而达到保护系统的目的。&lt;/p&gt;
&lt;p&gt;也就是说，我们心心念念的 Linux 应用程序与硬件驱动程序之间，就是各种各样的系统调用，所以无论出于何种目的，系统调用是学习 Linux 开发绕不开的话题。&lt;/p&gt;
&lt;p&gt;接下来通过「&lt;strong&gt;文件操作&lt;/strong&gt;」的两个实验，来演示使用「&lt;strong&gt;C 标准库&lt;/strong&gt;」与「&lt;strong&gt;系统调用&lt;/strong&gt;」方式的差异。&lt;/p&gt;
&lt;h2&gt;File Ops｜C Standard Lib&lt;/h2&gt;
&lt;p&gt;本小节讲解使用通用的 C 标准库接口访问文件，标准库实际是对系统调用再次进行了封装。使用 C 标准库编写的代码，能方便地在不同的系统上移植。&lt;/p&gt;
&lt;p&gt;例如 Windows 系统打开文件操作的系统 API 为 &lt;code&gt;OpenFile&lt;/code&gt;，Linux 则为 &lt;code&gt;open&lt;/code&gt;，C 标准库都把它们封装为 &lt;code&gt;fopen&lt;/code&gt;，Windows 下的 C 库会通过 fopen 调用 OpenFile 函数实现操作，而 Linux 下则通过 glibc 调用 open 打开文件。用户代码如果使用 fopen，那么只要根据不同的系统重新编译程序即可，而不需要修改对应的代码（代码可移植性）。&lt;/p&gt;
&lt;p&gt;在开发时，遇到不熟悉的库函数或系统调用，要善用 &lt;code&gt;man&lt;/code&gt; 手册，而不要老是从网上查找。C 标准库提供的常用文件操作简介如下：&lt;/p&gt;
&lt;h3&gt;1. fopen()&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
FILE *fopen(const char *pathname, const char *mode);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pathname&lt;/code&gt; 参数用于指定要打开或创建的文件名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mode&lt;/code&gt; 参数用于指定文件的打开方式，注意该参数是一个字符串，输入时需要带双引号：
&lt;ul&gt;
&lt;li&gt;“r”：以只读方式打开，文件指针位于文件的开头。&lt;/li&gt;
&lt;li&gt;“r+”：以读和写的方式打开，文件指针位于文件的开头。&lt;/li&gt;
&lt;li&gt;“w”：以写的方式打开，不管原文件是否有内容都把原内容清空掉，文件指针位于文件的开头。&lt;/li&gt;
&lt;li&gt;“w+”： 同上，不过当文件不存在时，前面的“w”模式会返回错误，而此处的“w+”则会创建新文件。&lt;/li&gt;
&lt;li&gt;“a”：以追加内容的方式打开，若文件不存在会创建新文件，文件指针位于文件的末尾。与“w+”的区别是它不会清空原文件的内容而是追加。&lt;/li&gt;
&lt;li&gt;“a+”：以读和追加的方式打开，其它同上。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;fopen 的返回值是 &lt;code&gt;FILE&lt;/code&gt; 类型的文件文件流，当它的值不为 NULL 时表示正常，后续的 fread、fwrite 等函数可通过文件流访问对应的文件。&lt;/p&gt;
&lt;h3&gt;2. fread()&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

// usage
char buffer[1024] = {0};
fread(buffer, sizeof(char), sizeof(buffer), p);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;stream 是使用 &lt;code&gt;fopen&lt;/code&gt; 打开的文件流，&lt;code&gt;fread&lt;/code&gt; 通过它指定要访问的文件，它从该文件中读取 count 项数据，每项的大小为 size，读取到的数据会被存储在 ptr 指向的数组中。fread的返回值为成功读取的项数（项的单位为 size）。&lt;/p&gt;
&lt;h3&gt;3. fwrite()&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
size_t fwrite(void *ptr, size_t size, size_t count, FILE *stream);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的操作与 &lt;code&gt;fread&lt;/code&gt; 相反，把 ptr 数组中的内容写入到 stream 文件流，写入的项数为 count，每项大小为 size，返回值为成功写入的项数（项的单位为 size）。&lt;/p&gt;
&lt;h3&gt;4. fclose()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fclose&lt;/code&gt; 库函数用于关闭指定的文件流，关闭时它会把尚未写到文件的内容都写出。因为标准 库会对数据进行缓冲，所以需要使用 fclose 来确保数据被写出。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;unistd.h&gt;
int close(int fd);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. fflush()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fflush&lt;/code&gt; 函数用于把尚未写到文件的内容立即写出。常用于确保前面操作的数据被写 入到磁盘上。&lt;code&gt;fclose&lt;/code&gt; 函数本身也包含了 fflush 的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
int fflush(FILE *stream);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. fseek()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fseek&lt;/code&gt; 函数用于设置下一次读写函数操作的位置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
int fseek(FILE *stream, long offset, int whence);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的 offset 参数用于指定位置，whence 参数则定义了 offset 的意义，whence 的可取值如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SEEK_SET&lt;/code&gt;：offset 是一个绝对位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SEEK_END&lt;/code&gt;：offset 是以文件尾为参考点的相对位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SEEK_CUR&lt;/code&gt;：offset 是以当前位置为参考点的相对位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7. Usage&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;string.h&gt;

//要写入的字符串
const char buf[] = &quot;filesystem_test:Hello World!\n&quot;;
//文件描述符
FILE *fp;
char str[100];

int main(void)
{
   //创建一个文件
   fp = fopen(&quot;filesystem_test.txt&quot;, &quot;w+&quot;);
   //正常返回文件指针
   //异常返回NULL
   if(NULL == fp){
      printf(&quot;Fail to Open File\n&quot;);
      return 0;
   }
   //将buf的内容写入文件
   //每次写入1个字节，总长度由strlen给出
   fwrite(buf, 1, strlen(buf), fp);

   //写入Embedfire
   //每次写入1个字节，总长度由strlen给出
   fwrite(&quot;Embedfire\n&quot;, 1, strlen(&quot;Embedfire\n&quot;),fp);

   //把缓冲区的数据立即写入文件
   fflush(fp);

   //此时的文件位置指针位于文件的结尾处，使用fseek函数使文件指针回到文件头
   fseek(fp, 0, SEEK_SET);

   //从文件中读取内容到str中
   //每次读取100个字节，读取1次
   fread(str, 100, 1, fp);

   printf(&quot;File content:\n%s \n&quot;, str);

   fclose(fp);

   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;File Ops｜System Calls&lt;/h2&gt;
&lt;p&gt;Linux 提供的文件操作系统调用常用的有 &lt;code&gt;open&lt;/code&gt;、&lt;code&gt;write&lt;/code&gt;、&lt;code&gt;read&lt;/code&gt;、&lt;code&gt;lseek&lt;/code&gt;、&lt;code&gt;close&lt;/code&gt; 等。&lt;/p&gt;
&lt;h3&gt;1. open()&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;sys/types.h&gt;
#include &amp;#x3C;sys/stat.h&gt;
#include &amp;#x3C;fcntl.h&gt;
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

// usage-1
fd = ::open(filename, O_RDWR | O_DIRECT | O_CREAT, 0666);
// usage-2
#include &amp;#x3C;fcntl.h&gt;
...
int fd;
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
char *filename = &quot;/tmp/file&quot;;
...
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, mode);
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Linux 使用 &lt;code&gt;open&lt;/code&gt; 函数来打开文件，并返回该文件对应的文件描述符。函数参数的具体说明如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pathname&lt;/code&gt;：要打开或创建的文件名；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flag&lt;/code&gt;：指定文件的打开方式，具体有以下参数，见下表 flag 参数值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 标志位   | 含义                                                         |
| -------- | ------------------------------------------------------------ |
| O_RDONLY | 以只读的方式打开文件，该参数与 O_WRONLY 和 O_RDWR 只能三选一 |
| O_WRONLY | 以只写的方式打开文件                                         |
| O_RDWR   | 以读写的方式打开文件                                         |
| O_CREAT  | 创建一个新文件                                               |
| O_APPEND | 将数据写入到当前文件的结尾处                                 |
| O_TRUNC  | 如果pathname文件存在，则清除文件内容                         |&lt;/p&gt;
&lt;p&gt;除此之外，还有 O_DIRECT 之类的，可以查 man 手册：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202412292243760.png&quot; alt=&quot;image-20241229224331602&quot;&gt;&lt;/p&gt;
&lt;p&gt;C 库函数 &lt;code&gt;fopen&lt;/code&gt; 的 mode 参数与系统调用 &lt;code&gt;open&lt;/code&gt; 的 flags 参数有如下表中的等价关系。&lt;/p&gt;
&lt;p&gt;| fopen 的 mode 参数 | open 的 flags 参数              |
| ------------------ | ------------------------------- |
| r                  | O_RDONLY                        |
| w                  | O_WRONLY | O_CREAT | O_TRUNC  |
| a                  | O_WRONLY | O_CREAT | O_APPEND |
| r+                 | O_RDWR                          |
| w+                 | O_RDWR | O_CREAT | O_TRUNC    |
| a+                 | O_RDWR | O_CREAT | O_APPEND   |&lt;/p&gt;
&lt;p&gt;⚠️ &lt;code&gt;mode&lt;/code&gt;：当 &lt;code&gt;open&lt;/code&gt; 函数的 flag 值设置为 &lt;em&gt;&lt;strong&gt;O_CREAT&lt;/strong&gt;&lt;/em&gt; 时，必须使用 mode 参数来设置文件 与用户相关的权限。mode 可用的权限如下表所示，表中各个参数可使用 &quot;|&quot; 来组合；或者直接用数字表示更快，比如 &lt;code&gt;0666&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;| \          | 标志位  | 含义                                     |
| ---------- | ------- | ---------------------------------------- |
| 当前用户   | S_IRUSR | 用户拥有读权限                           |
| \          | S_IWUSR | 用户拥有写权限                           |
| \          | S_IXUSR | 用户拥有执行权限                         |
| \          | S_IRWXU | 用户拥有读、写、执行权限                 |
| 当前用户组 | S_IRGRP | 当前用户组的其他用户拥有读权限           |
| \          | S_IWGRP | 当前用户组的其他用户拥有写权限           |
| \          | S_IXGRP | 当前用户组的其他用户拥有执行权限         |
| \          | S_IRWXG | 当前用户组的其他用户拥有读、写、执行权限 |
| 其他用户   | S_IROTH | 其他用户拥有读权限                       |
| \          | S_IWOTH | 其他用户拥有写权限                       |
| \          | S_IXOTH | 其他用户拥有执行权限                     |
| \          | S_IROTH | 其他用户拥有读、写、执行权限             |&lt;/p&gt;
&lt;h3&gt;2. read()&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;unistd.h&gt;
ssize_t read(int fd, void *buf, size_t count);

// usage
#include &amp;#x3C;sys/types.h&gt;
#include &amp;#x3C;unistd.h&gt;
char buf[20];
size_t nbytes;
ssize_t bytes_read;
int fd;
nbytes = sizeof(buf);
bytes_read = read(fd, buf, nbytes);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;read&lt;/code&gt; 函数用于从文件中读取若干个字节的数据，保存到数据缓冲区 buf 中，并返 回实际读取的字节数，具体函数参数如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fd：文件对应的文件描述符，可以通过 fopen 函数获得。另外，当一个程序运行时，Linux 默认有 0、1、2 这三个已经打开的文件描述符，分别对应了标准输入、标准输出、标准错误输出，即可以直接访问这三种文件描述符&lt;/li&gt;
&lt;li&gt;buf：指向数据缓冲区的指针&lt;/li&gt;
&lt;li&gt;count：读取多少个字节的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. write()&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;unistd.h&gt;
ssize_t write(int fd, const void *buf, size_t count);

// usage
#include &amp;#x3C;sys/types.h&gt;
#include &amp;#x3C;string.h&gt;
char buf[20];
size_t nbytes;
ssize_t bytes_written;
int fd;
strcpy(buf, &quot;This is a test\n&quot;);
nbytes = strlen(buf);
bytes_written = write(fd, buf, nbytes);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;write 函数用于往文件写入内容，并返回实际写入的字节长度，具体函数参数如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fd：文件对应的文件描述符，可以通过 fopen 函数获得&lt;/li&gt;
&lt;li&gt;buf：指向数据缓冲区的指针&lt;/li&gt;
&lt;li&gt;count：往文件中写入多少个字节&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. close()&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int close(int fd);

// usage
#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;unistd.h&gt;
#include &amp;#x3C;stdlib.h&gt;
#define LOCKFILE &quot;/etc/ptmp&quot;
int pfd;
FILE *fpfd;
if ((fpfd = fdopen (pfd, &quot;w&quot;)) == NULL) {
    close(pfd);
    unlink(LOCKFILE);
    exit(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. lseek()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;lseek&lt;/code&gt; 函数可以用与设置文件指针的位置，并返回文件指针相对于文件头的位置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;off_t lseek(int fd, off_t offset, int whence);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的用法与 &lt;code&gt;flseek&lt;/code&gt; 一样，其中的 offset 参数用于指定位置，whence 参数则定义了 offset 的意义，whence 的可取值如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SEEK_SET&lt;/code&gt;：offset 是一个绝对位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SEEK_END&lt;/code&gt;：offset 是以文件尾为参考点的相对位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SEEK_CUR&lt;/code&gt;：offset 是以当前位置为参考点的相对位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Usage&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;sys/stat.h&gt;
#include &amp;#x3C;unistd.h&gt;
#include &amp;#x3C;fcntl.h&gt;
#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;string.h&gt;

//文件描述符
int fd;
char str[100];


int main(void)
{
   //创建一个文件
   fd = open(&quot;testscript.sh&quot;, O_RDWR|O_CREAT|O_TRUNC, S_IRWXU);
   //文件描述符fd为非负整数
   if(fd &amp;#x3C; 0){
      printf(&quot;Fail to Open File\n&quot;);
      return 0;
   }
   //写入字符串pwd
   write(fd, &quot;pwd\n&quot;, strlen(&quot;pwd\n&quot;));

   //写入字符串ls
   write(fd, &quot;ls\n&quot;, strlen(&quot;ls\n&quot;));

   //此时的文件指针位于文件的结尾处，使用lseek函数使文件指针回到文件头
   lseek(fd, 0, SEEK_SET);

   //从文件中读取100个字节的内容到str中，该函数会返回实际读到的字节数
   read(fd, str, 100);

   printf(&quot;File content:\n%s \n&quot;, str);

   close(fd);

   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common header files&lt;/h2&gt;
&lt;p&gt;我们常常会用到以下头文件，此处进行简单说明，若想查看具体的头文件内容，使用 &lt;code&gt;locate&lt;/code&gt; 命令找到该文件目录后打开即可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;头文件 &lt;em&gt;stdio.h&lt;/em&gt;：C 标准输入与输出（standard input &amp;#x26; output）头文件，我们经常使用的打印函数 &lt;code&gt;printf&lt;/code&gt; 函数就位于该头文件中。&lt;/li&gt;
&lt;li&gt;头文件 &lt;em&gt;stdlib.h&lt;/em&gt;：C 标准库（standard library）头文件，该文件包含了常用的 &lt;code&gt;malloc&lt;/code&gt; 函数、&lt;code&gt;free&lt;/code&gt; 函数。&lt;/li&gt;
&lt;li&gt;头文件 &lt;em&gt;sys/stat.h&lt;/em&gt;：包含了关于文件权限定义，如 S_IRWXU、S_IWUSR，以 及函数 &lt;code&gt;fstat&lt;/code&gt; 用于查询文件状态。涉及系统调用文件相关的操作，通常都需要用到 sys/stat.h 文件。&lt;/li&gt;
&lt;li&gt;头文件 &lt;em&gt;unistd.h&lt;/em&gt;：UNIX C 标准库头文件，unix，linux 系列的操 作系统相关的 C 库，定义了 unix 类系统 POSIX 标准的符号常量头文件，比如 Linux 标准的输入文件描述符（&lt;code&gt;STDIN&lt;/code&gt;），标准输出文件描述符（&lt;code&gt;STDOUT&lt;/code&gt;），还有 &lt;code&gt;read&lt;/code&gt;、&lt;code&gt;write&lt;/code&gt; 等系统调用的声明。&lt;/li&gt;
&lt;li&gt;头文件 &lt;em&gt;fcntl.h&lt;/em&gt;：unix 标准中通用的头文件，其中包含的相关函数有 &lt;code&gt;open&lt;/code&gt;，&lt;code&gt;fcntl&lt;/code&gt;，&lt;code&gt;close&lt;/code&gt; 等操作。&lt;/li&gt;
&lt;li&gt;头文件 &lt;em&gt;sys/types.h&lt;/em&gt;：包含了 Unix/Linux 系统的数据类型的头文件，常用的有 &lt;code&gt;size_t&lt;/code&gt;，&lt;code&gt;time_t&lt;/code&gt;，&lt;code&gt;pid_t&lt;/code&gt; 等类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例代码中的开头包含了一系列 Linux 系统常用的头文件。今后学习 Linux 的过程中，我们可能会接触各种各样的头文件，因此了解一下 Linux 中头文件的用法十分有必要。&lt;/p&gt;
&lt;p&gt;在 linux 中，大部分的头文件在系统的 &lt;strong&gt;“/usr/include”&lt;/strong&gt; 目录下可以找到，它是&lt;strong&gt;系统自带的 GCC 编译器默认的头文件目录&lt;/strong&gt;，如下图所示，如果把该目录下的 &lt;code&gt;stdio.h&lt;/code&gt; 文件删除掉或更改名字（想尝试请备份），那么使用 GCC 编译 hello world 的程序会因为找不到 stdio.h 文件而报错。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;locate 查找&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ locate sys/stat.h
/usr/include/x86_64-linux-gnu/sys/stat.h


$ ls -al /usr/include/x86_64-linux-gnu/sys
total 496
drwxr-xr-x  3 root root 12288 Jun 11  2023 .
drwxr-xr-x 12 root root  4096 Dec 11 06:30 ..
-rw-r--r--  1 root root  3302 Jul  6  2022 acct.h
-rw-r--r--  1 root root  1260 Jul  6  2022 auxv.h
-rw-r--r--  1 root root    86 Jul  6  2022 bitypes.h
-rw-r--r--  1 root root 26600 Jul  6  2022 cdefs.h
-rw-r--r--  1 root root  3576 Jul  6  2022 debugreg.h
-rw-r--r--  1 root root   922 Jul  6  2022 dir.h
-rw-r--r--  1 root root  1024 Jul  6  2022 elf.h
-rw-r--r--  1 root root  5076 Jul  6  2022 epoll.h
-rw-r--r--  1 root root    19 Jul  6  2022 errno.h
-rw-r--r--  1 root root  1400 Jul  6  2022 eventfd.h
-rw-r--r--  1 root root  1292 Jul  6  2022 fanotify.h
-rw-r--r--  1 root root    19 Jul  6  2022 fcntl.h
-rw-r--r--  1 root root  1675 Jul  6  2022 file.h
-rw-r--r--  1 root root  1188 Jul  6  2022 fsuid.h
-rw-r--r--  1 root root  6210 Jul  6  2022 gmon.h
-rw-r--r--  1 root root  2577 Jul  6  2022 gmon_out.h
-rw-r--r--  1 root root  3901 Jul  6  2022 inotify.h
-rw-r--r--  1 root root  2027 Jul  6  2022 ioctl.h
-rw-r--r--  1 root root  5086 Jul  6  2022 io.h
-rw-r--r--  1 root root  1462 Jul  6  2022 ipc.h
-rw-r--r--  1 root root  1112 Jul  6  2022 kd.h
-rw-r--r--  1 root root  1204 Jul  6  2022 klog.h
-rw-r--r--  1 root root  5552 Jul  6  2022 mman.h
-rw-r--r--  1 root root  5706 Jul  6  2022 mount.h
-rw-r--r--  1 root root  2623 Jul  6  2022 msg.h
-rw-r--r--  1 root root 11111 Jul  6  2022 mtio.h
-rw-r--r--  1 root root  3149 Jul  6  2022 param.h
-rw-r--r--  1 root root   923 Jul  6  2022 pci.h
-rw-r--r--  1 root root  1127 Jul  6  2022 perm.h
-rw-r--r--  1 root root  2723 Jul  6  2022 personality.h
drwxr-xr-x  2 root root  4096 Jun 11  2023 platform
-rw-r--r--  1 root root  3025 Jul  6  2022 poll.h
-rw-r--r--  1 root root  1795 Jul  6  2022 prctl.h
-rw-r--r--  1 root root  4338 Jul  6  2022 procfs.h
-rw-r--r--  1 root root  1959 Jul  6  2022 profil.h
-rw-r--r--  1 root root  6282 Jul  6  2022 ptrace.h
-rw-r--r--  1 root root 19539 Jul  6  2022 queue.h
-rw-r--r--  1 root root  5173 Jul  6  2022 quota.h
-rw-r--r--  1 root root  1471 Jul  6  2022 random.h
-rw-r--r--  1 root root  1182 Jul  6  2022 raw.h
-rw-r--r--  1 root root  1633 Jul  6  2022 reboot.h
-rw-r--r--  1 root root  1827 Jul  6  2022 reg.h
-rw-r--r--  1 root root  4034 Jul  6  2022 resource.h
-rw-r--r--  1 root root  6715 Jul  6  2022 rseq.h
-rw-r--r--  1 root root  5039 Jul  6  2022 select.h
-rw-r--r--  1 root root  2660 Jul  6  2022 sem.h
-rw-r--r--  1 root root  1806 Jul  6  2022 sendfile.h
-rw-r--r--  1 root root  2131 Jul  6  2022 shm.h
-rw-r--r--  1 root root  1714 Jul  6  2022 signalfd.h
-rw-r--r--  1 root root    20 Jul  6  2022 signal.h
-rw-r--r--  1 root root  1182 Jul  6  2022 single_threaded.h
-rw-r--r--  1 root root 12382 Jul  6  2022 socket.h
-rw-r--r--  1 root root   141 Jul  6  2022 socketvar.h
-rw-r--r--  1 root root    29 Jul  6  2022 soundcard.h
-rw-r--r--  1 root root  2094 Jul  6  2022 statfs.h
-rw-r--r--  1 root root 13767 Jul  6  2022 stat.h
-rw-r--r--  1 root root  2821 Jul  6  2022 statvfs.h
-rw-r--r--  1 root root  1593 Jul  6  2022 swap.h
-rw-r--r--  1 root root  1256 Jul  6  2022 syscall.h
-rw-r--r--  1 root root  1518 Jul  6  2022 sysinfo.h
-rw-r--r--  1 root root  7777 Jul  6  2022 syslog.h
-rw-r--r--  1 root root  2103 Jul  6  2022 sysmacros.h
-rw-r--r--  1 root root    74 Jul  6  2022 termios.h
-rw-r--r--  1 root root  1155 Jul  6  2022 timeb.h
-rw-r--r--  1 root root  9139 Jul  6  2022 time.h
-rw-r--r--  1 root root  2583 Jul  6  2022 timerfd.h
-rw-r--r--  1 root root  1597 Jul  6  2022 times.h
-rw-r--r--  1 root root  2839 Jul  6  2022 timex.h
-rw-r--r--  1 root root  2499 Jul  6  2022 ttychars.h
-rw-r--r--  1 root root  3568 Jul  6  2022 ttydefaults.h
-rw-r--r--  1 root root  5713 Jul  6  2022 types.h
-rw-r--r--  1 root root  5842 Jul  6  2022 ucontext.h
-rw-r--r--  1 root root  6796 Jul  6  2022 uio.h
-rw-r--r--  1 root root  1453 Jul  6  2022 un.h
-rw-r--r--  1 root root    20 Jul  6  2022 unistd.h
-rw-r--r--  1 root root  5208 Jul  6  2022 user.h
-rw-r--r--  1 root root  2481 Jul  6  2022 utsname.h
-rw-r--r--  1 root root   161 Jul  6  2022 vfs.h
-rw-r--r--  1 root root  1880 Jul  6  2022 vlimit.h
-rw-r--r--  1 root root  1199 Jul  6  2022 vm86.h
-rw-r--r--  1 root root    22 Jul  6  2022 vt.h
-rw-r--r--  1 root root  6233 Jul  6  2022 wait.h
-rw-r--r--  1 root root  4275 Jul  6  2022 xattr.h
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Linux File System Evolution｜FAST&apos;13 Paper&lt;/h2&gt;
&lt;p&gt;研究涉及六个主要的Linux文件系统：Ext3、Ext4、XFS、Btrfs、ReiserFS和JFS。这些文件系统在功能、设计、实现和开发团队上都有所不同。研究团队检查了Linux 2.6系列中每个文件系统的每个补丁，通过理解每个补丁的意图并对其进行分类，从而深入量化地洞察文件系统开发过程。研究结果回答了诸如“大多数补丁是什么？”“常见的错误类型是什么？”等问题，并提供了对当前文件系统开发和维护中常见方法和问题的新的见解。&lt;/p&gt;
&lt;p&gt;主要观察结果包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;近50%的补丁是维护补丁&lt;/strong&gt;，反映了保持代码简单和可维护所需的持续重构工作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;剩余的主要类别是错误修复&lt;/strong&gt;（近40%，约1800个错误），显示了实现“正确”版本所需的努力。&lt;/li&gt;
&lt;li&gt;错误数量并没有随时间减少，即使对于稳定的文件系统也是如此。&lt;/li&gt;
&lt;li&gt;进一步分析错误类别，语义错误（需要理解文件系统语义才能找到或修复的错误）是主导错误类别（超过50%的所有错误）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发错误是第二常见的&lt;/strong&gt;（约占错误总数的20%），比用户级软件更为普遍。&lt;/li&gt;
&lt;li&gt;内存错误和错误代码处理错误也较为常见，大多数错误代码错误完全忽略了错误。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此外，研究还发现，大多数错误（研究中的错误）会导致崩溃或数据损坏，这些结果在语义、并发、内存和错误代码错误中都成立。研究还发现，B树（许多文件系统中用于可扩展性的结构）的错误数量相对较少。大约40%的错误发生在错误处理路径上，文件系统在尝试响应失败的内存分配、I/O错误或其他意外情况时，很容易犯下进一步的错误，如状态更新不正确和资源释放遗漏。&lt;/p&gt;
&lt;p&gt;性能和可靠性补丁也占有一定比例，分别占8%和7%。性能技术相对常见和广泛，例如去除不必要的I/O或降低写锁到读锁。约四分之一的性能补丁减少了同步开销。与性能技术相比，可靠性技术的添加似乎更加随意。&lt;/p&gt;
&lt;p&gt;研究的另一个成果是一个公开的文件系统补丁注释数据集，供文件系统开发者、系统语言设计者和错误检测工具构建者进一步研究。研究通过一个案例研究展示了这个数据集的实用性，特别是搜索数据集以找到所有文件系统中异常常见的错误、性能修复和可靠性技术。&lt;/p&gt;
&lt;h2&gt;A look at the dark history of Linux file systems&lt;/h2&gt;
&lt;h3&gt;Linus 又发飙了，这一次是 ext4&lt;/h3&gt;
&lt;p&gt;如果你订阅了 Linux Kernel 的 maillist，你一定发现最近 Linus 又爆粗口了，而这次的对象是 ext4 文件系统。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;On Sun, Aug 6, 2017 at 12:27 PM, Theodore Ts&apos;o tytso@mit.edu wrote:
&gt;
&gt; A large number of ext4 bug fixes and cleanups for v4.13&lt;/p&gt;
&lt;p&gt;A couple of these appear to be neither cleanups nor fixes. And a lot
of them appear to be very recent.&lt;/p&gt;
&lt;p&gt;I&apos;ve pulled this, but if I hear about problems, ext4 is going to be on
my shit-list, and you&apos;d better be a &lt;em&gt;lot&lt;/em&gt; more careful about pull
requests. Because this is not ok.&lt;/p&gt;
&lt;p&gt;Linus&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而这已经不是 Linus 第一次对 ext4 文件系统表达不满了。&lt;/p&gt;
&lt;p&gt;尽管 ext4 文件系统已经发布了多年，也被广泛应用于桌面及服务器，但关于 ext4 存在可能丢数据的 Bug 报告就一直没有中断过。例如在 2012 年的一封&lt;a href=&quot;https://lkml.org/lkml/2012/10/23/690&quot;&gt;邮件&lt;/a&gt;中，Theodore Ts&apos;o 报告了一次严重的 Bug，已经影响了部分 Linux 稳定版本的内核。&lt;/p&gt;
&lt;p&gt;如果你持续关注文件系统或内核技术，你一定注意过这样一篇文章：&lt;a href=&quot;https://lwn.net/Articles/685182/&quot;&gt;Fuzzing filesystem with AFL&lt;/a&gt;。Vegard Nossum 和 Quentin Casasnovas 在 2016 年将用户态的 Fuzzing 工具 AFL（American Fuzzing Lop）迁移到内核态，并针对文件系统进行了测试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202412300852927.png&quot; alt=&quot;image-20241230085245554&quot;&gt;&lt;/p&gt;
&lt;p&gt;结果是相当惊人的：Btrfs，作为 SLES（SUSE Linux Enterprise Server）的默认文件系统，仅在测试中坚持了 5 秒钟就挂了。而 ext4 坚持时间最长，但也仅有 2 个小时而已。&lt;/p&gt;
&lt;p&gt;这个结果给我们敲响了警钟，&lt;strong&gt;Linux 文件系统并没有我们想象中的那么稳定&lt;/strong&gt;。而事实上，在 Fuzz 测试下坚持时间长短仅仅体现出文件系统稳定性的一部分。数据可靠性，才是文件系统中最核心的属性。然而 Linux 文件系统社区的开发者往往都把注意力放在了性能，以及高级功能的开发上，而忽略了可靠性。&lt;/p&gt;
&lt;p&gt;带大家回顾一下 Linux 文件系统的黑历史，希望能够警醒大家，不要过分相信和依赖文件系统。同时，在使用文件系统构建应用时，也需要采用正确的“姿势”。&lt;/p&gt;
&lt;h3&gt;POSIX，一个奇葩的标准&lt;/h3&gt;
&lt;p&gt;谈到 Linux 文件系统，不得不提到 &lt;a href=&quot;https://en.wikipedia.org/wiki/POSIX&quot;&gt;POSIX（Portable Operating System Interface）&lt;/a&gt;，这样一个奇葩的标准。而开发者对于 POSIX 的抱怨，可谓是罄竹难书。&lt;/p&gt;
&lt;p&gt;作为一个先有实现，后有标准的 POSIX，在文件系统接口上的定义，可谓是相当的“简洁”。尤其当系统发生 crash 后，对于文件系统应有的行为，更是完全空白，这留给了文件系统开发者足够大的“想象空间”。也就是说，如果一个 Linux 文件系统在系统发生崩溃重启后，整个文件系统的内容都不见了，也是“符合标准”的。&lt;/p&gt;
&lt;p&gt;而事实上，类似的事情确实发生过：在 2015 年，ChromeOS 的开发者曾报告了一个 ext4 的问题，有可能导致 Chrome 发生崩溃。而来自 ext4 开发者的回答是，&lt;a href=&quot;https://issuetracker.google.com/issues/172227346?pli=1&quot;&gt;“Working As Intended”&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;在历史上，不断有人尝试给文件系统提供更加严谨的 Consistency（一致性）定义，尤其是 Crash-Consistency（故障后的一致性）。到目前为止，尽管 POSIX 也经历了几个版本，但关于文件系统接口的定义，还是那个老样子。而 POSIX 标准，也是造成了文件系统各种问题的一个很重要的因素。关于各种一致性的定义，我们后面也会有文章专门进行介绍。&lt;/p&gt;
&lt;h3&gt;文件系统的黑历史&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;《A Study of Linux File System Evolution》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;文件系统一直有着光辉的发展历史，也孕育了许多伟大的 Linux 内核贡献者。从最早的 FFS，到经典的 ext2/ext3/ext4，再到拥有黑科技的 Btrfs，XFS，BCacheFS 等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202412300857784.png&quot; alt=&quot;image-20241230085703571&quot;&gt;&lt;/p&gt;
&lt;p&gt;然而软件开发的过程，当然不是一帆风顺的。威斯康辛大学麦迪逊分校的研究者曾在 FAST &apos;13 上发表过一篇著名的论文&lt;a href=&quot;https://www.usenix.org/system/files/login/articles/03_lu_010-017_final.pdf&quot;&gt;《A Study of Linux File System Evolution》&lt;/a&gt;。文章对 8 年中，Linux 社区与文件系统相关的 5079 个 Patch 进行了统计和分析。从其数据中可以看出，有将近 40% 的文件系统相关的 Patch 属于 Bugfix 类型。换句话说，每提交两个 Patch，就有可能需要一个 Patch 用于 Bugfix。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202412300858522.png&quot; alt=&quot;image-20241230085816284&quot;&gt;&lt;/p&gt;
&lt;p&gt;而文件系统的 Bug 数量并没有随着时间的推移而逐渐收敛，随着新功能不断的加入，Bug 还在持续不断的产生。而 Bug 的集中爆发也往往源于大的功能演进。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202412300859408.png&quot; alt=&quot;image-20241230085918306&quot;&gt;&lt;/p&gt;
&lt;p&gt;而从上图中可以看出，在所有的 Bug 中，有接近 40% 的 Bug 可能导致数据损坏，这还是相当惊人的。&lt;/p&gt;
&lt;p&gt;可以想象，在 Linux 文件系统的代码库中，还隐藏着许多 Bug，在等待着被人们发现。&lt;/p&gt;
&lt;p&gt;哥伦比亚大学文件系统领域著名的专家 Junfeng Yang，曾经在 OSDI &apos;04 上发表了一篇论文&lt;a href=&quot;https://www.usenix.org/legacy/event/osdi04/tech/yang/yang.pdf&quot;&gt;《Using Model Checking to Find Serious File System Errors》&lt;/a&gt;，该论文也是当年 OSDI 的 Best Paper。在这篇论文中，Junfeng Yang 通过 FiSC，一种针对文件系统的 Model Checking 工具，对 ext3，JFS，ReiserFS 都进行了检查，结果共发现了 32 个 Bug。而不同于 AFL，FiSC 发现的 Bug 大部分都会导致数据丢失，而不仅仅是程序崩溃。例如文章中指出了一处 ext3 文件系统的 Bug，该 Bug 的触发原因是在通过 fsck 进行数据恢复时，使用了错误的写入顺序，在 journal replay 的过程中，journal 中的数据还没有持久化到磁盘上之前，就清理了 journal，如果此时发生断电故障，则导致数据永久性丢失。&lt;/p&gt;
&lt;h3&gt;对应用程序开发的影响&lt;/h3&gt;
&lt;p&gt;对于大部分应用程序开发者来说，并不会直接使用文件系统。很多程序员都是面向数据库进行编程，他们的数据大多是存在数据库中的。我们经常想当然的认为，数据库的开发者理应会理解文件系统可能存在的问题，并绕过文件系统的 Bug，帮助我们解决各种问题。然而这只是一种侥幸心理罢了，由于文件系统过于复杂，标准不清晰，即使是专业的数据库的开发人员，也往往无法避开文件系统中所有的问题。&lt;/p&gt;
&lt;p&gt;以 LevelDB，我们最常用的一种单机 Key-Value Store 举例。研究人员分别对 LevelDB 的两个版本，1.10 和 1.15 进行了测试，分别发现了 10 个和 6 个不同程度的漏洞。其中 1.10 版本有 1 个漏洞可能导致数据丢失，5 个漏洞导致数据库无法打开，4 个漏洞导致数据库读写错误。而 1.15 版本分别有 2 个漏洞导致数据库无法打开，2 个漏洞导致数据库读写错误。&lt;/p&gt;
&lt;p&gt;这些问题，大部分源自应用开发者对文件系统错误的假设。也就是说，他们以为文件系统可以保证的特性，而事实上并不能得到保证。而这些特性，也都是 POSIX 标准中未曾明确定义的。&lt;/p&gt;
&lt;p&gt;这里举个例子：&lt;strong&gt;Append atomicity，追加写原子性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;向文件中追加写入，并不意味着是原子性的。如前文 ChromeOS 开发者遇到的 ext4 的问题，其根本原因，就是假设 ext4 文件系统是保证追加写原子性的。在这封邮件中，开发者提供了一个可以复现问题的步骤。假设文件中已经有 2522 字节的数据，再追加写入 2500 字节的数据，文件大小本应为 5022 字节。而如果在追加写的过程中，遇到系统崩溃，在系统恢复后，文件的大小可能是 4096 字节，而非 5022 字节，而文件的内容，也可能是垃圾数据，无法被程序正确识别。&lt;/p&gt;
&lt;p&gt;LevelDB 同样也假设了文件系统具有追加写的原子性，前面提到的一些漏洞就源于此。&lt;/p&gt;
&lt;p&gt;而这仅仅是冰山一角。单单关于文件系统写入数据的原子性，就有包括：单 sector 覆盖写，单 sector 追加写，单 block 覆盖写，单 block 追加写，多 block 追加写等等。而对于不同类型的文件系统，甚至同一个文件系统的使用不同参数，对于原子性都可能具有不同范围的支持。再考虑到 POSIX 提供的其他接口，包括 &lt;code&gt;creat&lt;/code&gt;，&lt;code&gt;rename&lt;/code&gt;，&lt;code&gt;unlink&lt;/code&gt;，&lt;code&gt;truncate&lt;/code&gt; 等等。这使得开发应用系统，尤其是数据库系统，变得非常复杂。&lt;/p&gt;
&lt;h2&gt;开发者的正确姿势是什么&lt;/h2&gt;
&lt;p&gt;这里我们提供一些建议，希望能够帮助大家尽量少的踩坑。&lt;/p&gt;
&lt;p&gt;首先，对于大部分应用程序员来说，应尽可能选择使用成熟的数据库，而非直接操作文件。尽管如前文所说，在复杂的文件系统面前，数据库也无法幸免于难，但数据库开发者掌握的关于文件系统的知识，还是远远强于普通开发者的。数据库也通常提供了数据恢复工具，以及备份工具。这避免了开发者重新造轮子，也极大的减轻了灾难发生后可能带来的影响。&lt;/p&gt;
&lt;p&gt;而对于单机数据库，分布式数据库，以及分布式存储的开发者来说，我们的建议是尽量避免直接使用文件系统，尽可能多的直接使用裸设备，这避免了很多可能引起问题的接口，例如 &lt;code&gt;creat&lt;/code&gt;，&lt;code&gt;rename&lt;/code&gt;，&lt;code&gt;truncate&lt;/code&gt; 等。例如 &lt;a href=&quot;https://www.smartx.com/&quot;&gt;SmartX&lt;/a&gt; 在设计和实现分布式存储时，就直接使用裸设备。&lt;/p&gt;
&lt;p&gt;如果必须要使用文件系统，也要使用尽量简单的 IO 模型，避免多线程，异步的操作。同时，一定要在设计的过程中，把对于文件系统操作的模型抽象出来，并画成步骤图，这里我们推荐 &lt;a href=&quot;https://app.diagrams.net/&quot;&gt;draw.io&lt;/a&gt;，一个非常不错的免费画图工具。要假设每一个步骤都可能失败，每一个步骤失败后，都可能产生垃圾数据，要提前设计好数据校验以及处理垃圾数据的方式。如果步骤之间有存在依赖关系，一定要在执行下一步之前，调用 fsync()，以保证数据被持久化到磁盘中。&lt;/p&gt;
&lt;p&gt;最后，设计和实现完成后，在单元测试和集成测试的过程中，也一定要增加故障测试。例如在单元测试中，通过 mock 的方式模拟 IO 故障，在集成测试中，可以加入随机 kill 进程，随机重启服务器的测试用例，也可以通过 &lt;a href=&quot;https://www.kernel.org/doc/Documentation/device-mapper/delay.txt&quot;&gt;dm-delay&lt;/a&gt;，&lt;a href=&quot;https://www.kernel.org/doc/Documentation/device-mapper/dm-flakey.txt&quot;&gt;dm-flakey&lt;/a&gt; 等工具进行磁盘故障模拟。&lt;/p&gt;
&lt;p&gt;看了这么多黑历史，真的是三观都毁掉了。而事实上，我们每天确实都生活在这些危机中。&lt;/p&gt;
&lt;p&gt;这里要强调的是，我并不是想诋毁 Linux 文件系统，相反，我们非常感谢 Linux 内核开发者在文件系统方面做出的贡献。但同时，由于系统的复杂度所带来的严重问题也是无法回避的。在 Linux 文件系统的代码中，必然还存在着很多未被发现的严重 Bug，开发者和研究人员也从来没有停止过寻找 Bug 的努力。而随着新功能不断地加入，新的 Bug 也在不断的产生。我们多一些这方面的思考和谨慎，并不是什么坏事。&lt;/p&gt;
&lt;hr&gt;</content:encoded><h:img src="/_astro/202501222353334.CloU6lpF.jpeg"/><enclosure url="/_astro/202501222353334.CloU6lpF.jpeg"/></item><item><title>全闪存阵列｜mdadm 实操</title><link>https://coooredump.github.io/blog/system-architecture/madam-for-all-flash-array</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/madam-for-all-flash-array</guid><description>要将 4 个 SSD 组成一个 All-flash Array，可以通过 RAID 技术来完成，常见的方式是使用 Linux 软件 RAID（mdadm）来配置一个 RAID 阵列。这些 SSD 可以通过不同的 RAID 模式（如 RAID 0、RAID 1、RAID 5、RAID 10 等）组合在一起，具体选择哪种 RAID 取决于你对性能、冗余和容错的需求。</description><pubDate>Sat, 28 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;全闪存阵列搭建｜&lt;code&gt;mdadm&lt;/code&gt; 实操&lt;/h2&gt;
&lt;p&gt;用以下 4 个 SSD 组全闪存阵列（All-Flash Array），与组 raid 同法，简单记录下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ lsblk -f
NAME        FSTYPE   LABEL UUID FSAVAIL FSUSE% MOUNTPOINT
loop0       squashfs 			0   100% /snap/core20/2379
loop1       squashfs			0   100% /snap/lxd/24061
loop2       squashfs 			0   100% /snap/snapd/21759
loop3       squashfs 			0   100% /snap/core20/2434
loop4       squashfs 			0   100% /snap/lxd/29619
loop5       squashfs 			0   100% /snap/snapd/23258
sda         ext4           a35cd456-e07a-4d50-8118-1556a18a6971                
sdb         ext4           e2a3bb45-0b9b-4d0c-b9db-192dbc1b507e                
sdc         ext4           39a9734c-bfc2-4a6e-99b5-de18082385f8                
sdd         ext4           5f8065ab-88e5-47a5-9729-c1b3c286bf73                
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要将 4 个 SSD 组成一个 &lt;strong&gt;All-flash Array&lt;/strong&gt;，可以通过 RAID 技术来完成，常见的方式是使用 &lt;strong&gt;Linux 软件 RAID&lt;/strong&gt;（&lt;code&gt;mdadm&lt;/code&gt;）来配置一个 RAID 阵列。这些 SSD 可以通过不同的 RAID 模式（如 RAID 0、RAID 1、RAID 5、RAID 10 等）组合在一起，具体选择哪种 RAID 取决于你对性能、冗余和容错的需求。&lt;/p&gt;
&lt;h3&gt;步骤 1：安装 &lt;code&gt;mdadm&lt;/code&gt; 工具&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;mdadm&lt;/code&gt; 是用于创建和管理 Linux 软件 RAID 阵列的工具。如果你的系统上没有安装 &lt;code&gt;mdadm&lt;/code&gt;，可以使用以下命令进行安装：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ubuntu/Debian 系列&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update
sudo apt install mdadm
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;步骤 2：清除磁盘上的现有数据&lt;/h3&gt;
&lt;p&gt;在创建 RAID 阵列之前，你需要确保所有磁盘上没有任何分区或者已有数据。可以使用 &lt;code&gt;wipefs&lt;/code&gt; 命令清除磁盘上的任何现有文件系统和分区信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo wipefs --all /dev/sda
sudo wipefs --all /dev/sdb
sudo wipefs --all /dev/sdc
sudo wipefs --all /dev/sdd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;步骤 3：创建 RAID 阵列&lt;/h3&gt;
&lt;p&gt;决定你需要哪种 RAID 级别（0、1、5、10）。以下是几种常见 RAID 阵列的说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RAID 0&lt;/strong&gt;（条带化）：提供最高性能，但没有冗余，任何磁盘故障都会导致数据丢失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAID 1&lt;/strong&gt;（镜像）：提供数据冗余，但只使用两个磁盘，容量是最小磁盘大小的两倍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAID 5&lt;/strong&gt;（带奇偶校验的条带化）：提供冗余和良好的性能，至少需要 3 个磁盘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAID 10&lt;/strong&gt;（1+0，镜像 + 条带化）：提供较好的性能和冗余，至少需要 4 个磁盘。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;创建 RAID 0（条带化）阵列&lt;/h4&gt;
&lt;p&gt;如果你的目标是最大化性能，可以选择 RAID 0：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mdadm --create /dev/md0 --level=0 --raid-devices=4 /dev/sda /dev/sdb /dev/sdc /dev/sdd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将创建一个包含 4 个磁盘的 RAID 0 阵列，设备名为 &lt;code&gt;/dev/md0&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;创建 RAID 1（镜像）阵列&lt;/h4&gt;
&lt;p&gt;如果你希望有更高的冗余（需要两对磁盘进行镜像），你可以选择 RAID 1：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mdadm --create /dev/md0 --level=1 --raid-devices=2 /dev/sda /dev/sdb
sudo mdadm --create /dev/md1 --level=1 --raid-devices=2 /dev/sdc /dev/sdd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会创建两个 RAID 1 阵列，每对磁盘形成一个镜像。&lt;/p&gt;
&lt;h4&gt;创建 RAID 5（带奇偶校验的条带化）阵列&lt;/h4&gt;
&lt;p&gt;如果你希望实现性能和冗余的平衡，RAID 5 是一个不错的选择，它提供奇偶校验，能够承受一个磁盘故障：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mdadm --create /dev/md0 --level=5 --raid-devices=4 /dev/sda /dev/sdb /dev/sdc /dev/sdd
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;创建 RAID 10（镜像 + 条带化）阵列&lt;/h4&gt;
&lt;p&gt;RAID 10 提供了较好的性能和冗余，适合需要较高性能和数据保护的应用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mdadm --create /dev/md0 --level=10 --raid-devices=4 /dev/sda /dev/sdb /dev/sdc /dev/sdd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;步骤 4：查看 RAID 阵列状态&lt;/h3&gt;
&lt;p&gt;创建 RAID 阵列后，使用以下命令来检查阵列的状态：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mdadm --detail /dev/md0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将显示 &lt;code&gt;/dev/md0&lt;/code&gt; 阵列的详细信息，包括阵列的健康状态、磁盘的状态等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202412280419697.png&quot; alt=&quot;image-20241228041941123&quot;&gt;&lt;/p&gt;
&lt;h3&gt;步骤 5：格式化 RAID 阵列&lt;/h3&gt;
&lt;p&gt;创建 RAID 阵列后，你需要为其创建文件系统。通常使用 &lt;code&gt;ext4&lt;/code&gt; 或 &lt;code&gt;xfs&lt;/code&gt; 文件系统。以下是格式化 RAID 阵列的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mkfs.ext4 /dev/md0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;步骤 6：挂载 RAID 阵列&lt;/h3&gt;
&lt;p&gt;创建并格式化 RAID 阵列后，你需要将其挂载到文件系统中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mkdir /mnt/raid
sudo mount /dev/md0 /mnt/raid
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;步骤 7：自动挂载 /etc/fstab&lt;/h3&gt;
&lt;p&gt;如果你希望在每次启动时自动挂载 RAID 阵列，可以将其添加到 &lt;code&gt;/etc/fstab&lt;/code&gt; 文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 首先，获取阵列的 UUID
sudo blkid /dev/md0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后编辑 &lt;code&gt;/etc/fstab&lt;/code&gt; 文件并添加以下行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;UUID=&amp;#x3C;uuid_from_blkid&gt; /mnt/raid ext4 defaults 2 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;步骤 8：监控和管理&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;mdadm&lt;/code&gt; 来监控 RAID 阵列的状态，并检查是否有任何磁盘故障或阵列问题。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mdadm --detail /dev/md0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;针对 AFA 的读写放大问题，可以采用以下这条流程测试和监控：&lt;/p&gt;
&lt;h3&gt;✍️ 番外篇：iostat 监测磁盘 I/O｜fio 压测&lt;/h3&gt;
&lt;h4&gt;1️⃣ 使用 &lt;code&gt;iostat&lt;/code&gt; 监控磁盘 I/O&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;iostat&lt;/code&gt; 可以显示磁盘的读写性能，但它并不直接提供写放大倍数。不过你可以通过 &lt;strong&gt;总写入量&lt;/strong&gt; 和 &lt;strong&gt;实际写入量&lt;/strong&gt; 来间接推算。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;iostat -x 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202412280429861.png&quot; alt=&quot;image-20241228042922699&quot;&gt;&lt;/p&gt;
&lt;p&gt;详细信息解释：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;avg-cpu&lt;/strong&gt;：显示 CPU 使用情况的平均值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;%user&lt;/code&gt;：用户空间的 CPU 使用率&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%nice&lt;/code&gt;：以较低优先级运行的进程使用的 CPU 时间百分比&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%system&lt;/code&gt;：内核空间的 CPU 使用率&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%iowait&lt;/code&gt;：等待 I/O 操作完成时的 CPU 空闲时间百分比&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%steal&lt;/code&gt;：虚拟化环境中，被虚拟机监控程序抢占的 CPU 时间百分比&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%idle&lt;/code&gt;：CPU 空闲时间百分比&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;磁盘 I/O 信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;r/s&lt;/strong&gt;：每秒读取的请求数（I/O 操作次数）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;w/s&lt;/strong&gt;：每秒写入的请求数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rkB/s&lt;/strong&gt;：每秒读取的数据量（KB）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;wkB/s&lt;/strong&gt;：每秒写入的数据量（KB）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rrqm/s&lt;/strong&gt;: 每秒合并读操作的次数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;wrqm/s&lt;/strong&gt;: 每秒合并写操作的次数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;r_await&lt;/strong&gt;：每个读操作平均所需要的时间，不仅包括硬盘设备读操作的时间，也包括在内核队列中的时间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;w_await&lt;/strong&gt;：每个写操平均所需要的时间，不仅包括硬盘设备写操作的时间，也包括在队列中等待的时间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;svctm&lt;/strong&gt;：I/O 服务时间（毫秒），表示请求处理的平均时间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;%util&lt;/strong&gt;：设备的利用率，表示磁盘 I/O 操作的占用程度，如果值接近 100%，说明磁盘已经达到饱和&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个命令会每秒输出一次磁盘的读写性能，包括每个磁盘的读写 I/O 操作次数和每秒的字节数&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;iostat&lt;/code&gt; 命令的主要功能是展示每个磁盘（包括 RAID 阵列的虚拟磁盘）以及 CPU 的利用情况，显示磁盘设备的 I/O 性能指标，如每秒的读写字节数、I/O 请求数、等待时间等。&lt;/p&gt;
&lt;p&gt;常用的 &lt;code&gt;iostat&lt;/code&gt; 参数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-c&lt;/code&gt;：显示 CPU 使用情况&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt;：显示磁盘设备的 I/O 统计信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-x&lt;/code&gt;：显示磁盘设备的扩展统计信息（如磁盘的响应时间、队列长度等）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-k&lt;/code&gt;：以 KB 为单位显示数据（默认单位为字节）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-m&lt;/code&gt;：以 MB 为单位显示数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-t&lt;/code&gt;：显示时间戳&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p&lt;/code&gt;：显示分区的统计信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-z&lt;/code&gt;：仅显示有 I/O 操作的设备，不显示没有活动的设备&lt;/li&gt;
&lt;li&gt;&lt;code&gt;interval&lt;/code&gt;：更新统计信息的时间间隔，单位为秒&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ iostat [options] [interval] [count]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;interval&lt;/code&gt;：统计的更新频率，单位为秒。例如，每 5 秒刷新一次统计信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt;：显示多少次统计信息。例如，&lt;code&gt;iostat 5 3&lt;/code&gt; 表示每隔 5 秒输出一次统计信息，总共输出 3 次。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2️⃣ 使用 &lt;code&gt;fio&lt;/code&gt; 进行基准测试&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;fio&lt;/code&gt; 可以用来生成 I/O 工作负载，测试不同类型的读写模式，从而间接估算 RAID 阵列和 SSD 的性能表现。&lt;/p&gt;
&lt;p&gt;你可以通过特定的测试配置来模拟写操作，并计算写放大，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 全盘顺序写
sudo fio \
  --name=seq_write_test \
  --filename=/dev/md0 \
  --size=100% \
  --bs=4k \
  --rw=write \
  --iodepth=64 \
  --numjobs=4 \
  --direct=1

# 全盘随机写
sudo fio \
  --name=rand_write_test \
  --filename=/dev/md0 \
  --size=100% \
  --bs=4k \
  --rw=randwrite \
  --iodepth=64 \
  --numjobs=4 \
  --direct=1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3️⃣ 使用 &lt;code&gt;smartctl&lt;/code&gt; 检查磁盘/硬盘状态&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;smartctl&lt;/code&gt; 检查和控制硬盘驱动器（HDD）和固态硬盘（SSD）SMART（Self-Monitoring, Analysis, and Reporting Technology）状态，&lt;code&gt;smartctl&lt;/code&gt; 工具可以提供磁盘的健康状况、温度、错误信息等，通常用于监控单个硬盘的健康状况。&lt;/p&gt;
&lt;p&gt;不过 smartctl 只能查看单个磁盘/硬盘的 SMART 数据，&lt;strong&gt;无法直接查看整个 RAID 阵列（如 &lt;code&gt;/dev/md0&lt;/code&gt;）的读写放大（Write Amplification）情况&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;wyk 20:20:37 ~
$ sudo smartctl -a /dev/sda
smartctl 7.1 2019-12-30 r5022 [x86_64-linux-5.4.0-198-generic] (local build)
Copyright (C) 2002-19, Bruce Allen, Christian Franke, www.smartmontools.org

=== START OF INFORMATION SECTION ===
Device Model:     Fanxiang S103Pro 1TB
Serial Number:    2036E4AD5054
LU WWN Device Id: 5 00a075 1e4ad5054
Firmware Version: 22Z4VBND
User Capacity:    1,000,204,886,016 bytes [1.00 TB]
Sector Sizes:     512 bytes logical, 4096 bytes physical
Rotation Rate:    Solid State Device
Form Factor:      2.5 inches
Device is:        Not in smartctl database [for details use: -P showall]
ATA Version is:   ACS-3 T13/2161-D revision 5
SATA Version is:  SATA 3.3, 6.0 Gb/s (current: 6.0 Gb/s)
Local Time is:    Fri Dec 27 20:21:44 2024 UTC
SMART support is: Available - device has SMART capability.
SMART support is: Enabled

=== START OF READ SMART DATA SECTION ===
SMART overall-health self-assessment test result: PASSED

General SMART Values:
Offline data collection status:  (0x80) Offline data collection activity
                                        was never started.
                                        Auto Offline Data Collection: Enabled.
Self-test execution status:      (   0) The previous self-test routine completed
                                        without error or no self-test has ever 
                                        been run.
Total time to complete Offline 
data collection:                (    0) seconds.
Offline data collection
capabilities:                    (0x7b) SMART execute Offline immediate.
                                        Auto Offline data collection on/off support.
                                        Suspend Offline collection upon new
                                        command.
                                        Offline surface scan supported.
                                        Self-test supported.
                                        Conveyance Self-test supported.
                                        Selective Self-test supported.
SMART capabilities:            (0x0002) Does not save SMART data before
                                        entering power-saving mode.
                                        Supports SMART auto save timer.
Error logging capability:        (0x01) Error logging supported.
                                        General Purpose Logging supported.
Short self-test routine 
recommended polling time:        (   2) minutes.
Extended self-test routine
recommended polling time:        (  30) minutes.
Conveyance self-test routine
recommended polling time:        (   2) minutes.
SCT capabilities:              (0x0031) SCT Status supported.
                                        SCT Feature Control supported.
                                        SCT Data Table supported.

SMART Attributes Data Structure revision number: 16
Vendor Specific SMART Attributes with Thresholds:
ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH TYPE      UPDATED  WHEN_FAILED RAW_VALUE
  1 Raw_Read_Error_Rate     0x0000   100   100   000    Old_age   Offline      -       0
  5 Reallocated_Sector_Ct   0x0000   100   100   000    Old_age   Offline      -       0
  9 Power_On_Hours          0x0000   100   100   000    Old_age   Offline      -       575
 12 Power_Cycle_Count       0x0000   100   100   000    Old_age   Offline      -       52
148 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       37557
149 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       302
150 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       78
151 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       146
159 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       0
160 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       0
161 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       93
163 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       23
164 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       16394
165 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       14
166 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       1
167 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       5
168 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       3000
169 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       100
177 Wear_Leveling_Count     0x0000   100   100   050    Old_age   Offline      -       3751
181 Program_Fail_Cnt_Total  0x0000   100   100   000    Old_age   Offline      -       0
182 Erase_Fail_Count_Total  0x0000   100   100   000    Old_age   Offline      -       0
192 Power-Off_Retract_Count 0x0000   100   100   000    Old_age   Offline      -       7
194 Temperature_Celsius     0x0000   100   100   000    Old_age   Offline      -       25
195 Hardware_ECC_Recovered  0x0000   100   100   000    Old_age   Offline      -       0
196 Reallocated_Event_Count 0x0000   100   100   016    Old_age   Offline      -       0
199 UDMA_CRC_Error_Count    0x0000   100   100   050    Old_age   Offline      -       0
232 Available_Reservd_Space 0x0000   100   100   000    Old_age   Offline      -       100
241 Total_LBAs_Written      0x0000   100   100   000    Old_age   Offline      -       144248
242 Total_LBAs_Read         0x0000   100   100   000    Old_age   Offline      -       102956
245 Unknown_Attribute       0x0000   100   100   000    Old_age   Offline      -       172137

SMART Error Log Version: 1
No Errors Logged

SMART Self-test log structure revision number 1
No self-tests have been logged.  [To run self-tests, use: smartctl -t]

SMART Selective self-test log data structure revision number 1
 SPAN  MIN_LBA  MAX_LBA  CURRENT_TEST_STATUS
    1        0        0  Not_testing
    2        0        0  Not_testing
    3        0        0  Not_testing
    4        0        0  Not_testing
    5        0        0  Completed [00% left] (0-65535)
Selective self-test flags (0x0):
  After scanning selected spans, do NOT read-scan remainder of disk.
If Selective self-test is pending on power-up, resume after 0 minute delay.
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222240708.iCDyqeox.png"/><enclosure url="/_astro/202501222240708.iCDyqeox.png"/></item><item><title>PMDK Programming Guidelines</title><link>https://coooredump.github.io/blog/system-architecture/pmdk-programming-guidelines</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/pmdk-programming-guidelines</guid><description>在使用 libpmemobj 库时，不需要直接使用 mmap。libpmemobj 提供了高级的 API 来管理持久内存池和分配内存。mmap 通常用于更底层的内存映射操作，而 libpmemobj 封装了这些操作，使得管理持久内存更加方便和安全。</description><pubDate>Sat, 28 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;libpmemobj&lt;/h2&gt;
&lt;p&gt;在使用 &lt;code&gt;libpmemobj&lt;/code&gt; 库时，不需要直接使用 &lt;code&gt;mmap&lt;/code&gt;。&lt;code&gt;libpmemobj&lt;/code&gt; 提供了高级的 API 来管理持久内存池和分配内存。&lt;code&gt;mmap&lt;/code&gt; 通常用于更底层的内存映射操作，而 &lt;code&gt;libpmemobj&lt;/code&gt; 封装了这些操作，使得管理持久内存更加方便和安全。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;libpmemobj.h&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;cassert&gt;
#include &amp;#x3C;cstring&gt;
#include &amp;#x3C;unistd.h&gt;
#include &amp;#x3C;stdlib.h&gt;
#include &amp;#x3C;stdio.h&gt;

void init_pmem() {
    // create pool
    const char *pool_name = &quot;/mnt/pmem0/matianmao/fast_fair.data&quot;;
    const char *layout_name = &quot;fast_fair&quot;;
    size_t pool_size = 64LL * 1024 * 1024 * 1024; // 16GB

    if (access(pool_name, 0)) {
        pmem_pool = pmemobj_create(pool_name, layout_name, pool_size, 0666);
        if (pmem_pool == nullptr) {
            std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\tcreate fail\n&quot;;
            assert(0);
        }
        std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\tcreate\n&quot;;
    } else {
        pmem_pool = pmemobj_open(pool_name, layout_name);
        std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\topen\n&quot;;
    }
    std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\topen pmem pool successfully\n&quot;;
}

// 函数通过 pmemobj_zalloc 从持久内存池中分配指定大小的内存，并返回分配的内存地址
// 如果分配失败，输出错误信息并终止程序
void *allocate(size_t size) {
    // 用于存储分配的内存地址
    void *addr;
    // 用于存储持久内存对象的标识符
    PMEMoid ptr;
    // 调用 pmemobj_zalloc 函数从持久内存池 pmem_pool 中分配大小为 size 字节的内存，并将分配的内存对象标识符存储在 ptr 中
    int ret = pmemobj_zalloc(pmem_pool, &amp;#x26;ptr, sizeof(char) * size, TOID_TYPE_NUM(char));
    if (ret) {
        std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\tallocate btree successfully\n&quot;;
        assert(0);
    }
    // 将持久内存对象标识符 ptr 转换为直接指针，并将其存储在 addr 中
    addr = (char *)pmemobj_direct(ptr);
    // 返回分配的内存地址
    return addr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1️⃣ 使用 &lt;code&gt;libpmemobj&lt;/code&gt; 库函数读写持久内存的示例代码 &lt;em&gt;&lt;strong&gt;libpmemobj_pmem.cpp&lt;/strong&gt;&lt;/em&gt;（without mmap —— 封装📦好了）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;libpmemobj.h&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;cassert&gt;
#include &amp;#x3C;cstring&gt;
#include &amp;#x3C;unistd.h&gt;
#include &amp;#x3C;stdlib.h&gt;
#include &amp;#x3C;stdio.h&gt;

// 持久内存池的全局变量
PMEMobjpool *pmem_pool;

// 定义持久对象的类型编号
#define TOID_TYPE_NUM_CHAR 1

// 初始化持久内存池
void init_pmem() {
    // 持久内存池的名称和布局名称
    const char *pool_name = &quot;/mnt/pmem1/libpmemobj_pmem&quot;;
    const char *layout_name = &quot;fast_fair&quot;;
    // 持久内存池的大小（16GB）
    size_t pool_size = 16LL * 1024 * 1024 * 1024;

    // 检查持久内存池文件是否存在
    if (access(pool_name, 0)) {
        // 创建持久内存池
        pmem_pool = pmemobj_create(pool_name, layout_name, pool_size, 0666);
        if (pmem_pool == nullptr) {
            std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\tcreate fail\n&quot;;
            assert(0);
        }
        std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\tcreate\n&quot;;
    } else {
        // 打开持久内存池
        pmem_pool = pmemobj_open(pool_name, layout_name);
        if (pmem_pool == nullptr) {
            std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\topen fail\n&quot;;
            assert(0);
        }
        std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\topen\n&quot;;
    }
    std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\topen pmem pool successfully\n&quot;;
}

// 分配指定大小的持久内存，并返回分配的内存地址
void *allocate(size_t size) {
    // 用于存储分配的内存地址
    void *addr;
    // 用于存储持久内存对象的标识符
    PMEMoid ptr;
    // 调用 pmemobj_zalloc 函数从持久内存池 pmem_pool 中分配大小为 size 字节的内存，并将分配的内存对象标识符存储在 ptr 中
    int ret = pmemobj_zalloc(pmem_pool, &amp;#x26;ptr, sizeof(char) * size, TOID_TYPE_NUM_CHAR);
    if (ret) {
        std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\tallocate fail\n&quot;;
        assert(0);
    }
    // 将持久内存对象标识符 ptr 转换为直接指针，并将其存储在 addr 中
    addr = pmemobj_direct(ptr);
    // 返回分配的内存地址
    return addr;
}

int main() {
    // 初始化持久内存池
    init_pmem();

    // 分配 1024 字节的持久内存
    void *pmem_addr = allocate(1024);
    std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\tallocated 1024 bytes at &quot; &amp;#x3C;&amp;#x3C; pmem_addr &amp;#x3C;&amp;#x3C; &quot;\n&quot;;

    // 使用分配的持久内存（例如，写入数据）
    strcpy((char *)pmem_addr, &quot;Hello, Persistent Memory!&quot;);
    std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\tdata written: &quot; &amp;#x3C;&amp;#x3C; (char *)pmem_addr &amp;#x3C;&amp;#x3C; &quot;\n&quot;;

    // 关闭持久内存池
    pmemobj_close(pmem_pool);
    std::cout &amp;#x3C;&amp;#x3C; &quot;[FAST FAIR]\tpmem pool closed\n&quot;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ g++ -o libpmemobj libpmemobj_pmem.cpp -lpmemobj
$ ./libpmemobj
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;mmap&lt;/h2&gt;
&lt;p&gt;2️⃣ 如果不使用 &lt;code&gt;libpmemobj&lt;/code&gt; 库函数来读写持久内存（PM），你可以直接使用 &lt;code&gt;mmap&lt;/code&gt; 函数将持久内存映射到虚拟地址空间，然后通过指针操作进行读写。以下是一个示例代码 &lt;em&gt;&lt;strong&gt;mmap_pmem.cpp&lt;/strong&gt;&lt;/em&gt;，展示了如何使用 &lt;code&gt;mmap&lt;/code&gt; 来读写持久内存：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;fcntl.h&gt;
#include &amp;#x3C;sys/mman.h&gt;
#include &amp;#x3C;unistd.h&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;cstring&gt;
#include &amp;#x3C;cassert&gt;

#define PMEM_FILE_PATH &quot;/mnt/pmem1/mmap_pmem&quot;
#define PMEM_FILE_SIZE (16LL * 1024 * 1024 * 1024) // 16GB

void* pmem_addr = nullptr;
int pmem_fd = -1;

// 初始化持久内存
void init_pmem() {
    // 打开或创建持久内存文件
    pmem_fd = open(PMEM_FILE_PATH, O_RDWR | O_CREAT, 0666);
    if (pmem_fd &amp;#x3C; 0) {
        std::cerr &amp;#x3C;&amp;#x3C; &quot;Failed to open or create PMEM file&quot; &amp;#x3C;&amp;#x3C; std::endl;
        exit(1);
    }

    // 设置文件大小
    if (ftruncate(pmem_fd, PMEM_FILE_SIZE) != 0) {
        std::cerr &amp;#x3C;&amp;#x3C; &quot;Failed to set PMEM file size&quot; &amp;#x3C;&amp;#x3C; std::endl;
        close(pmem_fd);
        exit(1);
    }

    // 将文件映射到内存
    pmem_addr = mmap(nullptr, PMEM_FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, pmem_fd, 0);
    if (pmem_addr == MAP_FAILED) {
        std::cerr &amp;#x3C;&amp;#x3C; &quot;Failed to mmap PMEM file&quot; &amp;#x3C;&amp;#x3C; std::endl;
        close(pmem_fd);
        exit(1);
    }

    std::cout &amp;#x3C;&amp;#x3C; &quot;PMEM initialized successfully&quot; &amp;#x3C;&amp;#x3C; std::endl;
}

// 关闭持久内存
void close_pmem() {
    if (pmem_addr != nullptr) {
        munmap(pmem_addr, PMEM_FILE_SIZE);
        pmem_addr = nullptr;
    }
    if (pmem_fd &gt;= 0) {
        close(pmem_fd);
        pmem_fd = -1;
    }
    std::cout &amp;#x3C;&amp;#x3C; &quot;PMEM closed successfully&quot; &amp;#x3C;&amp;#x3C; std::endl;
}

int main() {
    // 初始化持久内存
    init_pmem();

    // 分配 1024 字节的持久内存
    void* data_addr = static_cast&amp;#x3C;char*&gt;(pmem_addr) + 1024;
    std::cout &amp;#x3C;&amp;#x3C; &quot;Allocated 1024 bytes at &quot; &amp;#x3C;&amp;#x3C; data_addr &amp;#x3C;&amp;#x3C; std::endl;

    // 使用分配的持久内存（例如，写入数据）
    strcpy(static_cast&amp;#x3C;char*&gt;(data_addr), &quot;Hello, Persistent Memory!&quot;);
    std::cout &amp;#x3C;&amp;#x3C; &quot;Data written: &quot; &amp;#x3C;&amp;#x3C; static_cast&amp;#x3C;char*&gt;(data_addr) &amp;#x3C;&amp;#x3C; std::endl;

    // 关闭持久内存
    close_pmem();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pmem_map_file&lt;/h2&gt;
&lt;p&gt;3️⃣ 调用 &lt;code&gt;pmem_map_file&lt;/code&gt; 函数来映射 PM，&lt;code&gt;pmem_map_file&lt;/code&gt; 是 &lt;code&gt;libpmem&lt;/code&gt; 库中的一个函数，用于将持久内存文件映射到虚拟地址空间。示例代码 &lt;em&gt;&lt;strong&gt;pmem.c&lt;/strong&gt;&lt;/em&gt; 如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;fcntl.h&gt;      // for open, O_RDWR, O_CREAT, O_TRUNC
#include &amp;#x3C;unistd.h&gt;     // for close, ftruncate
#include &amp;#x3C;sys/types.h&gt;  // for types
#include &amp;#x3C;sys/stat.h&gt;   // for ftruncate
#include &amp;#x3C;libpmem.h&gt;    // for PMDK functions
#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;stdlib.h&gt;

#define PMEM_SIZE 1024
#define PMEM_FILE &quot;/mnt/pmem1/pmem_file&quot;

int main() {
    // 创建持久内存文件
    int fd = open(PMEM_FILE, O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (fd &amp;#x3C; 0) {
        perror(&quot;open&quot;);
        return EXIT_FAILURE;
    }
    ftruncate(fd, PMEM_SIZE);

    // 映射持久内存
    void *pmem_addr = pmem_map_file(PMEM_FILE, PMEM_SIZE, PMEM_FILE_CREATE, 0666, NULL, NULL);
    if (pmem_addr == NULL) {
        perror(&quot;pmem_map_file&quot;);
        return EXIT_FAILURE;
    }

    // 写入数据
    sprintf(pmem_addr, &quot;Hello, Persistent Memory!&quot;);

    // 刷新持久内存
    pmem_persist(pmem_addr, PMEM_SIZE);

    // 读取数据
    printf(&quot;%s\n&quot;, (char *)pmem_addr);

    // 清理
    pmem_unmap(pmem_addr, PMEM_SIZE);
    close(fd);
    return EXIT_SUCCESS;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pmem_map_file&lt;/code&gt; 底层封装的也是 &lt;code&gt;mmap&lt;/code&gt;，以下是 &lt;code&gt;pmem_map_file&lt;/code&gt; 实现的一个简化示例，具体实现可能会有所不同：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;pmdk/src/libpmem/pmem.c 代码库中对 &lt;code&gt;pmem_map_file&lt;/code&gt; 的定义 -- &lt;strong&gt;create or open the file and map it to memory&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;libpmem.h&gt;
#include &amp;#x3C;sys/mman.h&gt;
#include &amp;#x3C;fcntl.h&gt;
#include &amp;#x3C;unistd.h&gt;
#include &amp;#x3C;stdio.h&gt;

void *pmem_map_file(const char *path, size_t len, int flags, mode_t mode, size_t *mapped_lenp, int *is_pmemp) {
    int fd = open(path, flags, mode);
    if (fd &amp;#x3C; 0) {
        perror(&quot;open&quot;);
        return NULL;
    }

    if (len == 0) {
        len = lseek(fd, 0, SEEK_END);
        if (len == (size_t)-1) {
            perror(&quot;lseek&quot;);
            close(fd);
            return NULL;
        }
    }

    void *addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        perror(&quot;mmap&quot;);
        close(fd);
        return NULL;
    }

    close(fd);

    if (mapped_lenp)
        *mapped_lenp = len;
    if (is_pmemp)
        *is_pmemp = 1; // Simplified, actual implementation may check if it&apos;s true PMEM

    return addr;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222348546.DxxOE8nT.png"/><enclosure url="/_astro/202501222348546.DxxOE8nT.png"/></item><item><title>浅析 trace 的处理</title><link>https://coooredump.github.io/blog/system-architecture/analysis-of-trace-processing</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/analysis-of-trace-processing</guid><description>trace 这个词有着很多的含义，在英文维基中计算机科学分类中就有 5 个代指，而实验室平常所说到的 trace 特指 I/O trace</description><pubDate>Sun, 08 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;由于作者见识有限，本文仅对 &lt;code&gt;WebSearch2.spc&lt;/code&gt; 这一 trace 进行讲解分析。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What is trace&lt;/h2&gt;
&lt;p&gt;trace 这个词有着很多的含义，在英文维基中计算机科学分类中就有 5 个代指。而实验室平常所说到的 trace 应该是特指 I/O trace。说来惭愧，一直在网上找不到实验室用的 I/O trace 的权威定义，根据之前跟诸位学长的探讨，自己的拙见如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I/O trace 就是一些真实在线系统的运行数天的磁盘所接受的 I/O 请求记录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而 I/O trace 也有着许多格式，例如本文提到的 WebSearch2 就来自于一个流行的搜索引擎，是真实的工作实际负载，它的格式定义遵循 SPC trace 文本规范$^{[1]}$。&lt;strong&gt;厂商之所以将其真实的负载公布出来&lt;/strong&gt;，也是为了让学术界对这些数据进行分析科研，让学术界和工业界紧密的结合，达到双赢的目的。&lt;/p&gt;
&lt;h2&gt;Download&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ wget http://skuld.cs.umass.edu/traces/storage/WebSearch2.spc.bz2
$ bunzip2 WebSearch2.spc.bz2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，我们就得到本文要分析的 trace 文件 &lt;code&gt;WebSearch2.spc&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Cognize 拿到一个 trace，首先要了解它的基本格式&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ head WebSearch2.spc
0,21741712,24576,R,0.000774
1,18960512,24576,R,0.000938
1,32558896,8192,R,0.008117
2,21841504,24576,R,0.008252
2,21841568,8192,R,0.008388
0,18600896,8192,R,0.011178
0,30860080,8192,R,0.012703
0,30503312,8192,R,0.016801
1,32558944,8192,R,0.020748
1,20802624,8192,R,0.025710

$ wc -l WebSearch2.spc
4579809 WebSearch2.spc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;可以看到 trace 由 400w 多行的数据构成，每一行都反映了一次 I/O 请求&lt;/strong&gt;。每行用 &lt;code&gt;,&lt;/code&gt; 号作为分隔符构成 5 个自然域。 将每个自然域以 &lt;code&gt;$i&lt;/code&gt; 来标识，则每个自然域分别代表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$1&lt;/code&gt; Application specific unit (ASU) 设备号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$2&lt;/code&gt; Logical block address (LBA) 逻辑块地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$3&lt;/code&gt; Size 请求的数据长度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$4&lt;/code&gt; Opcode 读请求或者写请求，WebSearch2 中只有读请求&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$5&lt;/code&gt; Timestamp 请求下达的时间戳&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Analysis&lt;/h2&gt;
&lt;p&gt;对 trace 及其格式有了基本的认识之后，我们再来做进一步的分析探讨：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;我们研究的意义是对 trace 进行重播，让我们研究的系统模拟真实负载下的性能&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;那么原先的 trace 并不一定适合我们想要测试研究的系统，我们要使用这一 trace 的时候就要对其进行重新组织&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;就让我们一步步的从每个自然域来分析下 WebSearch2.spc 这一 trace 文件吧。首先来看下这个 trace 访问设备的频率：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ awk &apos;BEGIN{FS=&quot;,&quot;};{print $1}&apos; WebSearch2.spc | sort | uniq -c
1544375 0
1517218 1
1515918 2
    765 3
    795 4
    738 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到这个 trace 的 I/O 请求主要集中于前三个设备，而且请求是均匀分布的。至于后 3 个设备那稀疏的请求次数，跟具体的系统有关，我们便不得而知了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二个作用域&lt;/strong&gt;我们需要关心的是每个设备系统的请求的最大的逻辑块地址，这关系到我们 trace 重播的设计：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ awk &apos;BEGIN{FS=&quot;,&quot;;max=0};{if($1==0&amp;#x26;&amp;#x26;$2&gt;max){max=$2}};END{print max}&apos; WebSearch2.spc
34967808
$ awk &apos;BEGIN{FS=&quot;,&quot;;max=0};{if($1==1&amp;#x26;&amp;#x26;$2&gt;max){max=$2}};END{print max}&apos; WebSearch2.spc
34662560
$ awk &apos;BEGIN{FS=&quot;,&quot;;max=0};{if($1==2&amp;#x26;&amp;#x26;$2&gt;max){max=$2}};END{print max}&apos; WebSearch2.spc
25949392
$ awk &apos;BEGIN{FS=&quot;,&quot;;max=0};{if($1==3&amp;#x26;&amp;#x26;$2&gt;max){max=$2}};END{print max}&apos; WebSearch2.spc
25949312
$ awk &apos;BEGIN{FS=&quot;,&quot;;max=0};{if($1==4&amp;#x26;&amp;#x26;$2&gt;max){max=$2}};END{print max}&apos; WebSearch2.spc
34643200
$ awk &apos;BEGIN{FS=&quot;,&quot;;max=0};{if($1==5&amp;#x26;&amp;#x26;$2&gt;max){max=$2}};END{print max}&apos; WebSearch2.spc
34951264
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⁉️可以看到最大的 LBA 为 3kw 多，我们至少需要 35000000×512B 的磁盘空间才能满足 trace 的需求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三个作用域&lt;/strong&gt;是请求块的大小：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ awk &apos;BEGIN{FS=&quot;,&quot;};{print $3}&apos; WebSearch2.spc | sort | uniq -c
 495744 16384
 406838 24576
 912770 32768
2764457 8192
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据 SPC 文档的规范，size 的单位是 &lt;code&gt;byte&lt;/code&gt;，于是可以看到请求块的大小只有 4 种分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8KB&lt;/li&gt;
&lt;li&gt;16KB&lt;/li&gt;
&lt;li&gt;24KB&lt;/li&gt;
&lt;li&gt;32KB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而 8KB 的请求占大多数，32KB 紧随其后&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四个作用域&lt;/strong&gt;是读请求或者写请求，WebSearch2 的 trace 大部分为读请求，固不做分析处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第五个作用域&lt;/strong&gt;是时间戳：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ awk &apos;BEGIN{FS=&quot;,&quot;;max=0};{if($5&gt;max){max=$5}};END{print max}&apos; WebSearch2.spc
15395.556800

$ tail -1 WebSearch2.spc
2,25487520,8192,R,15395.556800
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间戳是按照请求到达的顺序排列的，最大的时间也是最后一条请求到达的时间。根据 SPC 文档的规范，时间的单位为 &lt;code&gt;s&lt;/code&gt;，可以看到这个 trace 实际上只是系统运行 4 个多小时的记录。&lt;/p&gt;
&lt;h2&gt;Refactoring&lt;/h2&gt;
&lt;p&gt;我们对 trace 君的百般玩弄主要的目的是在于在我们自己的系统下重放 trace 的负载（&lt;em&gt;replay trace&lt;/em&gt;），分析系统的性能，主要是其平均响应时间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是我们的系统几乎不太可能与原先 trace 的工作系统一致，于是我们就需要对 trace 进行重构处理&lt;/strong&gt;：即处理 trace 的格式&lt;/p&gt;
&lt;h3&gt;Single Disk&lt;/h3&gt;
&lt;p&gt;如果仅对单盘进行 trace 重放，有两种方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一种方法，直接忽视设备号，每个请求都视为访问同一设备。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;优点：实现简单&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;缺点：丧失了原先的局部性，可以造成性能下降&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202409191055884.jpeg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第二种方法，将磁盘扩展，第二个设备视为直接第一个设备的衍生，并依次类推。即新的逻辑块地址=块设备号×总块数+旧逻辑块地址&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;优点：可能保留了局部性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;缺点：需要进行换算，实现会比较复杂，需要比较大的磁盘&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202409191057170.jpeg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Multiple Disk&lt;/h3&gt;
&lt;p&gt;对于多盘进行 trace 重放，如果盘数恰好相等，则无用多说。&lt;/p&gt;
&lt;p&gt;如若不等，则类似 Single Disk 的第二种方法，先将磁盘扩展为单盘，再进行切分。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202409191058387.jpeg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Question&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;如果想要测试磁盘的容量比 trace 的最大偏移地址大很多呢？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202409191059172.jpeg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类 RAID5 的测试：盘内存在校验块&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202409191100751.jpeg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Replay trace 的重放也有着两种方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;第一种，压力重放，无视时间戳的存在，直接循环中无间断执行每条请求&lt;/li&gt;
&lt;li&gt;第二种，守时重放，控制每条请求不能早于时间戳的时间执行&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Future Work&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;多种 trace 的对比&lt;/li&gt;
&lt;li&gt;tools: disksim&lt;/li&gt;
&lt;li&gt;etc..&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Rant&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;实验室的文档，传承&lt;/li&gt;
&lt;li&gt;引用出处，参考文献&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/202501222321048.BmWWUew3.png"/><enclosure url="/_astro/202501222321048.BmWWUew3.png"/></item><item><title>Linux 磁盘配置文件 /etc/fstab 详解</title><link>https://coooredump.github.io/blog/system-architecture/linux-disk-automatic-mounting</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/linux-disk-automatic-mounting</guid><description>磁盘被手动挂载之后都必须把挂载信息写入 /etc/fstab 这个文件中，否则下次开机启动时仍然需要重新挂载。系统开机时会主动读取 /etc/fstab 这个文件中的内容，根据文件里面的配置挂载磁盘。这样我们只需要将磁盘的挂载信息写入这个文件中我们就不需要每次开机启动之后手动进行挂载了。</description><pubDate>Sun, 08 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;/etc/fstab 文件的作用&lt;/h2&gt;
&lt;p&gt;磁盘被手动挂载之后都必须把挂载信息写入 &lt;code&gt;/etc/fstab&lt;/code&gt; 这个文件中，&lt;strong&gt;否则下次开机启动时仍然需要重新挂载&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;系统开机时会主动读取 &lt;code&gt;/etc/fstab&lt;/code&gt; 这个文件中的内容，根据文件里面的配置挂载磁盘。这样我们只需要将磁盘的挂载信息写入这个文件中我们就不需要每次开机启动之后手动进行挂载了。&lt;/p&gt;
&lt;h2&gt;挂载的限制&lt;/h2&gt;
&lt;p&gt;在说明这个文件的作用之前我想先强调一下挂载的限制。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根目录是必须挂载的，而且一定要先于其他 mount point 被挂载。因为 mount 是所有目录的根目录，其他都是由根目录 &lt;code&gt;/&lt;/code&gt; 衍生出来的&lt;/li&gt;
&lt;li&gt;挂载点必须是已经存在的目录&lt;/li&gt;
&lt;li&gt;挂载点的指定可以任意，但必须遵守必要的系统目录架构原则&lt;/li&gt;
&lt;li&gt;所有挂载点在同一时间只能被挂载一次&lt;/li&gt;
&lt;li&gt;所有分区在同一时间只能挂在一次&lt;/li&gt;
&lt;li&gt;若进行卸载，必须将工作目录退出挂载点（及其子目录）之外。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;/etc/fstab 文件中参数&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 查看当前系统已经存在的挂载信息
$ cat /etc/fstab
# /etc/fstab: static file system information.
#
# Use &apos;blkid&apos; to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# &amp;#x3C;file system&gt; &amp;#x3C;mount point&gt;   &amp;#x3C;type&gt;  &amp;#x3C;options&gt;       &amp;#x3C;dump&gt;  &amp;#x3C;pass&gt;
# / was on /dev/sda1 during installation
/dev/disk/by-uuid/dbe45dcb-0a04-428d-816d-ae4c004599d8 / ext4 defaults 0 1
# /boot/efi was on /dev/nvme1n1p1 during curtin installation
/dev/disk/by-uuid/8EC3-92ED /boot/efi vfat defaults 0 1
/swap.img       none    swap    sw      0       0
UUID=cf0c96db-89d0-41d9-bdc9-0dd5cb67bcde /mnt/pmem0 ext4 defaults 0 1
/dev/sda1 /mnt/sda1/zz_data ext4 defaults 0 2
/mnt/sda1/zz_data /home/zz/data_new/Nomad/src none bind 0 0
UUID=526d14d2-dbcc-4100-8cb5-85579eefae94 /home/lj ext4 defaults 0 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在文件中我已经把每一列都做出来表示方便识别，我们可以看到一共有六列。&lt;/p&gt;
&lt;h3&gt;第 1 列 [Device] 磁盘设备文件或者该设备的 Label 或者 UUID&lt;/h3&gt;
&lt;p&gt;Label 就是分区的标签，在最初安装系统时填写的挂载点就是标签的名字。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;UUID-Universally Unique IDentifiers 全局唯一标识符&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以通过查看一个分区的 &lt;strong&gt;superblock&lt;/strong&gt; 中的信息找到 &lt;strong&gt;UUID&lt;/strong&gt; 和 Label name。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 切换 root
$ sudo -i

# 查询 /dev/nvme0n1 的 UUID
$ blkid /dev/nvme0n1
/dev/nvme0n1: UUID=&quot;36df5186-24a0-4dad-9b4e-664a4230b7f1&quot; TYPE=&quot;ext4&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用设备名称（&lt;code&gt;/dev/sda&lt;/code&gt;)来挂载分区时是被固定死的，一旦磁盘的插槽顺序发生了变化，就会出现名称不对应的问题。因为这个名称是会改变的。&lt;/p&gt;
&lt;p&gt;不过使用 label 挂载就不用担心插槽顺序方面的问题。不过要随时注意你的 Label name。至于 UUID，每个分区被格式化以后都会有一个 UUID 作为唯一的标识号。使用 uuid 挂载的话就不用担心会发生错乱的问题了。&lt;/p&gt;
&lt;h3&gt;第 2 列 [Mount Point] 设备挂载点，就是你要挂载到哪个目录下&lt;/h3&gt;
&lt;h3&gt;第 3 列 [File System] 磁盘文件系统的格式，包括 ext2、ext3、ext4、nfs...&lt;/h3&gt;
&lt;h3&gt;第 4 列 [Parameters] 文件系统的参数&lt;/h3&gt;
&lt;p&gt;| 参数         | 说明                                                         |
| ------------ | ------------------------------------------------------------ |
| Async/sync   | 设置是否为同步方式运行，默认为 &lt;code&gt;async&lt;/code&gt;                       |
| auto/noauto  | 当下载 &lt;code&gt;mount -a&lt;/code&gt; 的命令时，此文件系统是否被主动挂载。默认为 &lt;code&gt;auto&lt;/code&gt; |
| rw/ro        | 是否以以只读或者读写模式挂载                                 |
| exec/noexec  | 限制此文件系统内是否能够进行”执行”的操作                     |
| user/nouser  | 是否允许用户使用 &lt;code&gt;mount&lt;/code&gt; 命令挂载                            |
| suid/nosuid  | 是否允许 &lt;code&gt;SUID&lt;/code&gt; 的存在                                       |
| Usrquota     | 启动文件系统支持磁盘配额模式                                 |
| Grpquota     | 启动文件系统对群组磁盘配额模式的支持                         |
| &lt;strong&gt;Defaults&lt;/strong&gt; | 同时具有 &lt;code&gt;rw,suid,dev,exec,auto,nouser,async&lt;/code&gt; 等默认参数的设置 |&lt;/p&gt;
&lt;h3&gt;第 5 列 [能否被 dump 备份命令作用] dump 是一个用来作为备份的命令，通常值为 0 或 1&lt;/h3&gt;
&lt;p&gt;| 参数 | 说明                         |
| ---- | ---------------------------- |
| 0    | 代表不要做 dump 备份         |
| 1    | 代表要每天进行 dump 的操作   |
| 2    | 代表不定日期的进行 dump 操作 |&lt;/p&gt;
&lt;h3&gt;第 6 列 [是否检验扇区] 开机的过程中，系统默认会以 &lt;code&gt;fsck&lt;/code&gt; 检验我们系统是否为完整（clean）&lt;/h3&gt;
&lt;p&gt;| 参数 | 说明                         |
| ---- | ---------------------------- |
| 0    | 不要检验                     |
| 1    | 最早检验（一般根目录会选择） |
| 2    | 1级别检验完成之后进行检验    |&lt;/p&gt;
&lt;h2&gt;Linux 磁盘分区 UUID 获取及其作用&lt;/h2&gt;
&lt;h3&gt;获取 UUID 的方法&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 方法一
$ sudo ls -l /dev/disk/by-uuid/

# 方法二
$ sudo blkid /dev/sda1
/dev/sda1: UUID=&quot;f0d9b5f8-24ef-4aba-b3ce-f4bf0a0c231a&quot; TYPE=&quot;ext4&quot; PARTUUID=&quot;e3d6d3a9-01&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;原因 1：它是真正的唯一标志符&lt;/h3&gt;
&lt;p&gt;UUID 为系统中的存储设备提供唯一的标识字符串，不管这个设备是什么类型的。如果你在系统中添加了新的存储设备如硬盘，很可能会造成一些麻烦，比如说启动的时候因为找不到设备而失败，而使用UUID则不会有这样的问题。&lt;/p&gt;
&lt;h3&gt;原因 2：设备名并非总是不变的&lt;/h3&gt;
&lt;p&gt;自动分配的设备名称并非总是一致的，它们依赖于启动时内核加载模块的顺序。如果你在插入了 USB 盘时启动了系统，而下次启动时又把它拔掉了，就有可能导致设备名分配不一致。&lt;/p&gt;
&lt;p&gt;使用 UUID 对于挂载移动设备也非常有好处──例如我有一个 24 合一的读卡器，它支持各种各样的卡，而使用 UUID 总可以使同一块卡挂载在同一个地方。&lt;/p&gt;
&lt;h3&gt;原因 3：ubuntu 中的许多关键功能现在开始依赖于 UUID&lt;/h3&gt;
&lt;p&gt;例如 &lt;strong&gt;grub&lt;/strong&gt; ──系统引导程序，现在可以识别 UUID，打开你的 &lt;code&gt;/boot/grub/menu.lst&lt;/code&gt;，你可以看到类似如下的语句：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ cat /boot/grub/menu.lst
title Ubuntu hardy (development branch), kernel 2.6.24-16-generic
root (hd2,0)
kernel /boot/vmlinuz-2.6.24-16-generic root=UUID=c73a37c8-ef7f-40e4-b9de-8b2f81038441 ro quiet splash
initrd /boot/initrd.img-2.6.24-16-generic
quiet
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/202501222240708.iCDyqeox.png"/><enclosure url="/_astro/202501222240708.iCDyqeox.png"/></item><item><title>Linux 分区更改 fdisk、格式化 mkfs、检查 fsck、挂载 mount</title><link>https://coooredump.github.io/blog/system-architecture/linux-partition</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/linux-partition</guid><description>本文详细介绍 Linux 系统下与磁盘分区相关的有 fdisk、fsck、mkfs、mount 等其他常用命令。</description><pubDate>Sun, 08 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Linux 系统下与磁盘分区相关的有 &lt;code&gt;fdisk&lt;/code&gt;、&lt;code&gt;fsck&lt;/code&gt;、&lt;code&gt;mkfs&lt;/code&gt;、&lt;code&gt;mount&lt;/code&gt; 等这些命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fdisk&lt;/code&gt; 是用来操作磁盘分区表相关的更改，比如更改分区表格式，创建分区表，新建/删除分区等&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkfs&lt;/code&gt; 则是在创建分区之后负责将分区格式化的工具&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mount&lt;/code&gt; 则是将分区挂载到 Linux 的文件树中（与之对应的卸载是 &lt;code&gt;umount&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我以向 Linux 系统添加了一块全新的磁盘以拓展存储空间为例。各种命令的详细使用方法使用 &lt;code&gt;man&lt;/code&gt; 查看，此处不再翻译。&lt;/p&gt;
&lt;h2&gt;fdisk&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;fdisk&lt;/code&gt; 命令对硬盘进行分区，创建分区表和分区。可以创建主分区、扩展分区和逻辑分区等。&lt;/p&gt;
&lt;p&gt;分区完成后，每个分区都会被赋予一个设备节点（例如：&lt;code&gt;/dev/sda1&lt;/code&gt;，&lt;code&gt;/dev/sdb2&lt;/code&gt; 等）。&lt;/p&gt;
&lt;p&gt;接下来，需要使用 &lt;code&gt;mkfs&lt;/code&gt; 命令对每个分区进行格式化，例如 &lt;code&gt;mkfs.ext4&lt;/code&gt;、&lt;code&gt;mkfs.xfs&lt;/code&gt; 等。&lt;/p&gt;
&lt;p&gt;最后，将格式化后的分区挂载到指定的挂载点（目录）上，使其可以被访问和使用。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;在添加磁盘之前，先执行 &lt;code&gt;fdisk -l&lt;/code&gt; &lt;strong&gt;列出系统中的物理磁盘&lt;/strong&gt;，记录下来，方便与添加磁盘之后做对比，找到新添加的磁盘设备号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;root@linux:~$ fdisk -l
Disk /dev/sda: 20 GiB, 21474836480 bytes, 41943040 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x25241c74
Device     Boot    Start      End  Sectors Size Id Type
/dev/sda1  *        2048 25165823 25163776  12G 83 Linux
/dev/sda2       25167870 41940991 16773122   8G  5 Extended
/dev/sda5       25167872 41940991 16773120   8G 82 Linux swap / Solaris
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，目前系统只安装了一块硬盘 &lt;code&gt;sda&lt;/code&gt;，有三个分区，现在可以关机加硬盘了。加完硬盘后开机再执行 &lt;code&gt;fdisk -l&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;root@linux:~$ fdisk -l
Disk /dev/sda: 20 GiB, 21474836480 bytes, 41943040 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x25241c74
Device     Boot    Start      End  Sectors Size Id Type
/dev/sda1  *        2048 25165823 25163776  12G 83 Linux
/dev/sda2       25167870 41940991 16773122   8G  5 Extended
/dev/sda5       25167872 41940991 16773120   8G 82 Linux swap / Solaris

Disk /dev/sdb: 10 GiB, 10737418240 bytes, 20971520 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，&lt;strong&gt;新添加的硬盘的设备号为 &lt;code&gt;sdb&lt;/code&gt;，&lt;code&gt;没有分区表&lt;/code&gt;，没有分区&lt;/strong&gt;。接下来就用 &lt;code&gt;fdisk /dev/sdb&lt;/code&gt; 来创建分区表和分区。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;root@linux:~$ fdisk /dev/sdb
Welcome to fdisk (util-linux 2.27.1).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.
Device does not contain a recognized partition table.
Created a new DOS disklabel with disk identifier 0x95942ae2.
Command (m for help):
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不知道怎么操作的此时可以按 &lt;code&gt;m&lt;/code&gt; 调出帮助界面&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Command (m for help): m
Help:
  DOS (MBR)
   a   toggle a bootable flag
   b   edit nested BSD disklabel
   c   toggle the dos compatibility flag
  Generic
   d   delete a partition
   F   list free unpartitioned space
   l   list known partition types
   n   add a new partition
   p   print the partition table
   t   change a partition type
   v   verify the partition table
   i   print information about a partition
  Misc
   m   print this menu
   u   change display/entry units
   x   extra functionality (experts only)
  Script
   I   load disk layout from sfdisk script file
   O   dump disk layout to sfdisk script file
  Save &amp;#x26; Exit
   w   write table to disk and exit
   q   quit without saving changes
  Create a new label
   g   create a new empty GPT partition table
   G   create a new empty SGI (IRIX) partition table
   o   create a new empty DOS partition table
   s   create a new empty Sun partition table
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;先创建一个 GPT 分区表&lt;/strong&gt;，按 &lt;code&gt;g&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Command (m for help): g
Created a new GPT disklabel (GUID: F4A12897-62F7-4ABA-9BC3-88BF53550DE3).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分区表创建后创建分区，按 &lt;code&gt;n&lt;/code&gt; 回车，Partition number、First sector、Last sector 参数不清楚的可以直接回车使用默认参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;指定起始扇区&lt;/strong&gt;：按回车使用默认值，通常是第一个可用扇区。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;指定结束扇区&lt;/strong&gt;或分区大小，你可以手动指定结束扇区，也可以通过输入大小来自动计算结束扇区。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;输入 &lt;code&gt;+50G&lt;/code&gt; 创建一个 50 GiB 的分区。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入 &lt;code&gt;+200G&lt;/code&gt; 创建一个 200 GiB 的分区。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Command (m for help): n
Partition number (1-128, default 1):
First sector (2048-20971486, default 2048):
Last sector, +sectors or +size{K,M,G,T,P} (2048-20971486, default 20971486):
Created a new partition 1 of type &apos;Linux filesystem&apos; and of size 10 GiB.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时已经在这块新加的 10GB 的硬盘中创建了一个 10GB 的分区。&lt;/p&gt;
&lt;p&gt;最后按 &lt;code&gt;w&lt;/code&gt; 将更改信息写入硬盘。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;mkfs&lt;/h2&gt;
&lt;p&gt;常见的 File System&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ext2&lt;/li&gt;
&lt;li&gt;ext3&lt;/li&gt;
&lt;li&gt;ext4&lt;/li&gt;
&lt;li&gt;btrfs&lt;/li&gt;
&lt;li&gt;xfs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然说文件系统的种类很多，但大部分 linux 下文件系统都有着类似的结构，包括超级块、inode、数据块、目录块等。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;超级块&lt;/strong&gt;包括了文件系统的总体信息，是文件系统的核心，所以磁盘中会有多个超级块，即使某一些超级块坏了，文件系统依然可用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;inode&lt;/strong&gt; 存储着所有与文件有关的数据，比如文件的权限等，并不包括文件内容和文件名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据块&lt;/strong&gt;是真实存储数据的，&lt;strong&gt;一个数据块默认的大小为 &lt;code&gt;4KB&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目录块&lt;/strong&gt;包括文件内容和文件名，以及 inode 的信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;注意：&lt;strong&gt;如果设备上没有分区，或者设备分区上没有文件系统，也会导致挂载失败&lt;/strong&gt;。你可以检查设备是否已经被格式化或有文件系统。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ sudo blkid /dev/nvme0n1
# 为空表示没有文件系统/还没被格式化过

$ sudo mkfs.ext4 /dev/nvme0n1
mke2fs 1.45.5 (07-Jan-2020)
Discarding device blocks: done                            
Creating filesystem with 122096646 4k blocks and 30531584 inodes
Filesystem UUID: 36df5186-24a0-4dad-9b4e-664a4230b7f1
Superblock backups stored on blocks: 
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 
        4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968, 
        102400000

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done

$ sudo blkid /dev/nvme0n1
/dev/nvme0n1: UUID=&quot;36df5186-24a0-4dad-9b4e-664a4230b7f1&quot; TYPE=&quot;ext4&quot;

$ sudo mount &amp;#x3C;设备&gt; &amp;#x3C;挂载点&gt;

# 已经被格式化过
$ sudo blkid /dev/nvme1n1
/dev/nvme1n1: PTUUID=&quot;8e912df5-211c-45ea-a422-6a0f83c045c3&quot; PTTYPE=&quot;gpt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;接下来 &lt;code&gt;mkfs.ext4 /dev/sdb1&lt;/code&gt; 格式化该分区（&lt;code&gt;mkfs&lt;/code&gt; 后面跟着 &lt;code&gt;.&lt;/code&gt; 与文件系统的格式表明要将目标分区格式化成什么文件系统）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;root@linux:~$ mkfs.ext4 /dev/sdb1
mke2fs 1.42.13 (17-May-2015)
Creating filesystem with 2621179 4k blocks and 655360 inodes
Filesystem UUID: 886f1d1a-a3ad-4bdf-89b3-54ee0c9238f2
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632
Allocating group tables: done
Writing inode tables: done
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;mount&lt;/h2&gt;
&lt;p&gt;在文件系统中创建一个空目录作为挂载点，并将分区挂载到挂载点&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;root@linux:~$ mkdir /mnt/new_mountpoint
root@linux:~$ mount -t ext4 /dev/sdb1 /mnt/new_mountpoint/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时 &lt;code&gt;/mnt/new_mountpoint/&lt;/code&gt; 下面的存储空间就是此次新添加的10GB磁盘的存储空间，可以用 &lt;code&gt;df -h&lt;/code&gt; 查看&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;root@linux:~$ df -h
Filesystem      Size  Used Avail Use% Mounted on
udev            3.9G     0  3.9G   0% /dev
tmpfs           799M  9.1M  790M   2% /run
/dev/sda1        12G  4.5G  6.7G  40% /
tmpfs           3.9G  188K  3.9G   1% /dev/shm
tmpfs           5.0M  4.0K  5.0M   1% /run/lock
tmpfs           3.9G     0  3.9G   0% /sys/fs/cgroup
tmpfs           799M   24K  799M   1% /run/user/108
tmpfs           799M     0  799M   0% /run/user/1000
/dev/sdb1       9.8G   23M  9.2G   1% /mnt/new_mountpoint
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;在使用 &lt;code&gt;mount&lt;/code&gt; 命令时，设备（source）和挂载点（target）的顺序很重要。&lt;strong&gt;设备&lt;/strong&gt;应该在前面，&lt;strong&gt;挂载点&lt;/strong&gt;在后面。具体的语法格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ mount [选项] &amp;#x3C;设备&gt; &amp;#x3C;挂载点&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;具体说明&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;设备&lt;/strong&gt;：指的是你要挂载的块设备或文件系统的路径，比如 &lt;code&gt;/dev/sda1&lt;/code&gt; 或 &lt;code&gt;/dev/nvme0n1p1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;挂载点&lt;/strong&gt;：指的是挂载该设备的目录路径，比如 &lt;code&gt;/mnt/nvme0n1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;挂载设备&lt;/strong&gt;： 假设你有一个设备 &lt;code&gt;/dev/sda1&lt;/code&gt;，你想挂载到 &lt;code&gt;/mnt/data&lt;/code&gt; 目录下，使用如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo mount /dev/sda1 /mnt/data
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;挂载文件系统类型&lt;/strong&gt;： 如果你想指定文件系统类型（例如 &lt;code&gt;ext4&lt;/code&gt;），可以使用 &lt;code&gt;-t&lt;/code&gt; 选项：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo mount -t ext4 /dev/sda1 /mnt/data
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;卸载设备&lt;/strong&gt;： 卸载挂载的设备使用 &lt;code&gt;umount&lt;/code&gt; 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo umount /mnt/data
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;常用选项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-t &amp;#x3C;type&gt;&lt;/code&gt;：指定文件系统类型，如 &lt;code&gt;ext4&lt;/code&gt;、&lt;code&gt;xfs&lt;/code&gt; 等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o &amp;#x3C;options&gt;&lt;/code&gt;：指定挂载选项，如 &lt;code&gt;ro&lt;/code&gt;（只读）、&lt;code&gt;rw&lt;/code&gt;（读写）、&lt;code&gt;noatime&lt;/code&gt;（不更新访问时间）等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;fdisk&lt;/code&gt; 分区挂载更加灵活，可以将磁盘划分为多个分区，每个分区可以有不同的文件系统类型，分区完后仍然需要对每个分区 &lt;code&gt;mkfs&lt;/code&gt; 格式化文件系统，或者也可以直接 &lt;code&gt;mkfs&lt;/code&gt; 选择对整个磁盘格式化，但整个磁盘将只能使用一个文件系统类型，无法将磁盘分割为多个独立的区域。&lt;/p&gt;
&lt;h2&gt;常用命令 lsblk、df、du&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;wyk 09:20:49 ~
$ lsblk
NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
loop0         7:0    0  79.9M  1 loop /snap/lxd/22923
loop1         7:1    0    62M  1 loop /snap/core20/1587
loop3         7:3    0  38.8M  1 loop /snap/snapd/21759
loop4         7:4    0  63.9M  1 loop /snap/core20/2318
loop5         7:5    0    87M  1 loop /snap/lxd/28373
sda           8:0    0 447.1G  0 disk 
├─sda1        8:1    0     1G  0 part 
└─sda2        8:2    0 446.1G  0 part /old8003-2
sdb           8:16   0   1.8T  0 disk /home/cyf/hdd
nvme4n1     259:0    0 894.3G  0 disk /home/lzh/ssd
nvme3n1     259:1    0 894.3G  0 disk /home/wyk/ssd
nvme2n1     259:2    0 894.3G  0 disk 
├─nvme2n1p1 259:3    0     1G  0 part /boot/efi
└─nvme2n1p2 259:4    0 893.2G  0 part /
nvme0n1     259:5    0 894.3G  0 disk /home/cyf/ssd0
nvme1n1     259:6    0 894.3G  0 disk /home/cyf/ssd1

wyk 09:21:02 ~
$ df -h
Filesystem      Size  Used Avail Use% Mounted on
tmpfs            13G  3.3M   13G   1% /run
/dev/nvme2n1p2  879G  308G  527G  37% /
tmpfs            63G     0   63G   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
/dev/nvme2n1p1  1.1G  6.1M  1.1G   1% /boot/efi
/dev/sda2       439G  336G   81G  81% /old8003-2
/dev/nvme3n1    880G   28K  835G   1% /home/wyk/ssd

wyk 09:17:50 ~
$ sudo du -h --max-depth=1 /old8003-2/home/
30G     /old8003-2/home/ymx
6.4G    /old8003-2/home/cjh
72G     /old8003-2/home/cyf
15G     /old8003-2/home/zzh
2.1G    /old8003-2/home/jyc
525M    /old8003-2/home/astl
2.7G    /old8003-2/home/lcc
1.1G    /old8003-2/home/whs
1.6G    /old8003-2/home/olh
6.4G    /old8003-2/home/wyk
95G     /old8003-2/home/lzh
8.0G    /old8003-2/home/zyu
239G    /old8003-2/home

wyk 09:24:37 ~
$ lsblk -d -o name,rota
NAME    ROTA
loop0      0
loop1      0
loop3      0
loop4      0
loop5      0
sda        0
sdb        1
nvme4n1    0
nvme3n1    0
nvme2n1    0
nvme0n1    0
nvme1n1    0
wyk 09:24:44 ~
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;lsblk -d -o name,rota&lt;/code&gt; 对于其返回值，看 ROTA 值来判断：&lt;/p&gt;
&lt;p&gt;若 ROTA=1，则意味该硬盘旋转，则其为机械硬盘；&lt;/p&gt;
&lt;p&gt;若 ROTA=0，则意味着该盘为固态硬盘；&lt;/p&gt;
&lt;p&gt;对于上述打印结果，&lt;strong&gt;sdb&lt;/strong&gt; 为固态硬盘，&lt;strong&gt;sda&lt;/strong&gt; 为机械硬盘。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;执行 &lt;code&gt;sudo umount /old8003-1&lt;/code&gt; &lt;strong&gt;不会擦除 SSD 上的数据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;umount&lt;/code&gt; 命令只是&lt;strong&gt;卸载文件系统&lt;/strong&gt;，意味着它将断开挂载点和设备的关联，让该挂载点不再可访问。数据仍然保留在 SSD 上的分区中，&lt;strong&gt;没有被删除&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;简单来说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;umount&lt;/code&gt;: 只断开文件系统，不会对数据产生任何影响。&lt;/li&gt;
&lt;li&gt;数据还在磁盘上，只是暂时不可通过该挂载点访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要删除数据，通常需要&lt;strong&gt;格式化磁盘&lt;/strong&gt;或者手动删除文件。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;某个 ssd/hdd 还挂载在某个目录下，如果要清除上面的数据并挂载到自己指定目录下要怎么做（全流程）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;这将显示所有在 &lt;code&gt;/old8003-1&lt;/code&gt; 目录下活动的文件和进程。如果有进程正在使用该目录，可以终止它们或手动关闭它们：&lt;code&gt;sudo lsof /old8003-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;格式化前先取消磁盘的挂载 &lt;code&gt;sudo umount /old8003-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;可以选择 &lt;code&gt;sudo fdisk /dev/nvme3n1&lt;/code&gt; 对磁盘进行分区，也可以不分区， 直接把整个磁盘格式化（就没有 nvme3n1p1、nvme3n1p2 这类分区了）&lt;/li&gt;
&lt;li&gt;指定分区的格式化文件系统类型 &lt;code&gt;sudo mkfs.ext4 /dev/nvme3n1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最后挂载上去即可 &lt;code&gt;sudo mount ~/ssd /dev/nvme3n1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/202501222240708.iCDyqeox.png"/><enclosure url="/_astro/202501222240708.iCDyqeox.png"/></item><item><title>广东·广州</title><link>https://coooredump.github.io/blog/tourism/guangzhou_2024-11-29</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/guangzhou_2024-11-29</guid><description>落叶随着风一阵摆动，家乡的银杏树一直都在，可是我已经回不去了</description><pubDate>Fri, 29 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100402666.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100403226.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100401275.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100403520.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506180307379._VaB681J.jpg"/><enclosure url="/_astro/202506180307379._VaB681J.jpg"/></item><item><title>关于 mmap 与 read/write</title><link>https://coooredump.github.io/blog/system-architecture/mmap-vs-read-write</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/mmap-vs-read-write</guid><description>前几天在看 malloc 实现资料的时候，发现自己并不是非常理解 mmap 的作用，于是查了一些资料，顺便把以前的知识梳理一下。</description><pubDate>Fri, 01 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;原文链接：https://nineright.github.io/2014/03/12/mmap-io.html&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;前几天在看 &lt;code&gt;malloc&lt;/code&gt; 实现资料的时候，看到 &lt;code&gt;mmap&lt;/code&gt;，发现自己并不是非常理解 mmap 的作用，于是查了一些资料，顺便把以前的知识梳理一下，于是就有了这篇博文。&lt;/p&gt;
&lt;p&gt;Linux 下对文件的访问主要有两种方式。一种是 read/write/seek，而另外一种是利用 mmap 系统调用将整个文件映射到内存中。$[8][9]$对比两种方式的性能，测试结果如图 1、2 所示。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;图1：mmap 读写文件测试&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202411011704564.jpeg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;图2：mmap 图像处理性能测试 (scale 是测试程序的一个参数)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202411011704724.jpeg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;mmap 的优势&lt;/h2&gt;
&lt;p&gt;Stackoverflow 上$[2]$有一个很好的讨论，对比 &lt;code&gt;read&lt;/code&gt; 和 &lt;code&gt;mmap&lt;/code&gt;。总结一下，通常使用 mmap() 的三种情况最终目的其实都一样：提高效率。&lt;/p&gt;
&lt;p&gt;🔥&lt;strong&gt;提高 I/O 效率&lt;/strong&gt;：传统的 file I/O 中 read 系统调用首先从磁盘拷贝数据到 kernel，然后再把数据从 kernel 拷贝到用户定义的 buffer 中（可能是 heap 也有可能是 stack 或者是全局变量中$[6]$）。而 mmap 直接由内核操刀，mmap 返回的指针指向映射内存的起始位置，然后可以像操作内存一样操作文件，而且如果是用 read/write 将 buffer 写回 page cache 意味着整个文件都要与磁盘同步（即使这个文件只有个别 page 被修改了），而 mmap 的同步粒度是 page，可以根据 page 数据结构的 dirty 位来决定是否需要与 disk 同步。这是 mmap 比 read 高效的主要原因。对于那种频繁读写同一个文件的程序更是如此。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;匿名内存映射&lt;/strong&gt;：匿名内存映射有点像 malloc()，其实 Heap 和 BSS 段就可以看成是一个 anonymous mmap [图 4]。有些 malloc 的实现中，当要分配较大的内存块时，malloc 会调用 mmap 进行匿名内存映射，此时内存操作区域不是堆区，内存释放后也会直接归还给 OS，不像 heap 中的内存可以再利用。匿名内存不是 POXIS 的标准，但是几乎所有的 OS 实现了这个功能。一个目前功能最好的 malloc 实现 dlmalloc 就是采用这种方式$[4][5][6]$。dlmalloc 有三种分配方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(1) 小于 64B 的 exact-size quicklist；&lt;/li&gt;
&lt;li&gt;(2) 小于 128KB 的 coalesce quicklist；&lt;/li&gt;
&lt;li&gt;(3) 对于较大请求直接调用 mmap。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;共享内存进程通信&lt;/strong&gt;：相对于管道、消息队列方式，通过内存映射的方式进程通信效率明显更高，它不需要任务数据拷贝。说到共享内存，还有一种 System V 保留下来的内存共享方法就是 shmget 相关系统调用。两者相比 mmap 更加简单易用一些，而 shmget 提供的功能更全面一些。&lt;/p&gt;
&lt;h2&gt;mmap 的一些限制&lt;/h2&gt;
&lt;p&gt;当然 &lt;code&gt;mmap&lt;/code&gt; 也有一定的限制。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;mmap 的对齐方式是 page 为大小的&lt;/strong&gt;，有存在内存内部碎片的可能（调用的时候 length 没有对齐)，所以 &lt;strong&gt;mmap 不适合小文件&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mmap 后的内存大小不能改变&lt;/strong&gt;。当一个文件被 mmap 后，如果其他程序要改变文件的大小，要特别留意[8]。&lt;/li&gt;
&lt;li&gt;mmap 不能处理所有类型的文件，例如 pipes、tty、网络设备文件就不能处理。&lt;/li&gt;
&lt;li&gt;mmap 要求进程提供一块连续的虚拟内存空间，对于大文件（1G）的内存映射。有时候会失败。尽管空闲内存大于 1G，但是很有可能找不到连续的 1G 的内存空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mmap 对于那种批量写 (write-only) 的情景并没有优势&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$[1]$讨论了在传统的数据库中对数据库文件的相关操作“为什么不用 mmap？”。传统数据库对 datafile 的读写大部分是通过 &lt;code&gt;open&lt;/code&gt; 系统调用加 &lt;code&gt;O_DIRECT&lt;/code&gt; 标志。使用 O_DIRECT 标志可以跳过 kernel 的 page cache 而直接与 block device（如磁盘）打交道，与普通的 read/write 相比少了一层缓存 (page cache)，数据库开发者通过实现在用户层的高效缓存来达到提高效率目的。但是这带来很大的复杂性问题，首先使用 O_DIRECT 的话，就必须以页为单位进行 I/O，而且既然放弃 kernel 的提供 page cache 以及相关的缓存策略，那么意味着是想通过 O_DIRECT 提供自己的更好的缓存策略，这个往往是很困难的。在 Linus 看来$[12]$，“O_DIRECT 是一个非常糟糕的接口，目前只为数据库开发者保留，其他人使用都属于脑残的行为”。数据库开发者想通过直接与 device 打交道（简单说，他们觉得能比 OS 干得更好），来提高 I/O 性能，例如提供更适合数据库的缓存策略（如 LIRS 缓存算法$[13]$）。例如 Innodb 中通过配置文件的形式提供了两种方式读写数据文件，一种是传统的 read/write 读写，一种是 O_DIRECT 访问。一般情况下 O_DIRECT 的性能要高$[11]$。&lt;/p&gt;
&lt;h2&gt;处理高并发的方案&lt;/h2&gt;
&lt;p&gt;在作者看来，用 mmap 管理是一个可行的方案：&lt;strong&gt;使用 mmap 可以减少 kernel 与 user space 之间的 context switch&lt;/strong&gt;；kernel 提供了 page cache 高效的缓存管理；内存被共享时 kernel 提供了同步功能。除了 mmap，配合 &lt;code&gt;mlock()&lt;/code&gt;、&lt;code&gt;madvise()&lt;/code&gt;、&lt;code&gt;msync()&lt;/code&gt;，开发者能够更自由的控制缓存策略。像 MongoDB 就是使用 mmap 来读写数据文件的。但是由于使用 madvise 的人不多，kernel 好像并没有利用 madvise 信息，或者效果不是很好$[12]$。 文中最后提了这种方式如何应对高并发的请求，并提了一些解决方案。&lt;/p&gt;
&lt;p&gt;Coroutine，用户级的协程的好处就是比 thread 的开销更小，但是有一个很大的问题，一旦一个协程调用系统调用阻塞时（如等待 I/O），协程所属的线程就会阻塞，也就意味着其他协程也要跟着阻塞。这里有几种解决方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果能够容忍阻塞对性能的影响，就不做处理。&lt;/li&gt;
&lt;li&gt;为阻塞的协程新建一个内核线程专门等待系统调用完成，这样其他协程就可以继续， Goroutine 采用的就是这种机制。&lt;/li&gt;
&lt;li&gt;使用 NonblockingIO，文中提到了 epoll+eventfd$[14]$。&lt;code&gt;Epoll&lt;/code&gt; 是 linux 下的一种多路复用技术，功能与 &lt;code&gt;select&lt;/code&gt;，&lt;code&gt;poll&lt;/code&gt; 一样，eventfd 则与 pipe 有点像，它通过创建的事件对象来读写一个 64 位的整数计数器，线程之间通过协商好事件对应的数值来协调通信。&lt;/li&gt;
&lt;li&gt;对于 O_DIRECT 访问 Disk，使用异步 IO，当然也可一配合 epoll 使用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$[1]$文中的评论给出了一些不同看法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;频繁的使用 mmap 会很容易耗尽内存的资源&lt;/strong&gt;，特别对于 32 位的机器。作者提出可以使用 cgroups$[15]$来控制资源的使用。&lt;/li&gt;
&lt;li&gt;mmap 并不适合那种 write-only 的场景，或者说这个时候没什么性能优势，而 database 的 commit log 就属于这一类型。&lt;/li&gt;
&lt;li&gt;mmap 不能提供一些灵活的控制缓存的需求，例如控制不同缓存块的写入顺序等等。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;图3：Linux虚拟内存空间（截图自 CSAPP 第二版）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202411011725615.jpeg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;图4：Linux 虚拟内存空间对应的重要数据结构$[16]$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202411011726033.jpeg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Page Cache&lt;/h2&gt;
&lt;p&gt;上文的讨论中，涉及到一个很重要的概念——Page Cache，简单概括，就是磁盘的数据以页式缓存的形式保存在内存中。可以从两个方面去理解 Page 和 Cache。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cache：缓存存在的最主要目的为了解决设备读写速度不均横的问题。例如内存可以理解为 Disk 等设备的缓存，CPU Cache 可以理解为内存的缓存。图 5 给出了传统计算机各个部件的速度，非常的直观。简单说一个好的缓存设计可以大大提高系统 IO 的性能。&lt;/li&gt;
&lt;li&gt;Page：Page 是 Disk block 在内存中的缓存结构，类似的 Cache Line 是内存数据在 CPU Cache 中的缓存单元。一般 Page 大小是可配置的，一般是 Disk block 的倍数，但是一个 Page 对应的多个 block 在 Disk 中不一定是连续的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个高效的缓存，必然会涉及到几个问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缓存替换算法&lt;/strong&gt;：最有名莫过于 LRU (Least Recently Used) 算法，但是 LRU 在批量读写大文件的时候，会清空当前缓存，而如果读取的大文件只是读取一次，那么意味着之前缓存的数据又要从磁盘重新读取，为了避免这种情况 Kernel 使用两条链表，一条 Hot 链表，存的是被访问一次以上的数据，而另一个链表存放第一次读取的 Page，两个链表都使用 LRU 算法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据写回侧策略&lt;/strong&gt;：主要两种：write through 和 write back。Kernel 为了效率使用 write back。后台使用 flush 线程将 page cache 与磁盘等设备同步。有三种情况会触发这个同步线程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;内存空闲（未与磁盘同步）页表数低于系统设定阈值；&lt;/li&gt;
&lt;li&gt;dirty 的页在内存中存在超过系统设置的时间；&lt;/li&gt;
&lt;li&gt;用户调用 &lt;code&gt;sync()&lt;/code&gt;，&lt;code&gt;fsync()&lt;/code&gt; 系统调用。Linux Kernel 中 flush 的线程数目等于系统磁盘（持久化设备）的个数，其实同步线程有一个演化的过程，从 bdflush 和 kupdated 配合使用到动态个数的 pdflush 线程再到现在的与外部设备等个数的 flush 线程。具体可参考$[17]$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;如何高效判断数据已在缓存中&lt;/strong&gt;：Kernel 中使用 &lt;strong&gt;Radix Tree&lt;/strong&gt; 进行索引，在 2.6 版本以前使用全局的 Hash Table 效率较低。现在是每个文件都会有一个 radix tree。提供一个文件的偏移值（偏移值对应 radix tree 的 key），可以在常数（与偏移值的位数有关）时间内找到对应的 page 项，如果没有，则先分配一个，再返回，并且每个 page 项表明了是否 dirty 等信息。&lt;strong&gt;Radix Tree 是 Trie 的压缩版本，Trie 是一个节点一个字符，Radix Tree 允许多个字符。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有关 Page Cache 的更细节的东西，可以参考《Linux kernel development》$[17]$第13 章。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;图5：一个典型 X86 架构各个部件的速度$[16]$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202411011738428.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202501222240708.iCDyqeox.png"/><enclosure url="/_astro/202501222240708.iCDyqeox.png"/></item><item><title>NVM 存储系统领域比较厉害的实验室</title><link>https://coooredump.github.io/blog/system-architecture/nvm-top-lab</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/nvm-top-lab</guid><description>步入研究生，开始了 NVM 方向的科研，近半年来逐渐探索到了一些在 NVM 方向上做的比较不错的团队，方便对 NVM 方向感兴趣的同学关注大佬团队的最新论文，也希望可以帮助想申请 NVM 方向 Ph.D 的同学，如有不足的地方，希望大家多多补充。</description><pubDate>Fri, 01 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;International Lab&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;美国 佐治亚理工&lt;/strong&gt; Joy Arulraj ，研究方向是 NVM上的database，毕业于CMU，导师是数据库大神 Andy Pavlo，个人网站 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//www.cc.gatech.edu/~jarulraj/&quot;&gt;https://www.cc.gatech.edu/~jarulraj/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;美国 UCSD大学&lt;/strong&gt; Non-Volatile Systems Lab 负责人Steven Swanson ，主要研究NVM上的软件，论文发表在体系结构领域的顶会上，个人网站 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//swanson.ucsd.edu/&quot;&gt;https://swanson.ucsd.edu/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;加拿大 Simon Fraser大学&lt;/strong&gt; Tianzheng Wang，主要研究NVM上的数据库索引，论文主要发表在数据库领域的顶会上，Dash 论文获得了vldb2020 的 best paper ，个人主页 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//www2.cs.sfu.ca/~tzwang/pubs.html&quot;&gt;https://www2.cs.sfu.ca/~tzwang/pubs.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;美国 Virginia Tech 大学&lt;/strong&gt;的 Changwoo Min，主要研究NVM上的索引和存储，论文发表在操作系统领域，个人网站 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//multics69.github.io/&quot;&gt;https://multics69.github.io/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;美国 UC,merced大学&lt;/strong&gt;的 Dong Li，主要研究NVM上高性能应用，Dong Li老师主要是做高性能和并行计算的，近几年开始研究将高性能计算和NVM结合起来，文章都发在了体系结构和超算的顶会上，个人网站 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//faculty.ucmerced.edu/dong-li/&quot;&gt;https://faculty.ucmerced.edu/dong-li/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;新加坡第四范式&lt;/strong&gt; Chen Cheng ,主要研究NVM上的database ,设计基于NVM的系统，个人网站&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//sites.google.com/site/chencheng1560/home&quot;&gt;https://sites.google.com/site/chencheng1560/home&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;韩国 KAIST&lt;/strong&gt; 的 Myoungsoo Jung，主要研究 flash、SSD和NVM存储方面的工作，还有并行计算、异构计算方面的高性能计算工作，论文发表在体系结构领域和高性能计算领域顶会上，个人网站 &lt;a href=&quot;https://link.zhihu.com/?target=http%3A//camelab.org/&quot;&gt;http://camelab.org/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Domestic Lab&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;清华 舒继武老师团队&lt;/strong&gt; &lt;a href=&quot;https://link.zhihu.com/?target=http%3A//storage.cs.tsinghua.edu.cn/~jiwu-shu/&quot;&gt;http://storage.cs.tsinghua.edu.cn/~jiwu-shu/&lt;/a&gt;，感觉陆游游老师在团队中很出色，年轻有为，主要研究NVM上的存储系统和 Flash 存储系统，个人主页 &lt;a href=&quot;https://link.zhihu.com/?target=http%3A//storage.cs.tsinghua.edu.cn/~lu/&quot;&gt;Home - Youyou Lu (tsinghua.edu.cn)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;清华 武永卫老师团队&lt;/strong&gt; &lt;a href=&quot;https://link.zhihu.com/?target=http%3A//madsys.cs.tsinghua.edu.cn/~yongweiwu/&quot;&gt;http://madsys.cs.tsinghua.edu.cn/~yongweiwu/&lt;/a&gt;，研究NVM上的数据结构和应用，他们组里图计算的工作也很出色，工作发表在数据库，存储和并行计算领域&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;上交 陈海波老师团队&lt;/strong&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//ipads.se.sjtu.edu.cn/zh/members/&quot;&gt;https://ipads.se.sjtu.edu.cn/zh/members/&lt;/a&gt;，将NVM和RDMA结合做研究，陈老师组里的文章可谓是 操作系统领域、体系结构领域、高性能计算领域顶会大满贯了，很厉害&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;计算所 陈世敏老师团队&lt;/strong&gt; &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//www.shimin-chen.com/&quot;&gt;https://www.shimin-chen.com/&lt;/a&gt;，研究NVM上的数据库索引和NVM上的系统，文章发表在数据库和存储领域；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;华科 金海老师团队&lt;/strong&gt; &lt;a href=&quot;https://link.zhihu.com/?target=http%3A//grid.hust.edu.cn/index.htm&quot;&gt;http://grid.hust.edu.cn/index.htm&lt;/a&gt;，金老师的组里还有很多老师，有的老师在NVM上搞研究，具体文章可以看他们的团队官网&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;华科 华宇老师团队&lt;/strong&gt; &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//csyhua.github.io/csyhua/index.html&quot;&gt;https://csyhua.github.io/csyhua/index.html&lt;/a&gt;，天才少年 Pengfei Zuo就是华老师的学生，他们在NVM上的研究很多，发表在体系结构、系统、存储、数据库等领域&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/202501222317097.HGE7Lqd0.jpeg"/><enclosure url="/_astro/202501222317097.HGE7Lqd0.jpeg"/></item><item><title>持久性内存｜Persistent Memory</title><link>https://coooredump.github.io/blog/system-architecture/persistent-memory-research-report</link><guid isPermaLink="true">https://coooredump.github.io/blog/system-architecture/persistent-memory-research-report</guid><description>持久化内存（Persistent Memory，简称 PMEM），也叫非易失性内存（Non-Volatile Memory，简称 NVM），是指一类支持字节寻址（Byte-Addressable）、可以通过 CPU 指令直接进行操作、断电后数据不丢失的存储硬件。</description><pubDate>Fri, 01 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;分层存储&lt;/h2&gt;
&lt;p&gt;学过计算机原理的人都知道，计算机系统的存储采用分层的结构（如上图所示 ）。其中，CPU regiesters、CPU Caches 和 DRAM 都是易失性的（volatile），SSD、HDD、Tape 是非易失性的（non-volatile）。&lt;/p&gt;
&lt;p&gt;企业级 SSD 可以提供 10 微秒级别的响应时间，DRAM 的响应时间大约是 100 纳秒这个级别。SSD 的响应时间和 DRAM 有着差不多 100 倍的差距。而 DRAM 和最后一级 CPU Cache 的响应时间只有 8~10 倍的差距。&lt;/p&gt;
&lt;p&gt;很明显，DRAM 和 SSD 之间存在比较巨大的性能鸿沟。所以在设计应用程序的时候，需要特别注意 I/O 相关的操作，避免 I/O 成为系统的性能瓶颈。&lt;/p&gt;
&lt;h2&gt;持久化内存&lt;/h2&gt;
&lt;p&gt;持久化内存（Persistent Memory，简称 PMEM），也叫非易失性内存（Non-Volatile Memory，简称 NVM），是指一类支持字节寻址（Byte-Addressable）、可以通过 CPU 指令直接进行操作、断电后数据不丢失的存储硬件。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pmem.io/persistent-memory/getting-started-guide/introduction&quot;&gt;计算机系统的分层存储&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202411011829692.png&quot; alt=&quot;20200910163421&quot;&gt;&lt;/p&gt;
&lt;p&gt;PMEM 提供亚微秒级别的延迟时间。从成本、性能、容量上看，PMEM 是位于 DRAM 和 SSD 之间的一层存储。PMEM 的出现，填补了 DRAM 和 SSD 之间的性能鸿沟，同时也将影响存储软件的架构设计。&lt;/p&gt;
&lt;h2&gt;Optane DIMMs&lt;/h2&gt;
&lt;p&gt;「&lt;strong&gt;学术界&lt;/strong&gt;」在很早以前就开始了对持久化内存的研究，比如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//www.usenix.org/system/files/conference/fast16/fast16-papers-xu.pdf&quot;&gt;NOVA: A Log-structured File System for Hybrid Volatile/Non-volatile Main Memories&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=http%3A//www.vldb.org/pvldb/vol11/p553-arulraj.pdf&quot;&gt;Bztree: A High-performance Latchfree Range Index for Non-volatile Memory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//www.pdl.cmu.edu/PDL-FTP/NVM/storage.pdf&quot;&gt;Let&apos;s talk about storage: Recovery methods for nonvolatile memory database systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;但以前没有真实的持久化内存硬件，只能基于软件模拟器进行仿真测试&lt;/strong&gt;。直到 2019 年 4 月，Intel 发布了第一款企业级的持久化内存 —— Intel Optane DC Persistent Memory（下面简称 &lt;strong&gt;Optane DIMMs&lt;/strong&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;由于模拟器没法百分之百模拟硬件，之前通过模拟器仿真出来的研究结果和真实硬件下的测试结果还是有一些差别的&lt;/strong&gt;。在 FAST&apos;20 上，有人发表了一篇论文，介绍 Intel Optane DC Persistent Memory 的使用特点 —— &lt;a href=&quot;https://www.usenix.org/conference/fast20/presentation/yang&quot;&gt;An Empirical Guide to the Behavior and Use of Scalable Persistent Memory&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;Intel Optane DC Persistent Memory 是目前唯一一款量产的持久化内存，同时目前也只有 Intel 的 Cascade Lake 处理器支持这款持久化内存。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Intel Optane DC Persistent Memory&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202411011831743.png&quot; alt=&quot;image-20241101183105702&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，Optane DIMMs 采用和 DRAM 一样的 DIMM 接口。这意味着，Optane DIMMs 可以直接插在内存插槽上，通过内存总线和 CPU 通信，而 CPU 也可以通过指令直接操作 Optane DIMMs。&lt;/p&gt;
&lt;h2&gt;Optane DIMMs 的持久化&lt;/h2&gt;
&lt;p&gt;Optane DIMM 有两种工作模式：Memory Mode 和 App Direct Mode。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Memory Mode 简单说就是把 Optane DIMMs 当成易失性内存使用，把 DRAM 当作 CPU 和 Optane DIMMs 之间的 cache，并且 DRAM 对外不可见（就像 CPU 的多级 cache 对外也不可见）。基于 Memory Mode 的工作模式，可以通过应用无感知的方式，解决一些内存数据库（比如 Redis、Memcached）单机 DRAM 容量不足或成本过高的问题。&lt;/li&gt;
&lt;li&gt;App Direct Mode 将 Optane DIMMs 当成一个持久化设备来使用，直接通过 CPU 指令读写 Optane DIMMs，不需要经过 DRAM。应用可以使用能够感知持久化内存的文件系统（比如 EXT4-DAX、XFS-DAX、NOVA）或其他组件（比如 PMDK）来管理、操作持久化内存设备。 Memory Mode 由于不考虑持久化问题，一般情况下将其当做一块更大的 DRAM 使用即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 App Direct Mode 工作模式下，尽管 Optane DIMMs 设备本身是非易失的，但是由于有 CPU Cache 的存在，当设备掉电时，“还没写入” Optane DIMMs 的数据还是会丢失。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.usenix.org/sites/default/files/conference/protected-files/fast20_slides_izraelevitz.pdf&quot;&gt;Asynchronous DRAM Refresh&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202411011817888.png&quot; alt=&quot;image-20241101181747727&quot;&gt;&lt;/p&gt;
&lt;p&gt;为了数据的持久化，Intel 提出了 Asynchronous DRAM Refresh（ADR）机制。ADR 机制保证，一旦写请求达到 ADR 中的 WPQ（Write Pending Queue），就能保证数据的持久性。除了 WPQ，Optane DIMMs 上也有缓存数据，ADR 机制同样会保证这部分数据的持久化。&lt;/p&gt;
&lt;p&gt;但是，ADR 机制无法保证 CPU Cache 中的数据的持久化。为了保证 CPU Cache 上的数据持久化，可以调用 CLFLUSHOPT 或 CLWB 指令，将 CPU Cache Line Flush 到 Optane DIMMs 中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CLFLUSHOPT&lt;/code&gt; 指令执行完成后，CPU Cache 中的相关数据被逐出。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLWB&lt;/code&gt; 指令执行完成后，CPU Cache 中的相关数据依然有效。 由于，CLFLUSHOPT 和 CLWB 指令都是异步执行的，所以一般需要跟随一个 SFENCE 指令，以保证 Flush 执行完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CPU 还提供了 &lt;code&gt;NTSTORE&lt;/code&gt;（Non-temporal stores）指令可以做到数据写入的时候 bypass CPU Cache，这样就不需要额外的 Flush 操作了。&lt;/p&gt;
&lt;h2&gt;Optane DIMMs 的读写延迟&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202411011819816.png&quot; alt=&quot;image-20241101181909719&quot;&gt;&lt;/p&gt;
&lt;p&gt;从论文中的测试数据看，Optane DIMMs 的读延迟是 DRAM 的 2~3 倍。另外，Optane DIMMs 顺序读的速度是随机读的 1.8 倍，相比之下，DRAM 顺序读的速度只有随机读的 1.2 倍。&lt;/p&gt;
&lt;p&gt;由于写入 Optane DIMMs 的数据只需要到达 ADR 的 WPQ 即可，DRAM 和 Optane DIMMs 的写入延迟接近。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202411011820219.png&quot; alt=&quot;image-20241101182001161&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;图正上方的三个数字的含义是：load 线程数 / ntstore 线程数 / store + clwb 线程数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;DRAM 的读写带宽几乎不受数据大小和并发线程数的影响，速度很快并且非常稳定。对 DRAM 来说，读带宽大约是写带宽的 1.3 倍，但是 Optane DIMMs 读带宽是写带宽的 2.9 倍。&lt;/p&gt;
&lt;p&gt;Optane DIMMs 的读写带宽在读写数据大小为 256B 时达到最大值。这是因为 Optane DIMMs 虽然支持字节寻址，但是每次读写的最小粒度是 256B。当一次读操作小于 256B 时，会浪费一些带宽。当一次写操作小于 256B 时，就会被转换成一次 read-modify-write，造成写放大（这点和 SSD 很像，只不过粒度更小，SSD 一般是大于等于 4KB）。&lt;/p&gt;
&lt;p&gt;最后，根据上图的最右所示，Optane DIMMs 在并发线程数较多且访问数据为 4KB 时，带宽掉了个大坑 —— 这和 Optane DIMMs 内部结构有关。主要原因有两个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对内部 buffer/cache 和内存控制器（iMC） 的争用。&lt;/li&gt;
&lt;li&gt;由于多条 Optane DIMMs 采用 4KB 交叉的方式组织成一个完整的持久化内存地址空间。每次访问对齐的 4 KB，请求都只能落在一条 Optane DIMMs 上，无法发挥多条 Optane DIMMs 通道并行执行的能力。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202411011821895.png&quot; alt=&quot;image-20241101182129772&quot;&gt;&lt;/p&gt;
&lt;p&gt;✅论文的最后总结了 4 条 Optane DIMMs 的最佳实践：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Avoid random accesses smaller than &amp;#x3C; 256 B. 避免小于 256 字节的随机读写。&lt;/li&gt;
&lt;li&gt;Use non-temporal stores when possible for large transfers, and control of cache evictions. 大内存操作时，使用 ntstore 指令绕过 CPU Cache。&lt;/li&gt;
&lt;li&gt;Limit the number of concurrent threads accessing a 3D XPoint DIMM. 限制一个 Optane DIMMs 通道的并发数。&lt;/li&gt;
&lt;li&gt;Avoid NUMA accesses (especially read-modify-write sequences). 避免 NUMA 访问。其实内存也一样，远端内存比本地内存要慢不少，这个问题在 Optane DIMMs 表现更突出，需要特别注意。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;更具体的内容，建议大家去看论文。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://arxiv.org/pdf/1903.05714v3&quot;&gt;Basic Performance Measurements of the Intel Optane DC Persistent Memory Module&lt;/a&gt; 这一篇也比较有代表性，数据更详细。&lt;/p&gt;
&lt;h2&gt;PMDK 简介&lt;/h2&gt;
&lt;p&gt;前面说了，为了保证数据的持久化，需要在合适的地方用一些底层的 CPU 指令来保证。这样做有两个明显的缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;太过于底层，代码写起来麻烦。&lt;/li&gt;
&lt;li&gt;可移植性差，不同 CPU 的指令是不一样的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了简化基于持久化内存的应用开发，Intel 开发和维护了 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//pmem.io/&quot;&gt;Persistent Memory Development Kit&lt;/a&gt; 这个开源组件。虽然这个组件目前由 Intel 开发和维护，但是理论上 PMDK 是与具体的硬件平台无关的——虽然现在依然只有 Intel 的一款持久化内存量产了。&lt;/p&gt;
&lt;p&gt;PMDK 中的库可以分成两大类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Volatile libraries。如果不关心数据的持久化，只想通过 persistent memory 扩展内存，可以使用这一类库。&lt;/li&gt;
&lt;li&gt;Persistent libraries。如果想要保证数据的 fail-safe，需要使用这一类库。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Volatile libraries&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//github.com/memkind/memkind&quot;&gt;libmemkind&lt;/a&gt; 提供 malloc 风格的接口，可以将持久化内存当成 DRAM 使用。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//pmem.io/vmemcache/manpages/master/vmemcache.3.html&quot;&gt;libvmemcache&lt;/a&gt; 是一个针对持久化内存的特点优化的易失性 LRU 缓存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Persistent libraries&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//pmem.io/pmdk/libpmem/&quot;&gt;libpmem&lt;/a&gt; 提供比较底层的操作持久化内存的接口，比如 pmem_map 类似 mmap、pmem_memcpy 类似 memcpy，具体可以参考官方文档。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//pmem.io/pmdk/libpmemobj/&quot;&gt;libpmemobj&lt;/a&gt; 提供基于持久化内存的对象存储能力。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//pmem.io/pmemkv/&quot;&gt;libpmemkv&lt;/a&gt; 是一个基于持久化内存的嵌入式 Key-Value 引擎，基于 B+ 树实现，针对读优化（&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//pmem.io/2017/02/21/pmemkv-intro.html&quot;&gt;libpmemkv 的内部实现&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//pmem.io/pmdk/manpages/linux/master/libpmemlog/libpmemlog.7.html&quot;&gt;libpmemlog&lt;/a&gt; 提供 append-only 的日志文件接口。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//pmem.io/pmdk/libpmemblk/&quot;&gt;libpmemblk&lt;/a&gt; 提供块存储接口，简单说就是将持久化内存抽象成一个数组。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;除此之外，PMDK 还提供了一些工具和命令用于辅助开发和部署基于持久化内存的应用，具体参考 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//pmem.io/pmdk/&quot;&gt;PMDK 官方文档&lt;/a&gt;。&lt;/p&gt;</content:encoded><h:img src="/_astro/202501222309189.DVzkczSw.png"/><enclosure url="/_astro/202501222309189.DVzkczSw.png"/></item><item><title>漳州·东山岛</title><link>https://coooredump.github.io/blog/tourism/dongshandao_2024-10-30</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/dongshandao_2024-10-30</guid><description>工作日的短暂逃离💨</description><pubDate>Wed, 30 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;工作日的短暂逃离💨&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100338356.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100338119.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506180305950.BrYvYe_r.jpg"/><enclosure url="/_astro/202506180305950.BrYvYe_r.jpg"/></item><item><title>福建·福州</title><link>https://coooredump.github.io/blog/tourism/fuzhou_2024-10-01</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/fuzhou_2024-10-01</guid><description>有一些东西要靠消失才能证明它的珍贵，如果这是无法返航的日子，那我祝你们一路向前，桥都坚固，隧道都光明，如果不能，那就祝你们曾经的理想能足够支撑当下的生活，等到未来偶然的一天回到这里，再聚的时候，我们能轻轻释怀所有的冷雨，微笑着轻描淡写地说：不过些许风霜罢了。</description><pubDate>Tue, 01 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;有一些东西要靠消失才能证明它的珍贵，如果这是无法返航的日子，那我祝你们一路向前，桥都坚固，隧道都光明，如果不能，那就祝你们曾经的理想能足够支撑当下的生活，等到未来偶然的一天回到这里，再聚的时候，我们能轻轻释怀所有的冷雨，微笑着轻描淡写地说：不过些许风霜罢了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100315786.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506171618926.CvOZZQKj.jpg"/><enclosure url="/_astro/202506171618926.CvOZZQKj.jpg"/></item><item><title>宁夏·银川</title><link>https://coooredump.github.io/blog/tourism/yinchuan_2024-07-27</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/yinchuan_2024-07-27</guid><description>生活的底片从来不是遥远的白日梦，而是热爱生活的自己</description><pubDate>Sat, 27 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;银川即是 “归雁入胡天” 的古来边关，更是 “大漠孤烟直” 的具像化，览山公园的风景让我着迷，我走过沙漠，骑骆驼 🐫，吹过旷野的风，这种无拘无束，自由掌握人生的感觉真美好。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100249871.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506180137434.DFRhr9Ox.jpg"/><enclosure url="/_astro/202506180137434.DFRhr9Ox.jpg"/></item><item><title>四川·成都</title><link>https://coooredump.github.io/blog/tourism/chengdu_2024-07-14</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/chengdu_2024-07-14</guid><description>和我在成都的街头走一走，直到所有灯都熄灭了也不停留</description><pubDate>Sun, 14 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;和我在成都的街头走一走，直到所有灯都熄灭了也不停留&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100242570.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100245109.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100243573.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100243620.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506111742728.BueiXHl7.jpg"/><enclosure url="/_astro/202506111742728.BueiXHl7.jpg"/></item><item><title>陕西·西安</title><link>https://coooredump.github.io/blog/tourism/xian_2024-06-24</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/xian_2024-06-24</guid><description>舌尖上的长安</description><pubDate>Mon, 24 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202245848.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100126191.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100138863.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100149952.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100215393.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100224155.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501100214048.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506180127490.DLEkr6WL.jpg"/><enclosure url="/_astro/202506180127490.DLEkr6WL.jpg"/></item><item><title>浙江·杭州</title><link>https://coooredump.github.io/blog/tourism/hangzhou_2024-06-15</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/hangzhou_2024-06-15</guid><description>The 26th ChinaSys Workshop</description><pubDate>Sat, 15 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092331350.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092339001.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092338967.JPG&quot; alt=&quot;浙江大学·玉泉校区&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506180120748.CE6lybGR.jpg"/><enclosure url="/_astro/202506180120748.CE6lybGR.jpg"/></item><item><title>漳州·南靖土楼</title><link>https://coooredump.github.io/blog/tourism/zhangzhou_2024-05-01</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/zhangzhou_2024-05-01</guid><description>田螺坑土楼群 &amp; 长汀古城</description><pubDate>Wed, 01 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092318410.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092317520.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092319547.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506180108407.GnZO39wd.jpg"/><enclosure url="/_astro/202506180108407.GnZO39wd.jpg"/></item><item><title>福建·泉州</title><link>https://coooredump.github.io/blog/tourism/quanzhou_2024-01-28</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/quanzhou_2024-01-28</guid><description>泉州，是你一生有机会至少要去一次的城市</description><pubDate>Sun, 28 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092244722.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501092303188.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506172337572.DfEKi53x.jpg"/><enclosure url="/_astro/202506172337572.DfEKi53x.jpg"/></item><item><title>「2023 年终总结」别急着赶路，去感受路</title><link>https://coooredump.github.io/blog/yearly-review/2023-feel-the-journey</link><guid isPermaLink="true">https://coooredump.github.io/blog/yearly-review/2023-feel-the-journey</guid><description>2023 年，我一直在出发，一直在路上。这也许就是旅行的意义，相比直观的风景，更有意义的往往是预料之外的相遇，同样是一座城，每个人都有自己能讲的故事，只要我还没走到终点，就还有新的山水可以期待。所以别急着赶路，去感受路！</description><pubDate>Sun, 21 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401192100960.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x01. 毕业季🎓&lt;/h2&gt;
&lt;p&gt;临近毕业答辩的前两月，我的课题方向在哲哥的指导下才有了一定的形状，于是年后的 2 月初，我紧赶慢赶地回到实验室继续完成我的毕设，可算在 2 月底完成这篇学术垃圾。别人都在想着怎么降重，我倒好，论文查重率为 0%，要么开天辟地，要么学术垃圾，我这篇就不一样了，是属于开天辟地的学术垃圾。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;知网查重&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191459949.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;来自我哥的锐评&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191459210.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;论文答辩、毕业典礼、毕业合照、寄行李、转档案、退宿舍，在一条条通知的催促下完成最后的 ddl，既没有真实的别离痛苦，也没有休息的闲情逸致，忙起来就觉得烦躁，闲下来又感到空虚。如果非要用一个词来形容的话，那就是“仓促”，仓促到没有多留下几张合照。我总计划着在毕业前和所有小伙伴们单独都拍一张合照，但总觉得时间很长，机会还有很多，于是一拖再拖。拖着拖着，临近毕业我才发现，原来上次瞥过的那一眼，已经是最后一面了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;那些年学校的活动&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191513782.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;报复性消费那些年错过的食堂&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191512211.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;临近毕业那几天的晚霞格外瑰丽，那个时候，跑步比其他任何事都来得快乐，骑着小电驴觉得怎么会有这么轻便的工具，在深夜的操场上尽情感受风的形状。送别的时候，我们都约着再见，约着回来，其实大家心里都清楚，再怎么强买桂花同载酒，也终不似，少年游。毕业的那段时光，我们都爱说“毕业快乐”，其实毕业一点都不快乐，可明知是没有人会一直在舞台上，怎么这么多人都舍不得大步迈向自己的人生。也许是太过于珍视某些人、某些事，珍视过去的快乐，以至于大家试图通过各种方式想要延续大学这段时光，可惜镜花水月，江中央已经再也回不去了。&lt;/p&gt;
&lt;p&gt;其实，人与人之间，一个 moment 就足够了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;美好的不只是我的 22 岁，还有 22 岁的我。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191457597.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401192100649.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;毕业快乐&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401190037861.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401190037013.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;也许人生最大的遗憾，是一个人无法同时拥有青春和对青春的感受。我不知道未来会是一帆风顺，还是人海浮沉，但起码我可以在照片里留住这些瞬间，对我数年来的学海泛舟，做个了结。感谢所有的遇见，祝好，在未来的每一个瞬间！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191514689.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;✍️毕业论文写下的致谢，共勉&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401182334762.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x02. 武汉｜樱花时节🌸&lt;/h2&gt;
&lt;p&gt;2022 年末参加的一场比赛，让我有幸得到主办方邀请前去武汉 HUST 参加颁奖典礼。&lt;/p&gt;
&lt;p&gt;依稀记得三天的武汉之旅，两天半的时间在下着大雨，好在亲眼目睹了武汉樱花园里盛开的樱花。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sakura&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401190136238.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;武汉的汉口江滩去了、汉口码头去了、武汉美术馆去了、吉庆街去了、黎黄陂路去了、黄鹤楼去了、长江大桥去了、粮道街去了、户部巷去了、昙华林去了。唯一遗憾的就是没有提前预约湖北省博物馆，没有亲眼见一见那被誉为“天下第一剑”的越王勾践剑、改写世界音乐史的曾侯乙编钟。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;武漢 plog&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191452004.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x03. 杭州｜之江实习🧑🏻‍💻&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191129886.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;2023 年 4 月 28 日，完成毕设答辩后，我即刻飞往杭州，开启了在「之江实验室」长达 3 个月的实习生活，下面分享一下我这三个月的实习体验、租房经历以及杭州的风景，算是给这段实习画上一个圆满的句号。&lt;/p&gt;
&lt;h3&gt;杭州租房小记&lt;/h3&gt;
&lt;p&gt;到达杭州前，我提前联系好了一个中介帮忙租房，押金付了，人也到了，中介半天都联系不上，就这样一个人在酒店楼下坐到了傍晚。当时真以为自己被骗了，还求助前台小哥直接联系房东，问题才得以解决。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;人生建议：真的不要摆脱中介找房，如果真的需要，也找一个靠谱点的中介。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;房子在杭州「临安区」青山湖附近，属于杭州的郊区了，但是户型我很喜欢，房间很大，空间够容纳两个人。月租也只需要 ¥1400 而已，实习的住房补贴能够完全 cover，房东人也非常好。要说唯一的槽点，那就是住在 15 楼真的很不方便，每每上班坐电梯，总要遭受非人的折磨。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;租房日记&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401190218006.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在杭州待到了 8 月，随后转租出去，按理说我提前结束租房，房东完全可以依据合同条款不偿还剩余的租金和违约金，房东人美心善，见我还是学生，不仅给我退租金和押金，还给我额外的💰，着实感动。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;转租&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401190244832.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;房东退租金&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401190248704.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;上有天堂，下有苏杭&lt;/h3&gt;
&lt;p&gt;入职那会正好赶上 5.1 放假，匆匆忙忙办好入职手续后，就又放了一个小长假，正巧又碰上师兄来杭州找我玩，便开始攻略杭州各大旅游景点。&lt;/p&gt;
&lt;p&gt;正所谓东南形胜，三吴都会，钱塘自古繁华。西湖的美自然不必多说，除此外，我还走过京杭大运河，游过良渚古城遗迹，进过灵隐寺，见过雷峰塔，打卡过一块钱人民币背面同款的三潭印月，经历过武林夜市的繁华...&lt;/p&gt;
&lt;p&gt;这期间我也走到过「&lt;a href=&quot;https://space.bilibili.com/946974&quot;&gt;影视飓风&lt;/a&gt;」的基地，可惜只能远远眺望，这是我非常喜欢的一位 up，或者说，一个创作团队，看着他拍出那么多优秀的作品，总能被感染到，一次又一次点燃我摄影的激情。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;江南 · 杭州&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191537682.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不过杭州不愧被誉为“美食荒漠”，兜兜转转找不出什么比较有亮点的美食，但我那几个月确实胖了不少，匪夷所思，也许一方水土养一斤肉吧。当然这偌大的杭州也不是我短短几月能逛完的，毕竟我都是抽空出去旅游，感觉还有许多有趣的地方都没来得及参观，就这样离开了，心里还是有一点点遗憾的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;真 · 美食荒漠&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191544030.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;ZHEJIANG Lab · 之江实验室 · 实习&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;以下记录仅代表个人观点，存在较强的主观性，每个研究院、每个中心的情况都不一样，仅供参考。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之江实验室是「浙江省」创建的开放协同的混合所有制新型研发机构，依托「浙江大学」和「阿里巴巴」为主要研究力量，坐落在浙江杭州余杭区。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191127347.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;地理位置&lt;/h4&gt;
&lt;p&gt;公司的位置超级偏僻，在余杭区南湖那里，地图导航搜索 “之江实验室新园区”，而不是人工智能小镇那里。每天早上公司都有专门的班车，大概 5 分钟就到了，但是我住临安区，上班通勤时间差不多要 40 分钟。&lt;/p&gt;
&lt;h4&gt;工作时间&lt;/h4&gt;
&lt;p&gt;初到杭州，给我的第一感受就是，大城市的节奏是会快一些，这里的人脚步快、做事也快，比如下电梯基本不会站立等待，会加快脚步跟着走，显得我有一丝丝格格不入。之江实验室反倒有些许不同，你在这里可以慢下来，慢慢生活，慢慢工作，不过这一切后面改变了，因为绩效考核越来越严格，公司采用阿里赛马制，但这都是针对正式员工而言，实习生倒是没什么过多要求。&lt;/p&gt;
&lt;p&gt;这里有最真实的 “955” 工作时间，不用加班而且双休，早上 9 点开工，到中午 11:30 即可去恰饭，中午午休 2 小时，然后从 13:30 干到 17:30 就可以下班了，每一天都是如此，除了有时开会可能会拖到 18 点，其他时候准时准点走人，不存在任何隐性加班的问题，实习生或正式员工都是如此。&lt;/p&gt;
&lt;h4&gt;之江团队&lt;/h4&gt;
&lt;p&gt;我所在的组是「之江-燧原联合创新研究中心」下的「新型通用智能计算芯片与定向加速芯片研究任务组」，程老师是我的组长，人特别好，也非常有耐心的带我，关照有加。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;工牌 🪪&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191206042.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;组内的学历全是博士和博士后，整个之江实验室「硕士:博士」的比例为 3:7，不过我是组内唯一一个 00 后大学生，估计也是之江屈指可数的 00 后了，这边的称呼都是以“老师”为称，所以当别人知道我的年龄，我听得最多的反应就是：“这么年轻”。&lt;/p&gt;
&lt;p&gt;之江实验室对员工的学历要求很高，这边遍地都是 985 本硕博，211 学历都会被卡。最夸张的不过当时听说有一位老师内推应聘，见他们在分析简历，发了 7 篇顶会，可以在浙大当个教授了都，但是因为第一学历不是 985 被卡在之江实验室的门外，当时深受震撼。当然我是走「项目聘用」的通道进来的，不然也不会有这个机会在这里实习。&lt;/p&gt;
&lt;p&gt;在这里我认识了很多很厉害的老师，也有幸能和他们打成一片，虽然我不擅长交际，但是和每位老师都能时不时说上几句，甚至认识不少燧原中心的其他老师和同在实习的小伙伴们。中间甚至有一位老师来求助我一些问题，大概是想考中科大非全日制在职博士，但是他很多年没参加这种考试了，拿着提纲就来问我复习哪些内容，给些推荐，最后他也如愿考上了，还特地线下感谢我了一波，当然也请我搓了一顿。还有三年博士海归、高考探花等等优秀的，就不一一说了，总之在这里、在他们身上，我学到了很多在学校学不到的知识和眼界。&lt;/p&gt;
&lt;h4&gt;工作日记&lt;/h4&gt;
&lt;p&gt;我以时间线的形式分享一下我在之江的一天。&lt;/p&gt;
&lt;p&gt;早上基本都将近 8 点起床，然后从青山湖站坐地铁🚇，接着到南湖站后等公交车🚌。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191417308.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;差不多 8:40 左右到达园区，然后刷卡/刷脸进入。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191419478.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;电梯不错，但是感觉这边的电梯调度算法有点问题🙋‍♂️&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191721094.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;公司食堂在主楼，并不算大，但是早餐种类很多，我最喜欢的就是这边的早餐了，便宜又好吃😋。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191420256.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;9:00 左右吃完早餐就去工位上， 9:10 开晨会汇报近两天的工作。虽说是九点上班，但很多员工都是九点后才到工位上，mentor 说这边打卡时间还是比较灵活的，毕竟从刷脸进园区那一瞬间就已经算上班了。&lt;/p&gt;
&lt;p&gt;关于会议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;周一、周三、周五晨会：主要讲讲自己这两天的进展&lt;/li&gt;
&lt;li&gt;周二晚论文分享会：每一两月一次即可&lt;/li&gt;
&lt;li&gt;周四晚技术分享会：中心分享，相对重量级&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有幸在组内做过一次分享，给大家介绍了我的科研工作，并得到较为客观的指导：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191556968.PNG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我的工位&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191422664.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;后期工位（从小到大都喜欢坐到角落位置）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191559334.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;预订会议室、厦大实验室组会｜I like this feel&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191714869.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;上午 11:30 就可以去食堂吃午饭了，正式员工有自动扣款的档口，实习生只能去刷卡的档口。食堂菜色还可以，整体价格大概是学校的 1.5 倍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191602941.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;食堂餐饮｜比看上去的还要好吃&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191536842.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;超市&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191612147.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;11:30 ~ 13:30 属于自由时间，午餐、睡觉或健身房都可，老师们一般是吃完午饭在园区内散散步然后回去午休。很多员工都会买折叠床，也可以在主楼（我所在的那栋楼）的客厅沙发上睡午觉。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191606119.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;之江园区&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191159195.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191607652.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191609224.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191620597.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;13:30 开始下午的工作（实习过我才发现午休的重要性），工作到 17:30 就可以下班了，可以选择在食堂吃晚餐，也可以回宿舍吃，我一般选择健身完吃饭后再回去。每周一、周三、周五下午，我基本都会和各位老师一起去打羽毛球🏸、打乒乓球🏓、打篮球🏀或者去健身房🏋️锻炼，这边的设施都很齐全。我特别喜欢羽毛球双打，自我感觉虽然打球不够专业，但是跑动能力和反应力还是挺强的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;羽毛球馆&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191617836.JPG&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;健身房&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191605052.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;图书馆和健身房&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191620917.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191633433.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;除了健身房、图书馆、咖啡店等常规配套设施外，园区内还有舞蹈协会、书法室、绘画室，还蛮新颖的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ZHEJIANG Lab · plog&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401190257641.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;之江团建&lt;/h4&gt;
&lt;p&gt;入职不久后，我刚好赶上公司半年一次的团建活动，地点在「杭州云上草原国际山地旅游度假区」，懂不懂周五团建的快乐啊😎&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191728452.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;福利待遇&lt;/h4&gt;
&lt;p&gt;正式员工有五险一金，12% 左右的公积金，生日福利，节日福利。&lt;/p&gt;
&lt;p&gt;正式员工每个月有 600 餐补，硕士 2000 房补，博士 2500 房补。&lt;/p&gt;
&lt;p&gt;年假 10 天左右，孕假更久，基本都是和国企对齐的，毕竟省政府创建。&lt;/p&gt;
&lt;p&gt;正式员工会有免费公寓，普通员工 40~50 平，职位高些能有 100+ 平米。&lt;/p&gt;
&lt;p&gt;不过有利有弊，我觉得有以下的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;财务部效率不高，首先是住房补贴，发票审核太久而且过于注重细枝末节，导致我入职一个月后才拿到住房补贴，而且发放实习工资的速度较慢&lt;/li&gt;
&lt;li&gt;不论正式员工还是实习生，工资水平相对不高，实习工资按照考核标准来的，优（3500）、良（3000）、合格（2500）、不合格（0），这还只是税前工资，到手工资扣税 20% 左右，第一个月我到手 ¥2950；至于正式员工，毋庸置疑肯定是比私企少的&lt;/li&gt;
&lt;li&gt;园区位置相当偏僻，属于郊区中的郊区，周围很荒芜，外卖都送不进来，也许这就是科研氛围吧&lt;/li&gt;
&lt;li&gt;之江实验室属于省属单位，所以拿不到杭州市人才补贴（硕士 5w，博士 10w）&lt;/li&gt;
&lt;li&gt;末位淘汰制度：这是我入职不久后在会议上听中心主任说的，我所处的部门更是核心部门，做芯片的，预计短期不会裁员，还在扩招&lt;/li&gt;
&lt;li&gt;博士生、大厂跳槽的人很多，硕士的晋升有限，以科研成果评估&lt;/li&gt;
&lt;li&gt;能学到的东西比较有限，如果你想提升代码水平，还是建议往私企大厂那边靠&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;工作氛围&lt;/h4&gt;
&lt;p&gt;这边很多的员工都是从阿里、腾讯、华为等大厂跳过来的，基本没有应届生，员工的年龄基本都是 35 岁以下的。我问过 mentor，他说一般只招 3~5 年工作经验的硕士，而且这两年不怎么招硕士了，招博士为主，毕竟研究所。&lt;/p&gt;
&lt;p&gt;工作的时候不会有人巡视，很自由，工作内容上主打科研，论文、专利、申请项目，也会有项目落地的工作，整体压力都不大，论文可以慢慢看，代码可以慢慢敲，工作时间内都能完成相应的任务。&lt;/p&gt;
&lt;p&gt;周围同事也都很友好，素养很高，完全不会有电视剧里出现的 “职场霸凌”、“勾心斗角”、“死气沉沉”、“PUA” 的情况，感觉每一天都有盼头，大家都很热爱自己的工作。&lt;/p&gt;
&lt;p&gt;三个月的实习经历在 8 月落幕了，故事的结尾也是该说再见了，期待下一次相遇👋&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401201613961.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;人只有在举目无亲的地方才算真正活着&lt;/h3&gt;
&lt;p&gt;不知道你有没有过这种想法，一个人居住在没人认识我的城市，我不需要帮助，也不想总是被问收入。一直以来，好像大家得到的很多爱都是以责备的方式表现出来的，爱使人愧疚，责备使人害怕，但这都不是什么好情绪，所以我真心实意地觉得：长大真好。虽然我的父母、亲人朋友们并不对我苛求，也不会干涉我的人生，但这并不影响我向往自由。&lt;/p&gt;
&lt;p&gt;长大之后，衣服脏了可以自己洗，杯子打碎了可以重新买，感冒发烧了就吃药睡一觉，去动车站只需拿出手机打一辆滴滴，就像自己孑然一身去杭州实习，自己能决定 ”天塌下来当被盖“ 的感觉真的很好，我可以很有底气的大手一挥说一句 “没事，我可以摆平”，这种自由掌控人生的状态让我着迷。原来打一辆车真的花不了多少钱，原来问题出现只需要解决问题本身，万万不用上升到道德层面，自己给自己兜底。&lt;/p&gt;
&lt;p&gt;不过开头说的住进孤岛的想法只是开个玩笑，并不是没有勇气，而是在世上有着割舍不掉的羁绊，因为这个世界上有在意的人，这同样也是我长大的意义，但偶尔人也要为自己而活一活，人只有在举目无亲的地方才能真正活着，也许这就是出发的意义。&lt;/p&gt;
&lt;p&gt;义无反顾实在是一个很美好的词，原来我爱的少年气，是对这些美好不愿意放下的追逐，我真的需要一点鲜活的、自由的、沸腾的、张扬的东西，只要一点点，真的就足够支撑我走很远的路。我知道大家都已经在生活中拼尽全力，但偶尔也想把世界从肩膀上卸下来，去做你喜欢的事吧，不要等到成年某刻再去宴请年少的自己，时间错位一切都将失去意义。&lt;/p&gt;
&lt;p&gt;去奔跑，去唱歌，去摔倒，去大声哭，就是趁我鲜活，不允许任何人熄灭我。我爱我的家乡，我可以生在这里，也可以死在这里，但我不能从生到死都在这里。&lt;/p&gt;
&lt;p&gt;而此刻，我将出发，我还要万里路要行。&lt;/p&gt;
&lt;h2&gt;0x04. 深圳｜CLKs 会议🌀&lt;/h2&gt;
&lt;p&gt;10 月份报名参加了「Linux 内核开发者大会」，导师报销车费住宿费，那我不得狠狠去深圳涨涨见识。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401192240722.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;此次去深圳有 7 人，10.27 都一块住深圳维也纳酒店，大概 ¥600 左右一晚，我的评价是不如厦门 ¥300 一晚。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;记录记录📝&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401192317527.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;三天的行程安排大致如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;10.27&lt;/strong&gt;：到达深圳，参观腾讯公司&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;10.28&lt;/strong&gt;：早上参加 CLKs 主场会议，下午参加内存管理分会场（参会人最多的一场，除此之外还有 “云和服务器”、“调试/eBPF/调度”、“Arch&amp;#x26;虚拟化&amp;#x26;I/O” 这几个分会场）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;10.29&lt;/strong&gt;：打道回府&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过这只是我导安排，我们还四处逛了逛，原本 28 号打算去欢乐谷玩，还好提前调研了一下，欢乐谷前一天刚好出了过山车碰撞🎢重大事故，已经闭园两个多月了，最近这几天才重新开园。还好晚到一天，逃过一劫，不然进 ICU 的就是哥几个了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401200017989.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;后面兜兜转转，酒吧基本都有低消（2k左右），还是选择了性价比最高的网咖，不得不说深圳的网咖是真的贵，价格我忘了，但我忘不了下单时的不舍。我们几人在网吧开黑到凌晨 3 点左右，就玩了俩游戏：「英雄联盟」和「求生之路」，开黑还得是后面这款游戏有意思。&lt;/p&gt;
&lt;p&gt;打车回酒店的路上，🚖师傅说这栋楼的 3 楼是深圳最有名最大的同性恋交友互动酒吧，好家伙，我回去还搜了一下，这酒吧叫 &lt;a href=&quot;https://www.bilibili.com/video/BV16N411K7o1/?vd_source=187e83a375c910488a1ad25cc2465299&quot;&gt;BONBON CTR&lt;/a&gt;，感兴趣的小伙伴可以去看看，反正是震撼到我了，深圳不愧是大城市，包容性就是强。&lt;/p&gt;
&lt;p&gt;我们这几天在深圳基本都吃自助餐，早餐自助，晚餐自助，午餐就吃披萨和会议提供的盒饭🍱。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;伙食（简）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401192341981.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;深圳 plog · 随手拍📷&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401192318863.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x05. 厦门大学｜生活 · 工作🌈&lt;/h2&gt;
&lt;p&gt;2023 年 9 月，我来到了厦大。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;短短 30 公里，我走了 &lt;a href=&quot;https://juejin.cn/post/7182648232969764921#heading-9&quot;&gt;8 年&lt;/a&gt;才到...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401201352769.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;厦大没有宵禁，任你几点回寝室都可以，给予学生最大限度的自由。&lt;/p&gt;
&lt;p&gt;校园生活也挺丰富，时不时会有一些大型活动。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401201423597.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;至于餐饮方面，我觉得厦大唯一的优势就是便宜，价格比你想象中的还要低，所以这里的生活成本并不高。但是质量方面，我觉得和师大还是有一定差距的，难怪被誉为 “福建吃饭大学“，时至今日我才彻底明白，那些优质的饮食，是我回不去的青春😭&lt;/p&gt;
&lt;p&gt;翔安校区很静也很偏僻，缺少了一丝文化底蕴，不像思明校区那般，仅仅走在芙蓉隧道你就可以深切感受到厦大的人文气息。毕竟人文思明，理工翔安。&lt;/p&gt;
&lt;p&gt;实验室里的生活也没有我想象中的枯燥，氛围十分融洽。偶尔学累了还可以打打羽毛球、乒乓球、台球、网球，也可以去健身房撸铁，去游泳馆放空自我，或者回寝室弹弹吉他，学会放松也是挺重要的，持续高压的环境下，弦是会断的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401200055027.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;22 年 8 月进入实验室以来，我都觉得自己的研究生生涯离不开圣哲、子航、佳泓及锋哥等等的帮助，其中受益最深的莫过于哲哥，感激之情溢于言表。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;23 年下旬那段时间压力真的很大&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401201621713.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;2023 年末，学校开办的讲座接踵而至，我最没想到的是&lt;a href=&quot;https://ipads.se.sjtu.edu.cn/zh/pub/members/haibo_chen/&quot;&gt;陈海波&lt;/a&gt;老师竟然来到厦大开讲座，学术界的大咖，讲座当天像极了粉丝见面会和新书发布会，我也很荣幸地得到了波哥的专属签名，浓墨重彩的一笔✍️&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;哲哥说名字怎么自己写的，给我笑拥了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401201448353.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;怕你们看得不清楚👀&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401201508486.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不过看到海波就会联想到自己的华为 P30 手机寿终正寝，鸿蒙系统没我想得那么强大，至少站在消费者的角度来说。&lt;/p&gt;
&lt;p&gt;12 月 20 日，我发现自己的手机会无限重启，进入系统后是紧急备份模式，无法正常使用。调查了下，发现是鸿蒙系统升级的锅，导致主板发热严重，主板虚焊脱落导致无法启动。当天去线下维修店，老板说可以维修，但是这种 CPU 烧坏了的情况，维修也就可以支撑几个月，还需要 ¥500 的维修费，所以我果断换把新机。也就是这种机缘巧合下，我凑齐了简约版的 “苹果全家桶”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iPhone 13&lt;/li&gt;
&lt;li&gt;iPad Air 4&lt;/li&gt;
&lt;li&gt;MacBook Air M2&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;全家桶合照&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401202318123.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;除此之外，我挺想拥有一台属于自己的相机，我热爱摄影，热爱记录生活中每个值得纪念的瞬间。读书让我了解世界，而摄影能让我把世界按照自己的想法具像化。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;咔嚓 📷&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401201533005.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;厦门印象&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401201543500.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;XMU&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401200054702.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x06. 读研是当代年轻人的青春疼痛文学🌓&lt;/h2&gt;
&lt;p&gt;2023 出现一个新名词 “孔乙己的长衫”，大概意思就是说，高学历不仅是块敲门砖，也是我下不来的高台，更是那孔乙己脱不下的长衫。如果我没上过大学，我就可以心安理得的去做任何工作。&lt;/p&gt;
&lt;p&gt;我听说过一种说法，从经济学方面来解释所谓的结构性失业，感到非常有意思，和大家分享一下。把问题放在时空的大背景下，这个原因，其实要追溯到当年为了应对 98 金融危机促进消费而进行的高校扩招。因为这样，大家就回把大量的钱放在教育投资上，当时预计扩招一倍，但事实上没能控制住，扩招了 6 倍。由此，我们迎来了长达十几年的繁荣发展，代价是直接导致了高中教育的内卷，而由于初中是义务教育无法直接内卷，于是高中内卷达到顶峰之后，接下来就是研究生学历的内卷。从这个角度来说，我们目前面临的困境，其实只是历史开了一个玩笑，道理就好像小时候，世界给了你一个承诺：“只要读好大学，就有好工作”，你相信了并且为之付出很多努力，然而现在毕业了，发现世界并没有兑现这个承诺，这只是一个时代的恶作剧。而读研就好像 10 年后，你要去取存在银行里的钱，但银行没钱，所以给你办个业务让你再存 3 年，3 年之后这些钱就按照约定给你了吗，没有人知道答案。&lt;/p&gt;
&lt;p&gt;提到读研，我猜测大部分同学对此的执着追求，并非是为了学术梦想，而是为了更高的工资更高的起点，我们从小接受的知识改变命运，大部分人理解的是学历改变命运。然而这套逻辑在 20 年前行得通，在今天却遇到了困难。所以在这种时代背景下，教育被拉下神坛，学习的意义被质疑，我们会感到非常痛苦、愤怒、无力，甚至走向虚无，认为一切都没有意义。&lt;/p&gt;
&lt;p&gt;我们不难发现，所谓孔乙己的长衫，本质其实是另一种形式的读书无用论，不过我想表达的是，大家总是要向前走的，即使世界没有兑现承诺，即使高学历成为泡沫，那我们就只能回归最朴素的 “价值决定价格”。这个 3 年之后会好的银行业务，到 3 年之后会不会兑现，仍然是个未知数，我们能做的只有摒弃 “等到什么什么之后就好了” 这种想法，不管是本科还是研究生，都认真提高自己真实的能力，或者我也可以称之为 “理解这个世界运作规律的能力”。从上往下看的话，高学历的价值本身就是外界赋予的，不管是 985 还是研究生，只是个人能力的货币体现，并非个人能力的铁碗保障，出于学历的傲慢，更是非常荒谬。就好比岳阳楼为什么出名呢？因为岳阳楼记。滕王阁为什么出名呢？因为滕王阁序。荷塘月色为什么出名呢？因为朱自清先生他写过荷塘月色。景观都是人文赋予的意义，景观唯一能自己给自己带来的意义，只能是珠穆朗玛峰它是世界第一高峰。因为美是人发现的，审美的主体不把情绪转移到审美的客体上，它也不会有什么附加价值，学历也是如此。&lt;/p&gt;
&lt;p&gt;当然这样讲可能是不公平而无奈的，因为在过去相当长一段时间，有太多人靠学历的光环，得到了远超自己能力的回报。然而现在，我们难以凭借单纯的学历而获得这些，很容易会有一种被骗了的感觉，但控诉时代意义甚微，看到很多人焦虑，其实还是在焦虑一些世俗的成功，而从世俗层面来说，作为个人能做的只有尽量在游戏规则里打出好战绩罢了。我只能像丁玲笔下的陆萍那样：“失望和颓丧都是她所怕的，所以不管遇着怎样的环境她都好好的，替它做一个宽容的恰到的解释”。&lt;/p&gt;
&lt;p&gt;我还没想好自己的价值，人也不是非要读研不可，其次如果读研，也要给学历祛魅，抛弃莫须有的研究生的傲慢，不寄希望于研究生学历给我带来什么额外的东西，尽量努力做到，这 3 年本身比最后的学历更重要吧。考研也好，保研也好，读研也好，读完研找工作也好，除了真正醉心于学术的诸公，剩下的被逼着走上这套流程的人里，没有谁能从头到尾笑着走出来，「研究生」这三个字好像成了当代年轻人的青春疼痛文学，甚至一经提起，都使人沉默郁然，人人都有满腹苦水兀自吞下，大家一直说 “直接工作也没关系”，心里却不是这么想的，在淘汰制的考试中，注定只有少部分人胜出，每往上走一层，就有大批留在下面一层的人。考试让太多人成了牺牲品，但是 “早知道去送外卖，就不用这么辛苦读书了“ 这个逻辑就完全是对的吗？我想也不尽然，高等教育的普及，就像鲁迅在暗室里的一声呐喊。学历改变命运有限，但我从不怀疑知识的珍贵性。&lt;/p&gt;
&lt;p&gt;这个世界，沉默的终究是大多数。&lt;/p&gt;
&lt;h2&gt;0x07. 罹患甲流😷&lt;/h2&gt;
&lt;p&gt;21 年以前是哮喘，22 年是新冠，23 年是甲流，未来不知道还有什么等着我。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401202141141.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;甲流症状还是比较明显的，刚开始体温迅速升高，通常会达到 38~40 度，我有幸刷到了 40 多度，同时会出现肌肉的酸痛，头疼和乏力，它和新冠有类似的症状，但是潜伏期要短一点。接着就是反复发烧、头疼、喉咙疼、流鼻涕，我发烧了 5 天左右，具体症状强度因人而异，反正我是进急诊了，个人感觉强度和 22 年得新冠差不多，但起码新冠才 39.6 度😭&lt;/p&gt;
&lt;p&gt;每逢大病初愈，我都会感叹道：活着真好！&lt;/p&gt;
&lt;p&gt;那你如果要问我活着的意义是什么，恕我难以告诉你，众生皆苦，活着很难，不是吗？&lt;/p&gt;
&lt;p&gt;很残酷的现实是，生命的意义是个经不起推敲的问题，因为我们的生命都没什么意义。&lt;/p&gt;
&lt;p&gt;站在时光的巨大尺度上，我们微小得令人恐惧，我们珍爱的人，所罹患的病，所背负的包裹，所经历过的喜悦与悲伤，所承受的爱与被爱的重量，都像被秋风扫落的枯叶一样脆弱，不会在世界上留下多少痕迹。&lt;/p&gt;
&lt;p&gt;包括我们所经历过的痛苦，也是毫无意义的。就如余华所说的，其实是你的情绪进入了死胡同，而不是你的人生进入了死胡同。&lt;/p&gt;
&lt;p&gt;我是说，生命是自己的，如果你觉得承受不住了，我尊重你的选择，也会祝你下辈子做个快乐的人。但如果你仍然有一丝留念值得你再坚持下去，我相信，对死亡已经不再有畏惧的你，一定会是个既坚强，又温柔的人。&lt;/p&gt;
&lt;p&gt;你会悄然改变自己甚至别人的生活。那时候你可能会觉得，有你活着，真是件好事。&lt;/p&gt;
&lt;p&gt;这段话送给此刻正在阅读文章的你，也送给曾经的那个自己。&lt;/p&gt;
&lt;h2&gt;0x08. 还好这个世界有音乐🎶&lt;/h2&gt;
&lt;p&gt;平日里非常喜欢听 Jay Chou 和雷子的歌曲，热爱民谣，也把玩着一把民谣吉他，最近迷上了 Eason Chan，应该没有人不喜欢粤语歌吧。我也尝试抢过 jay 和 eason 的演唱会，根本抢不到，希望有机会能去现场感受音乐，如果有价格合适的「杰伦 / 奕迅」演唱会门票 🎫 可以 call 我。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NetEaseMusic 年度报告&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401202209443.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;跟风测了测 mbti，不然都跟不上时代的说 [doge]&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401202212777.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x09. 在繁花深处，遇见梵高🌻&lt;/h2&gt;
&lt;p&gt;第一次见到梵高的画是在高一美术课上，在临摹过程中偶然瞥见两幅画，一幅是蒙克画笔下的「呐喊」，还有一幅便是梵高的「星空」，二者都给我留下很深的印象，似乎是从那一刻见识到了艺术，孤独而伟大。&lt;/p&gt;
&lt;p&gt;23 年 8 月，在泉州举办了一场「致敬梵高」的会展，我抓住最后的时间去参观了一趟，很幸运那是最后几天，整个艺术展里只有我和我朋友，像是只属于我们的艺术展，踏进场馆的那一刻仿佛置身于梵高的星空中，我在那里坐了一下午，我很喜欢这种放空的感觉。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401200053609.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;致敬梵高展&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401200053235.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Van Gogh&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401202300026.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;梵高的伟大在于，即使一生命运坎坷、穷困潦倒，仍对绘画保持着极为纯粹的热爱，对劳动者和大自然有着无比的热忱。他如火般炙热骄傲的内心，如同阿尔勒的向日葵，让我们在相隔百年之后，仍可以感受到他笔下传递出来的美。&lt;/p&gt;
&lt;h2&gt;0x0A. 节选自阿兰德波顿的「爱情笔记」☘️&lt;/h2&gt;
&lt;p&gt;我不想和你聊世俗，我想和你聊你的童年记忆，你的幸福时光和至暗时刻。&lt;/p&gt;
&lt;p&gt;想和你聊你是怎么一步一步走到今天，有哪些遗憾，错过了什么人，谁保护了你你又感激谁，想和你聊你的故作坚强：你的谎言和逃避，你为了自保而不得已的小阴暗。&lt;/p&gt;
&lt;p&gt;想知道是哪句话给你力量支撑你走到今天，想和你聊你对未来的期盼。&lt;/p&gt;
&lt;p&gt;至此，我才算浅薄地认识了你，了解了一点点你的来路和去处。&lt;/p&gt;
&lt;p&gt;我喜欢有深度和真诚的你，不喜欢没有瑕疵的你，我讨厌早安晚安式的闲谈，我想和你谈论大自然、死亡、性、世界、人类、智慧、生命意义、隐秘想法和你想过的理想人生、让你想跳舞的音乐、有趣的回忆。你说过的谎言、你的缺点、你最喜欢的气味、你的童年、让你彻夜难眠的东西、你的不安全感和恐惧。&lt;/p&gt;
&lt;p&gt;我喜欢你带着真实的情感说话，因为那是我要的有趣和血肉，而不是一直借助一些美好的事物和话语，装真实的自己从而表现得完美。&lt;/p&gt;
&lt;p&gt;这才是我关于爱你的详细备注。&lt;/p&gt;
&lt;h2&gt;0x0B. 别急着赶路，去感受路⛵️&lt;/h2&gt;
&lt;p&gt;非要找一个 2023 年的关键词，我会说是 “力所能及”。&lt;/p&gt;
&lt;p&gt;他们说，考上大学就好了，考过四六级就好了，考上教资就好了，考上研究生就好了。读研前我以为从此人生就会变好，但踏进新校园的那一刻，世界安安静静，什么都没发生，我恍然意识到，路的尽头还是路，那些我们以为重要的，甚至连转折点都算不上。莫名的我想起了前辈说的一句话，他说，你不要害怕失败，你可以试着去失败一次看看，你会发现，什么都不会发生。&lt;/p&gt;
&lt;p&gt;2023 年，我毕业了，我经历了很多分别，搞砸了很多关系，遇到了很多困难，痛苦的发现，很多自以为是的努力原来没有意义，曾经坚信的东西原来行不通，花费了很多精力去刻意避免的事，最终还是宿命般的发生。我只能在自己的能力范围内，做一些力所能及的东西， 力所能及的意思是太多因素无法控制，太多结果充满未知，但我仍然可以做点什么，虽然不多，终究是有可为之。&lt;/p&gt;
&lt;p&gt;2023 年，我体验过许多不同的人生，战战兢兢，倒也没从薄冰上掉下去，仍然会愤怒，也仍然会心软，我是没有办法改变什么，但我好像也没有被改变，甚至很多我的痛苦，正是来源于我没有被改变。我知道很多期待和想法在现实里并不可行但是，要是我放弃了它，我便不再是我。我是不太适应，但我也不太想适应，我们会遇到形形色色的人，会遇到无法理解的恶意，会感到失望，会感到委屈，但是只要过去了你就会发现，其实这些都是在提醒你：不要成为那样的人。同时我们也会遇到猝不及防的善意，会发自内心的喜悦，而这些都是在告诉我们：如果生活把你的门关上了，那你就再打开，这就是门，门就是这样用的。虽然有时会害怕，但我不想因为担心失败的风险，就停止出发。船停在港口是最安全的，但这不是造船的意义。&lt;/p&gt;
&lt;p&gt;最难忘的永远是无用的大笑，一群人凑一块聊两个钟头，拣不出一句有用的，做许许多多的傻事，然后迅速忘记它们，像风在晚霞里飞驰，什么都没有发生，如果生命力足够旺盛的话，我愿意一直坚持对生活的孜孜不倦。到底什么才是永远适用于未来的答案，生如寄，死能归吗？轻舟已过万重山，但彷徨未减，求仁得仁，但冷暖自知。我只是越来越发觉到，好像不必非要等到什么时候再，我们似乎可以在人生的任何一个坐标上，放置里程碑，没有什么差别。&lt;/p&gt;
&lt;p&gt;打球的最常说的一句话就是：“没事，再来”。这是由回合制的游戏规则决定的，一旦站到球场上，失误一个球之后，必须快速调整心态，否则下一个球也会丢。比起安全的推挡防守，我宁愿选择去拧拉刁钻的球，我想过那种非常值得回忆的人生，尽管我还没找到一个为之奋斗一生的目标，但我想这并不是一件很急的事情，一路一路，一年一年，一直走下去，终有一天，我会找到我存在的价值。人生路万条，成功和失败是每个人都要面对的命题，上一颗球没接好，是我的问题，我也知道我不可能接好每一颗球，但不用让着我，我会反复练习，我下一球一定会接得更好。&lt;/p&gt;
&lt;p&gt;2023 年，我一直在出发，一直在路上。这也许就是旅行的意义，相比直观的风景，更有意义的往往是预料之外的相遇，同样是一座城，每个人都有自己能讲的故事，只要我还没走到终点，就还有新的山水可以期待。所以别急着赶路，去感受路！&lt;/p&gt;
&lt;p&gt;故事到这里就结束了，很感谢你能看到这里，那我们，明年再见！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401211435686.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202310141443480.nJ_f8_Oy.jpg"/><enclosure url="/_astro/202310141443480.nJ_f8_Oy.jpg"/></item><item><title>福建·厦门</title><link>https://coooredump.github.io/blog/tourism/xiamen_2023-12-24</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/xiamen_2023-12-24</guid><description>海岛日记</description><pubDate>Sun, 24 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401201533005.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401201543500.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401200054702.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506281631760.Z891xsNc.jpg"/><enclosure url="/_astro/202506281631760.Z891xsNc.jpg"/></item><item><title>广东·深圳</title><link>https://coooredump.github.io/blog/tourism/shenzhen_2023-10-27</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/shenzhen_2023-10-27</guid><description>Shenzhen CLKs Conference</description><pubDate>Fri, 27 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401192318863.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401192341981.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506172330488.CwVJRlHN.jpg"/><enclosure url="/_astro/202506172330488.CwVJRlHN.jpg"/></item><item><title>Markdown Syntax Support</title><link>https://coooredump.github.io/blog/markdown/markdown-en</link><guid isPermaLink="true">https://coooredump.github.io/blog/markdown/markdown-en</guid><description>Markdown is a lightweight markup language.</description><pubDate>Wed, 26 Jul 2023 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Basic Syntax&lt;/h2&gt;
&lt;p&gt;Markdown is a lightweight and easy-to-use syntax for styling your writing.&lt;/p&gt;
&lt;h3&gt;Headers&lt;/h3&gt;
&lt;p&gt;When the content of the article is extensive, you can use headers to segment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# Header 1

## Header 2

## Large Header

### Small Header
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Header previews would disrupt the structure of the article, so they are not displayed here.&lt;/p&gt;
&lt;h3&gt;Bold and Italics&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;_Italic text_ and **Bold text**, together will be **_Bold Italic text_**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Italic text&lt;/em&gt; and &lt;strong&gt;Bold text&lt;/strong&gt;, together will be &lt;strong&gt;&lt;em&gt;Bold Italic text&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Links&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Text link [Link Name](http://link-url)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;Text link &lt;a href=&quot;http://link-url&quot;&gt;Link Name&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Inline Code&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;This is an `inline code`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;This is an &lt;code&gt;inline code&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Code Blocks&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;```js
// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  const result = fibonacci(n - 1) + fibonacci(n - 2) // [\!code --]
  return fibonacci(n - 1) + fibonacci(n - 2) // [\!code ++]
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  const result = fibonacci(n - 1) + fibonacci(n - 2) // [!code --]
  return fibonacci(n - 1) + fibonacci(n - 2) // [!code ++]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Currently using shiki as the code highlighting plugin. For supported languages, refer to &lt;a href=&quot;https://shiki.matsu.io/languages.html&quot;&gt;Shiki: Languages&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Inline Formula&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;This is an inline formula $e^{i\pi} + 1 = 0$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;This is an inline formula $e^{i\pi} + 1 = 0$&lt;/p&gt;
&lt;h3&gt;Formula Blocks&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} \, dx
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} , dx
$$&lt;/p&gt;
&lt;p&gt;Currently using KaTeX as the math formula plugin. For supported syntax, refer to &lt;a href=&quot;https://katex.org/docs/supported.html&quot;&gt;KaTeX Supported Functions&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Images&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;![CWorld](https://cravatar.cn/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cravatar.cn/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200&quot; alt=&quot;CWorld&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Strikethrough&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;~~Strikethrough~~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;~~Strikethrough~~&lt;/p&gt;
&lt;h3&gt;Lists&lt;/h3&gt;
&lt;p&gt;Regular unordered list&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- 1
- 2
- 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1&lt;/li&gt;
&lt;li&gt;2&lt;/li&gt;
&lt;li&gt;3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Regular ordered list&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;1. GPT-4
2. Claude Opus
3. LLaMa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GPT-4&lt;/li&gt;
&lt;li&gt;Claude Opus&lt;/li&gt;
&lt;li&gt;LLaMa&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can continue to nest syntax within lists.&lt;/p&gt;
&lt;h3&gt;Blockquotes&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&gt; Gunshot, thunder, sword rise. A scene of flowers and blood.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Gunshot, thunder, sword rise. A scene of flowers and blood.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You can continue to nest syntax within blockquotes.&lt;/p&gt;
&lt;h3&gt;Line Breaks&lt;/h3&gt;
&lt;p&gt;Markdown needs a blank line to separate paragraphs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;If you don&apos;t leave a blank line
it will be in one paragraph

First paragraph

Second paragraph
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;If you don&apos;t leave a blank line
it will be in one paragraph&lt;/p&gt;
&lt;p&gt;First paragraph&lt;/p&gt;
&lt;p&gt;Second paragraph&lt;/p&gt;
&lt;h3&gt;Separators&lt;/h3&gt;
&lt;p&gt;If you have the habit of writing separators, you can start a new line and enter three dashes &lt;code&gt;---&lt;/code&gt; or asterisks &lt;code&gt;***&lt;/code&gt;. Leave a blank line before and after when there are paragraphs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Advanced Techniques&lt;/h2&gt;
&lt;h3&gt;Inline HTML Elements&lt;/h3&gt;
&lt;p&gt;Currently, only some inline HTML elements are supported, including &lt;code&gt;&amp;#x3C;kdb&gt; &amp;#x3C;b&gt; &amp;#x3C;i&gt; &amp;#x3C;em&gt; &amp;#x3C;sup&gt; &amp;#x3C;sub&gt; &amp;#x3C;br&gt;&lt;/code&gt;, such as&lt;/p&gt;
&lt;h4&gt;Key Display&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Use &amp;#x3C;kbd&gt;Ctrl&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Alt&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Del&amp;#x3C;/kbd&gt; to reboot the computer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;Use Ctrl + Alt + Del to reboot the computer&lt;/p&gt;
&lt;h4&gt;Bold Italics&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;b&gt; Markdown also applies here, such as _bold_ &amp;#x3C;/b&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt; Markdown also applies here, such as &lt;em&gt;bold&lt;/em&gt; &lt;/p&gt;
&lt;h3&gt;Other HTML Writing&lt;/h3&gt;
&lt;h4&gt;Foldable Blocks&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;details&gt;&amp;#x3C;summary&gt;Click to expand&amp;#x3C;/summary&gt;It is hidden&amp;#x3C;/details&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;h3&gt;Tables&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;| Header1  | Header2  |
| -------- | -------- |
| Content1 | Content2 |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;| Header1  | Header2  |
| -------- | -------- |
| Content1 | Content2 |&lt;/p&gt;
&lt;h3&gt;Footnotes&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Use [^footnote] to add a footnote at the point of reference.

Then, at the end of the document, add the content of the footnote (it will be rendered at the end of the article by default).

[^footnote]: Here is the content of the footnote
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;Use [^footnote] to add a footnote at the point of reference.&lt;/p&gt;
&lt;p&gt;Then, at the end of the document, add the content of the footnote (it will be rendered at the end of the article by default).&lt;/p&gt;
&lt;p&gt;[^footnote]: Here is the content of the footnote&lt;/p&gt;
&lt;h3&gt;To-Do Lists&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- [ ] Incomplete task
- [x] Completed task
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Incomplete task&lt;/li&gt;
&lt;li&gt;[x] Completed task&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Symbol Escaping&lt;/h3&gt;
&lt;p&gt;If you need to use markdown symbols like _ # * in your description but don&apos;t want them to be escaped, you can add a backslash before these symbols, such as &lt;code&gt;\_&lt;/code&gt; &lt;code&gt;\#&lt;/code&gt; &lt;code&gt;\*&lt;/code&gt; to avoid it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;\_Don&apos;t want the text here to be italic\_

\*\*Don&apos;t want the text here to be bold\*\*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;_Don&apos;t want the text here to be italic_&lt;/p&gt;
&lt;p&gt;**Don&apos;t want the text here to be bold**&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Embedding Astro Components&lt;/h2&gt;
&lt;p&gt;See &lt;a href=&quot;/docs/integrations/components&quot;&gt;User Components&lt;/a&gt; and &lt;a href=&quot;/docs/integrations/advanced&quot;&gt;Advanced Components&lt;/a&gt; for details.&lt;/p&gt;</content:encoded><h:img src="/_astro/markdown.HAXFr_hw.jpg"/><enclosure url="/_astro/markdown.HAXFr_hw.jpg"/></item><item><title>Markdown 语法支持</title><link>https://coooredump.github.io/blog/markdown/markdown-zh</link><guid isPermaLink="true">https://coooredump.github.io/blog/markdown/markdown-zh</guid><description>Markdown 是一种轻量级的「标记语言」。</description><pubDate>Wed, 26 Jul 2023 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;基本语法&lt;/h2&gt;
&lt;p&gt;Markdown 是一种轻量级且易于使用的语法，用于为您的写作设计风格。&lt;/p&gt;
&lt;h3&gt;标题&lt;/h3&gt;
&lt;p&gt;文章内容较多时，可以用标题分段：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# 标题 1

## 标题 2

## 大标题

### 小标题
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;标题预览会打乱文章的结构，所以在此不展示。&lt;/p&gt;
&lt;h3&gt;粗斜体&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;_斜体文本_

**粗体文本**

**_粗斜体文本_**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;斜体文本&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;粗体文本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;粗斜体文本&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;链接&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;文字链接 [链接名称](http://链接网址)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;文字链接 &lt;a href=&quot;http://%E9%93%BE%E6%8E%A5%E7%BD%91%E5%9D%80&quot;&gt;链接名称&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;行内代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;这是一条 `单行代码`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;这是一条 &lt;code&gt;行内代码&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;```js
// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  return fibonacci(n - 1) + fibonacci(n - 2)
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  return fibonacci(n - 1) + fibonacci(n - 2)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当前使用 shiki 作为代码高亮插件，支持的语言请参考 &lt;a href=&quot;https://shiki.matsu.io/languages.html&quot;&gt;shiki / languages&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;行内公式&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;这是一条行内公式 $e^{i\pi} + 1 = 0$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;这是一条行内公式 $e^{i\pi} + 1 = 0$&lt;/p&gt;
&lt;h3&gt;公式块&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} \, dx
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} , dx
$$&lt;/p&gt;
&lt;p&gt;当前使用 KaTeX 作为数学公式插件，支持的语法请参考 &lt;a href=&quot;https://katex.org/docs/supported.html&quot;&gt;KaTeX Supported Functions&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;图片&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;![CWorld](https://cravatar.cn/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cravatar.cn/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200&quot; alt=&quot;CWorld&quot;&gt;&lt;/p&gt;
&lt;h4&gt;删除线&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;~~删除线~~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;~~删除线~~&lt;/p&gt;
&lt;h3&gt;列表&lt;/h3&gt;
&lt;p&gt;普通无序列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- 1
- 2
- 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1&lt;/li&gt;
&lt;li&gt;2&lt;/li&gt;
&lt;li&gt;3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;普通有序列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;1. GPT-4
2. Claude Opus
3. LLaMa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GPT-4&lt;/li&gt;
&lt;li&gt;Claude Opus&lt;/li&gt;
&lt;li&gt;LLaMa&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;列表里可以继续嵌套语法&lt;/p&gt;
&lt;h3&gt;引用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&gt; 枪响，雷鸣，剑起。繁花血景。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;枪响，雷鸣，剑起。繁花血景。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;引用里也可以继续嵌套语法。&lt;/p&gt;
&lt;h3&gt;换行&lt;/h3&gt;
&lt;p&gt;markdown 分段落是需要空一行的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;如果不空行
就会在一段

第一段

第二段
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;如果不空行
就会在一段&lt;/p&gt;
&lt;p&gt;第一段&lt;/p&gt;
&lt;p&gt;第二段&lt;/p&gt;
&lt;h3&gt;分隔符&lt;/h3&gt;
&lt;p&gt;如果你有写分割线的习惯，可以新起一行输入三个减号&lt;code&gt;---&lt;/code&gt; 或者星号 &lt;code&gt;***&lt;/code&gt;。当前后都有段落时，请空出一行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;高级技巧&lt;/h2&gt;
&lt;h3&gt;行内 HTML 元素&lt;/h3&gt;
&lt;p&gt;目前只支持部分段内 HTML 元素效果，包括 &lt;code&gt;&amp;#x3C;kdb&gt; &amp;#x3C;b&gt; &amp;#x3C;i&gt; &amp;#x3C;em&gt; &amp;#x3C;sup&gt; &amp;#x3C;sub&gt; &amp;#x3C;br&gt;&lt;/code&gt; ，如&lt;/p&gt;
&lt;h4&gt;键位显示&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;使用 &amp;#x3C;kbd&gt;Ctrl&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Alt&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Del&amp;#x3C;/kbd&gt; 重启电脑
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;使用 Ctrl + Alt + Del 重启电脑&lt;/p&gt;
&lt;h4&gt;粗斜体&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;b&gt; Markdown 在此处同样适用，如 _加粗_ &amp;#x3C;/b&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt; Markdown 在此处同样适用，如 &lt;em&gt;加粗&lt;/em&gt; &lt;/p&gt;
&lt;h3&gt;其他 HTML 写法&lt;/h3&gt;
&lt;h4&gt;折叠块&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;details&gt;&amp;#x3C;summary&gt;点击展开&amp;#x3C;/summary&gt;它被隐藏了&amp;#x3C;/details&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;h3&gt;表格&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;| 表头1 | 表头2 |
| ----- | ----- |
| 内容1 | 内容2 |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;| 表头1 | 表头2 |
| ----- | ----- |
| 内容1 | 内容2 |&lt;/p&gt;
&lt;h3&gt;注释&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;在引用的地方使用 [^注释] 来添加注释。

然后在文档的结尾，添加注释的内容（会默认于文章结尾渲染之）。

[^注释]: 这里是注释的内容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;在引用的地方使用 &lt;a href=&quot;%E8%BF%99%E9%87%8C%E6%98%AF%E6%B3%A8%E9%87%8A%E7%9A%84%E5%86%85%E5%AE%B9&quot;&gt;^注释&lt;/a&gt; 来添加注释。&lt;/p&gt;
&lt;p&gt;然后在文档的结尾，添加注释的内容（会默认于文章结尾渲染之）。&lt;/p&gt;
&lt;h3&gt;To-Do 列表&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- [ ] 未完成的任务
- [x] 已完成的任务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 未完成的任务&lt;/li&gt;
&lt;li&gt;[x] 已完成的任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;符号转义&lt;/h3&gt;
&lt;p&gt;如果你的描述中需要用到 markdown 的符号，比如 _ # * 等，但又不想它被转义，这时候可以在这些符号前加反斜杠，如 &lt;code&gt;\_&lt;/code&gt; &lt;code&gt;\#&lt;/code&gt; &lt;code&gt;\*&lt;/code&gt; 进行避免。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;\_不想这里的文本变斜体\_

\*\*不想这里的文本被加粗\*\*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;_不想这里的文本变斜体_&lt;/p&gt;
&lt;p&gt;**不想这里的文本被加粗**&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;内嵌 Astro 组件&lt;/h2&gt;
&lt;p&gt;See &lt;a href=&quot;/docs/integrations/components&quot;&gt;User Components&lt;/a&gt; and &lt;a href=&quot;/docs/integrations/advanced&quot;&gt;Advanced Components&lt;/a&gt; for details.&lt;/p&gt;</content:encoded><h:img src="/_astro/markdown.HAXFr_hw.jpg"/><enclosure url="/_astro/markdown.HAXFr_hw.jpg"/></item><item><title>福州·毕业季</title><link>https://coooredump.github.io/blog/tourism/fjnu_2023-06-14</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/fjnu_2023-06-14</guid><description>欲买桂花同载酒，终不似，少年游</description><pubDate>Wed, 14 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;欲买桂花同载酒，终不似，少年游&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;人与人之间，一个 moment 就足够了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191457597.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401192100649.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401190037861.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191514689.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202401190037013.CgYJsgtp.jpg"/><enclosure url="/_astro/202401190037013.CgYJsgtp.jpg"/></item><item><title>浙江·杭州</title><link>https://coooredump.github.io/blog/tourism/hangzhou_2023-04-28</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/hangzhou_2023-04-28</guid><description>去奔跑，去唱歌，去摔倒，去大声哭，就是趁我鲜活，不允许任何人熄灭我。</description><pubDate>Fri, 28 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我爱我的家乡，我可以生在这里，也可以死在这里，但我不能从生到死都在这里。&lt;/p&gt;
&lt;p&gt;而此刻，我将出发，我还要万里路要行。&lt;/p&gt;
&lt;h2&gt;杭州影像&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191537682.jpg&quot; alt=&quot;江南&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191544030.jpg&quot; alt=&quot;荒漠美食&quot;&gt;&lt;/p&gt;
&lt;h2&gt;之江实验室&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401190218006.jpg&quot; alt=&quot;租房日记&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401190257641.jpg&quot; alt=&quot;Zhejiang Lab&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191728452.jpg&quot; alt=&quot;之江团建&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506172254078.BfnfOFhH.jpg"/><enclosure url="/_astro/202506172254078.BfnfOFhH.jpg"/></item><item><title>湖北·武汉</title><link>https://coooredump.github.io/blog/tourism/wuhan_2023-04-04</link><guid isPermaLink="true">https://coooredump.github.io/blog/tourism/wuhan_2023-04-04</guid><description>吹起了樱花雪，落了满身春</description><pubDate>Tue, 04 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401190136238.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202401191452004.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202506172248993.BylrHnci.jpg"/><enclosure url="/_astro/202506172248993.BylrHnci.jpg"/></item><item><title>「2022 年终总结」人生是一片原野，而不是轨道</title><link>https://coooredump.github.io/blog/yearly-review/2022-life-is-a-field-not-a-track</link><guid isPermaLink="true">https://coooredump.github.io/blog/yearly-review/2022-life-is-a-field-not-a-track</guid><description>摆烂又或许是害怕失败的一种逃避手段。不确定性的存在是无解的，机会成本的产生是必然的，没有人愿意承担失败带来的代价，成功学的理念在大家 DNA 里刻得太深，不能失败的观念始终是悬在我们头顶的「达摩克里斯之剑」，把人压得喘不过气来，这一套对于高考来说是行得通的，因为这个社会的规则便是如此。但它并不适用于我们的人生，人生太长了，大家又不是只活二三十年。</description><pubDate>Sun, 01 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;🌀前言&lt;/h2&gt;
&lt;p&gt;分享下毛姆所著小说《刀锋》中的一段文字：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我并不怕犯错，搞不好会在其中一条冤枉路上，找到人生的目标，人生从来就没有白走的路，每一步都算数。&lt;/p&gt;
&lt;p&gt;我们这一生，都怕走冤枉路，都想找到一条捷径，好用最快的速度接近自己的目标。&lt;/p&gt;
&lt;p&gt;但事实上，当你并不清楚自己内心最真实的声音的时候，你只有不断尝试，才可能知道什么是适合自己的，就算尝试过后你还是不清楚自己想要什么，但你最起码知道一点，这不是自己喜欢的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;时间来到 2022 年的尾巴，如果没有笔耕不缀地记录着自己所体验、所尝试过的每一种生活，我大抵忘了这一年是怎么过来的了...&lt;/p&gt;
&lt;p&gt;不确定自己是否做好准备迎接 2023 年的到来，但 2022 已经接近尾声了，是该做个了结了...&lt;/p&gt;
&lt;h2&gt;❤️掘金 · 优秀创作者&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202301111506490.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这一年凭借掘金掘力值改版以及文章的输出，终于达到 &lt;strong&gt;Lv.5&lt;/strong&gt; 并获得了「优秀创作者」一称，来之不易，继续努力。&lt;/p&gt;
&lt;p&gt;2022 年撰写的文章主要围绕以下关键词：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vue&lt;/li&gt;
&lt;li&gt;Golang&lt;/li&gt;
&lt;li&gt;LeetCode&lt;/li&gt;
&lt;li&gt;设计模式&lt;/li&gt;
&lt;li&gt;Spring Boot&lt;/li&gt;
&lt;li&gt;Spring Cloud&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;很荣幸，「手撕设计模式」专栏得到了大家的赏识，至今的关注数已达 79 人，我不确定什么时候会再继续往下补充未完成的部分，但我保证一定会回来填坑的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2021 年数据概览&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141654566.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2022 年数据概览&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141654114.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;对比往年的数据，今年的数据较为可观，毕竟万事开头难，只要坚持下去，迟早都会收到正向的反馈。&lt;/p&gt;
&lt;p&gt;不过距离&lt;a href=&quot;https://juejin.cn/post/7097436547553132557&quot;&gt;最新发布的文章&lt;/a&gt;，时间也已经过去小半年了，这半年没有持续输出文章，实属一大憾事，相信来年我会继续创作的。&lt;/p&gt;
&lt;p&gt;回望我这一年 GitHub 的提交信息，频率下降了不少，值得反思。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141656372.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;🔥冬令营&lt;/h2&gt;
&lt;p&gt;2022 年初有幸参与过一次「大学生冬令营」活动，这是由「教育改变晋江」联合「灵源街道教育发展促进会」以及企业举办的活动，虽然为期短短四天，但是我从实践中收获不少知识，积攒了或多或少的经验，见识到了许多敢打敢拼的老一辈晋江企业家...&lt;/p&gt;
&lt;p&gt;最重要的是结识了一群伙伴，我很难用语言去形容这样一次奇妙的相遇，还有那些我亲身参与过的活动：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090808901.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090808970.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090807070.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;👋再见了，班长&lt;/h2&gt;
&lt;p&gt;数不清的打卡、核酸检测以及班上一些大大小小的事务，基本都由班长包揽了。&lt;/p&gt;
&lt;p&gt;有次在朋友圈无意看到这样一段文字，让我笑麻了，这就是班长：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090810481.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;没错就是我，担任一年班长，其实蛮辛苦的，不过在其位、谋其职，一切也似乎很合理。虽然呢，我没有做出什么卓有成就的贡献，但我问心无愧的做好了每一件事，不过迫于毕业季的压力，我也是顺其自然在大三结束后卸任班长一职。&lt;/p&gt;
&lt;p&gt;辅导员却是找了我（班长）和我舍友（团支书）来了一场近 30 分钟的交流，她希望我们能够继续连任，但我却从言语中感受到了 &quot;我们担任班长/团支书就是为了思政分，为了谋取利益，目的达成便逃之夭夭&quot;。&lt;/p&gt;
&lt;p&gt;存粹偏见，思政分尚且不至于让我耗费一年时间去争取，我本可以去做更多更有意义的事，那为什么当班长呢？我想，也许对于我而言，本身就是一件很有意义并且值得为此付诸精力的事情。&lt;/p&gt;
&lt;p&gt;退一万步说，在不损害他人利益的前提下，每个人为了自己的前途谋取利益，我认为这无可厚非。&lt;/p&gt;
&lt;p&gt;人生从来都是由自己定义的，我有权选择一种生活，也有权拒绝另一种生活。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一些工作&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141657945.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;🌏党员转正 | 再谈入党动机&lt;/h2&gt;
&lt;p&gt;其实去年 12 月份我俨然已经是一位（预备）党员，目前我只是满足了预备期满一年的条件，党员转正这个说法其实还不太准确，受疫情影响，「党员转正大会」推迟到下学期，还需要「党员转正大会」顺利召开后才能算是正式转正。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;转正申请书&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090910185.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;为什么要在年终总结提起这个问题，这还得回到暑假期间对面试的准备，那时候总感觉会对该问题进行提问，但提笔作答之时，我发现自己并没有给 2 年前「入党动机」一个准确的答复，或者说那时候我的回答没有掺杂自己的主观思考。我思绪万千，为什么要入党？现在，我要重新认识下这个问题。&lt;/p&gt;
&lt;p&gt;如果我的认识肤浅，那我也要寻找一个答案，这不是为了宏伟的目标，是为了给自己内心一个交代。&lt;/p&gt;
&lt;p&gt;也许当年写入党申请书的时候，我没有真切怀有人民公仆的情怀，但是担任一年的班长加之疫情防控管理，我真的体会到一种居庙堂之高的责任感，我只是一个普通人，但我也希望通过自己薄弱的一份力为集体散发光和热。&lt;/p&gt;
&lt;p&gt;我知道，入党不会让我有更多的特权和优越感，而是让我内心更加踏实，更有一种无形的约束感；或者说，这是一种信仰。&lt;/p&gt;
&lt;p&gt;如果大家能认同这样的观点，那就不只是我，而是我们。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090911274.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;一百多年前开始，党庇护一代又一代年轻人的成长，而如今，这崛起的一代又一代的党员是否能成就党和国家。&lt;/p&gt;
&lt;h2&gt;💪身体和灵魂，总有一个要在路上&lt;/h2&gt;
&lt;p&gt;今年的运动量可以说是微乎其微了，运动主要集中在上半年，相比去年少了不少健身的时间。&lt;/p&gt;
&lt;p&gt;锻炼量骤减有两个原因，一方面是下半年疲于奔命，另一方面是锻炼过量导致左右手受伤（如图），休养了小半年时间。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Keep · 2022&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090914280.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Keep Running&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090915661.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;3 公里用时突破 13 分钟！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090915323.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;部分器械&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090915325.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;小半年没运动，四肢有些退化了，至于下阶段的训练计划，已经在酝酿了，「这是计划的一部分」😏&lt;/p&gt;
&lt;h2&gt;😷疫情时代下的我们&lt;/h2&gt;
&lt;p&gt;大学才 4 年，疫情占 3 年，于是这段青春有了网课，打卡，封校；还有发不完的健康码和行程码，填不完的表格，回复不完的群接龙…&lt;/p&gt;
&lt;p&gt;其实不论高考、择校、上课、考研、就业，大学生的人生轨迹都受了疫情很大影响。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;奥密克戎时期弥足珍贵的物资&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090937626.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这轰轰烈烈的三年疫情管控慢慢地将要落下帷幕。从 2019 年 12 月 8 日武汉疫情爆发，到广州打响最后一枪，再到北京红码赋绿码和 2022 年 12 月 7 日「新十条」为标志，象征着「防疫工作」的结束。这三年的疫情，三年的艰难跋涉，有多少悲壮，就有多少欣慰与向往。&lt;/p&gt;
&lt;p&gt;遗憾吗？挺遗憾的。大学期间没能去更多的地方看看山水，还未踏足祖国大好河山。&lt;/p&gt;
&lt;p&gt;遗憾吗？也没什么遗憾的，我才 22 岁，我还有一只手数不过来的三年。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090923068.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;希望大家能明白结束的仅仅是防疫工作，并不是疫情！并且感谢这三年疫情所有无私奉献的人，山高路远，各自珍重。&lt;/p&gt;
&lt;p&gt;待疫情稳定，我也希望能再度到泉州这座充满岁月的古城逛逛，走在满城都是闽南红的街道，逛在几十座各类的宗教寺院中，享受地道的闽南风味...&lt;/p&gt;
&lt;h2&gt;👻助导 | 迎新季的新身份&lt;/h2&gt;
&lt;p&gt;早在七月份便接手了助导这个支线任务，导致八、九月份的工作应接不暇。&lt;/p&gt;
&lt;p&gt;八月份主要是在线上给新生答疑，我一人对线近 200 名软件工程专业的新生，键盘侠本人。&lt;/p&gt;
&lt;p&gt;九月份则是奔波于各个场合：新生宿舍、辅导员办公室、学生街...&lt;/p&gt;
&lt;p&gt;那段期间手上总有工作需要处理：发布不完的表格，统计不完的数据，开不完的会议，布置不完的场所，检查不完的宿舍，熬不完的夜...&lt;/p&gt;
&lt;p&gt;但不得不说，我的抗压能力又得到了进一步的提升。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;许许多多繁杂的材料需要整理和汇总&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090923507.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;迎新日&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090924572.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;🥳夏令营 &amp;#x26; 保研&lt;/h2&gt;
&lt;h3&gt;下一站，厦大&lt;/h3&gt;
&lt;p&gt;大三结束后，我便一头扎进「夏令营」的备考当中，期间准备了一大摞材料，在投递简历的过程给我提供了很大的便利，不过准备这些资料也花费了我非常多的心血...&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;夏令营面试材料 &amp;#x26; 推免材料&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090925007.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;一轮初筛后，我拿到了厦大、中南和中科院的面试机会，简要说说这三个院校的面试：&lt;/p&gt;
&lt;p&gt;[x] 中科院我途中退出了，Pass&lt;/p&gt;
&lt;p&gt;[x] 厦大是最早参加的，所以我基本的心思都在备战 XMU 夏令营，笔试面试各占 50%&lt;/p&gt;
&lt;p&gt;[x] 面试：英语口语有备而来轻松化解，个人介绍部分发挥稳定，不过问答环节稍显逊色【20 分钟】&lt;/p&gt;
&lt;p&gt;[x] 笔试：操作系统 + 计算机网络 + 数据结构 + 数据库 + JAVA【2.5 小时】&lt;/p&gt;
&lt;p&gt;[x] 中南是最后发的二轮面试，花了一个晚上草草准备了一下：个人介绍部分较为随意，较难的是「英语自我介绍」和「口语问答」，因为距离上次英语自我介绍已经过去一个月了，我脑子里已经没有任何英语口语相关的内容了，好在自由发挥比较出色，还被面试官表扬英语口语不错，口语问答也是临场发挥，有惊无险的通过了，整体面试下来只花了 15 分钟不到就结束了&lt;/p&gt;
&lt;p&gt;🌈有趣的是，在这次夏令营中我的编号是 No.1，而且也是软工系第一位报道的，同时我在 GitHub CS 保研仓库中唯一提交的信息也是厦大夏令营招生简章，最后就是夏令营舍友全部放鸽子了；只能说「天时·地利·人和」优势尽在我 [doge]&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090926799.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;备战 XMU 夏令营&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141658611.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;夏令营：中南大学 &amp;#x26; 中科院 &amp;#x26; 厦门大学&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141658934.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141659617.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141659434.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 5 月至 7 月挣扎的这段期间内，很感谢「林老师」和「毛老师」的鼎力支持，让我有幸能够在厦大夏令营拿到「&lt;strong&gt;优秀营员&lt;/strong&gt;」！夏令营结束后，本着躺平的心态也没打算继续参与「九推」。&lt;/p&gt;
&lt;p&gt;接下来就是静候本科保研名单的公示，如果不出意外的话，那应该是不出意外了😏&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;推免名单公示（仅截取部分）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090930222.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这算是给大学生涯的一个答复，至于我能以专业第一的名次保研，三年前我便深信不疑。你要想做成一件事，首先要相信这件事。&lt;/p&gt;
&lt;p&gt;值得一提的是，我们专业今年的保研率只有 &lt;code&gt;4%&lt;/code&gt;（6/150），我们班级的保研率在 &lt;code&gt;6%&lt;/code&gt;（3/50），我们宿舍的保研率在 &lt;code&gt;50%&lt;/code&gt;（2/4），有意思的一组数据。&lt;/p&gt;
&lt;p&gt;一切尘埃落定，当我在「研招网」接受厦大录取通知后，更多的不是喜悦，而是释怀，六年前的问题终于有了一个答案。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;初中毕业相册 | 2016 年写下...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202507040228471.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;XMU&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090928947.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;番外篇：推免名场面&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090930939.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090953119.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141701757.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141701239.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090932177.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090934546.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090932273.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090933375.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090937311.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090933441.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090934043.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090935483.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090934189.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141701684.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;🏆世界杯 | 阿根廷夺冠&lt;/h2&gt;
&lt;p&gt;世界杯中「阿根廷」的每一场比赛我都有看，虽然我不赌球，但是心里早已把冠军投给了阿根廷队，今年非常希望能看到「梅西」捧杯！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090941496.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;遥看 2014 年阿根廷在「巴西世界杯」决赛中惨遭德国绝杀，赛后梅西凝望着大力神杯的一幕令人动容。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一步之遥&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090942678.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;2018 年阿根廷对上了法国，可惜阿根廷 3 : 4 不敌法国。&lt;/p&gt;
&lt;p&gt;又一届世界杯，同样是阿根廷对战法国，这决赛的剧本属实把气氛拉满——下半场姆巴佩 97s 追平 2 球，加时赛 1 比 1 平拖入点球大战。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;迪马利亚破门&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090942688.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;加时赛梅老板再进一球！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-7eff5bde27cd98487faf2803de5e78b5_b.gif&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;绝杀！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-cea8f770d458ff659ead09a16b133fbe_b.gif&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;历史没有重蹈 4 年前的覆辙，历史唯一不变的事实，就是一切都会改变，最终梅西也是如愿以偿捧起大力神杯。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090943442.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;那天凌晨 2 点有幸见证阿根廷夺冠，现场的气氛真是燃炸了！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「烧烤店 · 阿根廷主场」又是哲哥请客的一天（哭死&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090943791.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;⚽除了阿根廷夺冠的画面，「贺炜」在世界杯决赛的解说也令人印象深刻：&lt;/p&gt;
&lt;p&gt;&quot;卢赛尔体育场绚烂的烟火，是在为阿根廷人欢呼！同样也为所有参加此次世界杯的人而送上祝福与敬意！冠军只有一个，但是所有人都有为自己的梦想去努力的机会。&quot;&lt;/p&gt;
&lt;p&gt;&quot;四年一度的世界杯，就像年轮一样，一圈一圈的镌刻着历史的脚步，讲述着巨星的叱咤风云或者黯然神伤，也讲述着我们自己生命的推演。&quot;&lt;/p&gt;
&lt;p&gt;&quot;要知道，梅西这一代的运动员，在上一次阿根廷队夺冠的时候，他们都还没有出生，但是他们给阿根廷整个国家带来的关于世界杯的美好记忆却是传承了下来。&quot;&lt;/p&gt;
&lt;p&gt;&quot;电视机前的观众朋友们，问问我们自己，四年前陪你看球的人现在还在联系吗？四年后看球的自己许过的愿望都实现了吗？&quot;&lt;/p&gt;
&lt;p&gt;&quot;我们为什么深爱着足球这项运动？因为它不仅展现了球员们励志的奋斗故事，还寄托了我们普通人平凡生活中的英雄梦想。&quot;&lt;/p&gt;
&lt;p&gt;&quot;我们恭喜阿根廷，我们也向法国队送上祝福。无论今晚你支持的球队是胜是负，都希望今天晚上的感悟能够帮助你勇敢面对明天早上推开门之后真实的生活，这才是这项运动真正的魅力。&quot;&lt;/p&gt;
&lt;p&gt;&quot;我爱足球，我想你们也是。&quot;&lt;/p&gt;
&lt;p&gt;&quot;波斯湾的故事讲完了，四年之后，让我们相约在落基山，尼亚加拉瀑布，尤卡坦半岛，让我们一起去玛雅文明曾经存在过的地方。观众朋友们，四年之后，让我们在美加墨世界杯再见。&quot;&lt;/p&gt;
&lt;h2&gt;😂家庭教师 | 有点东西但不多&lt;/h2&gt;
&lt;p&gt;今年在闲暇之余也是接手了一个支线任务「家教」：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;家教对象：初三学生&lt;/li&gt;
&lt;li&gt;家教课程：中考数学 &amp;#x26; 中考物理&lt;/li&gt;
&lt;li&gt;家教时长：每周末 4 小时&lt;/li&gt;
&lt;li&gt;家教价格：100 / hour&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;教材（还有许多习题集就不展示了）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090945811.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在家教期间，我看到了很多学生时代我所犯过的错误，也接触到了我从未犯过的错误，并了解到了我以后可能会犯的错误——包括学习，不止学习。&lt;/p&gt;
&lt;p&gt;其实兜兜转转说了这么多，我只想说一点，不要害怕犯错，尤其是年轻时代的犯错成本是很低的。你犯过的错，吃过的亏，那都是一剂疫苗，防止这些龃龉烂入骨髓腐蚀你的生活。以无声无息的形式温水煮青蛙，可比犯错一时揭伤疤来得狠戾。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;故事的结尾&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141703590.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;对方家里甚至有一位在上幼儿园的孩子已经在学习「少儿编程」了，我想听到这在座的各位心里多少都会有点感触。其实也不用过于焦虑，每个时代有每个时代的基准线，对于我们而言，做好当下每一件事，走好当下每一步路，就足够了。&lt;/p&gt;
&lt;h2&gt;😎升级打怪的实习期&lt;/h2&gt;
&lt;p&gt;身处互联网行业的我们，今年应该都能感受到「春招和秋招」的岗位明显缩减。强监管、反垄断、疫情扰动、消费不振等诸多不利因素钳制下，中国互联网流量红利已近枯竭，下沉市场的故事偃旗息鼓。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.36kr.com/p/2050218223736068&quot;&gt;2022 年互联网&lt;/a&gt;影响最深的一件事莫过于“一波接一波的裁员大潮”。从年初的大厂“毕业季”到年末的裁员大潮，还有硅谷的大裁员，国内外很多小伙伴都离开了自己的岗位，需要重新找工作，却又四处碰壁。&lt;/p&gt;
&lt;p&gt;这个行业实在变化太快，“一招鲜，吃遍天”这种事情是不存在的，我们总会遇到从未接触过的新挑战，怎么办？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;学习&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;就如同一个优秀的企业，它最有价值的地方在于它有无限的发展前景。那么一个优秀的互联网从业者，最有价值的地方就在于拥有无限的潜力。&lt;/p&gt;
&lt;p&gt;抛开远的不谈，对我来说值得庆幸的是，学校将实习时长从 4.5 个月降到了 3 个月~&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;未完成的实习报告手册&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141703670.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;租房那些事&lt;/h3&gt;
&lt;p&gt;我前后居住的 2 个宿舍分别是「民宿」和「客栈」，房租大概都在￥1500 左右，但我导报销。&lt;/p&gt;
&lt;p&gt;简单聊聊这几个月的居住感受。&lt;/p&gt;
&lt;h4&gt;三星民宿⭐⭐⭐&lt;/h4&gt;
&lt;p&gt;居住在民宿三楼，房东在一二楼，房顶晾晒衣物。&lt;/p&gt;
&lt;p&gt;民宿外观还行，但房间内部较为朴素，隔音较差，那段期间正好赶上周遭装修，体验可想而知，其次是洗衣晾晒不太方便，不过这都是些小问题，整体居住的感觉还行。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;民宿&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090945073.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;四星客栈⭐⭐⭐⭐&lt;/h4&gt;
&lt;p&gt;十月份结束后，从民宿转到客栈，只能说客栈完美的弥补了手洗衣服的缺点——洗衣机！除了房东不允许我凌晨 0~5 点洗衣服以外，感觉都还行，哈哈哈。&lt;/p&gt;
&lt;p&gt;虽然房间可以放置物品的空间不大，哑铃也只能随意堆在角落，双人间的装修可以说是很有观赏性（其实我有得住就知足了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;客栈&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090945878.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Massive Storage · 挑战赛&lt;/h3&gt;
&lt;p&gt;年末参与了一场「存储比赛」，那时候带着好奇加入飞哥的团队，闯一把，不过从头到尾我的贡献微乎其微，充当着重在参与的角色 [doge]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「华为」和「华科」联合举办，奖金有点诱人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141704481.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Coding...（99.99% 的代码都是飞哥码下的）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141704896.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;初赛 Rank 15th，决赛加油&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141705459.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;飞哥一路上带我闯进决赛，这个比赛过程中我也学到不少知识，不论最后结果如何，我这把比赛躺了😎&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bj.bcebos.com/baidu-rmb-video-cover-1/1df2f4d4da6d4a3f96b25aa3b9f00d0c.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;来到华科领奖啦&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141709638.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141709485.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;新手村培训&lt;/h3&gt;
&lt;p&gt;实习期内，哲哥牺牲个人的时间出了一期有针对性的培训，同时对我的问题知无不言言无不尽，二者都使我受益良多😭&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141710752.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;完成任务期间还求助过许多朋友，大家对我都很耐心，包容性极强。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141711196.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不过，我还是需要不断提升自己才行，毕竟编码能力菜就是原罪，现在的水平还是让人堪忧。&lt;/p&gt;
&lt;h3&gt;口语录制&lt;/h3&gt;
&lt;p&gt;今年也尝试了许多以前没有踏足的领域，比如给 PPT 录制视频并配上英文讲解（定稿）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141711286.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141711462.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;原本以为只是简简单单的口语短文小测，可惜我错了。录制 15 分钟的口语汇报，耗费了我 8 小时持续不停的口语朗读，就是一个在失败中不断尝试，再在尝试中不断失败的过程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;心凉了半截&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141712933.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原本想以普通人的身份和大家相处，没想到换来的却是疏远，不装了，我摊牌了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141712868.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;躺在邮箱中的邮件&lt;/h3&gt;
&lt;p&gt;自从不怎么使用 QQ 这个玩意儿，我基本都在各大论坛和开源社区闲逛，交流工具基本剩下「微信」和「邮件」，以前感受不到为什么都喜欢使用邮件，现在能明白了，前人发明的一切交流工具都是为了传递基本的信息，在表达清楚自己意图的基础上，怎么简单怎么来！&lt;/p&gt;
&lt;p&gt;偶尔我也会向大牛咨询一些问题或者索要代码（大牛基本都用邮件交流多一些，有别于我用邮件纯粹因为它简单）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;索要代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141712368.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;咨询问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141712549.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;勇于尝试，大胆尝试，就算被拒绝了也不见得是坏事。&lt;/p&gt;
&lt;h3&gt;周报真的是互联网最糟糕的发明&lt;/h3&gt;
&lt;p&gt;写了将近 4 个月的周报，我对周报的厌恶感不断攀升，虽然很痛苦，但还是要写。&lt;/p&gt;
&lt;p&gt;老板也很焦虑，进展不快的第一反应往往就是抓人效，首当其冲就是检查周报是否完成、尽善尽美。&lt;/p&gt;
&lt;p&gt;那么问题来了，周报写得好，真的能提升我们的成果吗？&lt;/p&gt;
&lt;p&gt;答案肯定是否定的。&lt;/p&gt;
&lt;p&gt;周报充其量是个人总结的一种方式，有别于会议，它并不具备实时性和快速性。有人会认为周报是组织交流、上传下达的重要渠道。但事实上，一次好的周报应该是有反馈的，但反馈的效果往往由你的 leader 决定，问题就出在这里，你可以是一个非常努力完成周报的人，但你不能要求你的 leader 跟你一样认真，这也是我前期周报认真完成，后期直接开摆的原因，因为我逐渐意识到了这个问题并且无法从根源上去解决。&lt;/p&gt;
&lt;p&gt;周报存在的意义是什么？单单从务实的角度讲，无非就两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;复盘过去的工作&lt;/li&gt;
&lt;li&gt;明确未来的工作&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;既然如此，不如直接简单开个短会来得明朗，不过每周的组会已经让我有点喘不过气了，所以建议能省就省。&lt;/p&gt;
&lt;h3&gt;夜猫子生活&lt;/h3&gt;
&lt;p&gt;白天不起，晚上不睡，在 11~12 月已成为我的常态。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当代年轻人熬夜报告，是我没错了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141713069.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;通宵除了思考人生，还可以等一轮日出（About 6 o ’clock）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090947626.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;深夜创作者&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://bj.bcebos.com/baidu-rmb-video-cover-1/8953017adee4b119b38df479ac74f449.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;我 de 工位&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Desktop 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090947559.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Desktop 2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090948474.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;必须得给大家伙秀一把新到的服务器，只能说太顶了！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090948127.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;开黑之夜&lt;/h3&gt;
&lt;p&gt;平安夜 5 人齐聚开黑，「2本科 + 1硕士 + 2博士」，这阵容一晚上赢不了一把游戏，哈哈哈&lt;/p&gt;
&lt;p&gt;除了一把没赢，我觉得整个过程都挺轻松愉快的，毕竟游戏有游戏的玩法，无关输赢，开心就好。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141713998.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;上手体验 ChatGPT&lt;/h3&gt;
&lt;p&gt;最近 ChatGPT 很火，如果你在互联网圈子里就应该听说过或者见过它的身影。简单的说，这是一个 AI 聊天工具，无聊的时候真的可以自顾自的聊一天。&lt;/p&gt;
&lt;p&gt;接下来聊聊怎么上手。&lt;/p&gt;
&lt;h4&gt;1. 注册 OpenAI 账号&lt;/h4&gt;
&lt;p&gt;进入 &lt;a href=&quot;https://chat.openai.com/&quot;&gt;OpenAI 主页&lt;/a&gt;，点击「Sign Up」进入注册流程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090951907.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在这一步，你需要填写注册邮箱和密码，还需要完成一个简单的验证。&lt;/p&gt;
&lt;p&gt;注意，输入的&lt;strong&gt;邮箱必须是非内地邮箱&lt;/strong&gt;，我用的是 Gmail 邮箱，也就是谷歌邮箱。&lt;/p&gt;
&lt;p&gt;填写完邮箱之后系统会提示你登录邮箱做一个验证，进入邮箱，点击验证链接就完成了账号创建。&lt;/p&gt;
&lt;p&gt;接下来，你会来到填写手机号码的步骤，这也是难住很多人的一步，因为我们大多数人都没有&lt;strong&gt;国外手机号&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090951348.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;别着急，凡事都有办法。&lt;/p&gt;
&lt;h4&gt;2. 注册虚拟手机号并获取验证码&lt;/h4&gt;
&lt;p&gt;前往 &lt;a href=&quot;https://sms-activate.org/&quot;&gt;SMS-ACTIVATE&lt;/a&gt; 并注册一个账号，这一步同样需要使用邮箱验证（&lt;strong&gt;内地邮箱即可&lt;/strong&gt;）。&lt;/p&gt;
&lt;p&gt;这个网站提供国外虚拟手机号注册功能，目的是获取可用的短信验证码。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你英文不好的话，可以切换为中文模式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141714284.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;注册成功后，需要先给账号充值。点击网站右上角的「余额」并进入「充值」模块。&lt;/p&gt;
&lt;p&gt;这里购买一个虚拟手机号接收一次验证码大概需要 11 卢布——俄罗斯货币。&lt;/p&gt;
&lt;p&gt;不过没关系，我们可以充值美元并兑换成卢布，而充值美元可以直接用支付宝支付。&lt;/p&gt;
&lt;p&gt;我算了下，只接收一次验证码的话充值 0.2 美元就够了，也就是说你需要花费大概 1.4 元人民币（我充值了 1 美元，有需要注册的小伙伴可以联系我，免费让你体验）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090952174.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;充值成功后，就可以选择对应的服务商来使用虚拟手机号接收验证码了。&lt;/p&gt;
&lt;p&gt;这里我选择的是印度的服务商，接着在服务项目里输入 OpenAI 并选择买入一个。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090952515.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;购买成功后，你会被分配一个虚拟手机号，下图中那一长串数字就是手机号。&lt;/p&gt;
&lt;p&gt;此时，复制这串号码回到 OpenAI 的注册现场，然后点击获取验证码。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090952117.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里要注意，你在 OpenAI 注册手机号选择的归属地和在这里注册的虚拟号码归属地要保持一致。&lt;/p&gt;
&lt;p&gt;大概几秒钟过后，你会在上面这个页面看到刷新出来一个 6 位数的数字，这就是你的验证码。&lt;/p&gt;
&lt;p&gt;复制这个验证码填写到 OpenAI 的注册页面，点击提交，此时账号就创建成功了。&lt;/p&gt;
&lt;p&gt;至此，你就可以正式开始使用 ChatGPT 了。&lt;/p&gt;
&lt;h4&gt;3. ChatGPT 初体验&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141714687.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;抛开内容不谈，你就说它回复了没 [doge]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141714194.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不过在我看来，使用 ChatGPT 最高的门槛不是上面这些步骤，而是如何向他提出一个好问题，也就是「提问的艺术」。&lt;/p&gt;
&lt;p&gt;此外，ChatGPT 目前并没有达到人机自如的理想状态，有些回答略显生涩和模板化，甚至会有纰漏。&lt;/p&gt;
&lt;p&gt;不过这不要紧，因为这只是一个开端，未来它的进化速度会很快，就像当初 AlphaGo 的进化能力一样。&lt;/p&gt;
&lt;p&gt;如果你有一些疑问，或者有关于人生的终极思考，又或者郁闷想找人聊聊天，不妨试试。&lt;/p&gt;
&lt;p&gt;ChatGPT 让我感受到了一种未来，就是某些职业可能会失业，比如新闻报道记者，比如通稿写作者。&lt;/p&gt;
&lt;p&gt;虽然这些影像还很模糊，但未来真实地在向我们靠近。&lt;/p&gt;
&lt;p&gt;未来已来，你还不来？&lt;/p&gt;
&lt;h2&gt;🍂又是一年 | 春夏与秋冬&lt;/h2&gt;
&lt;p&gt;须臾之间，又是一年，春夏与秋冬。&lt;/p&gt;
&lt;p&gt;以往本着「活在当下」的态度碌碌无为、自得其乐地度过了一年又一年的四季，未曾想记录生活又何尝不是一种自由。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;知乎 &amp;#x26; 飞书：用文字记录生活&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://bj.bcebos.com/baidu-rmb-video-cover-1/6c150d6f22b2b3c3c763dd3818b99a8b.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;网易云音乐 · 年终总结&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090955420.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;📸Snapshot&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090954713.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有点东西，但不多&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090955267.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;🍀我的摆烂文学&lt;/h2&gt;
&lt;p&gt;其实这一年走来，刻苦努力的那些日子里，总是让我百感交集。&lt;/p&gt;
&lt;p&gt;我赶过夜晚近 23 点的末班车，也进过凌晨 2 点的粥铺，目睹过凌晨 4 点空旷无垠的大街，等待着 6 点初升的太阳，更在熬了一夜过后的 8 点懒洋洋地回到宿舍...&lt;/p&gt;
&lt;p&gt;几乎在每一个不合理的时间点，我都存在过，在码字、在阅读、或者在思考，或许没意义，但是我乐意。&lt;/p&gt;
&lt;p&gt;除了精力充沛外，我知道那时候的自己想要什么，需要做点什么。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202310141715859.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不过有一点值得说明，我从未认为自己的行径算得上内卷，一方面可能是我努力的程度远远不及「内卷」，另一方面可能是我对于内卷的理解有所偏驳。&lt;/p&gt;
&lt;p&gt;我在知乎上曾看到一句话，曾经，并没有内卷这个词汇，每个人都只是为自己的前途而努力着，自始至终。记得高中的时候，为了考大学，我每天都抓紧各种零散时间努力学习，比别人更努力，即使结果不如愿，我从不觉得羞耻。我喜欢「山本耀司」的那句 “我从来不相信什么懒洋洋的自由，我向往的自由是通过勤奋和努力实现的更广阔的人生，那样的自由更加珍贵、更有价值”。&lt;/p&gt;
&lt;p&gt;第一次接触内卷，是在 2 年前，那时候内卷一词流行于各个角落，起初并不懂它的含义，书本将其定义为人类社会在一个发展阶段达到某种确定的形式后，停滞不前或无法转化为另一种高级模式的现象。&lt;/p&gt;
&lt;p&gt;好比一个流量很大的地铁站，早高峰很难挤上地铁，有一天，一个人决定先反向坐一站，提前上地铁，防止挤不上来。后来越来越多的人这样效仿，大家耗费了更多的时间和精力，却得到一样的结果。&lt;/p&gt;
&lt;p&gt;我从不认为谁的努力如“内卷”的释义一般。&lt;/p&gt;
&lt;p&gt;同时我也时常庆幸，在我的十二年寒窗里，内卷一词并未泛滥。我怀念高中的时候，我们在纸条上写下各自的理想大学，朝着各自目标努力着，简单、自由且纯粹。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不过，这种自由的状态也仅仅占有我生命中的少部分时候，我更多时候的状态是一如既往的「安于现状、得过且过」，我没有部分人认为的那般拼命和聪慧，却也是大部分人所认为的那般懒惰与笨拙。&lt;/p&gt;
&lt;p&gt;我也时常停下脚步，不知道为什么前进，那些超出预期的差池总是令我徘徊在常规性的自我厌弃之中，并不断靠着游戏和短视频等迷幻药短暂性的捕捉虚无的快乐。又或者是天一黑夜已深，网易云一开，气氛一到位，我就开始思考一些平时不假思索的问题，科技伦理，人类进化，宇宙边缘，生命尽头...&lt;/p&gt;
&lt;p&gt;十年前大家都在看「成功学」，现在大家都在看「摆烂学」，但大部分人却又处于「摆了又没完全摆，躺平又没完全躺，摸鱼又没完全摸」的处境。我能体会到为什么，因为烂的感觉并不好，搁在图书馆偷懒躺平摸鱼，躺在床上刷 b 站的滋味并不好受，它总是在清醒过后唤醒我灵魂深处最虔诚的忏悔和鄙视。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Wu-yikun/OSS/master/PicGo/202301090956350.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;摆烂又或许是害怕失败的一种逃避手段。不确定性的存在是无解的，机会成本的产生是必然的，没有人愿意承担失败带来的代价，成功学的理念在大家 DNA 里刻得太深，不能失败的观念始终是悬在我们头顶的「达摩克里斯之剑」，把人压得喘不过气来，这一套对于高考来说是行得通的，因为这个社会的规则便是如此。但它并不适用于我们的人生，人生太长了，大家又不是只活二三十年。&lt;/p&gt;
&lt;p&gt;但那个把全部的生活过成竞争，把所有的悲欢都取决于排名，把每一次失败都反求诸己的你，一定很累吧。&lt;/p&gt;
&lt;p&gt;你当然足够倔强，足够拼命，足够努力，足够优秀；&lt;/p&gt;
&lt;p&gt;但你真的足够自信，足够乐观，足够快乐，足够善良吗？&lt;/p&gt;
&lt;p&gt;还是说。&lt;/p&gt;
&lt;p&gt;其实你对父母充满愧疚，对名利充满贪婪，对自己充满怀疑，对同窗充满嫉妒，对社会充满怨言，对竞争充满恐惧，对成功沾沾自喜，对失败无所适从，对未来一片迷茫？&lt;/p&gt;
&lt;p&gt;也许这个社会并不需要所有人都成功，这并不是要宣传摆烂文学，只是想提醒自己保持一颗平常心。万事记得，来日方长。&lt;/p&gt;
&lt;h2&gt;🌗人生最重要的一课：感谢他人&lt;/h2&gt;
&lt;p&gt;致谢与铭记他人的恩情，从来都不是人生的选修课，而是必修课。&lt;/p&gt;
&lt;p&gt;2022 结束前，我还要感谢那些一而再，再而三，千千万万次救我于水深火热困境中的人们。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;感谢我「爸妈」的无私奉献，2022 以及以往的每一年里，都是你们用「物质和精神」默默支持着我的每一个决定&lt;/li&gt;
&lt;li&gt;感谢陪我耍过大学四年的舍友
&lt;ul&gt;
&lt;li&gt;感谢「鑫杰」，我永远不会忘记我们为了保研共同奋斗过的那些日子，我们有许多共通的爱好和相似的经历，也各自怀揣着不同的理想主义和现实眼光，也许是这样一种羁绊，才让我更加珍惜这段友谊无可替代。这一年里，不，或者说是这三年里，我从你身上不断学习到许多前所未闻的认知和视野，你始终站在我的知识盲区内，未来希望还能一起努力，一起进步&lt;/li&gt;
&lt;li&gt;感谢「鑫欣」给我上了一课又一课，三观和认知都凌驾在我之上的退伍军人，从你身上我见识到了军人般的自律，也让涉世未深的我学到不少深刻的道理，尤其是暑假独处的那段时间，我都默默看在眼里&lt;/li&gt;
&lt;li&gt;感谢「品庚」和「灿阳」一直以来的帮助，在我最需要的时候总是会义无反顾的出手帮我&lt;/li&gt;
&lt;li&gt;还有大一舍友——「光箭、健业、强钵、文杰」，虽然我们五人帮见面的机会少了，但一有时间我们总能无话不谈，事实证明，五个人的友情并不拥挤&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;感谢我的「大学同学和朋友们」，你们的存在点缀了我短暂的大学生活&lt;/li&gt;
&lt;li&gt;大学期间犯了不少错，自知给辅导员带去了不少麻烦，所以很感谢「黄导」和「张导」的包容&lt;/li&gt;
&lt;li&gt;感谢「展鹏学长」、「寿铭学长」、「泓浩学长」、「冠钞学长」、「良宽学长」以及本科期间一直以来不吝赐教的各位学长们&lt;/li&gt;
&lt;li&gt;感谢「慧怡学姐」和「王玲学姐」一直以来的耐心指导&lt;/li&gt;
&lt;li&gt;感谢「成双成队」的各位小伙伴给我这一年留下浓墨重彩的一笔&lt;/li&gt;
&lt;li&gt;还得狠狠感谢下「若珩」，这一年给了我不少帮助，不愧是心理学高材生。百忙中还给我送来了一些药物，同时谢谢一些关心我的小伙伴们&lt;/li&gt;
&lt;li&gt;还有就是当然要感谢实验室的 XDM，来自华科、华南理工、厦门、武大等众多高校的你们都比我优秀太多了，有太多值得我学习的地方了，我作为实验室的下水道级别，以后还请大家多多指教（说说这段时间以来的感悟）
&lt;ul&gt;
&lt;li&gt;在我融入实验室节奏的过程中，「哲哥」总是不厌其烦地给我讲解我所咨询的问题，懂得换位思考，简单的交流也能让我受益匪浅，以至于现在都有些许依赖性，遇到不顺的事会为我们打抱不平，甚至牺牲自己的时间来提升大家的水平，不愧是实验室的天花板选手；除此之外，生活中对我（们）都很包容和大度，心思缜密，双商拉满。总之我对哲哥的感激之情无法言喻。不过几个月的相处，我想就足以让我永远铭记于心了，令我感到可惜的是，我们相处的时间可能所剩无几了💔&lt;/li&gt;
&lt;li&gt;也很感谢「锋哥」，作为实验室科研的扛把子，你总能提出独到的见解和发人深省的问题，在科研理解上和生活中你都给予了我不少的帮助，今年更是直指顶会，预计 2023 年你便可驰骋于各大顶会带我们起飞。令我不解的是你总认为我有点东西，可惜我真没有哈哈哈，还有至今都还没有让你看得上的异性，也许是一心科研吧。&lt;/li&gt;
&lt;li&gt;认识「芝豪师兄」源于某次偶然的询问“我是谁”，虽然当时我在后座来着哈哈。芝豪师兄是软硬件都精通的大佬，平时服务器出现了点什么问题总会去麻烦和请教一下，虽然平时交流并不频繁，但都有问必答，而且一针见血。&lt;/li&gt;
&lt;li&gt;学累了怎么办？无所谓，「飞哥」会出手。最早熟悉的当属飞哥了，8 月份便和你一同居住，我能感受到，你将灵活科研的理念贯彻到底，「&quot;休闲&quot; &amp;#x26; 麦麦 &amp;#x26; 大骨」是提升你科研效率的三件套，耳濡目染下我也能熟练地运用你的三件套来融入我的学习中，而且作为网易和字节出来的人才，假以时日，你也能杀进各大顶会，于你而言简直是信手拈来。当然除了比赛带飞我，对于学习上不懂的问题，飞哥也总是能三两下帮我解决，突出一个醍醐灌顶。不过有一说一，体重该减就得减，明年带你一起减肥 [doge]&lt;/li&gt;
&lt;li&gt;从 8 月份开始，我就开始请教「子航师兄」了，关于我所研究的方向，如果没有子航师兄的指点，想必我又要走不少弯路，真的十分感谢。尽管素未谋面，但是子航师兄总是会竭尽所能给我讲解我不懂的知识，纠正我的认知偏差，悟性和学习能力极强的人大概就是如此了。不过在师兄看来较为简单的问题，但我总需要花费较多时间去学习，没办法我的悟性和学习能力较差，望谅解。同时我也为自己这段期间的打扰说一声抱歉。还有子航师兄总是能在我压力很大的时候开导我，不然可能真顶不住这跨越式的压力而崩溃，总之呢很期待之后师兄的回归，一定要注意身体，以后一起坐着把鱼摸嘻嘻。&lt;/li&gt;
&lt;li&gt;我觉得最有缘分的应该得算「佳泓师兄」了，6 月份凑巧有机会和泓哥取得联系，也很耐心地给我介绍了实验室的大体情况，我学习到不少。之后换宿舍也很巧地和泓哥一同居住，这一住便是 2 个月，只能说相处得十分融洽。佳泓师兄的每一句话都能很好的给我减压，平日里也很谦让我，还教会了我不少科研生存法则，有时总能和泓哥聊到了凌晨 5、6 点，堪比人生导师。有时也能约着一块打篮球，不得不说，你的篮球很有东西还能教会我不少技巧。让我难受的估计就是今年一别，不知道还有没有机会再见。&lt;/li&gt;
&lt;li&gt;然后得说到实验室的谐星（估计以后是我了）——「宇轩师兄」，估计是实验室最快乐的人了，初来乍到宇轩也很照顾我，在生活某些方面总能给予我帮助（比如餐卡），我很羡慕你那无忧无虑的生活，成功的人生也不过如此吧。你也教会了我不少的道理，让我明白了做人不能太自私，以后我也会多站在对方的角度去思考问题的。&lt;/li&gt;
&lt;li&gt;同时非常非常感谢「河山」，平日里很热情的解答我的问题，有困难你是真上。给我的感觉就是河山一出手，就知有没有（ACM 银不是开玩笑的）。学习上基本没有难得住你的问题，不仅游戏玩得好，主要是平易近人很玩得来。除了摸鱼水平能和你一较高低以外，感觉没有什么可以和你相提并论了&lt;/li&gt;
&lt;li&gt;还有要谢谢「张宇」抽空陪我摸鱼，当然对于我不懂的问题你也是知无不言，相信未来的某一天，你一定能给老师调整调整科研方向。&lt;/li&gt;
&lt;li&gt;虽然今年没机会见到「立瀚」，但没关系，线上该出手就出手，明年见。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;再次感谢「林老师」的推荐和支持以及「毛老师」和「吴老师」的帮助&lt;/li&gt;
&lt;li&gt;如果可以，我还要感谢一位朋友，让我真正的明白了我所拥有的一切都是侥幸，er 失去的才是人生，未来还多需努力。只不过，遗憾终究是遗憾。&lt;/li&gt;
&lt;li&gt;以及感谢那个未曾放弃且笃定前行的自己！&lt;/li&gt;
&lt;li&gt;...（此处省略 1W 字）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总之，感谢在这一年里对我如此包容的大家，我会永远铭记于心的。&lt;/p&gt;
&lt;h2&gt;💖未来&lt;/h2&gt;
&lt;p&gt;世界上每个人本来就有自己的发展时区。&lt;/p&gt;
&lt;p&gt;身边有些人看似走在你前面，也有人看似走在你后面。&lt;/p&gt;
&lt;p&gt;但其实每个人在自己的时区有自己的步程。&lt;/p&gt;
&lt;p&gt;不用嫉妒或嘲笑他们。&lt;/p&gt;
&lt;p&gt;他们都在自己的时区里，你也是。&lt;/p&gt;
&lt;p&gt;生命就是等待正确的行动时机。&lt;/p&gt;
&lt;p&gt;所以，放轻松。你没有落后，你没有领先。&lt;/p&gt;
&lt;p&gt;在命运为你安排的属于自己的时区里，一切都准时。&lt;/p&gt;
&lt;p&gt;至于未来，是极乐，是悬崖，还是围城？反正都是未知，不如索性期待。&lt;/p&gt;
&lt;p&gt;以上！&lt;/p&gt;</content:encoded><h:img src="/_astro/202412091603906.CafZsx05.jpg"/><enclosure url="/_astro/202412091603906.CafZsx05.jpg"/></item><item><title>Golang 编程指南</title><link>https://coooredump.github.io/blog/golang/golang-wiki</link><guid isPermaLink="true">https://coooredump.github.io/blog/golang/golang-wiki</guid><description>目前字节跳动已全面拥抱 Go 语言，除此之外，哔哩哔哩、七牛云、腾讯、百度、美团、Facebook、Google、Twitter、滴滴、深信服、知乎、去哪儿、360、微博等公司也在大量使用 Go 语言。</description><pubDate>Sat, 14 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文中的源码地址：https://github.com/wangkechun/go-by-example&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;学习目录&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202228870.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;1. Go 简介&lt;/h2&gt;
&lt;p&gt;介绍下 Go 语言的特性以及目前 Go 语言在市场上的 &quot;帝位&quot;。&lt;/p&gt;
&lt;h3&gt;1.1 Go 语言特性&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;高性能 &amp;#x26; 高并发：Go 语言天生支持高并发（&lt;strong&gt;goroutine &amp;#x26; channel &amp;#x26; 调度器&lt;/strong&gt;），不像很多语言通过库的形式支持；Go 像一些低级别的语言 (C/C++) 一样是一门&lt;strong&gt;编译型语言&lt;/strong&gt;，这意味着它的性能足以媲美 C/C++！&lt;/li&gt;
&lt;li&gt;语法简洁易懂：语法类似 C 语言，比如：同时去掉了不需要的 &lt;code&gt;()&lt;/code&gt;，循环也只简化成 &lt;code&gt;for&lt;/code&gt; 循环这一种表示... 该特点使得用 Go 编写的代码易于维护。&lt;/li&gt;
&lt;li&gt;丰富的标准库：Go 语言带有极其丰富与完善的标准库，&lt;strong&gt;无需再借助第三方库完成便可以应对日常的开发&lt;/strong&gt;，而且能持续享受到语言迭代带来的性能优化。&lt;/li&gt;
&lt;li&gt;完整的工具链：编译、代码格式化、错误检查、包管理、代码补全等都有相应的工具，Go 语言还&lt;strong&gt;内置了单元测试框架&lt;/strong&gt;（单元测试、性能测试、代码覆盖率、性能优化）。&lt;/li&gt;
&lt;li&gt;静态链接：所有的编译结果默认都是静态链接的，&lt;strong&gt;只需要拷贝编译后唯一的可执行文件即可部署运行&lt;/strong&gt;，线上发布的体积可以控制得很小。&lt;/li&gt;
&lt;li&gt;快速编译：Go 语言一开始设计就考虑到快速编译。它能像其他解释型语言一样（Python &amp;#x26; JavaScript），你不会注意它正在编译。&lt;/li&gt;
&lt;li&gt;跨平台：Go 语言能在 Linux、MacOS、Windows 等操作系统下运行，还能用于开发 Android、iOS 软件，甚至能运行在路由器、树莓派等等设备；同时具备&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E4%BA%A4%E5%8F%89%E7%B7%A8%E8%AD%AF%E5%99%A8&quot;&gt;交叉编译&lt;/a&gt;特性。&lt;/li&gt;
&lt;li&gt;垃圾回收：Go 无需考虑内存的分配与释放，因为其内存由 Go 自身进行管理，不同于 C/C++，和 Java 类似。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;简洁的 Go 语法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202236896.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1.2 拥抱 Go 语言&lt;/h3&gt;
&lt;p&gt;目前字节跳动已全面拥抱 Go 语言。除此之外，哔哩哔哩、七牛云、腾讯、百度、美团、Facebook、Google、Twitter、滴滴、深信服、知乎、去哪儿、360、微博等公司也在大量使用 Go 语言。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tva4.sinaimg.cn/large/006V2BYXly1h2g4d47r4lj31780hegtu.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202236553.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Go 语言在云计算、微服务、大数据、区块链、物联网等领域蓬勃发展，Docker、Kubernetes、etcd 等几乎所有的云原生组件全都是用 Go 语言实现。&lt;/p&gt;
&lt;h2&gt;2. Go 入门&lt;/h2&gt;
&lt;p&gt;😆这部分简单概括下如何搭建 Go 的&lt;strong&gt;开发环境&lt;/strong&gt;，浏览下 Go 语言的&lt;strong&gt;基础语法 &amp;#x26; 标准库&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;2.1 搭建开发环境&lt;/h3&gt;
&lt;h4&gt;2.1.1 下载安装 Golang&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;https://go.dev/ : Golang 官网&lt;/li&gt;
&lt;li&gt;https://studygolang.com/dl : Golang 中国镜像（Golang 官网无法打开的情况可用）&lt;/li&gt;
&lt;li&gt;https://goproxy.cn/ : 七牛云 Go 模块代理，配置 &lt;code&gt;go mod proxy&lt;/code&gt; 并按网站提示进行操作可助力提升下载第三方包的速度（优化访问 GitHub 较慢的情况）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.1.2 配置 Golang IDE&lt;/h4&gt;
&lt;p&gt;⭐首选目前市场上使用最广泛的两款 IDE : &lt;strong&gt;VSCode&lt;/strong&gt; &amp;#x26; &lt;strong&gt;GoLand&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VSCode：由微软公司开发，免费。能运行在 MacOS、Windows、Linux 上的跨平台开源代码编辑器（功能齐全的 IDE），需要在扩展市场中安装 Go 插件才能支持 Golang 开发。&lt;/li&gt;
&lt;li&gt;GoLand：由 JetBrains 公司开发，付费 (学生可申请免费使用)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202247867.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;2.1.3 云上开发环境&lt;/h4&gt;
&lt;p&gt;我们还可以使用基于 GitHub 的 &lt;a href=&quot;https://gitpod.io/&quot;&gt;Gitpod&lt;/a&gt; 在线编程环境来使用 golang，只需在你的开源仓库 URL 的 &lt;code&gt;https://&lt;/code&gt; 替换成 &lt;code&gt;https://gitpod.io/#&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202248160.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2.2 基础语法 &amp;#x26; 常用标准库&lt;/h3&gt;
&lt;p&gt;这部分开始正式学习 Golang 的语法以及常用标准库的使用。&lt;/p&gt;
&lt;h4&gt;2.2.0 从 &lt;code&gt;Hello World&lt;/code&gt; 见证 Go 程序的运行&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;fmt&quot;
)

func main() {
    fmt.Println(&quot;hello world&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有 2 种方式运行该段程序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;go run&lt;/code&gt; 命令可直接运行程序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go build&lt;/code&gt; 可将程序编译成二进制，编译完成后直接执行 &lt;code&gt;./main&lt;/code&gt; 即可运行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202249292.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;2.2.1 变量 &amp;#x26; 常量&lt;/h4&gt;
&lt;p&gt;Golang 是一门强类型语言（每个变量都有其对应的类型），变量 &lt;code&gt;var&lt;/code&gt; 如果没有标注类型会自动推导，常量 &lt;code&gt;const&lt;/code&gt; 默认没有确定类型（自动推导）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202249338.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：如果定义了变量，必须得使用，否则编译不通过&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;fmt&quot;
    &quot;math&quot;
)

// 全局变量
var x string = &quot;global&quot; // 可以

// x := &quot;global&quot;    // 不可以, := 仅可以在函数内部使用

const (
    A = iota   // iota=0, 值为 0
    B          // iota=1, 值为 1
    C = iota   // iota=2, 值为 2
    D = &quot;test&quot; // iota=3, 值为&quot;test&quot;
    E          // iota=4, 值为&quot;test&quot;
    F = 9      // iota=5, 值为 9
    G          // iota=6, 值为 9
)

func main() {
    /* ------------ 变量 ------------ */
    // 声明并赋值
    var a = &quot;掘了&quot;
    
    // 类型推导
    var b = true
    
    // 多变量定义 &amp;#x26; 类型推导
    var c, d int = 1, 9
    var e, f = 6, &quot;F&quot;
    var (
        g = 666
        h = false
    )
    
    // 简短定义(另一声明变量的方式 :=)
    i := 1.9
    
    // 类型转换: float64=&gt;float32
    j := float32(i)
    
    // 字符串可通过 + 拼接
    k := &quot;掘金&quot; + a
    
    // 匿名变量 _
    y, _ := 1, 2
    
    // Go 易于实现两数
    c, d = d, c
    
    /* ------------ 常量 ------------ */
    const l string = &quot;constant&quot;
    const m = 10
    const n = 2e3 / m
    
    // &quot;global&quot; &quot;掘了&quot; true 9 1 6 &quot;F&quot; 666 false 1.9 1.9 &quot;掘金掘了&quot; &quot;constant&quot; 10 200 1
    fmt.Println(x, a, b, c, d, e, f, g, h, i, j, k, l, m, math.Abs(n), y)
    
    // 0 1 2 &quot;test&quot; &quot;test&quot; 9 9
    fmt.Println(A, B, C, D, E, F, G)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.2 数据类型 &amp;#x26; 占位符&lt;/h4&gt;
&lt;p&gt;🌗Go 语言基本数据类型较多，主要有以下这些：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;byte&lt;/code&gt;、&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;int8&lt;/code&gt;、&lt;code&gt;int16&lt;/code&gt;、&lt;code&gt;int32&lt;/code&gt;、&lt;code&gt;int64&lt;/code&gt;、&lt;code&gt;uint&lt;/code&gt;...&lt;/li&gt;
&lt;li&gt;&lt;code&gt;float32&lt;/code&gt;、&lt;code&gt;float64&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;string&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rune&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202249262.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;🌓Go 语言囊括的复合数据类型范围较广 (下文慢慢介绍) ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组 &lt;code&gt;array&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;切片 &lt;code&gt;slice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;字典 &lt;code&gt;map&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;函数 &lt;code&gt;func&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;结构体 &lt;code&gt;struct&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通道 &lt;code&gt;channel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;接口 &lt;code&gt;interface&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;指针 &lt;code&gt;*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🚀根据数据特点又可分为&lt;strong&gt;值传递&lt;/strong&gt; &amp;#x26; &lt;strong&gt;引用传递&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;值传递：&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;float&lt;/code&gt;、&lt;code&gt;string&lt;/code&gt;、&lt;code&gt;bool&lt;/code&gt;、&lt;code&gt;array&lt;/code&gt;、&lt;code&gt;struct&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;引用传递：&lt;code&gt;slice&lt;/code&gt;、&lt;code&gt;map&lt;/code&gt;、&lt;code&gt;chan&lt;/code&gt;、&lt;code&gt;*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⭐至于&lt;strong&gt;运算符&lt;/strong&gt;（算法运算符、关系运算符、逻辑运算符、位运算符、赋值运算符）和其他语言基本一致，这里不再赘叙。&lt;/p&gt;
&lt;h4&gt;2.2.3 &lt;code&gt;if-else&lt;/code&gt; 条件判断&lt;/h4&gt;
&lt;p&gt;Go 语言中的 &lt;code&gt;if&lt;/code&gt; 判断语句不需要 &lt;code&gt;()&lt;/code&gt;；同理，&lt;code&gt;switch&lt;/code&gt; 和 &lt;code&gt;for&lt;/code&gt; 也不需要。&lt;/p&gt;
&lt;p&gt;Go 语言中的 &lt;code&gt;if&lt;/code&gt; 语句存在一种特殊的写法：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;err&lt;/code&gt; 是 &lt;code&gt;myFunc()&lt;/code&gt; 的返回值，执行后再对 &lt;code&gt;err==nil&lt;/code&gt; 语句进行判断。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202250628.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    // Golang 的 if、for、switch 均无需 ()
    if 5%2 == 0 {
        fmt.Println(&quot;5 is even&quot;)
    } else {
        fmt.Println(&quot;5 is odd&quot;)
    }
    
    // 特殊的 if 分支: if 执行语句; 判断语句 { }
    if num := 9; num &amp;#x3C; 0 {
        fmt.Println(num, &quot;is negative&quot;)
    } else if num &amp;#x3C; 10 {
        fmt.Println(num, &quot;has 1 digit&quot;)
    } else {
        fmt.Println(num, &quot;has multiple digits&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.4 &lt;code&gt;for&lt;/code&gt; 循环&lt;/h4&gt;
&lt;p&gt;Go 语言中没有 &lt;code&gt;while&lt;/code&gt;、&lt;code&gt;do while&lt;/code&gt; 这类循环，仅仅只有 &lt;code&gt;for&lt;/code&gt; 这一种循环。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    // while
    i := 1
    for i &amp;#x3C;= 3 {
        fmt.Println(i)
        i = i + 1
    }
    
    for j := 7; j &amp;#x3C; 9; j++ {
        fmt.Println(j)
    }
    
    // 死循环
    for {
        fmt.Println(&quot;endless loop&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.5 &lt;code&gt;switch&lt;/code&gt; 分支&lt;/h4&gt;
&lt;p&gt;相比 C/C++，Go 语言的 &lt;code&gt;switch&lt;/code&gt; 分支&lt;strong&gt;略有不同&lt;/strong&gt;，也更加&lt;strong&gt;强大&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不同之处：&lt;code&gt;case&lt;/code&gt; 中不需要再显式加 &lt;code&gt;break&lt;/code&gt;，执行完对应 &lt;code&gt;case&lt;/code&gt; 就会退出，不像 C/C++ 没加 &lt;code&gt;break&lt;/code&gt; 会跑完余下所有 &lt;code&gt;case&lt;/code&gt;；同时 &lt;code&gt;switch&lt;/code&gt; 后也不再需要 &lt;code&gt;()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;强大之处：Go 语言中 &lt;code&gt;switch&lt;/code&gt; 后能跟任意变量类型；也可不跟任何变量，然后在 &lt;code&gt;case&lt;/code&gt; 中写条件分支，这样就将 &lt;code&gt;switch-case&lt;/code&gt; 分支结构简化为 &lt;code&gt;if-else&lt;/code&gt; 条件判断结构了。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
)

func main() {
    
    // switch-case 标准结构
    a := 2
    switch a {
    case 1:
        fmt.Println(&quot;one&quot;)
    case 2:
        fmt.Println(&quot;two&quot;)	// 控制台只输出 &quot;two&quot;
    case 3:
        fmt.Println(&quot;three&quot;)
    case 4, 5:
        fmt.Println(&quot;four or five&quot;)
    default:
        fmt.Println(&quot;other&quot;)
    }
    
    // switch 后不跟变量
    t := time.Now()
    switch {
    case t.Hour() &amp;#x3C; 12:
        fmt.Println(&quot;It&apos;s before noon&quot;)
    default:
        fmt.Println(&quot;It&apos;s after noon&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.6 数组 array&lt;/h4&gt;
&lt;p&gt;数组是一个长度固定的元素序列，可利用索引取值/存值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    // 一维数组
    var a [5]int
    a[4] = 100
    fmt.Println(&quot;get:&quot;, a[2])   // get: 0
    fmt.Println(&quot;len:&quot;, len(a)) // len: 5
    
    // 3 种数组初始化方式 (1)
    b := [3]int{1, 2, 3} // 或 var b = [3]int{1, 2, 3}
    fmt.Println(b)       // [1 2 3]
    
    // 3 种数组初始化方式 (2)
    c := [...]int{1, 3, 2} // 或 var c = [...]int{1, 2, 3}
    fmt.Println(c)         // [1 3 2]
    
    // 3 种数组初始化方式 (3)
    d := [...]int{1: 3, 6: 5} // 或 var d = [...]int{1: 3, 6: 5}
    fmt.Println(d)            // [0 3 0 0 0 0 5]
    
    // 二维数组
    var twoD [2][3]int
    for i := 0; i &amp;#x3C; 2; i++ {
        for j := 0; j &amp;#x3C; 3; j++ {
            twoD[i][j] = i + j
        }
    }
    fmt.Println(&quot;2d: &quot;, twoD) // [[0 1 2] [1 2 3]]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🚫不过在真实的业务场景中，我们很少直接使用定长的数组，更多使用的是切片。&lt;/p&gt;
&lt;h4&gt;2.2.7 切片 slice&lt;/h4&gt;
&lt;p&gt;数组是定长的，所以 Go 推出可扩容的切片。与数组不同的是，切片不需要指定 &lt;code&gt;[]&lt;/code&gt; 里的长度。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 1.声明空切片
var slice1 []string
// 2.创建一个带默认长度的切片
var slice2 = make([]int, 3)
// 3.声明并初始化切片
slice3 := []string{&quot;g&quot;, &quot;o&quot;, &quot;o&quot;, &quot;d&quot;}
// 4.最常用切片创建方式
slice4 := make([]int, 3)
// 5.创建带有长度和容量的切片
slice5 := make([]int, 3, 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常情况下，我们会使用 &lt;code&gt;make&lt;/code&gt; 函数来创建一个切片；使用 &lt;code&gt;append&lt;/code&gt; 来追加元素，注意要将其结果赋值给原切片；然后可以像数组一样去取值；还可以通过 &lt;code&gt;[a:b]&lt;/code&gt; 来获取切片中指定范围的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    // 创建切片 —— 长度:3; 容量:5
    s := make([]string, 3, 5)
    s[0] = &quot;a&quot;
    s[1] = &quot;b&quot;
    s[2] = &quot;c&quot;
    fmt.Println(&quot;get:&quot;, s[2])   // c
    fmt.Println(&quot;len:&quot;, len(s)) // 3
    fmt.Println(&quot;cap:&quot;, cap(s)) // 5
    
    // append 追加元素
    s = append(s, &quot;d&quot;)
    s = append(s, &quot;e&quot;, &quot;f&quot;)
    fmt.Println(s) // [a b c d e f]
    
    // copy 拷贝元素
    c := make([]string, len(s))
    copy(c, s)
    fmt.Println(c) // [a b c d e f]
    
    cs := []string{&quot;w&quot;, &quot;w&quot;, &quot;w&quot;, &quot;w&quot;, &quot;w&quot;}
    copy(cs, s[:3])
    fmt.Println(cs) // [a b c w w]
    
    // slice 切片取值操作也可以像 python 中的一样取出指定范围的元素, 只不过不可以是负数索引
    fmt.Println(s[2:5]) // [c d e]
    fmt.Println(s[:5])  // [a b c d e]
    fmt.Println(s[2:])  // [c d e f]
    
    // 声明切片并初始化
    good := []string{&quot;g&quot;, &quot;o&quot;, &quot;o&quot;, &quot;d&quot;}
    fmt.Println(good) // [g o o d]
    
    // 将切片 c 完全添加到切片 good 中!
    good = append(good, c...)
    fmt.Println(good) // [g o o d a b c d e f]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🤔&lt;strong&gt;切片元素的删除&lt;/strong&gt;：Go 语言中并没有提供一个内置函数将切片中的元素进行删除，但我们可以使用 &lt;code&gt;[x,y]&lt;/code&gt; 或者 &lt;code&gt;append&lt;/code&gt; 来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 切片元素的删除
slice := []int{1, 2, 3, 4, 5}                 // 原始切片: [1 2 3 4 5]
slice = slice[1:]                             // 删除索引为0的元素: [2 3 4 5]
idx := 1                                      // 索引值 idx=1
slice = append(slice[:idx], slice[idx+1:]...) // 删除索引为1的元素: [2 4 5]

fmt.Println(slice) // [2 4 5]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⭐小结一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一个切片都引用了一个底层数组。&lt;/li&gt;
&lt;li&gt;切片创建时存储了一个长度和一个容量，还有一个指向数组的指针，当切片添加数据时，如果没有超过容量，直接添加，超出容量自动扩容成倍增长。&lt;/li&gt;
&lt;li&gt;一旦切片扩容，指针会指向一个新的底层数组内存地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2.8 字典 &lt;code&gt;map&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;map&lt;/code&gt; 是 Go 语言中内置的字典类型，存储的是键值对 &lt;code&gt;key:value&lt;/code&gt; 类型的数据，有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;map 是&lt;strong&gt;完全无序&lt;/strong&gt;的，遍历时不会按照字母顺序或插入顺序输出，而是随机的，且只能通过 key 访问对应的 value。&lt;/li&gt;
&lt;li&gt;空的 slice 是可以直接使用的，因为它有底层数组；但空的 map 不能直接使用，需要先 &lt;code&gt;make&lt;/code&gt; 或初始化后才能使用（map 是引用类型，如果声明没有初始化值，默认为 &lt;code&gt;nil&lt;/code&gt;，是不能直接使用的）。&lt;/li&gt;
&lt;li&gt;map 的 key 不能重复，否则新增加的值会覆盖原来 key 对应的 value。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建 &lt;code&gt;map&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 声明空 map: 不可直接使用
var map1 map[int]string
// 创建 map (已初始化——零值填充): 可直接使用
map2 := make(map[int]string)
// 声明并初始化 map: 可直接使用
map3 := map[string]int{&quot;one&quot;: 1, &quot;two&quot;: 2}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;map&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    // make 创建 map
    m := make(map[string]int)
    m[&quot;one&quot;] = 1
    m[&quot;two&quot;] = 2
    fmt.Println(m)           // map[one:1 two:2]
    fmt.Println(len(m))      // 2
    fmt.Println(m[&quot;one&quot;])    // 1
    fmt.Println(m[&quot;unknow&quot;]) // 0
    
    // 如果 map 为空, 不能直接使用, 否则报错 panic: assignment to entry in nil map
    var nilMap map[int]float32
    //nilMap[0] = 1.0     // panic: assignment to entry in nil map
    if nilMap == nil {
        nilMap = make(map[int]float32)
    }
    nilMap[0] = 1.0
    fmt.Println(nilMap) // map[0:1]
    
    // 判断 key 是否存在, value,ok := map[key]
    r, ok := m[&quot;unknow&quot;]
    fmt.Println(r, ok) // 0 false
    t, ok := m[&quot;two&quot;]
    fmt.Println(t, ok) // 2 true
    
    // map 中使用 delete 删除 key 对应的键值对
    delete(m, &quot;one&quot;)
    delete(m, &quot;two&quot;)
    fmt.Println(m) // map[]
    
    // 不使用 make 函数, 直接创建并初始化 map
    m2 := map[string]int{&quot;one&quot;: 1, &quot;two&quot;: 2}
    var m3 = map[string]int{&quot;one&quot;: 1, &quot;two&quot;: 2}
    fmt.Println(m2, m3) // map[one:1 two:2] map[one:1 two:2]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.9 &lt;code&gt;range&lt;/code&gt; 遍历&lt;/h4&gt;
&lt;p&gt;介绍下 &lt;code&gt;range&lt;/code&gt; 关键字：对于 slice 或者 map，我们可以使用 range 对其进行快速遍历。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;slice&lt;/strong&gt;：第一个是索引，第二个是值&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;map&lt;/strong&gt;：第一个是键，第二个是值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果不需要索引/键，可以直接使用 &lt;code&gt;_&lt;/code&gt; 匿名变量代替。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;fmt&quot;
    &quot;strconv&quot;
)

func main() {
    // slice —— range
    nums := []int{2, 3, 4}
    sum := 0
    for i, num := range nums {
        sum += num
        if num == 2 {
            fmt.Println(&quot;index:&quot;, i, &quot;num:&quot;, num) // index: 0 num: 2
        }
    }
    fmt.Println(sum) // 9
    // 如若不需要索引, 可使用 _ 匿名变量
    for _, num := range nums {
        fmt.Println(num)
    }
    
    // map —— range
    maps := make(map[int]string)
    maps[0] = &quot;hello&quot;
    maps[1] = &quot;world&quot;
    for key, value := range maps {
        // int 转 string(1): fmt.Sprintf(&quot;%d&quot;, intVal)
        fmt.Println(fmt.Sprintf(&quot;%d&quot;, key) + &quot;:&quot; + value)
        // int 转 string(2): strconv.Itoa(intVal)
        fmt.Println(strconv.Itoa(key) + &quot;:&quot; + value)
        // int 转 string(3): , 分隔
        fmt.Println(key, &quot;:&quot;, value)
    }
    // 如若不需要键, 可使用 _ 匿名变量
    for _, v := range maps {
        fmt.Println(&quot;v&quot;, v)
    }
    // 如若不需要值, 可使用 _ 匿名变量, 也可以直接省略
    for k := range maps {
        fmt.Println(&quot;key&quot;, k)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.10 函数 &lt;code&gt;func&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;💖Go 语言中的函数比较特殊，参数类型、返回值类型都是&lt;strong&gt;后置&lt;/strong&gt;的，而且函数首字母大写/小写的作用不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果&lt;strong&gt;函数名首字母大写&lt;/strong&gt;则表示&lt;strong&gt;公共函数&lt;/strong&gt;，其他包能够调用，前提得引入当前包。&lt;/li&gt;
&lt;li&gt;如果&lt;strong&gt;函数名首字母小写&lt;/strong&gt;则表示&lt;strong&gt;私有函数&lt;/strong&gt;，仅能够在本包中调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Go 语言的函数 &lt;code&gt;func&lt;/code&gt; 结构&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202250949.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;💖Golang 中的&lt;strong&gt;函数原生支持返回多个值&lt;/strong&gt;，且在实际业务场景中都返回两个值，第一个是真正的返回结果，第二个是错误信息。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;多返回值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202250693.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来看看函数的基本定义及其用法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func add(a int, b int) (int, string) {
    return a + b, &quot;ok&quot;
}

func add2(a, b int) int {
    return a + b
}

func exists(m map[string]string, k string) (v string, ok bool) {
    v, ok = m[k]
    return v, ok
}

func main() {
    res, _ := add(1, 2)
    fmt.Println(res) // 3
    
    v, ok := exists(map[string]string{&quot;a&quot;: &quot;A&quot;}, &quot;a&quot;)
    fmt.Println(v, ok) // A True
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;💖Go 语言中的函数也是一种数据结构，也可以被存储在一个变量中，调用变量的时候也就相当于调用函数——也就是可以&lt;strong&gt;将函数作为传递参数&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func add(a int, b int) (int, string) {
    return a + b, &quot;ok&quot;
}

func main() {
    // 函数作为传递参数
    f := add
    ret, _ := f(2, 3)
    fmt.Println(ret) // 5
    
    // 定义函数变量
    var addingFunc func(int, int) (int, string)
    addingFunc = add
    result, str := addingFunc(1, 5)
    fmt.Println(result, str) // 6 ok
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;💖Go 中定义匿名函数加上 &lt;code&gt;()&lt;/code&gt; 相当于直接调用；如果没有 &lt;code&gt;()&lt;/code&gt; 则表示定义一个函数，可将其赋值给变量然后进行多次调用——&lt;strong&gt;匿名函数&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;fmt&quot;
    &quot;strconv&quot;
)

func main() {
    // 匿名函数的简单定义与调用
    func() {
        fmt.Println(&quot;匿名函数&quot;)
    }() // 匿名函数
    
    // 匿名函数的使用
    sum := func(a int, b int) int {
        return a + b
    }(1, 2)
    fmt.Println(sum) // 3
    
    // 定义匿名函数并赋值给其他变量, 此处并没有调用匿名函数, 因为没有()
    myFunc := func(a, b int) string {
        return strconv.Itoa(a) + fmt.Sprintf(&quot;%d&quot;, b)
    }
    // 调用定义的匿名函数
    returnVal := myFunc(6, 66)
    fmt.Println(returnVal) // 666
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;💖甚至你可以将匿名函数作为另一个函数的参数/返回值；其中作为参数的函数叫做&lt;strong&gt;回调函数&lt;/strong&gt;，调用的函数叫做&lt;strong&gt;高阶函数&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

// 匿名函数作为返回值
func returnFunc(a int, b int) func() int {
    return func() int {
        return a + b
    }
}

func increase(a int, b int) int {
    return a + b
}

func reduce(a int, b int) int {
    return a - b
}

// opera: 高阶函数
// f: 回调函数
func opera(a int, b int, f func(int, int) int) int {
    res := f(a, b)
    return res
}

func main() {
    // 将匿名函数作为另一函数的参数
    num1 := opera(1, 2, increase) // 3
    num2 := opera(1, 2, reduce)   // -1
    fmt.Println(num1, num2)
    
    // 定义匿名函数作为函数参数
    num3 := opera(3, 4, func(a int, b int) int {
        return a * b
    })
    fmt.Println(num3) // 12
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;💖既然 Go 语言中的函数能作为返回值和参数，自然能打造&lt;strong&gt;闭包结构&lt;/strong&gt;，与 JS 闭包含义相同。&lt;/p&gt;
&lt;p&gt;所谓闭包，就是一个外层函数中有内层函数，这个内层函数会操作外层函数的局部变量，并且，外层函数把内层函数作为返回值，将内层函数与外层函数中的局部变量统称为闭包结构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202251102.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202251525.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;💖先捋清下为什么我们需要闭包结构，闭包有什么作用？&lt;/p&gt;
&lt;p&gt;首先看一个简单的计数器例子！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

var counter = 0

func add() int {
    counter++
    return counter
}

func main() {
    add()
    add()
    add()
    fmt.Println(counter) // 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然我们已经达到了目的，但是任意一个函数中都可以随意改动 &lt;code&gt;counter&lt;/code&gt; 的值，所以该计数器并不完美，那我们将 &lt;code&gt;counter&lt;/code&gt; 放到函数中如何？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func add() int {
    counter := 0
    counter++
    return counter
}

func main() {
    add()
    add()
    ret := add()
    fmt.Println(ret) // 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本意想输出 3，但由于局部变量在函数每次调用时都会被初始化为 0，所以达不到预期效果。所以我们此时就需要使用&lt;strong&gt;闭包&lt;/strong&gt;来解决了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func add() func() int {
    counter := 0
    innerFunc := func() int {
        counter++
        return counter
    }
    return innerFunc
}

func main() {
    inner := add()
    inner()
    inner()
    ret := inner()
    fmt.Println(ret) // 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;精简下闭包代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    // 以上闭包的简写形式
    add := func() func() int {
        counter := 0
        return func() int {
            counter++
            return counter
        }
    }()
    
    add()
    add()
    ret := add()
    
    fmt.Println(ret) // 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;💖现在估计你能很轻松的理解以下代码了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：由于闭包会携带包含它的函数的作用域，因此会比其他函数占用更多的内存。因此可以手动解除对内层匿名函数的引用，以便释放内存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    res := closure()
    // 执行 closure 函数返回的内层函数
    r1 := res()
    r2 := res()
    
    fmt.Println(res) // 返回内层函数函数体地址: 0x46c7e0
    fmt.Println(r1)  // 1
    fmt.Println(r2)  // 2
    
    // 手动解除对内层函数的引用, 以便释放内存
    res = nil
}

// 定义一个闭包结构的函数, 返回一个匿名函数
func closure() func() int { //外层函数
    // 定义外层函数的局部变量a
    a := 0
    // 定义内层函数并返回
    return func() int {
        // 内层函数用到了外层函数的局部变量, 此变量不会随着外层函数的结束而销毁
        a++
        return a
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;💖&lt;code&gt;defer&lt;/code&gt; 函数是 Go 语言中另一奇特的存在：当 defer 函数调用后，代码暂不执行，推迟到主函数 &lt;code&gt;main&lt;/code&gt; 执行结束后才会执行；一般用于资源的关闭。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    
    defer func() {
        fmt.Println(&quot;Close Resource&quot;)
    }()
    fmt.Println(&quot;defer...&quot;)
    
    // 输出结果:
    // defer...
    // Close Resource
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.11 指针&lt;/h4&gt;
&lt;p&gt;Golang 也支持指针，用法同 C/C++，只不过支持的操作比较有限。&lt;/p&gt;
&lt;p&gt;Go 语言中通过 &lt;code&gt;&amp;#x26;&lt;/code&gt; 获取变量的地址，通过 &lt;code&gt;* &lt;/code&gt; 获取指针所对应的变量存储的数值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func add2(n int) {
   n += 2
}

func add2ptr(n *int) {
   *n += 2
}

func main() {
   n := 5
   add2(n)
   fmt.Println(n) // 5
   add2ptr(&amp;#x26;n)
   fmt.Println(n) // 7
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🚀接着简单介绍下：「&lt;strong&gt;数组指针&lt;/strong&gt;」、「&lt;strong&gt;指针数组&lt;/strong&gt;」、「&lt;strong&gt;指针函数&lt;/strong&gt;」、「&lt;strong&gt;指针参数&lt;/strong&gt;」&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;区分&lt;strong&gt;数组指针&lt;/strong&gt;和&lt;strong&gt;指针数组&lt;/strong&gt;的定义，只需看清变量名更靠近 &lt;code&gt;[]&lt;/code&gt;（指针数组） 还是 &lt;code&gt;*&lt;/code&gt;（数组指针）。&lt;/p&gt;
&lt;p&gt;至于要区分这二者的使用方式只需要记得 &lt;code&gt;[]&lt;/code&gt; 优先级高于 &lt;code&gt;*&lt;/code&gt; 即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;（1）数组指针：指向数组的指针&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202251983.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    // 数组指针
    arr := [3]int{1, 2, 3}
    var ars *[3]int
    ars = &amp;#x26;arr
    (*ars)[0] = 0
    ars[1] = 1
    fmt.Println(ars)       // &amp;#x26;[0 1 3]
    fmt.Println(*ars)      // [0 1 3]
    fmt.Println((*ars)[1]) // 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（2）指针数组：数组元素皆为指针&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202251306.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 指针数组
a, b, c := 1, 2, 3
nums := [3]int{a, b, c}
numps := [3]*int{&amp;#x26;a, &amp;#x26;b, &amp;#x26;c}

*numps[1] = 1
*numps[2] = 6
fmt.Println(nums)      // [1 2 3]
fmt.Println(numps)     // [0xc000018128 0xc000018130 0xc000018138]
fmt.Println(*numps[0]) // 1
fmt.Println(*numps[2]) // 6
for _, v := range numps {
    fmt.Print(*v, &quot; &quot;) // 1 1 6
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（3）指针函数：如果一个函数返回结果是一个指针，那么这个函数就是一个指针函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    var p = pfunc()
    fmt.Println((*p)[1]) // 2
}

// 指针函数: 此处返回切片指针(用法同数组指针)
func pfunc() *[]int {
    arr := []int{1, 2, 3}
    return &amp;#x26;arr
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（4）指针参数：指针作为函数的形参&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    s := 19
    argpfunc(&amp;#x26;s)
    fmt.Println(s) // 6
}

// 指针参数
func argpfunc(p *int) {
    *p = 6
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.12 结构体 &amp;#x26; 结构体方法&lt;/h4&gt;
&lt;p&gt;Go 语言中不存在 Class 类的概念，但是可以通过结构体 &lt;code&gt;struct&lt;/code&gt; 来实现。同时在结构体中也支持指针，避免对大结构体拷贝的开销。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202251505.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

type user struct {
    name     string
    password string
}

func main() {
    a := user{name: &quot;wang&quot;, password: &quot;1024&quot;}
    b := user{&quot;wang&quot;, &quot;1024&quot;}
    c := user{name: &quot;wang&quot;}
    c.password = &quot;1024&quot;
    var d user
    d.name = &quot;wang&quot;
    d.password = &quot;1024&quot;
    e := new(user)
    e.name = &quot;wang&quot;
    e.password = &quot;1024&quot;
    
    fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
    fmt.Println(e)          // &amp;#x26;{wang 1024}
    
    fmt.Println(checkPassword(a, &quot;996&quot;)) // false
    changePassword(&amp;#x26;a, &quot;996&quot;)            // 通过指针修改结构体中的数据
    fmt.Println(a)                       // {wang 996}
    
    // 匿名结构体 &amp;#x26; 嵌套结构体
    p := struct {
        age int
        sex string
        u   user
    }{
        age: 21,
        sex: &quot;Male&quot;,
        u:   user{&quot;w&quot;, &quot;1024&quot;},
    }
    fmt.Println(p) // {21 Male {w 1024}}
}

func checkPassword(u user, password string) bool {
    return u.password == password
}

func changePassword(u *user, password string) {
    u.password = password
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Golang 中还可以为结构体去定义方法，类似其他语言的类成员函数，这样就可以使用 &lt;code&gt;对象.方法&lt;/code&gt; 去调用结构体方法了；结构体方法又分为&lt;strong&gt;带指针&lt;/strong&gt;和&lt;strong&gt;不带指针&lt;/strong&gt;两种，不带指针的是一种拷贝。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

// 结构体
type user struct {
    name     string
    password string
}

// 结构体方法
func (u user) checkingPassword(password string) bool {
    return u.password == password
}

// u user: 拷贝传入的结构体, 不修改原有结构体
// u *user: 可以修改传入的结构体
func (u *user) changingPassword(password string) {
    u.password = password
}

func main() {
    u := user{
        name:     &quot;w&quot;,
        password: &quot;1024&quot;,
    }
    // 结构体方法的调用
    isEqual := u.checkingPassword(&quot;1024&quot;)
    u.changingPassword(&quot;2022&quot;)
    
    fmt.Println(isEqual) // true
    fmt.Println(u)       // {w 2022}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.13 字符串操作&lt;/h4&gt;
&lt;p&gt;Go 语言中的 &lt;code&gt;strings&lt;/code&gt; 标准库含有很多操作字符串的工具函数，&lt;code&gt;strings&lt;/code&gt; 主要针对 &lt;code&gt;utf-8&lt;/code&gt; 编码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;fmt&quot;
    &quot;strings&quot;
)

func main() {
    a := &quot;hello&quot;
    b := &quot;你好&quot;
    fmt.Println(strings.Contains(a, &quot;he&quot;))         // true
    fmt.Println(strings.Index(a, &quot;l&quot;))             // 2
    fmt.Println(strings.Count(a, &quot;l&quot;))             // 2
    fmt.Println(strings.HasPrefix(a, &quot;he&quot;))        // true
    fmt.Println(strings.HasSuffix(a, &quot;lo&quot;))        // true
    fmt.Println(strings.Join([]string{a, b}, &quot;-&quot;)) // hello-你好
    fmt.Println(strings.Repeat(a, 2))              // hellohello
    fmt.Println(strings.Replace(a, &quot;l&quot;, &quot;i&quot;, 1))   // heilo
    fmt.Println(strings.Split(&quot;a-b-c&quot;, &quot;-&quot;))       // [a b c]
    fmt.Println(strings.ToUpper(a))                // HELLO
    fmt.Println(strings.ToLower(a))                // hello
    fmt.Println(len(a))                            // 5
    fmt.Println(len(b))                            // 6
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.14 字符串格式化&lt;/h4&gt;
&lt;p&gt;😎Go 可以使用 &lt;code&gt;fmt.Printf()&lt;/code&gt; 或者 &lt;code&gt;fmt.Sprintf()&lt;/code&gt; 格式化字符串（后者能将格式化后的字符串赋值给新字符串）！&lt;/p&gt;
&lt;p&gt;😎Go 语言中额外提供了占位符 &lt;code&gt;%v&lt;/code&gt; 来打印任意类型的变量，你还可以用 &lt;code&gt;%+v&lt;/code&gt;、&lt;code&gt;%#v&lt;/code&gt; 打印更加详细的信息 ...&lt;/p&gt;
&lt;p&gt;🔎&lt;strong&gt;字符串格式化符号|一览表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;| 占位符 | 说明                                           |
| ------ | ---------------------------------------------- |
| &lt;code&gt;%d&lt;/code&gt;   | 十进制的数字                                   |
| &lt;code&gt;%T&lt;/code&gt;   | 取类型                                         |
| &lt;code&gt;%s&lt;/code&gt;   | 取字符串                                       |
| &lt;code&gt;%t&lt;/code&gt;   | 取 bool 类型的值                               |
| &lt;code&gt;%p&lt;/code&gt;   | 取内存地址                                     |
| &lt;code&gt;%b&lt;/code&gt;   | 整数以二进制显示                               |
| &lt;code&gt;%o&lt;/code&gt;   | 整数以八进制显示                               |
| &lt;code&gt;%x&lt;/code&gt;   | 整数以十六进制显示                             |
| &lt;code&gt;%v&lt;/code&gt;   | 任意类型变量                                   |
| &lt;code&gt;%+v&lt;/code&gt;  | 在 &lt;code&gt;%v&lt;/code&gt; 基础上，对&lt;strong&gt;结构体&lt;/strong&gt;字段名和值进行展开 |
| &lt;code&gt;%#v&lt;/code&gt;  | 输出 Go 语言语法格式的值                       |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
)

type point struct {
    x, y int
}

func main() {
    s := &quot;hello&quot;
    n := 123
    f := 3.141592653
    b := true
    p := point{1, 2}
    
    // fmt.Printf()
    fmt.Printf(&quot;%s\n&quot;, s)   // hello
    fmt.Printf(&quot;%d\n&quot;, n)   // 123
    fmt.Printf(&quot;%b\n&quot;, n)   // 1111011
    fmt.Printf(&quot;%f\n&quot;, f)   // 3.141592653
    fmt.Printf(&quot;%.3f\n&quot;, f) // 3.142
    fmt.Printf(&quot;%t\n&quot;, b)   // true
    
    fmt.Printf(&quot;%T\n&quot;, f) // float64
    fmt.Printf(&quot;%T\n&quot;, p) // main.point
    
    fmt.Printf(&quot;s=%v\n&quot;, s)  // s=hello
    fmt.Printf(&quot;n=%v\n&quot;, n)  // n=123
    fmt.Printf(&quot;p=%v\n&quot;, p)  // p={1 2}
    fmt.Printf(&quot;p=%+v\n&quot;, p) // p={x:1 y:2}
    fmt.Printf(&quot;p=%#v\n&quot;, p) // p=main.point{x:1, y:2}
    
    // fmt.Sprintf()
    motto := fmt.Sprintf(&quot;Today is %s, and I&apos;m working under the %d system...\n&quot;, time.Now(), 965)
    fmt.Printf(motto)
    
    // 利用 fmt.Sprintf() 方法返回 string 的特性, 可以将 int 转为 string
    intVal := 1024
    var strVal string = fmt.Sprintf(&quot;%d&quot;, intVal) // int 转 string
    fmt.Printf(&quot;%T&quot;, strVal)                      // string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.15 接口&lt;/h4&gt;
&lt;p&gt;Golang 接口内可以定义多个方法，谁将这些方法实现，就可以认为是实现了该接口（这是一种约束，不像 Java 还需要 &lt;code&gt;implements&lt;/code&gt; 来显式实现），这样规范了方法。在调用的时候使用不同的结构体对象，可以实现执行不同的方法。这样就实现了 Go 语言中的多态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202252091.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

type action interface {
    // run: run in speed
    run(int)
    // get: get name
    get() string
}

type person struct {
    name  string
    speed int
}

type animal struct {
    types    string
    velocity int
}

func (per *person) run(speed int) {
    fmt.Println(&quot;Run in&quot;, speed, &quot;m/s&quot;)
}

func (per *person) get() string {
    return per.name
}

func (ani *animal) run(velocity int) {
    fmt.Println(&quot;Run in&quot;, velocity, &quot;m/s&quot;)
}

func (ani *animal) get() string {
    return ani.types
}

func main() {
    per := person{name: &quot;w&quot;, speed: 1}
    ani := animal{types: &quot;tiger&quot;, velocity: 6}
    
    var act action
    act = &amp;#x26;per
    act.run(per.speed) // Run in 1 m/s
    name := act.get()  // w
    
    act = &amp;#x26;ani
    act.run(ani.velocity) // Run in 6 m/s
    types := act.get()    // tiger
    
    fmt.Println(name, types)    // w tiger
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🌓Golang 还存在空接口 &lt;code&gt;interface{}&lt;/code&gt;，这种类型可以理解为任意类型，类似 Java 中的 &lt;code&gt;Object&lt;/code&gt; 类。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

// T 空接口的定义, 也可以直接使用 interface{}
type T interface {
}

func test1(t T) {
    fmt.Println(t)
}

// 简化: T=interface{}
func test2(t interface{}) {
    fmt.Println(t)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⭐既然空接口可以传递任意类型，我们就可以利用这个特性把空接口 &lt;code&gt;interface{}&lt;/code&gt; 当作容器使用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// interface{} 作为 map 的 value
maps := make(map[int]interface{})
maps[1] = 1
maps[3] = &quot;369&quot;
maps[6] = true
maps[9] = 9.9

fmt.Println(maps) // map[1:1 3:369 6:true 9:9.9]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

// Dictionary 封装map
type Dictionary struct {
    data map[string]interface{}
}

func NewDictionary() *Dictionary {
    return &amp;#x26;Dictionary{
        data: make(map[string]interface{}),
    }
}

func (dict *Dictionary) Set(key string, value interface{}) {
    dict.data[key] = value
}

func (dict *Dictionary) Get(key string) interface{} {
    return dict.data[key]
}

func main() {
    // Dictionary
    dict := NewDictionary()
    dict.Set(&quot;a&quot;, &quot;abandon&quot;)
    dict.Set(&quot;b&quot;, 2)
    dict.Set(&quot;c&quot;, false)
    
    fmt.Println(dict.Get(&quot;a&quot;))  // abandon
    fmt.Println(dict.Get(&quot;d&quot;))  // &amp;#x3C;nil&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;💖更多关于空接口的解释：&lt;a href=&quot;https://flaviocopes.com/go-empty-interface/&quot;&gt;The Go Empty Interface Explained&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;2.2.16 错误处理&lt;/h4&gt;
&lt;p&gt;💧错误和异常不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;错误是在程序中正常存在的，可以预知的失败在意料之中。&lt;/li&gt;
&lt;li&gt;异常通常指在不应该出现问题的地方出现问题，比如空指针，这在人们的意料之外。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 Golang 中，错误处理通常被单独作为一个返回值以传递错误信息。不同于 Java，Go 语言能很清晰的知道是哪个函数返回了错误，并且可以使用简单的 &lt;code&gt;if-else&lt;/code&gt; 语句加以处理。&lt;/p&gt;
&lt;p&gt;🔎&lt;code&gt;error&lt;/code&gt; 的定义是一个接口，接口内部包含一个返回字符串类型的方法 &lt;code&gt;Error()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type error interface {
    Error() string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;清楚 &lt;code&gt;error&lt;/code&gt; 的定义是一个接口类型后，那么只要实现了这个接口都可以用来处理错误信息，来返回一个错误提示给用户。&lt;/p&gt;
&lt;p&gt;Go 语言也提供了一个内置包 &lt;code&gt;errors&lt;/code&gt;，使用 &lt;code&gt;errors.New(&quot;&quot;)&lt;/code&gt; 来创建一个错误对象，以下为 &lt;code&gt;errors&lt;/code&gt; 内置包中 &lt;code&gt;errors.go&lt;/code&gt; 的定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
   return &amp;#x26;errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
   s string
}

func (e *errorString) Error() string {
   return e.s
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⭐通常的做法是：当出错时，返回一个 &lt;code&gt;nil&lt;/code&gt; 和一个 &lt;code&gt;error&lt;/code&gt;；否则直接返回原有值和 &lt;code&gt;nil&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;errors&quot;
    &quot;fmt&quot;
)

type user struct {
    name     string
    password string
}

func findUser(users []user, name string) (v *user, err error) {
    for _, u := range users {
        if u.name == name {
            return &amp;#x26;u, nil
        }
    }
    return nil, errors.New(&quot;not found&quot;)
}

func main() {
    users := []user{{&quot;w&quot;, &quot;1024&quot;}, {&quot;q&quot;, &quot;996&quot;}}
    
    u, e := findUser(users, &quot;w&quot;)
    if e != nil {
        fmt.Println(e)
        return
    }
    fmt.Println(*u) // {w 1024}
    
    if us, err := findUser(users, &quot;r&quot;); err != nil {
        fmt.Println(err) // not found
        return
    } else {
        fmt.Println(*us)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.17 时间处理&lt;/h4&gt;
&lt;p&gt;关于时间处理，最常用的莫过于 &lt;code&gt;time.now()&lt;/code&gt; 获取当前时间。以下还有一些 &lt;code&gt;time&lt;/code&gt; 内置包的常见用法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
)

func main() {
    nowTime := time.Now()
    fmt.Println(nowTime) // 2022-05-09 13:02:24.4523211 +0800 CST m=+0.007505401
    
    // Date(): 获取年月日
    year, month, day := time.Now().Date()
    fmt.Println(year, month, day) // 2022 May 09
    // Clock(): 获取时分秒
    hour, minute, second := time.Now().Clock()
    fmt.Println(hour, minute, second) // 13 1 1
    
    // 格式化时间
    formatTime1 := nowTime.Format(&quot;2006/01/02 15:04:05&quot;)
    fmt.Println(formatTime1) // 2022/05/09 11:40:29
    formatTime2 := nowTime.Format(&quot;2006年01月02日 15时04分05秒&quot;)
    fmt.Println(formatTime2) // 2022年05月09日 11时41分25秒
    
    // 构造带时区的时间
    created := time.Date(2022, 5, 9, 11, 12, 13, 0, time.UTC)
    fmt.Println(created) // 2022-05-09 11:12:13 +0000 UTC
    fmt.Println(created.Year(), created.Month(), created.Day(),
        created.Hour(), created.Minute(), created.Second()) // 2022 May 9 11 12 13
    
    // Add(): 对某个时间点进行增加时间间隔的操作
    // Sub(): 可以对两个时间点进行减法然后获取时间段
    another := created.Add(time.Hour + time.Minute*3)
    diff := another.Sub(created)
    fmt.Println(diff)                         // 1h3m0s
    fmt.Println(diff.Hours(), diff.Minutes()) // 1.05 63
    
    // 时间戳
    fmt.Println(nowTime.Unix()) // 1652417743
    
    // 时间解析
    t, err := time.Parse(&quot;2006-01-02 15:04:05&quot;, &quot;2022-05-09 11:12:13&quot;)
    if err != nil {
        panic(err)
    }
    fmt.Println(t == created) // true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;😮更多：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://polarisxu.studygolang.com/posts/go/why-time-use-2006/&quot;&gt;Go 的时间格式化为什么是 2006-01-02 15:04:05？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jianshu.com/notifications&quot;&gt;Golang 神奇的 2006-01-02 15:04:05&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2.18 JSON 处理&lt;/h4&gt;
&lt;p&gt;Go 中操作 JSON 非常简单，对于一个结构体我们只需要确保每个字段的首字母为大写（即公开字段），那么这个结构体就能够用 &lt;code&gt;json.Marshal&lt;/code&gt; 去序列化成一个 JSON &lt;code&gt;byte[]&lt;/code&gt;，如果要转化成字符串则通过 &lt;code&gt;string()&lt;/code&gt; 即可；序列化后的字符串也可以用 &lt;code&gt;json.Unmarshal&lt;/code&gt; 去反序列化到一个 &lt;code&gt;struct&lt;/code&gt; 变量中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;encoding/json&quot;
    &quot;fmt&quot;
)

type userInfo struct {
    Name  string
    Age   int `json:&quot;age&quot;` // `json:&quot;age&quot;` 是 json tag, 可以按 tag 名进行输出
    Hobby []string
}

func main() {
    // struct ==&gt; json(string)
    user := userInfo{Name: &quot;w&quot;, Age: 21, Hobby: []string{&quot;Java&quot;, &quot;Golang&quot;, &quot;Python&quot;, &quot;C++&quot;}}
    buf, err := json.Marshal(user) // struct ==&gt; byte[]
    if err != nil {
        panic(err)
    }
    fmt.Println(buf)         // byte[]: [123 34 78 97...]
    fmt.Println(string(buf)) // string: {&quot;Name&quot;:&quot;w&quot;,&quot;age&quot;:21,&quot;Hobby&quot;:[&quot;Java&quot;,&quot;Golang&quot;,&quot;Python&quot;,&quot;C++&quot;]}
    
    // struct ==&gt; json(string): 带有缩进的标准 JSON 格式
    buf, err = json.MarshalIndent(user, &quot;&quot;, &quot;\t&quot;)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(buf))
    /* 输出结果:
       {
               &quot;Name&quot;: &quot;w&quot;,
               &quot;age&quot;: 21,
               &quot;Hobby&quot;: [
                       &quot;Java&quot;,
                       &quot;Golang&quot;,
                       &quot;Python&quot;,
                       &quot;C++&quot;
               ]
       }
    */
    
    // json(string) ==&gt; struct
    var u userInfo
    err = json.Unmarshal(buf, &amp;#x26;u)
    if err != nil {
        panic(err)
    }
    fmt.Printf(&quot;%#v&quot;, u) // main.userInfo{Name:&quot;w&quot;, Age:21, Hobby:[]string{&quot;Java&quot;, &quot;Golang&quot;, &quot;Python&quot;, &quot;C++&quot;}}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.19 数字解析&lt;/h4&gt;
&lt;p&gt;接下来学习下字符串与数字之间的转换，在 Go 语言中，关于字符串和数字类型的转换都在 &lt;code&gt;strconv&lt;/code&gt; 内置包中，这个包名是 string &amp;#x26; convert 两个单词的缩写拼接而成。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;fmt&quot;
    &quot;strconv&quot;
)

func main() {
    f, _ := strconv.ParseFloat(&quot;1.234&quot;, 64)
    fmt.Println(f) // 1.234
    
    n, _ := strconv.ParseInt(&quot;111&quot;, 10, 64)
    fmt.Println(n) // 111
    
    n, _ = strconv.ParseInt(&quot;0x1000&quot;, 0, 64)
    fmt.Println(n) // 4096
    
    // string ==&gt; int
    n2, _ := strconv.Atoi(&quot;123&quot;) // Atoi is equivalent to `ParseInt()`
    fmt.Println(n2)              // 123
    
    // int ==&gt; string
    var str string = strconv.Itoa(123)
    fmt.Println(str) // 123
    
    n2, err := strconv.Atoi(&quot;AAA&quot;)
    fmt.Println(n2, err) // 0 strconv.Atoi: parsing &quot;AAA&quot;: invalid syntax
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.20 进程信息&lt;/h4&gt;
&lt;p&gt;在 Go 中，我们可以使用 &lt;code&gt;os.Args&lt;/code&gt; 来获取程序执行时指定的命令行参数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⭐比如我们编译一个二进制文件，执行 &lt;code&gt;go run example/20-env/main.go a b c d&lt;/code&gt; 命令，其中有 &lt;code&gt;a b c d&lt;/code&gt; 四个命令行参数，但是 &lt;code&gt;os.Args&lt;/code&gt; 会是长度为 5 的 slice，因为第一个成员代表二进制自身的名字。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;fmt&quot;
    &quot;os&quot;
    &quot;os/exec&quot;
)

func main() {
    // go run example/20-env/main.go a b c d
    fmt.Println(os.Args) // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
    slices := os.Args
    fmt.Println(len(slices))       // 5
    fmt.Println(os.Getenv(&quot;PATH&quot;)) // /usr/local/go/bin...
    fmt.Println(os.Setenv(&quot;AA&quot;, &quot;BB&quot;))
    
    buf, err := exec.Command(&quot;grep&quot;, &quot;127.0.0.1&quot;, &quot;/etc/hosts&quot;).CombinedOutput()
    if err != nil {
        panic(err)
    }
    fmt.Println(string(buf)) // 127.0.0.1       localhost
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🔎「Go 入门」中的部分图片引用自：https://juejin.cn/book/6844733833401597966&lt;/p&gt;
&lt;h2&gt;3. Go 实战程序&lt;/h2&gt;
&lt;p&gt;上文已经介绍了 Go 语言的基础语法和一些常用标准库的使用方法，接下来通过 3 个实例真正上手 Golang！&lt;/p&gt;
&lt;h3&gt;3.1 猜谜游戏&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202252758.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;唯一需要注意的是 &lt;strong&gt;Linux/Unix&lt;/strong&gt;、&lt;strong&gt;Windows&lt;/strong&gt;、&lt;strong&gt;Mac OS&lt;/strong&gt; 三个操作系统下&lt;strong&gt;换行符不一致&lt;/strong&gt;问题！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linux/Unix&lt;/strong&gt;：换行符为 &lt;code&gt;\n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mac OS&lt;/strong&gt;：换行符为 &lt;code&gt;\r&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows&lt;/strong&gt;：换行符为&lt;code&gt;\r\n&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;附部分 ASCII 码对照表&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202252971.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;OK，如下就可以实现一个简单的猜谜游戏程序了！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;bufio&quot;
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;os&quot;
    &quot;strconv&quot;
    &quot;strings&quot;
    &quot;time&quot;
)

func main() {
    maxNum := 100
    // 用时间戳初始化随机数种子
    rand.Seed(time.Now().UnixNano())
    // rand.Intn(100): 产生 0 到 100 之间的随机整数
    secretNumber := rand.Intn(maxNum)
    
    fmt.Print(&quot;Please input your guess: &quot;)
    reader := bufio.NewReader(os.Stdin)
    for {
        // 读取一行输入: 读到 \nxxxx\n ==&gt; 需要去掉\n
        input, err := reader.ReadString(&apos;\n&apos;) // 输入结束符: \n
        if err != nil {
            fmt.Println(&quot;An error occured while reading input. Please try again&quot;, err)
            continue
        }
        
        // Windows —— CRLF(回车+换行): \r\n
        // Linux/Unix —— LF(换行): \n
        // Mac OS —— CR(回车): \r
        /* strings.TrimSuffix(): 去掉最后读入的回车换行 &quot;\r\n&quot; */
        input = strings.TrimSuffix(input, &quot;\r\n&quot;)
        
        // Atoi: string ==&gt; int
        // Itoa: int ==&gt; string
        guess, err := strconv.Atoi(input)
        if err != nil {
            fmt.Println(&quot;Invalid input. Please enter an integer value&quot;)
            continue
        }
        fmt.Println(&quot;You guess is&quot;, guess)
        if guess &gt; secretNumber {
            fmt.Println(&quot;Your guess is bigger than the secret number. Please try again&quot;)
        } else if guess &amp;#x3C; secretNumber {
            fmt.Println(&quot;Your guess is smaller than the secret number. Please try again&quot;)
        } else {
            fmt.Println(&quot;Correct, you Legend!&quot;)
            break
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 在线词典&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202252847.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;以&lt;a href=&quot;https://fanyi.caiyunapp.com/#/&quot;&gt;彩云小译&lt;/a&gt;为例，来扒一下翻译接口：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202253295.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们需要在 Golang 中发送该请求，因为这个请求比较复杂，较难用代码构造，我们可以借助&lt;strong&gt;第三方工具 &lt;a href=&quot;https://curlconverter.com/&quot;&gt;curlconverter&lt;/a&gt;&lt;/strong&gt;  来生成代码。&lt;/p&gt;
&lt;p&gt;首先 &lt;code&gt;Copy as cURL (bash)&lt;/code&gt;，然后借助 curlconverter 工具生成 Golang 对应的请求代码：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202253481.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;将代码直接运行可得到请求成功后返回的 JSON 结果（&lt;code&gt;v1&lt;/code&gt;）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202253288.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后我们需要根据 &lt;code&gt;Response Body&lt;/code&gt; 构造出对应的结构体，然后将响应回来的 json 字符串反序列化到结构体中，显然我们不可能自己构造（繁琐且易出错），此时需要再借助&lt;strong&gt;第三方工具 &lt;a href=&quot;https://oktools.net/&quot;&gt;OKTools&lt;/a&gt;&lt;/strong&gt; 生成对应的结构体。&lt;/p&gt;
&lt;p&gt;具体做法是将彩云小译中响应的 json 字符串粘贴到 OKTools 生成对应的结构体：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202253844.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202253582.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;构造「请求结构体」与「响应结构体」后，再次发送请求试试（&lt;code&gt;v3&lt;/code&gt;）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202253987.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看出所有信息都已经打印出来，但这并不是我们想要的，我们只打印 &lt;code&gt;explanations&lt;/code&gt; 和 &lt;code&gt;prons&lt;/code&gt; 这两部分的信息即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202254564.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这样&lt;strong&gt;在线词典&lt;/strong&gt;就完成了，可以运行以下程序尝试一下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202254213.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;3.3 SOCKS5 代理&lt;/h3&gt;
&lt;p&gt;先浅浅演示下最终效果：启动 Golang 代理服务器程序，然后通过命令行 &lt;code&gt;curl -socks5 代理服务器地址 目标URL&lt;/code&gt; 测试（或者通过 SwitchyOmega 插件配置，然后直接访问网站），如果代理服务器能正常工作，那么 &lt;code&gt;curl&lt;/code&gt; 命令就会正常返回，代理服务器的日志也会打印出你所访问的网站域名或者 IP，这说明我们的网络流量是通过此代理服务器转发的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202254999.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里我们将要编写一个较为复杂的 socks5 代理服务器，虽然 socks5 协议是代理协议，但是它并不能用于出去，它的协议使用&lt;strong&gt;明文传输&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;该协议诞生于互联网早期，因为早些时候某些互联网的内网为了确保安全性，有很严格的防火墙策略，但是这会使其访问某些资源较为麻烦，所以 &lt;code&gt;socks5&lt;/code&gt; 应运而生，它相当于在防火墙上开个口子，让授权用户可以通过单个端口访问内部资源。&lt;/p&gt;
&lt;p&gt;实际上很多软件最终暴露的也是一个 &lt;code&gt;socks5&lt;/code&gt; 协议的端口，其实爬虫中所使用的 IP 代理池中很多代理协议就是 &lt;code&gt;socks5&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;接着简单了解下 &lt;code&gt;socks5&lt;/code&gt; 的工作原理（下附图解），大致流程是浏览器与 &lt;code&gt;socks5&lt;/code&gt; 代理服务器建立 TCP 连接，然后 &lt;code&gt;socks5&lt;/code&gt; 代理服务器再与目标服务器建立 TCP 连接，这里可分为四个阶段：&lt;strong&gt;握手阶段&lt;/strong&gt;、&lt;strong&gt;认证阶段&lt;/strong&gt;、&lt;strong&gt;请求阶段&lt;/strong&gt;、&lt;strong&gt;relay 阶段&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一阶段——&lt;strong&gt;握手&lt;/strong&gt;：浏览器向 &lt;code&gt;socks5&lt;/code&gt; 代理服务器发起请求，其中的数据包内容包括协议版本号 &lt;code&gt;VER&lt;/code&gt;，还有支持的认证种类 &lt;code&gt;NMETHODS&lt;/code&gt;，以及具体的认证方法 &lt;code&gt;METHOD&lt;/code&gt;。如果类型为 &lt;code&gt;00&lt;/code&gt; 则表示不需要认证，如果为其他类型则进入认证流程。&lt;/li&gt;
&lt;li&gt;第二阶段——&lt;strong&gt;认证&lt;/strong&gt;：不作详细介绍。&lt;/li&gt;
&lt;li&gt;第三阶段——&lt;strong&gt;请求&lt;/strong&gt;：认证通过后浏览器会向 socks5 代理服务器发起 Connection 请求，主要信息包括版本号 &lt;code&gt;VER&lt;/code&gt;、请求类型 &lt;code&gt;CMD&lt;/code&gt;、保留字段 &lt;code&gt;RSV&lt;/code&gt;、目标地址类型 &lt;code&gt;ATYP&lt;/code&gt;、目标 IP &amp;#x26; Port。代理服务器接收到请求后，会和目标服务器建立起连接，然后返回响应。&lt;/li&gt;
&lt;li&gt;第四阶段——&lt;strong&gt;relay&lt;/strong&gt;：此时浏览器与目标服务器就可以通过 socks5 代理进行数据的正常收发。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;SOCKS5&lt;/code&gt; 协议工作原理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202254305.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;😮在正式实现 &lt;code&gt;socks5&lt;/code&gt; 代理前，我们先用 Golang 实现一个简单的 &lt;strong&gt;TCP Echo Server&lt;/strong&gt; 过渡一下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202254490.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202255761.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;💖「&lt;code&gt;SOCKS5&lt;/code&gt; 代理服务器」完整代码（附超详细的代码注释）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202255643.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;🥰接着就是测试环节了，&lt;code&gt;命令行测试&lt;/code&gt;和&lt;code&gt;浏览器测试&lt;/code&gt;各演示一次。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;命令行测试&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202255056.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;浏览器测试&lt;/strong&gt;：通过 &lt;strong&gt;SwitchyOmega&lt;/strong&gt; 插件配置访问网站，代理服务器进行响应&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202255638.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202256437.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;4. 课后作业&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202256153.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;4.1 简化猜谜游戏&lt;/h3&gt;
&lt;p&gt;关键代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var guess int
_, err := fmt.Scanf(&quot;%d\r\n&quot;, &amp;#x26;guess) // windows
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终代码：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202256036.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;4.2 新增翻译引擎&lt;/h3&gt;
&lt;p&gt;所使用的翻译引擎：&lt;a href=&quot;https://ai.youdao.com/product-fanyi-text.s&quot;&gt;有道智云AI翻译&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;具体操作上文已经详细介绍过了，代码如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202256285.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;🚀这里补充推荐几个翻译接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cn.bing.com/translator/&quot;&gt;必应翻译&lt;/a&gt;：Level 1&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://translate.volcengine.com/&quot;&gt;火山翻译&lt;/a&gt;：Level 1&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fanyi.youdao.com/&quot;&gt;有道翻译&lt;/a&gt;：Level 2（接口被加密，没法轻易破解）
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;salt&lt;/code&gt; 随机数：时间 + rand 生成&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sign&lt;/code&gt;：md5 加密认证&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://translate.google.cn/&quot;&gt;谷歌翻译&lt;/a&gt;：Level 3
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jianshu.com/p/38a65d8d3e80&quot;&gt;百度翻译接口破解&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fanyi.baidu.com/&quot;&gt;百度翻译&lt;/a&gt;：Level 3
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.52pojie.cn/thread-707169-1-1.html&quot;&gt;谷歌翻译接口破解&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⭐后三种翻译平台想要免费使用的话都需要破解，或者你可以氪金去申请对应翻译平台的 API 接口，比较稳定。&lt;/p&gt;
&lt;h3&gt;4.3 并行请求翻译&lt;/h3&gt;
&lt;p&gt;关键代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    // ...
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
        queryYouDao(word)
        wg.Done()
    }()
    go func() {
        queryCaiYun(word)
        wg.Done()
    }()
    wg.Wait()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终代码：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202256440.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202501202213131.mKFcymW5.jpg"/><enclosure url="/_astro/202501202213131.mKFcymW5.jpg"/></item><item><title>编译器优化 C++ 拷贝构造函数</title><link>https://coooredump.github.io/blog/cpp/cpp-copy-constructor</link><guid isPermaLink="true">https://coooredump.github.io/blog/cpp/cpp-copy-constructor</guid><description>当返回值为对象时，gcc 对此做了优化，不再产生临时对象，因此不再调用拷贝构造函数。</description><pubDate>Fri, 25 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今天碰到一件令我百思不得其解的问题：&lt;strong&gt;为什么&lt;code&gt;拷贝构造函数&lt;/code&gt;不按自己所预期的结果输出&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;按 C++ 的语法来说，本该如此，并非自己理解有误而导致的！&lt;/p&gt;
&lt;p&gt;功夫不负有心人，经过几天的搜索🔍、学习👨‍💻，我总算明白并解决了这个问题，特此输出该文记录一下。&lt;/p&gt;
&lt;h2&gt;📈 背景知识&lt;/h2&gt;
&lt;h3&gt;1. 构造函数&lt;/h3&gt;
&lt;p&gt;💛&lt;strong&gt;创建并初始化&lt;/strong&gt;类的数据成员时调用&lt;/p&gt;
&lt;h3&gt;2. 析构函数&lt;/h3&gt;
&lt;p&gt;💚当对象生命周期终止时调用，用于释放对象占有的资源&lt;/p&gt;
&lt;h3&gt;3. 拷贝构造函数&lt;/h3&gt;
&lt;p&gt;❤调用时机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将某个对象用于&lt;strong&gt;初始化另一个新创建的对象&lt;/strong&gt;时&lt;/li&gt;
&lt;li&gt;当对象作为参数传递给函数，且函数形参为普通对象时（&lt;strong&gt;因为引用对象不会调用拷贝构造函数&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;对象作为&lt;strong&gt;函数的返回值&lt;/strong&gt;时&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;💙注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果在类中没有定义拷贝构造函数，编译器会自行定义一个；&lt;/li&gt;
&lt;li&gt;如果类带有指针变量，并有动态内存分配，则它必须有一个拷贝构造函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;🌄 进入正题&lt;/h2&gt;
&lt;p&gt;先来看一段包含&lt;strong&gt;构造函数&lt;/strong&gt;、&lt;strong&gt;析构函数&lt;/strong&gt;、&lt;strong&gt;拷贝构造函数&lt;/strong&gt;的简单代码，代码中穿插着许多注释，这里就不再一一解释。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文旨在探索&lt;code&gt;拷贝构造函数&lt;/code&gt;，构造函数与析构函数仅为顺带学习而提及，可略过这二者。&lt;/p&gt;
&lt;p&gt;顺带一提，注释中标注的各类函数调用顺序仅针对于&lt;strong&gt;预期结果&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;iostream&gt;

using namespace std;

class Point {
public:
    int x;
    int y;
    int *p;

    Point(int xx, int yy, int *pp);

    ~Point();

    Point(const Point &amp;#x26;point);
};

// 构造函数
Point::Point(int xx, int yy, int *pp) : x(xx), y(yy) {
    // 申请一块值为*pp的内存空间, 并让指针p指向它!
    p = new int(*pp);
    // p = new int;
    // *p = *pp;
    cout &amp;#x3C;&amp;#x3C; &quot;Point()&quot; &amp;#x3C;&amp;#x3C; endl;
}

// 析构函数
Point::~Point() {
    delete p;
    cout &amp;#x3C;&amp;#x3C; this-&gt;x &amp;#x3C;&amp;#x3C; &quot;~Point()&quot; &amp;#x3C;&amp;#x3C; endl;
}

// 拷贝构造函数
Point::Point(const Point &amp;#x26;point) {
    this-&gt;x = point.x;
    this-&gt;y = point.y;
    p = new int;
    *p = *point.p;
    cout &amp;#x3C;&amp;#x3C; &quot;Copy-Constructor()&quot; &amp;#x3C;&amp;#x3C; endl;
}

// 参数为对象, 调用拷贝构造函数
// 若定义为 const Point &amp;#x26;point 则不会调用拷贝构造函数: 因为 &amp;#x26; 是引用, 会指向同一个对象, 而不是拷贝!
void display(Point point) {
    point.x = 4;
}

// 返回值为对象, 调用拷贝构造函数
Point returnPoint() {
    int c = 6;
    Point point(7, 5, &amp;#x26;c);
    return point;
}

int main() {
    int z = 3;
    // 调用构造函数
    Point point1(1, 2, &amp;#x26;z);     // 1.Point()、11.~Point()

    // 情况1: 调用拷贝构造函数
    Point point2 = point1;                  // 2.Copy-Constructor()、10.~Point()
    point2.x = 2;

    // 情况2: 调用拷贝构造函数
    display(point2);                  // 3.Copy-Constructor()、4.~Point()

    // 情况3: 调用拷贝构造函数???
    Point point3 = returnPoint();           // 5.Point()、6.Copy-Constructor()、7.~Point()、8.Copy-Constructor()、9.~Point()、10.~Point()
    point3.x = 10;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;运行结果&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;Point()
Copy-Constructor()
Copy-Constructor()
4~Point()
Point()
10~Point()
2~Point()
1~Point()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;预期结果&lt;/h3&gt;
&lt;p&gt;🛕各类函数的调用顺序均已在注释中标明！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;Point()
Copy-Constructor()
Copy-Constructor()
4~Point()
Point()
Copy-Constructor()
7~Point()
Copy-Constructor()
7~Point()
10~Point()
2~Point()
1~Point()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;🤨 分析原因&lt;/h2&gt;
&lt;p&gt;浅浅分析下&lt;strong&gt;运行结果&lt;/strong&gt;与&lt;strong&gt;预期结果&lt;/strong&gt;之间&lt;strong&gt;拷贝构造函数调用的差异&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果你感兴趣，可以自己去调试下，最后发现问题出在 &lt;strong&gt;情况 3:  &lt;code&gt;Point point3 = returnPoint();&lt;/code&gt;&lt;/strong&gt; 处，也就是&lt;strong&gt;函数的返回值为对象&lt;/strong&gt;时。&lt;/p&gt;
&lt;p&gt;为什么？！&lt;/p&gt;
&lt;p&gt;🚀&lt;strong&gt;原来是 GCC 做了优化，当返回值为对象时，不再产生临时对象，因此不再调用拷贝构造函数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;再来对比下两个结果，可见直接把 2 个拷贝构造函数都优化掉了，&lt;/p&gt;
&lt;p&gt;⭐这时候又会有人问了：诶，为什么是 2 个，情况 3 不就是对象作为函数返回值吗？不就只会调用 1 次拷贝构造函数吗？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// ...
Point returnPoint() {
    int c = 6;
    Point point(7, 5, &amp;#x26;c);
    return point;
}

int main() {
    Point point3 = returnPoint();
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就这部分代码而言，我的猜想是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;returnPoint()&lt;/code&gt; 函数返回对象时，将其拷贝到一个临时对象 &lt;code&gt;temp&lt;/code&gt; 中（① 调用拷贝构造函数），然后释放函数中的局部对象；&lt;/li&gt;
&lt;li&gt;当执行到 &lt;code&gt;Point point3 = returnPoint();&lt;/code&gt; 时，将对象赋值给 &lt;code&gt;point3&lt;/code&gt;（② 再次调用拷贝构造函数），并释放临时对象 &lt;code&gt;temp&lt;/code&gt;，最后释放 &lt;code&gt;point3&lt;/code&gt; 对象。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;差不多是这么回事&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501222258668.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当然以上没有很严谨的科学依据，但是经过我几番调试，输出结果也吻合，估计是八九不离十！&lt;/p&gt;
&lt;h2&gt;🌍 解决办法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q&lt;/strong&gt;：如果一定想要让拷贝构造函数在这种情况下执行呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;：只需要让 GCC 不要优化：在编译命令中加入 &lt;code&gt;-fno-elide-constructors&lt;/code&gt; 参数，例如 &lt;code&gt;g++ -fno-elide-constructors CopyConstructor.cpp&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;我个人是使用的 C++ IDE 是 &lt;a href=&quot;https://www.jetbrains.com/clion/&quot;&gt;CLion&lt;/a&gt;，如下也给出相应的解决办法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因为使用 IDE 就是为了快速编译运行，不可能每次都执行相应代码来运行程序，所以需要配置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;只需在 &lt;code&gt;CMakeLists.txt&lt;/code&gt; 中添加如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;# 添加编译选项! ==&gt; 防止g++优化导致&quot;返回对象不调用拷贝构造函数&quot;！
add_definitions(-fno-elide-constructors)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🗺&lt;strong&gt;提醒一下&lt;/strong&gt;：如果你的代码依赖于拷贝构造函数的副作用，那么你的代码就写得很烂。你编写的拷贝构造函数就应该保证这样的优化是安全的。&lt;/p&gt;
&lt;h2&gt;⛵ 最后&lt;/h2&gt;
&lt;h3&gt;gcc 和 g++ 是什么，有什么区别？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;该段落出自：http://c.biancheng.net/view/7936.html&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;发展至今，GCC 编译器的功能也由最初仅能编译 C 语言，扩增至可以编译多种编程语言，其中就包括 C++ 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;除此之外，当下的 GCC 编译器还支持编译 Go、Objective-C，Objective-C ++，Fortran，Ada，D 和 BRIG（HSAIL）等程序，甚至于 GCC 6 以及之前的版本还支持编译 Java 程序。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么，在已编辑好 C 语言或者 C++ 代码的前提下，如何才能调用 GCC 编译器为我们编译程序呢？很简单，GCC 编译器已经为我们提供了调用它的接口，对于 C 语言或者 C++ 程序，可以通过执行 &lt;code&gt;gcc&lt;/code&gt; 或者 &lt;code&gt;g++&lt;/code&gt; 指令来调用 GCC 编译器。&lt;/p&gt;
&lt;p&gt;值得一提的是：实际使用中我们更习惯使用 &lt;code&gt;gcc&lt;/code&gt; 指令编译 C 语言程序，用 &lt;code&gt;g++&lt;/code&gt; 指令编译 C++ 代码。需要强调的一点是，&lt;code&gt;gcc&lt;/code&gt; 指令也可以用来编译 C++ 程序，同样 &lt;code&gt;g++&lt;/code&gt; 指令也可以用于编译 C 语言程序。&lt;/p&gt;
&lt;p&gt;⭐总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gcc 是 GCC 中的 GUN C Compiler（C 编译器）&lt;/li&gt;
&lt;li&gt;g++ 是 GCC 中的 GUN C++ Compiler（C++编译器）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CMakeLists.txt 超傻瓜式教程&lt;/h3&gt;
&lt;p&gt;CMake 命令官网：&lt;a href=&quot;https://cmake.org/cmake/help/v3.23/index.html&quot;&gt;cmake.org&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# 本CMakeLists.txt的project名称
# 会自动创建两个变量，PROJECT_SOURCE_DIR和PROJECT_NAME
# ${PROJECT_SOURCE_DIR}：本CMakeLists.txt所在的文件夹路径
# ${PROJECT_NAME}：本CMakeLists.txt的project名称
project(xxx)

# 获取路径下所有的.cpp/.c/.cc文件，并赋值给变量中
aux_source_directory(路径 变量)

# 给文件名/路径名或其他字符串起别名，用${变量}获取变量内容
set(变量 文件名/路径/...)

# 添加编译选项
add_definitions(编译选项)

# 打印消息
message(消息)

# 编译子文件夹的CMakeLists.txt
add_subdirectory(子文件夹名称)

# 将.cpp/.c/.cc文件生成.a静态库
# 注意，库文件名称通常为libxxx.so，在这里只要写xxx即可
add_library(库文件名称 STATIC 文件)

# 将.cpp/.c/.cc文件生成可执行文件
add_executable(可执行文件名称 文件)

# 规定.h头文件路径
include_directories(路径)

# 规定.so/.a库文件路径
link_directories(路径)

# 对add_library或add_executable生成的文件进行链接操作
# 注意，库文件名称通常为libxxx.so，在这里只要写xxx即可
target_link_libraries(库文件名称/可执行文件名称 链接的库文件名称)
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>你可以不使用排序库函数来解决这道题吗？</title><link>https://coooredump.github.io/blog/leetcode/tackle-without-sort-library</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/tackle-without-sort-library</guid><description>本文不是突发奇想，而是最近刷 LeetCode 曾被灵魂拷问过：“你可以不适用代码库中的排序函数来解决这道题吗？”</description><pubDate>Thu, 17 Feb 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501222221632.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;转念想想，好像让我随手写个&lt;code&gt;快排&lt;/code&gt;都有点棘手，时间偷走了我的记忆，那就用文字记录下叭。&lt;/p&gt;
&lt;p&gt;话不多说，本文归纳下各类经典的排序算法。&lt;/p&gt;
&lt;h2&gt;排序算法🎪&lt;/h2&gt;
&lt;p&gt;👑因为代码中添加了一些有助于理解的&lt;strong&gt;注释&lt;/strong&gt;，且很多算法都很常见，其排序思想就不再赘述了。&lt;/p&gt;
&lt;h3&gt;直接插入排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void insertSort(int[] data) {
    int length = data.length;
    for (int i = 1; i &amp;#x3C; length; i++) {
        int temp = data[i];
        if (data[i] - data[i - 1] &amp;#x3C; 0) {
            int j = i - 1;
            for (; j &gt;= 0 &amp;#x26;&amp;#x26; data[j] - temp &gt; 0; j--) {
                data[j + 1] = data[j];
            }
            data[j + 1] = temp;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;希尔排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void ShellSort(int[] data) {
    int arrayLength = data.length;
    int h = 1;
    while (h &amp;#x3C;= arrayLength / 3) {
        h = h * 3 + 1;
    }
    while (h &gt; 0) {
        for (int i = h; i &amp;#x3C; arrayLength; i++) {
            int temp = data[i];
            if (data[i] - data[i - h] &amp;#x3C; 0) {
                int j = i - h;
                for (; j &gt;= 0 &amp;#x26;&amp;#x26; data[j] - temp &gt; 0; j -= h) {
                    data[j + h] = data[j];
                }
                data[j + h] = temp;
            }
        }
        h = (h - 1) / 3;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;简单选择排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void selectSort(int[] data) {
    int arrayLength = data.length;
    for (int i = 0; i &amp;#x3C; arrayLength - 1; i++) {
        for (int j = i + 1; j &amp;#x3C; arrayLength; j++) {
            if (data[i] - data[j] &gt; 0) {
                int temp = data[i];
                data[i] = data[j];
                data[j] = temp;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;堆排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 堆排序
 */
public static void heapSort(int[] data) {
    int arrayLength = data.length;
    // 循环建堆
    for (int i = 0; i &amp;#x3C; arrayLength - 1; i++) {
        // 建堆
        buildMaxdHeap(data, arrayLength - 1 - i);
        // 交换堆顶和最后一个元素
        swap(data, 0, arrayLength - 1 - i);
    }
}

// 对data数组从0到lastIndex建大顶堆
private static void buildMaxdHeap(int[] data, int lastIndex) {
    // 从lastIndex处节点（最后一个节点）的父节点开始
    for (int i = (lastIndex - 1) / 2; i &gt;= 0; i--) {
        // k保存当前正在判断的节点
        int k = i;
        // 如果当前k节点的子节点存在
        while (k * 2 + 1 &amp;#x3C;= lastIndex) {
            // k节点的左子节点的索引
            int biggerIndex = 2 * k + 1;
            // 如果biggerIndex小于lastIndex，即biggerIndex +1
            // 代表k节点的右子节点存在
            if (biggerIndex &amp;#x3C; lastIndex) {
                // 如果右子节点的值较大
                if (data[biggerIndex] - data[biggerIndex + 1] &amp;#x3C; 0) {
                    // biggerIndex总是记录较大子节点的索引
                    biggerIndex++;
                }
            }
            // 如果k节点的值小于其较大子节点的值
            if (data[k] - data[biggerIndex] &amp;#x3C; 0) {
                // 交换它们
                swap(data, k, biggerIndex);
                // 将biggerIndex赋给k，开始while循环的下一次循环
                // 重新保证k节点的值大于其左、右节点的值
                k = biggerIndex;
            } else {
                break;
            }
        }
    }
}

// 交换data数组中i、j两个索引处的元素
private static void swap(int[] data, int i, int j) {
    int temp = data[i];
    data[i] = data[j];
    data[j] = temp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;冒泡排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void bubbleSort(int[] arr) {
    for (int i = 0; i &amp;#x3C; arr.length - 1; i++) {
        for (int j = 0; j &amp;#x3C; arr.length - 1 - i; j++) {
            if (arr[j] &gt; arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;归并排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 归并排序
 */
public static void mergeSort(int[] data) {
    sort(data, 0, data.length - 1);
}

// 将索引从left到right范围的数组元素进行归并排序
private static void sort(int[] data, int left, int right) {
    if (left &amp;#x3C; right) {
        //找出中间索引
        int center = (left + right) / 2;
        sort(data, left, center);
        sort(data, center + 1, right);
        //合并
        merge(data, left, center, right);
    }
}

// 将两个数组进行归并，归并前两个数组已经有序，归并后依然有序
private static void merge(int[] data, int left, int center, int right) {
    int[] tempArr = new int[data.length];
    int mid = center + 1;
    int third = left;
    int temp = left;
    while (left &amp;#x3C;= center &amp;#x26;&amp;#x26; mid &amp;#x3C;= right) {
        if (data[left] - data[mid] &amp;#x3C;= 0) {
            tempArr[third++] = data[left++];
        } else {
            tempArr[third++] = data[mid++];
        }
    }
    while (mid &amp;#x3C;= right) {
        tempArr[third++] = data[mid++];
    }
    while (left &amp;#x3C;= center) {
        tempArr[third++] = data[left++];
    }
    while (temp &amp;#x3C;= right) {
        data[temp] = tempArr[temp++];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基数排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void radixSort(int[] data, int radix, int d) {
    int arrayLength = data.length;
    int[] temp = new int[arrayLength];
    int[] buckets = new int[radix];
    for (int i = 0, rate = 1; i &amp;#x3C; d; i++) {
        // 重置count数组，开始统计第二个关键字
        Arrays.fill(buckets, 0);
        // 当data数组的元素复制到temp数组中进行缓存
        System.arraycopy(data, 0, temp, 0, arrayLength);
        for (int j = 0; j &amp;#x3C; arrayLength; j++) {
            int subKey = (temp[j] / rate) % radix;
            buckets[subKey]++;
        }
        for (int j = 1; j &amp;#x3C; radix; j++) {
            buckets[j] = buckets[j] + buckets[j - 1];
        }
        for (int m = arrayLength - 1; m &gt;= 0; m--) {
            int subKey = (temp[m] / rate) % radix;
            data[--buckets[subKey]] = temp[m];
        }
        rate *= radix;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;桶排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void BucketSort(int[] data, int min, int max) {
    int arrayLength = data.length;
    int[] temp = new int[arrayLength];
    int[] buckets = new int[max - min];
    
    for (int i = 0; i &amp;#x3C; arrayLength; i++) {
        buckets[data[i] - min]++;
    }
    
    for (int i = 1; i &amp;#x3C; max - min; i++) {
        buckets[i] = buckets[i] + buckets[i - 1];
    }
    
    System.arraycopy(data, 0, temp, 0, arrayLength);
    for (int k = arrayLength - 1; k &gt;= 0; k--) {
        data[--buckets[temp[k] - min]] = temp[k];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;快速排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 快速排序
 */
public static void quickSort(int[] data) {
    subSort(data, 0, data.length - 1);
}

private static void subSort(int[] data, int start, int end) {
    if (start &amp;#x3C; end) {
        int base = data[start];
        int low = start;
        int high = end + 1;
        while (true) {
            while (low &amp;#x3C; end &amp;#x26;&amp;#x26; data[++low] - base &amp;#x3C;= 0)
                ;
            while (high &gt; start &amp;#x26;&amp;#x26; data[--high] - base &gt;= 0)
                ;
            if (low &amp;#x3C; high) {
                swap(data, low, high);
            } else {
                break;
            }
        }
        swap(data, start, high);

        subSort(data, start, high - 1);
        subSort(data, high + 1, end);
    }
}

private static void swap(int[] data, int i, int j) {
    int temp = data[i];
    data[i] = data[j];
    data[j] = temp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度一览表🍦&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;图片源于&lt;a href=&quot;https://www.runoob.com/w3cnote/ten-sorting-algorithm.html&quot;&gt;菜鸟教程&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501222218190.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501222218938.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;😁对于算法的详细分析请参考：&lt;a href=&quot;https://www.runoob.com/w3cnote/ten-sorting-algorithm.html&quot;&gt;十大排序算法&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;何时调用库函数🔮&lt;/h2&gt;
&lt;p&gt;不仅是本题的排序算法，LeetCode 中有许多可以调用库函数的地方，那么究竟何时该调用何时别调用呢？&lt;/p&gt;
&lt;p&gt;举个栗子：&lt;a href=&quot;https://leetcode-cn.com/problems/reverse-words-in-a-string/&quot;&gt;151.翻转字符串里的单词&lt;/a&gt;，这题本身是综合考察对字符串的处理能力，如果直接调用 &lt;code&gt;split&lt;/code&gt; 和 &lt;code&gt;reverse&lt;/code&gt; 库函数，那么这道题就失去了它存在的意义。&lt;/p&gt;
&lt;p&gt;🚫所以如果题目关键代码可以直接调用库函数解决，建议不要使用库函数，毕竟面试官不是考察你对库函数的熟悉程度。&lt;/p&gt;
&lt;p&gt;🔍如果库函数仅是解题过程中的一小部分，并且你已经很清楚这个库函数内部的实现原理的话，可以考虑调用库函数，节省时间。&lt;/p&gt;
&lt;p&gt;本着提高代码水平的原则，我想你就会很清楚什么时候该调什么时候不该调了，只有才会有助于对算法的理解。&lt;/p&gt;
&lt;p&gt;🌈&lt;strong&gt;注意&lt;/strong&gt;：并非所有语言都像 Python 和 Java 有着丰富的库函数，C、C++ 等语言偏底层，这类所谓的库函数也许得自己手写。&lt;/p&gt;</content:encoded><h:img src="/_astro/202501222220361.Ch3SIm3N.jpg"/><enclosure url="/_astro/202501222220361.Ch3SIm3N.jpg"/></item><item><title>「2021 年终总结」唯有热爱，可抵漫长岁月</title><link>https://coooredump.github.io/blog/yearly-review/2021-passion</link><guid isPermaLink="true">https://coooredump.github.io/blog/yearly-review/2021-passion</guid><description>Farewell</description><pubDate>Sat, 01 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;遇见掘金的元年&lt;/h2&gt;
&lt;h3&gt;回顾写作历程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;2021 年 01 月 27 日&lt;/strong&gt;，我正式加入掘金社区，直到现在，掘金仍是我写博客理想且唯一的社区。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2021 年 03 月 05 日&lt;/strong&gt;，怀着忐忑的心情在掘金发表了第一篇文章，我永远也不会忘记那篇 &lt;em&gt;Lambda&lt;/em&gt; 文章前前后后修改了一天。最初，总是害怕自己写的文章过于逊色，迟迟不肯动笔，转念一想，先开始再说，不能总在计划而提不上日程，提升自己，不必在意别人的目光。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2021 年 08 月 31 日&lt;/strong&gt;，终于写下了心心念念的「LeetCode」相关文章，刷算法题的时间被我搁置太久了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2021 年 09 月 09 日&lt;/strong&gt;，创建了一个「深度思考」专栏，它为规划自己人生与理解生活提供了莫大的帮助。让这一年迷茫的我，短暂摆脱对未来的恐惧。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2021 年 10 月 20 日&lt;/strong&gt;，第一次完成掘金活动：&lt;a href=&quot;https://juejin.cn/post/7008476801634680869&quot;&gt;“程序员必懂小知识”创作挑战，专治写作困难症&lt;/a&gt;！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2021 年 11 月 29 日&lt;/strong&gt;，积极参与并达成11月活动打卡任务：&lt;a href=&quot;https://juejin.cn/post/7023643374569816095/&quot;&gt;2021最后一次更文挑战&lt;/a&gt;！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141816729.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;🏆 流量不是写作的第一目标，却又是激励写作的一大乐趣。&lt;/p&gt;
&lt;p&gt;🌹 我永远不会忘记第一次收到&lt;strong&gt;点赞|收藏|关注&lt;/strong&gt;的喜悦，这是对自己写文的一种肯定。&lt;/p&gt;
&lt;p&gt;💦 这一年在掘金的故事还未结束，2022 年在掘金的旅途也即将开始...&lt;/p&gt;
&lt;h3&gt;2021 掘金战利品&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;掘金徽章&lt;/strong&gt; 2 枚（立志 2022 年集齐所有徽章）&lt;/li&gt;
&lt;li&gt;掘金 Yoyo &lt;strong&gt;抱枕&lt;/strong&gt; 2 个（1 个是奖品，1 个是全部身家 8W 矿石兑换的）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;蓝牙音箱&lt;/strong&gt; 1 个（参与 “程序员必懂小知识” 获得）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;掘金定制拖鞋&lt;/strong&gt; 1 双&lt;/li&gt;
&lt;li&gt;掘金贴纸（收藏）&lt;/li&gt;
&lt;li&gt;掘金棒球帽（喜欢）&lt;/li&gt;
&lt;li&gt;双肩袋黑-活动限定（4W 矿石兑换）&lt;/li&gt;
&lt;li&gt;还有 &lt;strong&gt;11 月更文挑战&lt;/strong&gt; 奖品
&lt;ul&gt;
&lt;li&gt;午睡毯（Like）&lt;/li&gt;
&lt;li&gt;咖啡机（没咖啡豆就是了）&lt;/li&gt;
&lt;li&gt;字节保温杯（极其喜欢）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141816858.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141816980.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141817818.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141816722.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141817035.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;随波逐流的一年&lt;/h2&gt;
&lt;h3&gt;大学生活与工作🤪&lt;/h3&gt;
&lt;p&gt;不知不觉已是大三学子，大三这一年很荣幸能担任班长，即使辛苦，但能为集体发光发热的感觉真的挺不错，这也算是我人生道路上的一次小突破吧。&lt;/p&gt;
&lt;p&gt;回顾即将结束的这学期，文件堆积成山，都是一点一滴堆积起来的，它们见证了我这半年工作，纪念一下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141818446.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当然，大学生活可不仅限于工作，生活也是大学的一大主旋律。可说起自己这一年的生活，却是不尽人意，上半年的我踌躇满志，下半年的我~~混吃等死~~放纵不羁。唯一让自己比较满意的是，爱干净的习惯没有丢失，同时做事多少还葆有条理性。哎！回想起大一意气风发、雄心壮志的自己，再看看现在镜中一蹶不振的样子，羞耻之心油然而生。当然，人的眼光还是要向前看的，汲取教训就是对过往最好的答复。&lt;/p&gt;
&lt;p&gt;同时，今年（年初）首次尝试烫发，为什么烫发呢，不自信吗？可能多少有点吧，但更多的还是体验，人生有很多抉择，若只是推陈守旧，不敢尝试新事物，那么人生也太无趣了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于这次大胆的尝试，也有些许感受：Tony，烫得不错，下次不许烫了😅&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;今年 8 月份购入的 iPad 让我这个本就不富裕的家庭雪上加霜。但不得不说，iPad 手感不错，哈哈哈。今年没能让 iPad 的生产力最大化，明年是得挖掘下它本该有的价值了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141818154.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;年末时【英语六级前一天 | 也是被接收为预备党员的前一天】遭遇一次小型车祸，给大家瞅瞅我去医院拍 X 光片&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141818605.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141818463.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;那些值得纪念的时刻💦&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141818114.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141818266.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141819363.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141819339.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;学习🌅&lt;/h3&gt;
&lt;p&gt;🔥 &lt;strong&gt;GitHub&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141819727.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;🔥 &lt;strong&gt;Gitee&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141819797.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;🔥 &lt;strong&gt;书单&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 《操作系统导论》&lt;/li&gt;
&lt;li&gt;[x] 《算法》&lt;/li&gt;
&lt;li&gt;[ ] 《计算机组成原理》&lt;/li&gt;
&lt;li&gt;[x] 《现代操作系统》：基础知识又过了一遍，高深的知识还触碰不到&lt;/li&gt;
&lt;li&gt;[ ] 《$数据结构与算法分析_{Java语言描述}$》&lt;/li&gt;
&lt;li&gt;[x] 《$计算机网络_{自顶向下方法}$》：每天早上 5:30 起床就为了读这本书，坚持了一个月；还剩下一些目前对我来说比较困难的知识，无伤大雅&lt;/li&gt;
&lt;li&gt;[x] 《大话数据结构》：前后看了两遍，但遗忘速度也是够快&lt;/li&gt;
&lt;li&gt;[ ] 《鸟哥的 Linux 私房菜》：面向运维的书籍，用于查阅与学习指令足够了&lt;/li&gt;
&lt;li&gt;[ ] 《图解 TCP/IP》：可作为参考手册&lt;/li&gt;
&lt;li&gt;[x] 《图解 HTTP》：通俗易懂，绝了&lt;/li&gt;
&lt;li&gt;[ ] 《编码》：这本在学计算机组成原理一课给了我莫大的启事，书籍后半部分尚未有时间去咀嚼&lt;/li&gt;
&lt;li&gt;[ ] 《高性能 MySQL》：原以为这学期的高级数据库是讲 MySQL 呢，原来是 NoSQL 哇&lt;/li&gt;
&lt;li&gt;[x] 《MySQL 必知必会》&lt;/li&gt;
&lt;li&gt;[ ] 《$JavaScript高级程序设计_{第三版}$》：确实不错&lt;/li&gt;
&lt;li&gt;[ ] 《数学建模》：虽然我很热爱数学，但抵不过时间受限&lt;/li&gt;
&lt;li&gt;[x] 《批判性思维》：术语过于专业&lt;/li&gt;
&lt;li&gt;[ ] 《人性的弱点》：我最爱的书籍，它教会了我很多为人处世的道理&lt;/li&gt;
&lt;li&gt;[ ] 《圆圈正义》：罗翔老师受到那么多人追捧是不无道理的&lt;/li&gt;
&lt;li&gt;[x] 《小懒财富自由之路 · 从基金开始》：有关基金的基本知识在这里都能以最通俗易懂的方式呈现&lt;/li&gt;
&lt;li&gt;[x] 《小狗钱钱》：不错的理财入门书籍&lt;/li&gt;
&lt;li&gt;[ ] 《指数基金投资指南》：阅读该书的理由这还得从股神巴菲特的 20 年赌约说起&lt;/li&gt;
&lt;li&gt;[ ] 《穷爸爸·富爸爸》：典中典&lt;/li&gt;
&lt;li&gt;[ ] 《人性的优点》&lt;/li&gt;
&lt;li&gt;[ ] $...$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;健身💪&lt;/h3&gt;
&lt;p&gt;遥想 2019 那年开始健身，连续做 235 个俯卧撑，现在属于是廉颇老矣。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;今年 11 月于我而言，是人生中的低谷。不仅健身，连生活与学习都处于一个恍惚的状态&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141819809.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141819714.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141819791.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;凡是过往，皆为序章&lt;/p&gt;
&lt;h3&gt;感情🚫&lt;/h3&gt;
&lt;p&gt;感情方面一直很稳定，确实，单身能不稳定吗 🥱&lt;/p&gt;
&lt;h3&gt;不一样的圣诞节🎄&lt;/h3&gt;
&lt;p&gt;圣诞节这天班级里组织了志愿活动，前往养老院照顾老人们，意义非凡的一天&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141819082.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141820234.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141820883.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141820194.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;网易云年度歌单报告🎶&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141820907.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141820108.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141820732.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501202229924.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141820984.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;未来可期&lt;/h2&gt;
&lt;p&gt;至今为止，我所写的大部分文章都是一些笔记类文章，无法深入某个知识点进行钻研与学习，这也是我写博客的一大痛点。&lt;/p&gt;
&lt;p&gt;2022 年我会将写作的风格聚焦于具体的知识点，而非笔记博客类泛泛而谈，缺乏深度，聚焦后端。&lt;/p&gt;
&lt;p&gt;大学是最佳的试错阶段，不要害怕；过分自信起码还有敢于尝试的勇气，而怯懦则会让一切的机会消失殆尽，自信点！别让他人口中的 “太难了、你不行” 成为自己的阻碍，生活是自己的，走出舒适圈，收获新天地。怎么知道自己的人生是在走上坡路还是下坡路呢，感觉累就是上坡，感觉轻松就是下坡。你现在感觉累吗？共勉～&lt;/p&gt;</content:encoded><h:img src="/_astro/202501222008478.CTI44563.jpg"/><enclosure url="/_astro/202501222008478.CTI44563.jpg"/></item><item><title>不要以你现在的能力，束缚对未来的想象</title><link>https://coooredump.github.io/blog/future-survivor/dont-limit-your-future-imagination-with-your-current-abilities</link><guid isPermaLink="true">https://coooredump.github.io/blog/future-survivor/dont-limit-your-future-imagination-with-your-current-abilities</guid><description>实现目标犹如登山，而能力的提升是一个动态的过程，永远不要让现在的思维限制对未来的思考。</description><pubDate>Tue, 07 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;分享一篇稻盛和夫的演讲。正如他所说：“实现目标犹如登山，而能力的提升是一个动态的过程，永远不要让现在的思维限制对未来的思考。”在成功之前，稻盛和夫也曾无数次怀疑自己，但他最终登上了自己人生的顶峰。&lt;/p&gt;
&lt;h2&gt;01 每天比昨天进步一点，哪怕只一厘米&lt;/h2&gt;
&lt;p&gt;在我工作的第一家公司，我反复进行着各种实验，有失败也有成功。当时在无机化学的研究者中，同我年龄相仿的，有人拿到了奖学金赴美留学；有人在优秀的大企业里，使用最尖端的设备进行最先进的实验；而我在一个如此破旧、衰败的企业里，连最起码的设备都没有，日复一日地做着混合原料粉末这样简单的工作。&lt;/p&gt;
&lt;p&gt;「一直从事如此单调的工作，究竟能搞出什么科研成果来？」我问自己。再进一步地：「自己的人生将会怎样呢？」想到这些，我不禁心灰意冷，一度过得很消极。&lt;/p&gt;
&lt;p&gt;解除这样的迷惑，一般人的方法是和自己说：要预见到将来。就是说，不要将目光仅仅放在当下，而要从长远角度规划自己的人生蓝图；要把眼前的工作看作这长期规划中的一段过程。&lt;/p&gt;
&lt;p&gt;这也许是合乎逻辑的方法。然而，我采用的方法与此相反——我采用短期的观点来摆正自己对工作的态度。&lt;/p&gt;
&lt;p&gt;🏆「将来会搞出什么样的研究成果」、「自己的人生将会怎样」，我不再痴迷于这些不着边际的远景，而只是留神眼下的事情。就是说，我发誓，今天的目标今天一定要完成。工作的成绩和进度以今天一天为单位区分，然后切实完成。在今天这一天中，最低限度是必须向前跨进一步，今天比昨天，哪怕只是一厘米，也要向前推进。&lt;/p&gt;
&lt;p&gt;我就是这样思考问题的。&lt;/p&gt;
&lt;p&gt;同时，不单单是前进一步，而且要反省今天的工作，以便明天「要做一点改良」「要找一点窍门」。在前进一步时，一定同时是在改善、改进。&lt;/p&gt;
&lt;p&gt;奔着每一天的目标去，让每一天都有所创新，就会天天前进，天天获得积累。为达到目标，不管外面刮风也好、下雨也好，不管碰到多大的困难，我都全神贯注，全力以赴。先是坚持 1 个月，再坚持 1 年，然后是 5 年、 10 年，锲而不舍。这样做下去，你就能踏入当初根本无法想象的境地。&lt;/p&gt;
&lt;p&gt;将今天一天作为「生活的单位」，天天精神抖擞，日复一日，拼命工作，用这种踏实的步伐，就能走上人生的王道。&lt;/p&gt;
&lt;h2&gt;02 取胜之道：全力过好「今天」这一天&lt;/h2&gt;
&lt;p&gt;每天，持续过好内容充实的「今天」这一天，我在经营公司的时候就一直坚持这一点。&lt;/p&gt;
&lt;p&gt;公司创建至今，我们从来不建立长期的经营计划。新闻记者们采访我的时候，经常提出想听一听我们的中长期经营计划。当我回答 「我们从不设立长期的经营计划」 时，他们总觉得不可思议，露出疑惑的神情。&lt;/p&gt;
&lt;p&gt;那么，我们为什么不建立长期计划呢？因为说自己能够预见到久远的将来，这种话基本上都会以「谎言」的结局而告终。&lt;/p&gt;
&lt;p&gt;「多少年后销售额要达到多少，人员增加到多少，设备投资如何如何……」这一类蓝图，不管你怎样着力地描绘，但事实上，超出预想的环境变化、意料之外事态的发生都不可避免地会出现。这时就不得不改变计划，或将计划数字向下调整。有时甚至要无奈地放弃整个计划。&lt;/p&gt;
&lt;p&gt;这样的计划变更如果频繁发生，不管你建立什么计划，员工们都会认为，「反正计划中途就得变更」，他们就会轻视计划，不把它当回事。结果就会降低员工的士气和工作热情。&lt;/p&gt;
&lt;p&gt;同时，目标越是远大，为达此目的，就越需要持续付出不寻常的努力。但是，人们努力，再努力，如果仍然离终点很远很远，他们就难免泄气。「目标虽然没达成，能这样也就可以了，差不多就算了吧！」人们常常在中途泄气了。&lt;/p&gt;
&lt;p&gt;从心理学的角度看，如果达到目标的过程太长，也就是说，设置的目标过于远大，往往在中途就会遭遇挫折。&lt;/p&gt;
&lt;p&gt;与其中途就要作废，不如一开始就不要建立。这是我的观点。自京瓷创业以来，我只用心于建立一年的年度经营计划。3 年、5 年之后的事情，谁也无法准确预测，但是这一年的情况，应该大致能看清，不至于太离谱。&lt;/p&gt;
&lt;p&gt;做年度计划，就要细化成每个月甚至每一天的具体目标，然后千方百计努力达成。&lt;/p&gt;
&lt;p&gt;今天一天努力干吧，以今天一天的勤奋就一定能看清明天。这个月努力干吧，以这一个月的勤奋就一定能看清下个月。今年一年努力干吧，以今年一年的勤奋就一定能看清明年。&lt;/p&gt;
&lt;p&gt;就这样，一瞬间、一瞬间都会过得非常充实，就像跨过一座一座小山。小小的成就连绵不断地积累、无限地持续，这样，乍看宏大高远的目标就一定能实现。这个方法就是最确实的取胜之道。&lt;/p&gt;
&lt;h2&gt;03 别以你现在的能力，限制你对未来的想象&lt;/h2&gt;
&lt;p&gt;在建立目标时，要设定「超过自己能力之上的指标」 💦&lt;/p&gt;
&lt;p&gt;要设定现在自己「不能胜任」的有难度的目标，「我要在未来某个时点实现这个目标」，要下这样的决心。然后，想方设法提高自己的能力，以便在「未来这个时点」实现既定的目标。&lt;/p&gt;
&lt;p&gt;如果只用自己现有的能力来判断决定「能做」还是「不能做」，那么，就不可能挑战新事业，或者实现更高的目标。「现在做不到的事，今后无论如何也要达成」。如果缺乏这种强烈的愿望，就无法开拓新领域，无法达成高目标。&lt;/p&gt;
&lt;p&gt;我用 「能力要用将来进行时」 这句话来表达这一观点。这句话意味着「人具备无限的可能性」。也就是说：人的能力有无限伸展的可能。坚信这一点，面向未来，描绘自己人生的理想。&lt;/p&gt;
&lt;p&gt;这就是我想表达的意思。&lt;/p&gt;
&lt;p&gt;但是，很多人在自己的工作和生活中，很轻率地下结论说:「我不行，做不到」。😔这是因为他们仅以自己现有的能力判断自己「行」还是「不行」。&lt;/p&gt;
&lt;p&gt;这就错了。因为人的能力，在未来，一定会提高，一定会进步。&lt;/p&gt;
&lt;p&gt;事实上，大家今天在做的工作，几年前来看，你也会想：「我不会做，我做不好，无法胜任」。可是到了今天，你不是也觉得这个工作挺简单的？因为你已经驾轻就熟了。&lt;/p&gt;
&lt;p&gt;人这种动物，在各个方面都会进步。「神」就是这么造人的——我们应该这么思考。&lt;/p&gt;
&lt;p&gt;因为我没有学过，没有知识，没有技术，所以我不行。说这话可不行，应该这样思考：因为我没有学过，所以我没有知识，没有技术。但是，我有干劲、有信心，所以明年一定能行。而且就从这一瞬间开始，努力学习，获取知识，掌握技术。将来秘藏在我身上的能力一定能开花结果。我的能力一定能增长。&lt;/p&gt;
&lt;p&gt;对人生抱着消极态度，认为自己的人生就将以碌碌无为而告终，这么思考的年轻人并不多。但是，一旦面临困难的问题时，几乎所有的人都会脱口而出说自己「不行」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;绝对不要说「自己不行」这种话。面对难题，首先要做的就是相信自己。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;现在也许不行，但只要努力一定能行。首先相信自己，然后必须对“自己解决问题的能力怎样才能提高”进行具体深入的思考。只有这样，通向光明未来的大门才会打开。&lt;/p&gt;
&lt;p&gt;/ End / 共勉 🌹&lt;/p&gt;</content:encoded><h:img src="/_astro/202311131709309.C6Pyi_IJ.png"/><enclosure url="/_astro/202311131709309.C6Pyi_IJ.png"/></item><item><title>让你选一句话裱起来，你会选什么？</title><link>https://coooredump.github.io/blog/future-survivor/choose-one-sentence</link><guid isPermaLink="true">https://coooredump.github.io/blog/future-survivor/choose-one-sentence</guid><description>摩尔定律对软件开发也是间接奏效的，每过 18 个月就会有一半的知识会过期。我之前写的有些文章就已经过期了，今天我们来聊一聊那些业界“大佬”是怎么思考的？一个结构化的知识体系是怎样的？</description><pubDate>Thu, 11 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;摩尔定律对软件开发也是间接奏效的，每过 18 个月，就会有一半的知识会过期。我之前写的有些文章就已经过期了，今天我们来聊一个不会那么容易过时的话题 —— 那些业界‘大佬’是怎么思考的？一个结构化的知识体系是怎样的？&lt;/p&gt;
&lt;p&gt;不过要提前说明，本文没那么严肃，仅作抛砖引玉。如果你想要多了解这方面的知识，应该多读几本经典的书籍。&lt;/p&gt;
&lt;h2&gt;从思考框架到基本原则，再到具体最佳实践&lt;/h2&gt;
&lt;p&gt;前些日子学习了 &lt;a href=&quot;https://time.geekbang.org/column/article/73980&quot;&gt;《10x程序员工作法》&lt;/a&gt;、&lt;a href=&quot;https://time.geekbang.org/column/article/163482&quot;&gt;《研发效率破局之道》&lt;/a&gt;，内容本身质量很高自不必说，给我启发较大的是他们的内容组织方式。以 &lt;a href=&quot;https://time.geekbang.org/column/article/73980&quot;&gt;《10x程序员工作法》&lt;/a&gt;为例，它的内容是这样组织的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141753648.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这种设计可以让我们直观地把握课程的主要脉络。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思考框架&lt;/strong&gt;是事物的出发点，用于审视目标、把握方向；&lt;strong&gt;基本原则&lt;/strong&gt;是在思考框架下的核心指导思想；最后在基本原则指导下进行&lt;strong&gt;具体实践&lt;/strong&gt;。后面会详细说明这三者的关系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在我看来，这才是一种结构化的知识体系。这种方法可以帮助你建立一套自己的知识体系、认知模型，还可以用来指导你的行动实践&lt;/strong&gt;。希望本文也可以给你一些启发。&lt;/p&gt;
&lt;h3&gt;🤔思考框架&lt;/h3&gt;
&lt;p&gt;最上层的思考框架往往是一些哲学问题，无非就是保安经常问你的那三个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你是谁？&lt;/li&gt;
&lt;li&gt;从哪来？&lt;/li&gt;
&lt;li&gt;到哪去？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有类似的 &lt;a href=&quot;https://baike.baidu.com/item/wwh/10594463?fr=aladdin&quot;&gt;WWH&lt;/a&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why？ → 目的、理念&lt;/li&gt;
&lt;li&gt;What？→ 定义、概念、现象或成果&lt;/li&gt;
&lt;li&gt;How？ → 具体操作方法、措施&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⭐而《10x 程序员工作法》的思考框架是：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141753911.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;估计这个思考框架你从小学、幼儿园老师就会教你，不用多解释。那么问题来了，大家有没有形成这样的思考习惯呢❓❓❓&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这个思考框架，虽然简单，却可以受益终生💭。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它可以是任何行动的基础。比如在&lt;a href=&quot;https://www.notion.so/b92c170778554d258409d7b6b2de4095&quot;&gt;上一篇文章&lt;/a&gt;中“如何看待新技术章节”  也套用了这个模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是啥玩意？&lt;/li&gt;
&lt;li&gt;解决什么问题？&lt;/li&gt;
&lt;li&gt;怎么解决的？ 思想 →  流程 → 实现&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再比如下次产品经理给你一个需求，套用这个框架，你可以问他：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WHY&lt;/strong&gt; ？为什么要这个做这个功能？ 它可以给用户带来什么价值？ 或者说能给公司带来什么收益？→ 没价值，就没有做的意义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WHAT &amp;#x26; HOW&lt;/strong&gt; ？ 什么样的用户会用到这个功能，他们在什么场景下使用，他们又会怎样使用它？实现这个功能就只有这种方式吗？还有没有其他方案？→ 可以衡量这个功能是否有经过认真思考的，是不是自己 YY，是不是合理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果产品回答不上来，那不好意思，回去等通知吧。&lt;/p&gt;
&lt;p&gt;面试也很可能按照上面的套路考察你的 &apos;要性&apos; (阿里土话，道听途说)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你觉得你现在处于什么水平？有哪些不足&lt;/li&gt;
&lt;li&gt;你的目标是什么？想加入什么样的团队？&lt;/li&gt;
&lt;li&gt;你有什么计划？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OK，这里留一个思考题，如果你的老板在画大饼，你会怎么怼他呢？&lt;/p&gt;
&lt;h3&gt;🧠原则&lt;/h3&gt;
&lt;p&gt;接下来是在思考框架指导下的 ‘原则’。这些原则相比思考框架要具体一些，是针对特定领域的思想指导，在处理某个特定领域的问题时会更有用一些。&lt;/p&gt;
&lt;p&gt;比如《10x 程序员工作法》归纳了四个原则：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141753466.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;因为是付费专栏，所以我也不多剧透，可以看它的&lt;a href=&quot;https://time.geekbang.org/column/article/73980&quot;&gt;导读&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;可以举其他我们比较熟悉的例子，比如面向对象设计的 &lt;a href=&quot;https://zh.wikipedia.org/wiki/SOLID_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1)&quot;&gt;SOLID&lt;/a&gt; 原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;S 单一功能原则&lt;/strong&gt;: 认为 “对象应该仅具有一种单一功能” 的概念&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;O 开闭原则&lt;/strong&gt;: 认为 “软件体应该是对于扩展开放的，但是对于修改封闭的” 的概念。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;L 里氏替换原则&lt;/strong&gt;:  认为 “程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的” 的概念。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I 接口隔离原则&lt;/strong&gt;: 认为 “多个特定客户端接口要好于一个宽泛用途的接口” 的概念&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D 依赖反转原则&lt;/strong&gt;：认为一个方法应该遵从 “依赖于抽象而不是一个实例” 的概念。&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5&quot;&gt;依赖注入&lt;/a&gt;是该原则的一种实现方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;搞对象的人，看到这些原则就会如数家珍，刚入门的小白可能比较难以理解。他们是历代火影燃烧火的意志沉淀下来的宝贝，没有经过战场的洗礼理解可能不会那么深刻。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141753185.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;❤说一个我编程生涯比较受用的原则，那就是 &lt;strong&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E4%B8%80%E6%AC%A1%E4%B8%94%E4%BB%85%E4%B8%80%E6%AC%A1&quot;&gt;DRY&lt;/a&gt; (Don&apos;t repeat yourself)&lt;/strong&gt;，因为它相比 SOLID 原则、&lt;a href=&quot;https://zh.wikipedia.org/wiki/KISS%E5%8E%9F%E5%88%99&quot;&gt;KISS&lt;/a&gt; 原则，更好理解、或者说更有实践性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;DRY 原则简单说就是识别你的重复代码，思考，然后重构它。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你在编程时养成了这种习惯，你会发现你的代码自然而言会有比较良好的结构，同时也可能符合上述各种原则一些特征（实际上它们本来就是交叉和相通的）。&lt;/p&gt;
&lt;p&gt;或者说，经过 DRY 原则下的刻意训练，你会形成一种编程品味（敲黑板，这也是大厂考点）。&lt;/p&gt;
&lt;p&gt;⭐类似思想/原则还有很多，比如 &lt;a href=&quot;https://zh.wikipedia.org/wiki/Unix%E5%93%B2%E5%AD%A6&quot;&gt;Unix 哲学&lt;/a&gt; 、Windows 哲学，还有一些&lt;a href=&quot;https://zhuanlan.zhihu.com/p/73144439&quot;&gt;算法思想&lt;/a&gt;：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;算法思想&lt;/strong&gt;：源于 https://zhuanlan.zhihu.com/p/73144439&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141753375.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unix 哲学&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;下图有两个彩蛋：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个是 Ken Thompson (Unix、Go 作者之一，真大神级人物) 非常实用的 &quot;建议&quot;： &lt;strong&gt;“拿不准就穷举”,  干就是了，干了再说&lt;/strong&gt;。&lt;br&gt;
Unix 哲学用一个词概括就是 KISS (Keep It Simple, Stupid)，在这个 &apos;面试造火箭，上班拧螺丝钉&apos; 内卷年代,   很多人容易走偏，把事情复杂化，写一些花里胡哨代码，做一些花里胡哨的功能。Unix 的远古哲学告诫我们：&lt;strong&gt;简洁就是好，好就是简洁&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;第二个是我自己写的，尽管大学时就看过这本书，当时只有盲目崇拜，时隔多年再看，这些原则个个是说到心尖上了，顿时感叹应该把这些 &apos;哲学&apos; 裱起来，日看夜看。&lt;strong&gt;这也是本文的灵感来源。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141753121.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows 哲学&lt;/strong&gt;：小事重启、大事重装。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141753944.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这些原则你说难吗？其实不难，几句话就可以说清楚。&lt;/p&gt;
&lt;p&gt;如果你是多年的老鸟，可以让你返璞归真。如果你是菜鸟，那你应该背诵下来 (开玩笑)，或者裱起来挂客厅、搞成壁纸、做成鼠标垫、印在保温瓶上...&lt;/p&gt;
&lt;h3&gt;💪具体的最佳实践&lt;/h3&gt;
&lt;p&gt;再往下更具体，这是在原则指导下、经过实践总结出来的最佳实践/设计模式。可以用于指导解决具体的领域问题。&lt;/p&gt;
&lt;p&gt;还是以 《10x 程序员工作法》为例，它最终的知识结构如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141753952.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;举大家比较熟悉的例子，最典型的莫属于面向对象的&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_(%E8%AE%A1%E7%AE%97%E6%9C%BA)&quot;&gt;《设计模式》&lt;/a&gt;，它就是属于这个层次的知识：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141754579.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;设计模式就是在 SOLID 原则指导下的具体实践。&lt;/p&gt;
&lt;p&gt;并不是说我们只要学习思考框架和指导原则就行了，最佳实践也是要刻意学习的。&lt;strong&gt;三个层次相得益彰，这样形成的知识体系才是比较稳固的&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141754827.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最佳实践是在思考框架和指导原则下形成的产物。
&lt;ul&gt;
&lt;li&gt;如果只是掌握最佳实践，停留在皮毛，不去挖掘它的内在思想，则不能做到内化和升华。&lt;/li&gt;
&lt;li&gt;如果有幸，来到了一个新大陆，这里没有任何最佳实践和设计模式，要怎么办呢？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;思想和原则不能脱离实践。
&lt;ul&gt;
&lt;li&gt;最佳实践通常是别人实践总结出来的， 能复用的就复用是吧？最佳实践、准则、对我们来说是站在巨人的肩膀上，是捷径，让我们可以少走点弯路。&lt;/li&gt;
&lt;li&gt;由于每个人的场景千差万别，别人的实践并不一定适合你， 或者你走在世界前头，上层的思想则是创造最佳实践的有用指导。&lt;/li&gt;
&lt;li&gt;另外实践也在深化我们对上层思想的理解。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实践和思想是相互验证的关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后面大家看书、学习某些课程时，可以留意一下它们的组织结构，某种程度上可以折射出作者的水平。&lt;/p&gt;
&lt;p&gt;一切都是套路，套了又套。&lt;/p&gt;
&lt;h2&gt;那么，让你选一句话裱起来，你会选什么？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;来点真实的，软件开发领域选一句让你受益匪浅的话，裱起来告诫自己/后人，你会选什么?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我举一些例子吧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KISS&lt;/li&gt;
&lt;li&gt;DRY&lt;/li&gt;
&lt;li&gt;SOLID&lt;/li&gt;
&lt;li&gt;SPOT&lt;/li&gt;
&lt;li&gt;Unix 哲学 17 大原则&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%8B%89%E9%87%8C%C2%B7%E6%B2%83%E5%B0%94#%E7%A8%8B%E5%BA%8F%E5%91%98%E7%BE%8E%E5%BE%B7&quot;&gt;程序员的三大美德&lt;/a&gt;：懒惰、急躁、傲慢。—— Larry Wall
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;懒惰&lt;/strong&gt;，是一种品质，它会使你花很大力气去规避过度的精力消耗，敦促你写出节省体力的程序，别人也能很好地利用，你还会为此写出完善的文档，以免别人来问问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;急躁&lt;/strong&gt;，是计算机偷懒时，你会感到的一种愤怒。它会促使你写出超越预期的程序，而不只是响应需求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;傲慢&lt;/strong&gt;，极度自信，写出（或维护）别人挑不出毛病的程序。
不是开玩笑，这真是美德。如果身边多几个这样的程序员，就不用 996 了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Stay hungry,  Stay foolish —— Steve Jobs&lt;/li&gt;
&lt;li&gt;Talk is cheap. Show me the code —— Linus Torvalds&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/202311141752451.CbNuSxZV.jpeg"/><enclosure url="/_astro/202311141752451.CbNuSxZV.jpeg"/></item><item><title>学好算法，有三重境界</title><link>https://coooredump.github.io/blog/leetcode/advanced-algorithms</link><guid isPermaLink="true">https://coooredump.github.io/blog/leetcode/advanced-algorithms</guid><description>在最初的阶段，算法世界的大门刚刚打开，这个时候迷茫是正常的，解决迷茫的要诀在少想多做。怀着一颗 &quot;千磨万击还坚韧，任尔东西南北风&quot; 的恒心，爬上算法的高楼，做到 &quot;望尽天涯路&quot;。</description><pubDate>Thu, 11 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;王国维先生在《人间词话》中写道：“古今之成大事业、大学问者，必经过三种境界：&lt;/p&gt;
&lt;p&gt;‘&lt;em&gt;昨夜西风凋碧树。独上高楼，望尽天涯路&lt;/em&gt;。’ 此第一境也。&lt;br&gt;
‘&lt;em&gt;衣带渐宽终不悔，为伊消得人憔悴&lt;/em&gt;。’ 此第二境也。&lt;br&gt;
‘&lt;em&gt;众里寻他千百度，蓦然回首，那人却在，灯火阑珊处&lt;/em&gt;。‘ 此第三境也。”&lt;/p&gt;
&lt;p&gt;算法的学习之道也是如此。&lt;/p&gt;
&lt;h2&gt;夯实根基&lt;/h2&gt;
&lt;p&gt;在最初的阶段，算法世界的大门刚刚打开，这个时候迷茫是正常的，解决迷茫的要诀在于&lt;strong&gt;少想多做&lt;/strong&gt;，勇往直前。怀着一颗 &quot;千磨万击还坚韧，任尔东西南北风&quot; 的恒心，爬上算法的高楼，做到 &quot;望尽天涯路&quot;。&lt;/p&gt;
&lt;p&gt;从一个算法萌新入门，第一步便在于打牢根基。推荐阅读书籍：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;《算法第 4 版》&lt;/strong&gt;- Robert Sedgewick&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;《大话数据结构》&lt;/strong&gt;- &lt;em&gt;程杰&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;《算法图解》&lt;/strong&gt;- &lt;em&gt;Aditya Bhargava&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;《算法导论》&lt;/strong&gt;- &lt;em&gt;Cormen,T.H.&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;⭐**《算法第 4 版》&lt;strong&gt;适合初学者入门。&lt;br&gt;
⭐&lt;/strong&gt;《大话数据结构》&lt;strong&gt;和&lt;/strong&gt;《算法图解》&lt;strong&gt;这两本书的特点是有趣、易理解，也非常适合初学者。&lt;br&gt;
⭐&lt;/strong&gt;《算法导论》**的特点是全面，它是一本算法的百科全书，着重在于开阔算法视野，适合有一定算法基础后再去学习。&lt;/p&gt;
&lt;p&gt;入门阶段是看一些天赋的，花费时间因人而异，大约在 3～6 月之间，&lt;strong&gt;将上述提到的书籍选择其中一本看完基本就能入门了&lt;/strong&gt;。在这个阶段中，需要了解几类常用的算法：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501222248005.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;其中，&lt;strong&gt;暴力枚举、贪心算法容易&lt;/strong&gt;理解，可以很快上手。&lt;strong&gt;数论&lt;/strong&gt;相关的算法需要用到一些数学技巧，包括位运算、幂函数、求模等等性质。&lt;strong&gt;二分算法&lt;/strong&gt;和&lt;strong&gt;深度优先搜索算法&lt;/strong&gt;相对有些技巧性，好在他们都有固定的模板。另外，不得不提的是，深度优先搜索算法的思想非常重要，而且&lt;strong&gt;深度优先搜索是动态规划、分治和回溯的基础&lt;/strong&gt;，需要重点掌握。&lt;/p&gt;
&lt;p&gt;🌹在此过程中，可以辅以力扣（LeetCode）中的&lt;strong&gt;简单题目&lt;/strong&gt;，它们往往都代表了一类经典算法，如：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/climbing-stairs/&quot;&gt;70. 爬楼梯&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;假设你正在爬楼梯。需要 &lt;em&gt;n&lt;/em&gt; 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🏆&lt;strong&gt;动态规划&lt;/strong&gt; 算法的经典题目，通过此题目可以了解状态、边界条件、状态转移方程等基本概念。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/path-sum/&quot;&gt;112. 路径总和&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个二叉树和一个目标和，判断该树中是否存在根节点到叶子节点的路径，这条路径上所有节点值相加等于目标和。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🏆&lt;strong&gt;深度优先算法&lt;/strong&gt; 的入门题目，递归实现和迭代实现都不难，可以学习到深度优先算法的层层嵌套搜索、找到答案或到达边界停止的基本解题思路。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/search-insert-position/&quot;&gt;35. 搜索插入位置&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个排序数组和一个目标值，在数组中找到目标值，并返回其索引。如果目标值不存在于数组中，返回它将会被按顺序插入的位置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🏆&lt;strong&gt;二分算法&lt;/strong&gt; 的典型题目，使用二分算法的解题模板可以轻松解决，二分算法的算法思想清晰明确，一通百通。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/majority-element/&quot;&gt;169. 求众数&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个大小为 &lt;em&gt;n&lt;/em&gt; 的数组，找到其中的众数。众数是指在数组中出现次数&lt;strong&gt;大于&lt;/strong&gt; &lt;code&gt;⌊ n/2 ⌋&lt;/code&gt; 的元素。
你可以假设数组是非空的，并且给定的数组总是存在众数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🏆&lt;strong&gt;分治算法&lt;/strong&gt; 的简单题目，如果我们知道数组左边一半和右边一半的众数，我们就可以用线性时间知道全局的众数是哪个。这道题妙就妙在可以有多种解题方式，让初学者至少可以写出暴力枚举算法 AC 题目，然后再逐步深入，优化算法。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/delete-columns-to-make-sorted/&quot;&gt;944. 删列造序&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定由 N 个小写字母字符串组成的数组 A，其中每个字符串长度相等。
选取一个删除索引序列，对于 A 中的每个字符串，删除对应每个索引处的字符。 所余下的字符串行从上往下读形成列。
假设，我们选择了一组删除索引 &lt;code&gt;D&lt;/code&gt;，那么在执行删除操作之后，&lt;code&gt;A&lt;/code&gt; 中所剩余的每一列都必须是 &lt;strong&gt;非降序&lt;/strong&gt; 排列的，然后请你返回 &lt;code&gt;D.length&lt;/code&gt; 的最小可能值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🏆这是一道 &lt;strong&gt;贪心算法&lt;/strong&gt; 的简单题目，贪心算法理解简单，上手容易，适合作为初学者掌握的第一个算法。&lt;/p&gt;
&lt;h2&gt;融会贯通&lt;/h2&gt;
&lt;p&gt;学习算法理论如同阅读了一本武功秘籍，然而仅仅掌握理论是不够的，接下来就要进入到实际练习阶段。&lt;/p&gt;
&lt;p&gt;实战练习非常重要，不经过实战练习，理论仅仅是纸上谈兵。比如，不经过大量练习，永远不会知道二分算法是多么容易出现死循环。一个边界条件控制不好，程序就会显示无情的&quot;Time Limit Exceeded&quot;。在 20 分钟的调试后，或许仅仅是将 &lt;code&gt;while (left &amp;#x3C;= right)&lt;/code&gt; 改为了 &lt;code&gt;while (left &amp;#x3C; right)&lt;/code&gt; 。&lt;strong&gt;程序员说到底也是手艺人，这一个字符的改动，正是&quot;台上一分钟，台下十年功&quot;的体现，需要在大量的练习中才能理解两者之间的不同作用。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;再比如，动态规划算法中，递归的函数就像是《盗梦空间》中的&quot;梦中梦&quot;，一层套一层，又渐次展开，很难整体把控。&lt;strong&gt;在不断的练习后，才会知道，动态规划算法的重点是抓住动态转移方程，只处理两个状态之间的过渡和边界条件，慢慢&quot;大事化小，小事化了&quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;这一阶段花费的时间将会很长很长，伴随着不断地摔倒、爬起，你会对每类算法逐渐融会贯通。好在&lt;strong&gt;这一阶段是不看天赋只看勤奋&lt;/strong&gt;的，每次从坑里爬起，都是献给成长的一份力量。
推荐的进阶书籍有**《编程珠玑》**，本书探讨了程序设计人员面对一系列的实际问题以及解决问题的措施（解决方案的代码以 C/C++ 语言编写）。书中选取了许多具有典型意义的复杂编程和算法问题，并阐述和总结了许多独特精妙的设计原则、思考和解决问题的方法以及实用的程序设计技巧。&lt;/p&gt;
&lt;p&gt;🌹在这个阶段，可以尝试练习力扣上的&lt;strong&gt;中等题目&lt;/strong&gt;，中等题目基本上也只会使用一种算法，加上一些特殊的限制，好比让你在学习了直拳的理论后衍生出左勾拳和右勾拳。推荐练习题目有：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/longest-string-chain/&quot;&gt;1048. 最长字符串链&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给出一个单词列表，其中每个单词都由小写英文字母组成。
如果我们可以在 word1 的任何地方添加一个字母使其变成 word2，那么我们认为 word1 是 word2 的前身。例如，&quot;abc&quot; 是 &quot;abac&quot; 的前身。
词链是单词 [word_1, word_2, ..., word_k] 组成的序列，k &gt;= 1，其中 word_1 是 word_2 的前身，word_2 是 word_3 的前身，依此类推。
从给定单词列表 words 中选择单词组成词链，返回词链的最长可能长度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🏆分析题目可知，要求出答案必须遍历所有可能的词链，动态规划算法在其中起备忘录的作用，用于记录已经算过的答案，减少计算次数。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/permutations-ii/&quot;&gt;47. 全排列 II&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个可包含重复数字的序列，返回所有不重复的全排列。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🏆这道题是 &lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/permutations/&quot;&gt;46. 全排列&lt;/a&gt;&lt;/strong&gt; 的加强版，全排列 I 的题目是：给定一个 &lt;strong&gt;没有重复&lt;/strong&gt; 数字的序列，返回其所有可能的全排列。使用深度优先搜索算法即可解决。本题在其基础上加强了难度，有两种方法可解。第一种方法最简单，直接用全排列 I 的答案去重即可，第二种方法是先将数组排序，全排列时遇到重复数字则跳过，这样的剪枝优化可以减少遍历次数，提高算法效率。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/combination-sum-ii/&quot;&gt;40. 组合总和 II&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个数组 candidates 和一个目标数 target ，找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🏆深度优先搜索算法衍生出来的回溯算法，同样用到 47 题的剪枝优化思想：相同数字只允许递归第一个。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/gray-code/&quot;&gt;89. 格雷编码&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;格雷编码是一个二进制数字系统，在该系统中，两个连续的数值仅有一个位数的差异。
给定一个代表编码总位数的非负整数 n，打印其格雷编码序列。格雷编码序列必须以 0 开头。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🏆&lt;strong&gt;动态规划&lt;/strong&gt; 算法的实际应用之一。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/word-search/&quot;&gt;79. 单词搜索&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个二维网格和一个单词，找出该单词是否存在于网格中。
单词必须按照字母顺序，通过相邻的单元格内的字母构成，其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🏆深度优先搜索的中级应用，使用单独数组标记已使用过的元素，这也是 DFS 中较为常见的做法，难点在于将标记数组复原的时机，需要反复练习，熟练掌握。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;🌹当你把每一类算法的中等题目刷起来得心应手时，不妨开始尝试&lt;strong&gt;困难题目&lt;/strong&gt;的练习。困难题目总是融合两种或两种以上算法，或是加深难度的经典算法，如二维甚至三维动态规划。练习困难题目好比同时用上左勾拳和扫堂腿，不仅让思维酣畅淋漓，在每次 AC 之后还会带来无与伦比的成就感。推荐练习题目有：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/24-game/&quot;&gt;679. 24 点游戏&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你有 4 张写有 1 到 9 数字的牌。你需要判断是否能通过 &lt;code&gt;*&lt;/code&gt;，&lt;code&gt;/&lt;/code&gt;，&lt;code&gt;+&lt;/code&gt;，&lt;code&gt;-&lt;/code&gt;，&lt;code&gt;(&lt;/code&gt;，&lt;code&gt;)&lt;/code&gt; 的运算得到 24。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🧠只有 4 张牌，且只能执行 4 种操作。即使所有运算符都不进行交换，最多也只有 12 * 6 * 2 * 4 * 4 * 4 = 9216 种可能性，这使得我们可以尝试所有这些可能，如果用深度优先搜索算法则需要费一番功夫。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/&quot;&gt;124. 二叉树中的最大路径和&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个&lt;strong&gt;非空&lt;/strong&gt;二叉树，返回其最大路径和。
本题中，路径被定义为一条从树中任意节点出发，达到任意节点的序列。该路径&lt;strong&gt;至少包含一个&lt;/strong&gt;节点，且不一定经过根节点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🧠首先，考虑实现一个简化的函数：计算每个节点及其子树对路径和的最大贡献。再考虑第二点：最大路径不一定包括根节点。这意味着我们在每一步都检查哪种选择更好：是继续当前路径或者以当前节点作为最高节点计算新的路径。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/split-array-largest-sum/&quot;&gt;410. 分割数组的最大值&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个非负整数数组和一个整数 &lt;em&gt;m&lt;/em&gt;，你需要将这个数组分成 &lt;em&gt;m&lt;/em&gt; 个非空的连续子数组。设计一个算法使得这 &lt;em&gt;m&lt;/em&gt; 个子数组各自和的最大值最小。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🧠二分算法和贪心算法的综合练习，仔细分析可知其单调关系：数组和的最大值越小，分组数越大。并且数组和的范围是可以确定的。根据此特性，可以将题目转换为：当子数组的和最大为 maxSum 时，至少需要分多少组，能否在最多 m 组的限制范围内完成分割。在每次分割时，采用贪心策略，尽可能多的放置元素，直到一组放不下，再另起一组。如果满足分割条件，记录当前值，利用二分法，缩小子数组总和。否则扩大子数组总和，直到找到最佳答案。&lt;/p&gt;
&lt;h2&gt;推陈出新&lt;/h2&gt;
&lt;p&gt;事实上，大量程序员停留在第二重境界就无法再进一步。当提到某一类算法时，你可以说：&quot;我知道&quot;、&quot;我会用&quot;、&quot;踩过坑&quot;，但能说出&quot;&lt;strong&gt;我完全理解其思想&lt;/strong&gt;&quot;、甚至&quot;&lt;strong&gt;我能想办法改进&lt;/strong&gt;&quot;的人却很少很少。这一步仿佛武学中的攻守之道，当你掌握到这一层，便可不再拘泥于一刀一剑、一招一式，如金书中所说：飞花摘叶皆可伤人、草木竹石均可为剑。&lt;/p&gt;
&lt;p&gt;开创算法的过程是艰难又孤独的。每一个经典算法的诞生都伴随着&quot;一将功成万骨枯&quot;。比如现在我们在很多语言中都可以直接调用&lt;code&gt;Collection.sort()&lt;/code&gt;实现快速排序，而在快速排序算法出现之前，曾有一段时间仅有冒泡、选择、插入三种排序算法。直到1959年，希尔提出&quot;希尔排序&quot;算法，或许现在知道此算法的人已经很少了。但它是首个突破了复杂度的排序算法，它的基本算法思想如下：&lt;/p&gt;
&lt;p&gt;选择一个增量序列t1，t2，…，tk，其中 ti &gt; tj， tk = 1； 按增量序列个数k，对序列进行k 趟排序； 每趟排序，根据对应的增量ti，将待排序列分割成若干长度为 m 的子序列，分别对各子表进行直接插入排序。仅增量因子为1 时，整个序列作为一个表来处理，表长度即为整个序列的长度。&lt;/p&gt;
&lt;p&gt;希尔排序算法较为晦涩难懂，而且并不是最优的排序算法，现在已经被后来的快速排序算法给淘汰了。然而不可否认希尔对排序算法的演进具有开创性贡献，在攀越算法高峰的路上，每一步都走得战战兢兢，我们只有铭记这些伟大的引路人，以此激励自己不断前行。&lt;/p&gt;
&lt;p&gt;💖&lt;strong&gt;算法世界不尽完美。不仅有经典算法在前奠基，后起之秀遗传算法、深度学习算法也熠熠生辉。算法世界还有许多&quot;所罗门王的宝藏&quot;，一直静静地守候在&quot;灯火阑珊处&quot;，等待着人们去发掘。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;学习方法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202501222247946.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在网上有很多资源、博客、论坛可供我们更方便地学习知识片段。然而这种类似兵来将挡、水来土掩般的学习方法虽然有用，却并不特别的好。这里推荐大家在网上寻找一些系统的学习教程，以帮助自己由浅入深，一路成长。&lt;/p&gt;
&lt;p&gt;算法学习之道非一日之功，在技术提升的路上，力扣会一直助你前行。&lt;/p&gt;</content:encoded><h:img src="/_astro/202501222250241.DZR5C6xB.jpeg"/><enclosure url="/_astro/202501222250241.DZR5C6xB.jpeg"/></item><item><title>Git 的撤销操作</title><link>https://coooredump.github.io/blog/productivity-tool/git-undo</link><guid isPermaLink="true">https://coooredump.github.io/blog/productivity-tool/git-undo</guid><description>Git 版本管理时，往往需要撤销某些操作，本文介绍几种最主要的情况，给出详细的解释。</description><pubDate>Sat, 23 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文介绍几种最主要的情况，给出详细的解释。更多的命令可以参考&lt;a href=&quot;https://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html&quot;&gt;《常用 Git 命令清单》&lt;/a&gt;一文。&lt;/p&gt;
&lt;h2&gt;1. 撤销提交&lt;/h2&gt;
&lt;p&gt;一种常见的场景是，提交代码以后，你突然意识到这个提交有问题，应该撤销掉，这时执行下面的命令就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git revert HEAD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面命令的原理是，在当前提交后面，&lt;strong&gt;新增一次提交(commit+1)，抵消掉上一次提交导致的所有变化(workspace&amp;#x26;stage change)&lt;/strong&gt;。它&lt;strong&gt;不会改变过去的历史&lt;/strong&gt;，所以是首选方式，&lt;strong&gt;没有任何丢失代码的风险&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git revert&lt;/code&gt; 命令只能抵消上一个提交，如果想抵消多个提交，必须在命令行依次指定这些提交。比如，抵消前两个提交，要像下面这样写。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git revert [倒数第一个提交] [倒数第二个提交]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git revert&lt;/code&gt; 命令还有两个参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--no-edit&lt;/code&gt;：执行时不打开默认编辑器，直接使用 Git 自动生成的提交信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--no-commit&lt;/code&gt;：&lt;strong&gt;只抵消暂存区(stage)和工作区的文件变化，不产生新的提交&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141652573.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141652621.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;2. 丢弃提交&lt;/h2&gt;
&lt;p&gt;如果希望以前的提交在历史中彻底消失，而不是被抵消掉，可以使用&lt;code&gt;git reset&lt;/code&gt;命令，丢弃掉某个提交之后的所有提交。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git reset [last good SHA]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git reset&lt;/code&gt;的原理是，让最新提交的指针回到以前某个时点，该时点之后的提交都从历史中消失。&lt;/p&gt;
&lt;p&gt;默认情况下，&lt;code&gt;git reset&lt;/code&gt;不改变工作区的文件（但会改变暂存区），&lt;code&gt;--hard&lt;/code&gt; 参数可以让工作区里面的文件也回到以前的状态。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git reset --hard [last good SHA]
# 或者
$ git reset --hard HEAD^1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;执行 &lt;code&gt;git reset&lt;/code&gt; 命令之后，如果想找回那些丢弃掉的提交，可以使用 &lt;code&gt;git reflog&lt;/code&gt; 命令&lt;/strong&gt;，具体做法参考&lt;a href=&quot;https://github.blog/2015-06-08-how-to-undo-almost-anything-with-git/#redo-after-undo-local&quot;&gt;这里&lt;/a&gt;。不过，这种做法有&lt;strong&gt;时效性&lt;/strong&gt;，时间长了可能找不回来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141652591.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3. 替换上一次提交&lt;/h2&gt;
&lt;p&gt;提交以后，发现提交信息写错了，这时可以使用 &lt;code&gt;git commit&lt;/code&gt; 命令的 &lt;code&gt;--amend&lt;/code&gt; 参数，可以修改上一次的提交信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git commit --amend -m &quot;Fixes bug #42&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⭐它的原理是产生一个新的提交对象，替换掉上一次提交产生的提交对象。&lt;strong&gt;这时如果暂存区有发生变化的文件，会一起提交到仓库。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以，&lt;code&gt;--amend&lt;/code&gt; 不仅可以修改提交信息，还可以整个把上一次提交替换掉。&lt;/p&gt;
&lt;h2&gt;4. 撤销工作区的文件修改&lt;/h2&gt;
&lt;p&gt;如果工作区的某个文件被改乱了，但还没有 执行&lt;code&gt;git add&lt;/code&gt;，可以用 &lt;code&gt;git checkout&lt;/code&gt; 命令&lt;strong&gt;找回本次修改之前的文件&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git checkout -- [filename]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⭐它的原理是先找暂存区，如果该文件有暂存的版本，则恢复该版本，否则恢复上一次提交的版本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：工作区的文件变化一旦被撤销，就无法找回了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141652377.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;5. 从暂存区撤销文件&lt;/h2&gt;
&lt;p&gt;如果不小心把一个文件添加到暂存区，可以用下面的命令撤销。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git rm --cached [filename]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的命令不影响已经提交的内容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141652947.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;6. 撤销当前分支的变化&lt;/h2&gt;
&lt;p&gt;你在当前分支上做了几次提交，突然发现放错了分支，这几个提交本应该放到另一个分支。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 新建一个 feature 分支，指向当前最新的提交
# 注意，这时依然停留在当前分支
$ git branch feature

# 切换到这几次提交之前的状态
$ git reset --hard [当前分支此前的最后一次提交]

# 切换到 feature 分支
$ git checkout feature
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的操作等于是撤销当前分支的变化，将这些变化放到一个新建的分支。&lt;/p&gt;
&lt;p&gt;⭐也就是在当前 commit 位置（的&lt;strong&gt;另一个分支&lt;/strong&gt; feature 上）建立一个锚点保存最近几次 &lt;em&gt;commits&lt;/em&gt;，并且在&lt;strong&gt;本分支&lt;/strong&gt; master 回溯到前几次 commit 记录的位置！这样可以有效撤销当前分支的提交，还能将 &lt;em&gt;commits&lt;/em&gt; 转移到另一分支。&lt;/p&gt;
&lt;p&gt;其实也可以用 &lt;code&gt;git cherry-pick&lt;/code&gt; 实现！&lt;/p&gt;
&lt;p&gt;见博客：https://ruanyifeng.com/blog/2020/04/git-cherry-pick.html&lt;/p&gt;
&lt;p&gt;（完）&lt;/p&gt;</content:encoded><h:img src="/_astro/202311150150548.Ddg3RA3X.jpg"/><enclosure url="/_astro/202311150150548.Ddg3RA3X.jpg"/></item><item><title>探秘 .git 文件夹，理解 git 运作机制</title><link>https://coooredump.github.io/blog/productivity-tool/understanding-git-folder</link><guid isPermaLink="true">https://coooredump.github.io/blog/productivity-tool/understanding-git-folder</guid><description>近期需要给 git 仓库制作一个 commit-msg 钩子，进入 .git/hooks 文件夹正准备干活，突然想知道其它 git hooks 都是干啥的？.git 文件夹里面那么多文件，又都是干什么的呢？想要 `git` 进阶，了解 `.git` 文件夹也是最佳切入点，关于 `git` 运作机制的线索都可以在这里找到。</description><pubDate>Fri, 22 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. &lt;code&gt;.git&lt;/code&gt; 文件夹创建&lt;/h2&gt;
&lt;p&gt;任意文件夹中，用 &lt;code&gt;git init&lt;/code&gt; 命令初始化仓库，即可在此文件夹下创建 &lt;code&gt;.git&lt;/code&gt; 文件夹（&lt;code&gt;.&lt;/code&gt;打头为隐藏文件夹，所以平时可能看不到）。这个文件夹之外的部分叫做工作区（Working Directory），&lt;code&gt;.git&lt;/code&gt; 文件夹我们称做 Git 仓库 (Git Repository)。&lt;/p&gt;
&lt;p&gt;如果出于某种原因，想要重新来过，&lt;code&gt;rm -rf .git &amp;#x26;&amp;#x26; git init&lt;/code&gt;，此仓库的 git 记录会归零！（&lt;strong&gt;提醒：慎用！！！&lt;/strong&gt;）&lt;/p&gt;
&lt;h2&gt;2. &lt;code&gt;.git&lt;/code&gt; 结构&lt;/h2&gt;
&lt;p&gt;随便初始化一个仓库，&lt;code&gt;git init temp&lt;/code&gt;，运行 &lt;code&gt;cd temp &amp;#x26;&amp;#x26; ls -F1 .git&lt;/code&gt;，可以看到基本的 &lt;code&gt;.git&lt;/code&gt; 目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HEAD
config
description
hooks/
info/
objects/
refs/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这里面没有实质性内容，研究意义不大。我们找一个有过几次提交的仓库，运行 &lt;code&gt;ls -F1 .git&lt;/code&gt; 可以看到更丰富的 &lt;code&gt;.git&lt;/code&gt; 目录结构（通常会有 7 个文件 5 个目录）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;COMMIT_EDITMSG
HEAD
ORIG_HEAD
FETCH_HEAD
config
description
index
hooks/
info/
logs/
objects/
refs/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141635995.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重要：动手之前，请做好整个仓库的备份！！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重要：动手之前，请做好整个仓库的备份！！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重要：动手之前，请做好整个仓库的备份！！！&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2.1. 文件 COMMIT_EDITMSG&lt;/h3&gt;
&lt;p&gt;此文件是一个临时文件，&lt;strong&gt;存储最后一次提交的信息内容&lt;/strong&gt;，&lt;code&gt;git commit&lt;/code&gt; 命令之后打开的编辑器就是在编辑此文件，而你退出编辑器后，&lt;code&gt;git&lt;/code&gt; 会把此文件内容写入 commit 记录。&lt;/p&gt;
&lt;p&gt;⭐&lt;strong&gt;实际应用&lt;/strong&gt;： &lt;code&gt;git pull&lt;/code&gt; 远程仓库后，新增了很多提交，淹没了本地提交记录，直接 &lt;code&gt;cat .git/COMMIT_EDITMSG&lt;/code&gt; 就可以弄清楚最后工作的位置了，是不是很实用？&lt;/p&gt;
&lt;h3&gt;2.2 文件 HEAD&lt;/h3&gt;
&lt;p&gt;此文件&lt;strong&gt;永远&lt;/strong&gt;存储当前位置指针，就像 linux 中的 &lt;code&gt;$PWD&lt;/code&gt; 变量和命令提示符的箭头一样，永远指向当前位置，表明当前的工作位置。在 &lt;code&gt;git&lt;/code&gt; 中 &lt;code&gt;HEAD&lt;/code&gt; 永远指向当前正在工作的那个 &lt;code&gt;commit&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;分支 HEAD&lt;/h4&gt;
&lt;p&gt;HEAD 存储一个分支的 &lt;strong&gt;ref&lt;/strong&gt;，运行：&lt;code&gt;cat .git/HEAD&lt;/code&gt; 通常会显示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ref: refs/heads/master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141635306.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这说明你目前正在 &lt;code&gt;master&lt;/code&gt; 分支工作。此时你的任何 commit，默认自动附加到 &lt;code&gt;master&lt;/code&gt; 分支之上。&lt;/p&gt;
&lt;p&gt;执行 &lt;code&gt;git cat-file -p HEAD&lt;/code&gt;, 显示详细的提交信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git cat-file -p HEAD
tree 95a4c1cd778ad62586c47afc06d2a1b5dff1bdec
parent bfdec30d39951b49fa8964863bd801058878f3b2
author Wu-Yikun &amp;#x3C;577159462@qq.com&gt; 1634876894 +0800
committer Wu-Yikun &amp;#x3C;577159462@qq.com&gt; 1634876894 +0800

?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141635588.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;孤立 HEAD&lt;/h4&gt;
&lt;p&gt;HEAD 不关联任何分支，只指向某个 commit，运行 &lt;code&gt;git checkout bfdec30d&lt;/code&gt;，你会看到如下信息：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You are in &apos;detached HEAD&apos; state. You can look around, make experimental&lt;br&gt;
changes and commit them, and you can discard any commits you make in this&lt;br&gt;
state without impacting any branches by performing another checkout.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141635138.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;相信很多人一开始使用 git 都会对这段信息头大，其实它只是告诉你 &lt;code&gt;HEAD&lt;/code&gt; 这个文件中存储的信息已不再是一个分支信息，运行：&lt;code&gt;cat .git/HEAD&lt;/code&gt;，看到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ cat .git/HEAD
bfdec30d39951b49fa8964863bd801058878f3b2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141636474.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;看到区别了吗？HEAD 指向一个40字符的 SHA-1 提交记录，&lt;strong&gt;git 已经不知道你在哪个分支工作了，所以你如果生成新的 commit，git 不知道往哪里 &lt;code&gt;push&lt;/code&gt;，你只能做些实验性代码自嗨一把，无法影响到任何分支，也无法与人协同&lt;/strong&gt;。这就是所谓的 &lt;strong&gt;&lt;code&gt;&apos;detached HEAD&apos; state&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141636088.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;(由分支HEAD 变为 孤立HEAD)&lt;/p&gt;
&lt;p&gt;关于 &lt;code&gt;HEAD&lt;/code&gt; 的用法示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git push origin HEAD
$ git checkout HEAD~1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 文件 ORIG_HEAD&lt;/h3&gt;
&lt;p&gt;正因为 &lt;code&gt;HEAD&lt;/code&gt; 比较重要，此文件会在你进行&lt;strong&gt;危险操作&lt;/strong&gt;时备份 &lt;code&gt;HEAD&lt;/code&gt;，如以下操作时会&lt;strong&gt;触发备份&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git reset
$ git merge
$ git rebase
$ git pull
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此文件的应用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 回滚到上一次的状态(慎用!!!)
$ git reset --hard ORIG_HEAD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 文件 FETCH_HEAD&lt;/h3&gt;
&lt;p&gt;这个文件作用在于追踪远程分支的拉取与合并，与其相关的命令有 &lt;code&gt;git pull/fetch/merge&lt;/code&gt;，&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;git pull&lt;/code&gt; 命令相当于执行以下两条命令 (&lt;code&gt;pull&lt;/code&gt; = &lt;code&gt;fetch&lt;/code&gt; &amp;#x26; &lt;code&gt;merge&lt;/code&gt;)：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git fetch
$ git merge FETCH_HEAD

# 显示如下&gt;&gt;&gt;
From https://github.com/xxx/xxxx
* branch            master     -&gt; FETCH_HEAD
Updating f785638..59db1b2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;并且，此时会默默备份 &lt;code&gt;HEAD&lt;/code&gt; 到 &lt;code&gt;ORIG_HEAD&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;看看 &lt;code&gt;FETCH_HEAD&lt;/code&gt; 里面有什么内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ cat .git/FETCH_HEAD
848d7701250d5fee1449c5355158f629f6564484        branch &apos;master&apos; of https://github.com/xxxx/xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最前面是 hash 值，最后面是需要 &lt;code&gt;fetch&lt;/code&gt; 的分支信息。&lt;/p&gt;
&lt;p&gt;此文件可能不止一行，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ cat .git/FETCH_HEAD
848d7701250d5fee1449c5355158f629f6564484        	branch &apos;master&apos; of https://github.com/xxxx/xxx
81d84ed74fc2b29c73d6ac82d681e5819b4d35d3        	branch &apos;next&apos; of https://github.com/xxxx/xxx
a25f5f1615a479e717a82bc4a10d816a44de6cd1		not-for-merge   branch &apos;add-i18n&apos; of https://github.com/xxxx/xxx
065c1b268386d533be65f4ae34742b2f1780d589        not-for-merge   branch &apos;add-sche-catch&apos; of https://github.com/xxxx/xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⭐其中会有关键字 &lt;strong&gt;&lt;code&gt;not-for-merge&lt;/code&gt;&lt;/strong&gt;，由于 &lt;code&gt;git pull&lt;/code&gt; 其实就是 &lt;code&gt;fetch + merge&lt;/code&gt;，&lt;strong&gt;有这个标志就表明 &lt;code&gt;git pull&lt;/code&gt; 时只 &lt;code&gt;fetch&lt;/code&gt;，不 &lt;code&gt;merge&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;此特性在 2015 年 &lt;a href=&quot;https://github.com/git/git/blob/53f9a3e157dbbc901a02ac2c73346d375e24978c/Documentation/RelNotes/2.5.0.txt#L64&quot;&gt;git 2.5&lt;/a&gt; 之后被加入，可以看看 &lt;a href=&quot;https://github.com/git/git/blame/6a6c0f10a70a6eb101c213b09ae82a9cad252743/builtin/pull.c#L378&quot;&gt;源码&lt;/a&gt;，当 &lt;code&gt;git pull&lt;/code&gt; 时，&lt;code&gt;not-for-merge&lt;/code&gt; 会做为 &lt;em&gt;magic string&lt;/em&gt; 来判定是否要从远程合并到本地分支。上面这段源码的注释写得恰到好处：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Appends merge candidates from FETCH_HEAD that are not marked not-for-merge into merge_heads.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.5 文件 config&lt;/h3&gt;
&lt;p&gt;此文件存储项目本地的 git 设置，典型内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
        ignorecase = true
[remote &quot;origin&quot;]
        url = git@gitlab.xxxx.com/xxx.git
        fetch = +refs/heads/*:refs/remotes/origin/*
[branch &quot;master&quot;]
        remote = origin
        merge = refs/heads/master
[branch &quot;v2.6.0&quot;]
        remote = origin
        merge = refs/heads/v2.6.0
[branch &quot;v2.8.0&quot;]
        remote = origin
        merge = refs/heads/v2.8.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是典型的 &lt;a href=&quot;https://en.wikipedia.org/wiki/INI_file&quot;&gt;&lt;code&gt;INI&lt;/code&gt; 配置文件&lt;/a&gt;，每个 &lt;code&gt;section&lt;/code&gt; 可包含多个 &lt;code&gt;variable = value&lt;/code&gt;，其中 &lt;code&gt;[core]&lt;/code&gt; 字段包含各种 git 的参数设置，如 &lt;code&gt;ignorecase = true&lt;/code&gt; 表示忽略文件名大小写。&lt;/p&gt;
&lt;p&gt;⭐&lt;code&gt;git config --global&lt;/code&gt; 影响的则是全局配置文件 &lt;code&gt;~/.gitconfig&lt;/code&gt;；可执行以下命令对该文件进行修改：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git config --global -e
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[core]&lt;/code&gt; 段的内容跟 &lt;code&gt;git config&lt;/code&gt; 命令对应&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;执行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git config user.name abc
$ git config user.email abc@abc.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会在 &lt;code&gt;config&lt;/code&gt; 文件中追加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;... ...
[user]
        name = abc
        email = abc@abc.com
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[remote]&lt;/code&gt; 段表示远程仓库配置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;详见 &lt;a href=&quot;https://git-scm.com/book/en/v2/Git-Internals-The-Refspec&quot;&gt;Git Internals - The Refspec&lt;/a&gt;，注意这里的 &lt;code&gt;+&lt;/code&gt; 与 &lt;code&gt;*&lt;/code&gt; 的含义。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[branch]&lt;/code&gt; 段表示分支同步设置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设当前在 &lt;code&gt;master&lt;/code&gt; 分支，执行 &lt;code&gt;git pull&lt;/code&gt; 若出现以下提示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

   git pull &amp;#x3C;remote&gt; &amp;#x3C;branch&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就说明 &lt;code&gt;.git/config&lt;/code&gt; 文件缺少对应的 &lt;code&gt;[branch &quot;master&quot;]&lt;/code&gt; 字段。&lt;/p&gt;
&lt;p&gt;解决方案为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git branch -u origin/master master

# 或者执行一次 push
$ git push -u origin master

# 或者根据如下命令设置远程分支与本地分支的关联(如下命令表示本地分支的master与远程分支的master分支关联)
$ git push --set-upstream origin master:master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会出现提示：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Branch &apos;master&apos; set up to track remote branch &apos;master&apos; from &apos;origin&apos;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实就是生成以下内容在 &lt;code&gt;.git/config&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;[branch &quot;master&quot;]
        remote = origin
        merge = refs/heads/master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你去手动编辑 &lt;code&gt;.git/config&lt;/code&gt;，效果一样。这就是 &lt;code&gt;upstream&lt;/code&gt; 的真正含义，即生成 &lt;code&gt;config&lt;/code&gt; 中的这段配置。&lt;/p&gt;
&lt;h3&gt;2.6 文件 description&lt;/h3&gt;
&lt;p&gt;看到&lt;a href=&quot;https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain&quot;&gt;文档&lt;/a&gt;中有如下一段描述：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The description file is used only by the GitWeb program, so don’t worry about it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;说明这个文件主要用于 &lt;code&gt;GitWeb&lt;/code&gt; 的描述，如果我们要启动 &lt;code&gt;GitWeb&lt;/code&gt; 可用如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 确保lighttpd已安装: brew install lighttpd
$ git instaweb --start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认会启动 &lt;code&gt;lighttpd&lt;/code&gt; 服务并打开浏览器 &lt;code&gt;http://127.0.0.1:1234&lt;/code&gt; (试着改成对外IP并分享给别人？)&lt;/p&gt;
&lt;p&gt;以下显示当前的 git 仓库名称以及描述，默认的描述如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unnamed repository; edit this file &apos;description&apos; to name the repository.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141636779.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;上面这段话就是默认的 &lt;code&gt;description&lt;/code&gt; 文件的内容，编辑这个文件来让你 &lt;code&gt;GitWeb&lt;/code&gt; 描述更友好。除此之外没发现其它用处。&lt;/p&gt;
&lt;h3&gt;2.7 文件夹 hooks/&lt;/h3&gt;
&lt;p&gt;存放 &lt;code&gt;git hooks&lt;/code&gt;，&lt;strong&gt;用于在 git 命令前后做检查或做些自定义动作&lt;/strong&gt;。运行 &lt;code&gt;ls -F1 .git/hooks&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;prepare-commit-msg.sample  # git commit 之前，编辑器启动之前触发，传入 COMMIT_FILE，COMMIT_SOURCE，SHA1
commit-msg.sample          # git commit 之前，编辑器退出后触发，传入 COMMIT_EDITMSG 文件名
pre-commit.sample          # git commit 之前，commit-msg 通过后触发，譬如校验文件名是否含中文
pre-push.sample            # git push 之前触发

pre-receive.sample         # git push 之后，服务端更新 ref 前触发
update.sample              # git push 之后，服务端更新每一个 ref 时触发，用于针对每个 ref 作校验等
post-update.sample         # git push 之后，服务端更新 ref 后触发

pre-rebase.sample          # git rebase 之前触发，传入 rebase 分支作参数
applypatch-msg.sample      # 用于 git am 命令提交信息校验
pre-applypatch.sample      # 用于 git am 命令执行前动作
fsmonitor-watchman.sample  # 配合 core.fsmonitor 设置来更好监测文件变化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141636686.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://git-scm.com/docs/githooks?spm=a2c6h.12873639.0.0.59246941Acahcx&quot;&gt;https://git-scm.com/docs/githooks&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果要启用某个 hook，只需把 &lt;code&gt;.sample&lt;/code&gt; 删除即可，然后编辑其内容来实现相应的逻辑。&lt;/p&gt;
&lt;p&gt;比如我们要校验每个 &lt;em&gt;commit message&lt;/em&gt; 至少要包含两个单词，否则就提示并拒绝提交，将 &lt;code&gt;commit-msg.sample&lt;/code&gt; 改为 &lt;code&gt;commit-msg&lt;/code&gt; 后，编辑如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/bin/sh
grep -q &apos;\S\s\+\S&apos; $1 || { echo &apos;提交信息至少为两个单词&apos; &amp;#x26;&amp;#x26; exit 1; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样当提交一个 commit 时，会执行 bash 命令： &lt;code&gt;.git/hooks/commit-msg .git/COMMIT_EDITMSG&lt;/code&gt;，退出值不为 &lt;code&gt;0&lt;/code&gt;，就拒绝提交。&lt;/p&gt;
&lt;h3&gt;2.8 文件夹 info/&lt;/h3&gt;
&lt;p&gt;此文件夹基本就有两个文件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;文件 &lt;code&gt;info/exclude&lt;/code&gt; 用于排除规则，与 &lt;code&gt;.gitignore&lt;/code&gt; 功能类似。&lt;/li&gt;
&lt;li&gt;可能会包含文件 &lt;code&gt;info/refs&lt;/code&gt; ，用于跟踪各分支的信息。此文件一般通过命令 &lt;a href=&quot;https://git-scm.com/docs/git-update-server-info?spm=a2c6h.12873639.0.0.59246941Acahcx&quot;&gt;git update-server-info&lt;/a&gt; 生成，里面的内容：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;94e1a0d952f577fe1348d828d145507d3709e11e    refs/heads/master
# object hash                                   # branch reference
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这表示 master 分支所指向的文件对象 hash 值为：&lt;code&gt;94e1a0d952f577fe1348d828d145507d3709e11e&lt;/code&gt;，&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;git cat-file -p 94e1a0d952f577fe1348d828d145507d3709e11e&lt;/code&gt;，可以看到 master 分支最后提交的记录信息。&lt;/p&gt;
&lt;p&gt;同时：&lt;code&gt;cat .git/objects/94/e1a0d952f577fe1348d828d145507d3709e11e&lt;/code&gt; 可以看到最后提交文件的二进制内容表示。&lt;/p&gt;
&lt;p&gt;文件 &lt;code&gt;info/refs&lt;/code&gt; 对于 &lt;a href=&quot;https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols?spm=a2c6h.12873639.0.0.59246941Acahcx&quot;&gt;搭建 git 服务器&lt;/a&gt; 来说至关重要。&lt;/p&gt;
&lt;h3&gt;2.9 文件夹 logs/&lt;/h3&gt;
&lt;p&gt;记录了操作信息，&lt;code&gt;git reflog&lt;/code&gt; 命令以及像 &lt;code&gt;HEAD@{1}&lt;/code&gt; 形式的路径会用到。如果删除此文件夹（危险！），那么依赖于 &lt;a href=&quot;https://git-scm.com/docs/git-reflog&quot;&gt;reflog&lt;/a&gt; 的命令就会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ mv .git/logs .git/logs_bak
$ git checkout HEAD@{1}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报错信息如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;error: pathspec &apos;HEAD@{1}&apos; did not match any file(s) known to git&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.10 文件夹 objects/&lt;/h3&gt;
&lt;p&gt;此文件夹简单说，就是 &lt;code&gt;git的数据库&lt;/code&gt;，运行 &lt;code&gt;tree .git/objects&lt;/code&gt;，可以看到目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;.git/objects/
|-- 0c
|   `-- d370696b581c38ee01e62b148a759f80facc2d
|-- 59
|   `-- 3d5b490556791212acd5a516a37bbfa05d44dd
|-- 61
|   `-- be44eedde61d723e5761577a2b420ba0fc2794
|-- 64
|   `-- c0aed8ddcbb546bdcec2848938fc82348db227
|-- d4
|   `-- 9904676ce8ddde276bdbfa9bbec313e90e0f50
|-- info
`-- pack
    |-- pack-75e3f2aa378752ec93a8e9f375f01204d498605b.idx
    `-- pack-75e3f2aa378752ec93a8e9f375f01204d498605b.pack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些文件分两种形式：&lt;strong&gt;&lt;a href=&quot;https://github.com/git/git/blob/master/Documentation/technical/pack-format.txt?spm=a2c6h.12873639.0.0.59246941Acahcx&amp;#x26;file=pack-format.txt&quot;&gt;pack压缩包&lt;/a&gt; 形式放在 &lt;code&gt;pack/&lt;/code&gt; 目录下，除此之外都是 &lt;code&gt;hash文件&lt;/code&gt; 形式，被叫做 &lt;code&gt;loost objects&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个文件夹以及相应的算法，我没找到独立的名称，就叫它 &lt;code&gt;hash-object&lt;/code&gt; 体系吧。因为确实有个 &lt;code&gt;git hash-object&lt;/code&gt; 命令存在，是一个底层的负责生成这些 &lt;code&gt;loost objects&lt;/code&gt; 文件，如果要看到这些文件各自的含义，执行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git cat-file --batch-check --batch-all-objects
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;04c87c65f142f33945f2f5951cf7801a32dfa240 commit 194
098217953a6ca169bed33d2be8a07d584fcdaf30 tree 31
0cd370696b581c38ee01e62b148a759f80facc2d commit 245
2a810017bfc85d7db2627f4aabdaa1583212bda3 blob 19
3920a07c1d5694df6b8658592b0939241d70e9e5 tree 93
593d5b490556791212acd5a516a37bbfa05d44dd tag 148
61be44eedde61d723e5761577a2b420ba0fc2794 tree 154
... ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但你会发现这个列表里有些值在文件夹中并不存在，因为除了 &lt;code&gt;loost objects&lt;/code&gt; 它还汇总了 &lt;code&gt;pack&lt;/code&gt; 文件中的内容。&lt;/p&gt;
&lt;h4&gt;hash 文件&lt;/h4&gt;
&lt;p&gt;又称为 &lt;code&gt;loose object&lt;/code&gt;，文件名称共由 40 字符的 &lt;a href=&quot;https://en.wikipedia.org/wiki/SHA-1&quot;&gt;SHA-1&lt;/a&gt; hash 值组成，其中前两个字符为文件夹分桶，后 38 个字符为文件名称。&lt;/p&gt;
&lt;p&gt;按文件内容可分为四种类型：&lt;strong&gt;commit&lt;/strong&gt;, &lt;strong&gt;tree&lt;/strong&gt;, &lt;strong&gt;blob&lt;/strong&gt;, &lt;strong&gt;tag&lt;/strong&gt;，若执行以下命令会生成所有四种类型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ echo -en &apos;xx\n&apos; &gt; xx  # 共 3 个字符
$ git add .
$ git commit -m &apos;update xx&apos;
$ git tag -a &apos;v1.0&apos; -m &apos;release: 1.0.0&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过以上操作后，对比一下文件树，发现多了四个 &lt;code&gt;hash文件&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;|-- 0c
|   `-- d370696b581c38ee01e62b148a759f80facc2d
|-- 18
|   `-- 143661f96845f11e0b4ab7312bdc0f356834ce
|-- 30
|   `-- 20feea86d222d83218eb3eb5aa9f58f73df04d
|-- 59
|   `-- 3d5b490556791212acd5a516a37bbfa05d44dd
|-- 61
|   `-- be44eedde61d723e5761577a2b420ba0fc2794
|-- 64
|   `-- c0aed8ddcbb546bdcec2848938fc82348db227
|-- ad
|   `-- f4c9afac7afae3ff3e95e6c4eefe009d547f00
|-- cc
|   `-- c9bd67dc5c467859102d53d54c5ce851273bdd
|-- d4
|   `-- 9904676ce8ddde276bdbfa9bbec313e90e0f50
|-- info
`-- pack
    |-- pack-75e3f2aa378752ec93a8e9f375f01204d498605b.idx
    `-- pack-75e3f2aa378752ec93a8e9f375f01204d498605b.pack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这四个 &lt;code&gt;hash文件&lt;/code&gt; 分别是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;cc/c9bd67dc5c467859102d53d54c5ce851273bdd  # blob
30/20feea86d222d83218eb3eb5aa9f58f73df04d  # commit
ad/f4c9afac7afae3ff3e95e6c4eefe009d547f00  # tree
18/143661f96845f11e0b4ab7312bdc0f356834ce  # tag
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们想看下里面到底存的什么？其实这些文件都经过了压缩，压缩形式为 &lt;a href=&quot;https://www.zlib.net/&quot;&gt;zlib&lt;/a&gt;。先安装一下解压工具 macOS 版 &lt;code&gt;brew install pigz&lt;/code&gt; 或 windows 版 &lt;a href=&quot;https://blog.kowalczyk.info/software/pigz-for-windows.html?spm=a2c6h.12873639.0.0.59246941Acahcx&quot;&gt;pigz&lt;/a&gt;，后执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ pigz -d &amp;#x3C; .git/objects/cc/c9bd67dc5c467859102d53d54c5ce851273bdd

# BLOB类型，显示结果为&gt;&gt;&gt;&gt;(注意xx后有个\n)
blob 3xx
$ pigz -d &amp;#x3C; .git/objects/30/20feea86d222d83218eb3eb5aa9f58f73df04d

# COMMIT类型，显示结果为&gt;&gt;&gt;&gt;
commit 248tree adf4c9afac7afae3ff3e95e6c4eefe009d547f00
parent 0cd370696b581c38ee01e62b148a759f80facc2d
author jamesyang.yjm &amp;#x3C;jamesyang.yjm@alibaba-inc.com&gt; 1562044880 +0800
committer jamesyang.yjm &amp;#x3C;jamesyang.yjm@alibaba-inc.com&gt; 1562044880 +0800

update xx
$ pigz -d &amp;#x3C; .git/objects/ad/f4c9afac7afae3ff3e95e6c4eefe009d547f00

# TREE类型，显示结果为&gt;&gt;&gt;&gt;
tree 154100644 abc*???]}?bJ?ڡX2??100644 asdf???CK?)?wZ???S?100644 iou???CK?)?wZ???S?100644 xx?ɽg?\FxY-S?L\?Q&apos;;?100644 yy???CK?)?wZ???S?
$ pigz -d &amp;#x3C; .git/objects/18/143661f96845f11e0b4ab7312bdc0f356834ce

# TAG类型，显示结果为&gt;&gt;&gt;&gt;
tag 155object 3020feea86d222d83218eb3eb5aa9f58f73df04d
type commit
tag v1.0
tagger jamesyang.yjm &amp;#x3C;jamesyang.yjm@alibaba-inc.com&gt; 1562045942 +0800

release: 1.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会发现，显示结果都是 &lt;code&gt;type size+内容&lt;/code&gt; 形式，这就是 object 文件的存储格式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;[type] [size][NULL][content]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt; 可选值：commit, tree, blob, tag，&lt;code&gt;NULL&lt;/code&gt; 就是C语言里的字符结束符：&lt;code&gt;\0&lt;/code&gt;，&lt;code&gt;size&lt;/code&gt; 就是 &lt;code&gt;NULL&lt;/code&gt;后内容的字节长度。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt; 的几种类型可以使用 &lt;code&gt;git cat-file -t hash&lt;/code&gt; 看到，内容可以用 &lt;code&gt;git cat-file -p hash&lt;/code&gt; 看到。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git cat-file -t ccc9bd67dc5c467859102d53d54c5ce851273bdd

# 显示结果为&gt;&gt;&gt;&gt;
blob
$ git cat-file -p ccc9bd67dc5c467859102d53d54c5ce851273bdd

# 显示结果为&gt;&gt;&gt;&gt;
xx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以 &lt;code&gt;blob&lt;/code&gt; 文件就是对原文件内容的&lt;strong&gt;全量拷贝&lt;/strong&gt;，同时前面加了 &lt;code&gt;blob size\0&lt;/code&gt;，而文件名称的 &lt;code&gt;hash&lt;/code&gt; 值计算是计算整体字符的 &lt;code&gt;SHA-1&lt;/code&gt; 值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ echo -en &apos;blob 3\0xx\n&apos; | shasum
# 显示结果为&gt;&gt;&gt;&gt;
ccc9bd67dc5c467859102d53d54c5ce851273bdd  -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;知道原理后，其它类型格式请自行参考 &lt;a href=&quot;http://www-cs-students.stanford.edu/~blynn/gitmagic/ch08.html#_the_object_database&quot;&gt;斯坦福 Ben Lynn 所著的 GitMagic&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;所以，当我们 &lt;code&gt;git show 3020feea86d222d83218eb3eb5aa9f58f73df04d&lt;/code&gt; 时，会发生些什么？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到 &lt;code&gt;3020feea86d222d83218eb3eb5aa9f58f73df04d&lt;/code&gt; 这个 &lt;code&gt;commit&lt;/code&gt;，显示出来&lt;/li&gt;
&lt;li&gt;找到此 &lt;code&gt;commit&lt;/code&gt; 关联的 &lt;code&gt;tree object&lt;/code&gt;: &lt;code&gt;adf4c9afac7afae3ff3e95e6c4eefe009d547f00&lt;/code&gt;，拉取相应的 &lt;code&gt;blob&lt;/code&gt; 文件，并与当前工作区内的文件做 &lt;code&gt;diff&lt;/code&gt;，然后显示出来&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就是 &lt;code&gt;objects/&lt;/code&gt; 文件夹作为 &lt;code&gt;git数据库&lt;/code&gt; 被使用的真实例子。&lt;/p&gt;
&lt;h4&gt;pack 文件&lt;/h4&gt;
&lt;p&gt;为什么会有 &lt;code&gt;.pack&lt;/code&gt; 文件？&lt;/p&gt;
&lt;p&gt;由于每次 &lt;code&gt;commit&lt;/code&gt; 都会生成许多 &lt;code&gt;hash文件&lt;/code&gt;，并且由于 &lt;code&gt;blob&lt;/code&gt; 文件都是全量存储的，导致 git 效率下降，于是有了 &lt;a href=&quot;https://github.com/git/git/blob/master/Documentation/technical/pack-format.txt&quot;&gt;pack-format&lt;/a&gt;，优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于大仓库存储效率高&lt;/li&gt;
&lt;li&gt;利于网络传输，便于备份&lt;/li&gt;
&lt;li&gt;增量存储，优化磁盘空间&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将 &lt;code&gt;.git/objects&lt;/code&gt; 下的部分文件打包成 &lt;a href=&quot;https://github.com/git/git/blob/master/Documentation/technical/pack-format.txt&quot;&gt;pack格式&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ tree .git/objects/ | wc -l
311

$ git gc
Enumerating objects: 288, done.
Counting objects: 100% (288/288), done.
Delta compression using up to 4 threads
Compressing objects: 100% (287/287), done.
Writing objects: 100% (288/288), done.
Total 288 (delta 131), reused 90 (delta 0)

$ tree .git/objects/ | wc -l
12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到文件数量减小了不少，其中&lt;strong&gt;大部分&lt;/strong&gt;文件被打到一个 &lt;code&gt;.pack&lt;/code&gt; 包中，并且是增量存储，有部分变更的文件只存储 &lt;strong&gt;基础hash&lt;/strong&gt; ＋ &lt;strong&gt;变更内容&lt;/strong&gt;，磁盘空间优化很明显。&lt;/p&gt;
&lt;p&gt;⭐&lt;strong&gt;&lt;code&gt;git gc&lt;/code&gt; 其实运行了两条命令：&lt;code&gt;git repack&lt;/code&gt; 用来打包 和 &lt;code&gt;git prune-packed&lt;/code&gt; 用来移除已打包的 &lt;code&gt;hash文件&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你想打包&lt;strong&gt;所有&lt;/strong&gt;文件，并不推荐，但可以用以下命令：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git repack -a -d -f --depth=250 --window=250
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体可见：&lt;a href=&quot;https://stackoverflow.com/questions/28720151/git-gc-aggressive-vs-git-repack&quot;&gt;此问题&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果想看一下包里有啥，运行：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git verify-pack -v .git/objects/pack/pack-5963b552193021791c1a0ab9136c272f07124c98.pack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显示如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 commit 245 153 12
2305588a632214f266462260428c4395f936b5b0 commit 252 156 165
1fa9735670eb952b6468d17b418525717c8e3527 commit 248 156 321
3ffb7fb9830e232669c95b3b65f0f8f3fc7a6027 commit 248 155 477
86a5912f97d7dd8f90a28cab6bffc8ee78997e2c commit 244 151 632
94e1a0d952f577fe1348d828d145507d3709e11e commit 249 156 783
86903f8f5024485afa8480020a04cc00f228d23c commit 243 150 939
6efdffad4fb725aa8d0f4d7d29feb5aee7ea5dff commit 242 151 1089
04c87c65f142f33945f2f5951cf7801a32dfa240 commit 73 85 1240 1 6efdffad4fb725aa8d0f4d7d29feb5aee7ea5dff
2a810017bfc85d7db2627f4aabdaa1583212bda3 blob   19 27 1325
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 blob   0 9 1352
b5e810691433cf8a2960c27c1b33546fa96e2bef blob   16 26 1361
2f36e957afc2b3bcda988cb29a86e3a1490e8cc2 tree   153 106 1387
2ed6130bd33afa26817418308e29c4081ea056ec tree   5 15 1493 1 2f36e957afc2b3bcda988cb29a86e3a1490e8cc2
9df301ad27294a62ba1ae65aaed489072d778c79 tree   123 103 1508
7d48a14b9ca1dca2f6a593eef19633ce45f81bee blob   12 21 1611
a448b4d6450de854dcc6fe658bdb72e22c726cbb tree   123 102 1632
9e56fd51f52d8b9d242c50c24a4cae586d76ec7e blob   7 16 1734
bde15b851f135327ada02c9deac0fb1ee01cf343 tree   123 102 1750
58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c blob   4 13 1852
3920a07c1d5694df6b8658592b0939241d70e9e5 tree   7 17 1865 1 bde15b851f135327ada02c9deac0fb1ee01cf343
16729e3b94f19bc95cb6f563f776bfb4694a6e5b tree   4 14 1882 2 3920a07c1d5694df6b8658592b0939241d70e9e5
b72c74792528892694c395b2c9a3d6af740f3fb2 tree   63 50 1896
098217953a6ca169bed33d2be8a07d584fcdaf30 tree   31 42 1946
non delta: 20 objects
chain length = 1: 3 objects
chain length = 2: 1 object
.git/objects/pack/pack-5963b552193021791c1a0ab9136c272f07124c98.pack: ok
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面那串数字说明文档里很详细：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;When specifying the -v option the format used is:

        SHA-1 type size size-in-packfile offset-in-packfile

for objects that are not deltified in the pack, and

        SHA-1 type size size-in-packfile offset-in-packfile depth base-SHA-1

for objects that are deltified.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上最后有 hash 的条目，说明是增量存储的 &lt;strong&gt;基础hash&lt;/strong&gt;，其前是增量深度。&lt;/p&gt;
&lt;h3&gt;2.11 文件夹 refs/&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;refs&lt;/code&gt; 可以理解成文件系统中的 &lt;code&gt;symbol link&lt;/code&gt;，看下结构：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ tree .git/refs/

.git/refs
|-- heads
|   `-- master
`-- tags
    `-- v1.0

$ cat .git/refs/heads/master 
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5

$ cat .git/refs/tags/v1.0    
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5

$ git cat-file -t 5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5
commit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141637712.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到 &lt;code&gt;master&lt;/code&gt; 和 &lt;code&gt;v1.0&lt;/code&gt; 都指向 &lt;code&gt;5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5&lt;/code&gt; 这个 &lt;code&gt;commit&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;refs/heads/&lt;/code&gt; 文件夹内的 &lt;code&gt;ref&lt;/code&gt; 一般通过 &lt;code&gt;git branch&lt;/code&gt; 生成。&lt;code&gt;git show-ref --heads&lt;/code&gt; 可以查看。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;refs/tags/&lt;/code&gt; 文件夹内的 &lt;code&gt;ref&lt;/code&gt; 一般通过 &lt;code&gt;git tag&lt;/code&gt; 生成。&lt;code&gt;git show-ref --tags&lt;/code&gt; 可以查看。&lt;/p&gt;
&lt;p&gt;如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git branch abc

$ tree .git/refs/

.git/refs/
|-- heads
|   |-- abc
|   `-- master
`-- tags
    `-- v1.0

$ cat .git/refs/heads/abc 
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明新建分支其实就是生成了一个指向某个 &lt;code&gt;commit&lt;/code&gt; 的 &lt;code&gt;symbol link&lt;/code&gt;，当然在这里叫做 &lt;code&gt;ref&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;git tag&lt;/code&gt; 命令本质与 &lt;code&gt;git branch&lt;/code&gt; 相同，只生成一个 &lt;code&gt;ref&lt;/code&gt; 放在 &lt;code&gt;tags&lt;/code&gt; 目录下，所以被称为 &lt;code&gt;lightweight tag&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;git tag -a xx&lt;/code&gt; 命令会首先生成一个类型为 &lt;code&gt;tag&lt;/code&gt; 的 &lt;code&gt;hash文件&lt;/code&gt; 放到 &lt;code&gt;objects/&lt;/code&gt; 目录，然后生成 &lt;code&gt;ref&lt;/code&gt; 放到 &lt;code&gt;tags&lt;/code&gt; 目录下指向那个文件。这就叫做 &lt;code&gt;annotated tag&lt;/code&gt;，好处是可包含一些元信息如 &lt;code&gt;tagger&lt;/code&gt; 和 &lt;code&gt;message&lt;/code&gt;，被 git 的 &lt;code&gt;hash-object&lt;/code&gt; 算法管理，可被 GPG 签名等，所以&lt;strong&gt;更稳定，更安全&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;使用以下命令来拿到 &lt;code&gt;refs&lt;/code&gt; 文件夹存储的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git show-ref --head --dereference
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 HEAD
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/heads/abc
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/heads/master
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/tags/v1.0
5e84371048faa20412f5492e6af264a7e1edfec1 refs/tags/xx
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/tags/xx^{}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来看这些信息如何变化的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ touch new_file &amp;#x26;&amp;#x26; git add . &amp;#x26;&amp;#x26; git commit -m &apos;add new_file&apos;
[master 44b0d05] add new_file
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 new_file

$ git show-ref --head --dereference
44b0d05ddadaaa8d2cc40d6647cc474b26f5d8d3 HEAD
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/heads/abc
44b0d05ddadaaa8d2cc40d6647cc474b26f5d8d3 refs/heads/master
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/tags/v1.0
5e84371048faa20412f5492e6af264a7e1edfec1 refs/tags/xx
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/tags/xx^{}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;diff 一下可以看到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 HEAD
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/heads/master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两行发生了变化。也就是每次 commit 时，HEAD 与 &lt;code&gt;heads&lt;/code&gt; 都会自动更新。&lt;/p&gt;
&lt;h3&gt;2.12 文件 index&lt;/h3&gt;
&lt;p&gt;细心的读者发现，没有讲 &lt;code&gt;index&lt;/code&gt; 文件？原因在于：&lt;/p&gt;
&lt;p&gt;⭐&lt;strong&gt;&lt;code&gt;index&lt;/code&gt; 文件是整个 git 除 &lt;code&gt;hash-object&lt;/code&gt; 体系最核心的部分，值得用单独一篇来讲&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;可以先参考以下文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://www.ruanyifeng.com/blog/2018/10/git-internals.html&quot;&gt;阮一峰老师写的 Git 原理入门&lt;/a&gt; 中 &lt;code&gt;暂存区&lt;/code&gt; 的部分&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/git/git/blob/master/Documentation/technical/index-format.txt&quot;&gt;Git index format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/git/git/blob/master/Documentation/technical/racy-git.txt&quot;&gt;Use of index and Racy Git problem&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简要说一下，&lt;code&gt;index&lt;/code&gt; 是一个微型的 &lt;strong&gt;linux 文件系统&lt;/strong&gt;，用最经济的方式实现了 &lt;a href=&quot;https://en.wikipedia.org/wiki/Inode&quot;&gt;inode&lt;/a&gt;，这并不是偶然，因为创造这个想法的人同时也是 linux 的创造者 &lt;a href=&quot;https://en.wikipedia.org/wiki/Linus_Torvalds&quot;&gt;Linus Torvalds&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这个文件也叫做 &lt;code&gt;git&lt;/code&gt; 的暂存区(&lt;code&gt;Staging Area&lt;/code&gt;)，&lt;code&gt;git add&lt;/code&gt; 就是把工作区内的某些文件取部分 &lt;code&gt;stat&lt;/code&gt; 抓取的内容并写入 &lt;code&gt;.git/index&lt;/code&gt; 文件并存为相应的一条 &lt;code&gt;index entry&lt;/code&gt;，多条 &lt;code&gt;index entry&lt;/code&gt; 形成一个 &lt;code&gt;tree&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git commit&lt;/code&gt; 是把上一步形成的 &lt;code&gt;tree&lt;/code&gt; 结构及相应的 &lt;code&gt;blob&lt;/code&gt; 存储到 &lt;code&gt;objects/&lt;/code&gt; 文件夹下并同时生成一条 &lt;code&gt;commit&lt;/code&gt; 记录。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git reset&lt;/code&gt; 是将刚写入 &lt;code&gt;index&lt;/code&gt; 文件的 &lt;code&gt;tree&lt;/code&gt; 丢弃，并从 &lt;code&gt;HEAD&lt;/code&gt; 中恢复一个 &lt;code&gt;tree&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git status&lt;/code&gt; 是拿 &lt;code&gt;index&lt;/code&gt; 文件中存储的 &lt;code&gt;tree&lt;/code&gt; 与工作区内的文件在 &lt;code&gt;stat&lt;/code&gt; 层面做对比，并输出变更。&lt;/p&gt;
&lt;p&gt;以上，这几个文件夹咱们用一张图做总结：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141637576.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202311150150548.Ddg3RA3X.jpg"/><enclosure url="/_astro/202311150150548.Ddg3RA3X.jpg"/></item><item><title>一文带你认识 Docker 与 k8s</title><link>https://coooredump.github.io/blog/productivity-tool/docker-and-k8s</link><guid isPermaLink="true">https://coooredump.github.io/blog/productivity-tool/docker-and-k8s</guid><description>实际上一些小型公司，在业务不太复杂的情况下都是直接使用 Docker，尽管 k8s 有很多好处，但是众所周知它非常复杂，业务比较简单可以放弃使用 k8s，但 k8s 在业务达到一定规模后也得启用。</description><pubDate>Wed, 06 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;随着 k8s 作为容器编排解决方案变得越来越流行，有些人开始拿 Docker 和 k8s 进行对比，不禁问道：Docker 不香吗？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;k8s 是 kubernetes 的缩写，&apos;8&apos; 代表中间的八个字符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实 Docker 和 k8s 并非直接的竞争对手，它俩相互依存。 Docker 是一个容器化平台，而 k8s 是 Docker 等容器平台的协调器。&lt;/p&gt;
&lt;h2&gt;1. 容器化时代来了&lt;/h2&gt;
&lt;p&gt;虚拟化技术已经走过了三个时代，没有容器化技术的演进就不会有 Docker 技术的诞生。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141757545.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;（1）&lt;strong&gt;物理机时代&lt;/strong&gt;：多个应用程序可能会跑在一台机器上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141757856.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;（2）&lt;strong&gt;虚拟机时代&lt;/strong&gt;：一台物理机器安装多个虚拟机（VM），一个虚拟机跑多个程序。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141757739.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;（3）&lt;strong&gt;容器化时代&lt;/strong&gt;：一台物理机安装多个容器实例（container），一个容器跑多个程序。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141757067.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;容器化解决了软件开发过程中一个令人非常头疼的问题，用一段对话描述：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;测试人员：你这个功能有问题。&lt;/p&gt;
&lt;p&gt;开发人员：我&lt;strong&gt;本地&lt;/strong&gt;是好的呀！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;开发人员编写代码，在自己本地环境测试完成后，将代码部署到测试或生产环境中，经常会遇到各种各样的问题。明明本地完美运行的代码为什么部署后出现很多 bug，原因有很多：&lt;strong&gt;不同的操作系统、不同的依赖库等，总结一句话就是因为本地环境和远程环境不一致&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;容器化技术正好解决了这一关键问题，它将软件程序和运行的基础环境分开。开发人员编码完成后将程序打包到一个容器镜像中，镜像中详细列出了所依赖的环境，在不同的容器中运行标准化的镜像，从根本上解决了环境不一致的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;⭐虽然容器概念已经出现不短的时间，但 2013 年推出的开源项目 Docker 在很大程度上帮助推广了容器这项技术，并推动了软件开发中&lt;strong&gt;容器化&lt;/strong&gt;和&lt;strong&gt;微服务&lt;/strong&gt;的趋势，&lt;strong&gt;这种趋势后来被称为云原生开发&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;2. 容器化技术的尖刀武器&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141757986.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可移植性&lt;/strong&gt;：不依赖具体的操作系统或云平台，比如在阿里云或腾讯云直接随意迁移。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;占地小&lt;/strong&gt;：容器只需要其应用程序以及它需要运行的所有容器和库的依赖清单，不需要将所有的依赖库都打包在一起。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享 bin 和 lib&lt;/strong&gt;：不同的容器可以共享 bin 和 lib，进一步节省了空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. Docker 横空出世&lt;/h2&gt;
&lt;p&gt;2010 年一位年轻小伙子在美国旧金山成立了一家名叫【dotCloud】的公司， 开发了 Docker 的核心技术，从此开启了容器技术的时代。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141758706.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;后面 dotCloud 公司将自己的容器技术进行了简化和标准化，取名为 Docker，就是大家熟悉的鲸鱼 logo。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141758178.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;2013 年 dotCloud 公司宣布将 Docker 开源，随着越来越多的工程师发现了它的优点， Docker 的人气迅速攀升，成为当时最火爆的开源技术之一。&lt;/p&gt;
&lt;p&gt;当前有 30％ 以上的企业在其 AWS 环境中使用 Docker，并且这个数字还在继续增长。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141758692.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时的 Docker，已经成为行业里人气最火爆的开源技术，没有之一。甚至像 Google、微软、Amazon、VMware 这样的巨头，都对它青睐有加，表示将全力支持。&lt;/p&gt;
&lt;p&gt;Docker 火了之后，dotCloud 公司干脆把公司名字也改成了 Docker Inc. 。&lt;/p&gt;
&lt;p&gt;Docker 和容器技术为什么会这么火爆？说白了，就是因为它 “&lt;strong&gt;轻&lt;/strong&gt;”。&lt;/p&gt;
&lt;p&gt;在容器技术之前，业界的网红是&lt;strong&gt;虚拟机&lt;/strong&gt;。虚拟机技术的代表，是 &lt;strong&gt;VMWare&lt;/strong&gt; 和 &lt;strong&gt;OpenStack&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141758405.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;相信很多人都用过虚拟机。虚拟机，就是在你的操作系统里面，装一个软件，然后通过这个软件，再模拟一台甚至多台“子电脑”出来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141758807.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 “子电脑” 里，你可以和正常电脑一样运行程序，例如登录 QQ。如果你愿意，你可以变出好几个 “子电脑”，里面都登录上 QQ。“子电脑” 和 “子电脑” 之间，是&lt;strong&gt;相互隔离&lt;/strong&gt;的，互不影响。&lt;/p&gt;
&lt;p&gt;虚拟机属于虚拟化技术。而 Docker 这样的容器技术，也是虚拟化技术，属于&lt;strong&gt;轻量级的虚拟化&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;虚拟机虽然可以隔离出很多 “子电脑”，但占用空间更大，启动更慢，虚拟机软件可能还要花钱（例如：VMWare）。&lt;/p&gt;
&lt;p&gt;而容器技术恰好没有这些缺点。它不需要虚拟出整个操作系统，只需要虚拟一个小规模的环境（类似 “&lt;a href=&quot;https://www.jianshu.com/p/678d8836cdbd&quot;&gt;沙箱&lt;/a&gt;”）。Docker 可以轻松创建容器和基于容器的应用程序，&lt;strong&gt;最初是为 Linux 构建的&lt;/strong&gt;，现在也可以在 Windows 和 MacOS 上运行。&lt;/p&gt;
&lt;p&gt;它启动时间很快，几秒钟就能完成。而且，它对资源的利用率很高（一台主机可以同时运行几千个 Docker 容器）。此外，它占的空间很小，虚拟机一般要几 GB 到几十 GB 的空间，而容器只需要 MB 级甚至 KB 级。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141758829.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;正因为如此，容器技术受到了热烈的欢迎和追捧，发展迅速。大家需要注意，&lt;strong&gt;Docker 本身并不是容器&lt;/strong&gt;，它是创建容器的工具，是应用容器引擎。想要搞懂 Docker，其实看它的两句口号就行。&lt;/p&gt;
&lt;p&gt;第一句，是 &lt;strong&gt;“Build, Ship and Run”&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;第二句口号则是：&lt;strong&gt;“Build once，Run anywhere（搭建一次，到处能用）”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141758265.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build（构建镜像）&lt;/strong&gt;： 镜像就像是集装箱，包含文件以及运行环境等等资源；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ship（运输镜像）&lt;/strong&gt;：在&lt;a href=&quot;https://cloud.tencent.com/product/cdh?from=10680&quot;&gt;宿主机&lt;/a&gt;和仓库间进行运输，这里仓库就像是超级码头；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run（运行镜像）&lt;/strong&gt;：运行的镜像就是一个容器，容器就是运行程序的地方。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⭐说白了，这个 Docker 镜像，是一个特殊的文件系统。它除了提供容器运行时所需的程序、库、资源、配置等文件外，还包含了一些为运行时准备的一些配置参数（例如：环境变量）。&lt;strong&gt;镜像不包含任何动态数据，其内容在构建之后也不会被改变&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;⭐综上所述，Docker 的运行过程，也就是去仓库把镜像拉到本地，然后用执行命令把镜像运行起来变成容器，这也就是为什么人们常常将 Docker 称为码头工人或码头装卸工。&lt;/p&gt;
&lt;p&gt;⭐负责对 Docker 镜像进行管理的，是 &lt;strong&gt;Docker Registry 服务&lt;/strong&gt;（类似仓库管理员）。当然，不是任何人建的任何镜像都是合法的。万一有人构建的镜像存在问题呢？所以，Docker Registry 服务对镜像的管理是非常严格的。最常使用的 Registry 公开服务，是官方的 &lt;strong&gt;&lt;a href=&quot;https://hub.docker.com/&quot;&gt;Docker Hub&lt;/a&gt;&lt;/strong&gt;，这也是默认的 Registry，并拥有大量的高质量的官方镜像。&lt;/p&gt;
&lt;h2&gt;4. Docker 如何使用&lt;/h2&gt;
&lt;p&gt;其实大多数人谈论 Docker 时说的是 &lt;strong&gt;Docker Engine&lt;/strong&gt;，这只是一个构建和运行的容器。&lt;/p&gt;
&lt;p&gt;在运行容器前需要编写 Docker File，通过 &lt;strong&gt;dockerFile&lt;/strong&gt; 生成镜像，然后才能运行 Docker 容器。&lt;/p&gt;
&lt;p&gt;Docker File 定义了运行镜像（&lt;strong&gt;image&lt;/strong&gt;）所需的所有内容，包括操作系统和软件安装位置。一般情况下都不需要从头开始编写 Docker File，在 Docker Hub 中有来自世界各地的工程师编写好的镜像，你可以基于此修改。&lt;/p&gt;
&lt;p&gt;📚此外，Docker 容器提供了一种构建企业应用程序和业务流程应用程序的方法，这些应用程序比传统应用程序更容易安装、维护和移动。&lt;/p&gt;
&lt;p&gt;⭐Docker 容器支持&lt;strong&gt;隔离&lt;/strong&gt;：Docker 容器使应用程序不仅彼此隔离，而且与底层系统隔离。这不仅使软件栈更干净，而且更容易使容器化应用程序使用系统资源，例如 &lt;strong&gt;CPU、GPU、内存、I/O、网络&lt;/strong&gt;等，它还可以&lt;strong&gt;确保数据和代码保持独立&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;⭐Docker 容器支持&lt;strong&gt;可移植性&lt;/strong&gt;：Docker 容器在支持容器运行环境的任何机器上运行。应用程序不必绑定到主机操作系统，因此可以保持应用程序环境和底层操作环境的整洁和最小化。&lt;br&gt;
例如，采用容器的 MySQL 将在大多数支持容器的 Linux 系统上运行，应用程序的所有依赖项通常都在同一个容器中提供。基于容器的应用程序可以轻易从 on-prem 系统迁移到云环境中，或从开发人员的笔记本电脑移到服务器上，只要目标系统支持 Docker 以及可能与之一起使用的任何第三方工具，比如 Kubernetes。&lt;/p&gt;
&lt;p&gt;⭐通常，Docker 容器镜像必须为特定的平台构建。例如 Windows 容器不能在 Linux 上运行，反之亦然；以前，绕过此限制的一种方法是启动运行所需操作系统实例的虚拟机，并在虚拟机中运行容器。&lt;br&gt;
然而 Docker 团队后来设计了一个更优雅的解决方案，称为 &lt;strong&gt;manifest&lt;/strong&gt;，它允许多个操作系统的镜像并行打包。尽管 manifest 还处于试验阶段，但这暗示了容器可能成为跨平台应用程序解决方案和跨环境应用程序解决方案。&lt;/p&gt;
&lt;p&gt;⭐Docker 容器支持&lt;strong&gt;可组合性&lt;/strong&gt;：大多数业务应用程序由几个独立的组件组成，web 服务器、数据库和 cache 缓存。&lt;strong&gt;Docker 容器可以将这些部件组合成一个容易更换的功能单元。每个部分由不同的容器提供，可以独立于其他容器进行维护、更新、交换和修改。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;🔥 这本质上是应用程序设计的&lt;strong&gt;微服务模型&lt;/strong&gt;。通过将应用程序功能划分为独立的、自包含的服务，微服务模型为过程缓慢的传统开发和单一僵化的应用程序提供了一种解决方案，轻量级和便携式容器使构建和维护基于微服务的应用程序变得更加容易。&lt;/p&gt;
&lt;h2&gt;5. 编排系统的需求催生 k8s&lt;/h2&gt;
&lt;p&gt;尽管 Docker 为容器化的应用程序提供了开放标准，但随着容器越来越多出现了一系列新问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何&lt;strong&gt;协调、调度和管理&lt;/strong&gt;这些容器？&lt;/li&gt;
&lt;li&gt;如何在升级应用程序时&lt;strong&gt;不中断服务&lt;/strong&gt;？&lt;/li&gt;
&lt;li&gt;如何&lt;strong&gt;监视&lt;/strong&gt;应用程序的运行状况？&lt;/li&gt;
&lt;li&gt;如何批量重新启动容器里的程序？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决这些问题需要容器编排技术，可以将众多机器抽象，对外呈现出一台超大机器。现在业界比较流行的有：&lt;strong&gt;k8s&lt;/strong&gt;、Mesos、&lt;strong&gt;Docker Swarm&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在业务发展初期只有几个微服务，这时用 Docker 就足够了，但随着业务规模逐渐扩大，容器越来越多，运维人员的工作越来越复杂，这个时候就需要编排系统解救 opers。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141758983.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;一个成熟的容器编排系统需要具备以下能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;处理大量的容器和用户&lt;/li&gt;
&lt;li&gt;负载均衡&lt;/li&gt;
&lt;li&gt;鉴权和安全性&lt;/li&gt;
&lt;li&gt;管理服务通信&lt;/li&gt;
&lt;li&gt;多平台部署&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;🌊 &lt;strong&gt;其中，K8S，就是基于容器的集群管理平台，它的全称，是 kubernetes。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141758996.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;和 Docker 不同，K8S 的创造者，是众人皆知的行业巨头——&lt;strong&gt;Google&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;然而，K8S 并不是一件全新的发明。它的前身，是 Google 自己捣鼓了十多年的 &lt;strong&gt;Borg 系统&lt;/strong&gt;。K8S 是 Google 研发的容器协调器，已捐赠给 CNCF，现已开源。&lt;/p&gt;
&lt;p&gt;Google 利用在容器管理多年的经验和专业知识推出了 k8s，主要用于&lt;strong&gt;自动化部署应用程序容器&lt;/strong&gt;，可以支持众多容器化工具包括现在非常流行的 Docker。&lt;/p&gt;
&lt;p&gt;目前 k8s 是容器编排市场的领导者，开源并公布了一系列标准化方法，主流的公有云平台都宣布支持。&lt;/p&gt;
&lt;p&gt;一流的厂商都在抢占标准的制高点，一堆小厂商跟着一起玩，这就叫&lt;strong&gt;生态&lt;/strong&gt;了。国内的大厂商都在干嘛呢？抢社区团购市场，玩资本游戏，哎？！&lt;/p&gt;
&lt;h2&gt;6. k8s 架构和组件&lt;/h2&gt;
&lt;p&gt;k8s 由众多组件组成，组件间通过 API 互相通信，归纳起来主要分为三个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;controller manager&lt;/li&gt;
&lt;li&gt;nodes&lt;/li&gt;
&lt;li&gt;pods&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;k8s 集群架构图&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141759880.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Controller Manager&lt;/strong&gt;，即控制平面，用于&lt;strong&gt;调度&lt;/strong&gt;程序以及节点状态&lt;strong&gt;检测&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nodes&lt;/strong&gt;，构成了 Kubernetes 集群的集体计算能力，&lt;strong&gt;实际部署容器运行的地方&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pods&lt;/strong&gt;，Kubernetes 集群中&lt;strong&gt;资源的最小单位&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下图是 &lt;strong&gt;Kubernetes 集成 Jenkins 实现 CICD&lt;/strong&gt;（一图胜千言，需要对其有一个大致的认识）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141759376.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;而下图则是 &lt;strong&gt;GitLab + Jenkins Pipeline + Doker + k8s + Helm 自动化部署&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141759900.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;7. k8s 与 Docker Swarm 江湖恩怨&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Docker Swarm&lt;/strong&gt; 与 &lt;strong&gt;k8s&lt;/strong&gt; 同为容器编排技术。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141759963.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你非要拿 Docker 和 k8s 进行比较，其实你更应该拿 &lt;strong&gt;Docker Swarm&lt;/strong&gt; 和 &lt;strong&gt;k8s&lt;/strong&gt; 比较。&lt;/p&gt;
&lt;p&gt;Docker Swarm 是 Docker 自家针对集群化部署管理的解决方案，优点很明显，可以更紧密集成到 Docker 生态系统中。&lt;/p&gt;
&lt;p&gt;虽说 Swarm 是 Docker 亲儿子，但依旧没有 k8s 流行，不流行很大程度是因为商业、生态的原因，不多解释。&lt;/p&gt;
&lt;h2&gt;8. Docker 与 k8s 难舍难分&lt;/h2&gt;
&lt;p&gt;Docker 和 k8s 在业界非常流行，都已经是事实上的标准。&lt;/p&gt;
&lt;p&gt;Docker 是用于构建、分发、运行（Build, Ship and Run）容器的平台和工具。&lt;/p&gt;
&lt;p&gt;而 k8s 实际上是一个使用 Docker 容器进行编排的系统，主要围绕 pods 进行工作。&lt;strong&gt;Pods 是 k8s 生态中最小的调度单位，可以包含一个或多个容器。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Docker 和 k8s 是根本上不同的技术，两者可以很好的协同工作。&lt;/p&gt;
&lt;h2&gt;9. 开发实践，灵魂追问&lt;/h2&gt;
&lt;p&gt;（1）&lt;strong&gt;为什么还要用 k8s？没有 k8s 可以使用 docker 吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以。实际上一些小型公司，在业务不太复杂的情况下都是直接使用 Docker。尽管 k8s 有很多好处，但是众所周知它非常复杂，业务比较简单可以放弃使用 k8s。但 k8s 在业务达到一定规模后也得启用！&lt;/p&gt;
&lt;p&gt;（2）&lt;strong&gt;没有 Docker 可以使用 k8s 吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;k8s 只是一个容器编排器，没有容器拿什么编排？！&lt;/p&gt;
&lt;p&gt;k8s 经常与 Docker 进行搭配使用，但是也可以使用其他容器，如 RunC、Containerted 等。&lt;/p&gt;
&lt;p&gt;（3）&lt;strong&gt;Docker Swarm 和 k8s 怎么选？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;选 k8s。2019 年底 Docker Enterprise 已经出售给 Mirantis，Mirantis 声明要逐步淘汰 Docker Swarm，后续会将 k8s 作为默认编排工具。&lt;/p&gt;</content:encoded><h:img src="/_astro/202311141756070.hDBvTaXK.jpg"/><enclosure url="/_astro/202311141756070.hDBvTaXK.jpg"/></item><item><title>Tips for precise search on GitHub</title><link>https://coooredump.github.io/blog/productivity-tool/tips-for-precise-search-on-github</link><guid isPermaLink="true">https://coooredump.github.io/blog/productivity-tool/tips-for-precise-search-on-github</guid><description>GitHub 上有很多优秀的开源项目与学习资料，如何通过这些资源来抹平你的信息不对称呢？那么你就应该明白我们要如何搜索 GitHub，以下为大家带来精准搜索 GitHub 的神仙技巧。</description><pubDate>Tue, 05 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 普通的搜索📚&lt;/h2&gt;
&lt;p&gt;相信一般人搜索项目时，都是直接搜索技术栈相关的项目。&lt;/p&gt;
&lt;p&gt;高级一点的搜索，会根据 &lt;strong&gt;Best match&lt;/strong&gt;、&lt;strong&gt;Most starts&lt;/strong&gt; ... 来进行排序、选择相应的&lt;strong&gt;语言&lt;/strong&gt;、选择&lt;strong&gt;仓库或者代码&lt;/strong&gt;来进行筛选。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141824550.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是 GitHub 的搜索功能只支持以上这些而已吗 ？&lt;/p&gt;
&lt;p&gt;No！&lt;/p&gt;
&lt;p&gt;如果你只会用以上的功能，那你知道的仅仅是 GitHub 搜索的冰山一角！&lt;/p&gt;
&lt;p&gt;GitHub 的搜索是非常强大的！下面介绍更高级的搜索技巧！&lt;/p&gt;
&lt;h2&gt;2. 搜索语法📚&lt;/h2&gt;
&lt;p&gt;搜索 GitHub 时，你可以构建匹配特定数字和单词的查询。&lt;/p&gt;
&lt;h3&gt;2.1 查询大于或小于另一个值的值&lt;/h3&gt;
&lt;p&gt;你可以使用 &lt;code&gt;&gt;&lt;/code&gt;、&lt;code&gt;&gt;=&lt;/code&gt;、&lt;code&gt;&amp;#x3C;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;=&lt;/code&gt; 搜索大于、大于等于、小于以及小于等于另一个值的值。&lt;/p&gt;
&lt;p&gt;| 查询  | 示例                                                         |
| ----- | ------------------------------------------------------------ |
| &lt;code&gt;&gt;n&lt;/code&gt;  | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bstars%3A%3E1000%26type%3DRepositories&quot;&gt;cats vue:&gt;1000&lt;/a&gt;&lt;/strong&gt; 匹配含有 &quot;vue&quot; 字样、星标超过 1000 个的仓库。 |
| &lt;code&gt;&gt;=n&lt;/code&gt; | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Btopics%3A%3E%3D5%26type%3DRepositories&quot;&gt;vue topics:&gt;=5&lt;/a&gt;&lt;/strong&gt; 匹配含有 &quot;vue&quot; 字样、有 5 个或更多主题的仓库。 |
| &lt;code&gt;&amp;#x3C;n&lt;/code&gt;  | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bsize%3A%3C10000%26type%3DCode&quot;&gt;vue size:&amp;#x3C;10000&lt;/a&gt;&lt;/strong&gt; 匹配小于 10 KB 的文件中含有 &quot;vue&quot; 字样的代码。 |
| &lt;code&gt;&amp;#x3C;=n&lt;/code&gt; | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bstars%3A%3C%3D50%26type%3DRepositories&quot;&gt;vue stars:&amp;#x3C;=50&lt;/a&gt;&lt;/strong&gt; 匹配含有 &quot;vue&quot; 字样、星标不超过 50 个的仓库。 |&lt;/p&gt;
&lt;p&gt;你还可以使用&lt;strong&gt;范围查询&lt;/strong&gt;：搜索大于等于或小于等于另一个值的值。&lt;/p&gt;
&lt;p&gt;| 查询   | 示例                                                         |
| ------ | ------------------------------------------------------------ |
| &lt;code&gt;n..*&lt;/code&gt; | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bstars%3A10..*%26type%3DRepositories&quot;&gt;vue stars:10..*&lt;/a&gt;&lt;/strong&gt; 等同于 &lt;code&gt;stars:&gt;=10&lt;/code&gt; 并匹配含有 &quot;vue&quot; 字样、有 10 个或更多星号的仓库。 |
| &lt;code&gt;*..n&lt;/code&gt; | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bstars%3A%22*..10%22%26type%3DRepositories&quot;&gt;vue stars:*..10&lt;/a&gt;&lt;/strong&gt; 等同于 &lt;code&gt;stars:&amp;#x3C;=10&lt;/code&gt; 并匹配含有 &quot;vue&quot; 字样、有不超过 10 个星号的仓库。 |&lt;/p&gt;
&lt;h3&gt;2.2 查询范围之间的值&lt;/h3&gt;
&lt;p&gt;你可以使用范围语法 &lt;code&gt;n..n&lt;/code&gt; 搜索范围内的值，其中第一个数字 &lt;em&gt;n&lt;/em&gt; 是最小值，而第二个 &lt;em&gt;n&lt;/em&gt; 是最大值。&lt;/p&gt;
&lt;p&gt;| 查询   | 示例                                                         |
| ------ | ------------------------------------------------------------ |
| &lt;code&gt;n..n&lt;/code&gt; | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dcats%2Bstars%3A10..50%26type%3DRepositories&quot;&gt;vue stars:10..50&lt;/a&gt;&lt;/strong&gt; 匹配含有 &quot;vue&quot; 字样、有 10 到 50 个星号的仓库。 |&lt;/p&gt;
&lt;h3&gt;2.3 查询日期&lt;/h3&gt;
&lt;p&gt;你可以通过使用 &lt;code&gt;&gt;&lt;/code&gt;、&lt;code&gt;&gt;=&lt;/code&gt;、&lt;code&gt;&amp;#x3C;&lt;/code&gt;、&lt;code&gt;&amp;#x3C;=&lt;/code&gt; 和 范围查询 搜索早于或晚于另一个日期，或者位于日期范围内的日期。&lt;/p&gt;
&lt;p&gt;日期格式必须遵循 &lt;a href=&quot;https://link.juejin.cn?target=http%3A%2F%2Fen.wikipedia.org%2Fwiki%2FISO_8601&quot;&gt;ISO8601&lt;/a&gt; 标准，即 &lt;code&gt;YYYY-MM-DD&lt;/code&gt;（年-月-日）。&lt;/p&gt;
&lt;p&gt;| 查询                     | 示例                                                         |
| ------------------------ | ------------------------------------------------------------ |
| &lt;code&gt;&gt;YYYY-MM-DD&lt;/code&gt;            | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bcreated%3A%3E2016-04-29%26type%3DIssues&quot;&gt;vue created:&gt;2016-04-29&lt;/a&gt;&lt;/strong&gt; 匹配含有 &quot;vue&quot; 字样、在 2016 年 4 月 29 日之后创建的议题。 |
| &lt;code&gt;&gt;=YYYY-MM-DD&lt;/code&gt;           | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bcreated%3A%3E%3D2017-04-01%26type%3DIssues&quot;&gt;vue created:&gt;=2017-04-01&lt;/a&gt;&lt;/strong&gt; 匹配含有 &quot;vue&quot; 字样、在 2017 年 4 月 1 日或之后创建的议题。 |
| &lt;code&gt;&amp;#x3C;YYYY-MM-DD&lt;/code&gt;            | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dvue%2Bpushed%3A%3C2012-07-05%26type%3DCode%26utf8%3D%E2%9C%93&quot;&gt;vue pushed:&amp;#x3C;2012-07-05&lt;/a&gt;&lt;/strong&gt; 匹配在 2012 年 7 月 5 日之前推送的仓库中含有 &quot;vue&quot; 字样的代码。 |
| &lt;code&gt;&amp;#x3C;=YYYY-MM-DD&lt;/code&gt;           | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bcreated%3A%3C%3D2012-07-04%26type%3DIssues&quot;&gt;vue created:&amp;#x3C;=2012-07-04&lt;/a&gt;&lt;/strong&gt; 匹配含有 &quot;vue&quot; 字样、在 2012 年 7 月 4 日或之前创建的议题。 |
| &lt;code&gt;YYYY-MM-DD..YYYY-MM-DD&lt;/code&gt; | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bpushed%3A2016-04-30..2016-07-04%26type%3DRepositories&quot;&gt;vue pushed:2016-04-30..2016-07-04&lt;/a&gt;&lt;/strong&gt; 匹配含有 &quot;vue&quot; 字样、在 2016 年 4 月末到 7 月之间推送的仓库。 |
| &lt;code&gt;YYYY-MM-DD..*&lt;/code&gt;          | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bcreated%3A2012-04-30..*%26type%3DIssues&quot;&gt;vue created:2012-04-30..*&lt;/a&gt;&lt;/strong&gt; 匹配在 2012 年 4 月 30 日之后创建、含有 &quot;vue&quot; 字样的议题。 |
| &lt;code&gt;*..YYYY-MM-DD&lt;/code&gt;          | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bcreated%3A*..2012-07-04%26type%3DIssues&quot;&gt;vue created:*..2012-04-30&lt;/a&gt;&lt;/strong&gt; 匹配在 2012 年 7 月 4 日之前创建、含有 &quot;vue&quot; 字样的议题。 |&lt;/p&gt;
&lt;p&gt;你也可以在日期后添加可选的时间信息 &lt;code&gt;THH:MM:SS+00:00&lt;/code&gt;，以便按小时、分钟和秒进行搜索。 这是 &lt;code&gt;T&lt;/code&gt;，随后是 &lt;code&gt;HH:MM:SS&lt;/code&gt;（时-分-秒）和 UTC 偏移 (&lt;code&gt;+00:00&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;| 查询                        | 示例                                                         |
| --------------------------- | ------------------------------------------------------------ |
| &lt;code&gt;YYYY-MM-DDTHH:MM:SS+00:00&lt;/code&gt; | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bcreated%3A2017-01-01T01%3A00%3A00%2B07%3A00..2017-03-01T15%3A30%3A15%2B07%3A00%26type%3DIssues&quot;&gt;vue created:2017-01-01T01:00:00+07:00..2017-03-01T15:30:15+07:00&lt;/a&gt;&lt;/strong&gt; 匹配在 2017 年 1 月 1 日凌晨 1 点（UTC 偏移为 &lt;code&gt;07:00&lt;/code&gt;）与 2017 年 3 月 1 日下午 3 点（UTC 偏移为 &lt;code&gt;07:00&lt;/code&gt;）之间创建的议题。 UTC 偏移量 &lt;code&gt;07:00&lt;/code&gt;，2017 年 3 月 1 日下午 3 点。 UTC 偏移量 &lt;code&gt;07:00&lt;/code&gt;。 |
| &lt;code&gt;YYYY-MM-DDTHH:MM:SSZ&lt;/code&gt;      | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dvue%2Bcreated%3A2016-03-21T14%3A11%3A00Z..2016-04-07T20%3A45%3A00Z%26type%3DIssues&quot;&gt;vue created:2016-03-21T14:11:00Z..2016-04-07T20:45:00Z&lt;/a&gt;&lt;/strong&gt; 匹配在 2016 年 3 月 21 日下午 2:11 与 2016 年 4 月 7 日晚上 8:45 之间创建的议题。 |&lt;/p&gt;
&lt;h3&gt;2.4 排除特定结果&lt;/h3&gt;
&lt;p&gt;你可以使用 &lt;code&gt;NOT&lt;/code&gt; 语法排除包含特定字词的结果。 &lt;code&gt;NOT&lt;/code&gt; 运算符只能用于字符串关键词， 不适用于数字或日期。&lt;/p&gt;
&lt;p&gt;| 查询  | 示例                                                         |
| ----- | ------------------------------------------------------------ |
| &lt;code&gt;NOT&lt;/code&gt; | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dhello%2BNOT%2Bworld%26type%3DRepositories&quot;&gt;hello NOT world&lt;/a&gt;&lt;/strong&gt; 匹配含有 &quot;hello&quot; 字样但不含有 &quot;world&quot; 字样的仓库。 |&lt;/p&gt;
&lt;p&gt;缩小搜索结果范围的另一种途径是排除特定的子集。 你可以为任何搜索限定符添加 &lt;code&gt;-&lt;/code&gt; 前缀，以排除该限定符匹配的所有结果。&lt;/p&gt;
&lt;p&gt;| 查询         | 示例                                                         |
| ------------ | ------------------------------------------------------------ |
| &lt;code&gt;-QUALIFIER&lt;/code&gt; | &lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dvue%2Bstars%3A%3E10%2B-language%3Ajavascript%26type%3DRepositories&quot;&gt;vue stars:&gt;10 -language:javascript&lt;/a&gt;&lt;/strong&gt; 匹配含有 &quot;vue&quot; 字样、有超过 10 个星号但并非以 JavaScript 编写的仓库。 |
|              | &lt;strong&gt;&lt;a href=&quot;https://github.com/search?q=mentions%3AWu-Yikun+-org%3Agithub&amp;#x26;type=Issues&quot;&gt;mentions:Wu-Yikun -org:github&lt;/a&gt;&lt;/strong&gt; 匹配提及 @Wu-Yikun 且不在 GitHub 组织仓库中的议题 |&lt;/p&gt;
&lt;h3&gt;2.5 对带有空格的查询使用引号&lt;/h3&gt;
&lt;p&gt;如果搜索含有空格的查询，你需要用引号将其括起来。 例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/search?q=vue+cats+NOT+%22hello+world%22&amp;#x26;type=Repositories&quot;&gt;vue cats NOT &quot;hello world&quot;&lt;/a&gt; 匹配含有 &quot;vue&quot; 字样但不含有 &quot;hello world&quot; 字样的仓库。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dbuild%2Blabel%3A%22bug%2Bfix%22%26type%3DIssues&quot;&gt;build label:&quot;bug fix&quot;&lt;/a&gt; 匹配具有标签 &quot;bug fix&quot;、含有 &quot;build&quot; 字样的议题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;某些非字母数字符号（例如空格）会从引号内的代码搜索查询中删除，因此结果可能出乎意料。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2.6 使用用户名的查询&lt;/h3&gt;
&lt;p&gt;如果搜索查询包含需要用户名的限定符，例如 &lt;code&gt;user&lt;/code&gt;、&lt;code&gt;actor&lt;/code&gt; 或 &lt;code&gt;assignee&lt;/code&gt;，你可以使用任何 GitHub 用户名指定特定人员，或使用 &lt;code&gt;@me&lt;/code&gt; 指定当前用户。&lt;/p&gt;
&lt;p&gt;| 查询                 | 示例                                                         |
| -------------------- | ------------------------------------------------------------ |
| &lt;code&gt;QUALIFIER:USERNAME&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dauthor%3Anat%26type%3DCommits&quot;&gt;&lt;code&gt;author:biaochenxuying&lt;/code&gt;&lt;/a&gt; 匹配 @biaochenxuying 创作的提交。 |
| &lt;code&gt;QUALIFIER:@me&lt;/code&gt;      | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dis%3Aissue%2Bassignee%3A%40me%26type%3DIssues&quot;&gt;&lt;code&gt;is:issue assignee:@me&lt;/code&gt;&lt;/a&gt; 匹配已分配给结果查看者的议题 |&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@me&lt;/code&gt; 只能与限定符一起使用，而不能用作搜索词，例如 &lt;code&gt;@me main.workflow&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;3. 高级的搜索📚&lt;/h2&gt;
&lt;h3&gt;3.1 按仓库名称、说明或自述文件内容搜索&lt;/h3&gt;
&lt;p&gt;通过 &lt;code&gt;in&lt;/code&gt; 限定符，你可以将搜索限制为仓库名称、仓库说明、自述文件内容或这些的任意组合。&lt;/p&gt;
&lt;p&gt;如果省略此限定符，则只搜索仓库名称和说明。&lt;/p&gt;
&lt;p&gt;| 限定符            | 示例                                                         |
| ----------------- | ------------------------------------------------------------ |
| &lt;code&gt;in:name&lt;/code&gt;         | &lt;a href=&quot;https://github.com/search?q=jquery+in%3Aname&amp;#x26;type=Repositories&quot;&gt;&lt;strong&gt;jquery in:name&lt;/strong&gt;&lt;/a&gt; 匹配其名称中含有 &quot;jquery&quot; 的仓库。 |
| &lt;code&gt;in:description&lt;/code&gt;  | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dvue%2Bin%3Aname%2Cdescription%26type%3DRepositories&quot;&gt;&lt;strong&gt;vue in:name,description&lt;/strong&gt;&lt;/a&gt; 匹配其名称或说明中含有 &quot;vue&quot; 的仓库。 |
| &lt;code&gt;in:readme&lt;/code&gt;       | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dvue%2Bin%3Areadme%26type%3DRepositories&quot;&gt;&lt;strong&gt;vue in:readme&lt;/strong&gt;&lt;/a&gt; 匹配其自述文件中提及 &quot;vue&quot; 的仓库。 |
| &lt;code&gt;repo:owner/name&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Drepo%3Abiaochenxuying%2Fblog&quot;&gt;&lt;strong&gt;repo:biaochenxuying/blog&lt;/strong&gt;&lt;/a&gt; 匹配特定仓库名称，比如：用户为 biaochenxuying 的 blog 项目。 |&lt;/p&gt;
&lt;h3&gt;3.2 在用户或组织的仓库内搜索&lt;/h3&gt;
&lt;p&gt;要在 &lt;code&gt;特定用户或组织&lt;/code&gt; 拥有的所有仓库中搜索，你可以使用 &lt;code&gt;user&lt;/code&gt; 或 &lt;code&gt;org&lt;/code&gt; 限定符。&lt;/p&gt;
&lt;p&gt;| 限定符          | 示例                                                         |
| --------------- | ------------------------------------------------------------ |
| &lt;code&gt;user:USERNAME&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Duser%3Abiaochenxuying%2Bforks%3A%3E%3D100%26type%3DRepositories&quot;&gt;&lt;strong&gt;user:biaochenxuying forks:&gt;=100&lt;/strong&gt;&lt;/a&gt; 匹配来自 @biaochenxuying、拥有超过 100 fork 的仓库。 |
| &lt;code&gt;org:ORGNAME&lt;/code&gt;   | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dorg%3Agithub%26type%3DRepositories&quot;&gt;&lt;strong&gt;org:github&lt;/strong&gt;&lt;/a&gt; 匹配来自 GitHub 的仓库。 |&lt;/p&gt;
&lt;h3&gt;3.3 按仓库大小搜索&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;size&lt;/code&gt; 限定符使用 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Farticles%2Funderstanding-the-search-syntax&quot;&gt;大于、小于和范围限定符&lt;/a&gt; 查找匹配特定大小（以千字节为单位）的仓库。&lt;/p&gt;
&lt;p&gt;| 限定符   | 示例                                                         |
| -------- | ------------------------------------------------------------ |
| &lt;code&gt;size:n&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dsize%3A1000%26type%3DRepositories&quot;&gt;&lt;strong&gt;size:1000&lt;/strong&gt;&lt;/a&gt; 匹配恰好为 1 MB 的仓库。 |
|          | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dsize%3A%3E%3D30000%26type%3DRepositories&quot;&gt;&lt;strong&gt;size:&gt;=30000&lt;/strong&gt;&lt;/a&gt; 匹配至少为 30 MB 的仓库。 |
|          | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dsize%3A%3C50%26type%3DRepositories&quot;&gt;&lt;strong&gt;size:&amp;#x3C;50&lt;/strong&gt;&lt;/a&gt; 匹配小于 50 KB 的仓库。 |
|          | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dsize%3A50..120%26type%3DRepositories&quot;&gt;&lt;strong&gt;size:50..120&lt;/strong&gt;&lt;/a&gt; 匹配介于 50 KB 与 120 KB 之间的仓库。 |&lt;/p&gt;
&lt;h3&gt;3.4 按 followers 搜索&lt;/h3&gt;
&lt;p&gt;你可以使用 &lt;code&gt;followers&lt;/code&gt; 限定符以及&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Farticles%2Funderstanding-the-search-syntax&quot;&gt;大于、小于和范围限定符&lt;/a&gt;基于仓库拥有的关注者数量过滤仓库。&lt;/p&gt;
&lt;p&gt;| 限定符        | 示例                                                         |
| ------------- | ------------------------------------------------------------ |
| &lt;code&gt;followers:n&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dnode%2Bfollowers%3A%3E%3D10000&quot;&gt;&lt;strong&gt;node followers:&gt;=10000&lt;/strong&gt;&lt;/a&gt; 匹配有 10,000 或更多关注者提及文字 &quot;node&quot; 的仓库。 |
|               | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dstyleguide%2Blinter%2Bfollowers%3A1..10%26type%3DRepositories&quot;&gt;&lt;strong&gt;styleguide linter followers:1..10&lt;/strong&gt;&lt;/a&gt; 匹配拥有 1 到 10 个关注者并且提及 &quot;styleguide linter&quot; 一词的的仓库。 |&lt;/p&gt;
&lt;h3&gt;3.5 按 forks 搜索&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;forks&lt;/code&gt; 限定符使用&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Farticles%2Funderstanding-the-search-syntax&quot;&gt;大于、小于和范围限定符&lt;/a&gt;指定仓库应具有的复刻数量。&lt;/p&gt;
&lt;p&gt;| 限定符    | 示例                                                         |
| --------- | ------------------------------------------------------------ |
| &lt;code&gt;forks:n&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dforks%3A5%26type%3DRepositories&quot;&gt;&lt;strong&gt;forks:5&lt;/strong&gt;&lt;/a&gt; 匹配只有 5 个复刻的仓库。 |
|           | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dforks%3A%3E%3D205%26type%3DRepositories&quot;&gt;&lt;strong&gt;forks:&gt;=205&lt;/strong&gt;&lt;/a&gt; 匹配具有至少 205 个复刻的仓库。 |
|           | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dforks%3A%3C90%26type%3DRepositories&quot;&gt;&lt;strong&gt;forks:&amp;#x3C;90&lt;/strong&gt;&lt;/a&gt; 匹配具有少于 90 个复刻的仓库。 |
|           | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dforks%3A10..20%26type%3DRepositories&quot;&gt;&lt;strong&gt;forks:10..20&lt;/strong&gt;&lt;/a&gt; 匹配具有 10 到 20 个复刻的仓库。 |&lt;/p&gt;
&lt;h3&gt;3.6 按 stars 数量搜索&lt;/h3&gt;
&lt;p&gt;你可以使用 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Farticles%2Funderstanding-the-search-syntax&quot;&gt;大于、小于和范围限定符&lt;/a&gt; 基于仓库具有的 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Farticles%2Fsaving-repositories-with-stars&quot;&gt;星标&lt;/a&gt; 数量搜索仓库&lt;/p&gt;
&lt;p&gt;| 限定符    | 示例                                                         |
| --------- | ------------------------------------------------------------ |
| &lt;code&gt;stars:n&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dstars%3A500%26type%3DRepositories&quot;&gt;&lt;strong&gt;stars:500&lt;/strong&gt;&lt;/a&gt; 匹配恰好具有 500 个星号的仓库。 |
|           | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dstars%3A10..20%2Bsize%3A%3C1000%26type%3DRepositories&quot;&gt;&lt;strong&gt;stars:10..20&lt;/strong&gt;&lt;/a&gt; 匹配具有 10 到 20 个星号、小于 1000 KB 的仓库。 |
|           | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dstars%3A%3E%3D500%2Bfork%3Atrue%2Blanguage%3Avue%26type%3DRepositories&quot;&gt;&lt;strong&gt;stars:&gt;=500 fork:true language:vue&lt;/strong&gt;&lt;/a&gt; 匹配具有至少 500 个星号，包括复刻的星号（以 vue 编写）的仓库。 |&lt;/p&gt;
&lt;h3&gt;3.7 按仓库创建或上次更新时间搜索&lt;/h3&gt;
&lt;p&gt;你可以基于创建时间或上次更新时间过滤仓库。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于&lt;strong&gt;仓库创建的时间&lt;/strong&gt;，你可以使用 &lt;code&gt;created&lt;/code&gt; 限定符；&lt;/li&gt;
&lt;li&gt;要了解&lt;strong&gt;仓库上次更新的时间&lt;/strong&gt;，你要使用 &lt;code&gt;pushed&lt;/code&gt; 限定符。 &lt;code&gt;pushed&lt;/code&gt; 限定符将返回仓库列表，按仓库中任意分支上最近进行的提交排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两者均采用日期作为参数。 日期格式必须遵循 ISO8601 标准，即 &lt;code&gt;YYYY-MM-DD&lt;/code&gt;（年-月-日）。&lt;/p&gt;
&lt;p&gt;也可以在日期后添加可选的时间信息 &lt;code&gt;THH:MM:SS+00:00&lt;/code&gt;，以便按小时、分钟和秒进行搜索。 这是 &lt;code&gt;T&lt;/code&gt;，随后是 &lt;code&gt;HH:MM:SS&lt;/code&gt;（时-分-秒）和 UTC 偏移 (&lt;code&gt;+00:00&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;日期支持 &lt;code&gt;大于、小于和范围限定符&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;| 限定符               | 示例                                                         |
| -------------------- | ------------------------------------------------------------ |
| &lt;code&gt;created:YYYY-MM-DD&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dvue%2Bcreated%3A%3C2020-01-01%26type%3DRepositories&quot;&gt;&lt;strong&gt;vue created:&amp;#x3C;2020-01-01&lt;/strong&gt;&lt;/a&gt; 匹配具有 &quot;vue&quot; 字样、在 2020 年之前创建的仓库。 |
| &lt;code&gt;pushed:YYYY-MM-DD&lt;/code&gt;  | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dcss%2Bpushed%3A%3E2020-02-01%26type%3DRepositories&quot;&gt;&lt;strong&gt;css pushed:&gt;2020-02-01&lt;/strong&gt;&lt;/a&gt; 匹配具有 &quot;css&quot; 字样、在 2020 年 1 月之后收到推送的仓库。 |
|                      | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dvue%2Bpushed%3A%3E%3D2020-03-06%2Bfork%3Aonly%26type%3DRepositories&quot;&gt;&lt;strong&gt;vue pushed:&gt;=2020-03-06 fork:only&lt;/strong&gt;&lt;/a&gt; 匹配具有 &quot;vue&quot; 字样、在 2020 年 3 月 6 日或之后收到推送并且作为复刻的仓库。 |&lt;/p&gt;
&lt;h3&gt;3.8 按语言搜索&lt;/h3&gt;
&lt;p&gt;你可以基于其编写采用的主要语言搜索仓库。&lt;/p&gt;
&lt;p&gt;| 限定符              | 示例                                                         |
| ------------------- | ------------------------------------------------------------ |
| &lt;code&gt;language:LANGUAGE&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dvue%2Blanguage%3Ajavascript%26type%3DRepositories&quot;&gt;&lt;strong&gt;vue language:javascript&lt;/strong&gt;&lt;/a&gt; 匹配具有 &quot;vue&quot; 字样、以 JavaScript 编写的仓库。 |&lt;/p&gt;
&lt;h3&gt;3.9 按主题搜索&lt;/h3&gt;
&lt;p&gt;你可以查找归类为特定 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Farticles%2Fclassifying-your-repository-with-topics&quot;&gt;主题&lt;/a&gt; 的所有仓库。&lt;/p&gt;
&lt;p&gt;| 限定符        | 示例                                                         |
| ------------- | ------------------------------------------------------------ |
| &lt;code&gt;topic:TOPIC&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dtopic%3Aalgorithm%26type%3DRepositories%26ref%3Dsearchresults&quot;&gt;&lt;strong&gt;topic:algorithm&lt;/strong&gt;&lt;/a&gt; 匹配已归类为 &quot;algorithm&quot; 主题的仓库。 |&lt;/p&gt;
&lt;p&gt;估计又有很多人不知道 GitHub 上有话题一说的吧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141825666.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141825050.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;3.10 按主题数量搜索&lt;/h3&gt;
&lt;p&gt;你可以使用 &lt;code&gt;topics&lt;/code&gt; 限定符以及 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Farticles%2Funderstanding-the-search-syntax&quot;&gt;大于、小于和范围限定符&lt;/a&gt; 按应用于仓库的 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Farticles%2Fclassifying-your-repository-with-topics&quot;&gt;主题&lt;/a&gt; 数量搜索仓库。&lt;/p&gt;
&lt;p&gt;| 限定符     | 示例                                                         |
| ---------- | ------------------------------------------------------------ |
| &lt;code&gt;topics:n&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dtopics%3A5%26type%3DRepositories%26ref%3Dsearchresults&quot;&gt;&lt;strong&gt;topics:5&lt;/strong&gt;&lt;/a&gt; 匹配具有五个主题的仓库。 |
|            | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dtopics%3A%3E3%26type%3DRepositories%26ref%3Dsearchresults&quot;&gt;&lt;strong&gt;topics:&gt;3&lt;/strong&gt;&lt;/a&gt; 匹配超过三个主题的仓库。 |&lt;/p&gt;
&lt;h3&gt;3.11 使用可视界面搜索&lt;/h3&gt;
&lt;p&gt;还可以使用 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch&quot;&gt;search&lt;/a&gt; page 或 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%2Fadvanced&quot;&gt;advanced search&lt;/a&gt; page 搜索 GitHub 哦。&lt;/p&gt;
&lt;p&gt;这种搜索方式，估计就更少人知道了吧。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%2Fadvanced&quot;&gt;advanced search&lt;/a&gt; page 提供用于构建搜索查询的可视界面。&lt;/p&gt;
&lt;p&gt;你可以按各种因素过滤搜索，例如仓库具有的星标数或复刻数。 在填写高级搜索字段时，你的查询将在顶部搜索栏中自动构建。&lt;/p&gt;
&lt;h3&gt;3.12 按许可搜索&lt;/h3&gt;
&lt;p&gt;你可以按其&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Farticles%2Flicensing-a-repository&quot;&gt;许可&lt;/a&gt;搜索仓库。 你必须使用&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Farticles%2Flicensing-a-repository%2F%23searching-github-by-license-type&quot;&gt;许可关键词&lt;/a&gt;按特定许可或许可系列过滤仓库。&lt;/p&gt;
&lt;p&gt;| 限定符                    | 示例                                                         |
| ------------------------- | ------------------------------------------------------------ |
| &lt;code&gt;license:LICENSE_KEYWORD&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dlicense%3Aapache-2.0%26type%3DRepositories%26ref%3Dsearchresults&quot;&gt;&lt;strong&gt;license:apache-2.0&lt;/strong&gt;&lt;/a&gt; 匹配根据 Apache License 2.0 授权的仓库。 |&lt;/p&gt;
&lt;h3&gt;3.13 按公共或私有仓库搜索&lt;/h3&gt;
&lt;p&gt;你可以基于仓库是&lt;strong&gt;公共&lt;/strong&gt;还是&lt;strong&gt;私有&lt;/strong&gt;，以此过滤搜索。&lt;/p&gt;
&lt;p&gt;| 限定符       | 示例                                                         |
| ------------ | ------------------------------------------------------------ |
| &lt;code&gt;is:public&lt;/code&gt;  | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Fq%3Dis%3Apublic%2Borg%3Agithub%26type%3DRepositories%26utf8%3D%E2%9C%93&quot;&gt;&lt;strong&gt;is:public org:github&lt;/strong&gt;&lt;/a&gt; 匹配 GitHub 拥有的公共仓库。 |
| &lt;code&gt;is:private&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dpages%2Bis%3Aprivate%26type%3DRepositories&quot;&gt;&lt;strong&gt;is:private pages&lt;/strong&gt;&lt;/a&gt; 匹配你有访问权限且包含 &quot;pages&quot; 字样的私有仓库。 |&lt;/p&gt;
&lt;h3&gt;3.14 按仓库是否为镜像&lt;/h3&gt;
&lt;p&gt;你可以根据仓库是否为&lt;strong&gt;镜像&lt;/strong&gt;以及托管于其他位置托管来搜索它们。&lt;/p&gt;
&lt;p&gt;| 限定符         | 示例                                                         |
| -------------- | ------------------------------------------------------------ |
| &lt;code&gt;mirror:true&lt;/code&gt;  | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dmirror%3Atrue%2BGNOME%26type%3D&quot;&gt;&lt;strong&gt;mirror:true GNOME&lt;/strong&gt;&lt;/a&gt; 匹配是镜像且包含 &quot;GNOME&quot; 字样的仓库。 |
| &lt;code&gt;mirror:false&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dmirror%3Afalse%2BGNOME%26type%3D&quot;&gt;&lt;strong&gt;mirror:false GNOME&lt;/strong&gt;&lt;/a&gt; 匹配并非镜像且包含 &quot;GNOME&quot; 字样的仓库。 |&lt;/p&gt;
&lt;h3&gt;3.15 基于仓库是否已存档搜索&lt;/h3&gt;
&lt;p&gt;你可以基于仓库是否&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Farticles%2Fabout-archiving-repositories&quot;&gt;已存档&lt;/a&gt;来搜索仓库。&lt;/p&gt;
&lt;p&gt;| 限定符           | 示例                                                         |
| ---------------- | ------------------------------------------------------------ |
| &lt;code&gt;archived:true&lt;/code&gt;  | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Darchived%3Atrue%2BGNOME%26type%3D&quot;&gt;&lt;strong&gt;archived:true GNOME&lt;/strong&gt;&lt;/a&gt; 匹配已存档且包含 &quot;GNOME&quot; 字样的仓库。 |
| &lt;code&gt;archived:false&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Darchived%3Afalse%2BGNOME%26type%3D&quot;&gt;&lt;strong&gt;archived:false GNOME&lt;/strong&gt;&lt;/a&gt; 匹配未存档且包含 &quot;GNOME&quot; 字样的仓库。 |&lt;/p&gt;
&lt;h3&gt;3.16 基于具有 &lt;code&gt;good first issue&lt;/code&gt; 或 &lt;code&gt;help wanted&lt;/code&gt; 标签的议题数量搜索&lt;/h3&gt;
&lt;p&gt;你可以使用限定符 &lt;code&gt;help-wanted-issues:&gt;n&lt;/code&gt; 和 &lt;code&gt;good-first-issues:&gt;n&lt;/code&gt; 搜索具有最少数量标签为 &lt;code&gt;help-wanted&lt;/code&gt; 或 &lt;code&gt;good-first-issue&lt;/code&gt; 议题的仓库。&lt;/p&gt;
&lt;p&gt;| 限定符                  | 示例                                                         |
| ----------------------- | ------------------------------------------------------------ |
| &lt;code&gt;good-first-issues:&gt;n&lt;/code&gt;  | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Djavascript%2Bgood-first-issues%3A%3E2%26type%3D&quot;&gt;&lt;strong&gt;good-first-issues:&gt;2 javascript&lt;/strong&gt;&lt;/a&gt; 匹配具有超过两个标签为 &lt;code&gt;good-first-issue&lt;/code&gt; 的议题且包含 &quot;javascript&quot; 字样的仓库。 |
| &lt;code&gt;help-wanted-issues:&gt;n&lt;/code&gt; | &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsearch%3Futf8%3D%E2%9C%93%26q%3Dreact%2Bhelp-wanted-issues%3A%3E4%26type%3D&quot;&gt;&lt;strong&gt;help-wanted-issues:&gt;4 react&lt;/strong&gt;&lt;/a&gt; 匹配具有超过四个标签为 &lt;code&gt;help-wanted&lt;/code&gt; 的议题且包含 &quot;React&quot; 字样的仓库。 |&lt;/p&gt;
&lt;h2&gt;4. 更多技巧&lt;/h2&gt;
&lt;p&gt;其实，以上很多内容的都是来自于 GitHub 的官方文档，如果你还想学习更多技巧，请看 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn&quot;&gt;GitHub 官方文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://docs.github.com/en/github&quot;&gt;GitHub Docs&lt;/a&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141825967.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你还不了解或者还不会使用 GitHub ，可以看看这一章节：&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.github.com%2Fcn%2Ffree-pro-team%40latest%2Fgithub%2Fgetting-started-with-github%2Fgit-and-github-learning-resources&quot;&gt;Git 和 GitHub 学习资源&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/202501222159950.DntrDsIj.png"/><enclosure url="/_astro/202501222159950.DntrDsIj.png"/></item><item><title>本科生如何才能进入 BAT 等一流互联网大厂</title><link>https://coooredump.github.io/blog/future-survivor/how-can-undergraduates-enter-bat</link><guid isPermaLink="true">https://coooredump.github.io/blog/future-survivor/how-can-undergraduates-enter-bat</guid><description>成长的过程中，我发现身边的大环境是，总是会预设一个最优路径。中学时代大家的注意力都在高考上，觉得上了好大学就可以万事大吉。搞竞赛的同学容易认为打好 ACM 就可以获得一切。CSer 整日想法设法地想要进 BAT、谷歌。投资人对共享单车、共享充电宝这些项目趋之若鹜、蜂拥而上。然而，名校是终点吗？ACM World Final 是终点吗？Google 优雅舒适的工作环境里和身为谷歌员工的逼格是终点吗？或许从一开始我们就错了，不该过分执迷于一个成就、一个被预设为完美，得到之后却终究归于平淡的的 title...</description><pubDate>Thu, 09 Sep 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;分几点讲讲，校招最重要的素质都有哪些。&lt;/p&gt;
&lt;h2&gt;01&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;首先是项目经历。在国内找工作，尤其是非微软、谷歌等外企的情况下，这往往是重中之重。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当然，作为本科生，尤其是处于正在找实习阶段的本科生，这点要求可以相对放缓。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在最理想的状态下，你应该讲出能够让面试官听懂的、让面试官觉得你牛逼且方向对口的项目。这三点按重要程度从高到低排序。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⭐你做的事情应该能够让面试官听明白，这是最低也是最重要的一个要求。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;项目 low 不要紧，哪怕是讲课程设计，也聊胜于无。把话说清楚就行。毕竟哪怕项目不合心意，面试官还是可以转而从你扎实的专业基础或是灵活的解题思路上寻找亮点。&lt;/p&gt;
&lt;p&gt;面试终究是发生在人与人之间的一种羁绊。问答与交流只是一种手段，对于求职者而言，终极目的还是为了调动面试官的情绪，建立对自己的正面印象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;能让面试官对自己产生钦慕之心，自然是最高的追求&lt;/strong&gt;。反过来讲，面试很忌讳在两人之间形成一种微妙的龃龉。&lt;/p&gt;
&lt;p&gt;一个没给人家讲明白的项目，就像聊天群里除了你以外没人 get 到点的冷笑话般尴尬。不但没有意义，兴许还会产生负面作用。&lt;/p&gt;
&lt;p&gt;作为未来同事的候选人，面试官难免要因此质疑一下你的交流沟通能力能否 Hold 住可能的项目合作与交接。&lt;/p&gt;
&lt;p&gt;说到这里突然想起一个很多搞竞赛的同学会遇到的尴尬面试题：总会有一些不知道 ACM 竞赛有几个人组队的 b 面试官，在你做完自我介绍以后，冷不丁上来就让你直接给讲一个在 ACM 里做过的最难的算法题。&lt;/p&gt;
&lt;p&gt;毕竟术业有专攻，面试官不懂不能强求，这不是他的过错。&lt;/p&gt;
&lt;p&gt;可有些比较实在的同学，这时候就会真的给上一个爆难的算法题来维护竞赛选手的尊严。大致讲一遍解题流程，他不懂。&lt;/p&gt;
&lt;p&gt;接着细讲...&lt;/p&gt;
&lt;p&gt;结果四十分钟过去了，你会发现你们还在绕预处理数据时用到的一个小结论是怎么来的。面试官看时间到了，就客客气气请你回去等消息，换下一位进门~&lt;/p&gt;
&lt;p&gt;这样的故事我听多了，反正至今还不知道有谁在这种情况下最后面试通过的 XD。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;毕竟生活在这世界上，谁都不是一座孤岛；没有理解也就没有爱。面试也是同理。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;02&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;然后是，你需要面试官觉得你牛x&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如前面所说的，这种牛逼构筑于被理解的基础之上，是项目经历的核心所在。&lt;/p&gt;
&lt;p&gt;牛逼这个词其实微秒，说复杂也复杂。&lt;/p&gt;
&lt;p&gt;但说到底仍然是一种情绪、一种主观的印象。举个不恰当的、极端的例子：一个好项目，如果是放在一本学生身上，面试官自然会认为你优秀。但如果是个三本出身的倒霉孩子做的，也许面试官可以留下更为深刻的印象。&lt;/p&gt;
&lt;p&gt;你的项目最好在被面试官充分地展开、理解之后仍然被认为是复杂的。&lt;/p&gt;
&lt;p&gt;这种复杂性可能涉及艰辛的公式推导、精巧的代码结构或是用上了炫酷而繁琐的技术特性。这些都是相对客观的指标。&lt;/p&gt;
&lt;p&gt;然而互联网嘛，技术栈划分细、变化快。&lt;/p&gt;
&lt;p&gt;老道的面试官并不特别关心你做过什么，他会转而透过你的这段项目经历，去观察、揣摩你的智力、好奇心以及执行力分别到达了怎样的程度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⭐这里我的建议是：分配好精力。花大量时间，精心准备一个 “牛逼” 的项目。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;毕竟，在这个复杂的世界里，一个就够了。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;事实上你那几十分钟面试时间里也就够你们详谈一个项目。&lt;/p&gt;
&lt;p&gt;人的错觉有很多种，&lt;strong&gt;第一印象的效应尤为明显&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;又或是八二原理、马太效应、路径依赖……&lt;/p&gt;
&lt;p&gt;作为一个有志于盅惑人心的面试者，你得把自己想象成是一个剑客，十步杀一人、光速出剑、一击毙命。&lt;/p&gt;
&lt;p&gt;只要心够决，去把一个项目做好、做深、做到极致。做完以后再深入了解项目细节，包括上游客户需求、下游开源工具特性和原理、可行优化方案以及后续可能的开发方向。&lt;/p&gt;
&lt;p&gt;这是你的使命，只能一次成功，不容许失败。&lt;/p&gt;
&lt;p&gt;举个例子，记得 15 年的 7 月份那会有一篇爆款论文，关于如何利用神经网络训练一个转换艺术风格的迁移学习模型。&lt;/p&gt;
&lt;p&gt;如果你作为一个两个月后找算法工作的大三本科生，那么把论文细细读了，公式全部会推，写靠谱代码把项目做好。&lt;/p&gt;
&lt;p&gt;在面试前再把相关算法原理跟实践中遇到的困难以及你攻坚克难的过程耐下性子理清楚、面试的时候讲明白。&lt;/p&gt;
&lt;p&gt;是不是显得很有含金量、很能体现个人动手能力与技术好奇心、在一群连基本的 k-means 都写不好的校招生中，陡然间鹤立鸡群了？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⭐除了让面试官理解你牛以外，方向对口也重要。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;毕竟校招统一面试，如果没有恰到好处的内推，往往是需要部门主动捞你简历约面试的。&lt;/p&gt;
&lt;p&gt;又比如过了谷歌的面试，后续也还是需要做 team match。&lt;/p&gt;
&lt;p&gt;很多时候去哪不是你说了算，而是你的简历起决定性作用。另外方向对口对于面试本身的重要性更不必多说。就算是校招，相同水平下谁都更想找熟练工吧。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以你得提前很久想清楚自己想干什么，提前做准备。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;找工作这件事很多时候是蝴蝶效应。也许偶然帮老师做了个项目，然后主要靠这个项目找了个实习接着做相关方向，最后的正式校招就很可能这么一直续下去。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最好从一开始就要不将就。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有道是：Fuck everything, but growth.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;想清楚做什么才是有用、有效率的。&lt;/p&gt;
&lt;p&gt;比如本科毕业就打算工作的，如果真的想做机器学习算法，那么我认为极端情况下，宁愿去有活力的小公司做算法岗，也别去谷歌做前端实习。&lt;/p&gt;
&lt;p&gt;其实一次实习的机会成本还是挺高昂的，而实习的 title 在最后的校招中也未必如你想象得那么有用。&lt;/p&gt;
&lt;p&gt;我个人曾因为在微软实习的项目相对零散而兴趣不相关，在去年校招的过程中甚至直接将这一段实习经历删掉，以避免与面试官在这一点上陷入尬聊的窘境。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;然后是专业基础知识。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;正常情况下外企在这里不会做太多要求。&lt;/p&gt;
&lt;p&gt;而 BAT 三家都会考察基础知识，且各有侧重面，这个你们具体还是要看面经。&lt;/p&gt;
&lt;p&gt;不同考察方向都有哪些常见知识点，你们随便一搜都有。&lt;/p&gt;
&lt;p&gt;最好能结合之前的专业课所学，在具体的面试知识点上深入下去，了解细节。&lt;/p&gt;
&lt;p&gt;当然大学前几年能把计算机组成原理、计算机网络以及操作系统等几门专业课基础先打牢了，会好很多。&lt;/p&gt;
&lt;p&gt;我承认，本科的 CS 教育往往扯淡，但是我建议该上的课还是应该上一下的，哪怕自己跟着书本自学。不去上课，你的自制力恐怕没有想象中那么强。&lt;/p&gt;
&lt;p&gt;这些基础课程对以后的职业生涯会有潜移默化的影响。&lt;/p&gt;
&lt;p&gt;毕竟，计算机上的设计思想，很多地方都是可以互相借鉴的，这些知识会成为你以后解决工作中遇到的棘手问题的灵感来源。&lt;/p&gt;
&lt;p&gt;而且这部分知识都是成体系的，等工作了以后就没有整块时间去啃了。&lt;/p&gt;
&lt;p&gt;劝君惜取少年时！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⭐面试中所涉及的另一个重要部分是算法题、代码题，以及一些智力题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;面试时间有限，问到的题目都不会太难的。当然也看候选人背景，经历以竞赛为主的就会给难一些的 —— 不会涉及太繁琐的分析，往往只需要你灵机一动。&lt;/p&gt;
&lt;p&gt;这里还是有一些技巧的。&lt;/p&gt;
&lt;p&gt;不太好用语言表述出来，就像乒乓球一样，要在实践中练习击球的感觉。&lt;/p&gt;
&lt;p&gt;所以多争取面试机会很重要，尽量适应面试氛围，从而避免紧张而产生智商滑坡的情况。&lt;/p&gt;
&lt;p&gt;面试算法题、思维题，也是一种测试团队协作能力的方式。&lt;/p&gt;
&lt;p&gt;面对算法题，有经验的人往往会建议你，不要急着给出最优解，先讲基本方法，可以暴力一点，然后慢慢优化。这很有道理。&lt;/p&gt;
&lt;p&gt;其实最好能按一定的节奏来一步步地展现你的思考过程，甚至遇到不太会聊的面试官你得自己学会去引导，掌控面试的节奏。&lt;/p&gt;
&lt;p&gt;甚至有的时候，你给讲一些你觉得很靠谱的思考路线，面试官也会主动提醒你，想歪了。&lt;/p&gt;
&lt;p&gt;或是另一种情况，饶有兴致地陪着你按照新思路想下去，最后不论是否能解决问题，往往都会觉得你想法不错，是个面试加分项。&lt;/p&gt;
&lt;p&gt;实在没有好思路的情况下，试探性地讲些模糊的大体思路也比过久的沉默要好。&lt;/p&gt;
&lt;p&gt;哪怕随便瞎讲点什么，面试官兴许会提点你一下，继续观察你接下来的表现。&lt;/p&gt;
&lt;p&gt;用考场上的话来讲，面试中要学会尽量拿到步骤分。&lt;/p&gt;
&lt;p&gt;如果你以一个人冥思苦想的方式玩命怼一道难题而不得，中间过程一言不发，那么好比是考试交白卷。&lt;/p&gt;
&lt;h2&gt;03&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;最后，在校招前，争取做一份实习。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果你在武大国软这种自由放浪的环境下，从大一开始出去实习，到校招前实习个四五次完全存在理论上的可能性。&lt;/p&gt;
&lt;p&gt;实习次数多了，你也就可以循序渐进地换更好的公司，跟更牛逼的同事做更牛逼的项目。至于结识朋友、邂逅妹子、开阔视野什么的更不在话下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而对于大部分中规中矩度过前三年本科生涯，基本功还算扎实的同学来说，大三暑假的实习期将会是一个补充项目经历的大好机会。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最好能争取一个稍有难度的、相对独立的项目好好做。这是你将来的几个月冲刺校招的主要资本之一。&lt;/p&gt;
&lt;h2&gt;04&lt;/h2&gt;
&lt;p&gt;这些话很想讲给多年前的我自己听，但是不现实了。沉舟侧畔千帆过，现在我把积淀后的思想赠予你们。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;首先，快速迭代自己的方法论。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;很多孩子在刚上大学的时候，因为太习惯于被父母老师安排的人生，往往只重视战术，不懂得经营发展战略眼光。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;大局观很重要&lt;/strong&gt;。有的时候只是只言片语，一点小小的信息素，就有四两拨千斤的效果。&lt;/p&gt;
&lt;p&gt;人与人之间在判断力上的差距其实很重要。在一些关键的决策点上，如果能稍微提高百分之一的准确率，乘上可能的潜在收益或是损失，都会是很大的数学期望值。&lt;/p&gt;
&lt;p&gt;记得学长的一次讲座，提问环节的时候我问他，在曾有 FB 面试机会的情况下，直接去 CMU 读书，是否考虑过不妥。&lt;/p&gt;
&lt;p&gt;他说，这是他人生最后悔的决定之一，如果早入职几年，存在获得数百万美刀期权的可能性。&lt;/p&gt;
&lt;p&gt;如果让现在的我回到大学报到的时候，大概会出去做很多次实习、多认识很多朋友、去折腾很多奇怪的项目，甚至刷语言绩点准备出国。&lt;/p&gt;
&lt;p&gt;可是那时的我什么也不懂，这种状态持续了好几年。现在回想起来，本科时代的大部分事情我都做错了，做对的判断只是少数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;从个人角度出发，如何高效率地获取信息以及反刍，也是一个很有意思的课题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;举个例子，你可以考虑挑选一定数量的靠谱微信公众号来了解互联网信息，不要多，控制在每个公众号的推送都能定期读完的关注规模。&lt;/p&gt;
&lt;p&gt;当然，其实互联网圈的媒体人写东西都有点虚浮，对不同的观点你要有自己审慎的判断。&lt;/p&gt;
&lt;p&gt;上述的例子只是抛砖引玉。&lt;strong&gt;其实解决信息不对称，甚至是构筑自己相对于常人的信息壁垒，仍然有很多可行的方法有待探索。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;年轻人可以多尝试、多试错。毕竟年轻没有失败，&lt;strong&gt;等级低就是复活快&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其次就是：不要怂。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一点我深有感触。尤其是针对学 CS 的孩子来说，很重要。&lt;/p&gt;
&lt;p&gt;这个专业出身的同学，往往家里不是很富裕，见识不够广，不够自信。&lt;/p&gt;
&lt;p&gt;甚至有些还会因为过于敏感多思，反而过于独善其身，存在与人交流的障碍，又或是做事情瞻前顾后、缺乏决断，聪明反被聪明误。&lt;/p&gt;
&lt;p&gt;我也见过很多人，当本可进取时，却故作谦卑，因为不愿承担过大的心里压力，错过了唾手可得的面试、出国、比赛机会。&lt;/p&gt;
&lt;p&gt;我在读大学以前，一度非常自闭，不爱与人说话。&lt;/p&gt;
&lt;p&gt;这几年下来改变了很多，虽然仍有轻微的社交恐惧症，但只是面对陌生人会有点难受，正常交谈是没有问题了。&lt;/p&gt;
&lt;p&gt;事实上我心里清楚，我是花了大力气来打磨自己在这方面的性格缺陷的。&lt;/p&gt;
&lt;p&gt;我常常分析，为什么会对他人感到恐惧呢。&lt;/p&gt;
&lt;p&gt;后来发现，因为我总是习惯性地在潜意识里预设，他人、或是某个外部事物是完美的。&lt;/p&gt;
&lt;p&gt;但经历了很多之后又发现，没有什么是完美的，均值回归是普遍存在的现象。&lt;/p&gt;
&lt;p&gt;事物的诸多美好品质之间并不存在绝对的因果关系，往往只是弱相关。&lt;/p&gt;
&lt;p&gt;高大上的互联网公司、遗世独立的牛人、狂拽酷炫的技术，只是世人所见的一个片面。哪怕是那天上的月亮，也有圆缺，存在暗面。&lt;/p&gt;
&lt;p&gt;本该是不卑不亢的平等交流，却因为过分谨慎而表现得小心翼翼、唯唯诺诺；我也曾因此错过了爱情。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⭐最后，一定要有自己的追求。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这点见仁见智，不强求。像大多数人一样，我也总是在思考，人生的意义是什么。&lt;/p&gt;
&lt;p&gt;成长的过程中，我发现身边的大环境是，总是会预设一个最优路径。&lt;/p&gt;
&lt;p&gt;比如，中学时代大家的注意力都在高考上，觉得上了好大学就可以万事大吉。&lt;/p&gt;
&lt;p&gt;搞竞赛的同学容易认为打好 ACM 就可以获得一切。&lt;/p&gt;
&lt;p&gt;CS 专业的同学整日想法设法地想要进 BAT、谷歌。投资人对共享单车、共享充电宝这些项目趋之若鹜、蜂拥而上。&lt;/p&gt;
&lt;p&gt;然而，名校是终点吗？ACM World Final 是终点吗？Google 优雅舒适的工作环境里和身为谷歌员工的逼格是终点吗？&lt;/p&gt;
&lt;p&gt;无论是成绩突出的高中学霸，还是表现优异的大学生，在获得了满意的结果，进入人生的下一个阶段以后，还是会有很多感到迷茫。&lt;/p&gt;
&lt;p&gt;像艘驶入无人深空的太空飞船那样迷失了方向。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;或许从一开始我们就错了，不该过分执迷于一个成就、一个被预设为完美，得到之后却终究归于平淡的的 title。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;叔本华说，人生就是在痛苦和无聊这二者之间像钟摆一样摆来摆去：当你需要为生存而劳作时，你是痛苦的；当你的基本需求满足之后，你会感到无聊。&lt;/p&gt;
&lt;p&gt;我想，人生本来没有意义，痛苦欢快不过是虚幻。&lt;/p&gt;
&lt;p&gt;而创造，是生而为人的唯一救赎。&lt;/p&gt;
&lt;p&gt;Stay hungry, stay foolish.&lt;/p&gt;
&lt;p&gt;共勉！&lt;/p&gt;</content:encoded><h:img src="/_astro/202311131709309.C6Pyi_IJ.png"/><enclosure url="/_astro/202311131709309.C6Pyi_IJ.png"/></item><item><title>张鑫旭 12 年技术写作经验分享</title><link>https://coooredump.github.io/blog/future-survivor/technical-writing-experience</link><guid isPermaLink="true">https://coooredump.github.io/blog/future-survivor/technical-writing-experience</guid><description>几日前有幸能参与创作者训练营，在直播中也 Get 到不少有用的写作技巧，现在第二次直播回放，现帮大家归纳总结下张鑫旭前辈写作经验的几大要素。</description><pubDate>Thu, 09 Sep 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;写作像我这个掘金新人一样毫无头绪？那本文也许能帮助到你。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141738629.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;几日前有幸能参与&lt;a href=&quot;https://juejin.cn/post/7064192649523101726&quot;&gt;【创作者训练营】第四期&lt;/a&gt;，在直播中也 Get 到不少有用的写作技巧，现在第二次直播回放，现帮大家归纳总结下张鑫旭前辈写作经验的几大要素。&lt;/p&gt;
&lt;p&gt;⭐不论是没时间或者错过直播，还是想要复习的掘友们，希望本文对你们有所帮助。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;直播讲师&lt;/strong&gt;：张鑫旭&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;直播主题&lt;/strong&gt;：12 年技术写作经验分享&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;讲师介绍&lt;/strong&gt;：阅文集团前端技术专家，同时也是鑫空间鑫生活博主，十几年来一直笔耕不缀，创作了接近 800 篇前端技术原创文章，并著有书籍《CSS世界》《CSS选择器世界》和《CSS新世界》，在与用户体验相关的前端领域有较多的研究心得。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;直播回放&lt;/strong&gt;：&lt;a href=&quot;https://live.juejin.cn/4354/5299555&quot;&gt;张鑫旭 12 年技术写作经验分享&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;1. 前言：为什么会想不到分享的东西？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;总想搞波大事件
&lt;ul&gt;
&lt;li&gt;要稀缺&lt;/li&gt;
&lt;li&gt;要精致&lt;/li&gt;
&lt;li&gt;要干货&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;害怕带来的不安全感
&lt;ul&gt;
&lt;li&gt;害怕内容不行&lt;/li&gt;
&lt;li&gt;害怕版式糟糕&lt;/li&gt;
&lt;li&gt;害怕暴露菜鸟水平&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⭐其实不能让这些因素造成我们写作困难，&lt;strong&gt;我们更应该考虑的是“我有什么”&lt;/strong&gt;？！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我是谁？&lt;/li&gt;
&lt;li&gt;我的精力怎样？&lt;/li&gt;
&lt;li&gt;我的水平如何？&lt;/li&gt;
&lt;li&gt;我的优势是什么？&lt;/li&gt;
&lt;li&gt;我的突破口又在哪里？&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 关于写作选题&lt;/h2&gt;
&lt;h3&gt;写作选题方向&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;01 新特性、新方法介绍&lt;/strong&gt;（适合新人）：推荐 &lt;a href=&quot;https://caniuse.com/&quot;&gt;Can I Use&lt;/a&gt; 网站&lt;/p&gt;
&lt;p&gt;02 自认为厉害的小技巧、小创造&lt;/p&gt;
&lt;p&gt;03 原理剖析、深入理解&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;04 技术方案汇总&lt;/strong&gt;（适合新人）&lt;/p&gt;
&lt;p&gt;05 棘手问题解决经验分享&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;06 优秀框架、项目、工具的体验指南&lt;/strong&gt;（适合新人）&lt;/p&gt;
&lt;h3&gt;建议&lt;/h3&gt;
&lt;h4&gt;(1) 选题与自己学习相关&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;写作是学习的辅助手段&lt;/li&gt;
&lt;li&gt;容易坚持，就算没人看，自己也收获了成长&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;(2) 不要写雷同内容&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;同一个知识点可以从不同点切入&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;(3) 迷茫时候写写个人故事、感悟与困惑&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;既能寻找答案，又能获得访问&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 关于内容结构&lt;/h2&gt;
&lt;p&gt;🙄你的写作目的决定了你的内容结构！&lt;/p&gt;
&lt;h3&gt;功利写作&lt;/h3&gt;
&lt;p&gt;🌈如果你是为了升职加薪，为了换工作，为了出名而写作。那么你的写作需要更加有&lt;strong&gt;套路&lt;/strong&gt;一点，体现在两点：&lt;strong&gt;重点突出&lt;/strong&gt; &amp;#x26; &lt;strong&gt;有闭环有递进&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141738812.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;重点突出&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;一眼扫过去知道你在讲什么&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;讲结论的：结论先行&lt;/li&gt;
&lt;li&gt;讲交互的：效果先放&lt;/li&gt;
&lt;li&gt;罗列知识的：需要清晰的目录&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;有闭环有递进&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;完整的故事化表达&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;背景，思考，尝试，困难，解决与结果&lt;/li&gt;
&lt;li&gt;困难分1, 2, 3，解决后又出现了什么新问题&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;日常写作&lt;/h3&gt;
&lt;p&gt;👨‍💻如果是为了学习与自我成长，个人展现。那么&lt;strong&gt;遵从自己的内心&lt;/strong&gt;最重要，少一点套路，多一点真诚。因为真心想分享的心比什么乱七八糟的技巧都管用。&lt;/p&gt;
&lt;p&gt;🚀记住：&lt;strong&gt;写文章不要指望着让所有人都满意，让所有人都满意的文章一定是中庸的文章，即枯燥与乏味&lt;/strong&gt;。否则最后的文章一定是平平无奇，无法脱颖而出！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;看看张鑫旭大佬的文章结构（各式各样，随心而记）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/Wu-yikun/OSS/PicGo/202311141738208.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;4. 关于语言表达&lt;/h2&gt;
&lt;h3&gt;换位思考&lt;/h3&gt;
&lt;p&gt;🤔先抛出一个问题：&lt;strong&gt;技术文章&lt;/strong&gt;的语言表达，什么最重要？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简洁的语句？❌&lt;/li&gt;
&lt;li&gt;华丽的辞藻？❌&lt;/li&gt;
&lt;li&gt;搞笑的段子？❌&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;都不是！最重要的是&lt;strong&gt;换位思考&lt;/strong&gt;的能力。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果我是小白，这些术语懂吗？&lt;/li&gt;
&lt;li&gt;如果我是读者，好理解吗？
&lt;ul&gt;
&lt;li&gt;是不是有个耳熟能详的东西类比下？&lt;/li&gt;
&lt;li&gt;是不是代码要简化下，加注释？&lt;/li&gt;
&lt;li&gt;是不是这里应该放个图？&lt;/li&gt;
&lt;li&gt;是不是这里应该加个演示？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;观点&lt;/strong&gt;：除了是工作汇报、团队账号这样的严肃场景，否则一定是融入了个人感情的文章更有价值！&lt;/p&gt;
&lt;h3&gt;展现真实的自己&lt;/h3&gt;
&lt;p&gt;例如：我遇到了什么样的问题？我是怎么思考的？我又是怎么解决的？&lt;/p&gt;
&lt;p&gt;又例如：我觉得这个技术如何？我不太喜欢某某设计？我的建议是什么？&lt;/p&gt;
&lt;h4&gt;你是什么样的人，就用什么样的风格&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;拒绝模板，展现出真实的自我！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我话痨，喜欢扯东扯西，你就这么干，想到什么说什么！&lt;/p&gt;
&lt;p&gt;我御宅族，文章可以体现各种宅元素。&lt;/p&gt;
&lt;p&gt;我喜欢晒自己，那文章配图就多多展示。&lt;/p&gt;
&lt;p&gt;我是穷酸小透明，文章就不必强颜欢笑，透露出忧郁挺好！&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;😶真实的自我更容易让人产生共鸣！并且保持一致的风格和特色有诸多好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;糟糕的风格好过于毫无风格&lt;/li&gt;
&lt;li&gt;让别人记住你，提高影响力&lt;/li&gt;
&lt;li&gt;防盗版的手段之一&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 关于文章质量&lt;/h2&gt;
&lt;p&gt;🔮10 篇水文不如 1 篇高质量好文！&lt;/p&gt;
&lt;h3&gt;配图和演示&lt;/h3&gt;
&lt;p&gt;正所谓 “一例胜千图，一图胜千言”&lt;/p&gt;
&lt;h3&gt;对每一句话负责&lt;/h3&gt;
&lt;p&gt;出现了不确信的结论，一定要自己验证一遍&lt;/p&gt;
&lt;p&gt;例如：在桌面端 &lt;code&gt;document.scrollingElement&lt;/code&gt; 就是 &lt;code&gt;document.documentElement&lt;/code&gt;；在移动端 &lt;code&gt;document.scrollingElement&lt;/code&gt; 就是 &lt;code&gt;document.body&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;🥴对还是不是？Android 和 iOS 都是吗？&lt;/p&gt;
&lt;h3&gt;追求内心而不是热门&lt;/h3&gt;
&lt;p&gt;文章质量和访问量并不正相关&lt;/p&gt;
&lt;p&gt;例如：&quot;面试技巧，N个特性汇总&quot;这样的文章容易获得高赞，但它不一定能带给你影响力。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;01 关于文章选题&lt;/strong&gt;：与学习相关、不同切入点、感悟与困惑&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;02 关于内容结构&lt;/strong&gt;：重点突出、闭环与递进、遵照内心&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;03 关于语言表达&lt;/strong&gt;：换位思考、展示自我、保持风格&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;04 关于文章质量&lt;/strong&gt;：配图与实例、对结论负责、追寻内心&lt;/p&gt;
&lt;p&gt;💖大部分文字源于直播内容，直播回放地址已在文章开头贴出。&lt;/p&gt;</content:encoded><h:img src="/_astro/202311141740397.Bh3m5ZET.png"/><enclosure url="/_astro/202311141740397.Bh3m5ZET.png"/></item></channel></rss>