[{"content":"0 定义 从六月份开始，loop engineering这个词逐渐成为agent领域的当红炸子鸡，目前我找到的初始的系统性的介绍这个理念的blog是：Loop Engineering，他提到：“循环工程是指用设计系统来代替你来引导智能体，而不是由你来执行指令。 这里的循环可以理解为一个递归目标，你定义一个目标，人工智能会迭代执行直到完成。”，其实有点类似 claude code 的 /goal 模式，之前我一直想去区分goal和loop的区别，但看了anthropic的blog后，其实没必要去区分，本质上都是loops。\nloop engineering和之前所说的harness engineering之类的有什么联系吗，个人感觉理念上没有什么太大的区别，之前使用harness搭建的一个agent workflow，其实本质上也是满足loop的思想的，真的要仔细区分的话，网络上有人这样分类（第12章：Loop Engineering —— 从提示 Agent 到设计循环 —— 《AI时代的软件工程》）：\nLoop Engineering (谁提示 Agent) │ └── Harness Engineering (Agent 的运行环境) │ └── 方法论层 (Agent 怎么做事) │ Skills / SDD / Ralph Loop / gstack / Goal Workflow / autoresearch │ └── 项目层 (做什么事) goscapy / Web 应用 / 微服务... 用 Osmani 的话说：Harness 让一个 Agent 安全运行，Loop 让一群 Agent 自己跑起来。\n这里补充一句更严谨的区分：harness 更偏单个 agent 的运行环境，loop 更偏触发、分发、验证、状态和持续运行的系统层。但从我自己的使用体验看，很多 harness 搭出来的 agent workflow，本质上也已经在满足 loop 的思想，所以不必过度纠结名词边界。\nClaude Code 团队给出的loop定义更工程化：agent 重复执行若干工作周期，直到某个停止条件被满足。重复执行对于agent来说很容易做到，难的是判断什么时候该停。在传统自动化里，循环通常由明确的程序条件控制，例如队列为空、测试通过、时间到达。在 agent 场景里，如果停止条件只是“模型觉得差不多了”，就会引入主观判断。Claude Code 团队强调 /goal、验证技能、代码审查和 token 边界，本质上都是为了把停止条件从模型的模糊感觉，变成可被检查的工程事实。不是所有任务都需要复杂 loop。简单任务用简单交互完成即可。复杂 loop 应该是为了解决“单轮不够、人工成为瓶颈、任务有可验证边界”这些具体问题，而不是为了追求 agent 化而 agent 化。\n1 loop的组成 loops字面意思就是循环，循环的基础要素个人理解有两个：generation (workflow) \u0026amp; verification。在实际的任务中，设计一个loop，首先要想好这个场景的业务逻辑是什么，搭建出业务的workflow，也就是之前作为人类的工作流程沉淀成agent的执行过程。此外，最重要的是verification gate，类似于评判这个工作完成的标准，这个验证的过程类似之前agent一直强调的ReAct中的observation，观察周围环境其实进一步演化就是看验证器的返回是否满足要求，然后再去reasoning→action。\n补充一下：如果是 long-running loop，还需要显式的 state / memory。也就是要记录已经处理过什么、下一步是什么、失败在哪里。否则 loop 很容易重复处理同一个反馈，或者在会话恢复、上下文压缩后丢失进度。\nosmani给出了他认为loop的组成部分：\nAutomations Worktrees Skills Plugins and connectors Sub-agents State / memory 这些部分在codex或者cc中都是有对应的实现，\n例如Claude Code 是通过调度和钩子实现的。你可以使用 /loop 定时运行提示符或命令，可以安排 cron 任务，可以使用钩子在代理生命周期的特定节点执行 shell 命令，或者如果你希望它在你合上笔记本电脑后继续运行，可以将整个流程推送到 GitHub Actions。 worktrees不必多说，不同的agent处理同一个repo的时候需要互相隔离。 关于skill，osmani在文中提到：the skill is the authoring format and a plugin is how you ship it, When you want to share a skill across repos or bundle a few together you package them as a plugin，也就是说多个skill可以打包成一个plugin，然后发布出去，便于在不同repo \u0026amp; 场景下使用； plugin \u0026amp; connectors这部分其实是为了loop更好的执行，例如在github上编辑一个项目时，需要创建issue或者pr，这时候需要使用gh api or mcp。 子代理现在应用的非常广泛了，通常按照explore → implement → verify 这种形式去构建sub agent。 Claude Code 的 /goal 命令的底层工作原理：由一个全新的模型来判断循环是否结束，而不是由之前执行工作的模型来判断，这种创建者和检查者的分离机制也应用到了停止条件本身。甚至可以在claude code中使用codex：https://github.com/openai/codex-plugin-cc 这里需要注意一个边界：/goal 的 evaluator 不会独立跑命令或读文件，它只能根据对话里已经出现的证据判断目标是否达成。所以目标最好写成可以被 transcript 证明的形式，比如某个测试命令退出码为 0、某个队列为空、某个检查项完成。\n2 loop的分类 为了避免几类 loop 混在一起，可以先看这个对照：\n类型 触发条件 停止条件 适合任务 主要风险 Turn-based loop 用户发起一次 prompt agent 给出最终回答，或者用户停止 日常问答、代码修改、一次性排查 验证依赖用户记忆 Goal-based loop 上一轮结束后自动继续 evaluator 判断目标达成，或达到轮数/时间上限 有明确验收标准的多轮任务 目标不可测会导致空转 Time-based loop 时间间隔或计划任务 用户停止、任务过期，或 agent 判断无需继续 轮询 CI、PR、部署状态 过度轮询浪费 token Proactive loop 事件、计划、API 或外部系统触发 每个任务的目标完成，routine 被关闭，或治理规则拦截 issue triage、依赖升级、告警处理 权限、成本和质量治理复杂 (1) Turn-based loops 用户发出提示，Claude 读取上下文、执行动作、检查结果、必要时重复，然后返回它认为完成的结果。这个循环由用户手动触发，也由 Claude 自己判断何时结束或何时需要更多上下文。Claude 会读代码、修改代码、跑测试，然后交还结果，用户再检查并决定下一步。这里的风险是，验证往往依赖用户手工补位。如果用户每次都要提醒“打开浏览器检查”“看控制台”“截图对比”，那么 loop 虽然有 agent 参与，但质量门槛仍然外置在人的记忆里。\n改进方式也很简单，把人工验证步骤写进 SKILL.md。skill 不是为了让 prompt 变长，而是把团队的“完成定义”和“检查动作”变成可复用的程序化习惯。例如 UI 改动不能只看编辑成功，还要启动服务、交互验证、截图、检查控制台、跑性能审计。这样 turn-based loop 仍然简单，但质量边界更清楚。\n(2) Goal-based loop goal-based loop 用 /goal 扩展 agent 的迭代时间。它适合单轮无法完成、但完成标准可以被明确验证的任务。停止条件是目标达成，或者达到用户设定的最大轮数。这类 loop 的价值在于：用户不再需要每轮手动判断“继续还是停止”，系统会用目标条件约束 agent。一个 evaluator model 会检查条件，如果不满足就把 Claude 送回继续工作，直到达标或达到轮数上限。\n这里的设计重点是目标要可测。比如“把 Lighthouse 分数提升到 90 以上，最多尝试 5 次”比“把首页优化一下”更适合 /goal。前者有量化指标、清晰停止点和成本上限；后者会让 agent 在“还可以更好”的开放空间里游走。\n对工程团队来说，/goal 最适合三类任务：\n有自动化测试、评分或静态检查的任务。 有明确验收标准但需要多次尝试的修复。 可以容忍有限探索，但不能无限消耗的优化工作。 (3) Time-based loop time-based loop 用 /loop 或 /schedule 解决“任务本身会反复出现”的问题。它的触发条件不是用户当下发 prompt，而是时间间隔或计划任务。文章给的典型场景包括每天总结 Slack 消息，或者定期检查 PR 是否收到 review、CI 是否失败。这些任务共同点是：工作模式相同，输入随时间变化；或者外部系统状态会变化，需要 agent 定期查看并响应。文章也提醒不要频繁轮询。更长间隔或基于事件触发通常更省 token，也更接近真实需求。如果外部系统每小时才变化一次，每 5 分钟检查就是浪费。\n这里有一个重要边界：/loop 在本机运行，机器关掉就停止；/schedule 则把循环迁移到云端例行任务。这不是单纯部署位置差异，而是责任模型变化。本机 loop 更像个人辅助，云端 schedule 更像持续运行的自动化流程，需要更谨慎地设置频率、权限和失败处理。 cc的loop终止条件是no tool calls:\n更准确地说，这里的“no tool calls”对应 Claude Code Agent SDK / turn-based inner loop：Claude 评估、调用工具、接收工具结果并继续，直到某一轮没有 tool call 才输出最终答案。它不是 /loop 这种 time-based scheduled task 的停止条件。\n这种模式很适合修复已知问题，例如cc官方给出的例子：“修复 auth.ts 中失败的测试”，前几个turn可能会使用bash read write等tool，直到最后一次输出text-only response，这个时候便认为cc完成了这个loop。\n(4) Proactive loops proactive loop 是最高复杂度的形态：它由事件或计划触发，不需要人实时在场；每个任务有自己的目标，整个 routine 一直运行直到被关闭。适用场景包括 bug report、issue triage、迁移、依赖升级等“持续流入、结构相对稳定”的工作。它可以是多个 Claude Code 原语的组合：/schedule 负责定期检查，/goal 定义完成标准，dynamic workflows 负责把 triage、修复、review 等步骤编排给多个 agent，auto mode 让流程不必每一步都请求人工许可。\n这类系统的关键不是“自动化程度更高”，而是“治理难度更高”。一旦无人实时介入，就必须提前设计：\n哪些动作允许自动执行？ 哪些判断必须用更强模型或人工介入？ 如何避免重复处理同一个反馈？ 如何审查改动质量？ 如何限制 token 和并发 agent 数量？ 如何记录状态，以便下一次运行知道已经做过什么？ cc官方建议把小模型用于路由和常规任务，把最强模型用于判断型任务。这反映出一个实用原则：proactive loop 的成本控制不只靠少跑几次，也靠把不同难度的判断分配给不同能力和价格的模型。\n3 loop使用注意事项 (1) 系统环境很重要 这个系统环境我理解是agent运行的物理机器与目录 + 可用工具与权限 + 会话上下文生命周期 + 跨会话记忆 + 技能/Hook等。\nloop 的输出质量不是单个 prompt 决定的，而是由整个系统决定的。Claude 会沿用代码库已有模式，所以代码库越干净，agent 越容易做出一致改动；文档越容易访问，agent 越少凭过时知识猜测；验证 skill 越具体，agent 越能自查；独立 reviewer 越常用，越能减少主 agent 被自己推理路径影响的偏差。最值得注意的是：当某个结果不达标时，不应该只修那个单点问题，而应该把教训编码进系统。这句话本质上是在说，agent 工作流的改进对象不是“这一次回答”，而是“下一次循环的环境”。如果某类错误反复出现，就该写成 skill、测试、脚本、workflow 或 review gate。\n(2) 复杂 loop 必须有预算意识 CC把 token 管理拆成几个具体动作：\n选择合适的原语和模型，不要让小任务承受多 agent 或长 loop 的开销。 定义明确成功和停止标准，让 agent 不会过早停，也不会无限继续。 大规模运行前先小范围 pilot，尤其是 dynamic workflows 可能生成大量 agent。 目前凭我这几次使用dynamic workflows 的经验来看，该功能还不成熟，尤其是cc经常会重复生成agent，而且有的时候某些agent不会返回结果，这会导致这个workflow一直卡在未返回结果的agent上，造成阻塞 对确定性工作用脚本，而不是每次让模型重新推理。 不要把 routine 跑得比外部变化更频繁。 用 /usage、/goal、/workflows 等工具检查 token 消耗。 这里最有工程价值的是“脚本化确定性工作”。如果某件事步骤固定、输入输出明确，例如 PDF 表单填写、格式转换、批量检查，就应该写脚本让 agent 调用。模型负责判断和协调，脚本负责机械执行。这样既降低 token，又提高一致性。\n(3) 停止条件比触发条件更重要 触发条件决定 loop 何时开始，停止条件决定风险是否可控。大多数失败来自停止条件不清楚：agent 可能提前交付半成品，也可能为了“更好”继续扩大范围。好的停止条件通常具备三个特征：\n可观察：测试数、分数、CI 状态、队列长度、工单状态。 可判定：不是“更好一些”，而是“达到 X”或“没有 Y”。 有上限：最多轮数、最大时间、最大 token 或最大改动范围。 如果停止条件不可观察，evaluator 就只能猜。如果停止条件没有上限，loop 就很容易把一次任务变成无限探索。\n4 Future 如果一个loop搭建的很烂，然后这个loop一直都是ai review，ai fix，这样会陷入恶性循环，这不是我们想要的，真正应该做的是构建loop之前规划好自己的workflow，对每个phase都由最了解的人去把关，这样最后的效果才可能达到预期。对于工程师来说，我们不能只做一键启动loop的人，而是对业务逻辑，整体workflow了如指掌的人。\n换句话说，loop engineering 的重点不是让 agent 替你思考一切，而是把可重复的执行、可检查的验证和可追踪的状态设计好，让 agent 在这个系统里可靠地工作。\nCredits Getting started with loops | Claude by Anthropic Loop Engineering | Addy Osmani How the agent loop works | Claude Code Docs Keep Claude working toward a goal | Claude Code Docs Run prompts on a schedule | Claude Code Docs Automate work with routines | Claude Code Docs 第12章：Loop Engineering —— 从提示 Agent 到设计循环 —— 《AI时代的软件工程》 openai/codex-plugin-cc Citation Citation: When reposting or citing content from this article, please credit the original author and source.\nCited as:\nZachary WEI. (Jul 2026). Looping：从提示 Agent 到设计循环. https://zachary-ww.github.io/zh/posts/looping/\nOr\n@article{zachary2026-looping, title = \u0026#34;{Looping：从提示 Agent 到设计循环}\u0026#34;, author = \u0026#34;Zachary WEI\u0026#34;, journal = \u0026#34;zachary-ww.github.io\u0026#34;, year = \u0026#34;2026\u0026#34;, month = \u0026#34;Jul\u0026#34;, url = \u0026#34;https://zachary-ww.github.io/zh/posts/looping/\u0026#34; } ","permalink":"https://zachary-ww.github.io/zh/posts/looping/","summary":"一篇关于 loop engineering 的笔记，记录 loop 与 harness 的关系、loop 的组成、几类 loop 的差异，以及复杂 loop 在验证、预算和系统环境上的注意事项。","title":"Looping：从提示 Agent 到设计循环"},{"content":"C++ 语法基础 const 关键字 作用：修饰变量、函数、指针或成员函数，表示只读语义或不修改对象状态。\n（1）修饰变量：\n修饰常量，常量不能被改变；编译器可能进行常量折叠或替换。 可以用 const_cast 去除指针或引用的底层 const，但只有原对象本身不是 const 时才允许修改；若原对象本身为 const，修改属于未定义行为。 （2）修饰指针/引用\n常量指针（底层 const）指向的常量不可改变 const int* p; int const* p; 指针常量（顶层 const）指针不可改变 int * const p; 常量引用，只读常量的引用 const int a = 10; const int \u0026amp;b = a; （3）修饰成员函数（放在末尾）：表示保证不修改对象状态\nconst 对象只能调用 const 方法\nconst 函数中如果想调用其他方法，调用的方法必须是 const 的\nclass A { public: int func() const { // 表示保证不修改对象状态 // 编译错误，不能修改成员变量的值 m_value = 10; return m_value; } private: int m_value; }; static 关键字 static 可以修饰变量、函数以及类的静态成员，不能直接\u0026quot;修饰类\u0026quot;。\n（1）static 修饰变量\nstatic 全局变量 将变量的作用域限定在当前文件中 在程序启动时被初始化（执行main函数前执行全局初始化函数） 生命周期和程序一样长 static 局部变量 函数返回后不会销毁，下次调用函数时继续使用（只初始化一次），直到程序结束才销毁。 作用域仅限本函数，生命周期和程序一样长 （2）static 修饰函数\n将函数的作用域限定在当前文件中，使得其他文件无法访问该函数，以避免命名冲突和代码重复定义\n（3）static 修饰类成员变量和成员函数 （不属于对象，属于类）\nstatic 成员变量 类外初始化（const static 类内初始化） 多个对象共享变量 static 成员函数 没有 this 指针，因此静态成员函数不能声明为 const。 不能访问非 static 成员/函数 volatile 关键字 作用：修饰变量，表示该变量可能被编译器无法感知的外部因素修改，例如硬件寄存器或信号处理流程。编译器会避免把相关访问优化成寄存器缓存。注意：volatile 不是线程同步工具，不能保证原子性、内存顺序或数据竞争安全；线程间同步应使用 std::atomic、互斥锁等机制。\nmutable 关键字 用于修饰类的成员变量，表示该成员变量即使在一个 const 成员函数中也可以被修改。\nexplicit 关键字 explicit 通常用于构造函数的声明中，用于防止隐式转换，提高代码安全性\nextern 关键字 extern 用于指示变量或函数的定义在另一个源文件中，并在当前源文件中声明，说明该符号具有外部链接（external linkage）属性。\n编译完所有源文件后，链接器会将 extern 的所有引用（extern 声明在其他文件中会建立一个对该符号的引用）解析为在编译的源文件中找到的一个定义。 作用：\n声明变量但不定义 常量全局变量的外部链接（const 全局变量默认内部链接） extern \u0026ldquo;C\u0026rdquo; 的作用 如果不用 extern \u0026quot;C\u0026quot;，由于 C++ 和 C 语言在编译和链接时使用的命名规则不同，这会导致 C++ 代码无法调用 C 语言编写的函数或变量（链接时找不到符号）。\nextern \u0026#34;C\u0026#34; { // C 语言函数或变量的声明 } // 会采用 C 语言的链接规则，即符号的名称和调用约定与 C 语言相同 （1）C 和 C++ 函数命名规则不同（name mangling）\n对于 C++ 语言，由于需要支持重载，编译器会对函数名进行名称修饰（name mangling），通常会把函数名、命名空间、类作用域和参数类型等编码进符号名。函数重载本身通常不依赖返回值类型区分。\n命名规则在不同 OS、不同编译器都可能不同 C++ 声明和定义 声明是指告诉编译器某个符号的存在，在程序变量表中记录类型和名字，而定义则是指为该符号分配内存空间或实现其代码逻辑。\n（1）变量\n凡是没有带extern的声明同时也都是定义\n（2）函数\n带有{}是定义，否则是声明\ndefine 和 inline 区别 （1）define\n定义宏，可以定义常量/函数，直接替换，没有类型检查，预处理时替换\n#define SQUARE_SUM(x, y) ((x) * (x) + (y) * (y)) // 写宏函数一定要注意：参数和函数体应当用括号包围，避免因运算优先级导致的错误。 （2）inline\ninline 不保证实际展开，是否消除函数调用开销由编译器优化器决定；递归、复杂控制流等场景通常由编译器自行判断。语言层面上，inline 还允许函数定义出现在多个翻译单元中。 有类型检查，可以调试 除了inline，其他方法声明内联函数 定义在类内部 模板函数：模板定义通常放在头文件中，实例化后是否实际内联仍由编译器决定。 使用 __attribute__((always_inline))：在支持 GNU 扩展的编译器（如 GCC）中，可以使用 __attribute__ 尝试强制内联。 使用 #pragma 指令：在支持特定编译器指令的编译器中，可以使用 #pragma 指示编译器内联函数。 使用编译器特定的内联关键字：某些编译器可能有自己的内联关键字，比如在MSVC中可以使用__forceinline。 使用 constexpr 关键字：constexpr 表示函数可用于常量求值，不等价于一定被内联展开。 define 和 typedef 区别 （1）define：同上\n（2）typedef：是一种类型定义关键字，用于为现有类型创建新的名称（别名），编译阶段处理\n（3）区别：\n作用域：define（没有），typedef（受到命名空间、类限制） define 不支持模板，typedef 支持： // 使用 typedef 定义模板类型别名 template \u0026lt;typename T\u0026gt; struct MyContainer { typedef std::vector\u0026lt;T\u0026gt; Type; }; // 使用 using 定义模板类型别名（C++11 及以后） template \u0026lt;typename T\u0026gt; struct MyContainer { using Type = std::vector\u0026lt;T\u0026gt;; } 指针操作不同： typedef int * ptr; #define PTR int * const ptr p = \u0026amp;a; // int * const p const PTR q = \u0026amp;b; // const int * q 字节对齐 字节对齐是内存分配的一种策略，如果不对数据存储进行适当的对齐，可能会导致存取效率降低\n（1）自然边界对齐：基本类型的自身大小\n（2）结构体对齐：\n结构体本身的总大小也会根据其最大对齐边界的成员进行对齐（比如结构体成员包含的最长类型为int类型，那么整个结构体要按照4的倍数对齐），以便在数组中正确对齐\n空 struct / class 内存为 1，为了区别空结构体/类 （3）联合体union对齐\n联合体的大小等于其最大大小的成员，因为联合体的所有成员共享相同的内存空间。\n（4）通过编译器指令修改对齐规则\n例如 #pragma pack\n（5）指定对齐属性\n在 C++11 及更高版本中，可以使用 alignas 关键字为数据结构或变量指定对齐要求。这个命令是对某个类型或者对象生效的。例如，alignas(16) int x; 将确保 x 的地址是 16 的倍数。\nclass 和 struct 的区别 C 语言 struct 和 C++ struct 的区别：\nC 中没有方法（函数） C 中没有 private 和 protected 权限 不可以继承 C++中 class 和 struct 的区别\n默认权限/继承不同（private / public） 在模板参数列表中可以写 class 或 typename，不能写 struct。这不是 class 与 struct 类型定义能力的区别。 template \u0026lt;class T\u0026gt; struct Person { public: T age; }; // 错误 template \u0026lt;struct T\u0026gt; struct Person { public: T age; }; 使用 struct 多用于 POD（Plain Old Data），用于兼容旧的函数保持数据结构不变（例如 memcpy） sizeof 运算符 作用： 编译期间，计算一个类型/对象的所占用的内存大小（字节数）\n常考：\nsizeof(指针)：大小永远是固定的，取决于处理器位数，32位就是 4 字节，64位就是 8 字节 此外数组作为函数参数时会退化为指针，大小要按指针的计算 如果数组作为引用传递则不会退化，该是多大就多大，因为引用避免拷贝的同时传递的是相同的地址 sizeof(结构体)：内存对齐 sizeof(字符串数组)：算上末尾的 \u0026lsquo;\\0\u0026rsquo; sizeof 和 strlen strlen：C 标准库中的函数，用于计算 C 风格字符串（以空字符 \u0026lsquo;\\0\u0026rsquo; 结尾的字符数组）的长度，即不包括结尾的空字符的字符个数。\nsizeof：见上\nC++ 类型转换 （1）static_cast\n编译时期转换 基本类型的转换 指针类型之间的转换 上行转换：安全 下行转换（基类→派生类）：可能不安全 引用类型转换：同指针 （2）dynamic_cast\n主要应用于父子类层次结构中的安全类型转换\n解决下行转换不安全的问题 如果转换失败，dynamic_cast 将返回空指针（对于指针类型）或抛出异常（对于引用类型） 用于多态类型检查 dynamic_cast 只有在基类存在虚函数(虚函数表)的情况下才有可能将基类指针转化为子类 （3）const_cast\n当需要修改 const 对象时，可以使用 const_cast 来删除 const 属性。 （4）reinterpret_cast\n用于在不同类型之间进行低级别的转换，仅仅是重新解释底层比特（也就是对指针所指针的那片比特位换个类型做解释），而不进行任何类型检查。 int a = 42; int* int_ptr = \u0026amp;a; char* char_ptr = reinterpret_cast\u0026lt;char*\u0026gt;(int_ptr); // 将int指针转换为char指针 指针和引用的区别 区别：\n指针是一个变量，它保存了另一个变量的内存地址；引用是另一个变量的别名，与原变量共享内存地址。 指针(除指针常量)可以被重新赋值，指向不同的变量；引用在初始化后不能更改，始终指向同一个变量。 指针可以为 nullptr，表示不指向任何变量；引用必须绑定到一个变量，不能为 nullptr。 使用指针需要对其进行解引用以获取或修改其指向的变量的值；引用可以直接使用，无需解引用。 指针有多级，引用没有 指针和引用的自增(++）和自减含义不同，指针是指针运算, 而引用是代表所指向的对象对象执行++或\u0026ndash; 联系：\n很多实现中引用可按类似指针的方式实现，但语言层面引用不是指针常量。 指针传递、值传递和引用传递 值传递：形参的修改不会影响到实参 引用传递：是将实参的引用传递给形参。在这种情况下，函数内对形参的修改会影响到实参。 指针传递：是将实参的地址传递给形参（实际上也是值传递，传递的是指针）。在这种情况下，函数内对形参的修改会影响到实参。 智能指针 智能指针通过 RAII 管理动态资源，避免手动 new / delete 带来的泄漏和异常安全问题。\nstd::unique_ptr：独占所有权，不能拷贝，可以移动，适合表达唯一资源拥有者。 std::shared_ptr：共享所有权，通过引用计数管理对象生命周期；最后一个 shared_ptr 析构时释放对象。 std::weak_ptr：不增加引用计数，常用于打破 shared_ptr 循环引用。 C++ 面向对象 C++ 面向对象三大特性 （1）封装性\n将属性和方法封装在类中，隐藏类的内部实现细节，仅暴露必要的接口给外部。\n通过封装，可以控制类成员的访问级别，限制对类内部数据的直接访问，确保数据的完整性和安全性 （2）继承性\n继承是一个类（派生类，Derived Class）从另一个类（基类，Base Class）那里获得其属性和方法的过程\n减少重复代码，提高代码复用性和可维护性 单一、多重、菱形(虚继承) （3）多态性\n同一个函数或者操作在不同的对象上有不同的表现形式\n静态多态：在程序在编译期就确定了对象类型和调用的函数地址（函数模板、重载） 静态多态的对象类型和函数是在编译时确定的，因此不会产生额外的性能开销。同时静态多态也不需要为每个类创建虚表，因此在空间上也不会像动态多态那样产生额外的消耗 动态多态（基于虚表进行动态绑定：虚函数、纯虚函数） 必须通过基类的指针或者引用调用虚函数，被调用的函数是虚函数，且必须完成对基类虚函数的重写 动态多态发生在运行时，通常在运行时需要一个指针间接寻址，因此会有额外的性能开销 重载、重写、隐藏 （1）重载（Overloading）\n重载是指相同作用域(比如命名空间或者同一个类)内拥有相同的方法名，但具有不同的参数类型和/或参数数量的方法。 重载允许根据所提供的参数不同来调用不同的函数\n（2）重写（Overriding）\n重写是指在派生类中重新定义基类中的方法。当派生类需要改变基类方法的功能时，就需要用到重写。\n基类被重写函数需要使用virtual修饰 （3）隐藏（Hiding）\n派生类的函数屏蔽了与其同名的基类函数，只要同名函数，不管参数列表是否相同，基类函数都会被隐藏\n类对象构造和析构顺序 （1）类构造顺序：基类初始化（优先虚继承）→ 成员变量初始化（按照声明顺序）→ 构造函数\n（2）析构顺序和构造相反\n委托构造函数 这种构造函数可以委托其他构造函数来帮它完成一些对象成员的初始化操作\n被委托的构造函数在委托构造函数的初始化列表里被调用，而不是在委托构造函数的函数体里被调用。 委托构造函数的初始值列表中，只允许出现被委托的构造函数，而不能直接给成员变量进行初始化。 class student { public: int age = 18; int weight = 120; student(int give_age):student(give_age,180)//委托构造函数 { std::cout \u0026lt;\u0026lt; \u0026#34;委托老兄办点事！\u0026#34; \u0026lt;\u0026lt; std::endl; } student(int give_age,int give_weight):age(give_age),weight(give_weight) { std::cout \u0026lt;\u0026lt; \u0026#34;我被委托做一些事情！\u0026#34; \u0026lt;\u0026lt; std::endl; } }; int main() { const student stu = student(20); std::cout \u0026lt;\u0026lt; stu.age \u0026lt;\u0026lt; std::endl \u0026lt;\u0026lt; stu.weight \u0026lt;\u0026lt; std::endl; return 0; } // \u0026#34;我被委托做一些事情！\u0026#34; // \u0026#34;委托老兄办点事！\u0026#34; // 20 180 析构函数中是否可以抛出异常 不建议在析构函数中抛出异常。现代 C++ 中析构函数通常默认是 noexcept，析构期间抛出异常可能导致 std::terminate；如果正在栈展开时再次抛异常，风险更高。\n如果抛出异常的解决方法：\n（1）调用 abort()\nDBConn::~DBConn() { try { db.close(); } catch(...) { abort(); // 强制结束程序 } } （2）吞下异常\nDBConn::~DBConn() { try{ db.close();} catch(...) { //制作运转记录，记下对close的调用失败！ } } （3）把调用close的责任从DBConn析构函数手上移到DBConn客户手中\n如果一定要处理，只能在除析构函数以外的函数中处理\n浅拷贝和深拷贝 （1）浅拷贝\n仅复制对象的基本类型成员和指针成员的值，而不复制指针所指向的内存。可能导致两者共享资源，造成内存泄漏、意外修改资源\n默认拷贝构造函数 （2）深拷贝\n在浅拷贝基础上，还复制指针所指向的内存\n需要显式实现拷贝构造和赋值拷贝构造 类中this指针 this是一个指向当前对象的指针，python中为self\nthis 实际上是成员函数的一个形参，在调用成员函数时将对象的地址作为实参传递给 this（隐式） 常量成员函数（const member function）中，this 指针的类型是指向常量对象的常量指针（const pointer to const object）（这就是为什么static和const不能同时修饰类函数），因此不能用来修改成员变量的值 static 函数没有this指针，因为其属于类而非某个对象，不能访问非static 成员 C++虚函数表 虚函数表（Virtual Table，V-Table）中保存类的虚函数地址，用于支持运行期的动态派发。它能在基类指针或引用调用虚函数时，找到实际对象对应的函数实现。\n一个对象在内存中一般由成员变量（非静态）、虚函数表指针(vptr)构成 虚函数表指针指向一个数组，数组的元素就是各个虚函数的地址，通过函数的索引，我们就能直接访问对应的虚函数 纯虚函数 **定义：**纯虚函数是在基类中声明为 = 0 的虚函数，作用类似定义接口；派生类通常需要实现它，否则派生类也会成为抽象类。纯虚函数也可以在类外提供定义，但类仍然是抽象类。\n包含纯虚函数的类称为抽象类（Abstract Class），抽象类无法实例化。 class Shape { public: // 纯虚函数 virtual void draw() = 0; }; 构造函数和析构函数能不能是虚函数 （1）构造函数不能是虚函数\n虚函数的主要作用是实现多态，即派生类重写基类函数，但每个类都有自己的构造函数，不需要被重写 构造/析构期间调用虚函数不会动态派发到更派生类版本，而是按照当前正在构造或析构的类型解析。因此构造函数不应依赖虚调用实现多态行为。 （2）析构函数可以是虚函数\n当使用基类指针或引用指向派生类时，如果基类的析构函数没有设置成虚函数，则在 delete 基类时无法调用派生类的析构，可能造成内存泄漏。只有将基类析构函数设置成 virtual，才会正确调用派生类析构函数。 析构函数为什么默认不是虚函数？（零成本抽象）声明虚函数会产生 vptr 和 vtable，造成额外的内存开销，如果我们不希望该类为基类，则不需要设置虚函数。 一个函数不能既是模板函数又是虚函数 因为C++的编译与链接模型是\u0026quot;分离\u0026quot;的 一个 function template最后到底会被实例化为多少个函数，要等整个程序(所有的编译单元)全部被编译完成才知道 同时，virtual function的实现大多利用了一个\u0026quot;虚函数表\u0026quot;，虚函数表的内存布局需要在这个类编译完成的时候就被完全确定 综上就会导致编译时无法确定需要调用哪个特定的模板实例 C++ 空类的大小 为1，C++标准要求每个对象都必须具有独一无二的内存地址\n如果一个空类做基类，则基类不占用空间（空基类优化） 虚基类 解决菱形继承的问题，当派生类从多个基类派生，而这些基类又有共同基类，则在访问此共同基类中的成员时，将产生冗余，并有可能因冗余带来不一致性。\nC++11 新特性 nullptr 和 NULL 由于C++中引入了函数重载和模板，而NULL实际上只是整数的0，不能代表指针类型，故引入nullptr，避免了二义性，更加安全\n类型萃取(type_traits) type_traits是一种编译时技术，用于在编译期间 获取和操作 类型的信息，用于泛型编程以及在编译时做出决策\ntemplate \u0026lt;typename T\u0026gt; typename std::enable_if\u0026lt;std::is_integral\u0026lt;T\u0026gt;::value, T\u0026gt;::type foo(T t) { std::cout \u0026lt;\u0026lt; \u0026#34;foo() called with an integral type: \u0026#34; \u0026lt;\u0026lt; t \u0026lt;\u0026lt; std::endl; return t; } template \u0026lt;typename T\u0026gt; typename std::enable_if\u0026lt;std::is_floating_point\u0026lt;T\u0026gt;::value, T\u0026gt;::type foo(T t) { std::cout \u0026lt;\u0026lt; \u0026#34;foo() called with a floating point type: \u0026#34; \u0026lt;\u0026lt; t \u0026lt;\u0026lt; std::endl; return t; } int main() { foo(42); // Output: foo() called with an integral type: 42 foo(3.14); // Output: foo() called with a floating point type: 3.14 } 使用 std::enable_if 和类型萃取来选择不同的函数实现\n智能指针 见前文的 RAII 和智能指针小节。C++11 将 std::unique_ptr、std::shared_ptr、std::weak_ptr 纳入标准库，推荐优先使用智能指针表达资源所有权，减少裸指针负责释放资源的场景。\nLambda 表达式 https://zhuanlan.zhihu.com/p/384314474\nlambda 表达式可以让代码更简洁；它定义了一个匿名函数对象，并且可以捕获一定范围内的变量。\nlambda 表达式的构成：\n[captures]\u0026lt;tparams\u0026gt;(params)lambda-specifiers{body;}; captures 捕获列表，lambda可以把上下文变量以值或引用的方式捕获，在body中直接使用。 [] 什么也不捕获 [=] 按值的方式捕获所有变量 [\u0026amp;] 按引用的方式捕获所有变量 注意[=, =a]是错误的，[=, \u0026amp;a]正确的 [this] 在成员函数中，也可以直接捕获this指针，其实在成员函数中，[=]和[\u0026amp;]也会捕获this指针。 tparams 模板参数列表：C++20 引入，让 lambda 可以像模板函数一样被调用。 params 参数列表：C++14 之后允许使用 auto 作为参数类型。 lambda-specifiers 说明符：常见的是 mutable，允许修改按值捕获进来的副本；异常说明和尾置返回类型也可以出现在 lambda 声明中。 trailing-return-type 返回值类型：一般可以省略，由编译器推导。 body 函数体：函数的具体逻辑。 编译器会将 lambda 表达式翻译成一个类，并重载 operator()，如果 captures 是按值传递的（不含任何引用传递），自动加上 const，如果要修改，应该在说明符位置加上 mutable。\n左值引用、右值引用、move、完美转发 左值引用通常绑定到有名字、可取地址的对象；右值引用用于绑定临时对象，配合移动构造、移动赋值转移资源所有权。std::move 本身不移动对象，只是把表达式转换为右值引用；完美转发通常使用万能引用和 std::forward 保留实参的值类别。\nC++ 内存管理 C++ 内存分区 常见的进程内存区域包括代码区、全局/静态区、堆区和栈区。局部自动变量通常位于栈上，动态分配对象通常位于堆上，全局变量和静态变量位于全局/静态存储区。具体布局会受操作系统、编译器和 ABI 影响。\nC++ RAII 资源获取即初始化（Resource Acquisition Is Initialization，简称 RAII）是一种 C++ 编程技术，它将在使用前获取（分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥量、磁盘空间、数据库连接等有限资源）的资源的生命周期与某个对象的生命周期绑定在一起。\n利用栈上局部变量的自动析构来保证资源一定会被释放 C++ malloc/free 底层原理 在 Linux 中，若通过 malloc() 请求一大块内存，C 运行库将创建一个匿名内存映射，而不使用堆内存。\u0026ldquo;大块\u0026quot;意味着比阈值 MMAP_THRESHOLD 还大，缺省为 128KB，可通过 mallopt() 调整。\nmm_struct 结构是对整个用户空间（进程空间）的描述：\nstruct mm_struct { unsigned long start_brk, brk, start_stack; }; // 其中 start_brk 和 brk 分别是堆的起始和终止地址，我们使用 malloc 动态分配的内存就在这之间。 // start_stack是进程栈的起始地址，栈的大小是在编译时期确定的，在运行时不能改变。 实际中系统分配较大的堆空间，进程通过 malloc() 库函数在堆上进行空间动态分配，堆如果不够用，malloc 可以进行系统调用（brk 和 sbrk 系统调用），增大 brk 的值。\nmalloc 实现方案：\n为避免频繁进行用户态和内核态之间的切换，malloc采用的是内存池的实现方式（先申请一大块内存，然后将内存分成不同大小的内存块，然后用户申请内存时，直接从内存池中选择一块相近的内存块即可） new / delete 和 malloc / free 的区别 （1）申请内存的位置\nnew：自由存储区；malloc：堆。通常实现上都来自堆，但概念层次不同。\nps：自由存储区是一个抽象概念，一般为堆，也可以是其他区域，主要看operator new的实现\n指定内存地址分配：placement new：new (place_address) type，实际上调用了一个特殊的、不可重定义的 placement operator new。 （2）申请时是否需要指定大小\nnew：no；malloc：yes\n（3）内存分配失败的返回值\nnew：bad_alloc；malloc：NULL\n（4）是否调用构造和析构\nnew：yes；malloc：no\n（5）new/delete 底层可以调用malloc和free\nvoid * operator new (size_t size) { if(void * mem = malloc(size)) return mem; else throw bad_alloc(); } void operator delete(void *mem) noexcept { free(mem); } （6）是否可以重载\nnew：yes；malloc：no\n（7）是否可以重新分配内存\nnew：no；malloc：通过realloc （先判断当前的指针所指内存是否有足够的连续空间，如果有，原地扩大可分配的内存地址，并且返回原来的地址指针；如果空间不够，先按照新指定的大小分配空间，将原有数据从头到尾拷贝到新分配的内存区域，而后释放原来的内存区域。）\nC++ 内存泄漏 （1）定义：申请了动态内存，却没有释放\n（2）检测：\n手动检测 工具： Valgrind valgrind --leak-check=yes your_program AddressSanitizer （3）如何避免\nRAII / 智能指针 异常安全：在C++中，如果程序抛出异常，需要确保在异常处理过程中正确释放已分配的内存。使用try-catch块来捕获异常并在适当的位置释放内存 野指针和悬空指针 （1）野指针：未被初始化指针\n值不确定，可能指向任何地方，造成数据损坏 （2）空悬指针：指向已经被释放（如删除、回收）的内存的指针\nC++ 常见内存错误 （1）间接引用坏指针\nint val; scanf(\u0026#34;%d\u0026#34;, \u0026amp;val); // 坏指针： scanf(\u0026#34;%d\u0026#34;, val) // 如果val地址可RW，那么原先的内容就会被覆盖 （2）读没有初始化的内存\n例如堆内存里面的变量默认没有初始化\n（3）栈缓冲区溢出\nvoid bufoverflow() { char buf[64]; // gets 已被废弃，这里仅用于说明栈缓冲区溢出风险。 gets(buf); return; } （4）误解指针运算\nwhile (*p \u0026amp;\u0026amp; *p != val) p += sizeof(int); /* Should be p++ */ （5）引用不存在的变量\nint *stackref () { int val; return \u0026amp;val; } （6）内存泄漏\nCoredump 在程序发生某些错误而导致进程异常退出时，Linux 内核会根据进程当时的内存信息，生成一个 coredump 文件\ncore dump 文件的路径是由 /proc/sys/kernel/core_pattern 定义的 C++ STL STL Standard Template Library，标准模板库，STL 包含 6 大部件：容器、迭代器、算法、仿函数、容器适配器和空间配置器。\n容器：容纳一组元素的对象，提供各种数据结构 迭代器：提供访问容器元素的方法。从实现角度看，迭代器通常是重载了 operator*、operator-\u0026gt;、operator++ 等操作的类模板。 算法：包括查找算法、排序算法等等 仿函数：一个行为类似函数的对象，调用它就像调用函数一样，重载了 operator() 的类或者类模板 容器适配器：用来修饰容器等，比如queue和stack，底层借助了deque 空间配置器：负责空间配置和管理，是一个实现了动态空间配置，空间管理，空间释放的类模板 容器种类 （1）序列容器\narray(固定大小数组), vector, list, forward-list(单链表), deque\n（2）关联容器\nset, multiset, map, multimap\n（3）无序容器\nunordered_map, unordered_set, unordered_multiset, unordered_multimap\nvector （1）概述\n支持动态扩容的连续线性空间，支持快速随机访问，尾部增删元素\n（2）底层原理\n底层是一个动态数组，包含三个迭代器，start和finish之间是已经被使用的空间范围，end_of_storage是整块连续空间包括备用空间的尾部\n当插入N个元素时，推导出扩容次数为log2(N)，每次扩充引起的元素拷贝次数为：2^0 + 2^1 + 2^2, \u0026hellip;, 2^lgN。相加得：2 * 2^lgN - 1 约为 2N次，共拷贝了N次最后一个元素， 所以总的操作大概为3N。\n（3）扩容机制\n当空间不够容纳新元素（如 vec.push_back(val)）时，vector 会申请一片更大的连续空间，移动或拷贝原有元素，然后释放原空间。扩容倍率是实现细节，常见实现可能采用约 1.5 倍或 2 倍增长。只要触发重新分配，指向原 vector 存储区的迭代器、指针和引用都会失效。\n（4）size和capacity\nsize指容器的元素个数 capacity指容器分配存储空间中能够容纳的元素个数 （5）reserve和resize\n**reserve的作用是更改vector的容量（capacity），size没有变，使vector至少可以容纳n个元素，**如果n大于vector当前的容量，reserve会对vector进行扩容。其他情况下都不会重新分配vector的存储空间，可以减少多次开辟、释放空间的问题 resize是改变容器的大小，且创建对象，改变了capacity和size，分配内存且调用构造函数创建对象 （6）vector元素可以是引用吗\n不能，vector中的元素有两个要求：可以赋值和拷贝，引用必须初始化，但是vector开辟内存时无法初始化\n（7）vector迭代器失效的情况\nvector扩容造成内存重新分配，迭代器全部失效 erase删除元素，导致迭代器失效，erase返回下一个有效iter // 删除指定元素 int main() { vector\u0026lt;int\u0026gt; nums{1, 2, 3, 4}; for (auto iter = nums.begin(); iter != nums.end();) { if (*iter == 3) { iter = nums.erase(iter); } else { ++iter; } } for (auto i : nums) { cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; endl; } return 0; } （8）vector开辟内存在哪，能创建10万个元素吗\n动态扩容通常发生在堆上，系统必须有足够的可用内存来存储这些元素。\n（9）vector作为函数返回值\n返回 vector 时通常不必过度担心拷贝成本：RVO/NRVO 可以进行复制消除，C++11 以后还可以利用移动语义降低返回成本。\n如果希望复用已有对象或避免重新构造，也可以改成通过引用参数输出，例如 void fun(Base\u0026amp; x)。 （10）emplace_back和push_back\n相同：都支持左值和右值的传入，左值调用构造函数创建匿名对象，右值调用移动构造函数 不同：emplace_back 还支持另一种调用方式，原地构造，传入的参数可以是vector类型的构造函数的参数，直接原地构造 emplace_back(10, \u0026#34;test\u0026#34;) // 可以只调用一次构造函数 push_back(MyClass(10, \u0026#34;test\u0026#34;)) // 会先构造临时对象，再移动或拷贝到容器中；具体是否被优化由编译器和标准版本决定。 （11）一个vector内存很大但实际我只用了很小一部分怎么解决\n// 利用swap释放内存 vector\u0026lt;int\u0026gt; nums{1, 2, 3, 3, 4}; nums.reserve(10); vector\u0026lt;int\u0026gt;(nums).swap(nums); // nums_swap.reserve(5); cout \u0026lt;\u0026lt; \u0026#34;nums capacity\u0026#34; \u0026lt;\u0026lt; nums.capacity() \u0026lt;\u0026lt; endl; // 5 cout \u0026lt;\u0026lt; \u0026#34;nums size\u0026#34; \u0026lt;\u0026lt; nums.size() \u0026lt;\u0026lt; endl; // 5 （12）删除元素\n// erase 和 clear只改变size vector\u0026lt;int\u0026gt; nums{1, 2, 3, 3, 4}; nums.reserve(10); nums.erase(nums.begin(), nums.begin() + 1); for(auto i : nums) cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;nums capacity\u0026#34; \u0026lt;\u0026lt; nums.capacity() \u0026lt;\u0026lt; endl; // 10 cout \u0026lt;\u0026lt; \u0026#34;nums size\u0026#34; \u0026lt;\u0026lt; nums.size() \u0026lt;\u0026lt; endl; // 4 nums.clear(); for(auto i : nums) cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;nums capacity\u0026#34; \u0026lt;\u0026lt; nums.capacity() \u0026lt;\u0026lt; endl; // 10 cout \u0026lt;\u0026lt; \u0026#34;nums size\u0026#34; \u0026lt;\u0026lt; nums.size() \u0026lt;\u0026lt; endl; // 0 （13）为什么扩容成倍增长，而不是固定数字？\n如果是以固定数扩容的话，通过数学方法可以算出来平摊下来的push_back时间是O(n)，而以倍数增长的扩容方式push_back的时间复杂度是O(1)\n理想的分配方案是在第N次扩容时如果能复用之前N-1次释放的空间就太好了，2倍扩容必定大于前面的总和，过大会造成内存浪费，1.5倍扩容可以重用之前释放的内存 deque deque 是一种双端开口的序列容器，逻辑上支持随机访问，但底层通常由多段连续缓冲区组成，并不要求整体内存连续。\n（1）底层原理\n采用一块所谓的map作为主控，这里的map实际上是一块大小连续的空间，其中每一个元素我们称之为节点 node，都指向了另一段连续线性空间，成为缓冲区，缓冲区才是deque的真正存储空间主体\n通过建立 map 数组，deque 容器申请的这些分段的连续空间就能实现\u0026quot;整体连续\u0026quot;的效果。换句话说，当 deque 容 器需要在头部或尾部增加存储空间时，它会申请一段新的连续空间，同时在 map 数组的开头或结尾添加指向该空 间的指针，由此该空间就串接到了 deque 容器的头部或尾部。\n（2）map数组满了怎么办\n类似vector扩容\n（3）deque的iter\ndeque 的迭代器通常包含几个关键指针：cur 指向当前元素，first 指向当前缓冲区头部，last 指向当前缓冲区尾部，node 指向主控 map 中的节点。容器本身还会记录指向首尾节点的迭代器（常见实现中称为 start 和 finish）以及主控 map 的大小。\nlist list 是一个双向链表，每次插入或删除一个元素就配置或释放一个空间，而且原有的迭代器也不会失 效\nstack 使用 deque 作为底部结构并封闭其头端开口，就形成了一个 stack\nqueue 若以 deque 为底部结构并封闭其底端的出口，和头端的入口就形成 了一个 queue。(其实 list 也可以实现 deque)\nmap \u0026amp; set （1）为何关联容器的插入删除效率一般比用其他序列容器高？\n底层实现都是红黑树。对于关联容器来说，存储的只是节点。插入删除只是节点指针的换来换去，不需要做内存拷贝和内存移动\n（2）multiset 重复数据是怎么实现的\nset的返回值型别是由pair组织起来的两个值，pair第一个元素返回新元素的位置，或返回现存的同值元素的位置，pair第二个元素表示插入是否成功，如果插入失败，就只返回重复元素的位置\nmultiset中所有拥有位置提示参数的插入函数的返回值型别是相同的。这样就确保了至少有了一个通用型的插入函数，在各种容器中有共通接口\n（3）insert后iter会不会失效\n不会，只是节点指针的改变，每个节点的内存没有改变。iterator就是指向每个节点内存的指针，所以插入之后，该指针是不会变的\n（4）为何map和set不能像vector一样有个reserve函数来预分配数据\nmap和set会存储节点的信息，例如结构体之类的，无法提前分配\n（5）map使用[]越界了发生什么\n会自动插入一个你搜索的key和默认的value。如果value为内置类型，其值将被初始化为0;如果value为 自定义数据结构且用户定义了默认值则初始化为默认值，否则初始化为0\n迭代器和指针的区别 迭代器不一定是裸指针，通常可以是类模板对象。它通过重载 operator*、operator-\u0026gt;、operator++、operator-- 等操作，提供类似指针的访问方式。\n算法 （1）sort和stable_sort\nstable_sort (first, last)和sort() 函数功能相似，不同之处在于该函数不会改变它们的相对位置\nstable_sort() 函数是基于归并排序实现的 sort() 只由标准规定复杂度要求，常见实现多采用内省排序（introsort），不能简单等同于快速排序。 STL 容器是线程安全的吗 不是，\n加锁实现线程安全 固定 vector 大小只能避免扩容导致的迭代器/引用失效，并不等于线程安全，更不等于 lock-free；并发读写元素仍需同步。 vector\u0026lt;int\u0026gt; nums; nums.resize(10); // 不是reserve 优先队列 priority_queue是C++标准库中的一个容器适配器（container adapter），用于实现优先队列（priority queue）的数据结构。优先队列是一种特殊的队列，其中的元素按照一定的优先级进行排序，每次取出的元素都是优先级最高的。它的底层实现通常使用堆（heap）数据结构。\ntemplate\u0026lt; class T, class Container = std::vector\u0026lt;T\u0026gt;, class Compare = std::less\u0026lt;typename Container::value_type\u0026gt; \u0026gt; class priority_queue; Citation Citation: When reposting or citing content from this article, please credit the original author and source.\nCited as:\nZachary WEI. (Oct 2024). C++ 八股知识整理（一）：语法、面向对象、内存与 STL. https://zachary-ww.github.io/zh/posts/cpp-bagu-knowledge/\nOr\n@article{zachary2024-cpp-bagu-knowledge, title = \u0026#34;{C++ 八股知识整理（一）：语法、面向对象、内存与 STL}\u0026#34;, author = \u0026#34;Zachary WEI\u0026#34;, journal = \u0026#34;zachary-ww.github.io\u0026#34;, year = \u0026#34;2024\u0026#34;, month = \u0026#34;Oct\u0026#34;, url = \u0026#34;https://zachary-ww.github.io/zh/posts/cpp-bagu-knowledge/\u0026#34; } ","permalink":"https://zachary-ww.github.io/zh/posts/cpp-bagu-knowledge/","summary":"一份 C++ 八股知识整理，覆盖语法基础、面向对象、C++11 特性、内存管理与 STL 容器等常见面试知识点。","title":"C++ 八股知识整理（一）：语法、面向对象、内存与 STL"}]