[{"content":"Cuda Reduce（规约）学习笔记 本文是记录阅读https://zhuanlan.zhihu.com/p/2039848582770058820后自己的理解。\n什么是Reduce 规约就是把一组很多个数据，通过某种运算，合并成更少的数据，通常最后合并成一个结果。\nReduce是将多个输入变成一个输出的操作，例如：\n$$ \\text{Sum Reduction:} [1,2,3,4]\\rightarrow10\\\\ \\text{Min Reduction:} [1,2,3,4]\\rightarrow1\\\\ \\text{Max Reduction:} [1,2,3,4]\\rightarrow4\\\\ \\text{Product Reduction:} [1,2,3,4]\\rightarrow24 $$\n因为GPU中有很多线程，适合对上述操作进行并行处理，所以Reduce是Cuda中一个很重要的东西。\n简单版本的规约 下面以一个数组相加的问题为例子，使用规约进行计算。\n__global__ void reduce_v0(float* input, float* output, int n){ extern __shared__ float smem[]; int tid = threadIdx.x; int gid = blockDim.x * blockIdx.x + threadIdx.x; smem[tid] = (gid \u0026lt; n)?input[gid] : 0.0f; __syncthreads(); for (int step = 1; step \u0026lt; blockDim.x; step*= 2){ if (tid % (2 * step) == 0){ smem[tid] += smem[tid + step]; } __syncthreads(); } if(tid==0){ output[blockIdx.x] = smem[0]; } } 首先将input拷贝至当前block的share memory，使用tid定位当前的thread，用gid来访问input数组。值得注意的是，在数据拷贝后有一句__syncthreads()来同步线程。如果不同步的话会出现 race condition。\n然后从step=1开始，当前thread处理当前位置的值以及当前位置加step位置的值，所以是smem[tid]+=smem[tid+step]。\n(tid%(2*step)==0)用于判断当前thread是否为要使用的thread。\n例：当前tid为0，step为1，则当前thread激活。tid为1，step为1，则当前thread闲置。当tid为2时，step=1，当前thread激活。\n所以这里要跨2倍的step来选择thread，因为每一个激活的thread都处理tid位置与tid+step位置的值。下一个线程则位于当前线程位置再跨2倍的step。\n在每次step后还要使用__syncthreads()来等待所以线程完成，再进行下一次循环。否则会出现race condition。\nRace condition （竞争条件）。\n**意思是：**多个线程同时读写同一个数据，最终结果取决于它们执行的先后顺序。如果这个顺序不确定，结果就可能不稳定、错误。\n例如：thread 0: sum += 1，thread 1: sum += 1。理想结果是sum=2。但是实际可能发生：\nthread 0 读取 sum = 0 thread 1 读取 sum = 0\nthread 0 计算 0 + 1 = 1 thread 1 计算 0 + 1 = 1\nthread 0 写回 sum = 1 thread 1 写回 sum = 1\n最后的结果sum=1\n完成所有规约操作之后，结果存在smem[0]中。当前block中的第一个thread即tid=0激活，将结果写入output数组中当前block id的位置。至此就完成了当前block的计算。\nWarp Divergence GPU以Warp（32线程）为最小调度单位，Warp中所有线程都执行同样的指令。当****同一个 warp 里的线程走了不同的分支路径，导致这些路径不能真正同时执行，只能分批执行。在上面的简单规约中，if (tid % (2 * step) == 0)就会造成严重的Warp Divergence，\n在step=1时，只有一半的threads是真正工作的。step=2时，只有25%的线程工作。在最后的step里只有一个线程工作。会造成很差的线程利用率。\n版本V1 解决Warp Divergence 在上个版本中我们使用了取模来选择活跃的线程。为了解决Warp Divergence，这里使用 Strided Index来选择线程。\n如果使用普通的连续index：tid 0 -\u0026gt; index 0，tid 1 -\u0026gt; index 1，tid 2 -\u0026gt; index 2，tid 3 -\u0026gt; index 3 \u0026hellip;\n如果使用****Strided index：tid 0 -\u0026gt; index 0，tid 1 -\u0026gt; index 2，tid 2 -\u0026gt; index 4，tid 3 -\u0026gt; index 6 \u0026hellip;\n这里的线程是连续的，但是访问的数据下标是跳跃的。\n__global__ void reduce_v1(float* input, float* output, int n){ extern __shared__ float smem[]; int tid = threadIdx.x; int gid = blockIdx.x * blockDim.x + threadIdx.x; smem[tid] = (gid \u0026lt; n) ? input[gid] : 0.0f; __syncthreads(); for(int step = 1; s \u0026lt; blockDim.x; s *= 2){ int index = threadIdx.x * 2 * s; if(index \u0026lt; blockDim.x){ smem[index] = smem[index + s]; } __syncthreads(); } if (tid == 0){ output[blockIdx.x] = smem[0]; } } 在代码实现中与前一个版本的区别只有选择活跃线程的语句。\n以blockDim=256为例子：\ns=1时，index = 2*tid，活跃的线程范围是0～127，正好4个Warp，没有Warp Divergence。s=2,4时同理。\n当s=8时，线程0~15活跃，只有当前warp有divergence。s=16,32,64,128时同理。\n","permalink":"https://m3i.top/posts/cuda-reduce-notes/","summary":"\u003ch1 id=\"cuda-reduce规约学习笔记\"\u003eCuda Reduce（规约）学习笔记\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003e本文是记录阅读\u003c/strong\u003e\u003ca href=\"https://zhuanlan.zhihu.com/p/2039848582770058820\"\u003ehttps://zhuanlan.zhihu.com/p/2039848582770058820\u003c/a\u003e后自己的理解。\u003c/p\u003e\n\u003ch2 id=\"什么是reduce\"\u003e什么是Reduce\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e规约就是把一组很多个数据，通过某种运算，合并成更少的数据，通常最后合并成一个结果。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eReduce是将多个输入变成一个输出的操作，例如：\u003c/strong\u003e\u003c/p\u003e","title":"Cuda Reduce（规约）学习笔记"},{"content":"先别急着被术语吓到 这几年 AI 圈的新词冒得很快：Token、LLM、Agent、Workflow、Skill、RAG、Tool Calling、Context……看起来像一锅字母汤。\n但它们其实没有那么神秘。你可以先把现代 AI 应用想成一个小团队：\nToken 是 AI 读文字时看到的“小碎片”。 LLM 是那个很会读写和推理的“大脑”。 Prompt 是你给它的任务说明。 Context 是它当前能看到的资料。 RAG 是让它先翻资料再回答。 Tool Calling 是让它能调用工具，不只是嘴上说说。 Workflow 是提前写好的固定流程。 Agent 是能根据情况自己决定下一步的执行者。 Skill 是某类任务的专用说明书和工具包。 Token：AI 眼里的文字小碎片 在讲 LLM 之前，先认识一个更基础的概念：Token，中文的官方翻译为“词元”。\nToken 就像 AI 读文字时看到的“小碎片”。我们看到的是一句完整的话，模型看到的则是一块一块被切开的文本。\n比如这句话：\n今天天气很好 在模型眼里，它不一定是完整的一句话，而可能会被拆成几个 token。它不完全等于一个汉字，也不完全等于一个词；有时是一个字，有时是词的一部分，有时是标点或代码符号。\n不同模型的分词方式可能不一样，所以我们不需要记住某句话具体会被切成几个 token，只要理解它是模型处理文本的基本单位就够了。\nLLM 是什么？ LLM 是 Large Language Model，中文叫“大语言模型”。我们平常用到的豆包、元宝、DeepSeek 等 AI 产品或模型，背后通常都离不开大语言模型。\n它最核心的能力是：读懂上下文，然后生成接下来最合适的内容。\n听起来像“文字接龙”？有点像，但它不是只会机械接词。因为它在训练中看过大量文本、代码、数学、对话和知识材料，所以能学到很多语言背后的模式：\n这句话大概是什么意思 这个问题应该怎么拆 哪些代码看起来像 bug 一篇文章怎样组织更清楚 用户真正想要的可能是什么 比如你说：\n西红柿炒番茄怎么做？🤔 LLM 会尝试：\n理解：这句话大概是什么意思 识别：西红柿和番茄其实是同一种东西 推测：你是不是想问“西红柿炒鸡蛋怎么做” 补救：如果你确实想炒番茄，也可以给你一个能下锅的做法 背后最简单的逻辑：预测下一个 token 如果把大语言模型的底层逻辑讲到最简单，可以这样说：\n它每一步都在根据前面的上下文，预测“下一个 token 最可能是什么”。\n前面说过，token 是 AI 眼里的“文字小碎片”。AI生成回答时，就是不断根据上下文预测下一个文字小碎片。\n比如你输入：\n今天天气很 模型会根据它学到的语言规律，给很多可能的下一个 token 分配概率。比如它可能觉得：\n好：35% 冷：20% 热：15% 糟：8% 适合：5% ... 然后模型会从这些候选里选出一个，接着继续预测下一个 token。于是文本就像这样一步步长出来：\n今天天气很 → 好 → ， → 适合 → 出门 → 走走 这也是为什么 LLM 看起来像在“思考”：它不是一次性把整篇回答从脑袋里倒出来，而是在不断根据你的问题、历史对话和已经生成的内容，继续预测后面最合适的 token。\n不过这里有个有趣的点：模型通常不会永远选择概率最高的那个 token。否则它的回答会很稳定，但也可能很无聊，像每次点奶茶都只点“少冰三分糖”。所以生成时常常会保留一点随机性，让它能写出更自然、更有变化的回答。\n你可以把它想成一个很会接话的人：\n你说了上半句，它猜下半句。 你给了背景，它猜下一步该讲什么。 你要求“轻松一点”，它就提高轻松表达的概率。 你要求“严谨一点”，它就提高术语、结构化表达和细节解释出现的概率。 所以，你每次和它说的话，并不是在“命令模型背答案”，而是在改变它接下来生成内容的概率分布。Prompt 越清楚，模型越容易把概率集中到你想要的方向上。\n这也解释了 LLM的两个特点：\n它很擅长生成自然语言，因为语言本来就有大量可学习的模式。 它可能会一本正经地说错，因为“听起来合理”不等于“事实正确”。 Prompt 和 Context 讲完 Token 和 LLM，再看两个经常一起出现的词：Prompt 和 Context。\nPrompt Prompt 就是你给AI的输入。\n它可以很短😅：\n给我写一个故事。 也可以很长：\n你是一名语文老师。请用优美的语言给我讲一个故事，里面要包括一些典故，为了照顾不知道典故的人，在故事中给出自然的解释。 Prompt 写得好，AI就更容易知道你要什么。\n这有点像点奶茶：你说“来一杯”，店员只能给你白眼🙄，然后可能给你一杯洗碗水；你说“珍珠奶茶少冰、三分糖、不要珍珠”，店员就会给你一杯没有珍珠的珍珠奶茶🧋。\nContext Context，中文叫上下文。\n你可以把它想象成 AI 当前摊在桌面上的材料：你的提问、历史对话、系统规则、文件内容、工具返回结果，以及它已经生成过的内容，都可能成为上下文的一部分。\nAI每次回答时，都会根据这些上下文来继续生成，所以它才能尽量不跑题。\n当然，即便是机器，它一次能读取的内容也是有限的。这个限制通常叫 Context Window，也就是上下文窗口。所以真实 AI 系统经常要做一件事：只把当前任务最相关的资料放进上下文。\n这就引出了 RAG。\nRAG：让 AI 先翻资料再回答 RAG 是 Retrieval-Augmented Generation，中文常译作“检索增强生成”。\n这个名字有点硬，翻成人话就是：\n不要让AI闭卷瞎答，先让它查资料，再让它回答。📚\n一个典型 RAG 流程是：\n用户提问。 系统去知识库里搜索相关资料。 把搜到的资料放进模型上下文。 AI基于这些资料生成答案。 比如你问公司内部助手：\n今年差旅报销有什么新规则？ 如果只靠 LLM，它可能不知道你公司的最新制度。但如果系统先去内部文档里找“差旅报销”相关段落，再交给模型总结，答案就靠谱得多。\n不过 RAG 不是万能的。它也会翻车：\n检索错资料，答案就会跑偏。 资料太碎，模型看不出重点。 文档过旧，答案也会过旧。 没有引用来源，读者还是不知道能不能信。 Tool Calling：让 AI 不只会说，还能做 AI本身主要负责基于当前上下文生成内容。它不能天然查询天气、读数据库、发邮件、改文件。\nTool Calling，也叫 Function Calling，中文叫工具调用或者功能调用，就是把外部工具告诉AI，让AI在需要时请求调用。\n重点来了：AI不是自己偷偷跑去执行工具。一般流程是这样的：\n应用告诉 AI：你可以使用一个“查找某地天气”的工具。 用户问：“柏林今天冷吗？” AI 判断：这个问题需要实时天气，于是请求调用查天气工具。 应用程序真正执行查询，把天气结果交回 AI。 AI 结合查到的数据，用自然语言回答用户。 工具可以是很多东西：\n搜索网页 查询数据库 执行代码 操作浏览器 生成图片 Tool Calling 是很多 AI 应用从“聊天玩具”变成“能干活的系统”的关键一步。\nWorkflow：照着菜谱做事 Workflow 是工作流。\n它会把某一项工作拆成一个个步骤。当你需要 AI 做这件事时，系统会让它按照预定流程去完成。\n比如一个“西红柿炒鸡蛋”workflow：\n先切西红柿。 再打鸡蛋。 锅中热油。 先炒鸡蛋，再炒西红柿。 混合翻炒，加入适量调料。 这个流程的重点是：路线提前写好了。AI 可以参与其中某一步，比如判断调料该怎么写得更清楚，但整体顺序不是它临场自由发挥。\nAgent：会自己决定下一步的 AI 系统 Agent 是现在最火、也最容易被讲玄乎的词。它翻译过来应该叫智能体，它可以自己帮你处理好一个任务。\n一个 Agent 的循环大概是：\n理解目标。 制定下一步。 选择工具。 调用工具。 观察结果。 判断是否完成。 没完成就继续。 比如你说：\n帮我做一个周末去东京的旅行计划，预算不要太高，最好能安排交通、住宿和每天去哪玩。 一个 Agent 可能会自己决定：\n先确认出发城市、日期和预算 查询航班或火车等交通方案 比较几个住宿区域的价格和便利程度 根据天气、距离和开放时间安排每天的行程 发现预算超了，就调整酒店或景点顺序 最后整理成一份清晰的旅行计划 这和 workflow 的区别在于：\nWorkflow：路线提前写好。 Agent：路线可以边走边决定。 可以把 workflow 想成“导航路线已经规划好”，agent 更像“带着地图和工具自己探索怎么到目的地”。\n举个生活一点的例子：如果你说“照这个菜谱做西红柿炒鸡蛋”，那更像 workflow；如果你说“冰箱里有番茄、鸡蛋、半根葱，帮我想今晚做什么并列购物清单”，那就更像 agent，因为它需要自己判断目标、检查条件、安排步骤。\nSkill：给 AI 的专业小抄 Skill 可以理解为某类任务的“专用能力包”。\n它通常包含：\n什么时候应该使用这个 skill 做这类任务的步骤 需要遵守的规则 可复用的模板 相关脚本 示例和参考资料 比如一个“写作”的 skill 可能会写：\n先判断读者是谁 再确定文章想表达什么 结构要清楚，别一上来就堆概念 语言要自然，避免像说明书一样硬邦邦 结尾帮读者带走一个清晰观点 Prompt 是这一次怎么做，Skill 是以后遇到这类任务都按这套方法做。\nMemory：AI 真的会记住你吗？ Memory 也是热门词。\n但它容易被误解。AI 的 memory 不一定是“像人一样记忆”。在产品和系统里，memory 通常指保存一些对未来任务有用的信息，比如：\n用户偏好 项目背景 常用格式 已完成任务 长期目标 比如你总是希望AI的聊天语气“轻松一点，有活人感一点”，系统可以把这个偏好保存下来，下次自动应用。\nMemory 的好处是个性化，风险是隐私和错误积累。如果系统记错了你的偏好，它以后可能一直错下去。\nMultimodal：不只看文字 Multimodal 指多模态。\n传统语言模型主要处理文本。多模态模型可以处理更多类型的信息：\n文本 图片 音频 视频 屏幕截图 文件 比如你发一张 UI 截图，让 AI 说哪里布局不舒服；或者给它一张数学题照片，让它讲解步骤；或者让它听一段会议录音，整理纪要。\n多模态让 AI 更接近真实工作场景，因为人的工作本来就不只是一行文字。\n总结 LLM 是现代 AI 的核心，但它不是全部。\nRAG 让模型带资料回答，Tool Calling 让模型能使用工具，Workflow 让任务稳定执行，Agent 让模型能动态决策，Skill 让经验可以复用。\n所以以后再看到一个 AI 产品，你可以问几个问题：\n它是否是多模态？能否处理图片或者文件？\n它只是聊天，还是能调用工具？\n它的答案来自模型记忆，还是来自 RAG 检索？\n它走的是固定 workflow，还是 agent 自己决定步骤？\n它有没有 skill 来沉淀专业任务？\n能回答这些问题，你就已经不只是“听懂 AI 热词”了，而是在看它背后的系统设计。\n","permalink":"https://m3i.top/posts/intro-to-ai/","summary":"\u003ch1 id=\"先别急着被术语吓到\"\u003e先别急着被术语吓到\u003c/h1\u003e\n\u003cp\u003e这几年 AI 圈的新词冒得很快：Token、LLM、Agent、Workflow、Skill、RAG、Tool Calling、Context……看起来像一锅字母汤。\u003c/p\u003e","title":"简单讲讲“AI”"},{"content":"什么是指针？ 指针是一个对象，它的值是某个对象或函数的地址。\n如上图所示，指针ptr里存着的内容是变量num的地址。于是我们说指针ptr指向变量num。\n如果我们有一个指针和一个变量，怎么让指针指向这个变量呢？可以使用取地址符 \u0026amp;（address-of operator），这个符号的名字很直白，即取该变量的地址。代码如下：\nint num{26}; int* ptr{\u0026amp;num}; std::cout \u0026lt;\u0026lt; num \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // 26 std::cout \u0026lt;\u0026lt; \u0026amp;num \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // num 的地址 std::cout \u0026lt;\u0026lt; ptr \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // ptr 保存的地址，也就是 \u0026amp;num std::cout \u0026lt;\u0026lt; \u0026amp;ptr \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // ptr 这个指针变量自己的地址 std::cout \u0026lt;\u0026lt; *ptr \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // 26 如果我们想要取出指针指向的地址上的值，也就是 num，我们需要使用解引用符号 *（dereference operator）。\nstd::cout \u0026lt;\u0026lt; *ptr \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // 26 空指针 nullptr 指针也可以不指向任何对象。现代 C++ 中推荐使用 nullptr 表示空指针，而不是使用 0 或 NULL。\nint* ptr {nullptr}; 空指针没有指向有效对象，因此不能被解引用。解引用空指针会造成未定义行为（undefined behavior）。\nint* ptr {nullptr}; // 错误：ptr 没有指向有效对象 std::cout \u0026lt;\u0026lt; *ptr \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; 所以在不确定一个指针是否有效时，可以先判断它是否为空：\nint* ptr {nullptr}; if (ptr != nullptr) { std::cout \u0026lt;\u0026lt; *ptr \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 裸指针是否拥有资源？ 裸指针（raw pointer）本身只是一个地址，它并不会告诉我们“谁负责释放这块资源”。也就是说，int* 这个类型既可以表示拥有资源，也可以只是观察某个对象。\n比如下面这个指针只是观察 num，它不拥有 num，所以不能对它使用 delete：\nint num {26}; int* ptr {\u0026amp;num}; std::cout \u0026lt;\u0026lt; *ptr \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; 而下面这个指针指向由 new 创建的对象，它负责释放这个对象：\nint* ptr {new int {26}}; std::cout \u0026lt;\u0026lt; *ptr \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; delete ptr; 这就是裸指针容易出错的原因之一：单看 int* ptr 并不能判断它是否拥有资源。现代 C++ 中通常遵循这样的习惯：\n如果需要表达“独占拥有”，优先使用 std::unique_ptr。 如果需要表达“共享拥有”，再使用 std::shared_ptr。 如果只是临时访问或观察一个对象，可以使用裸指针、引用，或者在配合 shared_ptr 时使用 std::weak_ptr。 资源释放问题 当我们用 new 动态创建对象时，对象位于自由存储区（free store，通常可以简单理解为堆 heap）。这类资源不会随着指针变量离开作用域而自动释放，因此需要使用 delete 手动释放（deallocate）：\nint* p {new int}; delete p; 如果我们忘记释放，程序运行期间就可能造成内存泄漏（memory leak）。虽然程序结束时操作系统通常会回收进程占用的内存，但这并不能替代正确释放资源：对象的析构函数可能不会被调用，长时间运行的程序也会不断占用更多内存。而当我们不小心对同一块内存 delete 两次时，又会造成未定义行为（undefined behavior）。\n悬空指针（dangling pointer） delete 会释放指针指向的对象，但不会自动修改指针变量本身。也就是说，释放之后，指针里仍然保存着原来的地址，只是这个地址已经不再属于我们了。这样的指针叫做悬空指针。\nint* p {new int {10}}; delete p; // 错误：p 指向的对象已经被释放 std::cout \u0026lt;\u0026lt; *p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; 访问悬空指针同样会造成未定义行为。为了降低误用风险，释放之后可以把指针设为 nullptr：\nint* p {new int {10}}; delete p; p = nullptr; 不过更推荐的做法是避免手动管理这类资源，直接使用智能指针，让对象的释放交给 RAII 机制处理。\n智能指针（smart pointer） 为了解决这些问题，智能指针应运而生。智能指针会在合适的时机自动释放内存，从而减少上述问题。\n智能指针相关类型定义在头文件 \u0026lt;memory\u0026gt; 里，使用之前需要包含它。\nstd::unique_ptr（独占指针） std::unique_ptr 独占所指向的对象。意味着同一时间只有一个 unique_ptr 拥有该对象的所有权（ownership）。\n#include \u0026lt;iostream\u0026gt; #include \u0026lt;memory\u0026gt; int main() { std::unique_ptr\u0026lt;int\u0026gt; p = std::make_unique\u0026lt;int\u0026gt;(10); std::cout \u0026lt;\u0026lt; *p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return 0; } unique_ptr 最重要的特性就是无法被复制（copy），以下代码将会报错：\nstd::unique_ptr\u0026lt;int\u0026gt; p1 = std::make_unique\u0026lt;int\u0026gt;(10); // Error std::unique_ptr\u0026lt;int\u0026gt; p2 = p1; 我们说过 unique_ptr 独占该对象，所以不能同时有两个 unique_ptr 拥有同一个对象。\n不过所有权可以被转移：\n#include \u0026lt;utility\u0026gt; std::unique_ptr\u0026lt;int\u0026gt; p1 = std::make_unique\u0026lt;int\u0026gt;(10); std::unique_ptr\u0026lt;int\u0026gt; p2 = std::move(p1); std::move 会把传入的左值（lvalue）转换成右值引用，让 unique_ptr 的移动构造函数可以接管所有权。移动之后，p2 拥有对象，p1 会变成空指针。\nstd::shared_ptr（共享指针） std::shared_ptr 表示共享所有权。这就意味着可以有很多个指针指向并拥有同一对象。\n#include \u0026lt;iostream\u0026gt; #include \u0026lt;memory\u0026gt; int main() { std::shared_ptr\u0026lt;int\u0026gt; p1 = std::make_shared\u0026lt;int\u0026gt;(20); std::shared_ptr\u0026lt;int\u0026gt; p2 = p1; std::cout \u0026lt;\u0026lt; *p1 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; *p2 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return 0; } 在上面的例子中，p1 和 p2 共享这个整型对象的所有权，也就意味着它们都可以访问这个对象。\nshared_ptr 还有一大特性：引用计数（reference count）。这个值会记录当前有多少个 shared_ptr 拥有该对象。\nstd::cout \u0026lt;\u0026lt; p1.use_count() \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; 当该值为零时，意味着没有 shared_ptr 再拥有这个对象，对象就会被安全地自动释放。\n但是这样引出了一个新的问题：循环所有权（circular ownership）。循环所有权指的是两个对象通过 shared_ptr 彼此拥有对方，导致引用计数永远无法归零：\nclass Node { public: std::shared_ptr\u0026lt;Node\u0026gt; next {}; ~Node() { std::cout \u0026lt;\u0026lt; \u0026#34;Node destroyed\\n\u0026#34;; } }; auto first { std::make_shared\u0026lt;Node\u0026gt;() }; auto second { std::make_shared\u0026lt;Node\u0026gt;() }; first-\u0026gt;next = second; second-\u0026gt;next = first; 当 first 和 second 离开作用域时，它们各自仍然被对方的 next 成员拥有，所以引用计数不会变成零，析构函数也不会被调用。\n为了解决这个问题，std::weak_ptr出现了。\nstd::weak_ptr（弱指针） weak_ptr 是一种智能指针，用于观察由 shared_ptr 管理的对象，但它不会增加引用计数，也不会延长对象的生命周期。\n#include \u0026lt;iostream\u0026gt; #include \u0026lt;memory\u0026gt; int main() { std::shared_ptr\u0026lt;int\u0026gt; sp = std::make_shared\u0026lt;int\u0026gt;(30); std::weak_ptr\u0026lt;int\u0026gt; wp = sp; std::cout \u0026lt;\u0026lt; sp.use_count() \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // 1 return 0; } 这里的引用计数是 1 而不是 2。\n当我们想通过 weak_ptr 访问对象时，必须先使用 lock() 函数把它临时转换为一个 shared_ptr。\nif (std::shared_ptr\u0026lt;int\u0026gt; temp = wp.lock()) { std::cout \u0026lt;\u0026lt; *temp \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } else { std::cout \u0026lt;\u0026lt; \u0026#34;Object no longer exists\\n\u0026#34;; } 如果对象还存在，lock() 会返回一个有效的 shared_ptr；如果对象已经被释放，则会返回一个空的 shared_ptr。\n在循环所有权的场景中，通常把“不拥有对方，只是观察对方”的那一边改成 weak_ptr：\nclass Node { public: std::weak_ptr\u0026lt;Node\u0026gt; next {}; }; 总结 智能指针是基于 RAII 提出的。\nRAII（Resource Acquisition Is Initialization）即“资源获取即初始化”。简单来说，就是把资源的生命周期绑定到对象的生命周期。\n它的核心思想是，在对象的构造函数（constructor）中获取资源，并在对象的析构函数（destructor）中释放资源。\n当智能指针被创建时，它就拥有资源；当它被销毁时，资源会被自动释放。\n智能指针具有以下几个优点：\n它们可以减少内存泄漏。可以减少手动使用 new 和 delete 操作。并且有助于安全地处理异常，因为即使作用域退出，内存也会被释放。\n智能指针 所有权模型 能否复制 是否增加引用计数 典型用途 std::unique_ptr 独占所有权 不能复制，只能移动 否 默认首选，表达单一所有者 std::shared_ptr 共享所有权 可以复制 是 多个对象确实需要共同拥有同一资源 std::weak_ptr 弱引用，不拥有对象 可以复制 否 观察 shared_ptr 管理的对象，避免循环所有权 智能指针是现代 C++ 的一项重要特性。在大多数情况下，应当首选 std::unique_ptr，因为它简单、高效，且能清晰地表达所有权。仅当程序中的多个部分需要共享同一个对象时，才应使用 std::shared_ptr。当某个对象需要引用另一个对象，但又不希望延长该被引用对象的生命周期时，std::weak_ptr 便显得尤为有用。\n","permalink":"https://m3i.top/posts/c++-pointers/","summary":"\u003ch1 id=\"什么是指针\"\u003e什么是指针？\u003c/h1\u003e\n\u003cp\u003e指针是一个对象，它的值是某个对象或函数的地址。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Pointers in C Language\" loading=\"lazy\" src=\"data:image/png;base64,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\"\u003e\u003c/p\u003e\n\u003cp\u003e如上图所示，指针\u003ccode\u003eptr\u003c/code\u003e里存着的内容是变量\u003ccode\u003enum\u003c/code\u003e的地址。于是我们说指针\u003ccode\u003eptr\u003c/code\u003e指向变量\u003ccode\u003enum\u003c/code\u003e。\u003c/p\u003e","title":"C++ 中的指针与智能指针"},{"content":"你好，这里是我的新博客。\n","permalink":"https://m3i.top/posts/hello/","summary":"\u003cp\u003e你好，这里是我的新博客。\u003c/p\u003e","title":"Hello"}]