<?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>Liangbk&apos;s blog</title><description>Stay hungry, stay foolish</description><link>https://liang-bk.github.io</link><item><title>spring-ai练习</title><link>https://liang-bk.github.io/blog/java-project/ai-rag-mcp-agent</link><guid isPermaLink="true">https://liang-bk.github.io/blog/java-project/ai-rag-mcp-agent</guid><description>介绍java项目中的各种环境配置步骤</description><pubDate>Fri, 06 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Chat流程&lt;/h2&gt;
&lt;p&gt;新版本spring-ai（1.1.2）中，ChatClient底层依托ChatModel发送请求和接收响应&lt;/p&gt;
&lt;p&gt;与chatModel相关的主要包括call()和stream()，前者是一次性返回对话的所有的内容，后者是流式的（类似一个字一个字）返回对话的内容&lt;/p&gt;
&lt;p&gt;其他的调用与上下文信息（prompt）、RAG（advisors）、MCP（tools）、AGENT相关&lt;/p&gt;
&lt;p&gt;总览图：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;只需要记住这张图：
ChatClient.Builder（Spring自动注入，前提是配置里只有一个Client）
    │
    ├── .defaultSystem(...)       → 默认系统提示
    ├── .defaultAdvisors(...)     → 默认拦截器（记忆/RAG）
    ├── .defaultTools(...)        → 默认工具（MCP/Function）
    └── .build()
            │
            ▼
        ChatClient（你拿来用的）
            │
            └── .prompt()
                    ├── .system(...)    → 覆盖默认系统提示
                    ├── .user(...)      → 用户输入
                    ├── .advisors(...)  → 本次请求的拦截器参数
                    ├── .call()         → 普通调用
                    └── .stream()       → 流式调用
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;POM&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;properties&gt;
    &amp;#x3C;maven.compiler.source&gt;17&amp;#x3C;/maven.compiler.source&gt;
    &amp;#x3C;maven.compiler.target&gt;17&amp;#x3C;/maven.compiler.target&gt;
    &amp;#x3C;project.build.sourceEncoding&gt;UTF-8&amp;#x3C;/project.build.sourceEncoding&gt;
    &amp;#x3C;spring-ai.version&gt;1.1.2&amp;#x3C;/spring-ai.version&gt;
&amp;#x3C;/properties&gt;
&amp;#x3C;dependencyManagement&gt;
    &amp;#x3C;dependencies&gt;
        &amp;#x3C;dependency&gt;
            &amp;#x3C;groupId&gt;org.springframework.ai&amp;#x3C;/groupId&gt;
            &amp;#x3C;artifactId&gt;spring-ai-bom&amp;#x3C;/artifactId&gt;
            &amp;#x3C;version&gt;${spring-ai.version}&amp;#x3C;/version&gt;
            &amp;#x3C;type&gt;pom&amp;#x3C;/type&gt;
            &amp;#x3C;scope&gt;import&amp;#x3C;/scope&gt;
        &amp;#x3C;/dependency&gt;
    &amp;#x3C;/dependencies&gt;
&amp;#x3C;/dependencyManagement&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;单模型&lt;/h3&gt;
&lt;p&gt;POM中导入open-ai模型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependencies&gt;
    &amp;#x3C;dependency&gt;
        &amp;#x3C;groupId&gt;org.springframework.boot&amp;#x3C;/groupId&gt;
        &amp;#x3C;artifactId&gt;spring-boot-starter-web&amp;#x3C;/artifactId&gt;
    &amp;#x3C;/dependency&gt;


    &amp;#x3C;!--   使用openai模型  --&gt;
    &amp;#x3C;dependency&gt;
        &amp;#x3C;groupId&gt;org.springframework.ai&amp;#x3C;/groupId&gt;
        &amp;#x3C;artifactId&gt;spring-ai-starter-model-openai&amp;#x3C;/artifactId&gt;
    &amp;#x3C;/dependency&gt;

    &amp;#x3C;dependency&gt;
        &amp;#x3C;groupId&gt;org.springframework.boot&amp;#x3C;/groupId&gt;
        &amp;#x3C;artifactId&gt;spring-boot-starter-test&amp;#x3C;/artifactId&gt;
        &amp;#x3C;scope&gt;test&amp;#x3C;/scope&gt;
    &amp;#x3C;/dependency&gt;
&amp;#x3C;/dependencies&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分别使用ChatModel和ChatClient发送请求（以deepseek api为例）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;配置文件（application.yml）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;server:
  port: 8090

spring:
  ai:
    openai:
      base-url: https://api.deepseek.com
      api-key: sk-xxx
      chat:
        options:
          model: deepseek-chat
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置类&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class AiConfig {
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem(&quot;你是一个专业的技术助手，请用中文回答。&quot;)
                .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@SpringBootTest
public class ChatTest {
    // 第三方Bean自动注入
    @Resource
    private ChatModel chatModel;
    // 配置类注入
    @Resource
    private ChatClient chatClient;
    // ==================== ChatModel 测试 ====================
    /**
     * 测试：ChatModel 使用 Prompt，携带系统提示
     */
    @Test
    public void test_chatModel_withPrompt() {
        Prompt prompt = new Prompt(List.of(
                new SystemMessage(&quot;你是一个专业的 Java 开发助手，回答请简洁专业&quot;),
                new UserMessage(&quot;Spring AI 和 LangChain 有什么区别？&quot;)
        ));

        ChatResponse response = chatModel.call(prompt);

        // 获取回复文本
        String content = response.getResult().getOutput().getText();
        System.out.println(&quot;回复：&quot; + content);

        // 打印 token 用量
        var usage = response.getMetadata().getUsage();
        System.out.println(&quot;输入 tokens：&quot; + usage.getPromptTokens());
        System.out.println(&quot;输出 tokens：&quot; + usage.getCompletionTokens());
    }
    // ==================== ChatClient 测试 ====================

    /**
     * 测试：ChatClient 链式调用流式输出（最常用写法）
     */
    @Test
    public void test_chatClient_stream() throws InterruptedException {
        Flux&amp;#x3C;String&gt; flux = chatClient.prompt()
            	.system(&quot;你是一个幽默的程序员，回答时适当加入编程梗&quot;)
                .user(&quot;解释一下什么是递归&quot;)
                .stream()
                .content();

        // 同步阻塞订阅，测试环境用 blockLast()
        flux.doOnNext(token -&gt; System.out.print(token))
                .blockLast();

        System.out.println(); // 换行
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;解释一下需要了解的类和api&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prompt：提示词，由单条或多条有序的Message组成，基本可以看做是对话内容&lt;/li&gt;
&lt;li&gt;Message：根据类型分为System（系统设置），User（用户消息），Assistant（模型回复消息）&lt;/li&gt;
&lt;li&gt;ChatModel：对话模型客户端，负责给大模型发送消息和接收响应
&lt;ul&gt;
&lt;li&gt;call()&lt;/li&gt;
&lt;li&gt;stream()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ChatClient：封装了ChatModel和prompt以及其他的一些可能用到的工具流程（比如设置，RAG等），使其支持链式调用，而不必单独手动组合在一起
&lt;ul&gt;
&lt;li&gt;prompt()：输入的提示词&lt;/li&gt;
&lt;li&gt;system()：语法糖，就是输入的System Message&lt;/li&gt;
&lt;li&gt;user()：语法糖，就是输入的User Message&lt;/li&gt;
&lt;li&gt;call()&lt;/li&gt;
&lt;li&gt;stream()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多模型&lt;/h3&gt;
&lt;p&gt;当需要使用多个厂商的模型时（或者不同厂商提供的不同组件），在配置上以及第三方Bean的使用上与单模型有差别：&lt;/p&gt;
&lt;p&gt;POM引入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;org.springframework.ai&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;spring-ai-starter-model-ollama&amp;#x3C;/artifactId&gt;
&amp;#x3C;/dependency&gt;
&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;org.springframework.ai&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;spring-ai-starter-model-openai&amp;#x3C;/artifactId&gt;
&amp;#x3C;/dependency&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;配置文件需要配置多个厂商：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  ai:
    ollama:
      base-url: http://127.0.0.1:11434
      embedding:
        options:
          num-batch: 512
        model: nomic-embed-text	# 本地ollama提供的嵌入模型, 见RAG流程
    openai:
      base-url: https://api.deepseek.com
      api-key: sk-xxx
      chat:
        options:
          model: deepseek-chat	# deepseek api提供的对话模型
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三方Bean导入：&lt;/p&gt;
&lt;p&gt;使用各厂商的ChatModel作为参数，然后导入对应的ChatClient：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class AiConfig {
    @Bean(&quot;openAiChatClient&quot;)
    public ChatClient chatClient(OpenAiChatModel model) {
        return ChatClient.builder(model)
                .defaultSystem(&quot;你是 OpenAI 驱动的助手&quot;)
                .build();
    }

    @Bean(&quot;ollamaChatClient&quot;)
    public ChatClient ollamaChatClient(OllamaChatModel model) {
        return ChatClient.builder(model)
                .defaultSystem(&quot;你是本地 Ollama 驱动的助手&quot;)
                .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用chatOption指定各厂商提供的模型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void test_chatClientOption() {
    String content = ollamaChatClient.prompt()
        .options(OllamaChatOptions.builder()
                 .model(&quot;deepseek-r1:1.5b&quot;)	// 指定本地ollama提供的1.5b模型
                 .build())
        .user(&quot;你是什么助手？&quot;)
        .call()
        .content();
    System.out.println(&quot;ollama助手回答:&quot; +  content);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;RAG流程&lt;/h2&gt;
&lt;p&gt;以对话模型为例，当发送的消息比如“帮我写xxx算法的代码”，对话模型内部处理并不是自然语言，而是一堆数据（向量），也就是先有一个模型将各种自然语言转为[0.1,0.2,0.3....]之类的向量，然后对话模型内部才去处理，这个将自然语言转为向量的模型叫嵌入模型（embedding-model）&lt;/p&gt;
&lt;p&gt;RAG（Retrieval-Augmented-Generation）提前将资料（文本文档，图片，音频等）通过各类嵌入模型转为向量，然后存入专门的向量数据库中，在用户向对话模型提问时，将问题也经过嵌入模型转为向量后，去向量数据库中去比对，然后找到相似的信息，最后将数据库中的信息和用户消息同时作为prompt交给对话模型，以此让对话模型能够获取外部资料增强输出&lt;/p&gt;
&lt;p&gt;POM引入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;org.springframework.ai&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;spring-ai-tika-document-reader&amp;#x3C;/artifactId&gt;
&amp;#x3C;/dependency&gt;
&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;org.springframework.ai&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;spring-ai-starter-vector-store-pgvector&amp;#x3C;/artifactId&gt;
&amp;#x3C;/dependency&gt;
&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;org.springframework.ai&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;spring-ai-advisors-vector-store&amp;#x3C;/artifactId&gt;
&amp;#x3C;/dependency&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;介绍上述提到的概念对应到spring-ai中的类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;嵌入模型：EmbeddingModel，使用方式跟ChatModel类似，不过返回的内容是数组&lt;/li&gt;
&lt;li&gt;上传以及检索出来的资料：Document，文件资料可能以各种方式存在（txt，md，pdf），Document负责将其统一&lt;/li&gt;
&lt;li&gt;文本分割器：TokenTextSplitter，一个文件可能过大，需要将内容按指定规则分割多个小块，这些小块以Document的形式存在&lt;/li&gt;
&lt;li&gt;向量数据库：PgVectorStore，负责连接向量数据库并进行操作，其需要指定一个嵌入模型，之后在上传和检索时都会使用该模型进行文本转向量的处理&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;配置&lt;/h3&gt;
&lt;p&gt;配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    username: postgres
    password: postgres
    url: jdbc:postgresql://127.0.0.1:15432/ai-rag-mcp
    type: com.zaxxer.hikari.HikariDataSource
    # hikari连接池配置
    hikari:
      #连接池名
      pool-name: HikariCP
      #最小空闲连接数
      minimum-idle: 5
      # 空闲连接存活最大时间，默认10分钟
      idle-timeout: 600000
      # 连接池最大连接数，默认是10
      maximum-pool-size: 10
      # 此属性控制从池返回的连接的默认自动提交行为,默认值：true
      auto-commit: true
      # 此属性控制池中连接的最长生命周期，值0表示无限生命周期，默认30分钟
      max-lifetime: 1800000
      # 数据库连接超时时间,默认30秒
      connection-timeout: 30000
      # 连接测试query
      connection-test-query: SELECT 1
  ai:
    ollama:
      base-url: http://127.0.0.1:11434
      embedding:
        options:
          num-batch: 512
        model: nomic-embed-text	# ollama的嵌入模型
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate, OllamaEmbeddingModel ollamaEmbeddingModel) {
    return PgVectorStore.builder(jdbcTemplate, ollamaEmbeddingModel)
        .initializeSchema(true)
        .dimensions(768)
        .distanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE)
        .indexType(PgVectorStore.PgIndexType.HNSW)
        .build();
}

@Bean
public TokenTextSplitter  tokenTextSplitter() {
    return new TokenTextSplitter();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;向量数据库：&lt;/p&gt;
&lt;p&gt;使用docker启动一个vector_db，这里启动的是PGVector&lt;/p&gt;
&lt;p&gt;在docker终端创建并连接数据库，创建表：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 连接
psql -h 127.0.0.1 -p 5432 -U postgres

# 创建数据库
\l # 查看数据库列表
create databse ai-rag-mcp;
\c ai-rag-mcp # 使用对应数据库

# 创建表
CREATE EXTENSION IF NOT EXISTS vector; # 安装pgvector扩展
SELECT * FROM pg_extension WHERE extname = &apos;vector&apos;; # 验证是否安装成功（应该能看到一条记录）
\dt vector_store	# 检查表是否存在
DROP TABLE IF EXISTS vector_store;	# 删除表

CREATE TABLE IF NOT EXISTS vector_store (
    id UUID PRIMARY KEY,
    content TEXT,
    metadata JSONB,
    embedding vector(768)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;上传&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;reader读取文档&lt;/li&gt;
&lt;li&gt;tokenTextSplitter分割文档&lt;/li&gt;
&lt;li&gt;给文档打标签（也叫元信息，会一起存储到数据库里，可以根据这个进行检索前的过滤）&lt;/li&gt;
&lt;li&gt;vectorstore.add()将文档上传到向量数据库&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Resource
private PgVectorStore pgVectorStore;
@Resource
private TokenTextSplitter tokenTextSplitter;
@Test
public void test_upload_file_to_db() {
    
    TikaDocumentReader reader = new TikaDocumentReader(&quot;./data/env-build.md&quot;);
    List&amp;#x3C;Document&gt; documents = reader.get();
    List&amp;#x3C;Document&gt; chunks = tokenTextSplitter.apply(documents);
    chunks.forEach(document -&gt; {
        document.getMetadata().put(&quot;description&quot;, &quot;java-env-build&quot;);
    });
    pgVectorStore.add(chunks);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;检索&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在向量数据库中进行问题的相似度查询，得到结果&lt;/li&gt;
&lt;li&gt;将检索结果和问题一起拼装到prompt中，然后通过chatmodel发送&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void test_chat_with_db() {
    String question = &quot;如何进行微信鉴权？&quot;;
    List&amp;#x3C;Document&gt; documents = pgVectorStore.similaritySearch(
        SearchRequest.builder()
        .query(question)
        .topK(5)
        .similarityThreshold(0.5)
        .filterExpression(&quot;description == &apos;java-env-build&apos;&quot;)
        .build()
    );
    log.info(&quot;=======检索到的相关文档=======&quot;);
    documents.forEach(document -&gt; {
        System.out.println(&quot;· &quot; + document.getText());
    });
    String context = documents.stream()
        .map(Document::getText)
        .collect(Collectors.joining(&quot;\n\n&quot;));
    String ragPromptText = &quot;&quot;&quot;
        请根据以下参考资料回答问题，资料中没有的内容请如实说明。

        参考资料：
        %s

        问题：%s
        &quot;&quot;&quot;.formatted(context, question);

        Prompt prompt = new Prompt(List.of(
            new SystemMessage(&quot;你是一个知识库助手，请用中文简洁回答。&quot;),
            new UserMessage(ragPromptText)
        ));

    ChatResponse chatResponse = openAiChatModel.call(prompt);
    // Step5: 提取结果
    String answer = chatResponse.getResult().getOutput().getText();
    log.info(&quot;\n=== 模型回答 ===&quot;);
    log.info(answer);

    // 额外：查看 token 用量
    Usage usage = chatResponse.getMetadata().getUsage();
    log.info(&quot;\n输入tokens: &quot; + usage.getPromptTokens());
    log.info(&quot;输出tokens: &quot; + usage.getCompletionTokens());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用chatclient，则使用Advisors类来封装这个过程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void test_chat_client_rag() {
    String question = &quot;如何进行微信鉴权？&quot;;
    String content = openAiChatClient.prompt()
        .user(question)
        .advisors(QuestionAnswerAdvisor.builder(pgVectorStore)
                  .searchRequest(
                      SearchRequest.builder()
                      .topK(5)
                      .similarityThreshold(0.5)
                      .filterExpression(&quot;description == &apos;java-env-build&apos;&quot;)
                      .build()
                  )
                  .build())
        .call()
        .content();
    log.info(&quot;advisors rag content: {}&quot;, content);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;MCP流程&lt;/h2&gt;
&lt;p&gt;模型上下文协议（MCP）是一种使AI模型能够以结构化方式与外部工具和资源交互的协议（这个外部工具可能是本地进程，可能是服务器或其他遵循协议的程序）。&lt;/p&gt;
&lt;p&gt;说人话就是，有一个服务，对外暴露接口或api，把这些服务提供的api信息交给对话模型，模型的输出（json格式字符串）会自动调用工具（在json中表明要调用），然后客户端会调用对应的服务api，收到调用的结果后再发给对话模型去分析&lt;/p&gt;
&lt;h3&gt;基本流程&lt;/h3&gt;
&lt;p&gt;spring-ai将流程封装到了ChatClient的调用内部（也就是call()和stream()具体实现做的事），只提供几个类给开发人员调用，不易于理解流程，这里使用deepseek官网中的python进行阐述：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from openai import OpenAI

def send_messages(messages):
    response = client.chat.completions.create(
        model=&quot;deepseek-chat&quot;,
        messages=messages,
        tools=tools
    )
    return response.choices[0].message

client = OpenAI(
    api_key=&quot;&amp;#x3C;your api key&gt;&quot;,
    base_url=&quot;https://api.deepseek.com&quot;,
)
# tools数组，和对话消息一起发送给对话模型
tools = [
    {
        &quot;type&quot;: &quot;function&quot;,
        &quot;function&quot;: {
            &quot;name&quot;: &quot;get_weather&quot;,
            &quot;description&quot;: &quot;Get weather of a location, the user should supply a location first.&quot;,
            &quot;parameters&quot;: {
                &quot;type&quot;: &quot;object&quot;,
                &quot;properties&quot;: {
                    &quot;location&quot;: {
                        &quot;type&quot;: &quot;string&quot;,
                        &quot;description&quot;: &quot;The city and state, e.g. San Francisco, CA&quot;,
                    }
                },
                &quot;required&quot;: [&quot;location&quot;]
            },
        }
    },
]

messages = [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;How&apos;s the weather in Hangzhou, Zhejiang?&quot;}]
message = send_messages(messages)
print(f&quot;User&gt;\t {messages[0][&apos;content&apos;]}&quot;)

tool = message.tool_calls[0]
messages.append(message)

messages.append({&quot;role&quot;: &quot;tool&quot;, &quot;tool_call_id&quot;: tool.id, &quot;content&quot;: &quot;24℃&quot;})
message = send_messages(messages)
print(f&quot;Model&gt;\t {message.content}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;准备好tools（待调用的api说明），这里的python代码直接给出，平常需要由对话客户端主动向MCP服务器进行请求获取tools信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将tools和问题一起发送给对话模型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看返回结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;===response===
{
  &quot;id&quot;: &quot;2beffab1-837e-4031-b137-d1d49b6f7800&quot;,
  &quot;choices&quot;: [
    {   
      &quot;finish_reason&quot;: &quot;tool_calls&quot;,
      &quot;index&quot;: 0,
      &quot;logprobs&quot;: null,
      &quot;message&quot;: {
        &quot;content&quot;: &quot;I&apos;ll check the weather in Hangzhou, Zhejiang for you.&quot;,
        &quot;refusal&quot;: null,
        &quot;role&quot;: &quot;assistant&quot;,
        &quot;annotations&quot;: null,
        &quot;audio&quot;: null,
        &quot;function_call&quot;: null,
        &quot;tool_calls&quot;: [
          {
            &quot;id&quot;: &quot;call_00_oUNn7rFGaMXqENDHP3vC2ZRC&quot;,
            &quot;function&quot;: {
              &quot;arguments&quot;: &quot;{\&quot;location\&quot;: \&quot;Hangzhou, Zhejiang\&quot;}&quot;,
              &quot;name&quot;: &quot;get_weather&quot;
            },
            &quot;type&quot;: &quot;function&quot;,
            &quot;index&quot;: 0
          }
        ]
      }
    }
  ],
  &quot;created&quot;: 1772699828,
  &quot;model&quot;: &quot;deepseek-chat&quot;,
  &quot;object&quot;: &quot;chat.completion&quot;,
  &quot;service_tier&quot;: null,
  &quot;system_fingerprint&quot;: &quot;fp_eaab8d114b_prod0820_fp8_kvcache&quot;,
  &quot;usage&quot;: {
    &quot;completion_tokens&quot;: 62,
    &quot;prompt_tokens&quot;: 333,
    &quot;total_tokens&quot;: 395,
    &quot;completion_tokens_details&quot;: null,
    &quot;prompt_tokens_details&quot;: {
      &quot;audio_tokens&quot;: null,
      &quot;cached_tokens&quot;: 320
    },
    &quot;prompt_cache_hit_tokens&quot;: 320,
    &quot;prompt_cache_miss_tokens&quot;: 13
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要查看&quot;finish_reason&quot;，这里表明是&quot;tool_calls&quot;，也就是模型在分析之后决定要进行api调用了，然后是里面的&quot;tool_calls&quot;数组，里面表明了要调用的api信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由客户端（主程序）去调用这个api，将得到的信息拼接，并重新发给对话模型，得到最终结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;
User&gt;	 How&apos;s the weather in Hangzhou, Zhejiang?
===response===
{
  &quot;id&quot;: &quot;12f0a8df-cb1b-4391-917f-1b190914ab6f&quot;,
  &quot;choices&quot;: [
    {
      &quot;finish_reason&quot;: &quot;stop&quot;,
      &quot;index&quot;: 0,
      &quot;logprobs&quot;: null,
      &quot;message&quot;: {
        &quot;content&quot;: &quot;The current weather in Hangzhou, Zhejiang is 24°C (about 75°F). It&apos;s a pleasant, mild temperature - perfect for outdoor activities!&quot;,
        &quot;refusal&quot;: null,
        &quot;role&quot;: &quot;assistant&quot;,
        &quot;annotations&quot;: null,
        &quot;audio&quot;: null,
        &quot;function_call&quot;: null,
        &quot;tool_calls&quot;: null
      }
    }
  ],
  &quot;created&quot;: 1772699833,
  &quot;model&quot;: &quot;deepseek-chat&quot;,
  &quot;object&quot;: &quot;chat.completion&quot;,
  &quot;service_tier&quot;: null,
  &quot;system_fingerprint&quot;: &quot;fp_eaab8d114b_prod0820_fp8_kvcache&quot;,
  &quot;usage&quot;: {
    &quot;completion_tokens&quot;: 34,
    &quot;prompt_tokens&quot;: 414,
    &quot;total_tokens&quot;: 448,
    &quot;completion_tokens_details&quot;: null,
    &quot;prompt_tokens_details&quot;: {
      &quot;audio_tokens&quot;: null,
      &quot;cached_tokens&quot;: 384
    },
    &quot;prompt_cache_hit_tokens&quot;: 384,
    &quot;prompt_cache_miss_tokens&quot;: 30
  }
}
Model&gt;	 The current weather in Hangzhou, Zhejiang is 24°C (about 75°F). It&apos;s a pleasant, mild temperature - perfect for outdoor activities!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到模型返回的结果，&quot;finish_reason&quot;已经变为了&quot;stop&quot;，代表模型回复完了此次对话&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;spring-ai封装&lt;/h3&gt;
&lt;p&gt;POM引入：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;org.springframework.ai&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;spring-ai-starter-mcp-client&amp;#x3C;/artifactId&gt;
&amp;#x3C;/dependency&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务端：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;org.springframework.ai&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;spring-ai-starter-mcp-server&amp;#x3C;/artifactId&gt;
&amp;#x3C;/dependency&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;客户端MCP&lt;/h4&gt;
&lt;p&gt;配置文件：新增mcp项，可以使用外部文件的方式引入mcp server，也可以直接写在yaml中，这里直接写在yaml中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
    mcp:
      client:
        stdio:
          # servers-configuration: classpath:/config/mcp-servers-config.json
            connections:
              test-server:
                command: npx.cmd
                args:
                  - &quot;-y&quot;
                  - &quot;@modelcontextprotocol/server-filesystem&quot;
                  - &quot;C:\\Users\\86183\\Desktop\\mcp-test&quot;           # 授权访问的目录
        enabled: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面提到，在spring-ai中，ChatModel的调用已经封装了工具的调用，因此开发者只需要提供工具，这涉及到以下类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;McpSyncClient：与MCP Server对接的客户端对象，负责获取工具信息，调用工具等（理解有这个东西就好，一般工具在载入后会直接到SyncMcpToolCallbackProvider里）&lt;/li&gt;
&lt;li&gt;SyncMcpToolCallbackProvider：字面意思，调用工具提供方，也就是解析响应，通过mcpclient拿到工具列表，并连接client和每一个toolcallback&lt;/li&gt;
&lt;li&gt;ToolCallback[]：基本上可以理解是python流程中的tools数组，不过数组中的每一个tool都可以请求对应的mcp server api（通过McpSyncClient）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Resource(name = &quot;openAiChatClient&quot;)
private ChatClient openAiChatClient;

@Resource(name = &quot;openAiChatModel&quot;)
private ChatModel openAiChatModel;

@Resource
private List&amp;#x3C;McpSyncClient&gt; mcpSyncClients;

private ToolCallback[] getMcpTools() {
    return mcpSyncClients.stream()
        .map(SyncMcpToolCallbackProvider::new)
        .flatMap(p -&gt; Arrays.stream(p.getToolCallbacks()))
        .toArray(ToolCallback[]::new);
}
@Test
public void test_dynamic_mcp_tools() {
    String answer = openAiChatClient.prompt()
        .user(&quot;列出允许访问的目录并读取Desktop\\mcp-test\\mcp.txt 的内容&quot;)
        .toolCallbacks(List.of(getMcpTools()))      // ← 本次请求附加工具
        .call()
        .content();

    System.out.println(&quot;回答：&quot; + answer);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;服务端MCP&lt;/h4&gt;
&lt;p&gt;配置文件：在日志配置文件中不能让日志文件作为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  application:
    name: mcp-server-computer

  ai:
    mcp:
      server:
        name: ${spring.application.name}
        version: 1.0.0
        
  main:
    banner-mode: off
    web-application-type: none

  server:
    servlet:
      encoding:
        charset: UTF-8
        force: true
        enabled: true

logging:
  config: classpath:logback-spring.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在spring能够扫描的组件里的方法上加上注解@Tool(description = &quot;xxx&quot;)（代表是一个可调用的工具）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class ComputerService {

    @Tool(description = &quot;获取电脑配置&quot;)
    public ComputerFunctionResponse queryConfig(ComputerFunctionRequest request) {
        log.info(&quot;获取电脑配置信息 {}&quot;, request.getComputer());
        // 获取系统属性
        Properties properties = System.getProperties();

        // 操作系统名称
        String osName = properties.getProperty(&quot;os.name&quot;);
        // 操作系统版本
        String osVersion = properties.getProperty(&quot;os.version&quot;);
        // 操作系统架构
        String osArch = properties.getProperty(&quot;os.arch&quot;);
        // 用户的账户名称
        String userName = properties.getProperty(&quot;user.name&quot;);
        // 用户的主目录
        String userHome = properties.getProperty(&quot;user.home&quot;);
        // 用户的当前工作目录
        String userDir = properties.getProperty(&quot;user.dir&quot;);
        // Java 运行时环境版本
        String javaVersion = properties.getProperty(&quot;java.version&quot;);

        String osInfo = &quot;&quot;;
        // 根据操作系统执行特定的命令来获取更多信息
        if (osName.toLowerCase().contains(&quot;win&quot;)) {
            // Windows特定的代码
            osInfo = getWindowsSpecificInfo();
        } else if (osName.toLowerCase().contains(&quot;mac&quot;)) {
            // macOS特定的代码
            osInfo = getMacSpecificInfo();
        } else if (osName.toLowerCase().contains(&quot;nix&quot;) || osName.toLowerCase().contains(&quot;nux&quot;)) {
            // Linux特定的代码
            osInfo = getLinuxSpecificInfo();
        }

        ComputerFunctionResponse response = new ComputerFunctionResponse();
        response.setOsName(osName);
        response.setOsVersion(osVersion);
        response.setOsArch(osArch);
        response.setUserName(userName);
        response.setUserHome(userHome);
        response.setUserDir(userDir);
        response.setJavaVersion(javaVersion);
        response.setOsInfo(osInfo);

        return response;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注册（向外部提供这些工具的基本信息），在配置类中声明Bean&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public ToolCallbackProvider computerTools(ComputerService computerService) {
    return MethodToolCallbackProvider.builder()
        .toolObjects(computerService)
        .build();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Agent&lt;/h2&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>java项目环境配置</title><link>https://liang-bk.github.io/blog/java-project/env-build</link><guid isPermaLink="true">https://liang-bk.github.io/blog/java-project/env-build</guid><description>介绍java项目中的各种环境配置步骤</description><pubDate>Thu, 15 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;基本环境&lt;/h2&gt;
&lt;p&gt;我用的是windows + WSL2(ubuntu22) + docker&lt;/p&gt;
&lt;p&gt;windows负责写代码&lt;/p&gt;
&lt;p&gt;WSL提供Linux环境（类似服务器）&lt;/p&gt;
&lt;p&gt;Docker提供MYSQL，Redis镜像，将这些组件容器化，方便启动，停止或者删除&lt;/p&gt;
&lt;p&gt;如果是云服务器，操作基本类似&lt;/p&gt;
&lt;h3&gt;ssh客户端&lt;/h3&gt;
&lt;p&gt;ssh客户端直接用vscode，轻量级，自带文件传输（直接拖或者复制粘贴），自带图形界面好管理&lt;/p&gt;
&lt;h2&gt;WSL2&lt;/h2&gt;
&lt;p&gt;jdk和maven都要直接安装到wsl2环境上&lt;/p&gt;
&lt;h3&gt;JDK&lt;/h3&gt;
&lt;p&gt;java直接安装jdk17的包：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get install openjdk-17-jdk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想搜索有什么版本的jdk能安装，可以用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-cache search java sdk
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;maven&lt;/h3&gt;
&lt;p&gt;ubuntu22默认安装maven3.6，一般需要maven3.8版本往上&lt;/p&gt;
&lt;p&gt;在github仓库中有对应的环境安装包：&lt;a href=&quot;https://github.com/fuzhengwei/xfg-dev-tech-docker-install&quot;&gt;env&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;clone下来，在&lt;code&gt;environment/maven&lt;/code&gt;目录下，有3.8版本安装包，目前里面的安装脚本有点问题（默认使用apt下载），不要使用&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;environment/maven&lt;/code&gt;目录打开终端：&lt;/p&gt;
&lt;p&gt;新建一个安装脚本，安装的maven会被放在&lt;code&gt;/opt&lt;/code&gt;目录下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/usr/bin/env bash
set -e

# ===== 配置区 =====
MAVEN_VERSION=&quot;3.8.8&quot;
MAVEN_ARCHIVE=&quot;apache-maven-${MAVEN_VERSION}.zip&quot;
INSTALL_BASE=&quot;/opt&quot;
INSTALL_DIR=&quot;${INSTALL_BASE}/apache-maven-${MAVEN_VERSION}&quot;
PROFILE_FILE=&quot;$HOME/.bashrc&quot;

# ===== 工具函数 =====
info()  { echo -e &quot;\033[0;34m[INFO]\033[0m $1&quot;; }
ok()    { echo -e &quot;\033[0;32m[OK]\033[0m   $1&quot;; }
warn()  { echo -e &quot;\033[0;33m[WARN]\033[0m $1&quot;; }
err()   { echo -e &quot;\033[0;31m[ERR]\033[0m  $1&quot;; exit 1; }

# ===== 前置检查 =====
info &quot;检查 Java 环境&quot;
command -v java &gt;/dev/null 2&gt;&amp;#x26;1 || err &quot;未检测到 Java，请先安装 JDK&quot;

info &quot;检查 Maven 安装包&quot;
[[ -f &quot;$MAVEN_ARCHIVE&quot; ]] || err &quot;未找到 $MAVEN_ARCHIVE&quot;

# ===== 解压并安装 =====
info &quot;安装 Maven 到 $INSTALL_DIR&quot;
sudo mkdir -p &quot;$INSTALL_BASE&quot;

if [[ -d &quot;$INSTALL_DIR&quot; ]]; then
    warn &quot;已存在 $INSTALL_DIR，跳过解压&quot;
else
    sudo unzip -q &quot;$MAVEN_ARCHIVE&quot; -d &quot;$INSTALL_BASE&quot;
    ok &quot;解压完成&quot;
fi

[[ -x &quot;$INSTALL_DIR/bin/mvn&quot; ]] || err &quot;mvn 可执行文件不存在&quot;

# ===== 配置环境变量（用户级）=====
info &quot;配置环境变量到 $PROFILE_FILE&quot;

# 删除旧配置（仅 Maven）
sed -i &apos;/# &gt;&gt;&gt; maven &gt;&gt;&gt;/,/# &amp;#x3C;&amp;#x3C;&amp;#x3C; maven &amp;#x3C;&amp;#x3C;&amp;#x3C;/d&apos; &quot;$PROFILE_FILE&quot;

cat &gt;&gt; &quot;$PROFILE_FILE&quot; &amp;#x3C;&amp;#x3C;EOF

# &gt;&gt;&gt; maven &gt;&gt;&gt;
export MAVEN_HOME=$INSTALL_DIR
export PATH=\$MAVEN_HOME/bin:\$PATH
# &amp;#x3C;&amp;#x3C;&amp;#x3C; maven &amp;#x3C;&amp;#x3C;&amp;#x3C;
EOF

# 立即生效
export MAVEN_HOME=&quot;$INSTALL_DIR&quot;
export PATH=&quot;$MAVEN_HOME/bin:$PATH&quot;

# ===== 验证 =====
info &quot;验证 Maven&quot;
mvn -v | head -n 1

ok &quot;Maven ${MAVEN_VERSION} 安装完成&quot;
info &quot;如新终端中 mvn 不生效，请执行: source ~/.bashrc&quot;

&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-bash&quot;&gt;#!/usr/bin/env bash
set -e

# ===== 配置区 =====
MAVEN_VERSION=&quot;3.8.8&quot;
INSTALL_DIR=&quot;/opt/apache-maven-${MAVEN_VERSION}&quot;
PROFILE_FILE=&quot;$HOME/.bashrc&quot;

info() { echo -e &quot;\033[0;34m[INFO]\033[0m $1&quot;; }
ok()   { echo -e &quot;\033[0;32m[OK]\033[0m   $1&quot;; }
warn() { echo -e &quot;\033[0;33m[WARN]\033[0m $1&quot;; }

# ===== 删除 Maven =====
if [[ -d &quot;$INSTALL_DIR&quot; ]]; then
    info &quot;删除 $INSTALL_DIR&quot;
    sudo rm -rf &quot;$INSTALL_DIR&quot;
    ok &quot;Maven 文件已删除&quot;
else
    warn &quot;$INSTALL_DIR 不存在，跳过&quot;
fi

# ===== 清理环境变量 =====
info &quot;清理 ~/.bashrc 中的 Maven 配置&quot;
sed -i &apos;/# &gt;&gt;&gt; maven &gt;&gt;&gt;/,/# &amp;#x3C;&amp;#x3C;&amp;#x3C; maven &amp;#x3C;&amp;#x3C;&amp;#x3C;/d&apos; &quot;$PROFILE_FILE&quot;

ok &quot;环境变量已清理&quot;
info &quot;请执行: source ~/.bashrc 或重新打开终端&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;docker&lt;/h2&gt;
&lt;p&gt;docker上需要做的事：拉取mysql，redis，mq等镜像，然后直接启动，还有就是部署一整个项目&lt;/p&gt;
&lt;p&gt;windows上安装docker直接参考&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/wsl/tutorials/wsl-containers&quot;&gt;WSL 上的 Docker 容器入门 | Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;按教程一路点下去（先配置WSL2再安装docker，这样docker会自动在wsl2配好对应docker环境）&lt;/p&gt;
&lt;p&gt;之后要使用docker的时候先在windows上启动docker desktop，然后在wsl2中操作&lt;/p&gt;
&lt;h3&gt;mysql&lt;/h3&gt;
&lt;p&gt;拉取mysql镜像&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker pull registry.cn-hangzhou.aliyuncs.com/xfg-studio/mysql:8.0.32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在clone下来的&lt;a href=&quot;https://github.com/fuzhengwei/xfg-dev-tech-docker-install&quot;&gt;env&lt;/a&gt;环境中，找到&lt;code&gt;run_install_software.sh&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;添加可执行权限后运行&lt;/p&gt;
&lt;p&gt;选择安装mysql&lt;/p&gt;
&lt;p&gt;默认用户：root&lt;/p&gt;
&lt;p&gt;默认密码：123456&lt;/p&gt;
&lt;p&gt;默认端口：wsl2的13306&lt;/p&gt;
&lt;h3&gt;redis&lt;/h3&gt;
&lt;p&gt;拉取redis镜像&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker pull registry.cn-hangzhou.aliyuncs.com/xfg-studio/redis:6.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在clone下来的&lt;a href=&quot;https://github.com/fuzhengwei/xfg-dev-tech-docker-install&quot;&gt;env&lt;/a&gt;环境中，找到&lt;code&gt;run_install_software.sh&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;添加可执行权限后运行&lt;/p&gt;
&lt;p&gt;选择安装redis&lt;/p&gt;
&lt;p&gt;无用户名无密码&lt;/p&gt;
&lt;p&gt;默认端口：wsl2的16379&lt;/p&gt;
&lt;p&gt;可以使用docker tag给镜像起其他名称&lt;/p&gt;
&lt;h3&gt;项目部署&lt;/h3&gt;
&lt;h2&gt;内网穿透&lt;/h2&gt;
&lt;p&gt;下载地址：&lt;a href=&quot;https://natapp.cn/&quot;&gt;NATAPP-内网穿透&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;使用方式也很简单，下载exe客户端放到一个固定位置，配置&lt;code&gt;config.ini&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# https://natapp.cn/ - 支持免费使用，但付费会更稳定一些
# 将本文件放置于natapp同级目录 程序将读取 [default] 段
# 在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置
# 命令行参数 -config= 可以指定任意config.ini文件
[default]
authtoken=      #对应一条隧道的authtoken，你需要更换为你的。否则不能正常启动。
clienttoken=                    #对应客户端的clienttoken,将会忽略authtoken,若无请留空,
log=none                        #log 日志文件,可指定本地文件, none=不做记录,stdout=直接屏幕输出 ,默认为none
loglevel=ERROR                  #日志等级 DEBUG, INFO, WARNING, ERROR 默认为 DEBUG
http_proxy=                     #代理设置 如 http://10.123.10.10:3128 非代理上网用户请务必留空
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;微信鉴权&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;配置NATAPP&lt;/strong&gt;：在NATAPP中购买一个VIP隧道，顺道买一个&lt;strong&gt;二级域名&lt;/strong&gt;，配置本地web端口为本地web服务的端口&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;微信测试平台&lt;/strong&gt;：&lt;a href=&quot;https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&amp;#x26;t=sandbox/index&quot;&gt;测试平台&lt;/a&gt;&lt;/p&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;code&gt;URL&lt;/code&gt;配置为本地web服务中处理微信鉴权的controller地址：组合以下两个&lt;/p&gt;
&lt;p&gt;二级域名：&lt;code&gt;http://baishui.nat100.top/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;本地接口：&lt;code&gt;api/v1/weixin/portal/receive&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接口配置信息的&lt;code&gt;Token&lt;/code&gt;随便设置就行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JS接口安全域名就填上面二级域名&lt;/p&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;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Fuzzing101-xpdf</title><link>https://liang-bk.github.io/blog/fuzzing/fuzzing101</link><guid isPermaLink="true">https://liang-bk.github.io/blog/fuzzing/fuzzing101</guid><description>使用AFL++进行的第一个实际模糊测试</description><pubDate>Tue, 13 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;环境准备&lt;/h2&gt;
&lt;p&gt;这里的环境还是使用第一次构建的docker环境&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在物理机上创建目录下载文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/github
mkdir fuzzing_xpdf &amp;#x26;&amp;#x26; cd fuzzing_xpdf
# 下载xpdf3.02版本并解压
wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz
tar -xvzf xpdf-3.02.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建docker环境，&lt;code&gt;--name&lt;/code&gt;参数会给该容器命名为&quot;fuzzing101&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run --name &quot;fuzzing101&quot; -it \
  -v &quot;$(pwd)&quot;:/target \
  aflpp-built
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这表示将会使用&lt;code&gt;aflpp-built&lt;/code&gt;镜像来创建一个容器，同时将本地的&lt;code&gt;~/github/fuzzing_xpdf&lt;/code&gt;目录挂载在容器内的&lt;code&gt;/target&lt;/code&gt;目录下&lt;/p&gt;
&lt;p&gt;由于没有了&lt;code&gt;--rm&lt;/code&gt;参数，所以该容器在关闭后不会自行删除，如果需要重新启动，需要使用&lt;code&gt;docker start fuzzing101&lt;/code&gt;命令，同时，无法更改挂载的本地目录，如果需要更改，最好删除容器并重新构建一个&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;aflpp-built&lt;/code&gt;镜像里预先编译过了&lt;code&gt;AFLPLUSPLUS&lt;/code&gt;项目，直接使用对应的编译器来编译&lt;code&gt;xpdf3.02&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;有几个需要注意的点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AFL++一直在更新，&lt;code&gt;LLVM_CONFIG&lt;/code&gt;需要使用对应的版本，我这里是19&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果之前AFL++项目安装过，可以不必指定afl编译器的完整路径，只指定编译器名&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export LLVM_CONFIG=&quot;llvm-config-19&quot;
CC=afl-clang-fast CXX=afl-clang-fast++ ./configure --prefix=&quot;./fuzzing_xpdf/install/&quot;
make
make install
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;准备测试文件：&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;fuzzing_xpdf&lt;/code&gt;目录下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd $HOME/fuzzing_xpdf
mkdir pdf_examples &amp;#x26;&amp;#x26; cd pdf_examples
wget https://github.com/mozilla/pdf.js-sample-files/raw/master/helloworld.pdf
# 剩下两个网址已失效, 直接github找一个仓库拉取即可
# wget http://www.africau.edu/images/default/sample.pdf
# wget https://www.melbpc.org.au/wp-content/uploads/2017/10/small-example-pdf-file.pdf
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;测试启动&lt;/h2&gt;
&lt;p&gt;环境准备完毕后，使用下面的命令运行模糊测试器：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;afl-fuzz -i ./fuzzing_xpdf/pdf_examples/ -o ./fuzzing_xpdf/out/ -s 123 -- ./fuzzing_xpdf/install/bin/pdftotext @@ ./fuzzing_xpdf/output
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;-i&lt;/em&gt; 指示我们需要放置输入案例（即文件示例）的目录，这里就是&lt;code&gt;fuzzing_xpdf/pdf_examples&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;-o&lt;/em&gt; 就是AFL++记录运行结果的目录，包括编译后的文件，统计路径信息等&lt;/li&gt;
&lt;li&gt;&lt;em&gt;-s&lt;/em&gt; 表示要使用的静态随机种子，这使得模糊测试的过程是可复现的。如果你使用相同的种子、相同的输入和相同的目标程序再次运行 AFL，它将以完全相同的方式生成和测试输入。这对于复现和调试特定的崩溃很有用&lt;/li&gt;
&lt;li&gt;&lt;em&gt;@@&lt;/em&gt; 是占位符目标命令行，AFL 会将其替换为每个输入文件名，&lt;code&gt;xpdf3.02&lt;/code&gt;需要接受一个命令行参数（也就是pdf文件路径），AFL++在每次执行&lt;code&gt;xpdf&lt;/code&gt;的时候会把@@换成对应的文件路径&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;测试结果&lt;/h2&gt;
&lt;h3&gt;空指针错误&lt;/h3&gt;
&lt;p&gt;先用AFL++测了一个小时，前3个漏洞大致都相同，只能单独使程序出错，但不会耗尽资源&lt;/p&gt;
&lt;p&gt;漏洞出错的主要原因是流读取时出错，break之后没有做错误检查，导致的空指针异常&lt;/p&gt;
&lt;p&gt;这次使用镜像内自带的&lt;code&gt;lldb-19&lt;/code&gt;来调试程序：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# --参数就相当于gdb的--args参数
lldb-19 -- ./install/bin/pdftotext
# 禁用地址随机化以调试程序
(lldb) settings set target.disable-aslr false
# 运行程序
(lldb) run out/default/crashes/id:000000,sig:11,src:001033,time:276919,execs:242688,op:havoc,rep:3
Process 93421 launched: &apos;/target/install/bin/pdftotext&apos; (x86_64)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看栈帧：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;(lldb) bt
* thread #1, name = &apos;pdftotext&apos;, stop reason = signal SIGSEGV: address not mapped to object (fault address: 0x0)
  * frame #0: 0x000055814c6dfcdc pdftotext`EmbedStream::getChar(this=0x0000558177919510) at Stream.cc:787:22
    frame #1: 0x000055814c6915a4 pdftotext`Gfx::opBeginImage(this=0x0000558177917600, args=0x00007ffc97979b30, numArgs=0) at Gfx.cc:3880:44
    frame #2: 0x000055814c67f639 pdftotext`Gfx::execOp(this=0x0000558177917600, cmd=0x00007ffc97979b20, args=0x00007ffc97979b30, numArgs=0) at Gfx.cc:690:20
    frame #3: 0x000055814c67f008 pdftotext`Gfx::go(this=0x0000558177917600, topLevel=1) at Gfx.cc:581:13
    frame #4: 0x000055814c67ede1 pdftotext`Gfx::display(this=0x0000558177917600, obj=0x00007ffc97979e80, topLevel=1) at Gfx.cc:553:5
    frame #5: 0x000055814c6d8c10 pdftotext`Page::displaySlice(this=0x00005581779156e0, out=0x0000558177914e80, hDPI=72, vDPI=72, rotate=0, useMediaBox=0, crop=0, sliceX=-1, sliceY=-1, sliceW=-1, sliceH=-1, printing=0, catalog=0x00005581779144b0, abortCheckCbk=0x0000000000000000, abortCheckCbkData=0x0000000000000000) at Page.cc:317:17
    frame #6: 0x000055814c6d87e6 pdftotext`Page::display(this=0x00005581779156e0, out=0x0000558177914e80, hDPI=72, vDPI=72, rotate=0, useMediaBox=0, crop=1, printing=0, catalog=0x00005581779144b0, abortCheckCbk=0x0000000000000000, abortCheckCbkData=0x0000000000000000) at Page.cc:264:15
    frame #7: 0x000055814c6db49b pdftotext`PDFDoc::displayPage(this=0x0000558177914fe0, out=0x0000558177914e80, page=1, hDPI=72, vDPI=72, rotate=0, useMediaBox=0, crop=1, printing=0, abortCheckCbk=0x0000000000000000, abortCheckCbkData=0x0000000000000000) at PDFDoc.cc:317:34
    frame #8: 0x000055814c6db519 pdftotext`PDFDoc::displayPages(this=0x0000558177914fe0, out=0x0000558177914e80, firstPage=1, lastPage=1, hDPI=72, vDPI=72, rotate=0, useMediaBox=0, crop=1, printing=0, abortCheckCbk=0x0000000000000000, abortCheckCbkData=0x0000000000000000) at PDFDoc.cc:330:16
    frame #9: 0x000055814c6ff0f9 pdftotext`main(argc=2, argv=0x00007ffc9797a218) at pdftotext.cc:237:22
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在最后一个栈帧发现&lt;code&gt;str&lt;/code&gt;是一个&lt;code&gt;null&lt;/code&gt;，对应的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int EmbedStream::getChar() {
  if (limited &amp;#x26;&amp;#x26; !length) {
    return EOF;
  }
  --length;
  // str为null, 这里进行的空指针调用
  return str-&gt;getChar();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;向上找栈帧，找到了&lt;code&gt;str&lt;/code&gt;被赋值的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;void Gfx::opBeginImage(Object args[], int numArgs) {
  Stream *str;
  int c1, c2;

  // build dict/stream
  // str变量在这里被赋值
  str = buildImageStream();
  ...
  // 崩溃点
  c1 = str-&gt;getUndecodedStream()-&gt;getChar();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继续找这个&lt;code&gt;buildImageStream&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;Stream *Gfx::buildImageStream() {
  Object dict;
  Object obj;
  char *key;
  Stream *str;

  // build dictionary
  dict.initDict(xref);
  parser-&gt;getObj(&amp;#x26;obj);
  while (!obj.isCmd(&quot;ID&quot;) &amp;#x26;&amp;#x26; !obj.isEOF()) {
    if (!obj.isName()) {
      error(getPos(), &quot;Inline image dictionary key must be a name object&quot;);
      obj.free();
    } else {
      key = copyString(obj.getName());
      obj.free();
      parser-&gt;getObj(&amp;#x26;obj);
      if (obj.isEOF() || obj.isError()) {
	gfree(key);
	break;
      }
      dict.dictAdd(key, &amp;#x26;obj);
    }
    parser-&gt;getObj(&amp;#x26;obj);
  }
  if (obj.isEOF()) {
    error(getPos(), &quot;End of file in inline image&quot;);
    obj.free();
    dict.free();
    return NULL;
  }
  obj.free();

  // make stream
  str = new EmbedStream(parser-&gt;getStream(), &amp;#x26;dict, gFalse, 0);
  str = str-&gt;addFilters(&amp;#x26;dict);

  return str;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到&lt;code&gt;str&lt;/code&gt;是一个&lt;code&gt;EmbedStream&lt;/code&gt;类型的对象，才搞清楚这里new出来的&lt;code&gt;str&lt;/code&gt;（也就是&lt;code&gt;EmbedStream&lt;/code&gt;类型有一个成员也叫&lt;code&gt;str&lt;/code&gt;），所以实际上是&lt;code&gt;str-&gt;str&lt;/code&gt;为一个空指针&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打上断点，调试发现，问题出在&lt;code&gt;buildImageStream&lt;/code&gt;里的&lt;code&gt;while&lt;/code&gt;循环，在里面&lt;code&gt;break&lt;/code&gt;的时候是由于&lt;code&gt;obj.isError()&lt;/code&gt;，但是&lt;code&gt;break&lt;/code&gt;之后没有检查这个条件，导致构建对象时由于出错错误的设置了成员变量的初始值&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;修复：&lt;code&gt;while&lt;/code&gt;循环之后额外检查&lt;code&gt;obj.isError&lt;/code&gt;，如果为真就&lt;code&gt;return NULL&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;栈溢出错误&lt;/h3&gt;
&lt;p&gt;第4个漏洞是重量级漏洞：栈溢出漏洞&lt;/p&gt;
&lt;p&gt;使用lldb-19启动调试，输入崩溃文件，stack很明显的溢出&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;由于栈帧没法从&lt;code&gt;main&lt;/code&gt;开始看，只能使用&lt;code&gt;bt&lt;/code&gt;命令在终端打印所有栈帧&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./fuzzing101_xpdf_stackframe.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;从栈帧看出来，在&lt;code&gt;XRef::getCatalog&lt;/code&gt;中调用&lt;code&gt;XRef::fetch&lt;/code&gt;之后，启动了一个递归调用过程，导致了栈溢出&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从&lt;code&gt;PDFDoc::setup&lt;/code&gt;看出，传了一个文档拥有者的密码和用户密码，下一个调用是&lt;code&gt;catalog = new Catalog(xref)&lt;/code&gt;，传入的&lt;code&gt;xref&lt;/code&gt;对象是用&lt;code&gt;str-&gt;reset()&lt;/code&gt;初始化的，并且经过了检查，密码也经过了检查，继续深入&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;GBool PDFDoc::setup(GString *ownerPassword, GString *userPassword) {
  str-&gt;reset();

  // check header
  checkHeader();

  // read xref table
  xref = new XRef(str);
  if (!xref-&gt;isOk()) {
    error(-1, &quot;Couldn&apos;t read xref table&quot;);
    errCode = xref-&gt;getErrorCode();
    return gFalse;
  }

  // check for encryption
  if (!checkEncryption(ownerPassword, userPassword)) {
    errCode = errEncrypted;
    return gFalse;
  }

  // read catalog
  catalog = new Catalog(xref);
  if (!catalog-&gt;isOk()) {
    error(-1, &quot;Couldn&apos;t read page catalog&quot;);
    errCode = errBadCatalog;
    return gFalse;
  }

#ifndef DISABLE_OUTLINE
  // read outline
  outline = new Outline(catalog-&gt;getOutline(), xref);
#endif

  // done
  return gTrue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;catalog&lt;/code&gt;只看构造函数中的一部分即可，构造函数中会调用&lt;code&gt;xref-&gt;getCatalog(&amp;#x26;catDict)&lt;/code&gt;，这个&lt;code&gt;catDict&lt;/code&gt;重点关注一下，其是一个&lt;code&gt;Object&lt;/code&gt;类型的对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;Catalog::Catalog(XRef *xrefA) {
  Object catDict, pagesDict, pagesDictRef;
  Object obj, obj2;
  char *alreadyRead;
  int numPages0;
  int i;

  ok = gTrue;
  xref = xrefA;
  pages = NULL;
  pageRefs = NULL;
  numPages = pagesSize = 0;
  baseURI = NULL;
  // 这里是出问题的地方
  xref-&gt;getCatalog(&amp;#x26;catDict);
  if (!catDict.isDict()) {
    error(-1, &quot;Catalog object is wrong type (%s)&quot;, catDict.getTypeName());
    goto err1;
  }
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Object&lt;/code&gt;类型的私有数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;private:

  ObjType type;			// object type
  union {			// value for each type:
    GBool booln;		//   boolean
    int intg;			//   integer
    double real;		//   real
    GString *string;		//   string
    char *name;			//   name
    Array *array;		//   array
    Dict *dict;			//   dictionary
    Stream *stream;		//   stream
    Ref ref;			//   indirect reference
    char *cmd;			//   command
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;catDict&lt;/code&gt;调用了&lt;code&gt;Object&lt;/code&gt;的构造函数，只会将其&lt;code&gt;type&lt;/code&gt;设置为&lt;code&gt;None&lt;/code&gt;，推测&lt;code&gt;xref-&gt;getCatalog(&amp;#x26;catDict)&lt;/code&gt;是为这个&lt;code&gt;catDict&lt;/code&gt;赋值，继续查看下一个调用链&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fetch&lt;/code&gt;函数，其中&lt;code&gt;obj&lt;/code&gt;就是上面的&lt;code&gt;catDict&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;无限循环发生在&lt;code&gt;switch&lt;/code&gt;语句中，在&lt;code&gt;case xrefEntryUncompressed: &lt;/code&gt;里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其首先初始化了一个&lt;code&gt;parser&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;从&lt;code&gt;parser&lt;/code&gt;中初始化三个&lt;code&gt;object&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;检查过后，进入&lt;code&gt;parser-&gt;getObj(obj, encrypted ? fileKey : (Guchar *)NULL, encAlgorithm, keyLength, num, gen);&lt;/code&gt;无开始无限递归&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;Object *XRef::fetch(int num, int gen, Object *obj) {
  XRefEntry *e;
  Parser *parser;
  Object obj1, obj2, obj3;

  // check for bogus ref - this can happen in corrupted PDF files
  if (num &amp;#x3C; 0 || num &gt;= size) {
    goto err;
  }

  e = &amp;#x26;entries[num];
  switch (e-&gt;type) {

  case xrefEntryUncompressed:
    if (e-&gt;gen != gen) {
      goto err;
    }
    obj1.initNull();
    parser = new Parser(this,
	       new Lexer(this,
		 str-&gt;makeSubStream(start + e-&gt;offset, gFalse, 0, &amp;#x26;obj1)),
	       gTrue);
    parser-&gt;getObj(&amp;#x26;obj1);
    parser-&gt;getObj(&amp;#x26;obj2);
    parser-&gt;getObj(&amp;#x26;obj3);
    if (!obj1.isInt() || obj1.getInt() != num ||
	!obj2.isInt() || obj2.getInt() != gen ||
	!obj3.isCmd(&quot;obj&quot;)) {
      obj1.free();
      obj2.free();
      obj3.free();
      delete parser;
      goto err;
    }
    // 从这里开始就进入无限循环了
    parser-&gt;getObj(obj, encrypted ? fileKey : (Guchar *)NULL,
		   encAlgorithm, keyLength, num, gen);
    obj1.free();
    obj2.free();
    obj3.free();
    delete parser;
    break;
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过调试，&lt;code&gt;parser-&gt;getObj(obj, encrypted ? fileKey : (Guchar *)NULL, encAlgorithm, keyLength, num, gen);&lt;/code&gt;中的参数分别为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./fuzzing101_xpdf_var1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继续跟踪到&lt;code&gt;Parser&lt;/code&gt;中，这里先初始化了&lt;code&gt;obj&lt;/code&gt;，也就是传入的&lt;code&gt;catDict&lt;/code&gt;这个字典，然后向这个字典中添加数据，在进行了一系列的递归初始化数据与添加之后，执行了&lt;code&gt;makeStream&lt;/code&gt;函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;makeStream&lt;/code&gt;函数中执行&lt;code&gt;addFilters&lt;/code&gt;函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后找到&lt;code&gt;Dict.cc&lt;/code&gt;文件中的&lt;code&gt;Object *Dict::lookup(char *key, Object *obj)&lt;/code&gt;函数&lt;/p&gt;
&lt;p&gt;当该函数被调用时，比如&lt;code&gt;Stream.cc&lt;/code&gt;文件中调用&lt;code&gt;dict-&gt;dictLookup()&lt;/code&gt;函数，&lt;/p&gt;
&lt;p&gt;该函数负责在一个字典对象中根据&lt;code&gt;&quot;key&quot;&lt;/code&gt;找一个&lt;code&gt;&quot;value&quot;&lt;/code&gt;，并将该&lt;code&gt;&quot;value&quot;&lt;/code&gt;赋值给一个对象，但是&lt;code&gt;&quot;value&quot;&lt;/code&gt;本身可能是一个间接引用（就像指针一样指向别的地方），这个时候就需要继续去找，此时如果出现循环引用的问题，就会不停的找下去（类似两个类对象互相持有&lt;code&gt;std::shared_ptr&lt;/code&gt;最后无法释放），AFL++恶意构造的坏PDF文件中出现了这种引用并被xpdf程序记录到字典对象中，导致parser解析失败并进行循环调用，最后爆栈&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;修复：这是&lt;code&gt;parser&lt;/code&gt;本身设计的失误，改起来应该比较困难，看了一下新版本源码，直接限制了&lt;code&gt;dictLookup&lt;/code&gt;的次数，这样就限制了找间接引用的次数，从而不会爆栈&lt;/p&gt;
&lt;h3&gt;HELP. vscode进行图形化调试&lt;/h3&gt;
&lt;p&gt;直接使用&lt;code&gt;lldb-19&lt;/code&gt;这样的终端调试工具在打断点和调试的过程中可能不顺手，可以借用&lt;code&gt;codelldb&lt;/code&gt;插件&lt;/p&gt;
&lt;p&gt;直接&lt;code&gt;F5&lt;/code&gt;自动创建一个&lt;code&gt;launch.json&lt;/code&gt;，填写以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息，请访问: https://go.microsoft.com/fwlink/?linkid=830387
    &quot;version&quot;: &quot;0.2.0&quot;,
    &quot;configurations&quot;: [
        {
            &quot;name&quot;: &quot;Debug pdftotext&quot;,
            &quot;type&quot;: &quot;lldb&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;program&quot;: &quot;${workspaceRoot}/install/bin/pdftotext&quot;,
            &quot;args&quot;: [
                // 这里是可执行程序需要的命令行参数 &quot;${workspaceRoot}/out/default/crashes/id:000000,sig:11,src:001033,time:276919,execs:242688,op:havoc,rep:3&quot;,
                // 将转换后的文本输出到标准输出
                &quot;-&quot; 
            ],
            &quot;cwd&quot;: &quot;${workspaceRoot}&quot;,
            &quot;sourceMap&quot;: {
                // 将编译时记录的源码路径映射到当前工作区的实际路径
                // 假设编译时的源码根目录是 /target/xpdf-3.02
                &quot;/target/xpdf-3.02&quot;: &quot;${workspaceRoot}/xpdf-3.02&quot;
            }
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>AFL++ Fuzzing-Module模糊测试入门</title><link>https://liang-bk.github.io/blog/fuzzing/fuzzing-module</link><guid isPermaLink="true">https://liang-bk.github.io/blog/fuzzing/fuzzing-module</guid><description>模糊测试入门，使用AFL++仓库的推荐文档</description><pubDate>Sat, 03 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 环境准备&lt;/h2&gt;
&lt;h3&gt;1.1 配置docker&lt;/h3&gt;
&lt;p&gt;安装参考这一篇：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/yqbaowo/p/18889717&quot;&gt;Ubuntu 安装 Docker 的方法（基于Ubuntu 24.04 LTS测试） - 星尘的博客 - 博客园&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;镜像站使用的是1ms，貌似能够下载AFL++的image：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;registry-mirrors&quot;: [
    &quot;https://docker.1ms.run&quot;,
    &quot;https://docker.m.daocloud.io&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 配置AFL++容器&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;拉AFL++镜像：&lt;code&gt;docker pull aflplusplus/aflplusplus&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后执行&lt;code&gt;docker images&lt;/code&gt;应该能看到&lt;code&gt;aflplusplus/aflplusplus:latest&lt;/code&gt;这个标识&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行AFL++的镜像：&lt;code&gt;docker run -it --name aflpp aflplusplus/aflplusplus&lt;/code&gt;&lt;/p&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;code&gt;--name&lt;/code&gt;表示给这个容器起名叫&lt;code&gt;aflpp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aflplusplus/aflplusplus&lt;/code&gt;表示指定了镜像名称&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在容器内的终端里进入&lt;code&gt;/AFLplusplus&lt;/code&gt;文件夹，然后执行&lt;code&gt;make&lt;/code&gt;命令构建即可&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;退出容器&lt;code&gt;exit&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看容器ID：&lt;code&gt;docker ps- a&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;该命令会查看当前创建的所有容器（包括正在运行的和停止的）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提交该容器：&lt;code&gt;docker commit aflpp aflpp-built&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aflpp&lt;/code&gt;：指定容器名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aflpp-built&lt;/code&gt;：指定新镜像名字&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作用是将一个容器提交为一个新镜像&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;克隆&lt;code&gt;fuzzing-module&lt;/code&gt;项目：&lt;code&gt;git clone https://github.com/alex-maleno/Fuzzing-Module.git&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入&lt;code&gt;fuzzing-module&lt;/code&gt;项目的根目录，执行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run --rm -it \
  -v &quot;$(pwd)&quot;:/target \
  aflpp-built
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这表示使用&lt;code&gt;aflpp-built&lt;/code&gt;启动一个容器，将本地的&lt;code&gt;fuzzing-module&lt;/code&gt;所在目录挂载到容器的&lt;code&gt;/target&lt;/code&gt;目录中，并在容器退出后自动删除该容器&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至此，AFL++容器的配置都已经完成&lt;/p&gt;
&lt;h2&gt;2. 练习1&lt;/h2&gt;
&lt;h3&gt;2.1 编译代码&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;fuzzing-module&lt;/code&gt;项目根目录进容器：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run --rm -it \
  -v &quot;$(pwd)&quot;:/target \
  aflpp-built
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入&lt;code&gt;target&lt;/code&gt;目录：&lt;code&gt;cd /target&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以看到原先的&lt;code&gt;exercisexxx&lt;/code&gt;都在该目录下，进入&lt;code&gt;exercise1&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mkdir build &amp;#x26;&amp;#x26; cd build&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编译项目，指定AFL++项目构建出的编译器：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;CC=/AFLplusplus/afl-clang-fast CXX=/AFLplusplus/afl-clang-fast++ cmake ..
make
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.2 运行AFL++&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;构建种子文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 回到exercise1目录下，创建seeds目录并进入，将种子文件生成在该目录下
cd ..
mkdir seeds &amp;#x26;&amp;#x26; cd seeds
for i in {0..4}; do dd if=/dev/urandom of=seed_$i bs=64 count=10; done
cd ..
cd build
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用AFL++运行分析目标程序&lt;code&gt;/target/exercise1/build/simple_crash&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/AFLplusplus/afl-fuzz -i /target/exercise1/seeds -o out -m none -d -- /target/exercise1/build/simple_crash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后会有一个终端图形界面出来，显示正在运行，当上面的&lt;code&gt;crashes&lt;/code&gt;数字变更后，就可以强制停止了，生成的文件在&lt;code&gt;target/exercise1/build/out/default&lt;/code&gt;目录里，其中&lt;code&gt;/crashes&lt;/code&gt;目录是需要重点关注的文件夹&lt;/p&gt;
&lt;h3&gt;2.3 分析&lt;/h3&gt;
&lt;h4&gt;2.3.1 目录介绍&lt;/h4&gt;
&lt;p&gt;首先依次解释一下&lt;code&gt;default&lt;/code&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;&lt;strong&gt;&lt;code&gt;crashes/&lt;/code&gt;&lt;/strong&gt;: &lt;strong&gt;（最重要）&lt;/strong&gt; 这是一个目录，存放导致目标程序崩溃（例如，段错误 SIGSEGV、中止 SIGABRT 等）的输入文件。你应该首先分析这里的文件，因为它们直接指向了潜在的安全漏洞。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;hangs/&lt;/code&gt;&lt;/strong&gt;: 这是一个目录，存放导致目标程序超时（未在指定时间内完成执行）的输入文件。这些输入可能触发了死循环或非常耗时的操作，可能对应于拒绝服务（DoS）漏洞。&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;strong&gt;&lt;code&gt;queue/&lt;/code&gt;&lt;/strong&gt;: 这是一个目录，包含了所有“有趣”的测试用例（也称为语料库）。AFL++ 会将任何能够触发新代码路径（即覆盖新的“边”）的输入文件保存在这里。这些文件是 fuzzer 进行变异的基础。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fuzz_bitmap&lt;/code&gt;&lt;/strong&gt;: 这是一个二进制文件，记录了整个测试过程中所有被触发过的代码路径的“位图”。fuzzer 通过检查新输入是否会在此位图中点亮新的比特位，来判断该输入是否“有趣”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fastresume.bin&lt;/code&gt;&lt;/strong&gt;: 这是 AFL++ 的一个特性文件。它存储了 &lt;code&gt;queue&lt;/code&gt; 目录中每个文件的元数据摘要，使得在恢复或重启 fuzzer 时可以非常快速地加载语料库，而无需重新分析每个文件。&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;strong&gt;&lt;code&gt;fuzzer_stats&lt;/code&gt;&lt;/strong&gt;: 一个人类可读的文本文件，包含了 fuzzer 运行时的详细统计信息。你可以通过 &lt;code&gt;cat build/out/default/fuzzer_stats&lt;/code&gt; 查看，内容包括运行时间、执行速度、总路径数、崩溃数、周期数等。这是监控 fuzzer 状态的主要文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;plot_data&lt;/code&gt;&lt;/strong&gt;: 一个逗号分隔值（CSV）格式的文本文件，用于绘制 fuzzer 运行状态图。它记录了随时间变化的各种统计数据（如路径数、崩溃数等）。你可以使用 &lt;code&gt;afl-plot&lt;/code&gt; 工具来将此文件可视化，例如：&lt;code&gt;afl-plot build/out/default /path/to/plot/output&lt;/code&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;strong&gt;&lt;code&gt;cmdline&lt;/code&gt;&lt;/strong&gt;: 记录了启动本次 fuzzer 时所使用的完整命令行参数。这对于复现测试环境非常有用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fuzzer_setup&lt;/code&gt;&lt;/strong&gt;: 记录了 fuzzer 启动时的一些关键设置和检测到的环境信息，例如 CPU 亲和性、内存限制等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;target_hash&lt;/code&gt;&lt;/strong&gt;: 包含了目标二进制文件的 SHA256 哈希值。AFL++ 使用它来检测目标程序是否在 fuzzer 运行期间被修改过。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2.3.2 补充模糊测试基本知识&lt;/h4&gt;
&lt;p&gt;在分析之前，补充一些模糊测试的基本知识：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;为什么必须使用 AFL++ 的编译器？&lt;/p&gt;
&lt;p&gt;简而言之：为了&lt;strong&gt;插桩 (Instrumentation)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;AFL++ 不是一个“盲目”的 fuzzer。它是一个&lt;strong&gt;覆盖率引导 (Coverage-guided)&lt;/strong&gt; 的 fuzzer。这意味着它需要知道一个输入执行了程序的哪些代码路径，从而判断这个输入是否“有趣”（即是否探索了新的代码区域）。如果一个输入触发了新的代码路径，AFL++ 就会将其保留下来，并作为未来变异的基础。&lt;/p&gt;
&lt;p&gt;为了实现这一点，AFL++ 提供了一套编译器包装器（如 &lt;code&gt;afl-clang-fast++&lt;/code&gt;, &lt;code&gt;afl-gcc&lt;/code&gt; 等）。当你使用这些编译器来构建你的目标程序时，它们会在编译过程中向你的程序中&lt;strong&gt;注入（或称“插入”）一些额外的代码&lt;/strong&gt;。这些被注入的代码非常轻量，主要做一件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在程序的每个基本块（或分支跳转）处，插入一小段代码来更新一个共享的内存区域（称为“位图”或 &lt;code&gt;bitmap&lt;/code&gt;）。&lt;/strong&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;li&gt;&lt;code&gt;afl-fuzz&lt;/code&gt; 主进程会监控这个位图。如果一个变异后的输入导致位图中出现了之前从未有过的标记，&lt;code&gt;afl-fuzz&lt;/code&gt; 就知道这个输入发现了一条新路径。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结论：&lt;/strong&gt; 如果不使用 AFL++ 的编译器进行插桩，&lt;code&gt;afl-fuzz&lt;/code&gt; 就无法获得代码覆盖率的反馈。它会退化成一个“哑”fuzzer，只能随机生成输入，效率极低，也无法有效地探索程序深处的逻辑。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;种子 (Seed) 的作用和内容是什么？&lt;/p&gt;
&lt;p&gt;种子是模糊测试的&lt;strong&gt;起始点&lt;/strong&gt;。&lt;code&gt;afl-fuzz&lt;/code&gt; 不会从零开始完全随机地生成输入，而是会读取你提供的种子文件，将它们作为初始的测试用例集合。然后，它会对这些种子进行各种变异（比特翻转、算术运算、拼接、删除等）来生成新的输入，再去测试目标程序。&lt;/p&gt;
&lt;p&gt;一个好的种子集合可以极大地提升模糊测试的效率，因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;提供初始覆盖率&lt;/strong&gt;：如果种子本身就是合法的、能够被程序正确解析的输入（例如，一个有效的 PNG 图片作为图片解析器的种子），那么 fuzzer 从一开始就能覆盖到程序的核心处理逻辑，而不是浪费时间去“猜”出一个合法的文件格式。&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;p&gt;比如程序接受一个字符串，那就种子的内容就是一个字符串&lt;/p&gt;
&lt;p&gt;程序接受一个64×64的图片，种子的内容就可以是64×64的，数值在0~255之间的二维数组&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;dd&lt;/code&gt; 命令生成的内容：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for i in {0..4}; do dd if=/dev/urandom of=seed_$i bs=64 count=10; done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令的作用是创建 5 个（&lt;code&gt;seed_0&lt;/code&gt; 到 &lt;code&gt;seed_4&lt;/code&gt;）种子文件。每个文件包含 &lt;code&gt;64 * 10 = 640&lt;/code&gt; 字节的&lt;strong&gt;完全随机的数据&lt;/strong&gt;，这些数据来源于 &lt;code&gt;urandom&lt;/code&gt;（Linux 系统中的一个高质量随机数生成器）。&lt;/p&gt;
&lt;p&gt;在这种情况下，生成的种子是&lt;strong&gt;随机的、无结构的二进制数据&lt;/strong&gt;。这适用于测试那些期望接收任意二进制流的程序。但对于需要特定文件格式（如 XML, JSON, PNG）的程序，使用完全随机的数据作为种子效果很差。更好的做法是提供一些结构合法、内容简单的真实文件作为种子。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;afl-fuzz&lt;/code&gt; 命令和参数的解释&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;afl-fuzz -i [seeds directory] -o out -m none -d -- [path to the executable]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令是启动 AFL++ fuzzer 的核心指令。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;afl-fuzz&lt;/code&gt;&lt;/strong&gt;: 启动 fuzzer 的主程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-i [full path to your seeds directory]&lt;/code&gt;&lt;/strong&gt;: 指定**输入（input）**目录，也就是你存放种子文件的目录。&lt;code&gt;afl-fuzz&lt;/code&gt; 会从这里加载初始测试用例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-o out&lt;/code&gt;&lt;/strong&gt;: 指定**输出（output）**目录。所有 fuzzer 的发现和状态都会保存在这个名为 &lt;code&gt;out&lt;/code&gt; 的目录中，包括我们之前讨论的 &lt;code&gt;crashes/&lt;/code&gt;, &lt;code&gt;queue/&lt;/code&gt;, &lt;code&gt;fuzzer_stats&lt;/code&gt; 等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-m none&lt;/code&gt;&lt;/strong&gt;: 设置内存限制（&lt;strong&gt;memory limit&lt;/strong&gt;）。&lt;code&gt;none&lt;/code&gt; 表示不为目标程序设置内存限制。这通常在你知道程序本身内存占用较大，或者在受控环境（如 Docker）中已经有全局内存限制时使用，以避免 fuzzer 因误判而频繁杀死正常运行的子进程。默认情况下，AFL++ 会设置一个比较严格的内存限制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-d&lt;/code&gt;&lt;/strong&gt;: 启用**“确定性”变异阶段（deterministic** stages）。在进行完全随机的“havoc”变异之前，AFL++ 会先尝试一些简单的、可预测的变异策略，如按位翻转、整数加减等。这是一个非常高效的阶段，通常能很快找到浅层的 bug。&lt;code&gt;-d&lt;/code&gt; 标志可以跳过这个阶段，直接进入随机性更强的 havoc 阶段。在某些情况下，如果种子文件非常大或者确定性变异阶段耗时过长，可以使用 &lt;code&gt;-d&lt;/code&gt; 来加速。但在大多数情况下，建议保留确定性阶段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--&lt;/code&gt;&lt;/strong&gt;: 这是一个分隔符。它告诉 &lt;code&gt;afl-fuzz&lt;/code&gt;，后面的所有参数都属于要执行的&lt;strong&gt;目标程序命令&lt;/strong&gt;，而不是 &lt;code&gt;afl-fuzz&lt;/code&gt; 自身的参数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;[full path to the executable]&lt;/code&gt;&lt;/strong&gt;: 你要测试的目标程序的路径和名称。&lt;code&gt;afl-fuzz&lt;/code&gt; 会反复执行这个程序。AFL++ 通常会通过 &lt;code&gt;@@&lt;/code&gt; 来指定输入文件的位置，例如 &lt;code&gt;/path/to/my_app @@&lt;/code&gt;。如果省略 &lt;code&gt;@@&lt;/code&gt;，AFL++ 默认会通过标准输入（stdin）将测试用例喂给目标程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2.3.3 调试程序找出bug&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;文件解释：&lt;/p&gt;
&lt;p&gt;导致程序崩溃的文件都在&lt;code&gt;build/out/default/crashes&lt;/code&gt;下，其文件名即各种元信息的组合：&lt;/p&gt;
&lt;p&gt;以 &lt;code&gt;id:000000,sig:06,src:000004,time:160,execs:116,op:flip4,pos:7&lt;/code&gt; 为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id:000000&lt;/code&gt;: 崩溃的唯一标识符，从 0 开始递增。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sig:06&lt;/code&gt;: &lt;strong&gt;（关键信息）&lt;/strong&gt; 导致程序终止的信号编号。&lt;code&gt;06&lt;/code&gt; 代表 &lt;code&gt;SIGABRT&lt;/code&gt; (Abort)，通常由断言失败 &lt;code&gt;assert()&lt;/code&gt; 或其他库检测到的内部错误触发。另一个常见的是 &lt;code&gt;sig:11&lt;/code&gt;，代表 &lt;code&gt;SIGSEGV&lt;/code&gt; (Segmentation Fault)，即段错误，通常由无效的内存访问（如空指针解引用、越界读写）引起。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src:000004&lt;/code&gt;: 源输入文件的 ID。这个崩溃是由 &lt;code&gt;queue/id:000004&lt;/code&gt; 文件经过变异得到的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;time:160&lt;/code&gt;: 发现此崩溃时，fuzzer 已经运行的时间（毫秒）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;execs:116&lt;/code&gt;: 发现此崩溃时，fuzzer 已经执行的总次数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;op:flip4&lt;/code&gt;: 产生这个崩溃的最后一个变异操作是 &lt;code&gt;flip4&lt;/code&gt;（翻转了文件中的 4 个比特位）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pos:7&lt;/code&gt;: 变异操作发生在源文件的偏移量 7 处。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文件内容即导致目标崩溃的内容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;gdb调试程序&lt;/p&gt;
&lt;p&gt;进入&lt;code&gt;/build&lt;/code&gt;目录下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开启调试：&lt;code&gt;gdb --args ./simple_crash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 GDB 提示符 &lt;code&gt;(gdb)&lt;/code&gt; 后，输入 &lt;code&gt;run&lt;/code&gt; 命令并重定向输入：&lt;code&gt;run &amp;#x3C; build/out/default/crashes/id:000000,sig:06,src:000004,time:160,execs:116,op:flip4,pos:7&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分析崩溃点&lt;/strong&gt;：
程序会在崩溃处停下来。GDB 会显示导致崩溃的代码行。然后你可以使用以下命令来检查程序状态：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bt&lt;/code&gt; (backtrace): 显示函数调用栈。这是最有用的命令，可以让你看到函数是如何一步步调用到崩溃点的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p &amp;#x3C;variable&gt;&lt;/code&gt; (print): 打印变量的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;info registers&lt;/code&gt;: 显示寄存器的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;info locals&lt;/code&gt;: 显示当前栈帧的局部变量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x/i $pc&lt;/code&gt;: 查看当前指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过源码和接收的字符串来看，程序接受一行字符串，如果该字符串有效范围内的的第一个字符和最后一个字符都是&lt;code&gt;\x00&lt;/code&gt;，或者字符串内有递增的数字字符出现（比如&quot;0@1&quot;），就会崩溃&lt;/p&gt;
&lt;h2&gt;3. 练习2&lt;/h2&gt;
&lt;p&gt;和练习1没什么区别，就是封装了一个类，在&lt;code&gt;interact&lt;/code&gt;函数里面接收标准输入流&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始有两个乘员，&lt;code&gt;Captain&lt;/code&gt;，&lt;code&gt;CoPilot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;t&lt;/code&gt;：飞机有乘员则起飞&lt;/li&gt;
&lt;li&gt;&lt;code&gt;l&lt;/code&gt;：飞机有乘员则降落&lt;/li&gt;
&lt;li&gt;&lt;code&gt;h&lt;/code&gt;：增加一个乘员&lt;/li&gt;
&lt;li&gt;&lt;code&gt;f&lt;/code&gt;：减少一个乘员&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;指定&lt;code&gt;l&lt;/code&gt;命令时，如果没有乘员就&lt;code&gt;abort&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;4. 练习3&lt;/h2&gt;
&lt;p&gt;该练习主要介绍&lt;code&gt;slice-fuzzing&lt;/code&gt;的办法，人话就是只针对部分代码片段进行模糊测试&lt;/p&gt;
&lt;p&gt;&lt;code&gt;exercise3&lt;/code&gt;中给了一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;/*
 *
 *
 * This file isolates the Specs class and tests out the 
 * choose_color function specifically.
 * 
 * 
 * 
 */

#include &quot;specs.h&quot;

int main(int argc, char** argv) {
    // In order to call any functions in the Specs class, a Specs
    // object is necessary. This is using one of the constructors
    // found in the Specs class.
    Specs spec(505, 110, 50);
    // By looking at all the code in our project, this is all the 
    // necessary setup required. Most projects will have much more
    // that is needed to be done in order to properly setup objects.

    // This section should be in your code that you write after all the 
    // necessary setup is done. It allows AFL++ to start from here in 
    // your main() to save time and just throw new input at the target.
    #ifdef __AFL_HAVE_MANUAL_CONTROL
        __AFL_INIT();
    #endif

    spec.choose_color();
    //spec.min_alt();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该代码独立&lt;code&gt;main.cpp&lt;/code&gt;，在&lt;code&gt;cmake&lt;/code&gt;中被额外添加构建可执行文件的规则，这相当于主动编写模糊测试驱动程序，在这个程序中，只去测试&lt;code&gt;Specs&lt;/code&gt;类的&lt;code&gt;choose_color()&lt;/code&gt;功能, 然后&lt;strong&gt;连接 Fuzzer&lt;/strong&gt;：它包含接收 fuzzer 输入并传递给目标函数的逻辑（这部分通常是隐式的，例如目标函数从标准输入读取数据）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AFL++持久模式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;标准的模糊测试流程中：AFL++ 每测试一个输入，都需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动一个全新的进程。&lt;/li&gt;
&lt;li&gt;在那个新进程里，从&lt;code&gt;main&lt;/code&gt;函数开始完整地执行一遍程序。&lt;/li&gt;
&lt;li&gt;程序执行完毕后，销毁进程。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果在 fuzz 的关键步骤前面有诸如“载入配置文件”等步骤，仍然会造成效率浪费。因此，我们可以自行选择 fuzz 入口，然后添加以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#ifdef __AFL_HAVE_MANUAL_CONTROL
  __AFL_INIT();
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该代码之前的部分只被执行一次，之后的部分才是测试的重点，会不断执行，以此来提高fuzzing的效率&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自定义切片代码：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;类似给出的样例，可以创建cpp文件来测试需要的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &quot;specs.h&quot;

int main(int argc, char** argv) {
    Specs spec(505, 110, 50);
    
    #ifdef __AFL_HAVE_MANUAL_CONTROL
        __AFL_INIT();
    #endif

    spec.fuel_cap();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;cmake&lt;/code&gt;添加构建规则，然后指定fuzz的目标即可&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>linux环境下cmake + clangd + lldb搭建cpp开发环境</title><link>https://liang-bk.github.io/blog/linux-cpp-environment</link><guid isPermaLink="true">https://liang-bk.github.io/blog/linux-cpp-environment</guid><description>快速搭建现代cpp环境</description><pubDate>Mon, 03 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;linux环境下cmake + clangd + lldb搭建cpp开发环境&lt;/h1&gt;
&lt;h2&gt;一. 基础环境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;wsl2&lt;/li&gt;
&lt;li&gt;ubuntu22&lt;/li&gt;
&lt;li&gt;ubuntu24&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上均可（其他没试过，不过理论上都可以）&lt;/p&gt;
&lt;h2&gt;二. 源配置&lt;/h2&gt;
&lt;p&gt;如果要配置多编译器环境或者更想用&lt;strong&gt;gcc/g++&lt;/strong&gt;，这里的源配置主要针对需要下载高等级的gcc版本&lt;/p&gt;
&lt;p&gt;由于一些低版本的ubuntu（如22）默认下载gcc11版本，如果想要下载更高的版本，12以及往上，可以使用下面两条命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo add-apt-repository ppa:ubuntu-toolchain-r/test
sudo add-apt-repository ppa:ubuntu-toolchain-r/ppa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后可以安装高版本的gcc和g++&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;apt search gcc-12
# apt search gcc-13
sudo apt install gcc-12
sudo apt install g++-12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过由于是国外源，下载的会比较慢，需要额外配置镜像（这个镜像不是&lt;code&gt;/etc/apt/sources.list&lt;/code&gt;文件）&lt;/p&gt;
&lt;p&gt;执行上面两条&lt;code&gt;add-apt-repository&lt;/code&gt;命令后，查看&lt;code&gt;/etc/apt/sources.list.d&lt;/code&gt;目录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ll /etc/apt/sources.list.d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下图所示（一个ppa，一个test）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/source1.CoEhSP_g_N6voY.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;以&lt;code&gt;ppa-jammy.list&lt;/code&gt;为例，里面的内容原本是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;deb https://ppa.launchpadcontent.net/ubuntu-toolchain-r/ppa/ubuntu/ jammy main 
# deb-src https://ppa.launchpadcontent.net/ubuntu-toolchain-r/ppa/ubuntu/ jammy main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用如下命令换为中科大的源（最后两个参数是&lt;code&gt;ppa&lt;/code&gt;和&lt;code&gt;test&lt;/code&gt;两个文件名）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo sed -i &apos;s|https://ppa.launchpadcontent.net|https://launchpad.proxy.ustclug.org|g&apos; /etc/apt/sources.list.d/ubuntu-toolchain-r-ubuntu-ppa-jammy.list /etc/apt/sources.list.d/ubuntu-toolchain-r-ubuntu-test-jammy.list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update
sudo apt install gcc-12 g++-12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就会快很多了&lt;/p&gt;
&lt;h2&gt;三. gcc/g++版本切换（可选）&lt;/h2&gt;
&lt;p&gt;如果电脑下有多个gcc/g++，想要切换版本，首先查看各个gcc/g++的版本号：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -l /usr/bin | grep gcc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/gcc-version.pH_izhKP_1I45GF.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;update-alternatives&lt;/code&gt;命令设置各个gcc/g++的优先级：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 13的权值是100, 11是80...
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 --slave /usr/bin/g++ g++ /usr/bin/g++-13 --slave /usr/bin/gcov gcov /usr/bin/gcov-13
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 80 --slave /usr/bin/g++ g++ /usr/bin/g++-11 --slave /usr/bin/gcov gcov /usr/bin/gcov-11
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 60 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置完毕后再执行&lt;code&gt;gcc -v&lt;/code&gt;就能看到当前是&lt;code&gt;gcc-13&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果想要切换版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo update-alternatives --config gcc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下图，选择对应版本即可：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/gcc-choose.CctL2c3k_ZlYgQF.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;四. llvm配置&lt;/h2&gt;
&lt;p&gt;国外的网址：&lt;a href=&quot;https://apt.llvm.org/&quot;&gt;apt llvm&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;找到下图中的脚本命令：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/apt-llvm-script.iYnZXaRY_1fxcoA.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;和版本：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/apt-llvm-version.DY62qT2v_Z2p9SM8.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;（我安装的时候stable版本是20）&lt;/p&gt;
&lt;p&gt;但不要执行，同样是由于国外源太慢的问题，这次换清华源：&lt;a href=&quot;https://mirrors4.tuna.tsinghua.edu.cn/help/llvm-apt/&quot;&gt;tsinghua apt llvm&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/apt-llvm-script-20.DFjyT5mz_2tuoBF.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;LLVM版本&lt;/code&gt;选为20（一般选stable版本就对了）&lt;/p&gt;
&lt;p&gt;将上面的命令逐行粘贴到终端执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo wget https://mirrors.tuna.tsinghua.edu.cn/llvm-apt/llvm.sh
sudo chmod +x llvm.sh
sudo ./llvm.sh all -m https://mirrors.tuna.tsinghua.edu.cn/llvm-apt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后再安装&lt;code&gt;libstdc++&lt;/code&gt;，配置&lt;code&gt;gcc/g++&lt;/code&gt;使用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get install libc++-20-dev libc++abi-20-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就安装好了llvm全套（&lt;code&gt;clang/clang++/clangd/lldb&lt;/code&gt;）和&lt;code&gt;libc++&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;五. cmake配置&lt;/h2&gt;
&lt;p&gt;不推荐直接从apt下载cmake，版本一般比较旧&lt;/p&gt;
&lt;p&gt;一般从源码安装或者作者仓库中拉取，具体参考下面这篇文章：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/694017813&quot;&gt;ubuntu 22.04环境中cmake安装 - 知乎&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;六. vscode配置&lt;/h2&gt;
&lt;p&gt;插件安装列表（自行安装），还有一个Test Explorer UI插件没有截进来：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/vscode-plug.CEga8tQB_eYvLE.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;6.1 clangd&lt;/h3&gt;
&lt;h4&gt;6.1.1 clangd path&lt;/h4&gt;
&lt;p&gt;刚下载完clangd，会弹窗提示没有下载clangd-server，这时候不要点击，直接去clangd设置中：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/clangd-enter-setting.DkL8Idu8_ZSOc4M.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不是用户的设置而是wsl2或者虚拟机的设置中，找到&lt;code&gt;Clangd: Path&lt;/code&gt;选项，之前下载&lt;code&gt;llvm&lt;/code&gt;组件时已经下载了&lt;code&gt;clangd-&amp;#x3C;version&gt;&lt;/code&gt;（&lt;code&gt;&amp;#x3C;version&gt;&lt;/code&gt;是stable的版本号），直接填入到选项中：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/clangd-path.oyV4BC89_1ybXKO.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;6.1.2 .clangd配置系统文件寻找路径&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用clang/clang++头文件&lt;/p&gt;
&lt;p&gt;在项目目录下创建&lt;code&gt;.clangd&lt;/code&gt;文件，然后填入下面的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;CompileFlags:
  Add:
    - &quot;-stdlib=libc++&quot;  # 主要是这行
    - &quot;-ferror-limit=0&quot;
  Compiler: clang++-20
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用gcc，可以这么写（不需要配置&lt;code&gt;-stdlib&lt;/code&gt;，因为linux下默认用&lt;code&gt;libstdc++&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;CompileFlags:
  Add:
    - &quot;-I/usr/include/c++/13&quot; # 
    - &quot;-ferror-limit=0&quot;
  Compiler: g++-13
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.2 codelldb&lt;/h3&gt;
&lt;h4&gt;6.2.1 安装vsix&lt;/h4&gt;
&lt;p&gt;下载插件后，可能会报错，弹窗&lt;code&gt;open ... url&lt;/code&gt;，点击后在浏览器下载一个&lt;code&gt;.vsix&lt;/code&gt;文件&lt;/p&gt;
&lt;p&gt;然后在扩展中安装：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/codelldb-install.CT4j6nWt_Xkklj.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;6.2.2 调试&lt;/h4&gt;
&lt;p&gt;配置好cmake tools之前的cmake项&lt;/p&gt;
&lt;p&gt;打断点，按&lt;code&gt;F5&lt;/code&gt;调试，一开始会报错，让创建&lt;code&gt;launch.json&lt;/code&gt;文件，跟着步骤创建：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息，请访问: https://go.microsoft.com/fwlink/?linkid=830387
    &quot;version&quot;: &quot;0.2.0&quot;,
    &quot;configurations&quot;: [
        {
            &quot;type&quot;: &quot;lldb&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;name&quot;: &quot;Debug&quot;,
            &quot;program&quot;: &quot;${workspaceFolder}/&amp;#x3C;executable file&gt;&quot;,
            &quot;args&quot;: [],
            &quot;cwd&quot;: &quot;${workspaceFolder}&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把&lt;code&gt;&quot;program&quot;&lt;/code&gt;项换为可执行文件的位置，就可以正常调试了&lt;/p&gt;
&lt;h3&gt;6.3 cmake&lt;/h3&gt;
&lt;h4&gt;6.3.1 cmake path&lt;/h4&gt;
&lt;p&gt;在CMake插件中的设置中，找到&lt;code&gt;Cmake: Cmake Path&lt;/code&gt;选项，设置为cmake的可执行文件路径：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/cmake-path.DJtJlxrV_Z2qgbjH.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;6.3.2 cmake快速创建项目&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;创建一个文件夹，然后使用vscode打开&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + Shift + P&lt;/code&gt;唤出命令面板&lt;/li&gt;
&lt;li&gt;输入&lt;code&gt;cmake:quick&lt;/code&gt;，找到&lt;code&gt;CMake: Quick Start&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;按提示步骤完成剩余操作&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;6.3.3 CMAKE_EXPORT_COMPILE_COMMANDS&lt;/h4&gt;
&lt;p&gt;使用&lt;code&gt;git&lt;/code&gt;下载一个cmake项目，比如&lt;code&gt;fmt: git clone https://github.com/fmtlib/fmt.git&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果要使用clang++，则找到项目根目录下的&lt;code&gt;CMakeLists.txt&lt;/code&gt;，加入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# 生成 compile_commands.json
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述命令会在&lt;code&gt;build&lt;/code&gt;目录下生成&lt;code&gt;compile_commands.json&lt;/code&gt;，clangd会根据这个文件生成索引，存放在项目的&lt;code&gt;.cache&lt;/code&gt;目录下，如果没有，可能不会正常工作&lt;/p&gt;
&lt;p&gt;至此，clangd应该能够正常工作了&lt;/p&gt;
&lt;h4&gt;6.3.4 cmake tools&lt;/h4&gt;
&lt;p&gt;使用过clion和vs2022都清楚有比较方便的切换启动哪个可执行程序的功能，借助cmake tools插件，可以做到这一点，修改&lt;code&gt;launch.json&lt;/code&gt;（主要是&lt;code&gt;&quot;program&quot;&lt;/code&gt;项）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息，请访问: https://go.microsoft.com/fwlink/?linkid=830387
    &quot;version&quot;: &quot;0.2.0&quot;,
    &quot;configurations&quot;: [
        {
            &quot;type&quot;: &quot;lldb&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;name&quot;: &quot;Debug&quot;,
            &quot;program&quot;: &quot;${command:cmake.launchTargetPath}&quot;,
            &quot;args&quot;: [],
            &quot;cwd&quot;: &quot;${workspaceFolder}&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在vscode左侧cmake-tools的图标中，选择启动和调试的目标即可：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/cmake-tools-auto.Coj1ieGU_2gjVP5.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;6.4 unit-test&lt;/h3&gt;
&lt;p&gt;安装&lt;code&gt;C++ TestMate&lt;/code&gt;和&lt;code&gt;Test Explorer UI&lt;/code&gt;后，在左侧测试按钮中可以进行对应的测试：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/unit-test.BxMtfZKg_2dLTRy.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在编译后也可以在对应的测试文件中直接使用图形化运行按钮进行测试&lt;/p&gt;
&lt;p&gt;但是测试文件（指可执行文件）的识别默认为build或者out目录下的带有&lt;code&gt;test&lt;/code&gt;名的文件，所以在生成可执行文件时要注意正确的命名：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/test-name.BRDEwoI6_Z147A9v.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;七. windows下环境配置&lt;/h2&gt;
&lt;p&gt;请参考&lt;a href=&quot;https://www.ispellbook.com/post/VSCodeCppDevConfig&quot;&gt;VSCode C/C++开发环境配置 Clang/LLVM+LLDB+CMake&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;windows上使用lldb和msvc stl只能看到stl组件的地址，看不到内容，建议直接使用cmake-tools自带的cppdbg，或者用lldb配合MinGW使用，个人认为window上还是直接用msvc工具链比较好&lt;/p&gt;
&lt;h2&gt;八. vcpkg配置&lt;/h2&gt;
&lt;h3&gt;8.1 下载与环境配置&lt;/h3&gt;
&lt;p&gt;下载：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/microsoft/vcpkg.git # 克隆仓库
cd vcpkg
./bootstrap-vcpkg.sh # Linux bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;环境配置：&lt;/p&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 VCPKG_ROOT=/path/to/vcpkg # vcpkg文件夹的实际路径
export PATH=$VCPKG_ROOT:$PATH
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.2 案例&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建vcpkg依赖&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir vcpkg_test &amp;#x26;&amp;#x26; cd vcpkg_test
vcpkg new --application # 初始化vcpkg项目
vcpkg add port fmt # 添加 fmt 依赖项
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建cmake项目&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Ctrl&lt;/code&gt; + &lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;P&lt;/code&gt;唤出vscode命令行，使用&lt;code&gt;CMake: Quick Start&lt;/code&gt;创建cmake项目&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置&lt;code&gt;CMakePresets.json&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;项目目录创建&lt;code&gt;CMakePresets.json&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;version&quot;: 2,
    &quot;configurePresets&quot;: [
        {
            &quot;name&quot;: &quot;vcpkg&quot;,
            &quot;generator&quot;: &quot;Ninja&quot;,
            &quot;binaryDir&quot;: &quot;${sourceDir}/build&quot;,
            &quot;cacheVariables&quot;: {
                &quot;CMAKE_TOOLCHAIN_FILE&quot;: &quot;$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake&quot;
            },
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如有需要可以再创建&lt;code&gt;CMakeUserPresets.json&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;version&quot;: 2,
    &quot;configurePresets&quot;: [
        {
            &quot;name&quot;: &quot;default&quot;,
            &quot;inherits&quot;: &quot;vcpkg&quot;,
            &quot;environment&quot;: {
                &quot;VCPKG_ROOT&quot;: &quot;&quot; // 填vcpkg的实际路径
            }
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此时可以在CMake-Tools提供的配置中选择对应的配置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/CMake-tools-configuration.GYmdv0I3_2aClom.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CMakeLists.txt&lt;/code&gt;，&lt;code&gt;main.cpp&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;cmake_minimum_required(VERSION 3.10)

project(Helloworld)
# 生成 compile_commands.json
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_FLAGS &quot;${CMAKE_CXX_FLAGS} -stdlib=libc++&quot;)
# 设置链接标志
set(CMAKE_EXE_LINKER_FLAGS &quot;${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++ -lc++abi&quot;)
find_package(fmt CONFIG REQUIRED) # 指定vcpkg的查找的库

add_executable(Helloworld helloworld.cpp)

target_link_libraries(Helloworld PRIVATE fmt::fmt)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;fmt/core.h&gt;
int main()
{
    fmt::print(&quot;Hello World!\n&quot;);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编译即可&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果要下载到全局可以直接：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vcpkg install fmt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;路径在：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;xxx/vcpkg/installed/x64-linux&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果要让clangd解析到对应的头文件，需要额外配置.clangd&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;CompileFlags:
  Add:
    - &quot;-I/usr/include/c++/13&quot; #
    - &quot;-Isystem/xxx/vcpkg/installed/x64-linux/include&quot;
    - &quot;-ferror-limit=0&quot;
  Compiler: g++-13
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.3 其他配置&lt;/h3&gt;
&lt;h3&gt;8.3.1 适配clang与libc++&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果不是非使用&lt;code&gt;libc++&lt;/code&gt;库不可，就不需要下面的内容，直接&lt;code&gt;CMakePresets.json&lt;/code&gt;里面指定编译器就好了，反正&lt;code&gt;clang++&lt;/code&gt;和&lt;code&gt;libstdc++&lt;/code&gt;也能一块用&lt;/p&gt;
&lt;p&gt;vcpkg安装和主动拉取依赖默认用gcc工具链（如果有的话），编译链接也一样&lt;/p&gt;
&lt;p&gt;如果要配clang和libc++，需要修改以下几个配置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;CC，CXX：&lt;/p&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 CC=clang-20
export CXX=clang++-20
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CMakePresets.json&lt;/code&gt;，原先写在CMake中的内容，现在要放在该文件里：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;version&quot;: 2,
    &quot;configurePresets&quot;: [
        {
            &quot;name&quot;: &quot;vcpkg&quot;,
            &quot;generator&quot;: &quot;Ninja&quot;,
            &quot;binaryDir&quot;: &quot;${sourceDir}/build&quot;,
            &quot;cacheVariables&quot;: {
                &quot;VCPKG_TARGET_TRIPLET&quot;: &quot;x64-linux-clang-libcxx&quot;,
                &quot;CMAKE_BUILD_TYPE&quot;: &quot;Debug&quot;,
                &quot;CMAKE_C_COMPILER&quot;: &quot;clang-20&quot;,
                &quot;CMAKE_CXX_COMPILER&quot;: &quot;clang++-20&quot;,
                &quot;CMAKE_TOOLCHAIN_FILE&quot;: &quot;$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake&quot;
            },
            &quot;environment&quot;: {
                &quot;CC&quot;: &quot;clang-20&quot;,
                &quot;CXX&quot;: &quot;clang++-20&quot;
            }
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;xxx/vcpkg/triplets/community&lt;/code&gt;中新建&lt;code&gt;x64-linux-clang-libcxx.cmake&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;set(VCPKG_TARGET_ARCHITECTURE x64)
set(VCPKG_CRT_LINKAGE dynamic)
set(VCPKG_LIBRARY_LINKAGE static)
set(VCPKG_CMAKE_SYSTEM_NAME Linux)

# 注意：VCPKG_CXX_FLAGS 是会被传入编译器的变量
set(VCPKG_C_FLAGS &quot;${VCPKG_C_FLAGS}&quot;)
set(VCPKG_CXX_FLAGS &quot;${VCPKG_CXX_FLAGS} -stdlib=libc++&quot;)
set(VCPKG_LINKER_FLAGS &quot;${VCPKG_LINKER_FLAGS} -stdlib=libc++&quot;)
set(VCPKG_EXE_LINKER_FLAGS &quot;${VCPKG_EXE_LINKER_FLAGS} -stdlib=libc++ -lc++abi&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接用&lt;code&gt;vcpkg install&lt;/code&gt;命令默认使用&lt;code&gt;/vcpkg/triplets/x64-linux.cmake&lt;/code&gt;文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;vcpkg install fmt:x64-linux-clang-libcxx&lt;/code&gt;安装对应的包，同样的CMakeLists.txt指定链接标志，见&lt;strong&gt;9.1的解决方案&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;8.3.2 先找全局再拉依赖&lt;/h3&gt;
&lt;p&gt;案例中vcpkg的使用方式属于manifest，属于自动下载依赖到项目中，不会去vcpkg的已经安装的包里找，工作模式类似一般CMake项目中的&lt;code&gt;third_party&lt;/code&gt;文件夹，优点是版本能控制，缺点就是每次修改CMakeLists.txt重新载入的时间就比较长&lt;/p&gt;
&lt;p&gt;查了一下好像没办法在这种模式下直接的先找全局包，没找到再拉依赖&lt;/p&gt;
&lt;p&gt;问了下gemini给出的答案是：&lt;/p&gt;
&lt;p&gt;配置&lt;strong&gt;二进制缓存&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;虽然这依然会在项目下生成 &lt;code&gt;vcpkg_installed&lt;/code&gt; 目录，但它&lt;strong&gt;不会重新编译&lt;/strong&gt;代码，而是直接从缓存（或全局缓存）中拷贝编译好的二进制文件。这是 vcpkg 官方推荐的复用机制。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设置环境变量：&lt;/strong&gt; 在你的系统环境变量中添加（或确保默认已启用）：&lt;/p&gt;
&lt;p&gt;Bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VCPKG_DEFAULT_BINARY_CACHE=&amp;#x3C;你的缓存路径&gt;
# Windows 默认通常在 %LOCALAPPDATA%\vcpkg\archives
# Linux/Mac 默认通常在 $HOME/.cache/vcpkg/archives
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;效果：&lt;/strong&gt; 当你在全局 &lt;code&gt;vcpkg install xxx&lt;/code&gt; 时，编译产物会存入 Cache。 当你在项目中 build 时，vcpkg 发现 &lt;code&gt;vcpkg.json&lt;/code&gt; 需要同样的库（且版本、编译器参数一致），它会直接从 Cache 极速解压到 &lt;code&gt;vcpkg_installed&lt;/code&gt;，跳过编译过程。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;8.4 常用指令&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;list&lt;/li&gt;
&lt;li&gt;install&lt;/li&gt;
&lt;li&gt;remove&lt;/li&gt;
&lt;li&gt;search&lt;/li&gt;
&lt;li&gt;update&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;九. 常见问题&lt;/h2&gt;
&lt;h3&gt;9.1 llvm标准库头文件内报错&lt;/h3&gt;
&lt;h4&gt;问题描述&lt;/h4&gt;
&lt;p&gt;此问题针对强迫症患者，如果你点进去头文件碰到了下面的情况：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/clangd-failed-working.pNMwl4gZ_Z1mxxCD.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;那么在项目下配置&lt;code&gt;.clangd&lt;/code&gt;已经没用了&lt;/p&gt;
&lt;h4&gt;解决方案&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;~/&lt;/code&gt;目录下新增&lt;code&gt;.config/clangd/config.yaml&lt;/code&gt;文件，把&lt;code&gt;.clangd&lt;/code&gt;的内容复制过来&lt;/p&gt;
&lt;p&gt;这个全局文件的优先级比项目下&lt;code&gt;.clangd&lt;/code&gt;要高，所以配置之后项目路径下里面相同的配置会被全局的覆盖掉，所以可以在全局配置配clangd去解析哪个编译器的头文件，然后项目的&lt;code&gt;.clangd&lt;/code&gt;配置第三方库的头文件。至于为什么内容头文件会爆这么多错，搜了一堆也没找到答案，所以该方法就凑活用吧&lt;/p&gt;
&lt;h3&gt;9.2 codelldb调试无法正常显示STL内容&lt;/h3&gt;
&lt;h4&gt;问题描述&lt;/h4&gt;
&lt;p&gt;比如源码为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct node {
    unordered_map&amp;#x3C;int, int&gt; map;
};
int main() {
    node node;
    node.map.insert({1, 1});
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;return 0&lt;/code&gt;处打断点，显示的是：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/code-lldb-bug.CJFRTne2_Aqucp.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到内嵌到&lt;code&gt;struct&lt;/code&gt;或者&lt;code&gt;class&lt;/code&gt;内的STL组件可能无法正常显示&lt;/p&gt;
&lt;h4&gt;解决方案&lt;/h4&gt;
&lt;p&gt;https://github.com/vadimcn/codelldb/issues/707&lt;/p&gt;
&lt;p&gt;原因还是llvm工具链的问题，&lt;code&gt;clang++&lt;/code&gt;，&lt;code&gt;lldb&lt;/code&gt;，&lt;code&gt;libc++&lt;/code&gt;对应的是&lt;code&gt;g++&lt;/code&gt;，&lt;code&gt;gdb&lt;/code&gt;，&lt;code&gt;libstdc++&lt;/code&gt;，虽然&lt;code&gt;llvm&lt;/code&gt;支持使用&lt;code&gt;libstdc++&lt;/code&gt;做链接，但是多少有些小问题，比如性能上，再比如这个&lt;code&gt;lldb&lt;/code&gt;不能正确显示&lt;code&gt;unordered_map&lt;/code&gt;，最新的&lt;code&gt;llvm-21&lt;/code&gt;中的&lt;code&gt;lldb&lt;/code&gt;已经把这个问题修复了：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/llvm/llvm-project/pull/164251&quot;&gt;[lldb] Fix StdUnorderedMapSynthProvider for GCC #164251&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;有两种解决办法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;CMakeLists.txt&lt;/code&gt;，使用llvm组件（这些组件在llvm配置时应该就配置完成了）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# 设置编译器
set(CMAKE_CXX_COMPILER clang++-20)
# 添加编译标志 库名 libc++.so
set(CMAKE_CXX_FLAGS &quot;${CMAKE_CXX_FLAGS} -stdlib=libc++&quot;)
# 设置链接标志 库名 libc++abi.so
set(CMAKE_EXE_LINKER_FLAGS &quot;${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++ -lc++abi&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新编译，再调试：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/code-lldb-bug-fixed.BnSViBDX_omusW.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这么做会强制程序链接&lt;code&gt;libc++&lt;/code&gt;库，但是linux上一般会默认链接&lt;code&gt;libstdc++&lt;/code&gt;，因此每个cmake项目都要搞一遍，尤其是再去配vcpkg环境或者是xmake的项目（默认都是链接&lt;code&gt;libstdc++&lt;/code&gt;的），都得额外搞一套配置，不然到时候&lt;code&gt;libstdc++&lt;/code&gt;和&lt;code&gt;libc++&lt;/code&gt;链接冲突了很难搞，考虑到库切换起来很麻烦，切换编译器很简单（而且兼容），推荐还是用第二种方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改本地的lldb formatter，然后改codelldb的后端&lt;/p&gt;
&lt;p&gt;用下面的命令找&lt;code&gt;gnu_libstdcpp.py&lt;/code&gt;这个文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find /usr -name &quot;gnu_libstdcpp.py&quot; 2&gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复制一份备用，然后拉取llvm项目上最新的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 复制一份备用
sudo cp /usr/lib/llvm-20/lib/python3.10/site-packages/lldb/formatters/cpp/gnu_libstdcpp.py \
        /usr/lib/llvm-20/lib/python3.10/site-packages/lldb/formatters/cpp/gnu_libstdcpp.py.bak
# 下载github最新文件
sudo wget https://raw.githubusercontent.com/llvm/llvm-project/main/lldb/examples/synthetic/gnu_libstdcpp.py \
          -O /usr/lib/llvm-20/lib/python3.10/site-packages/lldb/formatters/cpp/gnu_libstdcpp.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vscode修改codelldb，使用本地的lldb：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl&lt;/code&gt; + &lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;P&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&gt;LLDB: Use Alternate Backend&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;输入&lt;code&gt;lldb-&amp;#x3C;version&gt;&lt;/code&gt;（下哪个版本就用哪个版本）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;codelldb自己会解析后端，完事，之后codelldb会调用本地修改后的lldb来调试，一劳永逸&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ps：在linux上使用llvm工具链还是不太方便，修改了好多配置，不想折腾还是默认使用g++和gdb比较好&lt;/p&gt;
&lt;h3&gt;9.3 clangd设置代码补全&lt;/h3&gt;
&lt;h4&gt;问题描述&lt;/h4&gt;
&lt;p&gt;clangd默认配置在代码补全的时候会把模板或函数的所有参数都补全出来，例如，当你输入了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;std::set
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并试图使用clangd提供的代码补全时，它会补全为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;std::set&amp;#x3C;class, class, class&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以使用&lt;code&gt;Tab&lt;/code&gt;一个一个填充或删除参数，但这样比较麻烦&lt;/p&gt;
&lt;h4&gt;解决方案&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;.vscode&lt;/code&gt;文件夹下新增&lt;code&gt;settings.json&lt;/code&gt;，内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;clangd.arguments&quot;: [
        &quot;--function-arg-placeholders=false&quot;,
        &quot;--completion-style=bundled&quot;
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样会影响在当前项目目录下&lt;code&gt;clangd&lt;/code&gt;的补全功能，当输入&lt;code&gt;std::set&lt;/code&gt;时，会补全为&lt;code&gt;std::set&amp;#x3C;&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;也可以直接在&lt;code&gt;clangd&lt;/code&gt;插件的设置中找到&lt;code&gt;arguments&lt;/code&gt;项，设置上面两个参数，这样会影响所有目录下的补全行为&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sysgeek.cn/ubuntu-install-gcc-compiler/&quot;&gt;如何在 Ubuntu 中安装和切换多版本 GCC 编译器 - 系统极客&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/639332690&quot;&gt;ubuntu 22.04 切 gcc/g++ 版本 - 知乎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1gPNJe6E3z/?spm_id_from=333.1387.top_right_bar_window_history.content.click&amp;#x26;vd_source=b93859dd8360e859dab85c63d1b91f9f&quot;&gt;WSL下 C++ 使用 clang19 编译器并配置 vscode 的 clangd 服务器_哔哩哔哩_bilibili&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_45896211/article/details/135310728&quot;&gt;基于VSCode+Clangd+lldb搭建Linux C++环境_vscode lldb-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/694017813&quot;&gt;ubuntu 22.04环境中cmake安装 - 知乎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.ispellbook.com/post/VSCodeCppDevConfig&quot;&gt;VSCode C/C++开发环境配置 Clang/LLVM+LLDB+CMake&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>c++20制作简易协程库3</title><link>https://liang-bk.github.io/blog/coroutine/coro_lab3</link><guid isPermaLink="true">https://liang-bk.github.io/blog/coroutine/coro_lab3</guid><pubDate>Fri, 14 Nov 2025 22:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;io_info&lt;/h2&gt;
&lt;p&gt;lab2中提到过sqe数据结构中可以传入一个数据指针，该指针会cqe中原封不动的返回，如果我们想要设置提交的IO类型，与IO绑定的协程句柄等等，使用该数据指针绑定一个结构体就会很方便：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 执行的IO类型
enum io_type
{
    nop,
    tcp_accept,
    tcp_connect,
    tcp_read,
    tcp_write,
    tcp_close,
    stdin,
    timer,
    none
};

struct io_info
{
    coroutine_handle&amp;#x3C;&gt; handle; // IO 绑定的协程句柄
    int32_t            result; // IO 执行完的结果
    io_type            type; // IO 类型
    uintptr_t          data; // IO 绑定的内存区域
    cb_type            cb; // IO 绑定的回调函数
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;io_awaiter&lt;/h2&gt;
&lt;p&gt;要想在协程中发起一个异步IO操作，需要使用&lt;code&gt;co_await&lt;/code&gt;关键字，其后跟一个&lt;code&gt;awaiter&lt;/code&gt;或&lt;code&gt;awaitable&lt;/code&gt;对象&lt;/p&gt;
&lt;p&gt;这里设置对应的&lt;code&gt;io_await&lt;/code&gt;，实现内部的&lt;code&gt;ready&lt;/code&gt;，&lt;code&gt;suspend&lt;/code&gt;，&lt;code&gt;resume&lt;/code&gt;逻辑，完成基础IO框架：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class base_io_awaiter
{
public:
    base_io_awaiter() noexcept : m_urs(coro::detail::local_engine().get_free_urs())
    {
        assert(m_urs != nullptr &amp;#x26;&amp;#x26; &quot;io submit rate is too high&quot;);
    }

    constexpr auto await_ready() noexcept -&gt; bool { return false; }

    auto await_suspend(std::coroutine_handle&amp;#x3C;&gt; handle) noexcept -&gt; void { m_info.handle = handle; }

    auto await_resume() noexcept -&gt; int32_t { return m_info.result; }

protected:
    io_info             m_info;
    coro::uring::ursptr m_urs;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;base_io_awaiter&lt;/code&gt;为所有 IO 相关的awaiter提供了一个基类并实现了 awaiter 的全部调度逻辑。当&lt;code&gt;base_io_awaiter&lt;/code&gt;被构造时会自动从当前线程绑定的 engine 获取一个sqe，当其被&lt;code&gt;co_await&lt;/code&gt;时&lt;code&gt;base_io_awaiter&lt;/code&gt;会在&lt;code&gt;await_suspend&lt;/code&gt;中记录调用协程的句柄并使该协程陷入 suspend 状态。而在 IO 完成后会通过&lt;code&gt;await_resume&lt;/code&gt;返回 IO 的执行结果。&lt;/p&gt;
&lt;h3&gt;tcp_accept_awaiter&lt;/h3&gt;
&lt;p&gt;考虑服务器要做一个异步连接的动作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class tcp_accept_awaiter : public detail::base_io_awaiter
{
public:
    tcp_accept_awaiter(int listenfd, int io_flag = 0, int sqe_flag = 0) noexcept {
        m_info.type = io_type::tcp_accept;
        m_info.cb   = &amp;#x26;tcp_accept_awaiter::callback;

        io_uring_sqe_set_flags(m_urs, sqe_flag);
        io_uring_prep_accept(m_urs, listenfd, nullptr, &amp;#x26;len, io_flag);
        io_uring_sqe_set_data(m_urs, &amp;#x26;m_info); // old uring version need set data after prep
        local_engine().add_io_submit();
    }

    static auto callback(io_info* data, int res) noexcept -&gt; void {
        data-&gt;result = res;
    	submit_to_context(data-&gt;handle);
    }

private:
    inline static socklen_t len = sizeof(sockaddr_in);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将&lt;code&gt;m_info&lt;/code&gt;的IO类型绑定为&lt;code&gt;accpet&lt;/code&gt;类型，标识这是一个连接动作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;绑定IO完成后的回调函数，该回调函数在IO执行完成后，应由engine执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto engine::handle_cqe_entry(urcptr cqe) noexcept -&gt; void
{
    auto data = reinterpret_cast&amp;#x3C;io::detail::io_info*&gt;(io_uring_cqe_get_data(cqe));
    data-&gt;cb(data, cqe-&gt;res);
    m_running_io--;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回调做两件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将IO执行的结果（成功，失败或其他信息）存回&lt;code&gt;io_info&lt;/code&gt;结构体，这样在协程恢复的时候，执行&lt;code&gt;await_resume()&lt;/code&gt;就可以获取IO执行的结果&lt;/li&gt;
&lt;li&gt;将协程再次提交回原context&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用获取的sqe，填充数据，并告知当前线程所绑定的engine，当前有IO任务需要提交执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;剩余的awaiter都大同小异，都使用对应的&lt;code&gt;io_uring_pre_*&lt;/code&gt;系列函数填充相应数据&lt;/p&gt;
&lt;p&gt;如果需要扩展IO类型，只需要在此基础之上，继承&lt;code&gt;base_io_awaiter&lt;/code&gt;，在io_uring上寻找对应的函数，填充数据即可&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>c++20制作简易协程库5</title><link>https://liang-bk.github.io/blog/coroutine/coro_lab5</link><guid isPermaLink="true">https://liang-bk.github.io/blog/coroutine/coro_lab5</guid><pubDate>Fri, 14 Nov 2025 22:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;协程高级同步组件&lt;/h2&gt;
&lt;p&gt;高级同步组件，这里指的是需要配合的各种同步组件，比如：&lt;/p&gt;
&lt;p&gt;条件变量condition_variable的实现要和互斥锁mutex配合&lt;/p&gt;
&lt;p&gt;通道channel的实现要使用互斥锁mutex和条件变量condition_variable共同实现&lt;/p&gt;
&lt;h3&gt;condition_variable&lt;/h3&gt;
&lt;p&gt;协程意义下的条件变量，与&lt;code&gt;std::condition_variable&lt;/code&gt;类似，主要api为&lt;code&gt;wait&lt;/code&gt;和&lt;code&gt;notify()&lt;/code&gt;，当在协程中调用&lt;code&gt;wait()&lt;/code&gt;函数时，应该让当前协程陷入suspend状态，而线程可以继续运行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto wait(mutex&amp;#x26; mtx) noexcept -&gt; awaiter; // 协程调用，awaiter 的 await_resume 返回 void
auto wait(mutex&amp;#x26; mtx, cond_type&amp;#x26;&amp;#x26; cond) noexcept -&gt; awaiter; // 协程调用，awaiter 的 await_resume 返回 void
auto wait(mutex&amp;#x26; mtx, cond_type&amp;#x26; cond) noexcept -&gt; awaiter; // 协程调用，awaiter 的 await_resume 返回 void
auto notify_one() noexcept -&gt; void; // 普通调用
auto notify_all() noexcept -&gt; void; // 普通调用
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wait&lt;/code&gt;： 阻塞当前协程，直到另一个协程调用同一个&lt;code&gt;condition_variable&lt;/code&gt;实例的&lt;code&gt;notify&lt;/code&gt;系方法，或者直到指定的谓词函数 (即条件) 返回 true。如果没有收到通知，即使条件为 true 也不会继续执行。如果收到通知了，但是条件不成立将仍然被阻塞，不会执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;notify_one&lt;/code&gt;：唤醒一个等待该条件变量的协程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;notify_all&lt;/code&gt;：唤醒所有等待该条件变量的线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;条件变量通常需要和互斥量mutex共同使用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;co_await cv.wait(mutex)&lt;/code&gt;：将获得的锁释放，将协程加入条件变量的等待队列，等待被唤醒，然后获取锁，继续执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;co_await cv.wait(mutex, pred)&lt;/code&gt;：语义即：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;while (!pred()) {
    co_await wait(mutex);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;notify_xxx&lt;/code&gt;：唤醒条件变量的等待队列中一个协程&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从上面的介绍可以看出，在使用wait系列的函数之前， 必须先获得mutex锁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;condition_variable cv;
mutex mtx;
int global_id;
task&amp;#x3C;&gt; func(int id) {
  auto guard = co_await mtx.lock_guard();
  co_await cv.wait(mtx, [&amp;#x26;](){id==global_id;});
  global_id+=1;
  cv.notify_all();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;实现里特别需要注意的点在于&lt;/strong&gt;：condition_variable本身会维护一个等待队列，但如果其被唤醒了，需要等锁，那么该协程应该转移到mutex的等待队列中，不再由condition_variable的等待队列负责&lt;/p&gt;
&lt;p&gt;同时，notify系列的函数，可以在不获取互斥锁的情况下被调用，也就是说，notify是可以被并行执行的，那就需要保证状态的线程安全性，因此，设计状态时，无法像mutex一样，只使用一个原子变量就能保证协程的等待与恢复是以&lt;strong&gt;FIFO&lt;/strong&gt;实现的，综合性能和实用性来考虑，这里使用自旋锁&lt;strong&gt;spinlock&lt;/strong&gt;和&lt;strong&gt;队列头尾指针&lt;/strong&gt;来作为状态（使用锁的逻辑比直接使用原子变量的逻辑更加清晰）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;状态设计：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;detail::spinlock m_spin;	// 自旋锁
cv_awaiter* m_awaiter_head{nullptr}; // 队列头指针
cv_awaiter* m_awaiter_tail{nullptr}; // 队列尾指针
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;状态变化：&lt;/p&gt;
&lt;p&gt;安全的含义是使用自旋锁来保证头尾指针读写的线程安全&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wait&lt;/code&gt;：其状态变化很简单，就是安全的将awaiter指针加入条件变量的等待队列，然后&lt;strong&gt;释放协程锁&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;带条件的&lt;code&gt;wait&lt;/code&gt;：先检查条件是否为true，如果为true了，那么不应该等待，直接恢复这个协程，否则像wait一样，安全的将awaiter指针加入条件变量的等待队列，然后&lt;strong&gt;释放协程锁&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;notify_one&lt;/code&gt;：从条件变量的等待队列中取出第一个awaiter指针，&lt;strong&gt;尝试让它获取锁&lt;/strong&gt;（即让它尝试加入协程锁的等待队列中），如果成功加入了协程锁的等待队列，那么该awaiter携带的协程将由协程锁mutex的&lt;code&gt;unlock&lt;/code&gt;负责恢复，否则（没有加入协程锁的等待队列，代表获取了锁），直接恢复&lt;/li&gt;
&lt;li&gt;&lt;code&gt;notify_all&lt;/code&gt;：首先安全的替换条件变量的队列头指针，然后一次性对所有awaiter指针做&lt;strong&gt;尝试获取锁&lt;/strong&gt;的操作即可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关于&lt;strong&gt;尝试让condition_variable的awaiter获取锁&lt;/strong&gt;的操作，可以选择将其设置为mutex的的友元类，然后通过复现mutex的awaiter对mutex的状态来将其加入mutex的等待队列&lt;/p&gt;
&lt;p&gt;另一种方式是，令condition_variable的awaiter继承自mutex的awaiter，自动复用其中的代码&lt;/p&gt;
&lt;h4&gt;cv_awaiter的实现&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct cv_awaiter : public mutex::mutex_awaiter {
    condition_variable&amp;#x26; cv;
    cond_type pred;
    using mutex_awaiter::mutex_awaiter;
    cv_awaiter(mutex&amp;#x26; mutex, condition_variable&amp;#x26; condition_variable): 
    mutex::mutex_awaiter(mutex), cv(condition_variable), pred(nullptr) {}

    cv_awaiter(mutex&amp;#x26; mutex, condition_variable&amp;#x26; condition_variable, cond_type&amp;#x26; cond): 
    mutex::mutex_awaiter(mutex), cv(condition_variable), pred(cond) {}

    cv_awaiter(mutex&amp;#x26; mutex, condition_variable&amp;#x26; condition_variable, cond_type&amp;#x26;&amp;#x26; cond): 
    mutex::mutex_awaiter(mutex), cv(condition_variable), pred(std::move(cond)) {}

    ~cv_awaiter() override {}

    auto await_ready() noexcept -&gt; bool {
        ctx.register_wait();
        return false;
    }

    auto await_suspend(std::coroutine_handle&amp;#x3C;&gt; awaiting_handle) noexcept -&gt; bool {
        handle = awaiting_handle;
        return add_waiting_list();
    }
    auto await_resume() noexcept -&gt; void {
        ctx.unregister_wait();
    }
	// cv_awaiter自己的add_waiting_list
    auto add_waiting_list() noexcept -&gt; bool {
        if (!pred || pred() == false) {
            // 挂起
            next = nullptr;
            {
                std::lock_guard&amp;#x3C;detail::spinlock&gt; lock(cv.m_spin);
                if (cv.m_awaiter_head == nullptr) {
                    cv.m_awaiter_head = cv.m_awaiter_tail = this;
                } else {
                    cv.m_awaiter_tail-&gt;next = this;
                    cv.m_awaiter_tail = this;
                }
            }
            // 解锁
            mtx.unlock();
            return true;
        }
        // 不挂起, 继续执行
        return false;
    }
    // 尝试让condition_variable的awaiter获取锁
    auto try_wake() noexcept -&gt; void {
        // 复用mutex_awaiter的add_waiting_list
        if (!mutex_awaiter::add_waiting_list()) {
            resume_coro();
        }
        // 获取失败, this会被挂载到mtx所在的链表上, 由mtx.unlock调用重载后的resume_coro
    }
    auto resume_coro() noexcept -&gt; void override {
        // resume_coro对应的handle获得了协程锁mtx
        if (!pred || pred() == false) {
            // 虚假唤醒, 加回链表并释放锁
            next = nullptr;
            {
                std::lock_guard&amp;#x3C;detail::spinlock&gt; lock(cv.m_spin);
                if (cv.m_awaiter_head == nullptr) {
                    cv.m_awaiter_head = cv.m_awaiter_tail = this;
                } else {
                    cv.m_awaiter_tail-&gt;next = this;
                    cv.m_awaiter_tail = this;
                }
            }
            mtx.unlock();
        } else {
            // 获取协程的mtx继续运行
            ctx.submit_task(handle);
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;wait和notify系列函数&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto wait(mutex&amp;#x26; mtx) noexcept -&gt; cv_awaiter { 
        return cv_awaiter(mtx, *this);
    }
    // 如何避免虚假唤醒?
    auto wait(mutex&amp;#x26; mtx, cond_type&amp;#x26;&amp;#x26; cond) noexcept -&gt; cv_awaiter {
        return cv_awaiter(mtx, *this, std::move(cond)); 
    }

    auto wait(mutex&amp;#x26; mtx, cond_type&amp;#x26; cond) noexcept -&gt; cv_awaiter {
        return cv_awaiter(mtx, *this, cond); 
    }

    auto notify_one() noexcept -&gt; void {
        cv_awaiter* awaiter = nullptr;
        {
            std::lock_guard&amp;#x3C;detail::spinlock&gt; lock(m_spin);
            if (m_awaiter_head == nullptr) {
                return;
            }
            awaiter = m_awaiter_head;
            m_awaiter_head = static_cast&amp;#x3C;cv_awaiter*&gt;(m_awaiter_head-&gt;next);
        }
        awaiter-&gt;try_wake();
    };

    auto notify_all() noexcept -&gt; void {
        cv_awaiter* awaiter = nullptr;
        {
            std::lock_guard&amp;#x3C;detail::spinlock&gt; lock(m_spin);
            if (m_awaiter_head == nullptr) {
                return;
            }
            awaiter = m_awaiter_head;
            m_awaiter_head = nullptr;
        }
        while (awaiter) {
            cv_awaiter* temp = static_cast&amp;#x3C;cv_awaiter*&gt;(awaiter-&gt;next);
            awaiter-&gt;try_wake();
            awaiter = temp;
        }
    };
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;channel&lt;/h3&gt;
&lt;p&gt;channel是一个使用协程锁mutex和协程条件变量的多生产者多消费者的阻塞队列&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>c++20制作简易协程库1</title><link>https://liang-bk.github.io/blog/coroutine/coro_lab1</link><guid isPermaLink="true">https://liang-bk.github.io/blog/coroutine/coro_lab1</guid><pubDate>Fri, 14 Nov 2025 22:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;task类&lt;/h2&gt;
&lt;p&gt;c++20中的协程要求用户自定义一个&lt;code&gt;task&lt;/code&gt;类作为返回值&lt;/p&gt;
&lt;p&gt;同时为&lt;code&gt;task&lt;/code&gt;类必须绑定&lt;code&gt;promise_type&lt;/code&gt;类型&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;promise_type&lt;/code&gt;类型，可以理解为一个官方要求实现的接口，包含以下函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 创建任务对象
auto get_return_object() noexcept -&gt; task;
// 协程创建时的调度
auto initial_suspend() noexcept -&gt; std::suspend_always;
// 协程返回时的调度
auto final_suspend() noexcept -&gt; std::suspend_never;
// 协程函数无返回值需使用
auto return_void() noexcept -&gt; void;
// 协程函数有返回值需使用
// auto return_value(T val) noexcept -&gt; void;
// 协程执行过程中出现异常
auto unhandled_exception() noexcept -&gt; void;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;promise_base&lt;/h3&gt;
&lt;p&gt;接口函数针对协程的返回值有两种不同的函数：&lt;code&gt;return_xxx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;并且这两种函数不能同时存在，所以对于协程有返回值和无返回值（&lt;code&gt;void&lt;/code&gt;），至少需要构建两种不同的&lt;code&gt;promise_type&lt;/code&gt;，于是可以将共性部分抽取到&lt;code&gt;promise_base&lt;/code&gt;类中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;协程创建时的调度：&lt;code&gt;initial_suspend()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;协程返回时的调度：&lt;code&gt;final_suspend()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;c++协程是非对称协程，这意味着协程函数之间存在调用关系，当协程因为调度而暂停时，执行权应返回给调用该协程的一方，这么说比较抽象，以入门案例中的最后一个程序为例：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;main&lt;/code&gt;中执行了&lt;code&gt;add(1, 2, 1)&lt;/code&gt;，&lt;/p&gt;
&lt;p&gt;&lt;code&gt;add(1, 2, 1)&lt;/code&gt;中又执行了&lt;code&gt;add(2, 30, 2)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;期望的返回顺序应该像普通函数调用那样&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;add(2, 30, 2)&lt;/code&gt;执行完毕，返回&lt;code&gt;add(1, 2, 1)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;add(1, 2, 1)&lt;/code&gt;执行完毕，返回&lt;code&gt;main&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但实际上当时的程序没有按照预想的情况进行执行&lt;/p&gt;
&lt;p&gt;协程创建和返回时的调度应该满足以上期望的返回顺序，这就需要改造&lt;code&gt;promise_base&lt;/code&gt;中的调度函数：&lt;/p&gt;
&lt;h4&gt;initial_suspend&lt;/h4&gt;
&lt;p&gt;对于一个协程函数来说，不希望其在创建时能够直接执行，而是在&lt;code&gt;initial_suspend&lt;/code&gt;后暂停执行，这样协程函数会返回对应的&lt;code&gt;task&lt;/code&gt;对象&lt;/p&gt;
&lt;p&gt;因此返回&lt;code&gt;suspend_always{}&lt;/code&gt;即可&lt;/p&gt;
&lt;h4&gt;final_suspend&lt;/h4&gt;
&lt;p&gt;该调度点在执行&lt;code&gt;co_return&lt;/code&gt;后，期望的情况是当某个协程函数返回时，执行权应该交给调度它的协程函数，就像普通函数调用那样，这要求保存协程的调用链，具体做法就是在&lt;code&gt;promise&lt;/code&gt;类中设置一个&lt;code&gt;std::coroutine_handle&amp;#x3C;&gt; caller&lt;/code&gt;变量，当执行&lt;code&gt;final_suspend&lt;/code&gt;调度时，将执行权交给&lt;code&gt;caller&lt;/code&gt;即可，这要求自定义&lt;code&gt;final_suspend()&lt;/code&gt;的返回值&lt;code&gt;final_awaiter&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;code&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;介绍一下代码中的&lt;code&gt;std::coroutine_handle&amp;#x3C;&gt; await_suspend(std::coroutine_handle&amp;#x3C;promise_type&gt; handle) noexcept&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;co_return&lt;/code&gt;时，会进入定义的&lt;code&gt;final_awaiter&lt;/code&gt;的&lt;code&gt;ready&lt;/code&gt;-&lt;code&gt;suspend&lt;/code&gt;-&lt;code&gt;resume&lt;/code&gt;三项流程&lt;/p&gt;
&lt;p&gt;&lt;code&gt;await_suspend&lt;/code&gt;的入参是当前执行&lt;code&gt;co_return&lt;/code&gt;的协程句柄，返回值是当前协程句柄的调用者的句柄&lt;/p&gt;
&lt;p&gt;如果没有调用者，会返回一个&lt;code&gt;std::noop_coroutine()&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;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct promise_base
{
    struct final_awaiter {
        bool await_ready() noexcept {return false;}
        template&amp;#x3C;typename promise_type&gt;
        auto await_suspend(std::coroutine_handle&amp;#x3C;promise_type&gt; handle) noexcept -&gt; std::coroutine_handle&amp;#x3C;&gt; {
            auto&amp;#x26; promise = handle.promise();
            return promise.caller_ != nullptr ? promise.caller_ : std::noop_coroutine();
        }
        void await_resume() noexcept {}
    };
    friend final_awaiter;

    promise_base() noexcept {}
    ~promise_base() {}

    auto initial_suspend() noexcept {
        return std::suspend_always{}; 
    }

    [[CORO_TEST_USED(lab1)]] auto final_suspend() noexcept -&gt; final_awaiter
    {
        return final_awaiter{};
    }
    void set_caller(std::coroutine_handle&amp;#x3C;&gt; caller) {
        caller_ = caller;
    }
    
    void set_detached() {
        state_ = task_state::DETACHED;
    }

    bool is_detached() {
        return state_ == task_state::DETACHED;
    }
protected:
    std::coroutine_handle&amp;#x3C;&gt; caller_{nullptr};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;特化promise&amp;#x3C;&gt;&lt;/h3&gt;
&lt;p&gt;为了处理协程函数中各种返回类型，需要使用模板类型来保存返回结果，但是函数可能返回&lt;code&gt;void&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;当协程函数返回void时，不应该使用&lt;code&gt;promise&lt;/code&gt;，因为里面已经定义了&lt;code&gt;return_value&lt;/code&gt;函数，因此需要特化一个模板，专门处理没有返回值的情况&lt;/p&gt;
&lt;p&gt;这个类相对简单，只需要定义一个&lt;code&gt;void return_void()&lt;/code&gt;函数，并附带上记录异常的功能即可&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;code&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;template&amp;#x3C;&gt;
struct promise&amp;#x3C;void&gt; : public promise_base
{
    using task_type        = task&amp;#x3C;void&gt;;
    using coroutine_handle = std::coroutine_handle&amp;#x3C;promise&amp;#x3C;void&gt;&gt;;

    promise() noexcept                  = default;
    promise(const promise&amp;#x26;)             = delete;
    promise(promise&amp;#x26;&amp;#x26; other)            = delete;
    promise&amp;#x26; operator=(const promise&amp;#x26;)  = delete;
    promise&amp;#x26; operator=(promise&amp;#x26;&amp;#x26; other) = delete;
    ~promise()                          = default;

    auto get_return_object() noexcept -&gt; task_type;

    constexpr auto return_void() noexcept -&gt; void
    {
    }

    auto unhandled_exception() noexcept -&gt; void
    {
        m_exception_ptr = std::current_exception();
    }
	// result函数负责获取协程返回值, 由于task&amp;#x3C;void&gt;类型没有返回值，这里只需要抛出协程执行过程中的异常
    auto result() -&gt; void
    {
        if (m_exception_ptr)
        {
            std::rethrow_exception(m_exception_ptr);
        }
    }

private:
    std::exception_ptr m_exception_ptr{nullptr};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;promise&lt;/h3&gt;
&lt;h4&gt;container容器&lt;/h4&gt;
&lt;p&gt;为了保存协程函数的返回值，需要定义一个容器，容器有三种状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unset_return_value&lt;/code&gt;：一个内部定义的空结构体，仅用于表示“未设置值”的初始状态。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;store_type&lt;/code&gt;：存储协程的返回值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::exception_ptr&lt;/code&gt;：存储从协程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里直接将三种状态放在一个&lt;code&gt;union&lt;/code&gt;中，使用&lt;code&gt;std::variant&lt;/code&gt;来保存（更安全的&lt;code&gt;union&lt;/code&gt;结构，这意味着任何时刻变量中只能是三者之一）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct unset_return_value
{
    unset_return_value() {}
    unset_return_value(unset_return_value&amp;#x26;&amp;#x26;)      = delete;
    unset_return_value(const unset_return_value&amp;#x26;) = delete;
    auto operator=(unset_return_value&amp;#x26;&amp;#x26;)          = delete;
    auto operator=(const unset_return_value&amp;#x26;)     = delete;
};
static constexpr bool return_type_is_reference = std::is_reference_v&amp;#x3C;T&gt;;
using stored_type =
    std::conditional_t&amp;#x3C;return_type_is_reference, std::remove_reference_t&amp;#x3C;T&gt;*, std::remove_const_t&amp;#x3C;T&gt;&gt;;
using variant_type = std::variant&amp;#x3C;unset_return_value, stored_type, std::exception_ptr&gt;;
// 保存用户通过co_return返回的数据
variant_type m_storage{};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::is_reference_v&lt;/code&gt;：这是一个编译期常量，用于判断模板参数T是否是一个引用类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::conditional_t&lt;/code&gt;：这是一个编译期的 &lt;code&gt;if-else&lt;/code&gt;。它根据第一个布尔参数，从后面两个类型中选择一个作为最终类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;store_type&lt;/code&gt;表示如果T是一个引用，就将存储对应的指针类型（variant不能直接存储引用），否则存储原始类型（去除const）&lt;/p&gt;
&lt;h4&gt;return_value&lt;/h4&gt;
&lt;p&gt;当协程函数&lt;code&gt;co_return val;&lt;/code&gt;时触发，此时应将返回值保存到容器中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;template&amp;#x3C;typename value_type&gt;
        requires(return_type_is_reference and std::is_constructible_v&amp;#x3C;T, value_type &amp;#x26;&amp;#x26;&gt;) or
                (not return_type_is_reference and std::is_constructible_v&amp;#x3C;stored_type, value_type &amp;#x26;&amp;#x26;&gt;)
auto return_value(value_type&amp;#x26;&amp;#x26; value) -&gt; void
{
    if constexpr (return_type_is_reference)
    {
        T ref = static_cast&amp;#x3C;value_type&amp;#x26;&amp;#x26;&gt;(value);
        m_storage.template emplace&amp;#x3C;stored_type&gt;(std::addressof(ref));
    }
    else
    {
        m_storage.template emplace&amp;#x3C;stored_type&gt;(std::forward&amp;#x3C;value_type&gt;(value));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么不使用类模板T而是新增一个函数模板value_type？&lt;/p&gt;
&lt;p&gt;这里实际上使用了一个万能引用（&lt;code&gt;value_type&amp;#x26;&amp;#x26;&lt;/code&gt;）来避免不必要的拷贝，并通过&lt;code&gt;requires&lt;/code&gt;子句确保&lt;code&gt;co_return&lt;/code&gt;的表达式可以被用来构造上面定义好的&lt;code&gt;store_type&lt;/code&gt;类型&lt;/p&gt;
&lt;p&gt;根据模板T是否是引用类型（比如定义的&lt;code&gt;task&amp;#x3C;int&amp;#x26;&gt;&lt;/code&gt;，T就是&lt;code&gt;int&amp;#x26;&lt;/code&gt;），就获取传入的值的地址，再由&lt;code&gt;m_storage&lt;/code&gt;中的指针来接收，否则利用完美转发，将值传给&lt;code&gt;m_storage&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;result&lt;/h4&gt;
&lt;p&gt;获取协程返回值，&lt;code&gt;return_value&lt;/code&gt;将返回值存在容器中，但要获取它还需要主动调用&lt;code&gt;promise&lt;/code&gt;对象的&lt;code&gt;result&lt;/code&gt;方法，此时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;m_storage&lt;/code&gt;是&lt;code&gt;unset_return_value&lt;/code&gt;，报运行时错误&lt;/li&gt;
&lt;li&gt;&lt;code&gt;m_storage&lt;/code&gt;是&lt;code&gt;std::exception_ptr&lt;/code&gt;，重新抛出对应的异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;m_storage&lt;/code&gt;是&lt;code&gt;stored_type&lt;/code&gt;，返回对应的值&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto result() &amp;#x26; -&gt; decltype(auto)
{
    if (std::holds_alternative&amp;#x3C;stored_type&gt;(m_storage))
    {
        if constexpr (return_type_is_reference)
        {
            return static_cast&amp;#x3C;T&gt;(*std::get&amp;#x3C;stored_type&gt;(m_storage));
        }
        else
        {
            return static_cast&amp;#x3C;const T&amp;#x26;&gt;(std::get&amp;#x3C;stored_type&gt;(m_storage));
        }
    }
    else if (std::holds_alternative&amp;#x3C;std::exception_ptr&gt;(m_storage))
    {
        std::rethrow_exception(std::get&amp;#x3C;std::exception_ptr&gt;(m_storage));
    }
    else
    {
        throw std::runtime_error{&quot;The return value was never set, did you execute the coroutine?&quot;};
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;task&lt;/h3&gt;
&lt;p&gt;与&lt;code&gt;promise&lt;/code&gt;对应，为了表达返回值，需要定义为模板类，这样在定义协程函数时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;task&amp;#x3C;void&gt; func1() {
    ...
}
task&amp;#x3C;int&gt; func2() {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;task&lt;/code&gt;类持有一个协程句柄，指向当前与&lt;code&gt;task&lt;/code&gt;关联的协程帧&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;为了让&lt;code&gt;promise&lt;/code&gt;能够构造&lt;code&gt;task&lt;/code&gt;对象，其需要定义一个从协程句柄转化而来的有参构造函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;task&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;/li&gt;
&lt;li&gt;
&lt;p&gt;封装协程句柄的&lt;code&gt;resume&lt;/code&gt;和&lt;code&gt;destroy&lt;/code&gt;函数，对外提供操作协程的函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提供获取&lt;code&gt;promise&lt;/code&gt;对象和协程句柄的函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;template&amp;#x3C;typename return_type&gt;
class task
{
public:
    using task_type        = task&amp;#x3C;return_type&gt;;
    using promise_type     = detail::promise&amp;#x3C;return_type&gt;;
    using coroutine_handle = std::coroutine_handle&amp;#x3C;promise_type&gt;;

    task() noexcept : m_coroutine(nullptr) {}

    explicit task(coroutine_handle handle) : m_coroutine(handle) {}
    task(const task&amp;#x26;) = delete;
    task(task&amp;#x26;&amp;#x26; other) noexcept : m_coroutine(std::exchange(other.m_coroutine, nullptr)) {}

    ~task()
    {
        if (m_coroutine != nullptr)
        {
            m_coroutine.destroy();
        }
    }

    auto operator=(const task&amp;#x26;) -&gt; task&amp;#x26; = delete;

    auto operator=(task&amp;#x26;&amp;#x26; other) noexcept -&gt; task&amp;#x26;
    {
        if (std::addressof(other) != this)
        {
            if (m_coroutine != nullptr)
            {
                m_coroutine.destroy();
            }

            m_coroutine = std::exchange(other.m_coroutine, nullptr);
        }

        return *this;
    }

    /**
     * @return True if the task is in its final suspend or if the task has been destroyed.
     */
    auto is_ready() const noexcept -&gt; bool { return m_coroutine == nullptr || m_coroutine.done(); }

    auto resume() -&gt; bool
    {
        if (!m_coroutine.done())
        {
            m_coroutine.resume();
        }
        return !m_coroutine.done();
    }

    auto destroy() -&gt; bool
    {
        if (m_coroutine != nullptr)
        {
            m_coroutine.destroy();
            m_coroutine = nullptr;
            return true;
        }

        return false;
    }

    auto promise() &amp;#x26; -&gt; promise_type&amp;#x26; { return m_coroutine.promise(); }
    auto promise() const&amp;#x26; -&gt; const promise_type&amp;#x26; { return m_coroutine.promise(); }
    auto promise() &amp;#x26;&amp;#x26; -&gt; promise_type&amp;#x26;&amp;#x26; { return std::move(m_coroutine.promise()); }

    auto handle() &amp;#x26; -&gt; coroutine_handle { return m_coroutine; }
    auto handle() &amp;#x26;&amp;#x26; -&gt; coroutine_handle { return std::exchange(m_coroutine, nullptr); }
private:
    coroutine_handle m_coroutine{nullptr};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最重要的是重载&lt;code&gt;co_await&lt;/code&gt;运算符，以支持&lt;code&gt;co_await&lt;/code&gt;语义实现协程嵌套执行和正确的调用返回&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;co_await func();&lt;/code&gt;的语义，其执行流程是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;auto task_func = func();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;触发&lt;code&gt;promise.initial_suspend()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;co_await task_func;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;触发&lt;code&gt;task_func&lt;/code&gt;对象中的&lt;code&gt;co_await&lt;/code&gt;运算符重载&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以该重载应该返回一个&lt;code&gt;awaiter&lt;/code&gt;来实行调度，期望的调度是：将执行权由调用协程交给被调用的协程，在&lt;code&gt;suspend&lt;/code&gt;中就需要返回被调用协程的句柄&lt;/p&gt;
&lt;p&gt;同时为了在被调用协程返回后正确的回到调用协程的位置，需要设置&lt;code&gt;promise_base&lt;/code&gt;的&lt;code&gt;caller&lt;/code&gt;成员变量保存调用协程的句柄，这样在被调用协程执行&lt;code&gt;final_suspend&lt;/code&gt;时正确的转移执行权&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct awaitable_base
{
    awaitable_base(coroutine_handle coroutine) noexcept : m_coroutine(coroutine) {}

    auto await_ready() const noexcept -&gt; bool { return !m_coroutine || m_coroutine.done(); }

    auto await_suspend(std::coroutine_handle&amp;#x3C;&gt; awaiting_coroutine) noexcept -&gt; std::coroutine_handle&amp;#x3C;&gt;
    {
        // TODO[lab1]: Add you codes
        auto&amp;#x26; m_promise = m_coroutine.promise();
        m_promise.set_caller(awaiting_coroutine);
        return m_coroutine;
    }

    std::coroutine_handle&amp;#x3C;promise_type&gt; m_coroutine{nullptr};
};

auto operator co_await() const&amp;#x26; noexcept
{
    struct awaitable : public awaitable_base
    {
		// 子协程co_return -&gt; 子协程调用result_value/result_void -&gt; 子协程调用final_suspend()
        // -&gt; 执行权转移给父协程 -&gt; 父协程调用await_resume获取返回值
        auto await_resume() -&gt; decltype(auto) { return this-&gt;m_coroutine.promise().result(); }
    };

    return awaitable{m_coroutine};
}

auto operator co_await() const&amp;#x26;&amp;#x26; noexcept
{
    struct awaitable : public awaitable_base
    {
        auto await_resume() -&gt; decltype(auto) { return std::move(this-&gt;m_coroutine.promise()).result(); }
    };

    return awaitable{m_coroutine};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;通过promise构造task&lt;/h4&gt;
&lt;p&gt;即实现&lt;code&gt;promise&lt;/code&gt;的&lt;code&gt;get_return_object()&lt;/code&gt;函数，只需要调用&lt;code&gt;task&lt;/code&gt;的有参构造函数，返回&lt;code&gt;task&lt;/code&gt;对象即可：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;template&amp;#x3C;typename return_type&gt;
inline auto promise&amp;#x3C;return_type&gt;::get_return_object() noexcept -&gt; task&amp;#x3C;return_type&gt;
{
    return task&amp;#x3C;return_type&gt;{coroutine_handle::from_promise(*this)};
}

inline auto promise&amp;#x3C;void&gt;::get_return_object() noexcept -&gt; task&amp;#x3C;&gt;
{
    return task&amp;#x3C;&gt;{coroutine_handle::from_promise(*this)};
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>c++20制作简易协程库4</title><link>https://liang-bk.github.io/blog/coroutine/coro_lab4</link><guid isPermaLink="true">https://liang-bk.github.io/blog/coroutine/coro_lab4</guid><pubDate>Fri, 14 Nov 2025 22:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;协程同步组件&lt;/h2&gt;
&lt;p&gt;使用scheduler多线程执行协程任务时，仍会面临与多线程任务相同的问题：共同读写某个变量，常用的手段是使用线程同步组件如&lt;code&gt;mutex&lt;/code&gt;，&lt;code&gt;condition_variable&lt;/code&gt;来确保多线程下对共享变量操作的正确性&lt;/p&gt;
&lt;p&gt;但线程同步组件会阻塞整个线程，如果协程任务使用了线程同步组件，会使整个线程陷入阻塞态，从而线程上其他的协程任务无法继续执行，那么使用协程带来的优势就将不存在&lt;/p&gt;
&lt;p&gt;我们期望设计协程相关的同步组件，当一个协程使用对应的同步组件被阻塞时，只将自身陷入到&lt;code&gt;suspend&lt;/code&gt;状态中，而不会将整个线程阻塞，使得同一个线程上其他能够运行的协程任务仍然能够继续运行，当同步操作完成时，再由同步组件唤醒，重新将协程提交到对应的context任务队列中，恢复原先的运行&lt;/p&gt;
&lt;h3&gt;event&lt;/h3&gt;
&lt;p&gt;event的功能与&lt;code&gt;std::promise&lt;/code&gt;类似，带有一个返回类型模板参数且默认为空，并且提供&lt;code&gt;set&lt;/code&gt;以及&lt;code&gt;wait&lt;/code&gt;两个接口，&lt;code&gt;set&lt;/code&gt;可以直接调用，但调用&lt;code&gt;wait&lt;/code&gt;需要是协程的形式&lt;code&gt;co_await wait()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;event被设定为模板类，方便&lt;code&gt;set&lt;/code&gt;函数传值&lt;/p&gt;
&lt;p&gt;对于模板参数为空的event，功能即传递信号：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto set() noexcept -&gt; void; // 普通调用
auto wait() noexcept -&gt; awaiter; // 协程调用，awaiter 的 await_resume 返回 void
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不为空，传信号的同时传递对应的值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;template&amp;#x3C;typename value_type&gt;
auto set(value_type&amp;#x26;&amp;#x26; value) noexcept -&gt; void; // 普通调用
auto wait() noexcept -&gt; awaiter; // 协程调用，awaiter 的 await_resume 返回 set 设置的值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;event&amp;#x3C;int&gt; ev;
task&amp;#x3C;&gt; set_func() {
  // codes...
  ev.set(number);
}
task&amp;#x3C;&gt; wait_func() {
  auto number = co_await ev.wait();
  // codes ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;锁&lt;/h4&gt;
&lt;p&gt;由于不同协程可能会运行在不同的线程上，因此event的设计要实现线程安全，使用&lt;code&gt;mutex&lt;/code&gt;锁可以轻松的做到这一点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;std::mutex m_mux;
std::condition_variable m_cv;
size_t m_waited{0};		// 等待陷入suspend状态的协程
bool status{false};		// 当前event有没有被set
std::queue&amp;#x3C;base_awaiter*&gt; awaiters;	// 保存陷入suspend状态的协程队列
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;struct base_awaiter&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;当一个协程执行&lt;code&gt;co_await ev.wait();&lt;/code&gt;时，执行调度流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果event已经处于set的状态，协程就不应该陷入&lt;code&gt;suspend&lt;/code&gt;状态，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;否则，协程将自己陷入&lt;code&gt;suspend&lt;/code&gt;状态，并将当前的awaiter对象放入待恢复的队列中（&lt;code&gt;awaiters&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct base_awaiter {
    friend class event_base;
    // 构造: 绑定对应的event对象和context对象, 以便后面恢复协程
    base_awaiter(event_base&amp;#x26; event) noexcept : m_event(event), m_ctx(local_context()) {}
    auto await_ready() noexcept -&gt; bool {
        m_ctx.unregister_wait();
        // 对event对象的互斥量上锁
        std::lock_guard&amp;#x3C;std::mutex&gt; lock(m_event.m_mux);
        // 如果event已经被set了, 执行co_await的协程不应该陷入suspend, 而是继续运行
        if (m_event.status) {
            return true;
        }
        // 否则将awaiter添加要恢复队列中
        m_event.awaiters.push(this);
        // m_waited代表当前还有多少个加入恢复队列的协程没有进入suspend状态
        m_event.m_waited += 1;
        return false;
    }
    auto await_suspend(std::coroutine_handle&amp;#x3C;&gt; handle) noexcept -&gt; bool {
        // 保存要恢复的协程句柄
        m_handle = handle;
        std::unique_lock&amp;#x3C;std::mutex&gt; lock(m_event.m_mux);
        m_event.m_waited -= 1;
        // 如果m_waited为0, 说明所有加入队列的awaiter都绑定好了要恢复的协程句柄
        // 这样event对象在set的时候就不会恢复一个没有绑定协程句柄的awaiter
        if (m_event.m_waited == 0) {
            m_event.m_cv.notify_all();
        }
        return true;
    }
    auto await_resume() noexcept -&gt; void{
        m_ctx.unregister_wait();
    }
protected:
    event_base&amp;#x26; m_event;	// 当前awaiter绑定的event对象
    context&amp;#x26; m_ctx;			// 协程所在的context对象
    std::coroutine_handle&amp;#x3C;&gt; m_handle{nullptr};	// 保存陷入suspend状态的协程句柄
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;class event_base&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class event_base {
public:
    friend struct base_awaiter;
public:
    event_base() noexcept {};
    ~event_base() noexcept {}

protected:
    // set时, 恢复所有的协程
    void resume_awaiters() noexcept {
        std::queue&amp;#x3C;base_awaiter*&gt; to_resume;
        {
            std::unique_lock&amp;#x3C;std::mutex&gt; lock(m_mux);
            m_cv.wait(lock, [this]() {
                return m_waited == 0;
            });
            // 记录当前event已经被set了
            status = true;
            awaiters.swap(to_resume);
        }
        // 将所有的协程句柄提交回绑定的context
        while (!to_resume.empty()) {
            auto resume_a = to_resume.front();
            to_resume.pop();
            resume_a-&gt;m_ctx.submit_task(resume_a-&gt;m_handle);
        }
    }
protected:
    std::mutex m_mux;
    std::condition_variable m_cv;
    size_t m_waited{0};
    bool status{false};
    std::queue&amp;#x3C;base_awaiter*&gt; awaiters;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;无锁&lt;/h4&gt;
&lt;p&gt;锁的功能很强大，但涉及到底层系统调用，开销大，同时会锁住整个线程，不利于高并发&lt;/p&gt;
&lt;p&gt;因此考虑使用非阻塞式的&lt;strong&gt;原子操作&lt;/strong&gt;、**CAS（Compare-and-Swap）**来实现对共享数据的访问和修改，其可以降低系统调用的次数，但是设计上更加复杂，需要考虑线程安全，内存模型等因素&lt;/p&gt;
&lt;p&gt;初次接触无锁编程，仅考虑线程安全，不去考虑内存模型（统一使用最严格的内存序）&lt;/p&gt;
&lt;p&gt;无锁编程通常使用原子变量来保存共享数据，对于event来说，需要保存的最关键的数据就是awaiter队列（这里可以用链表实现）&lt;/p&gt;
&lt;p&gt;那么对于该原子变量来说可能有三个状态：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;没有被set过，并且当前链表长度为0（&lt;strong&gt;kUNSETNOAWAITER&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;没有被set过，并且当前链表长度为n（保存指向链表头的指针）&lt;/li&gt;
&lt;li&gt;已经被set（&lt;strong&gt;kSET&lt;/strong&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为要保存指针类型，使用&lt;code&gt;atomic&amp;#x3C;void&gt; m_state&lt;/code&gt;来保存状态，其初始状态应为&lt;strong&gt;kUNSETNOAWAITER&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;状态变化：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;event::set()&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;该函数会将&lt;code&gt;m_state&lt;/code&gt;强制转为&lt;strong&gt;kSET&lt;/strong&gt;状态，并将&lt;code&gt;m_state&lt;/code&gt;原先存的状态取出，如果状态是指向链表头的指针，就恢复协程的运行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;event::wait()&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;该函数的功能是负责等待对应的event被set，关键步骤在&lt;code&gt;await_suspend()&lt;/code&gt;中：&lt;/p&gt;
&lt;p&gt;将自身挂载到链表上，并让协程陷入阻塞态，但直接修改状态时会出现一些竞争状态，即在该函数执行时，可能有以下情况发生：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态被修改为了&lt;strong&gt;kSET&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;其他协程也在执行&lt;code&gt;awaiter::await_suspend()&lt;/code&gt;，此时两者都想改变状态，将自身添加到链表头部&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于竞争状态，需要手动进行&lt;strong&gt;循环CAS&lt;/strong&gt;，即“如果值没被别人改过，那就更新成功；如果别人先一步改了，我就失败并重试。”&lt;/p&gt;
&lt;p&gt;循环CAS的核心是失败后的重试，这要求成功之前都必须处在一个循环过程中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;while (true) {
    // 当前已经被set了, 就退出
    if (m_state == kSET) {
        break;
    }
    // 原子变量的CAS操作, 尝试挂载自身到链表中
	if (try_add_to_list(this, m_state)) {
        // 成功了, 退出循环
        break;
    }
    // 失败了, 循环重试
}
&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;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;c++中的原子变量提供了各种CAS操作，常见的有：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// expected: 期望的原子变量的状态
// desired: 替换为的状态
// 语义: 如果当前原子变量的状态为expected, 就尝试替换为desired, 替换成功返回true, 否则返回false
// 如果返回false, expected会被改为当前原子变量的新值
bool compare_exchange_strong(T&amp;#x26; expected, T desired,
                             memory_order success_order,
                             memory_order failure_order) noexcept;

bool compare_exchange_weak(T&amp;#x26; expected, T desired,
                           memory_order success_order,
                           memory_order failure_order) noexcept;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的&lt;code&gt;memory_order&lt;/code&gt;是内存序，默认为&lt;code&gt;memory_order_seq_cst&lt;/code&gt;，也就是保证线程安全的一档，内存序太过复杂，这里不多涉及，使用默认就好&lt;/p&gt;
&lt;p&gt;&lt;code&gt;strong&lt;/code&gt;和&lt;code&gt;weak&lt;/code&gt;的区别在于&lt;code&gt;weak&lt;/code&gt;可能会假失败（即使原子变量中存的是&lt;code&gt;expected&lt;/code&gt;值也有可能替换失败返回false），但是执行速度会快一些，一般在循环中使用&lt;code&gt;weak&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;// 内存序默认memory_order_seq_cst
// 原子读
T load(memory_order order);
// 原子写
void store(T val, memory_order order);
// 原子交换, 返回原子变量中原先存的值
T exchange(T val, memory_order order);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么不直接用&lt;code&gt;exchange()&lt;/code&gt;之类的函数？&lt;/p&gt;
&lt;p&gt;考虑这么一种情况：协程1执行到了&lt;code&gt;set()&lt;/code&gt;，协程2执行到了&lt;code&gt;await::suspend()&lt;/code&gt;，协程2检查当前状态不为&lt;strong&gt;kSET&lt;/strong&gt;，但在执行&lt;code&gt;exchange()&lt;/code&gt;之前，协程1执行了CAS并把状态修改为了&lt;strong&gt;kSET&lt;/strong&gt;，此时协程2的行为应该是退出&lt;code&gt;await::suspend()&lt;/code&gt;继续运行，但实际上会执行&lt;code&gt;exchange()&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;class event_base {
public:
    struct base_awaiter {
        friend class event_base;
        base_awaiter(event_base&amp;#x26; event) noexcept : m_event(event), m_ctx(local_context()) {}
        auto await_ready() noexcept -&gt; bool {
            m_ctx.register_wait();
            return m_event.is_set();
        }
        auto await_suspend(std::coroutine_handle&amp;#x3C;&gt; handle) noexcept -&gt; bool {
            m_handle = handle;
            void *old_state = m_event.m_state.load(std::memory_order_acquire);
            while (true) {
                // 竞争情况: 准备队列时, event被set了
                if (old_state == reinterpret_cast&amp;#x3C;void*&gt;(kSet)) {
                    return false;
                }
                // 正常情况: 把自己加入等待者链表
                m_next = (old_state == kUnsetNowaiter) ? nullptr : static_cast&amp;#x3C;base_awaiter*&gt;(old_state);
                if (m_event.m_state.compare_exchange_strong(
                    old_state, 
                    this,
                    std::memory_order_acq_rel,
                    std::memory_order_relaxed
                )) {
                    return true;
                }
            }
        }
        auto await_resume() noexcept -&gt; void{
            m_ctx.unregister_wait();
        }
    protected:
        event_base&amp;#x26; m_event;
        context&amp;#x26; m_ctx;
        std::coroutine_handle&amp;#x3C;&gt; m_handle{nullptr};
        base_awaiter* m_next{nullptr};
    };
    friend struct base_awaiter;
public:
    event_base() noexcept : m_state(kUnsetNowaiter) {};
    ~event_base() noexcept {}

    auto is_set() const noexcept -&gt; bool {
        return m_state.load(std::memory_order_acquire) == reinterpret_cast&amp;#x3C;void*&gt;(kSet);
    }

    auto reset() -&gt; void {
        void *expected_state = reinterpret_cast&amp;#x3C;void*&gt;(kSet);
        m_state.compare_exchange_strong(expected_state, kUnsetNowaiter, std::memory_order_acq_rel);
    }

protected:
    void resume_awaiters() noexcept {
        void* old_state = m_state.exchange(reinterpret_cast&amp;#x3C;void*&gt;(kSet), std::memory_order_acq_rel);
        if (old_state != reinterpret_cast&amp;#x3C;void*&gt;(kSet)) {
            // 链表挂载等待的协程, 唤醒
            // base_awaiter* awaiter = static_cast&amp;#x3C;base_awaiter*&gt;(old_state);
            base_awaiter* head = static_cast&amp;#x3C;base_awaiter*&gt;(old_state);
            while (head != nullptr) {
                auto* next_awaiter = head-&gt;m_next;
                head-&gt;m_ctx.submit_task(head-&gt;m_handle);
                head = next_awaiter;
            }
        }
    }
protected:
    static constexpr void* kUnsetNowaiter = nullptr;
    static constexpr std::uintptr_t kSet = 1;
    std::atomic&amp;#x3C;void*&gt; m_state;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;mutex&lt;/h3&gt;
&lt;p&gt;协程意义上的锁，当一个协程获取锁而陷入阻塞时，只让自己陷入&lt;code&gt;suspend&lt;/code&gt;状态，而非通常意义上整个线程陷入阻塞状态，此时，context线程仍然会继续从engine取出协程任务并执行&lt;/p&gt;
&lt;p&gt;其api如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto try_lock() noexcept -&gt; bool; // 普通调用
auto lock() noexcept -&gt; awaiter; // 协程调用，awaiter 的 await_resume 返回 void
auto unlock() noexcept -&gt; void; // 普通调用
auto lock_guard() noexcept -&gt; awaiter; // 协程调用，awaiter 的 await_resume 返回 lock_guard
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;try_lock&lt;/code&gt;即尝试获取锁，返回值表示是否成功获取锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lock&lt;/code&gt;作为协程调用需要通过&lt;code&gt;co_await mutex.lock()&lt;/code&gt;的形式调用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unlock&lt;/code&gt;即释放锁，并唤醒一个 suspend awaiter（如果存在的话）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lock_guard&lt;/code&gt;封装了一系列复合操作，通过&lt;code&gt;co_await mutex.lock_guard()&lt;/code&gt;的形式调用来获取锁并返回 lock_guard，而 lock_guard 在生命周期结束后会自动释放锁，该函数只需要在awaiter_resume返回对应的lock_guard&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先明确的是，mutex不能像event那样，使用&lt;code&gt;std::mutex&lt;/code&gt;来实现，毕竟不能尝试使用锁来实现锁，并且二者语义也不同，因此只能考虑无锁版本&lt;/p&gt;
&lt;p&gt;有了event的无锁实现经验，会发现mutex与其类似，核心数据结构是一个awaiter队列，这里同样使用&lt;code&gt;atomic&amp;#x3C;void&gt; m_state&lt;/code&gt;来表示一个链表，接下来就是考虑它的状态：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当前锁没有被使用（&lt;strong&gt;kUNLOCKED&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;当前锁被使用了，但没有等待者（&lt;strong&gt;kLOCKED_NO_WAITER&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;当前锁被使用了，并且有等待者（&lt;strong&gt;LOCKED_HAVE_WAITER&lt;/strong&gt;，保存指向链表头的指针）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;初始状态下应为&lt;strong&gt;kUNLOCKED&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;状态变化：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;lock()&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;该函数负责对当前互斥量mutex上锁，其核心操作同样在&lt;code&gt;await_suspend()&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;ul&gt;
&lt;li&gt;互斥量被释放了，&lt;code&gt;m_state&lt;/code&gt;变为了&lt;strong&gt;kUNLOCKED&lt;/strong&gt;状态&lt;/li&gt;
&lt;li&gt;其他协程也在等待互斥量，并尝试将自身挂到链表中&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;while (true) {
    // 当前锁没有获取, 尝试获取
    if (m_state == kUNLOCKED) {
        if (m_state.compare_exchange_weak(
                            kUNLOCKED, 
                            kLOCKED_NO_WATIER)) {
            break;
        }
        // 获取锁失败, 被别的协程获取了, 重试
        continue;
    }
    // 原子变量的CAS操作, 尝试挂载自身到链表中
	if (try_add_to_list(this, m_state)) {
        // 成功了, 退出循环
        break;
    }
    // 失败了, 循环重试
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;unlock()&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;该函数负责在协程中解除锁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;m_state&lt;/code&gt;不能&lt;strong&gt;kUNLOCKED&lt;/strong&gt;状态，因为不能解锁一个没有上锁的互斥量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果&lt;code&gt;m_state&lt;/code&gt;为&lt;strong&gt;kLOCKED_HAVE_WAITER&lt;/strong&gt;状态，就恢复一个协程，该协程会自动获得锁，并且当前链表为空了，应该将其状态转为&lt;strong&gt;kLOCKED_NO_WAITER&lt;/strong&gt;状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果&lt;code&gt;m_state&lt;/code&gt;为&lt;strong&gt;kLOCKED_NO_WAITER&lt;/strong&gt;状态，那么应该将其状态改为&lt;strong&gt;kUNLOCKED&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类似&lt;code&gt;lock()&lt;/code&gt;，使用CAS循环：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;while (true) {
    // 尝试释放锁
    if (state == kLOCKED_NO_WAITER) {
        // 尝试改变状态为kUNLOCKED
        if (m_state.compare_exchange_weak(
                         kLOCKED_NO_WAITER, 
                         kUNLOCKED)) {
            return;
        }
        // 改变失败, 重试
        continue;
    }
    // 有协程尝试解锁未上锁的mutex, 应该报error
    if (state == kUNLOCKED) {
    	ERROR;
	}
    // 当前有等待者, 尝试唤醒一个等待者, 并尝试修改状态为 m_state.next 或者kLOCKED_NO_WAITER
   	if (m_state.compare_exchange_weak()) {
        resume();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;优化：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为实现方便，在&lt;strong&gt;kLOCKED_HAVE_WAITER&lt;/strong&gt;状态下，&lt;code&gt;m_state&lt;/code&gt;按照后进先出的顺序来恢复陷入&lt;code&gt;suspend&lt;/code&gt;状态的协程，并且相比event的&lt;code&gt;set&lt;/code&gt;函数，在&lt;code&gt;unlock()&lt;/code&gt;函数使用了CAS循环，进一步降低了性能&lt;/p&gt;
&lt;p&gt;考虑到&lt;code&gt;unlock&lt;/code&gt;只会被上锁的协程来调用，这个时候是不会有多个协程同时&lt;code&gt;unlock()&lt;/code&gt;的，于是可以设置一个awaiter指针（非原子变量），每次解锁时，如果该指针为空指针，并且当前状态为&lt;strong&gt;kLOCKED_HAVE_WAITER&lt;/strong&gt;，就将链表转移给awaiter指针，之后别的协程再次释放锁时，直接从该指针恢复协程即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 有协程尝试解锁未上锁的mutex, 应该报error
if (m_state == kUNLOCKED) {
    ERROR;
}
// 
if (awaiter_ptr == nullptr) {
    if (m_state == kLOCKED_NO_WAITER) {
        // 尝试替代为初始状态
        if (m_state.compare_exchange_strong(
                kLOCKED_NO_WAITER,
                kUNLOCKED)) {
            // 替换成功, 退出
            return;
        }
    }
    // 否则当前状态为kLOCKED_HAVE_WAITER, m_state实际存储的是链表头指针
    void* waiting_list = m_state.exchange(kLOCKED_NO_WATIER, std::memory_order_acq_rel);
    assert(waiting_list != kUNLOCKED &amp;#x26;&amp;#x26; &quot;can&apos;t unlock unlocked mutex!&quot;);
    awaiter_ptr = static_cast&amp;#x3C;awaiter*&gt;(waiting_list);
    // 如果希望FIFO, 就翻转链表
    
}
// 当前实际状态是kLOCKED_HAVE_WAITER
// 移动头结点
awaiter* to_resume = awaiter_ptr;
awaiter_ptr = to_resume-&gt;next;
// 恢复弹出的协程
to_resume-&gt;resume_coro();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么这里判断了&lt;code&gt;m_state == kLOCKED_NO_WAITER&lt;/code&gt;后，之后状态不可能被设置为&lt;strong&gt;kLOCKED_NO_WAITER&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;原因很简单，最多只有一个协程执行&lt;code&gt;unlock()&lt;/code&gt;，且当前状态不能为&lt;strong&gt;kUNLOCKED&lt;/strong&gt;，这代表&lt;code&gt;lock()&lt;/code&gt;此时也不可能把状态设置为&lt;strong&gt;kLOCKED_NO_WAITER&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;核心成员：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;inline static void* kUNLOCKED = nullptr;
inline static void* kLOCKED_NO_WATIER = reinterpret_cast&amp;#x3C;void*&gt;(1);
std::atomic&amp;#x3C;void*&gt; m_state;	
mutex_awaiter* m_awaiter_head{nullptr};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;struct mutex_awaiter&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct mutex_awaiter {
    std::coroutine_handle&amp;#x3C;&gt; handle;
    mutex&amp;#x26; mtx;
    context&amp;#x26; ctx;
    mutex_awaiter* next;	// 作用类似链表的next指针
    // 其他函数见代码
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&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;class mutex
{
public:
    struct mutex_awaiter {
        std::coroutine_handle&amp;#x3C;&gt; handle;
        mutex&amp;#x26; mtx;
        context&amp;#x26; ctx;
        mutex_awaiter* next;

        explicit mutex_awaiter(mutex&amp;#x26; mutex) noexcept : mtx(mutex), 
            ctx(local_context()), handle(nullptr), next(nullptr) {}

        virtual ~mutex_awaiter() {}    

        auto await_ready() noexcept -&gt; bool {
            ctx.register_wait(); 
            void* expected = kUNLOCKED;
            return mtx.m_state.compare_exchange_strong(
                expected,
                 kLOCKED_NO_WATIER,
                 std::memory_order_acquire);
        }

        auto await_suspend(std::coroutine_handle&amp;#x3C;&gt; awaiting_handle) noexcept -&gt; bool {
            handle = awaiting_handle;
            return add_waiting_list();
        }

        auto await_resume() noexcept -&gt; void {
            ctx.unregister_wait();
        }

        auto add_waiting_list() noexcept -&gt; bool {
            void* old_state = mtx.m_state.load();
            while (true) {
                // 竞争情况：在我们准备排队时，锁被释放了。
                // 我们可以再次尝试获取锁，避免不必要的挂起。
                if (old_state == kUNLOCKED) {
                    if (mtx.m_state.compare_exchange_weak(
                            old_state, 
                            kLOCKED_NO_WATIER)) {
                        return false; // 成功获取锁，不要挂起。
                    }
                    // CAS 失败，意味着 old_state 被其他线程改变了，循环会用新值重试。
                    continue;
                }

                // 正常情况：锁被持有，我们需要把自己加入链表。
                // old_state 要么是 kLockedNoWaiters，要么是链表头。
                next = (old_state == kLOCKED_NO_WATIER) ? nullptr : static_cast&amp;#x3C;mutex_awaiter*&gt;(old_state);

                // release 内存序确保了在我们挂起协程之前的代码，对唤醒我们的那个线程是可见的。
                if (mtx.m_state.compare_exchange_weak(
                        old_state, 
                        this)) {
                    return true; // 成功把自己加入队列，现在可以安全挂起了。
                }
                // CAS 失败，循环会用新的 old_state 值重试。
            }
        }

        virtual auto resume_coro() noexcept -&gt; void {
            ctx.submit_task(handle);
        }
    };

    struct guard_awaiter : mutex_awaiter
    {
        using mutex_awaiter::mutex_awaiter;
        auto await_resume() -&gt; detail::lock_guard&amp;#x3C;mutex&gt; { 
            ctx.unregister_wait();
            return detail::lock_guard&amp;#x3C;mutex&gt;(mtx); 
        }
    };
public:
    mutex() noexcept : m_state(kUNLOCKED), m_awaiter_head(nullptr) {}
    ~mutex() noexcept {
        assert(m_state.load() == kUNLOCKED);
    }
    mutex(const mutex&amp;#x26;) = delete;
    mutex&amp;#x26; operator=(const mutex&amp;#x26;) = delete;

    auto try_lock() noexcept -&gt; bool { 
        void* expected = kUNLOCKED;
        return m_state.compare_exchange_strong(
            expected, 
            kLOCKED_NO_WATIER);
    }


    auto lock() noexcept -&gt; mutex_awaiter {
        return mutex_awaiter{*this}; 
    };

    auto unlock() noexcept -&gt; void {
        assert(m_state != kUNLOCKED &amp;#x26;&amp;#x26; &quot;can&apos;t unlock unlocked mutex!&quot;);
        if (m_awaiter_head == nullptr) {
            // kLOCKED_NO_WAITER
            void* expected = kLOCKED_NO_WATIER;
            if (m_state.compare_exchange_strong(
                expected,
                kUNLOCKED,
                std::memory_order_acq_rel,
                std::memory_order_acq_rel)) {
                    // 当前没有等待者
                    return;
                }
            // HAVE WAITER
            void* waiting_list = m_state.exchange(kLOCKED_NO_WATIER, std::memory_order_acq_rel);
            assert(waiting_list != kUNLOCKED &amp;#x26;&amp;#x26; &quot;can&apos;t unlock unlocked mutex!&quot;);
            m_awaiter_head = static_cast&amp;#x3C;mutex_awaiter*&gt;(waiting_list);
            // reverse the list, to implement FIFO
            mutex_awaiter* awaiter = m_awaiter_head;
            m_awaiter_head = nullptr;
            while (awaiter != nullptr) {
                auto next_awaiter = awaiter-&gt;next;
                awaiter-&gt;next = m_awaiter_head;
                m_awaiter_head = awaiter;
                awaiter = next_awaiter;
            }
        }
        // m_state is LOCKED_HAVE_WAITER
        mutex_awaiter* to_resume = m_awaiter_head;
        m_awaiter_head = to_resume-&gt;next;
        to_resume-&gt;resume_coro();
    };

    auto lock_guard() noexcept -&gt; guard_awaiter { return guard_awaiter{*this};};
private:
    inline static void* kUNLOCKED = nullptr;
    inline static void* kLOCKED_NO_WATIER = reinterpret_cast&amp;#x3C;void*&gt;(1);
    std::atomic&amp;#x3C;void*&gt; m_state;
    mutex_awaiter* m_awaiter_head;
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>c++20制作简易协程库2</title><link>https://liang-bk.github.io/blog/coroutine/coro_lab2</link><guid isPermaLink="true">https://liang-bk.github.io/blog/coroutine/coro_lab2</guid><pubDate>Fri, 14 Nov 2025 22:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;io_uring简介&lt;/h2&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;p&gt;github克隆liburing项目：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git clone https://github.com/axboe/liburing.git&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;编译并安装：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd liburing
./configure --cc=gcc --cxx=g++
make -j
make liburing.pc
sudo make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;usr/include/&lt;/code&gt;目录下找到&lt;code&gt;liburing&lt;/code&gt;即为成功&lt;/p&gt;
&lt;h3&gt;基本使用&lt;/h3&gt;
&lt;p&gt;liburing提供了io_uring简化后的api调用，类似epoll的工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化io_uring：&lt;code&gt;io_uring_queue_init()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;从请求队列中获取一个SQE：&lt;code&gt;io_uring_get_sqe()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;填充读写请求：&lt;code&gt;io_uring_prep_read() / write()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;提交请求：&lt;code&gt;io_uring_submit()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;等待结果：&lt;code&gt;io_uring_wait_cqe()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;标记结果已处理：&lt;code&gt;io_uring_cqe_seen()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;核心数据结构&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;io_uring：&lt;/p&gt;
&lt;p&gt;类似&lt;code&gt;epoll_create()&lt;/code&gt;会创建跟epoll空间关联的句柄，&lt;code&gt;liburing&lt;/code&gt;要求使用&lt;/p&gt;
&lt;p&gt;&lt;code&gt;struct io_uring ring;&lt;/code&gt;定义一个操作空间，之后的IO操作（包括提交请求SQE，获取结果CQE）都在上面进行，其在用户态通过&lt;code&gt;mmap&lt;/code&gt;映射了内核中的两个环形队列（提交队列和完成队列），从而实现了内核与用户态的高效通信&lt;/p&gt;
&lt;p&gt;&lt;code&gt;io_uring_queue_init&lt;/code&gt;负责对该空间进行初始化，第一个参数指定了最多可以有多少个IO任务&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* * entries: 队列深度。指定了最多可以有多少个“在途”未处理的 IO 任务
 * 建议是2的幂，如 4096
 * ring:    指向你定义的结构体指针，库函数会填充它
 * flags:   标志位（如 IORING_SETUP_IOPOLL 开启轮询模式）
 */
int io_uring_queue_init(unsigned entries, struct io_uring *ring, unsigned flags);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sqe：&lt;/p&gt;
&lt;p&gt;类似&lt;code&gt;epoll&lt;/code&gt;中的&lt;code&gt;struct epoll_event&lt;/code&gt;，但它们的功能有比较大的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;epoll_event&lt;/code&gt; 是告诉内核“我要&lt;strong&gt;监控&lt;/strong&gt;这个 fd 的可读事件”&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqe&lt;/code&gt; 是告诉内核“我要&lt;strong&gt;执行&lt;/strong&gt;这个具体的 I/O 操作”。 可以将 &lt;code&gt;sqe&lt;/code&gt; 想象成一张**“任务工单”**，把要做的操作（读、写、连）填好，交给内核&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;申请一个IO请求：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 从 ring 中申请一个空的工单槽位
struct io_uring_sqe *sqe = io_uring_get_sqe(&amp;#x26;ring);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于&lt;code&gt;io_uring_sqe&lt;/code&gt;：通常使用 &lt;code&gt;io_uring_prep_*&lt;/code&gt; 辅助函数（可以是&lt;code&gt;accept&lt;/code&gt;，&lt;code&gt;send&lt;/code&gt;，&lt;code&gt;recv&lt;/code&gt;，&lt;code&gt;read&lt;/code&gt;，&lt;code&gt;write&lt;/code&gt;）来填充，但理解其内部字段有助于理解底层原理&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct io_uring_sqe {
    __u8    opcode;       // 操作码。决定是 Read, Write, Accept 还是 Connect
                          // 类似 epoll_ctl 中的 EPOLL_CTL_ADD 等动作
    __u8    flags;        // 标志位 (如 IOSQE_IO_LINK 开启链式调用)
    __u16   ioprio;       // 优先级
    __s32   fd;           // 要操作的文件描述符 (类似 epoll_ctl 的第一个参数)
    __u64   off;          // 文件的偏移量 (pread/pwrite 用)
    __u64   addr;         // 用户态缓冲区 buffer 的地址 (读写数据的存放地)
    __u32   len;          // buffer 的长度
    
    /* * 【核心字段】user_data 
     * 这是一个 64 位的“回执编号”或“上下文指针”。
     * 类似 epoll_event.data.ptr。
     * 你在这里填什么，内核在完成任务后，会原封不动地在 cqe 里还给你。
     * 通常用来存放用户自定义信息结构体的指针。
     */
    __u64   user_data;    
    
    // ... 其他联合体字段用于特定操作
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cqe：类似于 &lt;code&gt;epoll_wait&lt;/code&gt; 返回的&lt;strong&gt;就绪事件&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;epoll&lt;/code&gt; 中，返回意味着“你可以去读写了”（Reactor 模型）&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;io_uring&lt;/code&gt; 中，返回意味着“&lt;strong&gt;内核已经帮你读写完了&lt;/strong&gt;，这是结果”（Proactor 模型），可以将 &lt;code&gt;cqe&lt;/code&gt; 想象成任务完成后的**“结果回执单”**&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;获取完成的IO请求：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct io_uring_cqe *cqe;
// 阻塞等待至少一个任务完成
io_uring_wait_cqe(&amp;#x26;ring, &amp;#x26;cqe);
// 或者非阻塞查看
io_uring_peek_cqe(&amp;#x26;ring, &amp;#x26;cqe);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于&lt;code&gt;io_uring_cqe&lt;/code&gt;：结构非常精简，只包含结果必须的信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct io_uring_cqe {
    /* * 【核心字段】user_data
     * 对应 sqe 中填入的 user_data。
     * 通过它，你才能知道这个结果是属于哪一个 fd 的，或者是哪一次 read 操作的。
     */
    __u64   user_data;    

    /* * 【核心字段】res
     * 操作的返回值。
     * 如果 &gt;= 0：表示成功传输的字节数 (类似 read() 的返回值)。
     * 如果 &amp;#x3C; 0：表示出错，值为 -errno (如 -EAGAIN, -ECANCELED)。
     */
    __s32   res;          

    /* * flags
     * 元数据标志。例如在使用 Provided Buffers 时，这里会包含
     * 内核自动选用的 Buffer ID。
     */
    __u32   flags;        
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;engine&lt;/h2&gt;
&lt;p&gt;engine是协程执行单元，负责存储所有的task和异步IO并向外提供执行的接口&lt;/p&gt;
&lt;p&gt;要存储任务，就要有对应的存储空间，这里选择使用&lt;a href=&quot;https://github.com/max0x7ba/atomic_queue&quot;&gt;atomic queue&lt;/a&gt;无锁队列来存储对应的协程任务，在这里不存储具体的task对象，然后存储&lt;code&gt;std::coroutine_handle&amp;#x3C;&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;初始化&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;engine成员：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 封装操作后的io_uring
uring_proxy m_upxy;

// 无锁队列存储协程
mpmc_queue&amp;#x3C;coroutine_handle&amp;#x3C;&gt;&gt; m_task_queue;

atomic&amp;#x3C;size_t&gt; m_submit_io{0}; // 当前待提交的io操作数量
atomic&amp;#x3C;size_t&gt; m_running_io{0}; // 当前正在执行的io操作数量
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程局部变量&lt;/p&gt;
&lt;p&gt;为简单处理多线程的情况，一旦某个协程句柄被提交到某个engine的任务队列中，即使其暂停恢复后，也应该继续在原先的engine上执行，所以设置一个线程局部变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct local_info
{
    context* ctx{nullptr};
    engine*  egn{nullptr};
};
inline thread_local local_info linfo;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化和销毁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto engine::init() noexcept -&gt; void
{
    // 设置线程局部变量
    linfo.egn = this;
    m_upxy.init(config::kEntryLength);
    m_submit_io.store(0, memory_order_relaxed);
    m_running_io.store(0, memory_order_relaxed);
}

auto engine::deinit() noexcept -&gt; void
{
    linfo.egn = nullptr;
    m_upxy.deinit();
    mpmc_queue&amp;#x3C;coroutine_handle&amp;#x3C;&gt;&gt; empty_queue;
    m_task_queue.swap(empty_queue); // 清空任务队列
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;添加IO任务&lt;/h3&gt;
&lt;p&gt;当协程中需要执行IO任务时，分两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从engine中获取一个sqe&lt;/li&gt;
&lt;li&gt;告诉engine当前提交了一个IO操作，对应的&lt;code&gt;m_submit_io&lt;/code&gt;就要加1&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto engine::get_free_urs() noexcept -&gt; ursptr
{
    return m_upxy.get_free_sqe();
}
auto engine::add_io_submit() noexcept -&gt; void
{
    m_submit_io.fetch_add(1, memory_order_relaxed);
    wakeup(); // 后面解释作用
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;提交并执行协程任务&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;提供提交任务的接口，外部可以通过对应的接口来提交对应的协程句柄：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto engine::submit_task(coroutine_handle&amp;#x3C;&gt; handle) noexcept -&gt; void
{
    if (!m_task_queue.try_push(handle)) {
        wakeup();
    } else {
        // 队列满, 应该直接运行协程或者丢掉该协程并报错
    }
}
&lt;/code&gt;&lt;/pre&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;ready()&lt;/code&gt;&lt;/strong&gt; ：工作线程用来判断 engine 的任务队列是否还有待执行的任务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;num_task_schedule()&lt;/code&gt;&lt;/strong&gt; ：得到当前 engine 的任务队列还有多少任务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;exec_one_task()&lt;/code&gt;&lt;/strong&gt; ：调用&lt;code&gt;schedule()&lt;/code&gt;，engine 会从任务队列中弹出一个任务并作为返回值返回，然后恢复对应协程的执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;empty_io()&lt;/code&gt;&lt;/strong&gt; ：工作线程调用此函数来判断 engine 内部是否还有未处理的 I/O 任务，这包括未提交和正在执行但未完成的 I/O，如果&lt;code&gt;empty_io()&lt;/code&gt;返回了 false 那么即使工作线程收到停止信号也不会直接停止，因为这表明还有任务没有执行完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;poll_submit()&lt;/code&gt;&lt;/strong&gt; ：这是 engine 最为核心的函数，实现对 I/O 的提交以及处理已经完成的 I/O，对于从 io_uring 取出的 cqe，采取批量消费的模式，使用&lt;code&gt;handle_cqe_entry()&lt;/code&gt;函数，获取IO的结果，如果有回调，就执行对应的回调函数。而在处理 I/O 的过程中也应该变更 I/O 执行状态&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto engine::ready() noexcept -&gt; bool
{
    return !m_task_queue.was_empty();
}

auto engine::num_task_schedule() noexcept -&gt; size_t
{
    return m_task_queue.was_size();
}

auto engine::schedule() noexcept -&gt; coroutine_handle&amp;#x3C;&gt;
{
    auto handle = m_task_queue.pop();
    return handle;
}

auto engine::exec_one_task() noexcept -&gt; void
{
    auto coro = schedule();
    coro.resume();
}

auto engine::empty_io() noexcept -&gt; bool
{
    return m_submit_io == 0 &amp;#x26;&amp;#x26; m_running_io == 0;
}

auto engine::poll_submit() noexcept -&gt; void
{
    // 当前有可提交的IO任务，就提交io_uring中
    if (m_submit_io &gt; 0) {
        m_upxy.submit();
        m_running_io += m_submit_io;
        m_submit_io.store(0, memory_order_relaxed);
    }
    // 等待任务完成, peek_uring()确保当前有IO任务完成才会消费, 否则当前engine是由于其他原因被唤醒, 跳过处理IO的部分
    if (m_upxy.wait_eventfd() &amp;#x26;&amp;#x26; m_upxy.peek_uring()) {
        m_upxy.handle_for_each_cqe(
            std::bind(&amp;#x26;engine::handle_cqe_entry, this, std::placeholders::_1),
            true
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;wakeup&lt;/h3&gt;
&lt;p&gt;当engine执行了协程任务，并提交了IO任务时，从其角度来看，当前没有协程任务了，也没有IO任务要提交了，需要做的事情就是等待新的协程或者IO任务到来，或者IO任务完成的通知&lt;/p&gt;
&lt;p&gt;这一点通过io_uring与eventfd的配合来解决&lt;/p&gt;
&lt;p&gt;eventfd 是Linux下的轻量级的用于事件通知的文件描述符，可以在io_uring实例中注册一个eventfd，那么每当有IO完成时，io_uring就会向其写入一个值&lt;/p&gt;
&lt;p&gt;如果eventfd中没有数据，那么从eventfd中读数据本身是一个阻塞的操作，可以利用这一点，在&lt;code&gt;poll_submit()&lt;/code&gt;中阻塞当前的工作线程，等待IO任务完成&lt;/p&gt;
&lt;p&gt;如果有新的协程任务或者IO任务到来，可以利用这一机制，主动向eventfd中写入一个值，&lt;code&gt;poll_submit()&lt;/code&gt;检查当前不是由于IO任务完成而被唤醒，就会跳过消费cqe这一行为，转而去执行新的协程任务/提交IO任务&lt;/p&gt;
&lt;h2&gt;context&lt;/h2&gt;
&lt;p&gt;engine向外提供了提交task和异步IO操作的接口，那么context需要利用这些接口，去执行协程任务或是提交IO操作，具体做法是开启一个线程来进行事件循环：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;执行计算任务（&lt;code&gt;exec_one_task&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;提交IO任务并等待（&lt;code&gt;poll_submit&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;判断当前是否已经执行完所有任务，还有就阻塞，没有就退出&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;理想的情况是外部定义一个context，提交要执行的协程任务（要提交IO任务可以开启一个协程任务，在里面执行IO任务，原因是IO任务本身被设计成awaiter而不是task），然后调用run函数启动事件循环，等待任务执行完毕后自动退出即可&lt;/p&gt;
&lt;h3&gt;初始化&lt;/h3&gt;
&lt;p&gt;context持有一个engine对象和一个工作线程，使用工作线程是为了外部在使用context时无需关心context内部的实现（包括初始化和清理工作）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;成员变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;engine   m_engine; // 执行引擎，提供执行函数的接口
unique_ptr&amp;#x3C;jthread&gt; m_job; // jthread, 能够在析构时自动join, 也能接收停止信号
ctx_id              m_id;

atomic&amp;#x3C;int&gt; m_waited_cnt{0}; // 记录当前有多少陷入suspend状态的协程任务
std::function&amp;#x3C;void()&gt; m_try_stop{nullptr}; // 停止context的运行
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化和清理：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto context::init() noexcept -&gt; void
{
    // 线程局部变量记录当前的context
    linfo.ctx = this;
    // 初始化执行引擎
    m_engine.init();
    m_waited_cnt = 0;
}

auto context::deinit() noexcept -&gt; void
{
    linfo.ctx = nullptr;
    m_engine.deinit();
    m_waited_cnt = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;事件循环&lt;/h3&gt;
&lt;p&gt;三个动作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;执行计算任务（&lt;code&gt;exec_one_task&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;提交IO任务并等待（&lt;code&gt;poll_submit&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;判断当前是否已经执行完所有任务，还有就阻塞，没有就退出&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto context::run(stop_token token) noexcept -&gt; void
{
    while (!token.stop_requested()) {
        int task_num = m_engine.num_task_schedule();
        for (int i = 0; i &amp;#x3C; task_num; i++) {
            m_engine.exec_one_task();
        }
    
        if (check_stop_sig()) {
            if (m_try_stop) {
                m_try_stop();
            }
        }
        m_engine.poll_submit();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断条件发生在提交IO任务之前，这是因为提交IO任务是一个阻塞函数，设想一种情况，外部通过&lt;code&gt;submit&lt;/code&gt;接口预先提交了所有任务，但是里面没有涉及IO，那么当这一组任务在context中由&lt;code&gt;exec_one_task&lt;/code&gt;一次性执行完毕后，没有协程任务也没有IO任务了，自然应该退出了，如果先执行&lt;code&gt;poll_submit&lt;/code&gt;，会阻塞在eventfd的读取上，外部没有提交协程任务或者IO任务，内部也没有IO任务完成，就会一直阻塞下去&lt;/p&gt;
&lt;h4&gt;判断当前是否能够停止&lt;/h4&gt;
&lt;p&gt;三个条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;有没有待运行的协程任务&lt;/li&gt;
&lt;li&gt;有没有处于&lt;code&gt;suspend&lt;/code&gt;状态的协程任务&lt;/li&gt;
&lt;li&gt;有没有未完成/未提交的IO任务&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto context::check_stop_sig() noexcept -&gt; bool {
    return m_engine.empty_io() &amp;#x26;&amp;#x26; m_waited_cnt &amp;#x3C;= 0 &amp;#x26;&amp;#x26; !m_engine.ready();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;发送停止信号&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;jthread&lt;/code&gt;可以发送停止信号：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;jthread::request_stop();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当&lt;code&gt;jthread&lt;/code&gt;执行的函数需要循环查看&lt;code&gt;stop_token&lt;/code&gt;，然后主动退出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto context::run(stop_token token) noexcept -&gt; void
{
    while (!token.stop_requested()) {
    	...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是我们可以提前定义一个函数对象&lt;code&gt;m_try_stop&lt;/code&gt;，当context符合条件时，主动发送线程停止信号，同时向eventfd写入一个值，使后面的&lt;code&gt;poll_submit&lt;/code&gt;不会阻塞：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;m_try_stop = [this]() {
    notify_stop();
};
// 
auto context::notify_stop() noexcept -&gt; void
{
    m_job-&gt;request_stop();
    m_engine.wakeup();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;start&lt;/h3&gt;
&lt;p&gt;start函数中运行工作线程，因为&lt;code&gt;jthread&lt;/code&gt;使用智能指针管理，所以单个context可以实现复用（前提是&lt;code&gt;jthread&lt;/code&gt;工作线程正常结束工作了）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto context::start() noexcept -&gt; void
{
    m_job = make_unique&amp;#x3C;jthread&gt;(
        [this](stop_token token)
        {
            this-&gt;init();
            if (m_try_stop == nullptr) {
                m_try_stop = [this]() {
                    notify_stop();
                };
            }
            this-&gt;run(token);
            this-&gt;deinit();
        });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后就可以使用如下方式来使用context：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;context ctx;
ctx.submit_task(...);
// repeat submit...
ctx.start(); // 启动运行(非阻塞)
ctx.join(); // 主动等待 context 完成所有任务
// 后面要使用context可以继续submit然后start
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;scheduler&lt;/h2&gt;
&lt;p&gt;为了充分利用cpu的多核能力，创建scheduler来管理多个context，方便并发执行各个协程或IO任务，用户无需手动管理各个context，只要把任务交给scheduler，由scheduler进行任务的调度&lt;/p&gt;
&lt;p&gt;流程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;scheduler::init(); // 入参为 context 数量，默认为当前机器的逻辑 CPU 核心数
submit_to_scheduler(task);
// more submit...
scheduler::loop(); // 等待全部 context 完成任务后再关闭全部 context 并返回(阻塞式)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当调用&lt;code&gt;scheduler::loop&lt;/code&gt;时，程序会调用context开始执行任务，并阻塞等待所有任务完成&lt;/p&gt;
&lt;h3&gt;任务管理&lt;/h3&gt;
&lt;p&gt;当调用多个context执行任务的时候，我们不希望每个context在执行完自己的任务后，检查到队列中没有任务了就停止，而是希望由scheduler统一发送停止信号，这一点可以通过设置各个context的&lt;code&gt;m_try_stop&lt;/code&gt;为一个空函数（非&lt;code&gt;nullptr&lt;/code&gt;）来做到&lt;/p&gt;
&lt;p&gt;其次，既然需要scheduler来统一发送停止信号，scheduler就必须知道什么时候停止各个context，这时候出现了一个矛盾，本来是每个context掌管自己运行任务的数量，scheduler现在要强行接管这一功能，还要同时清楚多个context上的任务数量，看上去并不易实现&lt;/p&gt;
&lt;p&gt;接下来介绍的包装协程可以解决这一痛点&lt;/p&gt;
&lt;h4&gt;wrapper_task&lt;/h4&gt;
&lt;p&gt;lab1中介绍的通用型&lt;code&gt;task&lt;/code&gt;类可以封装各种类型的协程任务，但有一点不好，在&lt;code&gt;co_return&lt;/code&gt;后的&lt;code&gt;final_suspend&lt;/code&gt;会返回给上一个协程任务，没有就暂停自身，这可能影响协程帧的释放：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;task&amp;#x3C;void&gt; func1() {
    ...
    co_await IO; // 将自身挂起等待IO完成
    co_return;
}
task&amp;#x3C;void&gt; func2() {
    co_await func1();
    ...
    co_return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;试想一下，当我们将&lt;code&gt;func2()&lt;/code&gt;提交给&lt;code&gt;scheduler&lt;/code&gt;或者&lt;code&gt;context&lt;/code&gt;，被执行引擎运行时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto engine::exec_one_task() noexcept -&gt; void
{
    auto coro = schedule();
    coro.resume();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其运行过程应该是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;engine&lt;/code&gt;取出&lt;code&gt;func2()&lt;/code&gt;的句柄，然后&lt;code&gt;resume()&lt;/code&gt;执行&lt;/li&gt;
&lt;li&gt;执行&lt;code&gt;func2()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;co_await func1()&lt;/code&gt;将执行权转移给&lt;code&gt;func1()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;执行&lt;code&gt;func1()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;co_await IO&lt;/code&gt;，&lt;code&gt;func1()&lt;/code&gt;会被挂起&lt;/li&gt;
&lt;li&gt;执行权回到&lt;code&gt;engine::exec_one_task()&lt;/code&gt;中，继续执行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当一个IO完成，&lt;code&gt;func1()&lt;/code&gt;的句柄被重新送入任务队列：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;engine&lt;/code&gt;取出&lt;code&gt;func1()&lt;/code&gt;的句柄，然后&lt;code&gt;resume()&lt;/code&gt;执行&lt;/li&gt;
&lt;li&gt;执行&lt;code&gt;func1()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;co_return&lt;/code&gt;将执行权转移给&lt;code&gt;func2()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;执行&lt;code&gt;func2()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;co_return&lt;/code&gt;将执行权转移给&lt;code&gt;noop_coroutine&lt;/code&gt;，同时&lt;code&gt;func1()&lt;/code&gt;对应的&lt;code&gt;task&lt;/code&gt;被析构，释放对应的句柄&lt;/li&gt;
&lt;li&gt;&lt;code&gt;noop_coroutine&lt;/code&gt;什么都不做，执行权回到&lt;code&gt;engine::exec_one_task()&lt;/code&gt;中继续执行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时，如果外部没有保存&lt;code&gt;func2()&lt;/code&gt;的句柄，&lt;code&gt;func2()&lt;/code&gt;对应的协程帧就永远不会被释放（除非被外部唤醒或是由外部主动&lt;code&gt;destroy&lt;/code&gt;释放），那么就会出现内存泄露&lt;/p&gt;
&lt;p&gt;一个可行的解决办法是，定义一个只允许在&lt;code&gt;scheduler&lt;/code&gt;或者&lt;code&gt;context&lt;/code&gt;中的使用的特别&lt;code&gt;task&lt;/code&gt;类，该类负责对用户传入的协程进行一次包装，并在&lt;code&gt;final_suspend&lt;/code&gt;时返回&lt;code&gt;suspend_never&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto make_wrapper_task(coro::task&amp;#x3C;void&gt; user_task) -&gt; wrapper_task {
    co_await user_task;
    co_return;
    // user_task will be destruct, its coroutine frame will be deleted
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，外部提交的协程都会由于RAII，使得协程句柄在&lt;code&gt;task&lt;/code&gt;的析构函数中释放，而&lt;code&gt;wrapper_task&lt;/code&gt;则由于&lt;code&gt;final_suspend&lt;/code&gt;返回&lt;code&gt;suspend_never&lt;/code&gt;，直接运行到&lt;code&gt;clean code&lt;/code&gt;清除自身&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wrapper_task&lt;/code&gt;只封装右值&lt;code&gt;task&amp;#x3C;void&gt;&lt;/code&gt;类型，这是因为用户提交到&lt;code&gt;scheduler&lt;/code&gt;或&lt;code&gt;context&lt;/code&gt;的协程任务不应该在意返回值，同时也不应该在外部持有协程句柄造成二次释放，如果协程有返回值需要处理，应该提前封装：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;task&amp;#x3C;int&gt; inner() {
    ...
    co_return 1;
}
task&amp;#x3C;void&gt; to_submit() {
    auto n = co_await inner();
    co_return;
}
submit_to_scheduler(to_submit());
// 或者如下方式:
// auto task = to_submit();
// submit_to_scheduler(std::move(task));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// hpp
class wrapper_task;

class wrapper_promise {
public:
    wrapper_promise();
    ~wrapper_promise();
    // delete the copy constructor and operator
    wrapper_promise(const wrapper_promise&amp;#x26;) = delete;
    wrapper_promise(wrapper_promise&amp;#x26;&amp;#x26;) = delete;
    auto operator=(const wrapper_promise&amp;#x26;) -&gt; wrapper_promise&amp;#x26; = delete;
    auto operator=(wrapper_promise&amp;#x26;&amp;#x26;) -&gt; wrapper_promise&amp;#x26; = delete;
    // promise function
    auto get_return_object() noexcept -&gt; wrapper_task;
    auto initial_suspend() noexcept -&gt; std::suspend_always;
    auto final_suspend() noexcept -&gt; std::suspend_never;
    auto return_void() noexcept -&gt; void;
    auto unhandled_exception() noexcept -&gt; void;
    auto set_before_exit_func(std::function&amp;#x3C;void()&gt; before_exit_func) noexcept -&gt; void;
private:
    std::function&amp;#x3C;void()&gt; m_before_exit_func{nullptr};
    std::exception_ptr m_exception_ptr{nullptr};
};
/*
 * this class should be used to encapsulate the top-level 
 * coroutine submitted by the user.
 */
class wrapper_task {
public:
    using promise_type = wrapper_promise;
    
    explicit wrapper_task(std::coroutine_handle&amp;#x3C;wrapper_promise&gt; handle);
    ~wrapper_task();
    // delete the copy constructor and operator
    wrapper_task(const wrapper_task&amp;#x26;) = delete;
    wrapper_task(wrapper_task&amp;#x26;&amp;#x26;);
    auto operator=(const wrapper_task&amp;#x26;) -&gt; wrapper_task&amp;#x26; = delete;
    auto operator=(wrapper_task&amp;#x26;&amp;#x26;) -&gt;wrapper_task&amp;#x26;;
    // nodiscard指明编译器去检查返回值有没有被使用过, 即强制调用者去使用返回值
    [[nodiscard]] auto promise() const -&gt; const wrapper_promise&amp;#x26; {
        return m_handle.promise();
    }
    [[nodiscard]] auto promise() -&gt; wrapper_promise&amp;#x26; {
        return m_handle.promise();
    }
    [[nodiscard]] auto handle() const -&gt; const std::coroutine_handle&amp;#x3C;wrapper_promise&gt;&amp;#x26; {
        return m_handle;
    }
    [[nodiscard]] auto handle() -&gt; std::coroutine_handle&amp;#x3C;wrapper_promise&gt;&amp;#x26; {
        return m_handle;
    }
    auto resume() -&gt; bool {
        if (!m_handle.done()) {
        m_handle.resume();
    }
    return !m_handle.done();
    }
private:
    std::coroutine_handle&amp;#x3C;wrapper_promise&gt; m_handle{nullptr};
};

auto make_wrapper_task(coro::task&amp;#x3C;void&gt; user_task) -&gt; wrapper_task;

// cpp
wrapper_promise::wrapper_promise() {

}

wrapper_promise::~wrapper_promise() {

}

auto wrapper_promise::get_return_object() noexcept -&gt; wrapper_task {
    return wrapper_task{std::coroutine_handle&amp;#x3C;wrapper_promise&gt;::from_promise(*this)};
}

auto wrapper_promise::initial_suspend() noexcept -&gt; std::suspend_always {
    return std::suspend_always{};
}

auto wrapper_promise::final_suspend() noexcept -&gt; std::suspend_never {
    if (m_before_exit_func) {
        m_before_exit_func();
    }
    return std::suspend_never{};
}

auto wrapper_promise::return_void() noexcept -&gt; void {

}
auto wrapper_promise::unhandled_exception() noexcept -&gt; void {
    // store it, but didn&apos;t use it, user can not touch this
    m_exception_ptr = std::current_exception();
}
auto wrapper_promise::set_before_exit_func(std::function&amp;#x3C;void()&gt; before_exit_func) noexcept -&gt; void {
    m_before_exit_func = std::move(before_exit_func);
}

wrapper_task::wrapper_task(std::coroutine_handle&amp;#x3C;wrapper_promise&gt; handle)
    : m_handle(handle) {

}
wrapper_task::~wrapper_task() {
    // no-op
    // suspend_never means once co_return, coroutine frame will self-destruct
}
wrapper_task::wrapper_task(wrapper_task&amp;#x26;&amp;#x26; other) :m_handle(std::exchange(other.m_handle, nullptr)) {

}
auto wrapper_task::operator=(wrapper_task&amp;#x26;&amp;#x26; other) -&gt; wrapper_task&amp;#x26; {
    if (std::addressof(other) != this) {
        if (m_handle != nullptr) {
            m_handle.destroy();
            m_handle = nullptr;
        }
        m_handle = std::exchange(other.m_handle, nullptr);
    }
    return *this;
}

auto make_wrapper_task(coro::task&amp;#x3C;void&gt; user_task) -&gt; wrapper_task {
    co_await user_task;
    co_return;
    // user_task will be destruct, its coroutine frame will be deleted
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;任务计数&lt;/h4&gt;
&lt;p&gt;从&lt;code&gt;submit_to_scheduler()&lt;/code&gt;提交的协程默认被视为顶层协程，只有其生命周期结束，才意味着一个协程任务的结束，这使我们可以不再关心协程内部的运行情况，因此可以设置一个原子计数器，每当&lt;code&gt;submit_to_scheduler()&lt;/code&gt;被调用时，将计数器加1，当顶层协程结束时，借由&lt;code&gt;wrapper_task&lt;/code&gt;，将计数器减1，当计数器为0时，代表着所有协程任务已经被执行完毕，可以退出&lt;/p&gt;
&lt;h3&gt;初始化&lt;/h3&gt;
&lt;p&gt;scheduler采用单例模式来实现，为了外部使用方便，只对外保留静态接口，将&lt;code&gt;getInstance&lt;/code&gt;设置为私有&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 静态变量
private:
	// 外部提交的顶层协程个数
    static std::atomic&amp;#x3C;int&gt; m_task_cnt;
	// 条件变量, 负责在协程任务结束后唤醒阻塞的线程
    static std::condition_variable m_cv;
	static std::mutex m_mux;
// 成员变量
private:
	// context个数
    size_t                                              m_ctx_cnt{0};
    // 存储context
	detail::ctx_container                               m_ctxs;
    // 派发机制
	detail::dispatcher&amp;#x3C;coro::config::kDispatchStrategy&gt; m_dispatcher;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;懒汉单例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;private:
static auto get_instance() noexcept -&gt; scheduler*
{
    static scheduler sc;
    return &amp;#x26;sc;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;inline static auto init(size_t ctx_cnt = std::thread::hardware_concurrency()) noexcept
    -&gt; void
{
    if (ctx_cnt == 0)
    {
        ctx_cnt = std::thread::hardware_concurrency();
    }
    get_instance()-&gt;init_impl(ctx_cnt);
}
// .cpp
// 静态变量在cpp文件中初始化
std::atomic&amp;#x3C;int&gt; scheduler::m_task_cnt = 0;
std::condition_variable scheduler::m_cv;
std::mutex scheduler::m_mux;
auto scheduler::init_impl(size_t ctx_cnt) noexcept -&gt; void
{
    detail::init_meta_info();
    m_task_cnt = 0;
    m_ctx_cnt = ctx_cnt;
    m_ctxs    = detail::ctx_container{};
    m_ctxs.reserve(m_ctx_cnt);
    for (int i = 0; i &amp;#x3C; m_ctx_cnt; i++)
    {
        m_ctxs.emplace_back(std::make_unique&amp;#x3C;context&gt;());
    }
    m_dispatcher.init(m_ctx_cnt, &amp;#x26;m_ctxs);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;提交任务&lt;/h3&gt;
&lt;p&gt;对于外部提交进来的任务，使用&lt;code&gt;wrapper_task&lt;/code&gt;封装，增加原子计数，在&lt;code&gt;wrapper_task&lt;/code&gt;的&lt;code&gt;final_suspend&lt;/code&gt;函数中减少原子计数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;inline void submit_to_scheduler(task&amp;#x3C;void&gt;&amp;#x26;&amp;#x26; task) noexcept
{
    scheduler::submit(std::move(task));
}
// 类函数
static inline auto submit(task&amp;#x3C;void&gt;&amp;#x26;&amp;#x26; task) noexcept -&gt; void
{
    m_task_cnt.fetch_add(1, std::memory_order_acq_rel);
    auto wrapper_task = detail::make_wrapper_task(std::move(task));
    // 设置原子计数的增减
    wrapper_task.promise().set_before_exit_func([]() {
        m_task_cnt.fetch_sub(1, std::memory_order_acq_rel);
        if(m_task_cnt.load(std::memory_order_relaxed) == 0) {
            m_cv.notify_all();
        }
    });
    auto handle = wrapper_task.handle();
    submit(handle);
}
auto scheduler::submit_task_impl(std::coroutine_handle&amp;#x3C;&gt; handle) noexcept -&gt; void
{
    // 获取当前要派发到context的对应id
    size_t ctx_id = m_dispatcher.dispatch();
    m_ctxs[ctx_id]-&gt;submit_task(handle);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;执行任务&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;loop&lt;/code&gt;函数中开启所有的context运行，利用条件变量阻塞当前线程，等待所有任务完成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto scheduler::loop_impl() noexcept -&gt; void
{
    for (int i = 0; i &amp;#x3C; m_ctx_cnt; i++) {
        m_ctxs[i]-&gt;set_try_stop([]() {
            // 设置空函数体, context不会自己停止
            return;
        });
    }
    for (int i = 0; i &amp;#x3C; m_ctx_cnt; i++) {
        m_ctxs[i]-&gt;start();
    }
    {
        std::unique_lock&amp;#x3C;std::mutex&gt; lock(m_mux);
        m_cv.wait(lock, [this]() {
            return m_task_cnt.load(std::memory_order_relaxed) == 0;
        });
    }
    // 所有任务完成, 统一发送停止信号
    stop_impl();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有任务完成后，告知所有context，任务结束：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto scheduler::stop_impl() noexcept -&gt; void
{
    for (int i = 0; i &amp;#x3C; m_ctx_cnt; i++)
    {
        m_ctxs[i]-&gt;notify_stop();
    }
    for (int i = 0; i &amp;#x3C; m_ctx_cnt; i++) {
        m_ctxs[i]-&gt;join();
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>c++20协程入门</title><link>https://liang-bk.github.io/blog/coroutine</link><guid isPermaLink="true">https://liang-bk.github.io/blog/coroutine</guid><description>c++20简易入门教程</description><pubDate>Fri, 14 Nov 2025 22:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;c++20协程入门&lt;/h2&gt;
&lt;h3&gt;前置语法&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;一个函数体内如果有下面三个关键字的任意一个将被视为协程&lt;/p&gt;
&lt;p&gt;&lt;code&gt;co_await&lt;/code&gt;，&lt;code&gt;co_yield&lt;/code&gt;，&lt;code&gt;co_return&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;协程无法直接使用&lt;code&gt;void/int/vector&amp;#x3C;string&gt;&lt;/code&gt;这样的形式作为函数的返回类型，而是返回&lt;code&gt;Task&lt;/code&gt;（一个由用户自定义的类），并指定其&lt;code&gt;promise_type&lt;/code&gt;（通过以下方式）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Task {
    ...
};
class promise1 {
    ...
};
struct std::coroutine_traits&amp;#x3C;Task&gt; {
    // promise1也由用户自定义，但需要满足一些规范
    using promise_type = promise1; 
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者下面的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class promise1 {
    ...
};
class Task {
public:
    using promise_type = promise;
    ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现&lt;code&gt;Task&lt;/code&gt;和&lt;code&gt;promise_type&lt;/code&gt;完毕后，利用如下语法编写一个协程函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Task run(int a, int b) {
    int c = a + b;
    co_return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;协程函数的调用者在调用协程函数之后可以拿到协程的返回值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int main () {
    auto task = run(1, 2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里&lt;code&gt;task&lt;/code&gt;变量是上面定义的&lt;code&gt;Task&lt;/code&gt;类型，通过该变量，我们可以在&lt;code&gt;main&lt;/code&gt;函数中与协程交互（如让协程恢复执行，获取结果等）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;强烈推荐视频&lt;/strong&gt;：&lt;a href=&quot;https://www.bilibili.com/video/BV1K14y1v7cw/?spm_id_from=333.1387.homepage.video_card.click&amp;#x26;vd_source=b93859dd8360e859dab85c63d1b91f9f&quot;&gt;【协程革命】理论篇！扫盲，辟谣一条龙！全语言通用，夯实基础，准备起飞！&lt;/a&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;/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;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;T some_coroutine(P param)
{
  // 为协程帧分配内存，如果 promsie 存在内存分配重载则利用 promise 的内存分配函数
  auto* f = new coroutine_frame(std::forward&amp;#x3C;P&gt;(param));

  // returnObject即Task对象, 会在协程第一次陷入 suspend 状态后通过语句`return returnObject`返回给调用者
  auto returnObject = f-&gt;promise.get_return_object();

  // 控制协程创建时的调度逻辑
  co_await promise.initial_suspend();
    
  // 用户定义代码执行前
  try
  {
    // 用户在协程函数体内定义的逻辑
    &amp;#x3C;body-statements&gt;
  }
  catch (...)
  {
    // 处理协程运行中抛出的异常
    promise.unhandled_exception();
  }
  // 用户定义代码返回后

FinalSuspend:
  // 控制协程结束时的调度逻辑
  co_await promise.final_suspend();
  // 清理工作, 比如释放先前创建的协程帧
  clean code;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://en.cppreference.com/w/cpp/language/coroutines.html&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://sakurs2.gitbook.io/tinycorolab-docs/knowledgeextension/coroutine_compiler_view&quot;&gt;从编译器视角揭秘 C++ 协程 | tinyCoroLab Docs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下面会从以下几个根据协程的调度阶段，一个一个介绍上述执行流程的部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;coroutine frame&lt;/li&gt;
&lt;li&gt;promise结构&lt;/li&gt;
&lt;li&gt;&lt;code&gt;co_await&lt;/code&gt;，&lt;code&gt;co_return&lt;/code&gt;语义&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;协程的创建&lt;/h4&gt;
&lt;h5&gt;协程函数运行之前的准备工作&lt;/h5&gt;
&lt;p&gt;从下面这个简单的协程函数开始，一步一步查看协程是如何创建的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;Task run(int a, int b) {
    int c = a + b;
    co_return;
}
int main() {
    auto task = run(1, 2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当&lt;code&gt;main&lt;/code&gt;函数调用&lt;code&gt;run&lt;/code&gt;时，&lt;code&gt;run&lt;/code&gt;函数不会像普通函数那样直接一路执行下去，编译器在函数体内部做了某些准备工作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建协程帧(&lt;strong&gt;co_frame&lt;/strong&gt;)：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/coroutine1.eIQ8c-y7_jXwGQ.webp&quot; alt=&quot;image-20250801145042265&quot;&gt;&lt;/p&gt;
&lt;p&gt;由于c++中是&lt;strong&gt;无栈协程&lt;/strong&gt;（有栈的概念可以参考线程栈，指每创建一个协程，就有自己的栈区来支配），其运行依靠系统栈，但协程本身可以被暂停和恢复运行（此时系统栈的变化类似多次函数调用和结束），为了保存函数体内的变量，需要动态开辟内存空间，将一些数据存到堆区。&lt;/p&gt;
&lt;p&gt;上述步骤由编译器自动生成代码执行，协程帧由一些变量来组成，包括&lt;code&gt;run&lt;/code&gt;的函数参数，&lt;code&gt;run&lt;/code&gt;中创建的局部变量，&lt;strong&gt;恢复执行后&lt;/strong&gt;下一条要执行的指令地址（只是为了容易理解，实际存储的并不一定是这个，但仍然与指令地址具有相同的效果），以及&lt;code&gt;promise_type&lt;/code&gt;对象，其中最重要的是&lt;code&gt;promise_type&lt;/code&gt;对象，它与协程的生命周期绑定，并可以通过其来控制协程的行为&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成&lt;code&gt;Task&lt;/code&gt;对象，执行第一次调度：&lt;/p&gt;
&lt;p&gt;这一步由&lt;code&gt;promise_type&lt;/code&gt;的函数实现，&lt;code&gt;promise_type&lt;/code&gt;是一个需要用户实现的接口，形式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class promise_type
{
    friend class Task;
public:
    promise_type(){}
    Task get_return_object()
    {
        return Task{std::coroutine_handle&amp;#x3C;promise_type&gt;::from_promise(*this)};
    }
    constexpr std::suspend_always initial_suspend() { return {}; }
    
    /*
    void return_void() {}
    void unhandled_exception() {}
    constexpr std::suspend_always final_suspend() noexcept { return {}; }
    */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注释里的内容暂时不需要关注，编译器在构造了协程对应的&lt;code&gt;promise_type&lt;/code&gt;对象后，按顺序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;执行&lt;code&gt;promise_type.get_return_object()&lt;/code&gt;函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过&lt;code&gt;promise_type&lt;/code&gt;对象来构建协程句柄（&lt;code&gt;coroutine_handle&lt;/code&gt;，可以理解为一个指针，指向协程帧）&lt;/li&gt;
&lt;li&gt;使用协程句柄构建一个&lt;code&gt;Task&lt;/code&gt;对象（&lt;strong&gt;Task类需提供对应的构造函数&lt;/strong&gt;），该&lt;code&gt;Task&lt;/code&gt;对象将作为协程函数的返回值，在协程第一次进入&lt;strong&gt;暂停&lt;/strong&gt;（suspend）状态时被自动返回&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在代码中，执行&lt;code&gt;auto task = run(1, 2);&lt;/code&gt;后，&lt;code&gt;task&lt;/code&gt;变量就是该&lt;code&gt;Task&lt;/code&gt;对象，通过该对象，&lt;code&gt;main&lt;/code&gt;函数中可以利用Task类定义的方法简单的操作协程（恢复协程执行，获取结果等等）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行&lt;code&gt;co_await promise_type.initial_suspend()&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;该函数应返回一个&lt;code&gt;Awaitable&lt;/code&gt;对象，关于&lt;code&gt;Awaitable&lt;/code&gt;和&lt;code&gt;Awaiter&lt;/code&gt;，两者关系如下图所示，&lt;code&gt;std::suspend_always&lt;/code&gt;和&lt;code&gt;std::suspend_never&lt;/code&gt;都属于&lt;code&gt;Awaiter&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/coroutine2.C5q14kNw_Z1qfCoE.webp&quot; alt=&quot;image-20250801150750416&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;返回&lt;code&gt;Awaitable&lt;/code&gt;对象后，进入暂停—恢复流程，决定当前协程是否会立即运行，比如上面的&lt;code&gt;promise_type&lt;/code&gt;中返回&lt;code&gt;std::suspend_always&lt;/code&gt;，那么此时协程会立即暂停，并将自己的状态更新到协程帧中，然后将控制权交给调用者&lt;code&gt;main&lt;/code&gt;函数，此时&lt;code&gt;main&lt;/code&gt;函数继续执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;main&lt;/code&gt;函数拿到了可以和协程交互的对象&lt;code&gt;task&lt;/code&gt;，但代码中并不恢复协程，那么就会直接退出程序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;task&lt;/code&gt;被析构&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;**协程是如何根据suspend_always来把控制权交还给main函数的？**其实很简单，创建协程时除了会在堆区创建协程帧，也会像普通函数一样创建函数栈帧：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/coroutine3.C5OpRa7K_icKN4.webp&quot; alt=&quot;image-20250801151519153&quot;&gt;&lt;/p&gt;
&lt;p&gt;只不过在&lt;code&gt;run&lt;/code&gt;函数执行到其函数体的第一条语句之前，编译器就检查到&lt;code&gt;std::suspend_always&lt;/code&gt;需要让协程暂停，此时会执行类似函数内&lt;code&gt;return&lt;/code&gt;的指令，&lt;code&gt;run frame&lt;/code&gt;被pop，回到&lt;code&gt;main frame&lt;/code&gt;中对应位置执行，控制权就自然而然的交回给&lt;code&gt;main&lt;/code&gt;函数了，至于如何恢复协程的执行，会在之后的篇幅进行讲解&lt;/p&gt;
&lt;p&gt;而如果&lt;code&gt;initial_suspend&lt;/code&gt;返回的是&lt;code&gt;std::suspend_never&lt;/code&gt;，&lt;code&gt;run frame&lt;/code&gt;会保留，协程就会立即执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;协程的销毁&lt;/h4&gt;
&lt;h5&gt;co_return&lt;/h5&gt;
&lt;p&gt;当&lt;code&gt;run&lt;/code&gt;函数执行到&lt;code&gt;co_return&lt;/code&gt;语句时，协程的逻辑执行已经完成，接下来需要主动或被动的释放协程资源（主要是协程帧的销毁）&lt;/p&gt;
&lt;p&gt;与协程的创建类似，执行&lt;code&gt;co_return&lt;/code&gt;语句时，会触发一系列明确的收尾动作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class promise_type
{
    friend class Task;
public:
    /*
    promise_type(){}
    Task get_return_object()
    {
        return Task{std::coroutine_handle&amp;#x3C;promise_type&gt;::from_promise(*this)};
    }
    constexpr std::suspend_always initial_suspend() { return {}; }
    */
    
    void return_void() {}
    void unhandled_exception() {}
    constexpr std::suspend_always final_suspend() noexcept { return {}; }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;设置最终结果：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;co_return;&lt;/code&gt;会自动调用&lt;code&gt;promise_type.return_void()&lt;/code&gt;，这将会通知&lt;code&gt;Promise&lt;/code&gt;对象协程已经正常结束，但没有返回值&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果需要协程返回值该怎么做？&lt;/strong&gt;
在&lt;code&gt;promise_type&lt;/code&gt;类中增加一个&lt;code&gt;void return_value(T v)&lt;/code&gt;函数和成员变量&lt;code&gt;val&lt;/code&gt;，利用&lt;code&gt;val&lt;/code&gt;保存&lt;code&gt;v&lt;/code&gt;，同时&lt;code&gt;co_return;&lt;/code&gt;变为&lt;code&gt;co_return v;&lt;/code&gt;即可&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：一个&lt;code&gt;promise_type&lt;/code&gt;类中无法同时实现&lt;code&gt;return_value&lt;/code&gt;和&lt;code&gt;return_void&lt;/code&gt;函数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果协程因为一个未捕获的异常而终止会怎么样？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;编译器会自动调用&lt;code&gt;promise::unhandle_exception()&lt;/code&gt;函数来存储异常信息，等待后续逻辑处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;跳转到&lt;code&gt;FinalSuspend&lt;/code&gt;标号处，执行&lt;code&gt;co_await promise_type.final_suspend()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;与创建时&lt;code&gt;co_await initial_suspend()&lt;/code&gt;类似，由编译器主动调用，函数的返回结果也是一个&lt;code&gt;Awaitable&lt;/code&gt;对象，这个对象将直接决定协程接下来的行为，仍以&lt;code&gt;std::suspend_always&lt;/code&gt;和&lt;code&gt;std::suspend_never&lt;/code&gt;为例做介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;该函数返回&lt;code&gt;std::suspend_always()&lt;/code&gt;：根据上面的介绍，返回这个对象代表着协程当前要暂停，并将执行权转移给&lt;strong&gt;调用者（caller）&lt;/strong&gt;，此时&lt;strong&gt;堆中创建的协程帧仍然存在&lt;/strong&gt;，如果调用者之后不主动恢复该协程（大部分情况都不会再恢复，因为此时协程的逻辑生命已经结束了）或者不主动调用协程句柄的&lt;code&gt;destroy()&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;strong&gt;协程句柄&lt;/strong&gt;，其应与&lt;code&gt;Task&lt;/code&gt;绑定&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Task {
    // ...
    std::coroutine_handle&amp;#x3C;promise_type&gt; handle;
	Task(std::coroutine_handle&amp;#x3C;promise_type&gt; h) : handle(h) {}
    ~Task() {
        if (handle) {
            // 协程帧还存在的话，就销毁
            handle.destroy();
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中，&lt;code&gt;std::coroutine_handle&amp;#x3C;promise_type&gt;&lt;/code&gt;在&lt;code&gt;Task&lt;/code&gt;类中定义一个与&lt;code&gt;promise_type&lt;/code&gt;类绑定的协程句柄，该协程句柄由&lt;code&gt;promise_type::get_return_object()&lt;/code&gt;设置值&lt;/p&gt;
&lt;p&gt;在析构函数中检查句柄的有效性，并使用&lt;code&gt;destroy()&lt;/code&gt;主动释放协程帧&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关于&lt;code&gt;std::coroutine_handle&lt;/code&gt;和协程句柄的更详细的知识，将在之后进行介绍&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这样，在协程逻辑上退出之后，借由返回的&lt;code&gt;Task&lt;/code&gt;对象，就能主动释放协程所占的资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;该函数返回&lt;code&gt;std::suspend_never()&lt;/code&gt;，根据上面的介绍，返回这个对象代表着协程当前不会暂停，会继续执行，而当前协程已经运行到函数体末尾，在函数体的末尾之后，是编译器自动生成的&lt;strong&gt;清理代码（cleanup code）&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;strong&gt;这是一个危险的选项，尤其是其绑定的task对象还存活的时候&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;co_return&lt;/code&gt; 之后，调用者仍然尝试使用协程句柄（例如，通过&lt;code&gt;Task&lt;/code&gt;对象），这将立即导致&lt;strong&gt;悬垂指针和未定义行为&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;协程的恢复&lt;/h3&gt;
&lt;h4&gt;std::coroutine_handle&lt;/h4&gt;
&lt;p&gt;操控协程的句柄，其类似一个指针，指向一个协程帧，并向用户暴露操控对应协程的接口&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::coroutine_handle&lt;/code&gt;是一个类模板，有两种形式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;std::coroutine_handle&amp;#x3C;&gt;&lt;/code&gt;：是对所有协程类型进行类型擦除后的版本，能表示任何协程句柄&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::coroutine_handle&amp;#x3C;promise_type&amp;#x3C;return_type&gt;&gt;&lt;/code&gt;：只能表示&lt;code&gt;promise_type&amp;#x3C;return_type&gt;&lt;/code&gt;对应的协程句柄&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;成员函数&lt;/h4&gt;
&lt;h5&gt;static&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;from_promise(promise_type &amp;#x26;p)&lt;/code&gt;：从&lt;code&gt;promise_type&lt;/code&gt;对象中创建一个协程句柄。&lt;/p&gt;
&lt;p&gt;该函数返回一个类型为&lt;code&gt;std::coroutine_handle&amp;#x3C;promise_type&amp;#x3C;return_type&gt;&gt;&lt;/code&gt;的协程句柄，但&lt;code&gt;std::coroutine_handle&amp;#x3C;&gt;&lt;/code&gt;类并不具有此静态函数，无法调用该函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;from_address(void* ptr)&lt;/code&gt;：从协程帧地址中创建一个协程句柄&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::coroutine_handle&amp;#x3C;&gt;&lt;/code&gt;和&lt;code&gt;std::coroutine_handle&amp;#x3C;promise_type&gt;&lt;/code&gt;类都有该函数，返回的句柄类型也与调用时的类 类型相同&lt;/p&gt;
&lt;p&gt;通常需要使用另一个协程句柄传入地址来构建新的协程句柄，不太常用&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;non-static&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;done()&lt;/code&gt;：判断协程是否结束，返回&lt;code&gt;true&lt;/code&gt;或&lt;code&gt;false&lt;/code&gt;，判断条件是协程是否在最终挂起点处暂停（即&lt;code&gt;promise_type.final_suspend()&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resume()&lt;/code&gt;：使该协程句柄所代表的协程继续运行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;destroy()&lt;/code&gt;：销毁该协程句柄所代表的协程（协程句柄&lt;code&gt;done()&lt;/code&gt;必须为&lt;code&gt;true&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;promise()&lt;/code&gt;：返回该协程内部&lt;code&gt;promise_type&lt;/code&gt;对象的引用（&lt;code&gt;std::coroutine_handle&amp;#x3C;&gt;&lt;/code&gt;类没有该函数）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;address()&lt;/code&gt;：返回协程句柄对应的协程帧的地址&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总之，c++通过&lt;code&gt;std::coroutine_handle&lt;/code&gt;为用户提供了操作一个协程的接口，方便操作协程&lt;/p&gt;
&lt;p&gt;其通常作为&lt;code&gt;Task&lt;/code&gt;类的成员变量存在&lt;/p&gt;
&lt;h3&gt;一个简单的协程案例&lt;/h3&gt;
&lt;p&gt;了解了协程的创建和销毁，就可以创建一个简单的协程来模拟一个普通的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;coroutine&gt;
#include &amp;#x3C;exception&gt;
#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;ostream&gt;

class Task {
public:
    class promise_type; // 前向声明
    using coroutine_handle = std::coroutine_handle&amp;#x3C;promise_type&gt;;
    class promise_type {
    public:
        // 必须实现
        Task get_return_object()
        {
            return Task { coroutine_handle::from_promise(*this) };
        }
        // 必须实现
        std::suspend_always initial_suspend() { return {}; }
        // 必须实现
        void unhandled_exception()
        {
            ex = std::current_exception();
        }
        // void 和 value选一种实现
        // void return_void() { }
        void return_value(int res) { res_ = res; }
        // 必须实现 并置为noexcept
        std::suspend_always final_suspend() noexcept { return {}; }

    public:
        std::exception_ptr ex;
        int res_;
    };

public:
    Task()
        : handle_(nullptr)
    {
    }
    explicit Task(coroutine_handle handle)
        : handle_(handle)
    {
    }
    ~Task()
    {
        if (handle_) {
            handle_.destroy();
        }
    }
    bool resume()
    {
        if (!handle_.done()) {
            handle_.resume();
        }
        return !handle_.done();
    }
    int result()
    {
        return handle_.promise().res_;
    }

private:
    coroutine_handle handle_;
};
Task add(int x, int y)
{
    int z = x + y;
    co_return z;
}
int main(int argc, char const* argv[])
{
    auto task = add(1, 2);
    task.resume();
    std::cout &amp;#x3C;&amp;#x3C; &quot;call coroutine add, result is &quot; &amp;#x3C;&amp;#x3C; task.result() &amp;#x3C;&amp;#x3C; std::endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前来看，创建一个普通函数所花费的功夫貌似比创建一个协程要小的多，那么为什么还要使用协程？后面会继续介绍c++协程中最重要的关键字&lt;code&gt;co_await&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;co_await&lt;/h3&gt;
&lt;p&gt;在之前介绍的协程中创建和销毁中，刻意省略了一些过程，这些过程都与&lt;code&gt;co_await&lt;/code&gt;相关：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;co_await promise_type.initial_suspend();
co_await promise_type.final_suspend();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;co_await&lt;/code&gt;和协程的调度相关联，和之前的&lt;code&gt;Awaitable&lt;/code&gt;对象共同掌管着一个协程的暂停和恢复&lt;/p&gt;
&lt;h4&gt;co_await语义&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;co_await&lt;/code&gt;的语法是：&lt;code&gt;co_await &amp;#x3C;expr&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;co_await&lt;/code&gt;关键字用于暂停协程，其中&lt;code&gt;&amp;#x3C;expr&gt;&lt;/code&gt;必须是或返回一个&lt;code&gt;Awaitable&lt;/code&gt;对象&lt;/p&gt;
&lt;p&gt;在调用协程函数时，编译器为我们隐式执行了&lt;code&gt;co_await promise_type::initial_suspend();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在协程&lt;code&gt;co_return&lt;/code&gt;时，编译器也会隐式执行&lt;code&gt;co_await promise_type::final_suspend();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;co_await &amp;#x3C;expr&gt;&lt;/code&gt;时，编译器会展开该语句，执行跟&lt;code&gt;Awaitable&lt;/code&gt;对象相关的固定流程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;{
	auto&amp;#x26;&amp;#x26; value = &amp;#x3C;expr&gt;;
    // 伪代码, 获取awaitable和awaiter
 	auto&amp;#x26;&amp;#x26; awaitable = get_awaitable(promise, static_cast&amp;#x3C;decltype(value)&gt;(value));
 	auto&amp;#x26;&amp;#x26; awaiter = get_awaiter(static_cast&amp;#x3C;decltype(awaitable)&gt;(awaitable));
	// 跟awaiter相关的执行流程
  	if (!awaiter.await_ready()) {
	    using handle_t = std::coroutine_handle&amp;#x3C;P&gt;;

    	using await_suspend_result_t =
      	decltype(awaiter.await_suspend(handle_t::from_promise(promise)));

    	// 在该处协程准备陷入 suspend 状态，编译器生成代码来保存协程当前的运行状态
    	&amp;#x3C;suspend-coroutine&gt;

    	if constexpr (std::is_void_v&amp;#x3C;await_suspend_result_t&gt;) {
            awaiter.await_suspend(handle_t::from_promise(promise));
            &amp;#x3C;return-to-caller-or-resumer&gt;
    	}
    	else {
        	// 这里暂不考虑返回 std::coroutine_handle 的 await_suspend
            static_assert(
                std::is_same_v&amp;#x3C;await_suspend_result_t, bool&gt;,
                &quot;await_suspend() must return &apos;void&apos; or &apos;bool&apos;.&quot;);

            if (awaiter.await_suspend(handle_t::from_promise(promise))) {
                &amp;#x3C;return-to-caller-or-resumer&gt;
            }
        }
        // 协程陷入 suspend 状态后恢复会从该处开始执行而不是从&amp;#x3C;suspend-coroutine&gt;处
		&amp;#x3C;resume-point&gt;
    }
	return awaiter.await_resume();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Awaiter&lt;/h4&gt;
&lt;p&gt;从协程的创建中，应大致了解了&lt;code&gt;Awaiter&lt;/code&gt;对象和&lt;code&gt;Awaitable&lt;/code&gt;对象的关系，现在将协程的创建与&lt;code&gt;std::suspend_always&lt;/code&gt;与&lt;code&gt;co_wait&lt;/code&gt;关联起来介绍：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;std::suspend_always&lt;/code&gt;是一个&lt;code&gt;Awaiter&lt;/code&gt;对象：&lt;/p&gt;
&lt;p&gt;一个&lt;code&gt;Awaiter&lt;/code&gt;对象要求实现以下三个函数**（都必须声明为&lt;code&gt;noexcept&lt;/code&gt;）**：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;await_ready()&lt;/code&gt;：字面意思，指执行&lt;code&gt;co_await awaiter;&lt;/code&gt;语句的协程是否能够继续运行，&lt;code&gt;true&lt;/code&gt;代表可以，否则进入&lt;code&gt;suspend_point&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;await_suspend(coroutine_handle&amp;#x3C;&gt;)&lt;/code&gt;：参数&lt;code&gt;coroutine_handle&amp;#x3C;&gt;&lt;/code&gt;指的是执行了&lt;code&gt;co_await std::suspend_always&lt;/code&gt;的协程（&lt;strong&gt;这一点很重要&lt;/strong&gt;），并执行协程暂停前的一些准备工作，调用该函数后，编译器会生成相应代码来暂停协程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;await_resume()&lt;/code&gt;：恢复上一步&lt;code&gt;suspend&lt;/code&gt;的协程，可以在此处做一些恢复的动作（如准备返回值），调用该函数，编译器会生成相应代码来恢复协程&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct awaiter
{
    bool await_ready() noexcept;
    void await_suspend(coroutine_handle&amp;#x3C;&gt;) noexcept;
    void await_resume() noexcept;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在编译器生成的代码中，其工作流程如下，其中，&lt;code&gt;await_suspend()&lt;/code&gt;有三种返回值，编译器根据返回值类型的不同，会做不一样的动作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;void await_suspend(std::coroutine_handle&amp;#x3C;&gt;) noexcept&lt;/code&gt; ：调用完毕后立即暂停入参指向的协程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;bool await_suspend(std::coroutine_handle&amp;#x3C;&gt;) noexcept&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;返回&lt;code&gt;true&lt;/code&gt;则暂停入参指向的协程&lt;/p&gt;
&lt;p&gt;返回&lt;code&gt;fasle&lt;/code&gt;则继续运行入参指向的协程（仍然需要经过&lt;code&gt;await_resume&lt;/code&gt;逻辑）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;std::coroutine_handle&amp;#x3C;&gt; await_suspend(std::coroutine_handle&amp;#x3C;&gt;) noexcept&lt;/code&gt;：挂起入参指向的协程，并恢复返回值指向的协程的运行&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;if (!awaiter.await_ready()) {
    // 编译器会在这里准备挂起，并拿到当前协程的句柄 `h`
    // std::coroutine_handle&amp;#x3C;&gt; h = /* ... get current handle ... */;

    // 调用 await_suspend(h)，并根据其返回值决定下一步
    // --- 情况 A: await_suspend 返回 void ---
    if constexpr (std::is_void_v&amp;#x3C;decltype(awaiter.await_suspend(h))&gt;) {
        awaiter.await_suspend(h);
        // 编译器魔法: 保存当前所有状态，挂起协程，将控制权返回给调用者/恢复者
        COMPILER_SAVE_STATE_AND_SUSPEND(); 
        // 当协程被 .resume() 时，将从这里继续执行
        :resume_label1; 
    }

    // --- 情况 B: await_suspend 返回 bool ---
    else if constexpr (std::is_same_v&amp;#x3C;decltype(awaiter.await_suspend(h)), bool&gt;) {
        if (awaiter.await_suspend(h)) { // 如果返回 true
            // 编译器魔法: 同上，挂起协程
            COMPILER_SAVE_STATE_AND_SUSPEND();
            :resume_label2;
        }
    }

    // --- 情况 C: await_suspend 返回另一个协程句柄 ---
    else if constexpr (std::is_convertible_v&amp;#x3C;decltype(awaiter.await_suspend(h)), std::coroutine_handle&amp;#x3C;&gt;&gt;) {
        auto next_coro_to_run = awaiter.await_suspend(h);
        // 编译器魔法: 对称转移。挂起当前协程，并立即调度 next_coro_to_run 恢复执行
        COMPILER_PERFORM_SYMMETRIC_TRANSFER(next_coro_to_run);
        :resume_label3;
    }
}

// 调用 await_resume() 获取最终结果
// (如果表达式有返回值的话)
// ValueType result = awaiter.await_resume();
awaiter.await_resume(); // 如果返回 void
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所谓注释中的&lt;strong&gt;当前协程的句柄&lt;/strong&gt;：简单理解为调用&lt;code&gt;co_await awaiter;&lt;/code&gt;的那个协程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;介绍了&lt;code&gt;awaiter&lt;/code&gt;接口后，再看&lt;code&gt;std::suspend_always&lt;/code&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct suspend_always
{
    constexpr bool await_ready() const noexcept { return false; }
    constexpr void await_suspend(coroutine_handle&amp;#x3C;&gt;) const noexcept {}
    constexpr void await_resume() const noexcept {}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据1中介绍的流程，&lt;code&gt;co_await std::suspend_always{}&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;调用&lt;code&gt;await_ready()&lt;/code&gt;，结果为&lt;code&gt;fasle&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后调用&lt;code&gt;await_suspend()&lt;/code&gt;，返回值为&lt;code&gt;void&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保存协程状态，挂起协程，将控制权返回给调用者**（指协程函数的调用者，不是await对象方法的调用者）**&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;用户可以构建自己的awaiter类，只要实现了以上三个函数即可（都要标记为&lt;code&gt;noexcept&lt;/code&gt;）&lt;/p&gt;
&lt;h4&gt;Awaitable&lt;/h4&gt;
&lt;p&gt;将一个对象称为可&lt;code&gt;Awaitable&lt;/code&gt;，当且仅当其满足了以下三个条件中的一个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现了&lt;code&gt;await_ready()&lt;/code&gt;，&lt;code&gt;await_suspend(std::coroutine_handle&amp;#x3C;&gt;)&lt;/code&gt;，&lt;code&gt;await_resume()&lt;/code&gt;三个函数&lt;/li&gt;
&lt;li&gt;重载了&lt;code&gt;operator co_await()&lt;/code&gt;运算符，并令其返回一个&lt;code&gt;Awaiter&lt;/code&gt;对象&lt;/li&gt;
&lt;li&gt;实现了&lt;code&gt;await_transform&lt;/code&gt;成员函数&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;为task实现operator co_await()&lt;/h5&gt;
&lt;p&gt;上面阐述了&lt;code&gt;co_await&lt;/code&gt;是如何与&lt;code&gt;awaiter&lt;/code&gt;协同工作的，但我们更多的时候需要让&lt;code&gt;co_await&lt;/code&gt;后跟另一个协程函数，即：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;Task func1() {
    std::cout &amp;#x3C;&amp;#x3C; &quot;func1()&quot; &amp;#x3C;&amp;#x3C; endl;
    co_return 1;
}

Task func2() {
    co_await func1();
    std::cout &amp;#x3C;&amp;#x3C; &quot;func2()&quot; &amp;#x3C;&amp;#x3C; endl;
    co_return 2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那就可以考虑在Task类中实现&lt;code&gt;co_await()&lt;/code&gt;运算符：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;实现一个最简单的&lt;code&gt;operator co_await()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Task {
public:
    class promise_type; // 前向声明
    using coroutine_handle = std::coroutine_handle&amp;#x3C;promise_type&gt;;
    class promise_type {
    /* ... */
    };

public:
    /*...*/
    std::suspend_always operator co_await() {
      return std::suspend_always{};
    }
    /*...*/

private:
    coroutine_handle handle_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自定义&lt;code&gt;awaiter&lt;/code&gt;，在父协程调用子协程时，挂起父协程，运行子协程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Task {
public:
    class promise_type; // 前向声明
    using coroutine_handle = std::coroutine_handle&amp;#x3C;promise_type&gt;;
    class promise_type {
    /* ... */
    };
public:
    struct awaiter {
        awaiter() : handle_(nullptr) {}
        explicit awaiter(coroutine_handle handle) : handle_(handle) {}
        bool await_ready() const noexcept {return false;}
        coroutine_handle await_suspend(std::coroutine_handle&amp;#x3C;&gt; coroutine) noexcept {
            return handle_;
        }
        int await_resume() noexcept {return handle_.promise().res_;}
        std::coroutine_handle&amp;#x3C;promise_type&gt; handle_;
    };
public:
    /*...*/
    awaiter operator co_await() {
      return awaiter{handle_};
    }
    /*...*/

private:
    coroutine_handle handle_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;理解operator co_await()&lt;/h5&gt;
&lt;p&gt;以&lt;code&gt;func2()&lt;/code&gt;中&lt;code&gt;co_await func1()&lt;/code&gt;为例（为避免混淆，我将&lt;code&gt;func2()&lt;/code&gt;对应的返回值称为Task2，将&lt;code&gt;func1()&lt;/code&gt;对应的返回值称为Task1）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto&amp;#x26;&amp;#x26; value = &amp;#x3C;expr&gt;;
// 伪代码, 获取awaitable和awaiter
auto&amp;#x26;&amp;#x26; awaitable = get_awaitable(promise, static_cast&amp;#x3C;decltype(value)&gt;(value));
auto&amp;#x26;&amp;#x26; awaiter = get_awaiter(static_cast&amp;#x3C;decltype(awaitable)&gt;(awaitable));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器会执行类似上面伪代码形式的流程，具体的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;执行&lt;code&gt;func1()&lt;/code&gt;，进行&lt;strong&gt;协程的创建&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;auto* f = new coroutine_frame(std::forward&amp;#x3C;P&gt;(param));
auto Task1 = f-&gt;promise.get_return_object();
co_await promise.initial_suspend(); // 将Task返回
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用&lt;code&gt;Task1&lt;/code&gt;中的&lt;code&gt;operator co_await()&lt;/code&gt;，获取返回的&lt;code&gt;awaiter&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行&lt;code&gt;co_await awaiter;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;add&lt;/code&gt;协程实例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;Task add(int x, int y, int id)
{
    std::cout &amp;#x3C;&amp;#x3C; &quot;enter coroutine &quot; &amp;#x3C;&amp;#x3C; id &amp;#x3C;&amp;#x3C; std::endl;
    int z = x + y;
    if (id == 2) {
        std::cout &amp;#x3C;&amp;#x3C; &quot;coroutine &quot; &amp;#x3C;&amp;#x3C; id &amp;#x3C;&amp;#x3C; &quot; will return\n&quot;;
        co_return z;
    }
    z += co_await add(y, 30, id + 1);
    std::cout &amp;#x3C;&amp;#x3C; &quot;coroutine &quot; &amp;#x3C;&amp;#x3C; id &amp;#x3C;&amp;#x3C; &quot; will return\n&quot;;
    co_return z;
}
int main(int argc, char const* argv[])
{
    auto task = add(1, 2, 1);
    task.resume();
    std::cout &amp;#x3C;&amp;#x3C; &quot;call coroutine 1 add, result is &quot; &amp;#x3C;&amp;#x3C; task.result() &amp;#x3C;&amp;#x3C; std::endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/coroutine_co_await_awaitable.gYWCmP2D_MQg8u.webp&quot; alt=&quot;image-20250805170410587&quot;&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;add&lt;/code&gt;函数里执行了加法，但是结果没有传入&lt;code&gt;result&lt;/code&gt;中，从输出来看，嵌套的协程2正常返回了，但是&lt;code&gt;main&lt;/code&gt;函数调用的协程1没有正常返回，解释如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在执行&lt;code&gt;task = add(1, 2, 1)&lt;/code&gt;后，&lt;code&gt;main&lt;/code&gt;函数主动恢复协程1运行，此时堆区和&lt;code&gt;main&lt;/code&gt;线程栈区如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/co_await_analyze1.CkRlaJcz_Z1gOqoQ.webp&quot; alt=&quot;image-20250805171503828&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当协程1执行了&lt;code&gt;co_await add(y, 30, id + 1)&lt;/code&gt;后：&lt;/p&gt;
&lt;p&gt;协程1暂停，并返回协程2的句柄，这样协程2将会继续从上一次暂停中恢复运行（从&lt;code&gt;initial_suspend()&lt;/code&gt;处恢复运行，直到&lt;code&gt;co_return&lt;/code&gt;返回一个&lt;code&gt;std::suspend_always&lt;/code&gt;暂停了协程2&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/co_await_analyze2.Di2ogfS6_pyIN4.webp&quot; alt=&quot;image-20250805171952319&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在暂停协程2后，理应将执行权交给逻辑上的调用者（即协程1），但这里并没有，原因是：在协程2调用&lt;code&gt;co_return&lt;/code&gt;后，执行的&lt;code&gt;final_suspend()&lt;/code&gt;只是暂停了当前的协程，那么协程2的栈帧被pop，执行权将交给栈中的前一个栈帧（即main栈帧），从main栈帧继续执行&lt;/p&gt;
&lt;p&gt;要注意的是，此时堆上的协程2的协程帧并没有被释放，因为它被暂停了，还&lt;strong&gt;没有执行清理代码&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;main&lt;/code&gt;中没有再主动恢复协程1，协程1不会运行，&lt;code&gt;main&lt;/code&gt;结束后，先执行了协程1对应的Task1对象的析构函数，在销毁协程1帧时，协程2对应的Task2对象作为协程1帧的一部分，也会调用Task2对象的析构函数，由此来完成对两个协程帧的释放&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至此，c++20中的协程的基本运行流程介绍完毕，但想要使用协程来执行异步操作，还需要将其封装成库，并将其调用封装成普通函数那样的执行顺序&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>javaweb 笔记</title><link>https://liang-bk.github.io/blog/javaweb</link><guid isPermaLink="true">https://liang-bk.github.io/blog/javaweb</guid><pubDate>Mon, 20 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;javaweb&lt;/h1&gt;
&lt;h2&gt;maven&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;管理jar包&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;ol&gt;
&lt;li&gt;
&lt;p&gt;下载地址&lt;a href=&quot;https://maven.apache.org/download.cgi&quot;&gt;Download Apache Maven – Maven&lt;/a&gt;，解压目录不要有中文&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;apache-maven-xxx/conf/settings.xml&lt;/code&gt;修改：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;!--找到localRepository， 添加本地仓库地址--&gt;
&amp;#x3C;localRepository&gt;xxx\apache-maven-3.9.11\repo&amp;#x3C;/localRepository&gt;

&amp;#x3C;!--mirrors 标签中新增标签mirror，为阿里云--&gt;
&amp;#x3C;mirror&gt;
    &amp;#x3C;id&gt;alimaven&amp;#x3C;/id&gt;
    &amp;#x3C;name&gt;aliyun maven&amp;#x3C;/name&gt;
    &amp;#x3C;url&gt;http://maven.aliyun.com/nexus/content/groups/public/&amp;#x3C;/url&gt;
    &amp;#x3C;mirrorOf&gt;central&amp;#x3C;/mirrorOf&gt;
&amp;#x3C;/mirror&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;apache-maven-xxx/bin&lt;/code&gt;添加到环境变量中&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;idea集成&lt;/h3&gt;
&lt;h4&gt;创建maven项目&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;setting&lt;/code&gt;-&gt;&lt;code&gt;Build...&lt;/code&gt;-&gt;&lt;code&gt;Build Tools&lt;/code&gt;-&gt;&lt;code&gt;Maven&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改&lt;code&gt;maven home path&lt;/code&gt;为maven根目录&lt;/li&gt;
&lt;li&gt;修改&lt;code&gt;setting file&lt;/code&gt;为&lt;code&gt;setting.xml&lt;/code&gt;文件&lt;/li&gt;
&lt;li&gt;修改&lt;code&gt;Local repository&lt;/code&gt;为设置的本地仓库文件夹&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建空项目后，新创建&lt;code&gt;module&lt;/code&gt;，&lt;code&gt;build system&lt;/code&gt;选择maven即可&lt;/p&gt;
&lt;h4&gt;maven坐标&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;groupId&gt;org.example&amp;#x3C;/groupId&gt;
&amp;#x3C;artifactId&gt;maven-project01&amp;#x3C;/artifactId&gt;
&amp;#x3C;version&gt;1.0-SNAPSHOT&amp;#x3C;/version&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;groupId：项目隶属组织名称（域名反写）&lt;/li&gt;
&lt;li&gt;artifactId：项目名称&lt;/li&gt;
&lt;li&gt;version：项目版本号&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这三项在后面配置依赖时会用到&lt;/p&gt;
&lt;h4&gt;导入maven项目&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;将文件夹复制到空项目中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;File&lt;/code&gt;-&gt;&lt;code&gt;Project Structures&lt;/code&gt;-&gt;&lt;code&gt;Modules&lt;/code&gt;-&gt;&lt;code&gt;+&lt;/code&gt;-&gt;&lt;code&gt;import module&lt;/code&gt;-&gt;&lt;code&gt;选择文件夹中的pom.xml&lt;/code&gt;文件&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;依赖管理&lt;/h3&gt;
&lt;h4&gt;依赖&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://mvnrepository.com/&quot;&gt;依赖下载官网&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;&amp;#x3C;dependencies&gt;&lt;/code&gt;标签下增加&lt;code&gt;&amp;#x3C;dependency&gt;&lt;/code&gt;标签，然后刷新xml文件即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependencies&gt;
    &amp;#x3C;!-- https://mvnrepository.com/artifact/org.springframework/spring-context --&gt;
    &amp;#x3C;dependency&gt;
        &amp;#x3C;groupId&gt;org.springframework&amp;#x3C;/groupId&gt;
        &amp;#x3C;artifactId&gt;spring-context&amp;#x3C;/artifactId&gt;
        &amp;#x3C;version&gt;6.1.4&amp;#x3C;/version&gt;
    &amp;#x3C;/dependency&gt;
&amp;#x3C;/dependencies&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;排除依赖&lt;/h4&gt;
&lt;p&gt;在对应的&lt;code&gt;&amp;#x3C;dependecy&gt;&lt;/code&gt;标签内增加&lt;code&gt;&amp;#x3C;exclusions&gt;&lt;/code&gt;标签：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;org.springframework&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;spring-context&amp;#x3C;/artifactId&gt;
    &amp;#x3C;version&gt;6.1.4&amp;#x3C;/version&gt;
    &amp;#x3C;!--排除依赖--&gt;
    &amp;#x3C;exclusions&gt;
        &amp;#x3C;exclusion&gt;
            &amp;#x3C;groupId&gt;io.micrometer&amp;#x3C;/groupId&gt;
            &amp;#x3C;artifactId&gt;micrometer-observation&amp;#x3C;/artifactId&gt;
        &amp;#x3C;/exclusion&gt;
    &amp;#x3C;/exclusions&gt;
&amp;#x3C;/dependency&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;依赖范围&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;&amp;#x3C;dependency&gt;&lt;/code&gt;标签内的maven坐标下添加&lt;code&gt;&amp;#x3C;scope&gt;&lt;/code&gt;标签，标签值和作用如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/maven_scope.CKCHRL2h_Z11w7vL.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Y表示可以使用，-表示不行&lt;/p&gt;
&lt;p&gt;比如&lt;code&gt;&amp;#x3C;scope&gt;test&amp;#x3C;/scope&gt;&lt;/code&gt;表示让某一个依赖包只能在项目的test包下使用，其他包不能使用&lt;/p&gt;
&lt;h3&gt;生命周期&lt;/h3&gt;
&lt;p&gt;三个周期相互独立&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;clean：清除之前编译的结果&lt;/li&gt;
&lt;li&gt;default：编译-&gt;测试-&gt;打包-&gt;安装 （执行后一个前会先执行前一个）&lt;/li&gt;
&lt;li&gt;site：上线&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;直接在idea的maven面板中的&lt;code&gt;LifeCycle&lt;/code&gt;运行或者在模块目录下运行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mvn [clean/compile/test/pacakge/install]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JUnit（单测）&lt;/h2&gt;
&lt;h3&gt;使用&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pom.xml&lt;/code&gt;添加依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;org.junit.jupiter&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;junit-jupiter&amp;#x3C;/artifactId&gt;
    &amp;#x3C;version&gt;5.9.1&amp;#x3C;/version&gt;
&amp;#x3C;/dependency&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类名：&lt;code&gt;xxxTest&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;位置：&lt;code&gt;test/java/com.xxxx/xxxTest&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;方法：必须是&lt;code&gt;public void&lt;/code&gt;，加上注解&lt;code&gt;@Test&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class MainTest {
    @Test
    public void test() {
        
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;断言&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;Assertions&lt;/code&gt;包，最后一个参数通常设置为错误提示信息（String）&lt;/p&gt;
&lt;p&gt;文档：&lt;a href=&quot;https://docs.junit.org/5.0.1/api/org/junit/jupiter/api/Assertions.html&quot;&gt;Assertions (JUnit 5.0.1 API)&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;常见注解&lt;/h3&gt;
&lt;h4&gt;环境准备和销毁&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@BeforeAll&lt;/code&gt;：方法必须为static，通常作为setup初始化资源，在所有测试启动前仅运行一次&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterAll&lt;/code&gt;：方法必须为static，通常用于释放资源，在所有测试完成后仅运行一次&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@BeforeEach&lt;/code&gt;：通常作为setup初始化资源，在每个测试启动前都运行一次&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterEach&lt;/code&gt;：通常用于释放资源，在每个测试完成后都运行一次&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;参数化测试&lt;/h4&gt;
&lt;p&gt;同时使用下面两个注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@ParameterizedTest&lt;/code&gt;：标识方法会使用多个参数进行测试&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ValueSource([])&lt;/code&gt;：在()内添加一个数组来指明使用的参数&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;设置测试名&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;@DisplayName(&quot;xxx&quot;)&lt;/code&gt;：显示测试类/方法名为&quot;xxx&quot;&lt;/p&gt;
&lt;h2&gt;Spring Boot&lt;/h2&gt;
&lt;h3&gt;创建&lt;/h3&gt;
&lt;p&gt;idea可以自动化配置spring boot：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;new module&lt;/code&gt;-&gt;&lt;code&gt;Spring Boot&lt;/code&gt;-&gt;&lt;code&gt;Type: Maven&lt;/code&gt;-&gt;&lt;code&gt;设置项目名与jdk版本&lt;/code&gt;-&gt;&lt;code&gt;next&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;-&gt;&lt;code&gt;选择Spring boot版本&lt;/code&gt;-&gt;&lt;code&gt;Dependency:Web:Spring Web&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;启动入口&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;main/java/xxx/SpringbootxxxApplication&lt;/code&gt;中的main方法，带有&lt;code&gt;@SpringBootApplication&lt;/code&gt;注解&lt;/p&gt;
&lt;h4&gt;请求注解&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;@RestController&lt;/code&gt;：类注解，标识其是一个请求处理类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestMapping(&quot;&quot;)&lt;/code&gt;：方法注解，填写对应的请求路径，标识其是该请求路径对应的处理方法&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
public class HelloController {
    @RequestMapping(&quot;/hello&quot;)
    public String Hello(String name) {
        System.out.println(&quot;name: &quot; + name);
        return &quot;Hello &quot; + name + &quot;~&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HTTP&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;li&gt;基于TCP&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;spring boot处理请求&lt;/h4&gt;
&lt;p&gt;内置的tomcat web服务器会将http文本封装为&lt;code&gt;HttpServletRequest&lt;/code&gt;对象，在使用时可以直接将其作为函数参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String method = request.getMethod();	// get/post
String url = request.getRequestURL().toString();	// http://localhost:8080/request
String uri = request.getRequestURI();	// request
String protocol = request.getProtocol();	// HTTP/1.1
String name = request.getParameter(&quot;name&quot;);	// 
String age = request.getParameter(&quot;age&quot;);
// 获取请求头参数
String accept = request.getHeader(&quot;accept&quot;);
String contentType = request.getHeader(&quot;content-type&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;响应码&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;1xx：响应中，临时状态码&lt;/li&gt;
&lt;li&gt;2xx：成功&lt;/li&gt;
&lt;li&gt;3xx：重定向&lt;/li&gt;
&lt;li&gt;4xx：客户端错误&lt;/li&gt;
&lt;li&gt;5xx：服务端错误&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;spring boot返回响应&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;用&lt;code&gt;HttpServletResponse&lt;/code&gt;对象来设置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;response.setStatus(HttpServletResponse.SC_OK);
response.setHeader(&quot;name&quot;, &quot;1&quot;);
response.getWriter().write(&quot;&amp;#x3C;h1&gt;hello response&amp;#x3C;/h1&gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;ResponseEntity&lt;/code&gt;对象来链式设置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RequestMapping(&quot;/response&quot;)
public ResponseEntity&amp;#x3C;String&gt; response(HttpServletRequest request) {
    return ResponseEntity
        .status(401)
        .header(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;)
        .body(&quot;&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一般不手动设置响应状态码和响应头&lt;/p&gt;
&lt;h3&gt;三层架构&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Controller（控制层）：接受请求、响应数据&lt;/li&gt;
&lt;li&gt;Service（业务逻辑层）：处理具体的业务逻辑&lt;/li&gt;
&lt;li&gt;dao（数据访问层）：负责数据访问（增删改查...）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;架构：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;--controller
	--xxxController.java
--dao
	--impl
		xxxDaoImpl.java
	xxxDao.java
--service
	--impl
		xxxServiceImpl.java
	xxxService.java
Application.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;impl&lt;/code&gt;存接口实现，&lt;code&gt;xxxDao&lt;/code&gt;存的是接口，&lt;code&gt;service&lt;/code&gt;同理&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;controller直接使用service的功能，将请求数据解析后调用service处理业务逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;service直接使用dao的功能，在处理业务逻辑时涉及数据的增删改查，最后将数据返回给controller&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dao负责提供数据增删改查的接口，一般和model（模型），数据库关联&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;controller在调用完service完成业务逻辑处理后，将数据封装一层作为相应的数据返回给客户端&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;分层解耦&lt;/h4&gt;
&lt;p&gt;为了解决在Controller中主动new Service对象和在Service中主动new Dao对象导致的不方便改变接口实现的问题&lt;/p&gt;
&lt;p&gt;通过Spring boot框架提供的&lt;strong&gt;Bean&lt;/strong&gt;对象来解决上述问题**（Bean对象是容器中创建和管理的对象）**&lt;/p&gt;
&lt;h5&gt;控制反转（IOC）&lt;/h5&gt;
&lt;p&gt;将对象的创建控制权转移给外部容器&lt;/p&gt;
&lt;h5&gt;依赖注入（DI）&lt;/h5&gt;
&lt;p&gt;容器为应用程序提供运行时所依赖的资源&lt;/p&gt;
&lt;h5&gt;解决方案1&lt;/h5&gt;
&lt;p&gt;使用&lt;code&gt;@Component&lt;/code&gt;和&lt;code&gt;@Autowired&lt;/code&gt;注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;被&lt;code&gt;@Component&lt;/code&gt;标注的类，在组件扫描路径内时，会被 Spring 扫描、实例化并注册为 Bean，由容器统一管理生命周期&lt;/li&gt;
&lt;li&gt;被&lt;code&gt;@Autowired&lt;/code&gt;标注的依赖，Spring 会在 Bean 初始化阶段根据类型（必要时根据名字或&lt;code&gt;@Qualifier&lt;/code&gt;）从容器中查找合适的 Bean 注入进去&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// IOC: 将对象交给容器管理
@Component
public class UserDaoImpl implements UserDao {
    ...
}

// DI: 从容器中取对象
@Component
public class UserServiceImpl implements UserServcie{
    @Autowired
    private UserDao userDao;
}

// DI: 从容器中取对象
@RestController
public class UserController {
    @Autowired
    private UserDao userDao;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;解决方案2&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Repository
public class UserDaoImpl implements UserDao {
    ...
}

@Serivce
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
}

@RestController
public class UserController {
    @Autowired
    private UserDao userDao;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@Repository&lt;/code&gt;和&lt;code&gt;@Service&lt;/code&gt;细化了&lt;code&gt;@Component&lt;/code&gt;注解，controller一般只设置&lt;code&gt;@RestController&lt;/code&gt;注解，因为其包含了&lt;code&gt;@Controller&lt;/code&gt;&lt;/p&gt;
&lt;h5&gt;DI详解&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;基于&lt;code&gt;@Autowired&lt;/code&gt;注解的三种注入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在变量名上添加：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Autowired
private UserDao userDao;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造函数上添加：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Serivce
public class UserServiceImpl implements UserService {
    private final UserDao userDao;
    @Autowired
    UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置set方法（省略，一般用前两种）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@Autowired&lt;/code&gt;默认按照类型注入，即如果有多个类实现了同一个接口，bean对象就不知道该保存哪个类型，从而报错，解决方案如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;@Primary&lt;/code&gt;注解表明哪个类是主要实现的类&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Primary
@Repository
public class UserDaoImpl implements UserDao {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;要使用不同的实现&lt;/strong&gt;：使用&lt;code&gt;@Qualifier&lt;/code&gt;注解变量或构造函数表明使用哪个实现类&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Autowired
@Qualifier(&quot;userDaoImpl&quot;)
private UserDao userDao;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;@Resource&lt;/code&gt;注解变量表明使用的实现类的名称（&lt;strong&gt;不是Spring框架的提供，推荐使用第二个实现&lt;/strong&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Resource(name = &quot;userDaoImpl&quot;)
private UserDao userDao;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;配置文件&lt;/h3&gt;
&lt;p&gt;将原来的&lt;code&gt;.property&lt;/code&gt;文件换为&lt;code&gt;.yml&lt;/code&gt;文件（键值对配置）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  applicaiton:
    name: springboot-mybatis-quickstart
  datasource:
    type: ..
    url: ..
    driver-class-name: ..
&lt;/code&gt;&lt;/pre&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;测试类要在引导程序同包或子包下，同时对类添加&lt;code&gt;@SpringBootTest&lt;/code&gt;注解&lt;/p&gt;
&lt;h2&gt;MYSQL&lt;/h2&gt;
&lt;h3&gt;连接&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;mysql -u{username} -p [-h{ip} -P{port}]&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{username}&lt;/code&gt;：要填的用户名，自用填&lt;code&gt;root&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{ip}&lt;/code&gt;：远程数据库的ip地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{port}&lt;/code&gt;：远程数据库的端口&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SQL基本操作&lt;/h3&gt;
&lt;p&gt;DDL/DML/DQL/DCL（定义/操作/查询/控制）&lt;/p&gt;
&lt;h4&gt;DLL&lt;/h4&gt;
&lt;p&gt;定义数据库对象（数据库，表，字段）&lt;/p&gt;
&lt;h5&gt;数据库操作&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;-- 查询数据库
show database;
-- 创建指定数据库
create database dbname;
-- 删除指定数据库
drop database dbname;
-- 查看数据库表
select database();
-- 使用指定数据库
use dbname;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;表操作&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;-- 创建表 []代表可选项
create table tablename(
    字段1 字段类型 [约束] [comment 注释],
    ...
)[comment 表注释]
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;约束：
&lt;ul&gt;
&lt;li&gt;非空&lt;code&gt;not null&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;唯一&lt;code&gt;unique&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;主键&lt;code&gt;primary key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;默认&lt;code&gt;default&lt;/code&gt; 跟一个值，表示字段默认值&lt;/li&gt;
&lt;li&gt;外键&lt;code&gt;foreign&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;自增&lt;code&gt;auto increment&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数据类型：
&lt;ul&gt;
&lt;li&gt;数字：&lt;code&gt;tinyint&lt;/code&gt;，&lt;code&gt;int&lt;/code&gt;，&lt;code&gt;bigint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;字符串：&lt;code&gt;char&lt;/code&gt;（固定），&lt;code&gt;varchar&lt;/code&gt;（不固定）&lt;/li&gt;
&lt;li&gt;日期：&lt;code&gt;date&lt;/code&gt;，&lt;code&gt;datetime&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;-- 查看当前数据库所有表
show tables;
-- 查看表的所有字段
desc tablename;
-- 查询建表语句
show create table tablename;
-- 向表中增加，修改，重命名，删除字段以及重命名表名
alter table tablename add 字段 类型 [comment] [约束];
alter table tablename modify 字段 类型;
alter table change 旧字段名 新字段名 类型 [comment] [约束];
alter table tablename drop column 字段;
alter table tablename rename to 表名;
-- 删除表
drop table [if exists] tablename;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;DML&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;插入&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;insert into tablename(字段1， 字段2) values (值1 ，值2);
insert into tablename values (值1， 值2);
-- 可叠加
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更新&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;update tablename set 字段1 = 值1, 字段2 = 值2, ... [where ...]
-- 不加where条件表示更新所有行
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;delete from tablename [where ...]
-- 不加where会删除所有行
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;DQL&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select 
	-- distinct 去重
	[distinct] 字段 [AS 别名1] 
from
	tablename
where
	-- 比较 &amp;#x3C;, &gt;, =, !=, between ... and, in(...), like _/%, is null
	-- 逻辑 &amp;#x26;&amp;#x26;, ||, !
	条件
group by
	-- 聚合函数 count(), max(), min(), avg(), sum(), select用
	-- 使用group by, 前面的select只能选择分组的字段或者聚合函数
	分组字段
having 
	-- 解决where后不能跟聚合函数的问题
	分组后条件列表
order by
	-- 默认asc升序排序, 要降序需指明desc
	排序字段列表 [asc]	
limit
	-- 如0, 5表示查出来的结果从第0条开始展示，最多展示5条, 第一个参数为0可以省略
	起始索引, 查询记录数
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;JDBC&lt;/h3&gt;
&lt;p&gt;一套接口，由数据库厂商实现，比如使用mysql要引入对应的jar包：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;com.mysql&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;mysql-connector-j&amp;#x3C;/artifactId&gt;
    &amp;#x3C;version&gt;8.0.33&amp;#x3C;/version&gt;
&amp;#x3C;/dependency&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 1. 注册
class.forName(&quot;com.mysql.cj.jdbc.Driver&quot;);
// 2. 连接
String url = &quot;jdbc:mysql://localhost:3306/dbname&quot;;
String username = &quot;root&quot;;
String password = &quot;123456&quot;;
Connection conn = DriverManager.getConnection(url, username, password);
// 3. 获取sql执行对象
Statement statement = connection.createStatement();
// 4. 执行语句, i是执行语句后影响的行数
int i = statement.executeUpdate(&quot;update user set age = 25 where id = 1&quot;);
// 5. 释放连接
statement.close();
conn.close();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;预编译执行查询&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String sql = &quot;select id, username from user where username = ?&quot;;
pStatement = conn.prepareStatement(sql);
// 设置sql语句中的?占位符
pStatement.setString(1, &quot;xxx&quot;);
// 执行语句, 得到结果集
resultSet = pStatement.executeQuery();
// 遍历结果集
while (resultSet.next()) {
    // getXxx: 可以输入列标号, 也可以输入列名
    resultSet.getInt(&quot;id&quot;);
}
resultSet.close();
pStatement.close();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;防sql注入&lt;/li&gt;
&lt;li&gt;数据库会缓存语句，然后对占位符&lt;code&gt;?&lt;/code&gt;替换，性能更高&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Mybatis&lt;/h3&gt;
&lt;p&gt;ORM（对象关系映射）框架，简化数据库操作，在这里是对JDBC的进一步封装，底层使用了连接池初始化了一定数量的连接来实现资源复用&lt;/p&gt;
&lt;h4&gt;配置&lt;/h4&gt;
&lt;p&gt;在项目&lt;code&gt;application&lt;/code&gt;文件写入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;spring.datasource.url=jdbc:mysql://localhost:3306/dbname
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=1234
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用方法&lt;/h4&gt;
&lt;p&gt;新建mapper包（代替三层架构中的dao层），定义&lt;code&gt;Mapper&lt;/code&gt;接口，使用&lt;code&gt;@Mapper&lt;/code&gt;注解&lt;/p&gt;
&lt;h5&gt;注解&lt;/h5&gt;
&lt;p&gt;在&lt;code&gt;Mapper&lt;/code&gt;接口的方法中加入&lt;code&gt;@Select()&lt;/code&gt;，&lt;code&gt;@Update()&lt;/code&gt;，&lt;code&gt;@Insert()&lt;/code&gt;，&lt;code&gt;@Delete()&lt;/code&gt;注解，并填入SQL语句，以实现对应的功能&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;框架会在程序运行时自动为接口创建一个实现类对象（代理对象），并将该对象存入IOC容-&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;以查询为例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Mapper
public interface UserMapper {
    // #{} 在实际执行时会被替换为 ?
    // 当函数有多个参数时需要使用@Param注解为对应的参数起名， 且与#{}中一致
    @Select(&quot;select * from user where username = #{username} and password = #{password}&quot;)
    public User findByUsernameAndPassword(@Param(&quot;username&quot;) String username, @Param(&quot;password&quot;) String password);
    
    // 查询结果会被自动封装到User实体类中
    // 多行结果可以使用List接收
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;XML映射文件&lt;/h5&gt;
&lt;p&gt;注解适合写一些简单的SQL语句，复杂的一般在XML文件中写&lt;/p&gt;
&lt;p&gt;目录结构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/mybatis_xml.DHgMwYiB_1v7Dug.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同包同名&lt;/li&gt;
&lt;li&gt;xml文件的namespace属性为Mapper接口全限定名一致&lt;/li&gt;
&lt;li&gt;xml文件的sql语句的id与Mapper接口中的方法名一致，并保持返回类型一致&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/mybatis_xml2.CYXoxhrL_ZICiln.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;xml文件中的SQL语句写法和注解相同，使用&lt;code&gt;#{}&lt;/code&gt;来代表参数占位，由函数参数传入，如果有多个，就在函数参数上加入&lt;code&gt;@Param&lt;/code&gt;注解来标明&lt;/p&gt;
&lt;h2&gt;RESTful&lt;/h2&gt;
&lt;p&gt;一种规范&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;URL定位资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HTTP请求方法描述操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET&lt;/code&gt;：&lt;code&gt;select&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE&lt;/code&gt;：&lt;code&gt;delete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST&lt;/code&gt;：&lt;code&gt;insert&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PUT&lt;/code&gt;：&lt;code&gt;update&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;实战&lt;/h2&gt;
&lt;h3&gt;日志&lt;/h3&gt;
&lt;p&gt;使用&lt;strong&gt;logback&lt;/strong&gt;包（springboot框架自带依赖）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;配置文件&lt;/p&gt;
&lt;p&gt;创建&lt;code&gt;src/main/resources/logback.xml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&amp;#x3C;configuration&gt;
    &amp;#x3C;!-- 控制台输出 --&gt;
    &amp;#x3C;appender name=&quot;STDOUT&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
        &amp;#x3C;encoder class=&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;&gt;
            &amp;#x3C;!--格式化输出：%d表示日期，%thread表示线程名，%-5level：级别从左显示5个字符宽度  %msg：日志消息，%n是换行符 --&gt;
            &amp;#x3C;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n&amp;#x3C;/pattern&gt;
        &amp;#x3C;/encoder&gt;
    &amp;#x3C;/appender&gt;

    &amp;#x3C;!-- 日志输出级别 --&gt;
    &amp;#x3C;root level=&quot;ALL&quot;&gt;
        &amp;#x3C;appender-ref ref=&quot;STDOUT&quot; /&gt;
    &amp;#x3C;/root&gt;
&amp;#x3C;/configuration&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;输出到控制台/文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;!-- 控制台输出 --&gt;
&amp;#x3C;appender name=&quot;STDOUT&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
    &amp;#x3C;encoder class=&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;&gt;
            &amp;#x3C;!--格式化输出：%d 表示日期，%thread 表示线程名，%-5level表示级别从左显示5个字符宽度，%msg表示日志消息，%n表示换行符 --&gt;
            &amp;#x3C;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n&amp;#x3C;/pattern&gt;
    &amp;#x3C;/encoder&gt;
&amp;#x3C;/appender&gt;
&amp;#x3C;!-- 按照每天生成日志文件 --&gt;
&amp;#x3C;appender name=&quot;FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&gt;
    &amp;#x3C;rollingPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&quot;&gt;
        &amp;#x3C;!-- 日志文件输出的文件名, %i表示序号 --&gt;
        &amp;#x3C;FileNamePattern&gt;D:/tlias-%d{yyyy-MM-dd}-%i.log&amp;#x3C;/FileNamePattern&gt;
        &amp;#x3C;!-- 最多保留的历史日志文件数量 --&gt;
        &amp;#x3C;MaxHistory&gt;30&amp;#x3C;/MaxHistory&gt;
        &amp;#x3C;!-- 最大文件大小，超过这个大小会触发滚动到新文件，默认为 10MB --&gt;
        &amp;#x3C;maxFileSize&gt;10MB&amp;#x3C;/maxFileSize&gt;
    &amp;#x3C;/rollingPolicy&gt;

    &amp;#x3C;encoder class=&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;&gt;
        &amp;#x3C;!--格式化输出：%d 表示日期，%thread 表示线程名，%-5level表示级别从左显示5个字符宽度，%msg表示日志消息，%n表示换行符 --&gt;
        &amp;#x3C;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n&amp;#x3C;/pattern&gt;
    &amp;#x3C;/encoder&gt;
&amp;#x3C;/appender&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开关配置&lt;/p&gt;
&lt;p&gt;&lt;code&gt;level&lt;/code&gt;可以为&lt;code&gt;ALL&lt;/code&gt;，&lt;code&gt;OFF&lt;/code&gt;，&lt;code&gt;debug&lt;/code&gt;，&lt;code&gt;info&lt;/code&gt;，&lt;code&gt;warn&lt;/code&gt;，&lt;code&gt;error&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;!-- 日志输出级别 --&gt;
&amp;#x3C;root level=&quot;ALL&quot;&gt;
    &amp;#x3C;!--输出到控制台--&gt;
    &amp;#x3C;appender-ref ref=&quot;STDOUT&quot; /&gt;
    &amp;#x3C;!--输出到文件--&gt;
    &amp;#x3C;appender-ref ref=&quot;FILE&quot; /&gt;
&amp;#x3C;/root&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用日志&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;@Slf4j&lt;/code&gt;注解：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Slf4j
public class DeptController {
    public Result list(){
		log.debug(&quot;&quot;);
        log.info(&quot;&quot;);
        log.warn(&quot;&quot;);
        log.error(&quot;&quot;);
        ...;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;响应结果封装&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.itheima.pojo;

import lombok.Data;
import java.io.Serializable;

/**
 * 后端统一返回结果
 */
@Data
public class Result {

    private Integer code; //编码：1成功，0为失败
    private String msg; //错误信息
    private Object data; //数据

    public static Result success() {
        Result result = new Result();
        result.code = 1;
        result.msg = &quot;success&quot;;
        return result;
    }

    public static Result success(Object object) {
        Result result = new Result();
        result.data = object;
        result.code = 1;
        result.msg = &quot;success&quot;;
        return result;
    }

    public static Result error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Controller&lt;/h3&gt;
&lt;h4&gt;使用不同HTTP方法请求&lt;/h4&gt;
&lt;p&gt;在方法上增加&lt;code&gt;@GetMapping&lt;/code&gt;注解，表示仅接受&lt;code&gt;GET&lt;/code&gt;请求&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 两种注解都可以
// @RequestMapping(value = &quot;/depts&quot;, method = RequestMethod.GET)
@GetMapping(&quot;/depts&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他请求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PostMapping&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PutMapping&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DeleteMapping&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;路由分组&lt;/h4&gt;
&lt;p&gt;（在&lt;strong&gt;Controller&lt;/strong&gt;类上使用&lt;code&gt;@RequestMapping&lt;/code&gt;注解，类中的所有方法都是以该路径为前缀）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RequestMapping(&quot;/depts&quot;)
@RestController
public class DeptController {
    ...
    // 相当于/depts/{id}
    @GetMapping(&quot;/{id}&quot;)
    public Result getInfo() {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;接收简单参数&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;参数形式&lt;/strong&gt;：&lt;code&gt;/xxx?id=1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;@RequestParam&lt;/code&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;code&gt;required&lt;/code&gt;属性可设置为false，表示默认不需要该参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;defaultValue&lt;/code&gt;属性设置默认值&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@DeleteMapping(&quot;/depts&quot;)
public Result deleteById(@RequestParam(&quot;id&quot;) Integer id) {
    System.out.println(&quot;根据id删除部门：&quot; + id);
    return Result.success();
}
&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-java&quot;&gt;// 请求参数类
@Data
public class EmpQueryParam {
    
    private Integer page = 1; //页码
    private Integer pageSize = 10; //每页展示记录数
    private String name; //姓名
    private Integer gender; //性别
    @DateTimeFormat(pattern = &quot;yyyy-MM-dd&quot;)
    private LocalDate begin; //入职开始时间
    @DateTimeFormat(pattern = &quot;yyyy-MM-dd&quot;)
    private LocalDate end; //入职结束时间
    
}

// 处理请求的方法, 使用类来接收传入的参数
@GetMapping
public Result page(EmpQueryParam empQueryParam) {
    log.info(&quot;查询请求参数： {}&quot;, empQueryParam);
    PageResult pageResult = empService.page(empQueryParam);
    return Result.success(pageResult);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;接收Json数据&lt;/h4&gt;
&lt;p&gt;POST发送json数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;name&quot;: &quot;name&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;@RequestBody&lt;/code&gt;注解，并指定实体对象来接收，json数据中的&lt;strong&gt;键名&lt;/strong&gt;要和对象内部&lt;strong&gt;成员名&lt;/strong&gt;对应&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@PostMapping(&quot;/depts&quot;)
public Result add(@RequestBody Dept dept) {
    System.out.println(&quot;添加部门：&quot; + dept.getName());
    deptService.addDept(dept);
    return Result.success();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;接收路径参数&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;参数形式&lt;/strong&gt;：&lt;code&gt;/depts/1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;@PathVariable&lt;/code&gt;注解（参数名和方法参数名相同可省略）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@GetMapping(&quot;/depts/{id}&quot;)
public Result getInfo(@PathVariable(&quot;id&quot;) Integer id) {
    System.out.println(&quot;根据id查询部门数据：&quot; + id);
    Dept dept = deptService.getInfo(id);
    return Result.success(dept);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Service&lt;/h3&gt;
&lt;h4&gt;事务控制&lt;/h4&gt;
&lt;p&gt;本质是一组SQL操作的集合，要成功都成功，有一个失败就全部失败&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;-- 启动事务
start transaction; /begin;
insert ...
insert ...
-- 提交
commit;
-- 失败就回滚
rollback;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spring的事务管理通常在Service层中，因为Dao层的操作都是针对单条SQL语句&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;@Transactional&lt;/code&gt;注解（业务层的方法，类，接口上都可以使用，范围从小到大分别是方法&amp;#x3C;类&amp;#x3C;接口）&lt;/p&gt;
&lt;p&gt;事务管理日志配置（&lt;code&gt;application.yml&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;#spring事务管理日志
logging: 
  level: 
    org.springframework.jdbc.support.JdbcTransactionManager: debug
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;异常回滚机制&lt;/h4&gt;
&lt;p&gt;一般只有RuntimeException（运行时异常）才会回滚事务&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;@Transactional(rollbackFor = Exception.class)&lt;/code&gt;可以指定出现何种异常类型时回滚事务（包括编译时异常）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional(rollbackFor = Exception.class)
public void doBusiness() throws Exception {
    ...
    throw new IOException(&quot;Checked Exception&quot;);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;事务传播&lt;/h4&gt;
&lt;p&gt;一个事务里面执行另外一个事务时（比如service的一个方法调用另一个方法）&lt;/p&gt;
&lt;p&gt;默认将新事务看做旧事务的一部分（新旧事务任一执行失败就全部失败）&lt;/p&gt;
&lt;p&gt;在新事务上指定&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)&lt;/code&gt;属性，原事务和新事务会被看做两个独立的事务&lt;/p&gt;
&lt;h4&gt;事务四大特性（ACID）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;原子性（Atomicity）：事务是不可分割的最小单元，要么全部成功，要么全部失败。&lt;/li&gt;
&lt;li&gt;一致性（Consistency）：事务完成时，必须使所有的数据都保持一致状态。&lt;/li&gt;
&lt;li&gt;隔离性（Isolation）：数据库系统提供的隔离机制，保证事务在不受外部并发操作影响的独立环境下运行。&lt;/li&gt;
&lt;li&gt;持久性（Durability）：事务一旦提交或回滚，它对数据库中的数据的改变就是永久的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Dao&lt;/h3&gt;
&lt;h4&gt;对查询结果进行数据封装&lt;/h4&gt;
&lt;p&gt;使用Mybatis查询返回的结果会被自动封装到定义的实体类中，但需要各个字段名一致，如果不一致，有如下三种解决办法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;@Results&lt;/code&gt;注解（&lt;code&gt;column&lt;/code&gt;表示表列名，&lt;code&gt;property&lt;/code&gt;表示实体类成员名）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Results({
    @Result(column = &quot;create_time&quot;, property = &quot;createTime&quot;),
    @Result(column = &quot;update_time&quot;, property = &quot;updateTime&quot;)
})
@Select(&quot;select id, name, create_time, update_time from dept order by update_time desc&quot;)
List&amp;#x3C;Dept&gt; findAll();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在SQL语句中为字段取别名（设置为实体类成员名）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Select(&quot;select id, name, create_time as createTime, update_time as updateTime from dept order by update_time desc&quot;)
List&amp;#x3C;Dept&gt; findAll();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置中开启驼峰命名转化（需要正确命名实体类成员名）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;mybatis:
  configuration:
    map-underscore-to-camel-case: true
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;多参数传递对应占位符&lt;/h4&gt;
&lt;p&gt;重点说一下方法参数是类的情况，同样使用&lt;code&gt;@Param&lt;/code&gt;注解为入参取名，然后放入SQL语句占位符中&lt;/p&gt;
&lt;p&gt;假如参数是两个类对象：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;注解写法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Mapper
public interface UserMapper {
	// 访问userInfo的age成员 department的id成员
    @Select(&quot;SELECT * FROM user WHERE info.age = #{userInfo.age} AND dep.id = #{department.id}&quot;)
    User selectUser(@Param(&quot;userInfo&quot;) UserInfo userInfo,
                    @Param(&quot;department&quot;) Department department);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;xml写法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;select id=&quot;selectUser&quot; resultType=&quot;User&quot;&gt;
    SELECT *
    FROM user
    WHERE age = #{userInfo.age}
      AND dep_id = #{department.id}
&amp;#x3C;/select&gt;
&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;用SQL（三个参数）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;count：获取总记录数（用select count(*)）&lt;/li&gt;
&lt;li&gt;page：查询的页码&lt;/li&gt;
&lt;li&gt;pageSize：每页记录数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由此可得limit参数&lt;code&gt;start = (page - 1) * pageSize&lt;/code&gt;，&lt;code&gt;pageSize&lt;/code&gt;（从第几条记录开始查，查多少条）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pagehelper插件（无需手动分页）：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pom.xml&lt;/code&gt;引入依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;!--分页插件PageHelper--&gt;
&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;com.github.pagehelper&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;pagehelper-spring-boot-starter&amp;#x3C;/artifactId&gt;
    &amp;#x3C;version&gt;1.4.7&amp;#x3C;/version&gt;
&amp;#x3C;/dependency&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public PageResult page(Integer page, Integer pageSize) {
    //1. 设置分页参数
    PageHelper.startPage(page,pageSize);

    //2. 执行查询（不带limit的查询实现）
    List&amp;#x3C;Emp&gt; empList = empMapper.list();
    Page&amp;#x3C;Emp&gt; p = (Page&amp;#x3C;Emp&gt;) empList;

    //3. 封装结果
    return new PageResult(p.getTotal(), p.getResult());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PageHelper只会对紧跟在其后的第一条SQL语句进行分页处理：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;条件分页&lt;/p&gt;
&lt;p&gt;即在SQL语句中使用条件模糊匹配，继续复用pagehelper&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;主键返回功能&lt;/h4&gt;
&lt;p&gt;Mybatis提供，执行&lt;code&gt;insert&lt;/code&gt;语句后，自动将生成的主键封装回数据：&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;Options&lt;/code&gt;注解，&lt;code&gt;keyProperty&lt;/code&gt;属性标识封装回&lt;code&gt;emp&lt;/code&gt;的成员名称，插入语句完成后会将&lt;code&gt;id&lt;/code&gt;封装回&lt;code&gt;emp&lt;/code&gt;对象中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Options(useGeneratedKeys = true, keyProperty = &quot;id&quot;, keyColumn = &quot;id&quot;)
@Insert(&quot;insert into emp(username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) &quot; +
        &quot;values (#{username},#{name},#{gender},#{phone},#{job},#{salary},#{image},#{entryDate},#{deptId},#{createTime},#{updateTime})&quot;)
void insert(Emp emp);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;mapper xml文件标签&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;基本标签：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;mapper&gt;&lt;/code&gt;标签：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;namespace&lt;/code&gt;：值是java文件中定义的mapper类路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;select&gt;&lt;/code&gt;标签，标识查询语句&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;：值是类中的方法名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resultType&lt;/code&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;p&gt;&lt;code&gt;&amp;#x3C;where&gt;&lt;/code&gt;标签，表述SQL中的&lt;code&gt;where&lt;/code&gt;条件，并去除最终语句中多余的&lt;code&gt;and&lt;/code&gt;和&lt;code&gt;or&lt;/code&gt;关键字&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;set&gt;&lt;/code&gt;标签，表示SQL中的&lt;code&gt;set&lt;/code&gt;关键字，设置值并去除最终语句中多余的&lt;code&gt;,&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;if&gt;&lt;/code&gt;标签，当&lt;code&gt;test&lt;/code&gt;条件为真时使用定义的SQL语句，否则不使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;foreach&gt;&lt;/code&gt;标签，遍历&lt;code&gt;collection&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;code&gt;&amp;#x3C;resultMap&gt;&lt;/code&gt;标签：一对多查询（一个表的一条记录对应另一个表的多个记录）使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;select + where + if&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;!--定义Mapper映射文件的约束和基本结构--&gt;
&amp;#x3C;!DOCTYPE mapper
        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;

&amp;#x3C;mapper namespace=&quot;com.itheima.mapper.EmpMapper&quot;&gt;
    &amp;#x3C;select id=&quot;list&quot; resultType=&quot;com.itheima.pojo.Emp&quot;&gt;
        select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id
        &amp;#x3C;where&gt;
            &amp;#x3C;if test=&quot;name != null and name != &apos;&apos;&quot;&gt;
                e.name like concat(&apos;%&apos;,#{name},&apos;%&apos;)
            &amp;#x3C;/if&gt;
            &amp;#x3C;if test=&quot;gender != null&quot;&gt;
                and e.gender = #{gender}
            &amp;#x3C;/if&gt;
            &amp;#x3C;if test=&quot;begin != null and end != null&quot;&gt;
                and e.entry_date between #{begin} and #{end}
            &amp;#x3C;/if&gt;
        &amp;#x3C;/where&gt;
    &amp;#x3C;/select&gt;
&amp;#x3C;/mapper&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;insert + foreach&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;
&amp;#x3C;!DOCTYPE mapper
        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;
&amp;#x3C;mapper namespace=&quot;com.itheima.mapper.EmpExprMapper&quot;&gt;

    &amp;#x3C;!--批量插入员工工作经历信息--&gt;
    &amp;#x3C;insert id=&quot;insertBatch&quot;&gt;
        insert into emp_expr (emp_id, begin, end, company, job) values
        &amp;#x3C;foreach collection=&quot;exprList&quot; item=&quot;expr&quot; separator=&quot;,&quot;&gt;
            (#{expr.empId}, #{expr.begin}, #{expr.end}, #{expr.company}, #{expr.job})
        &amp;#x3C;/foreach&gt;
    &amp;#x3C;/insert&gt;

&amp;#x3C;/mapper&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;collection：集合名称&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;item：集合遍历出来的元素/项&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;separator：每一次遍历使用的分隔符&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;open：遍历开始前要拼接的字符串&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;close：遍历结束后要拼接的字符串&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;select + resultMap&lt;/h5&gt;
&lt;p&gt;一个员工信息对应多条工作经历&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;!--自定义结果集ResultMap--&gt;
&amp;#x3C;resultMap id=&quot;empResultMap&quot; type=&quot;com.itheima.pojo.Emp&quot;&gt;
    &amp;#x3C;!--封装员工信息--&gt;
    &amp;#x3C;id column=&quot;id&quot; property=&quot;id&quot; /&gt;
    &amp;#x3C;result column=&quot;username&quot; property=&quot;username&quot; /&gt;
    &amp;#x3C;result column=&quot;password&quot; property=&quot;password&quot; /&gt;
    &amp;#x3C;result column=&quot;name&quot; property=&quot;name&quot; /&gt;
    &amp;#x3C;result column=&quot;gender&quot; property=&quot;gender&quot; /&gt;
    &amp;#x3C;result column=&quot;phone&quot; property=&quot;phone&quot; /&gt;
    &amp;#x3C;result column=&quot;job&quot; property=&quot;job&quot; /&gt;
    &amp;#x3C;result column=&quot;salary&quot; property=&quot;salary&quot; /&gt;
    &amp;#x3C;result column=&quot;image&quot; property=&quot;image&quot; /&gt;
    &amp;#x3C;result column=&quot;entry_date&quot; property=&quot;entryDate&quot; /&gt;
    &amp;#x3C;result column=&quot;dept_id&quot; property=&quot;deptId&quot; /&gt;
    &amp;#x3C;result column=&quot;create_time&quot; property=&quot;createTime&quot; /&gt;
    &amp;#x3C;result column=&quot;update_time&quot; property=&quot;updateTime&quot; /&gt;

    &amp;#x3C;!--封装exprList--&gt;
    &amp;#x3C;collection property=&quot;exprList&quot; ofType=&quot;com.itheima.pojo.EmpExpr&quot;&gt;
        &amp;#x3C;id column=&quot;ee_id&quot; property=&quot;id&quot;/&gt;
        &amp;#x3C;result column=&quot;ee_company&quot; property=&quot;company&quot;/&gt;
        &amp;#x3C;result column=&quot;ee_job&quot; property=&quot;job&quot;/&gt;
        &amp;#x3C;result column=&quot;ee_begin&quot; property=&quot;begin&quot;/&gt;
        &amp;#x3C;result column=&quot;ee_end&quot; property=&quot;end&quot;/&gt;
        &amp;#x3C;result column=&quot;ee_empid&quot; property=&quot;empId&quot;/&gt;
    &amp;#x3C;/collection&gt;
&amp;#x3C;/resultMap&gt;

&amp;#x3C;!--根据ID查询员工的详细信息--&gt;
&amp;#x3C;select id=&quot;getById&quot; resultMap=&quot;empResultMap&quot;&gt;
    select e.*,
        ee.id ee_id,
        ee.emp_id ee_empid,
        ee.begin ee_begin,
        ee.end ee_end,
        ee.company ee_company,
        ee.job ee_job
    from emp e left join emp_expr ee on e.id = ee.emp_id
    where e.id = #{id}
&amp;#x3C;/select&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;update + set&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;!--根据ID更新员工信息--&gt;
&amp;#x3C;update id=&quot;updateById&quot;&gt;
    update emp
    &amp;#x3C;set&gt;
        &amp;#x3C;if test=&quot;username != null and username != &apos;&apos;&quot;&gt;username = #{username},&amp;#x3C;/if&gt;
        &amp;#x3C;if test=&quot;password != null and password != &apos;&apos;&quot;&gt;password = #{password},&amp;#x3C;/if&gt;
        &amp;#x3C;if test=&quot;name != null and name != &apos;&apos;&quot;&gt;name = #{name},&amp;#x3C;/if&gt;
        &amp;#x3C;if test=&quot;gender != null&quot;&gt;gender = #{gender},&amp;#x3C;/if&gt;
        &amp;#x3C;if test=&quot;phone != null and phone != &apos;&apos;&quot;&gt;phone = #{phone},&amp;#x3C;/if&gt;
        &amp;#x3C;if test=&quot;job != null&quot;&gt;job = #{job},&amp;#x3C;/if&gt;
        &amp;#x3C;if test=&quot;salary != null&quot;&gt;salary = #{salary},&amp;#x3C;/if&gt;
        &amp;#x3C;if test=&quot;image != null and image != &apos;&apos;&quot;&gt;image = #{image},&amp;#x3C;/if&gt;
        &amp;#x3C;if test=&quot;entryDate != null&quot;&gt;entry_date = #{entryDate},&amp;#x3C;/if&gt;
        &amp;#x3C;if test=&quot;deptId != null&quot;&gt;dept_id = #{deptId},&amp;#x3C;/if&gt;
        &amp;#x3C;if test=&quot;updateTime != null&quot;&gt;update_time = #{updateTime},&amp;#x3C;/if&gt;
    &amp;#x3C;/set&gt;
    where id = #{id}
&amp;#x3C;/update&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;全局异常处理器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;定义一个异常处理类，使用注解&lt;code&gt;@RestControllerAdvice&lt;/code&gt;，代表定义了全局异常处理器&lt;/li&gt;
&lt;li&gt;在类中，定义一个方法来捕获异常：使用注解&lt;code&gt;@ExceptionHandler&lt;/code&gt;。通过&lt;code&gt;@ExceptionHandler&lt;/code&gt;注解当中的value属性来指定我们要捕获的是哪一类型的异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestControllerAdvice
public class GlobalExceptionHandler {
    
    //处理异常
    @ExceptionHandler
    public Result ex(Exception e){//方法形参中指定能够处理的异常类型
        e.printStackTrace();//打印堆栈中的异常信息
        //捕获到异常之后，响应一个标准的Result
        return Result.error(&quot;对不起,操作失败,请联系管理员&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;校验&lt;/h3&gt;
&lt;h4&gt;JWT&lt;/h4&gt;
&lt;p&gt;HTTP无状态，多次请求-响应之间独立&lt;/p&gt;
&lt;p&gt;为了让同一会话之间的多次请求之间共享数据（比如是否已经登录）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Cookie：服务器下发，客户端保存&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 先请求/c1，再请求/c2，c2就能拿到c1下发的cookie
@Slf4j
@RestController
public class SessionController {

    //设置Cookie
    @GetMapping(&quot;/c1&quot;)
    public Result cookie1(HttpServletResponse response){
        response.addCookie(new Cookie(&quot;login_username&quot;,&quot;itheima&quot;)); //设置Cookie/响应Cookie
        return Result.success();
    }
        
    //获取Cookie
    @GetMapping(&quot;/c2&quot;)
    public Result cookie2(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if(cookie.getName().equals(&quot;login_username&quot;)){
                System.out.println(&quot;login_username: &quot;+cookie.getValue()); //输出name为login_username的cookie
            }
        }
        return Result.success();
    }
}    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;移动端APP不适用&lt;/li&gt;
&lt;li&gt;客户端明文存储，不安全，且用户可禁用Cookie&lt;/li&gt;
&lt;li&gt;不支持跨域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Session：Cookie升级版，服务器下发，服务器存储&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 请求/s1，如果是初次请求，就生成session id，设置在cookie返回给客户端
// 再请求/s2，通过客户端传来的session id，服务器找到对应的session
// 从session中取数据

@Slf4j
@RestController
public class SessionController {

    @GetMapping(&quot;/s1&quot;)
    public Result session1(HttpSession session){
        log.info(&quot;HttpSession-s1: {}&quot;, session.hashCode());

        session.setAttribute(&quot;loginUser&quot;, &quot;tom&quot;); //往session中存储数据
        return Result.success();
    }

    @GetMapping(&quot;/s2&quot;)
    public Result session2(HttpServletRequest request){
        HttpSession session = request.getSession();
        log.info(&quot;HttpSession-s2: {}&quot;, session.hashCode());

        Object loginUser = session.getAttribute(&quot;loginUser&quot;); //从session中获取数据
        log.info(&quot;loginUser: {}&quot;, loginUser);
        return Result.success(loginUser);
    }
}
&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;/li&gt;
&lt;li&gt;客户端可以禁用Cookie，使Session无效&lt;/li&gt;
&lt;li&gt;移动端APP不能用&lt;/li&gt;
&lt;li&gt;不能跨域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;jwt令牌技术&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;服务器生成一个字符串（包含共享信息），下发给客户端，客户端可以自己选择存储方式&lt;/p&gt;
&lt;p&gt;当客户端需要使用这些共享信息，就在请求中附加该字符串，由服务器验证是否有效&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持PC、移动端&lt;/li&gt;
&lt;li&gt;支持集群使用&lt;/li&gt;
&lt;li&gt;减轻服务器存储压力&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;组成&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Header(头）， 记录令牌类型、签名算法等。 例如：{&quot;alg&quot;:&quot;HS256&quot;,&quot;type&quot;:&quot;JWT&quot;}&lt;/li&gt;
&lt;li&gt;Payload(有效载荷），携带一些自定义信息、默认信息等。 例如：{&quot;id&quot;:&quot;1&quot;,&quot;username&quot;:&quot;Tom&quot;}&lt;/li&gt;
&lt;li&gt;Signature(签名），防止Token被篡改、确保安全性。将header、payload，并加入指定秘钥，通过指定签名算法计算而来。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;生成&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;引入依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;!-- JWT依赖--&gt;
&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;io.jsonwebtoken&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;jjwt&amp;#x3C;/artifactId&gt;
    &amp;#x3C;version&gt;0.9.1&amp;#x3C;/version&gt;
&amp;#x3C;/dependency&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// claims是jwt中的payload部分，即共享信息，使用map存储
// signWith(algo, key)，指定哈希算法生成签名，并指定密钥
// addClaims(claims) 添加负载
// setExpiration() 设置过期时间
// compact() 生成对应字符串
@Test
public void testGenJwt() {
    Map&amp;#x3C;String, Object&gt; claims = new HashMap&amp;#x3C;&gt;();
    claims.put(&quot;id&quot;, 10);
    claims.put(&quot;username&quot;, &quot;itheima&quot;);

    String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, &quot;aXRjYXN0&quot;)
        .addClaims(claims)
        .setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))
        .compact();

    System.out.println(jwt);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;校验&lt;/h5&gt;
&lt;p&gt;需要使用生成时同样的密钥来做校验&lt;/p&gt;
&lt;p&gt;对字符串进行了篡改或令牌失效都会导致校验失败（报异常）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void testParseJwt() {
    Claims claims = Jwts.parser().setSigningKey(&quot;aXRjYXN0&quot;)
        .parseClaimsJws(&quot;eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiaXRoZWltYSIsImV4cCI6MTcwMTkwOTAxNX0.N-MD6DmoeIIY5lB5z73UFLN9u7veppx1K5_N_jS9Yko&quot;)
        .getBody();
    System.out.println(claims);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;过滤器 Filter&lt;/h4&gt;
&lt;p&gt;拦截请求（所有发送的请求，包括对静态资源的请求），不属于Spring组件&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类上使用&lt;code&gt;@WebFilter(urlPatterns = &quot;&quot;)&lt;/code&gt;注解，附带要拦截的请求路径&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现&lt;code&gt;Filter&lt;/code&gt;接口&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@WebFilter(urlPatterns = &quot;/*&quot;) //配置过滤器要拦截的请求路径（ /* 表示拦截浏览器的所有请求 ）
public class DemoFilter implements Filter {
    //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println(&quot;init ...&quot;);
    }

    //拦截到请求时,调用该方法,可以调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        System.out.println(&quot;拦截到了请求...&quot;);
    }

    //销毁方法, web服务器关闭时调用, 只调用一次
    public void destroy() {
        System.out.println(&quot;destroy ... &quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Springboot启动类上添加&lt;code&gt;@ServletComponentScan&lt;/code&gt;注解&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;拦截后放行：&lt;code&gt;chain.doFilter(request, response); return;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拦截后不放行：&lt;code&gt;response.setStatus(); return;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@WebFilter(urlPatterns = &quot;&quot;)&lt;/code&gt;注解配置拦截路径，前缀不同的拦截路径不会同时发挥作用：&lt;/p&gt;
&lt;p&gt;比如&lt;code&gt;/dept&lt;/code&gt;和&lt;code&gt;/login&lt;/code&gt;，请求&lt;code&gt;/dept&lt;/code&gt;不会触发&lt;code&gt;/login&lt;/code&gt;的拦截器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;前缀相同的拦截器的执行顺序：比如&lt;code&gt;/*&lt;/code&gt;和&lt;code&gt;/login&lt;/code&gt;，默认按类名排序（字典序靠前就先执行），手动配置需要xml文件&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;拦截器 Interceptor&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Spring组件，可直接注入&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只拦截&lt;code&gt;Controller&lt;/code&gt;层请求&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定义（新建&lt;code&gt;interceptor&lt;/code&gt;包）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实现&lt;code&gt;HandlerInterceptor&lt;/code&gt;接口，并重写方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;//自定义拦截器
@Component
public class DemoInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行。 返回true：放行    返回false：不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(&quot;preHandle .... &quot;);
        
        return true; //true表示放行
    }

    //目标资源方法执行后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(&quot;postHandle ... &quot;);
    }

    //视图渲染完毕后执行，最后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(&quot;afterCompletion .... &quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;interceptor&lt;/code&gt;包上级目录创建&lt;code&gt;WebConfig&lt;/code&gt;类，实现&lt;code&gt;WebMvcConfigurer&lt;/code&gt;接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration  
public class WebConfig implements WebMvcConfigurer {

    //自定义的拦截器对象
    @Autowired
    private DemoInterceptor demoInterceptor;

    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注册自定义拦截器对象
        registry.addInterceptor(demoInterceptor).addPathPatterns(&quot;/**&quot;);//设置拦截器拦截的请求路径（ /** 表示拦截所有请求）
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置类注解&lt;code&gt;@Configuration&lt;/code&gt;：类上加上该注解表明该类是一个配置类，Springboot程序启动时会自动执行里面的某些方法（在这是实现了&lt;code&gt;WebMvcConfigurer&lt;/code&gt;的&lt;code&gt;addInteceptors&lt;/code&gt;方法：注册拦截器以生效）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拦截路径：&lt;code&gt;addPathPatterns(&quot;url&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不拦截路径：&lt;code&gt;excludePathPatterns(&quot;url&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AOP&lt;/h3&gt;
&lt;h4&gt;简介&lt;/h4&gt;
&lt;p&gt;基于动态代理的方法，在不修改原代码的情况下，增强功能&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pom依赖：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;org.springframework.boot&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;spring-boot-starter-aop&amp;#x3C;/artifactId&gt;
&amp;#x3C;/dependency&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;p&gt;类上加&lt;code&gt;@Aspect&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类中方法加&lt;code&gt;@Around(&quot;execution...&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在里面通过&lt;code&gt;ProceedingJoinPoint&lt;/code&gt;对象执行原始方法，并增添新功能&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component
@Aspect //当前类为切面类
@Slf4j
public class RecordTimeAspect {
	// 代理某个方法
    @Around(&quot;execution(* com.itheima.service.impl.DeptServiceImpl.*(..))&quot;)
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        // 执行原始方法之前
        
        // 执行原始方法
        Object result = pjp.proceed();
        // 执行原始方法之后
        
        // 返回原始方法的结果
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**应用场景：**一般用于增强业务层（Service）方法的功能（日志记录、权限控制、事务管理）&lt;/p&gt;
&lt;h4&gt;概念&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;连接点：&lt;code&gt;JointPoint&lt;/code&gt;，指可被AOP控制的方法，通俗点就是业务层&lt;code&gt;Bean&lt;/code&gt;的&lt;code&gt;public&lt;/code&gt;方法&lt;/li&gt;
&lt;li&gt;通知：&lt;code&gt;Advice&lt;/code&gt;，AOP层的方法，要在里面完成增强的功能，同时代理连接点的功能&lt;/li&gt;
&lt;li&gt;切入点：&lt;code&gt;PointCut&lt;/code&gt;，匹配连接点的条件，&lt;code&gt;@Around(&quot;execution(...)&quot;)&lt;/code&gt;就描述了一个切入点表达式，表明要代理哪个方法&lt;/li&gt;
&lt;li&gt;切面：&lt;code&gt;Aspect&lt;/code&gt;，一组通知和切入点的集合&lt;/li&gt;
&lt;li&gt;目标：&lt;code&gt;Target&lt;/code&gt;，理解为切入点所代理的方法所在的&lt;code&gt;Bean&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;code&gt;@Around&lt;/code&gt;：AOP层方法参数是&lt;code&gt;ProceedingJoinPoint&lt;/code&gt;类型，需要在里面调用&lt;code&gt;proceed()&lt;/code&gt;来执行被代理的方法，代理方法前后都可以写自定义的功能&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Before&lt;/code&gt;：AOP层方法参数是&lt;code&gt;JointPoint&lt;/code&gt;类型，只需要写自定义的功能，被代理方法前会执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@After&lt;/code&gt;：同上，被代理方法后执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterReturning&lt;/code&gt;：同上，被代理方法&lt;strong&gt;正常返回&lt;/strong&gt;后会执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterThrowing&lt;/code&gt;：同上，被代理方法&lt;strong&gt;出现异常&lt;/strong&gt;时才会执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;预定义不同切入点（使用&lt;code&gt;@PointCut&lt;/code&gt;注解）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;//切入点方法（公共的切入点表达式）
@Pointcut(&quot;execution(* com.itheima.service.*.*(..))&quot;)
public void pt(){}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更改通知注解形式：&lt;code&gt;@Around(&quot;pt()&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;执行顺序&lt;/h4&gt;
&lt;p&gt;多个切面类使用了相同的切入点，执行顺序默认按切面类类名升序执行&lt;/p&gt;
&lt;p&gt;要指定顺序，在切面类上使用&lt;code&gt;@Order(num)&lt;/code&gt;，指定num的相对大小&lt;/p&gt;
&lt;h4&gt;切入点表达式&lt;/h4&gt;
&lt;p&gt;指AOP方法注解里面那一坨&lt;code&gt;execution(...)&lt;/code&gt;，作用是指定被代理的方法，有两种写法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;访问修饰符（&lt;code&gt;public&lt;/code&gt;），包名.类名.，throws 异常这些都可以省略，但不建议省略包名.类名&lt;/li&gt;
&lt;li&gt;可以用&lt;code&gt;*&lt;/code&gt;匹配&lt;strong&gt;一个&lt;/strong&gt;独立的任意符号，可以是修饰符，一个包名，一个类名，任意类型的一个参数&lt;/li&gt;
&lt;li&gt;可以用&lt;code&gt;..&lt;/code&gt;匹配任意个独立的符号&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;/li&gt;
&lt;li&gt;例子：&lt;code&gt;execution(* com.itheima.service.DeptService.*(..))&lt;/code&gt;匹配&lt;code&gt;service&lt;/code&gt;包下的&lt;code&gt;DeptService&lt;/code&gt;接口中的任意方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@annotation(自定义注解全类名)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;需要自定义注解，一般放在&lt;code&gt;anno&lt;/code&gt;包下&lt;/li&gt;
&lt;li&gt;在业务层方法需要代理的方法上加上自定义的注解&lt;/li&gt;
&lt;li&gt;AOP方法通知注解里的&lt;code&gt;execution&lt;/code&gt;换成&lt;code&gt;@annotation(自定义注解全类名)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;SpringBoot&lt;/h3&gt;
&lt;h4&gt;作用域&lt;/h4&gt;
&lt;p&gt;IOC容器中管理的&lt;code&gt;Bean&lt;/code&gt;默认是单例（只有一个对象）&lt;/p&gt;
&lt;p&gt;如果要求管理的&lt;code&gt;Bean&lt;/code&gt;有多个实例：&lt;/p&gt;
&lt;p&gt;在使用&lt;code&gt;@Component&lt;/code&gt;注解的类上&lt;strong&gt;添加&lt;/strong&gt;一个注解&lt;code&gt;@Scope(&quot;prototype&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;之后每一次使用该&lt;code&gt;@Bean&lt;/code&gt;时会创建一个新的实例&lt;/p&gt;
&lt;h4&gt;第三方Bean管理&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;添加一个配置类&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置类&lt;/strong&gt;上使用注解**&lt;code&gt;@Configuration&lt;/code&gt;**标识该类是一个配置类&lt;/li&gt;
&lt;li&gt;定义一个方法，返回值是要让IOC容器管理的对象，在&lt;strong&gt;方法&lt;/strong&gt;上使用注解**&lt;code&gt;@Bean&lt;/code&gt;**&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.itheima.config;

import com.itheima.utils.AliyunOSSOperator;
import com.itheima.utils.AliyunOSSProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OSSConfig {
    @Bean
    public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties ossProperties) {
        return new AliyunOSSOperator(ossProperties);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果方法有参数，Spring会自动去IOC容器里面找对应类型的数据&lt;/p&gt;
&lt;h4&gt;起步依赖和自动配置&lt;/h4&gt;
&lt;p&gt;起步依赖就是形如&lt;code&gt;spring-boot-starter-xxxx&lt;/code&gt;（官方starter）或者&lt;code&gt;xxxx-spring-boot-starter&lt;/code&gt;（第三方starter）的整合包，里面设置了一组依赖包，在引入starter时会根据maven包管理自动引入所有依赖包&lt;/p&gt;
&lt;p&gt;自动配置指的是当在maven包管理引入包，Spring容器启动后，包中的一些&lt;strong&gt;配置类&lt;/strong&gt;，&lt;strong&gt;bean对象&lt;/strong&gt;自动存入到容器中，无需手动做上述的Bean管理&lt;/p&gt;
&lt;p&gt;实现方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在Springboot启动类上使用**&lt;code&gt;@Import&lt;/code&gt;**注解&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;导入普通类：&lt;code&gt;@Import(Gson.class)&lt;/code&gt;，该类会实例化成为一个Bean被Spring容器管理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;导入配置类：&lt;code&gt;@Import(Config.class)&lt;/code&gt;，参考“第三方Bean管理”的配置类，类中所有配置&lt;code&gt;@Bean&lt;/code&gt;的方法都会实例化成为一个Bean被Spring容器管理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;导入&lt;code&gt;ImportSelector&lt;/code&gt;接口实现类：&lt;/p&gt;
&lt;p&gt;实现&lt;code&gt;ImportSelector&lt;/code&gt;接口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //返回值字符串数组（数组中封装了全限定名称的类）
        return new String[]{&quot;com.example.HeaderConfig&quot;};
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导入：&lt;code&gt;@Import(MyImportSelector.class)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三方依赖通常会封装自己的&lt;code&gt;@Import&lt;/code&gt;，对外提供**&lt;code&gt;@EnableXXXX&lt;/code&gt;**注解，在启动类上使用该注解，相当于导入了第三方的配置类和Bean对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)//指定要导入哪些bean对象或配置类
public @interface EnableHeaderConfig { 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;自动配置原理&lt;/h5&gt;
&lt;p&gt;在启动类上的&lt;code&gt;@SpringBootApplication&lt;/code&gt;注解，这个注解封装了下面三个注解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;@SpringBootConfiguration&lt;/code&gt;：该注解又封装了&lt;code&gt;@Configuration&lt;/code&gt;，声明当前类是一个配置类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ComponentScan&lt;/code&gt;：通过该注解可以扫描启动类所在包及其子包中的被&lt;code&gt;@Component&lt;/code&gt;标注的类，实例化为Bean对象管理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@EnableAutoConfiguration&lt;/code&gt;：&lt;strong&gt;重点&lt;/strong&gt;，封装了&lt;code&gt;@Import&lt;/code&gt;注解，会读取当前项目下所有依赖jar包中&lt;code&gt;META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports&lt;/code&gt;文件里面定义的配置类，然后导入这些配置类&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;导入配置类时，配置类中定义的Bean不会一次性全部导入，由&lt;code&gt;@Conditional&lt;/code&gt;注解来管理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@ConditionalOnClass&lt;/code&gt;：判断环境中有对应字节码文件，才注册bean到IOC容器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ConditionalOnMissingBean&lt;/code&gt;：判断环境中没有对应的bean(类型或名称)，才注册bean到IOC容器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ConditionalOnProperty&lt;/code&gt;：判断配置文件中有对应属性和值，才注册bean到IOC容器&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>modern C++ 个人总结</title><link>https://liang-bk.github.io/blog/modern-cpp</link><guid isPermaLink="true">https://liang-bk.github.io/blog/modern-cpp</guid><description>记录现代c++(11/14/17/20)中的特性，使用一些简明的例子来展示</description><pubDate>Wed, 30 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;reference（引用）&lt;/h2&gt;
&lt;p&gt;c++中的引用是一种给变量起别名的方法，类似于指针，原变量和引用将指向同一块内存地址&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;void add_three(int &amp;#x26;a);
int a = 10;
int &amp;#x26;b = a;	// b是a的别名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;指针引用&lt;/strong&gt;：顾名思义是对指针变量起别名，其形式通常为&lt;code&gt;type* &amp;#x26;x&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 可以这么理解，&amp;#x26;b代表b是一个引用, 而int*是这个引用的类型
void change_pointer(int* &amp;#x26;b);

int* a = new int;
int* &amp;#x26;b = a;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;move_semantics（移动语义）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;移动语义是指在c++对象中移动资源的这一动作，普通的对象拷贝可能花费不小的时间和空间资源，而&lt;strong&gt;移动语义可以直接将对象A的资源转移给对象B，同时A的资源被清零&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由于移动针对**类对象（class object）**之间的过程，所以对普通类型（如int，double，裸指针）之间使用移动语义并不能移动资源，移动语义也不应该在普通类型中进行使用&lt;/p&gt;
&lt;p&gt;移动语义离不开下面要介绍的左值和右值的概念，在c++中，任何一个&lt;strong&gt;表达式&lt;/strong&gt;要么是左值要么是右值，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// a 是左值, 10 是右值
int a = 10;
// b 是左值, a * 2 是右值
int b = a * 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;lvalue（左值）and rvalue（右值）&lt;/h3&gt;
&lt;p&gt;官方定义是，左值表示在内存中占据某个可识别位置的表达式（在表达式被计算完毕后仍然能够存在）&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int add(int x, int y);
int a = 5;
int *p = &amp;#x26;a;
int b = add(1, 3);
int &amp;#x26;c = b;
vector &amp;#x26;&amp;#x26;d = {1,2,3}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中，&lt;code&gt;=&lt;/code&gt;左边的表达式（变量）都是左值（&lt;code&gt;vector &amp;#x26;&amp;#x26;&lt;/code&gt;表示一个&lt;code&gt;vector&lt;/code&gt;的右值引用，后面会介绍）&lt;/p&gt;
&lt;p&gt;与左值相反，是那些表达式被计算完毕后，无法继续存在的临时值，如字面量（字符串除外），或者一个函数的返回结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int add(int x, int y);
10;
add(1, 3);
2 * 4 + add(1, 3);
int x = 1, y = 2;
x * y;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的&lt;code&gt;10&lt;/code&gt;，&lt;code&gt;add(1, 3)&lt;/code&gt;，&lt;code&gt;x * y&lt;/code&gt;都是右值&lt;/p&gt;
&lt;h3&gt;左值引用和右值引用&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;reference&lt;/code&gt;之前已经介绍过，就是给变量起别名&lt;/p&gt;
&lt;p&gt;左值引用就是给左值变量起别名&lt;/p&gt;
&lt;p&gt;右值引用就是给右值变量起别名，但右值在表达式结束后不应该存在，所以现代C++新的规则就是右值引用会延长引用的右值的生命周期，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 如果a是int类型，那么10在赋值表达式结束后，生命周期结束
int &amp;#x26;&amp;#x26;a = 10;
// 现在a是一个右值引用，并且引用了10，那么10在内存中不会消失，直到a消失后其再消失

// getString()会返回一个临时的string对象，如果s1是一个string类型，那么该对象在赋值结束后，生命周期结束
string getString();
string &amp;#x26;&amp;#x26;s1 = getString();
// 现在这个对象被s1所引用，同样的，该临时变量的生命周期只有在s1结束后才结束
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;tips&lt;/strong&gt;：一个右值引用的变量仍然是一个左值（因为在赋值表达式计算完毕后其仍能存在）&lt;/p&gt;
&lt;h3&gt;std::move&lt;/h3&gt;
&lt;p&gt;头文件&lt;code&gt;&amp;#x3C;utility&gt;&lt;/code&gt;中的&lt;code&gt;std::move&lt;/code&gt;函数负责将一个左值无条件的转为右值，除此之外其什么都不做（不做资源的实际转移）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 定义一个vector对象
std::vector&amp;#x3C;int&gt; stealing_ints = {1, 2, 3}

// std::move()将stealing_ints变为一个右值，不产生新对象，也不改变原对象的内容
std::vector&amp;#x3C;int&gt; &amp;#x26;&amp;#x26;rvalue_stealing_ints = std::move(stealing_ints);
// rvalue_stealing_ints是绑定在该右值的一个右值引用

// 因为是引用，所以可以同时使用stealing_ints和rvalue_stealing_ints来访问或修改该对象
std::cout &amp;#x3C;&amp;#x3C; &quot;Printing from stealing_ints: &quot; &amp;#x3C;&amp;#x3C; stealing_ints[1] &amp;#x3C;&amp;#x3C; std::endl;
&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;// 定义一个vector
std::vector&amp;#x3C;int&gt; int_array = {1, 2, 3, 4};

// 将int_array的资源移动到stealing_ints上 
std::vector&amp;#x3C;int&gt; stealing_ints = std::move(int_array);
// 该表达式结束之后, int_array的资源就被清空了（如底层指针变为nullptr）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码首先将&lt;code&gt;int_array&lt;/code&gt;转为一个右值，然后将其资源转移到&lt;code&gt;stealing_ints&lt;/code&gt;中，在此表达时候不应该直接使用&lt;code&gt;int_array&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;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;void move_add_three_and_print(std::vector&amp;#x3C;int&gt; &amp;#x26;&amp;#x26;vec) {
  // 调用vector的移动构造函数，转移资源
  std::vector&amp;#x3C;int&gt; vec1 = std::move(vec);
  vec1.push_back(3);
  for (const int &amp;#x26;item : vec1) {
    std::cout &amp;#x3C;&amp;#x3C; item &amp;#x3C;&amp;#x3C; &quot; &quot;;
  }
  std::cout &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
}
int main() {
	std::vector&amp;#x3C;int&gt; int_array2 = {1, 2, 3, 4};
    // vec右值引用，引用了int_array2
    move_add_three_and_print(std::move(int_array2));
    // 之后不应该直接使用int_array2,因为其资源已经被移动
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么要在函数中再次调用&lt;code&gt;std::move&lt;/code&gt;？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vec&lt;/code&gt;是一个右值引用，但其本身是一个左值，而移动构造函数接受一个右值作为参数，所以需要将&lt;code&gt;vec&lt;/code&gt;先转为右值再赋值给&lt;code&gt;vec1&lt;/code&gt;才能够正确转移，否则会调用&lt;code&gt;vector&lt;/code&gt;的拷贝构造函数&lt;/p&gt;
&lt;p&gt;具体可以参考下面这段代码（来源：&lt;a href=&quot;https://changkun.de/modern-cpp/zh-cn/03-runtime/#%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8%E5%92%8C%E5%B7%A6%E5%80%BC%E5%BC%95%E7%94%A8&quot;&gt;现代c++教程&lt;/a&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;

void reference(std::string&amp;#x26; str) {
    std::cout &amp;#x3C;&amp;#x3C; &quot;左值&quot; &amp;#x3C;&amp;#x3C; std::endl;
}
void reference(std::string&amp;#x26;&amp;#x26; str) {
    std::cout &amp;#x3C;&amp;#x3C; &quot;右值&quot; &amp;#x3C;&amp;#x3C; std::endl;
}

int main()
{
    std::string lv1 = &quot;string,&quot;; // lv1 是一个左值
    // std::string&amp;#x26;&amp;#x26; r1 = lv1; // 非法, 右值引用不能引用左值
    std::string&amp;#x26;&amp;#x26; rv1 = std::move(lv1); // 合法, std::move可以将左值转移为右值
    std::cout &amp;#x3C;&amp;#x3C; rv1 &amp;#x3C;&amp;#x3C; std::endl; // string,

    const std::string&amp;#x26; lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期
    // lv2 += &quot;Test&quot;; // 非法, 常量引用无法被修改
    std::cout &amp;#x3C;&amp;#x3C; lv2 &amp;#x3C;&amp;#x3C; std::endl; // string,string,

    std::string&amp;#x26;&amp;#x26; rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期
    rv2 += &quot;Test&quot;; // 合法, 非常量引用能够修改临时变量
    std::cout &amp;#x3C;&amp;#x3C; rv2 &amp;#x3C;&amp;#x3C; std::endl; // string,string,string,Test

    reference(rv2); // 输出左值

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;reference(rv2)&lt;/code&gt;实际输出的是左值，也就代表着其实际调用了&lt;code&gt;void reference(std::string&amp;#x26;amp; str)&lt;/code&gt;函数&lt;/p&gt;
&lt;h2&gt;move_constructors（移动构造）&lt;/h2&gt;
&lt;p&gt;移动构造函数和移动赋值运算符是在类内部实现的方法，用于将一个对象的资源转移到另一个对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Person {
public:
  Person() : age_(0), nicknames_({}), valid_(true) {}

  // 构造函数通过接受一个std::vector&amp;#x3C;std::string&gt;右值作为参数
  // 这使得构造时比一般的会更快，因为不需要复制vector的底层数组
  Person(uint32_t age, std::vector&amp;#x3C;std::string&gt; &amp;#x26;&amp;#x26;nicknames)
      : age_(age), nicknames_(std::move(nicknames)), valid_(true) {}

  // Person类的移动构造函数：接受一个Person类型的右值
  // 具体过程是：将传入的右值中的对象成员，通过std::move()转为右值
  // 再调用vector的移动构造函数，实现资源转移
  
  // vector是如何转移资源的？
  // 假设vector有一个void* arr指针用于指向开辟的动态数组
  // nicknames_(std::move(person.nicknames_))实际上做：
  // 1. nicknames_.arr = person.nicknames_.arr;
  // 2. person.nicknames_arr = nullptr;
  Person(Person &amp;#x26;&amp;#x26;person)
      : age_(person.age_), nicknames_(std::move(person.nicknames_)),
        valid_(true) {
    std::cout &amp;#x3C;&amp;#x3C; &quot;Calling the move constructor for class Person.\n&quot;;
    // 将被移动对象的实例标志置为false
    person.valid_ = false;
  }

  // Person类的移动赋值运算符，原理与移动构造函数相同
  Person &amp;#x26;operator=(Person &amp;#x26;&amp;#x26;other) {
    std::cout &amp;#x3C;&amp;#x3C; &quot;Calling the move assignment operator for class Person.\n&quot;;
    age_ = other.age_;
    nicknames_ = std::move(other.nicknames_);
    valid_ = true;

    // 将被移动对象的实例标志置为false
    other.valid_ = false;
    return *this;
  }

  // delete关键字代表将类的指定构造函数删除
  // Person类将不再有拷贝构造函数
  Person(const Person &amp;#x26;) = delete;
  Person &amp;#x26;operator=(const Person &amp;#x26;) = delete;

  uint32_t GetAge() { return age_; }

  // 返回类型中的这个 &amp;#x26; 符号意味着我们返回对 nicknames_[i] 处字符串的引用。
  // 这也意味着我们不拷贝结果字符串，
  // 此函数返回的内存地址实际上是指向向量 nicknames_ 内存的地址。
  std::string &amp;#x26;GetNicknameAtI(size_t i) { return nicknames_[i]; }

  void PrintValid() {
    if (valid_) {
      std::cout &amp;#x3C;&amp;#x3C; &quot;Object is valid.&quot; &amp;#x3C;&amp;#x3C; std::endl;
    } else {
      std::cout &amp;#x3C;&amp;#x3C; &quot;Object is invalid.&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
  }

private:
  uint32_t age_;
  std::vector&amp;#x3C;std::string&gt; nicknames_;
  // 跟踪对象的数据是否有效，即是否所有数据都已移动到另一个实例。
  bool valid_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int main() {
  // 创建一个Person对象，此时调用的是Person(uint32_t age, std::vector&amp;#x3C;std::string&gt; &amp;#x26;&amp;#x26;nicknames)构造函数
  Person andy(15445, {&quot;andy&quot;, &quot;pavlo&quot;});
  std::cout &amp;#x3C;&amp;#x3C; &quot;Printing andy&apos;s validity: &quot;;
  andy.PrintValid();

  // 将andy对象转为右值，并使用移动赋值运算符将andy的资源移动到andy1
  Person andy1;
  andy1 = std::move(andy);

  // 此时andy1的资源是有效的，而andy的资源已经被转移了，不再有效
  std::cout &amp;#x3C;&amp;#x3C; &quot;Printing andy1&apos;s validity: &quot;;
  andy1.PrintValid();
  std::cout &amp;#x3C;&amp;#x3C; &quot;Printing andy&apos;s validity: &quot;;
  andy.PrintValid();

  // 该表达式使用Person类的移动构造函数
  
  Person andy2(std::move(andy1));

  // 跟上面一样，该表达式结束后，andy2将拥有andy1的资源，andy1资源被转移，不再有效
  // 后续不应该继续使用andy1，除非重新初始化andy1，
  std::cout &amp;#x3C;&amp;#x3C; &quot;Printing andy2&apos;s validity: &quot;;
  andy2.PrintValid();
  std::cout &amp;#x3C;&amp;#x3C; &quot;Printing andy1&apos;s validity: &quot;;
  andy1.PrintValid();

  // 拷贝构造函数和拷贝运算符函数被删除，下面的代码都会报编译错误
  // Person andy3;
  // andy3 = andy2;

  // Person andy4(andy2);

  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;C++ Templates（C++模板）&lt;/h2&gt;
&lt;h3&gt;模板类&lt;/h3&gt;
&lt;p&gt;在类定义前使用&lt;code&gt;template&amp;#x3C;typename T&gt;&lt;/code&gt;来标识模板类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;template&amp;#x3C;typename T&gt;
class Foo {
  public:
    Foo(T var) : var_(var) {}
    void print() {
      std::cout &amp;#x3C;&amp;#x3C; var_ &amp;#x3C;&amp;#x3C; std::endl;
    }
  private:
    T var_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;T&lt;/code&gt;是传递的类型，&lt;code&gt;typename&lt;/code&gt;用作声明类型的关键字（也可以用&lt;code&gt;class&lt;/code&gt;），模板类型可以设置多个，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;template&amp;#x3C;typename T, typename U&gt; 
class Foo2 {
  public:
    Foo2(T var1, U var2) 
      : var1_(var1)
      , var2_(var2) {}
    void print() {
      std::cout &amp;#x3C;&amp;#x3C; var1_ &amp;#x3C;&amp;#x3C; &quot; and &quot; &amp;#x3C;&amp;#x3C; var2_ &amp;#x3C;&amp;#x3C; std::endl;
    }
  private:
    T var1_;
    U var2_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特化模板类：当模板类型为指定类型（如&lt;code&gt;size_t&lt;/code&gt;）时单独设计的代码，当外部显示指定了类型时，会使用这部分代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;template&amp;#x3C;typename T&gt;
class FooSpecial {
  public:
    FooSpecial(T var) : var_(var) {}
    void print() {
      std::cout &amp;#x3C;&amp;#x3C; var_ &amp;#x3C;&amp;#x3C; std::endl;
    }
  private:
    T var_;
};

// 特化的模板类，专门化于float类型。
// 指定类型FooSpecial&amp;#x3C;float&gt; foo;时，会使用下面的类
template&amp;#x3C;&gt;
class FooSpecial&amp;#x3C;float&gt; {
  public:
    FooSpecial(float var) : var_(var) {}
    void print() {
      std::cout &amp;#x3C;&amp;#x3C; &quot;hello float! &quot; &amp;#x3C;&amp;#x3C; var_ &amp;#x3C;&amp;#x3C; std::endl;
    }
  private:
    float var_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类模板的参数不一定是类型，可以是值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 这里的T不是类型，而是在使用时需要填入的一个值，如Bar&amp;#x3C;10&gt;
// std::array就使用了该类模板，用于指定静态数组的大小
template&amp;#x3C;int T&gt;
class Bar {
  public: 
    Bar() {}
    void print_int() {
      std::cout &amp;#x3C;&amp;#x3C; &quot;print int: &quot; &amp;#x3C;&amp;#x3C; T &amp;#x3C;&amp;#x3C; std::endl;
    }
};
&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;template&amp;#x3C;typename T&gt;
class Foo3 {
  public:
    Foo3(T var) : var_(var) {}
    template&amp;#x3C;typename U&gt;
    void solve(U var) {
        // 做一些处理跟var有关的事
    }
    void print() {
      std::cout &amp;#x3C;&amp;#x3C; var_ &amp;#x3C;&amp;#x3C; std::endl;
    }
  private:
    T var_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;tips&lt;/strong&gt;：使用模板时，声明与定义应该放在同一个文件中，通常做法都是在头文件中声明并定义模板&lt;/p&gt;
&lt;h2&gt;杂项&lt;/h2&gt;
&lt;h3&gt;wrapper class（包装类）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;包装类使用RAII（资源获取即初始化）的思想，将一部分资源交给包装类来管理（内存，文件，网络描述符），当包装类被实例构造时，代表其管理的底层资源是可用的，被析构时，资源自动变得不可用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;包装类通常禁用拷贝构造函数，避免多个对象管理同一资源；但支持移动语义，转移资源的所有权&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class IntPtrManager {
  public:
    // 包装类的所有构造函数都应该初始化一个资源。
    // 在这种情况下，这意味着分配我们正在管理的内存。
    // 这个指针数据的默认值是 0。
    IntPtrManager() {
      ptr_ = new int;
      *ptr_ = 0;
    }

    // 这个包装类的另一个构造函数，接受一个初始值。
    IntPtrManager(int val) {
      ptr_ = new int;
      *ptr_ = val;
    }

    // 包装类的析构函数。析构函数必须销毁它正在管理的资源；
    // 在这种情况下，析构函数删除指针！
    ~IntPtrManager() {
      // 注意，由于移动构造函数通过将 ptr_ 值设置为 nullptr 来标记对象无效，
      // 我们必须在析构函数中考虑这一点。
      // 我们不想对 nullptr 调用 delete！
      if (ptr_) {
        delete ptr_;
      }
    }

    // 这个包装类的移动构造函数。注意在移动构造函数被调用后，
    // 实际上将 other 的所有数据移动到被构造的指定实例中，
    // other 对象不再是 IntPtrManager 类的有效实例，
    // 因为它没有内存可以管理。
    IntPtrManager(IntPtrManager&amp;#x26;&amp;#x26; other) {
      ptr_ = other.ptr_;
      other.ptr_ = nullptr;
    }

    // 这个包装类的移动赋值操作符。
    // 与移动构造函数类似的技术。
    IntPtrManager &amp;#x26;operator=(IntPtrManager &amp;#x26;&amp;#x26;other) {
      if (ptr_ == other.ptr_) {
        return *this;
      }
      if (ptr_) {
        delete ptr_;
      }
      ptr_ = other.ptr_;
      other.ptr_ = nullptr;
      return *this;
    }

    // 我们删除拷贝构造函数和拷贝赋值操作符，
    // 所以这个类不能被拷贝构造。
    IntPtrManager(const IntPtrManager &amp;#x26;) = delete;
    IntPtrManager &amp;#x26;operator=(const IntPtrManager &amp;#x26;) = delete;

    // 设置函数。
    void SetVal(int val) {
      *ptr_ = val;
    }

    // 获取函数。
    int GetVal() const {
      return *ptr_;
    }

  private:
    int *ptr_;

};

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;namespace（命名空间）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;命名空间主要用于提供作用域、组织代码、防止命名冲突&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;namespace xxx&lt;/code&gt;定义一个命名空间（可以嵌套）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;namespace name1 {
    ...
    namespace name2 {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;::&lt;/code&gt;操作符访问不同命名空间的标识符，如&lt;code&gt;name::name2::x&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;using&lt;/code&gt;关键字引入命名空间或其中的标识符，如&lt;code&gt;using namespace std;&lt;/code&gt;，&lt;code&gt;using std::cout&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;匿名空间，即不指定名字的命名空间：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;namespace {
    ...
}
&lt;/code&gt;&lt;/pre&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;p&gt;在头文件中定义有名字的命名空间，在源文件名定义匿名空间&lt;/p&gt;
&lt;p&gt;（匿名空间定义在头文件中，那么所有包含了头文件的源文件都将有一个对应空间的副本）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在头文件中适当使用&lt;code&gt;using name::;&lt;/code&gt;，不要使用&lt;code&gt;using namespace xxx;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;（头文件中使用&lt;code&gt;using namespace xxx&lt;/code&gt;会影响到所有包含其的源文件）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果只是想暂时使用命名空间或某个标识符，可以在函数中使用&lt;code&gt;using&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;iterator（迭代器）&lt;/h3&gt;
&lt;p&gt;提供对容器（数组、链表、树）中的元素的统一访问方式，无需暴露容器内部结构、&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;形式：定义在每个容器内部，定义一个&lt;code&gt;vector&lt;/code&gt;的迭代器&lt;code&gt;std::vector&amp;#x3C;int&gt;::iterator it&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提供访问和修改：类似指针，指向容器中的某个位置，使用&lt;code&gt;*&lt;/code&gt;操作符可以操作对应的数据，根据容器类型的不同有不同的数据类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;顺序容器（如&lt;code&gt;vector&lt;/code&gt;）：&lt;code&gt;*iterator&lt;/code&gt;即为&lt;code&gt;T&lt;/code&gt;类型&lt;/li&gt;
&lt;li&gt;关联容器（如&lt;code&gt;map&lt;/code&gt;）：&lt;code&gt;*iterator&lt;/code&gt;即为&lt;code&gt;pair&amp;#x3C;K, V&gt;&lt;/code&gt;类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提供移动：从容器的某个位置移动到另一个位置，前缀&lt;code&gt;++&lt;/code&gt;，&lt;code&gt;--&lt;/code&gt;操作符，分别表示将迭代器从当前位置向后移动一位和向前移动一位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容器提供：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/container_iterator.C0ciL_uK_CtKtW.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;算法提供：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/algo_iterator.DkAgNGJx_Z1z24Nv.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容器&lt;/h2&gt;
&lt;h3&gt;vector&lt;/h3&gt;
&lt;h3&gt;priority_queue&lt;/h3&gt;
&lt;p&gt;默认是大根堆，需要重载&lt;code&gt;&amp;#x3C;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;greater&lt;/code&gt;指定小根堆，需要重载&lt;code&gt;&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;map&lt;/h3&gt;
&lt;h3&gt;unordered_map&lt;/h3&gt;
&lt;h2&gt;自动推导&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;decltype&lt;/code&gt;和&lt;code&gt;auto&lt;/code&gt;是现代c++中的类型推导工具&lt;/p&gt;
&lt;h3&gt;auto&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;auto&lt;/code&gt;可以极大简化代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 下面两行代码等价
// std::vector&amp;#x3C;int&gt;::iterator it = nums.begin();
auto it = nums.begin();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;auto&lt;/code&gt;还可以定义&lt;code&gt;lambda&lt;/code&gt;表达式，自动推导函数类型，函数返回类型等...&lt;/p&gt;
&lt;p&gt;&lt;code&gt;auto&lt;/code&gt;会自动推导赋值表达式右侧的类型，类似模板参数&lt;code&gt;template&amp;#x3C;typename T&gt;&lt;/code&gt;，二者的推导规则几乎相同&lt;/p&gt;
&lt;h4&gt;按值传递&lt;/h4&gt;
&lt;p&gt;使用&lt;code&gt;auto&lt;/code&gt;，默认会忽略顶层&lt;code&gt;const&lt;/code&gt;和引用（行为与使用模板参数的函数传值类似）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int x = 42;
int &amp;#x26;y = x;
const int &amp;#x26;z = y;

auto a = x; // 即int a = x;
auto b = y; // 即int b = y;
auto c = z; // 即int c = z;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;引用传递&lt;/h4&gt;
&lt;p&gt;使用&lt;code&gt;auto&amp;#x26;&lt;/code&gt;，与普通类型引用类似，同时会保留&lt;code&gt;const&lt;/code&gt;属性：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int x = 42;
int &amp;#x26;y = x;
const int &amp;#x26;z = y;

auto&amp;#x26; a = x; // 即int&amp;#x26; a = x;
auto&amp;#x26; b = y; // 即int&amp;#x26; b = y;
auto&amp;#x26; c = z; // 即const int&amp;#x26; c = z;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;万能引用&lt;/h4&gt;
&lt;p&gt;使用&lt;code&gt;auto&amp;#x26;&amp;#x26;&lt;/code&gt;，这不是上面提到的右值引用，而是遵守&lt;strong&gt;引用折叠规则&lt;/strong&gt;的万能引用，它根据赋值表达式右侧的值类型（左值/右值）来决定左侧变量的类型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int x = 42;
const int &amp;#x26;y = x;

auto&amp;#x26;&amp;#x26; a = x; // 即int&amp;#x26; a = x;
auto&amp;#x26;&amp;#x26; b = y; // 即const int&amp;#x26; b = y;
auto&amp;#x26;&amp;#x26; c = 42; // 即int&amp;#x26;&amp;#x26; c = 42;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这与模板参数的推导类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;template&amp;#x3C;typename T&gt;
auto func1(T&amp;#x26;&amp;#x26; t) -&gt; void {
    // t的类型根据传入参数决定
}
// 上面的代码
// 在传入左值时参数被推导为T&amp;#x26; t
// 在传入右值时参数被推导为T&amp;#x26;&amp;#x26; t
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;安全实践&lt;/h4&gt;
&lt;p&gt;由于使用&lt;code&gt;auto&lt;/code&gt;来定义变量会默认发生拷贝，比如：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;auto vec = nums;&lt;/code&gt;（假设&lt;code&gt;nums&lt;/code&gt;是一个&lt;code&gt;vector&amp;#x3C;int&gt;&lt;/code&gt;类型变量）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vec&lt;/code&gt;和&lt;code&gt;nums&lt;/code&gt;就是两个不同的数组&lt;/p&gt;
&lt;p&gt;当只需要读取而不需要修改变量，且不想要拷贝带来的开销，可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; nums1 = {
    {1, 2, 3}, 
    {4, 5, 6}
};
// 使用了容器的便利特性
for (const auto&amp;#x26; vec : nums1) {
    // vec不会拷贝原数组
    for (const auto&amp;#x26; i : vec) {
        cout &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &apos; &apos;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;decltype&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;decltype&lt;/code&gt;像是一个高级的&lt;code&gt;auto&lt;/code&gt;，它根据给定的表达式，推导出原表达式的精确类型，包括顶层&lt;code&gt;const&lt;/code&gt;引用&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;decltype(e)&lt;/code&gt;来说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果&lt;code&gt;e&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;obj.member&lt;/code&gt;），那么 &lt;code&gt;decltype(e)&lt;/code&gt; 就是该变量或成员声明的类型。&lt;/li&gt;
&lt;li&gt;如果表达式 &lt;code&gt;e&lt;/code&gt; 值类别是&lt;strong&gt;左值&lt;/strong&gt;，&lt;code&gt;decltype(e)&lt;/code&gt; 是 &lt;code&gt;T&amp;#x26;&lt;/code&gt;（引用类型），其中 &lt;code&gt;T&lt;/code&gt; 是 &lt;code&gt;e&lt;/code&gt; 的类型（这个&lt;code&gt;T&lt;/code&gt;不是模板类型那个&lt;code&gt;T&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;如果表达式 &lt;code&gt;e&lt;/code&gt; 值类别是&lt;strong&gt;纯右值&lt;/strong&gt;），&lt;code&gt;decltype(e)&lt;/code&gt; 是 &lt;code&gt;T&lt;/code&gt;（原类型）。&lt;/li&gt;
&lt;li&gt;如果表达式&lt;code&gt;e&lt;/code&gt;值类别是&lt;strong&gt;将亡值&lt;/strong&gt;，&lt;code&gt;decltype(e)&lt;/code&gt;是&lt;code&gt;T&amp;#x26;&amp;#x26;&lt;/code&gt;（右值引用）。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int i = 0;
// 规则1 
decltype(i); // 推导为int
// 规则2
decltype((i)); // 推导为int&amp;#x26;

int&amp;#x26; func() {
    return i;
}
decltype(func()); //推导为int&amp;#x26;

// 规则3
decltype(1 + 2); // 推导为int

// 规则4
decltype(std::move(i)); // 推导为int&amp;#x26;&amp;#x26;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;函数类型推导&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;auto&lt;/code&gt;可以推导出迭代器，函数类型等复杂的类型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int add_func(int a, int b) {
    return a + b;
}

int main() {
    auto minus_func = [](int a, int b) { return a - b; };

    std::vector&amp;#x3C;std::function&amp;#x3C;decltype(add_func)&gt;&gt; funcVec = {
        add_func,
        minus_func
    };

    funcVec[0](1, 2);
    funcVec[1](1, 2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;函数模板返回值推导&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;decltype(auto)&lt;/code&gt;表示使用&lt;code&gt;decltype&lt;/code&gt;的规则来推导&lt;code&gt;auto&lt;/code&gt;，当不确定模板的返回值时，使用其可以返回精确的类型&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;template &amp;#x3C;typename Container, typename Index&gt;
decltype(auto) get(Container&amp;#x26; c, Index i) { // 更简洁！
    return c[i]; // 如果 c[i] 返回 int&amp;#x26;，则函数返回 int&amp;#x26;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;内存管理&lt;/h2&gt;
&lt;h3&gt;unique_ptr&lt;/h3&gt;
&lt;h3&gt;shared_ptr&lt;/h3&gt;
&lt;h3&gt;weak_ptr&lt;/h3&gt;
&lt;h2&gt;锁与同步&lt;/h2&gt;
&lt;h3&gt;读写锁&lt;/h3&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;ul&gt;
&lt;li&gt;&lt;code&gt;std::shared_mutex&lt;/code&gt;允许&lt;strong&gt;共享锁定&lt;/strong&gt;和&lt;strong&gt;独占锁定&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::shared_lock&lt;/code&gt;以共享的方式来占用一个&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;/li&gt;
&lt;li&gt;&lt;code&gt;std::unique_lock&lt;/code&gt;以独占的方式来占用一个&lt;code&gt;std::shared_mutex&lt;/code&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;mutex&gt;
#include &amp;#x3C;shared_mutex&gt;
#include &amp;#x3C;thread&gt;

// 定义一个全局count变量和一个供所有线程使用的shared mutex
int count = 0;
std::shared_mutex m;

// 读线程使用std::shared_lock来获得对count变量的只读共享访问，并读取count变量
// 有意思的点在于只读是人为规定的, 如果某个线程执行了写入, 可能出现一些意想不到的效果
void read_value() {
  std::shared_lock lk(m);
  std::cout &amp;#x3C;&amp;#x3C; &quot;Reading value &quot; + std::to_string(count) + &quot;\n&quot; &amp;#x3C;&amp;#x3C; std::flush;
}

// 这个函数使用std::unique_lock来获得对count变量的独占访问并写入值, 一旦其获得锁, 除非其主动释放, 否则其他线程无法获得锁
void write_value() {
  std::unique_lock lk(m);
  count += 3;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://book.douban.com/subject/36596125/&quot;&gt;C++之旅&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://book.douban.com/subject/36596125/&quot;&gt;CMU-bootcamp 翻译版&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/cpp_icon.CPjKbVGJ.png"/><enclosure url="/_astro/cpp_icon.CPjKbVGJ.png"/></item><item><title>LeetCode 每日一题</title><link>https://liang-bk.github.io/blog/leetcode-everyday</link><guid isPermaLink="true">https://liang-bk.github.io/blog/leetcode-everyday</guid><description>LeetCode 每日一题题解</description><pubDate>Fri, 26 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;3729. 统计有序数组中可被K整除的子数组数量（2025.10.27）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/count-distinct-subarrays-divisible-by-k-in-sorted-array/description/&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;题目要求&lt;code&gt;[l, r]&lt;/code&gt;的对数，使得：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(sum[r] - sum[l - 1]) % k == 0&lt;/code&gt;（&lt;code&gt;sum&lt;/code&gt;是前缀和）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nums[l1, r2] != nums[l2, r2]&lt;/code&gt;（长度不同或者长度相同内容不同）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为&lt;code&gt;nums&lt;/code&gt;是非降序的，考虑更严苛的条件，一个&lt;strong&gt;升序&lt;/strong&gt;数组，那么第二个条件就没用了，因为所有的子数组必定不相同，再来考虑如果求出满足第一个条件的&lt;code&gt;[l, r]&lt;/code&gt;对数：&lt;/p&gt;
&lt;p&gt;第一个条件可以转化成&lt;code&gt;sum[r] % k == sum[l - 1] % k&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;于是可以枚举右端点r，同时在哈希表中记录左端点&lt;code&gt;l(l &amp;#x3C;= r)&lt;/code&gt;的数量，其中&lt;code&gt;key&lt;/code&gt;是&lt;code&gt;sum[0, l - 1] % k&lt;/code&gt;，然后计算&lt;code&gt;sum[0, r] % k&lt;/code&gt;，去哈希表中查有多个左端点满足条件，就能得出右端点为r时有多少个子数组满足条件，总和相加即为答案&lt;code&gt;ans&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果升序数组变为非降序数组，用上面的方法计算会出现一个重复计算的问题，如样例&lt;code&gt;[2, 2, 2, 2, 2, 2]&lt;/code&gt;和模值&lt;code&gt;6&lt;/code&gt;，但考虑重复计数的情况，两个&lt;code&gt;[l, r]&lt;/code&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;code&gt;[x, x, ..., x]&lt;/code&gt;这样的形式时才会出现重复计数，那么我们可以统计出来所有这样的子数组，以&lt;code&gt;&amp;#x3C;x, length&gt;&lt;/code&gt;的形式存为一个数组&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;&amp;#x3C;x, length&gt;&lt;/code&gt;这样一个子数组，先前的&lt;code&gt;ans&lt;/code&gt;中出现了重复计数，主要原因是相同长度统计了多次，考虑使用&lt;code&gt;m&lt;/code&gt;个&lt;code&gt;x&lt;/code&gt;能够整除&lt;code&gt;k&lt;/code&gt;，即&lt;code&gt;m * x % k == 0&lt;/code&gt;，容易使用最小公倍数算出&lt;code&gt;m = lcm(x, k) / x&lt;/code&gt;，那么该子数组最少有&lt;code&gt;m + 1&lt;/code&gt;个&lt;code&gt;x&lt;/code&gt;，才有可能出现重复计数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;m&lt;/code&gt;个&lt;code&gt;x&lt;/code&gt;，重复计数了&lt;code&gt;length - m&lt;/code&gt;次&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;2m&lt;/code&gt;个&lt;code&gt;x&lt;/code&gt;，重复计数了&lt;code&gt;length - 2m&lt;/code&gt;次&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;令&lt;code&gt;l = length / m&lt;/code&gt;（下取整），可以得到子数组&lt;code&gt;&amp;#x3C;x, length&gt;&lt;/code&gt;的重复计数：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;repeat_x = l * length - m * (l + 1) * l / 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;ans&lt;/code&gt;减去每一个&lt;code&gt;repeat_x&lt;/code&gt;即为答案&lt;/p&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
    using i64 = long long;
public:
    int gcd(int x, int y) {
        return y == 0 ? x : gcd(y, x % y);
    }

    i64 lcm(int x, int y) {
        return 1ll * x * y / gcd(x, y);
    }
    long long numGoodSubarrays(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
        
        i64 ans = 0;
        // sum[r] % k == sum[l - 1] % k
        unordered_map&amp;#x3C;i64, int&gt; u_map;
        u_map[0] = 1;
        i64 sum_r = 0;
        int n = nums.size();
        for (int i = 0; i &amp;#x3C; n; i++) {
            sum_r = (sum_r + nums[i]) % k;
            if (u_map.contains(sum_r)) {
                ans += u_map[sum_r];
            }
            u_map[sum_r]++;
        }
        // &amp;#x3C;num, length&gt;
        vector&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; total;
        int num = nums[0], cnt = 0;
        for (int i = 0; i &amp;#x3C; n; i++) {
            if (nums[i] == num) {
                cnt++;
            } else {
                total.push_back({num, cnt});
                num = nums[i];
                cnt = 1;
            }
            if (i == n - 1) {
                total.push_back({num, cnt});
            }
        }
        i64 repeat = 0;
        for (int i = 0; i &amp;#x3C; total.size(); i++) {
            i64 m = lcm(total[i].first, k) / total[i].first;
            i64 l = total[i].second / m;
            repeat += l * total[i].second - (m * l * (l + 1) / 2);
        }
        ans -= repeat;
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(nlog(max(nums_i, k))$，其中$log(max(nums_i, k))$是计算最小公约数的时间&lt;/p&gt;
&lt;h2&gt;3728. 边界与内部和相等的稳定子数组（2025.10.26）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/stable-subarrays-with-equal-boundary-and-interior-sum/description/&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;求子数组的数量，$10^4$以下能用$O(n^2)$，$10^5$以上就是$O(nlogn)$或者$O(n)$的做法，根据经验，多数与差分，前缀和有关（双指针只是在这个过程用到的技巧），从前缀和的角度去考虑两端的数l和r，找到l和r的关系后，枚举一端（通常是r），同时移动另一端使其满足先前的关系&lt;/p&gt;
&lt;p&gt;以问题为例，长度至少为3，首尾元素需相等，中间元素的和需要等于首元素，假设已经有前缀和$sum$，那么上面的条件如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;r - l + 1 &gt;= 3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;capacity[l] == capacity[r]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sum[r - 1] - sum[l] == capacity[l]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;将最后一个式子变形得：&lt;code&gt;sum[r - 1] == sum[l] + capacity[l]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;于是可以在枚举r时，同时记录&lt;code&gt;{capacity[l], sum[l] + capacity[l]}&lt;/code&gt;的数量（使用map），在满足&lt;code&gt;r - l + 1 &gt;= 3&lt;/code&gt;的情况下，搜索记录&lt;code&gt;{capacity[r], sum[r - 1]}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果之前记录的左端被记录过，说明是对于当前r来说，满足以上三个条件的左端点，就可以累加，累加的结果就是答案&lt;/p&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    long long countStableSubarrays(vector&amp;#x3C;int&gt;&amp;#x26; capacity) {
        using i64 = long long;
        int n = capacity.size();
        i64 sum_l = 0, sum_r = 0;
        sum_r += capacity[0] + capacity[1];
        map&amp;#x3C;pair&amp;#x3C;i64, i64&gt;, i64&gt; map;
        i64 ans = 0;
        for (int r = 2, l = 0; r &amp;#x3C; n; r++) {
            while (r - l + 1 &gt;= 3) {
                sum_l += capacity[l];
                pair&amp;#x3C;i64, i64&gt; index {capacity[l], capacity[l] + sum_l};
                map[index]++;
                l++;
            }
            pair&amp;#x3C;i64, i64&gt; index {capacity[r], sum_r};
            sum_r += capacity[r];
			if (map.contains(index)) {
                ans += map[index];
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(nlogn)$&lt;/p&gt;
&lt;h2&gt;3347. 执行操作后元素的最高频率 II（2025.10.22）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-frequency-of-an-element-after-performing-operations-ii/description/&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本题相对3346，扩大了$[s, e]$的范围，这使得无法暴力枚举来求出答案，但通过观察可以发现，$[s, e]$中并不是所有的数字都值得被枚举，这一点可以通过&lt;strong&gt;差分&lt;/strong&gt;来证明：&lt;/p&gt;
&lt;p&gt;对于每一个$nums[i]$，其能够转为的数字在$[nums[i]-k, nums[i]+k]$之间，考虑一个数轴，这表示在$[nums[i]-k, nums[i]+k]$这段数字上+1（代表这段数字可以由$num[i]$操作后得到），如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/3347_1.BYcuMFFi_1ydtUr.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;从图中可以看出，只需要枚举$num[i], nums[i]-k, nums[i]+k$，枚举区间上的其他数字，其$sum$值不会超过枚举这三个数字得到的$sum$值，因此可以缩减枚举的区间，从$[s, e]$到所有的$nums[i], nums[i]-k, nums[i]+k$，同样使用滑动窗口来遍历原数组，做法和3346相同&lt;/p&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    using i64 = long long;
    int maxFrequency(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k, int numOperations) {
        sort(nums.begin(), nums.end());
        using PII = pair&amp;#x3C;int, int&gt;;
        vector&amp;#x3C;PII&gt; u_nums;
        int num = nums[0], cnt = 1;
        int n = nums.size();
        for (int i = 1; i &amp;#x3C; n; i++) {
            if (nums[i] != num) {
                u_nums.push_back({num, cnt});
                num = nums[i];
                cnt = 1;
            } else {
                cnt++;
            }
        }
        u_nums.push_back({num, cnt});
        vector&amp;#x3C;int&gt; candidate;
        n = u_nums.size();
        for (int i = 0; i &amp;#x3C; n; i++) {
            candidate.push_back(u_nums[i].first - k);
            candidate.push_back(u_nums[i].first);
            candidate.push_back(u_nums[i].first + k);
        }
        sort(candidate.begin(), candidate.end());
        n = candidate.size();
        queue&amp;#x3C;int&gt; q;
        int sum = 0, ans = 0;
        for (int s = 0, i = 0, j = 0; s &amp;#x3C; n; s++) {
            i64 l = 1ll * candidate[s] - k, r = 1ll * candidate[s] + k;
            while (!q.empty() &amp;#x26;&amp;#x26; u_nums[q.front()].first &amp;#x3C; l) {
                int index = q.front();
                q.pop();
                sum -= u_nums[index].second;
            }
            while (j &amp;#x3C; u_nums.size() &amp;#x26;&amp;#x26; u_nums[j].first &amp;#x3C;= r) {
                q.push(j);
                sum += u_nums[j].second;
                j++;
            }
            if (i &amp;#x3C; u_nums.size() &amp;#x26;&amp;#x26; candidate[s] == u_nums[i].first) {
                int op = min(sum - u_nums[i].second, numOperations);
                ans = max(ans, op + u_nums[i].second);
                i++;
            } else {
                int op = min(sum, numOperations);
                ans = max(ans, op);
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(nlogn)$，$n$是数组长度&lt;/p&gt;
&lt;h2&gt;3346. 执行操作后元素的最高频率 I（2025.10.21）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-frequency-of-an-element-after-performing-operations-i/?envType=daily-question&amp;#x26;envId=2025-10-21&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;题目中声明所有数字最多被操作一次，考虑到数据量较小，频率最高的数字的范围只可能在$[s, e] = [min(nums), max(nums)]$，这不难证明，如果操作能使得频率最高的超出这个范围设为$x$，那么在范围内一定存在一个数$y$，使得$y$的频率和$x$相等，题目只要求求出最大的频率，那么这个范围自然是越小越好&lt;/p&gt;
&lt;p&gt;那么可以考虑枚举$[s, e]$当中的每一个数$t$，则能够通过操作变为$t$的数字范围就在$[t - k, t + k]$，只需要去原数组中查找这个范围的数有多少个即可&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对原数组排序&lt;/li&gt;
&lt;li&gt;对原数组去重，并统计相同数字的数量&lt;/li&gt;
&lt;li&gt;在遍历$[s, e]$中，使用滑动窗口同步滑动处理后的数组，将滑动窗口内的数组限制在$[t - k, t + k]$中，统计滑动窗口内的数字数量，代表有$sum$多少个数字可以转成$t$，然后和可操作数量取最小值，代表可以有$op$个数字可以转成$t$，再比较每个$t$中的$op$最大值&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;需要注意的是，如果$t$在原数组中出现过，需要额外处理，因为原数组中的这些数字不再需要操作变成$t$&lt;/p&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    int maxFrequency(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k, int numOperations) {
        sort(nums.begin(), nums.end());
        using PII = pair&amp;#x3C;int, int&gt;;
        vector&amp;#x3C;PII&gt; u_nums;
        int num = nums[0], cnt = 1;
        int n = nums.size();
        for (int i = 1; i &amp;#x3C; n; i++) {
            if (nums[i] != num) {
                u_nums.push_back({num, cnt});
                num = nums[i];
                cnt = 1;
            } else {
                cnt++;
            }
        }
        u_nums.push_back({num, cnt});
        int sum = 0, ans = 0;
        n = u_nums.size();
        queue&amp;#x3C;int&gt; q;
        int s = u_nums[0].first, e = u_nums[n - 1].first;
        for (int i = 0, j = 0; s &amp;#x3C;= e; s++) {
            int l = s - k, r = s + k;
            // [l, r]
            while (!q.empty() &amp;#x26;&amp;#x26; u_nums[q.front()].first &amp;#x3C; l) {
                int index = q.front();
                q.pop();
                sum -= u_nums[index].second;
            }
            while (j &amp;#x3C; n &amp;#x26;&amp;#x26; u_nums[j].first &amp;#x3C;= r) {
                q.push(j);
                sum += u_nums[j].second;
                j++;
            }
            if (i &amp;#x3C; n &amp;#x26;&amp;#x26; s == u_nums[i].first) {
                int op = min(sum - u_nums[i].second, numOperations);
                ans = max(ans, u_nums[i].second + op);
                i += 1;
            } else {
                int op = min(sum, numOperations);
                ans = max(ans, op);
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(nlogn + m)$，$n$是数组大小，$m$是数组中取值范围&lt;/p&gt;
&lt;h2&gt;1625. 执行操作后字典序最小的字符串（2025.10.19）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/lexicographically-smallest-string-after-applying-operations/description/?envType=daily-question&amp;#x26;envId=2025-10-19&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;将字符串考虑为图中的一个结点&lt;/p&gt;
&lt;p&gt;当进行累加操作时，字符串x变为字符串y，代表从x设置一条有向边指向y&lt;/p&gt;
&lt;p&gt;同理，进行轮转操作时，字符串u变为w，代表从u设置一条有向边指向w&lt;/p&gt;
&lt;p&gt;那么就可以通过遍历整个图来找到初始字符串可以转换成的所有字符串，然后返回其中字典序最小的一个即可&lt;/p&gt;
&lt;p&gt;code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    void bfs(const string&amp;#x26; state, int a, int b) {
        queue&amp;#x3C;string&gt; q;
        q.push(state);
        vis.insert(state);
        while (!q.empty()) {
            string st = std::move(q.front());
            q.pop();
            // 两条边, 累加, 或者轮转
            string tmp = st;
            int n = tmp.size();
            for (int i = 1; i &amp;#x3C; n; i += 2) {
                char ch = (tmp[i] - &apos;0&apos; + a) % 10 + &apos;0&apos;;
                tmp[i] = ch;
            }
            if (!vis.contains(tmp)) {
                vis.insert(tmp);
                q.push(tmp);
            }
            tmp = st.substr(n - b);
            tmp += st.substr(0, n - b);
            if (!vis.contains(tmp)) {
                vis.insert(tmp);
                q.push(tmp);
            }
        }
        return;
    }
    string findLexSmallestString(string s, int a, int b) {
        bfs(s, a, b);
        auto it = vis.begin();
        return (*it);
    }
    set&amp;#x3C;string&gt; vis;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(100n)$，其中$n$是字符串的长度&lt;/p&gt;
&lt;h2&gt;3397. 执行操作后不同元素的最大数量（2025.10.18）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-number-of-distinct-elements-after-operations/description/?envType=daily-question&amp;#x26;envId=2025-10-18&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;不难想到一个贪心思路：将数组从小到大进行排序，设排序后的数组为：
$$
a = [x1, x1, x2, x3, x3, ... xk]
$$
数组中可能有多个数字相同，为了尽可能让数组拥有不同元素，则应该：&lt;/p&gt;
&lt;p&gt;$a[0] = x1 - k$&lt;/p&gt;
&lt;p&gt;$a[1] = x1 - k + 1$&lt;/p&gt;
&lt;p&gt;关键在于当相邻的数字不同时的处理：&lt;/p&gt;
&lt;p&gt;表面上看，应该令$a[2] = x2 - k$，但$x1 - k + 1$有可能大于或等于$x2 - k$，这代表$x2 - k$这个数字已经被使用过了，因此下一个要使用的数字应该是$max(x1 - k + 2, x2 - k)$&lt;/p&gt;
&lt;p&gt;因此，可以保存一个$next_value$或者$last_value$，当相邻两个数不同时，根据上一个数当前被映射为数字，决定当前数字应该变为多少，（注意，如果$x1 - k + 2 &gt; x2 + k$，那么$x2$变或不变都无所谓了，能变的数字肯定都出现过了）&lt;/p&gt;
&lt;p&gt;code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    using i64 = long long;
    int maxDistinctElements(vector&amp;#x3C;int&gt;&amp;#x26; nums, int k) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        i64 elem = nums[0];
        i64 last = elem - k;
        int cnt = 0;
        for (int i = 0; i &amp;#x3C; n; i++) {
            if (nums[i] != elem) {
                elem = nums[i];
                last = max(last, elem - k);
            }
            if (last &amp;#x3C;= elem + k) {
                last++;
                cnt++;
            }
        }
        return cnt;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(n)$&lt;/p&gt;
&lt;h2&gt;2598. 执行操作后的最大MEX（2025.10.16）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/smallest-missing-non-negative-integer-after-operations/description/?envType=daily-question&amp;#x26;envId=2025-10-16&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;设nums.length为n，则MEX最大只能为n（当nums = [0, 1, 2, ..., n-1]时）&lt;/p&gt;
&lt;p&gt;数组中的每个元素都可以任意加上/减去value（value &gt; 0），考虑每个元素对value的正模值x：
$$
nums[i] \equiv x \pmod{value}
$$
如果多个元素的正模值相同，则明显可以将这些元素转化为：[x, x + value, ..., x + k * value]，使得改变后的数组尽可能被不同的数字填充&lt;/p&gt;
&lt;p&gt;因此，可以统计不同正模值在原数组中出现的频数，然后将其转化为上述形式（只记录在n以内的数字即可），然后遍历0~n，如果某个数不能由原数组的数转化而来，则该数即是答案&lt;/p&gt;
&lt;p&gt;code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    int findSmallestInteger(vector&amp;#x3C;int&gt;&amp;#x26; nums, int value) {
        int n = nums.size();
        if (value == 1) {
            return n;
        }
        unordered_map&amp;#x3C;int, int&gt; frequency;
        vector&amp;#x3C;int&gt; mex(n + 1, 0);
        for (int i = 0; i &amp;#x3C; n; i++) {
            int m = nums[i] % value;
            if (m &amp;#x3C; 0) {
                m += value;
            }
            frequency[m]++;
        }
        for (auto&amp;#x26; [k, v] : frequency) {
            int cnt = v;
            int i = k;
            while (cnt &gt; 0) {
                if (i &amp;#x3C;= n)
                    mex[i] = 1;
                i += value;
                cnt -= 1;
            }
        }
        for (int i = 0; i &amp;#x3C; n; i++) {
            if (mex[i] == 0) {
                return i;
            }
        }
        return n;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(n)$&lt;/p&gt;
&lt;h2&gt;3186. 施咒的最大总伤害（2025.10.11）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-total-damage-with-spell-casting/?envType=daily-question&amp;#x26;envId=2025-10-11&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;先排序，很明显，当两个咒语的power值相同时，那两个咒语一定要一起选，所以合并power值相同的咒语**（以[power[i], k * power[i]]的方式，其中k代表有k个power值相同的咒语）**&lt;/p&gt;
&lt;p&gt;处理完毕后，序列中所有咒语的power值各不相同，然后按照power值排序，设最后得到的序列为&lt;strong&gt;a&lt;/strong&gt;，题目要求的是某个序列power值和的最大值，可以从动态规划集合论的角度来思考：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;状态表示：&lt;strong&gt;dp[i]表示以a[i]的power值为最大power值的子序列集合&lt;/strong&gt;（即该集合一定选择a[i]对应的咒语，并且由于a序列中power值各不相同且从小到大排序，所以dp[i]对应的集合各不相同）&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;ul&gt;
&lt;li&gt;
&lt;p&gt;首先考虑如何划分集合，集合的相同点是子序列中最大的power值是a[i]的power值，则可以根据第二大的power值对集合进行划分，于是集合可以被划分为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;以a[0]的power值为最大的power值的子序列集合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以a[1]的power值为最大的power值的子序列集合&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以a[j]的power值为最大的power值的子序列集合&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中$j &amp;#x3C; i, a[j].power &amp;#x3C; a[i].power - 2$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转移公式：
$$
dp(i) = max(dp(j)) + a[i].sumpower
$$
其中：
$$
j &amp;#x3C; i, a[j].power &amp;#x3C; a[i].power - 2
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化：由于a序列是排过序的，因此在遍历a[i]时，只需要一个下标指向$max(dp(j))$，随着i的移动，判断$a[j].power &amp;#x3C; a[i].power - 2$后更新$max(dp(j))$，就能实现一次遍历求出答案&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    using i64 = long long;
    long long maximumTotalDamage(vector&amp;#x3C;int&gt;&amp;#x26; power) {
        int n = power.size();
        unordered_map&amp;#x3C;int, i64&gt; hashmap;
        for (int i = 0; i &amp;#x3C; power.size(); i++) {
            hashmap[power[i]] += power[i];
        }
        vector&amp;#x3C;int&gt; keys;
        for (auto&amp;#x26; [key, _] : hashmap) {
            keys.push_back(key);
        }
        sort(keys.begin(), keys.end());
        // dp[i]: 表示用power值为keys[i]的咒语集合
        // 集合最大值
        // max(&amp;#x3C;(keys[i] - 2)) + map(keys[i])
        int m = keys.size();
        vector&amp;#x3C;i64&gt; dp(m + 1, 0);
        dp[0] = hashmap[keys[0]];
        i64 before = 0;
        for (int i = 1, j = 0; i &amp;#x3C; m; i++) {
            while (j &amp;#x3C; i &amp;#x26;&amp;#x26; keys[j] &amp;#x3C; keys[i] - 2) {
                before = max(before, dp[j]);
                j++;
            }
            dp[i] = hashmap[keys[i]] + before;
        }
        i64 res = 0;
        for (int i = 0; i &amp;#x3C; m; i++) {
            res = max(res, dp[i]);
        }
        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(nlogn)$&lt;/p&gt;
&lt;h2&gt;1488. 避免洪水泛滥（2025.10.7）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/avoid-flood-in-the-city/description/?envType=daily-question&amp;#x26;envId=2025-10-07&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从左到右遍历抽干日， 对于一个&lt;strong&gt;抽干&lt;/strong&gt;日，其可能有多个湖泊进行选择，将这些可选择的湖泊（第i个可选择的湖泊）上次下雨和该次下雨时间记为$[last_i, now_i]$，按照贪心的思想，应该将其按照$now_i$从小到大进行排序，即越早可能&lt;strong&gt;泛洪&lt;/strong&gt;的湖泊应该更早被抽水，$now_i$更大的湖泊可以尽可能的交给后面的抽干日去处理&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;ol start=&quot;2&quot;&gt;
&lt;li&gt;从左到右遍历湖泊时，假设湖泊$j$会发生洪水，根据其上一次的下雨时间$last$和本次下雨时间$now$，找寻$(last, now)$内的抽干日，假设这些抽干日有$cnt$个，将其从小到大进行排列，根据贪心思想，应该选择距离$last$最近的抽干日，而越晚的抽干日，灵活性越大，可以用于抽干更晚装满的湖，即越晚的抽水日应该留到后面给$now$更大的湖泊使用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以上两种贪心思想等价，但是第二种在代码实现上更简单：利用哈希表记录每个湖泊上次的下雨时间，并将所有的抽干日存储到set中，然后遍历下雨日，在set中找到第一个$&gt; last$的抽干日，并判断其是否合法即可&lt;/p&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    vector&amp;#x3C;int&gt; avoidFlood(vector&amp;#x3C;int&gt;&amp;#x26; rains) {
        int n = rains.size();
        vector&amp;#x3C;int&gt; ans(n, -1);
        unordered_map&amp;#x3C;int, int&gt; last_rain;
        set&amp;#x3C;int&gt; dry_days;
        for (int i = 0; i &amp;#x3C; n; i++) {
            if (rains[i] == 0) {
                dry_days.insert(i);
            }
        }
        for (int i = 0; i &amp;#x3C; n; i++) {
            if (rains[i] &gt; 0) {
                if (last_rain.contains(rains[i])) {
                    int j = last_rain[rains[i]];
                    // &gt; j的第一个, 要用set的upper_bound方法, algorithm的方法会退化为线性时间
                    auto it = dry_days.upper_bound(j);
                    // 必须在第i天之前
                    if (it == dry_days.end()) return {};
                    if ((*it) &gt;= i) return {};
                    ans[(*it)] = rains[i];
                    dry_days.erase(it);
                }
                last_rain[rains[i]] = i;
            }
        }
        // 如果抽干日比下雨日更多，选择湖泊1
        for (int i = 0; i &amp;#x3C; n; i++) {
            if (rains[i] == 0 &amp;#x26;&amp;#x26; ans[i] == -1) {
                ans[i] = 1;
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(nlogn)$&lt;/p&gt;
&lt;h2&gt;778. 水位上升的泳池中游泳（2025.10.6）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/swim-in-rising-water/description/?envType=daily-question&amp;#x26;envId=2025-10-06&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;题目提到在方格之间移动不花费时间，求最小的时间t，使得能从(0, 0)移动到(n-1, n-1)&lt;/p&gt;
&lt;p&gt;答案t一定有上下界（因为格子中的数值有限），下界为0，上界为$n^2 - 1$，那么可以考虑二分答案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;二分答案t&lt;/li&gt;
&lt;li&gt;判断水位为t时，能否从(0, 0)到(n - 1, n - 1)
&lt;ul&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;从(0, 0)进行BFS，根据当前水位，可以判断能否到达(n-1, n-1)&lt;/p&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    bool bfs(queue&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; &amp;#x26;q, vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; &amp;#x26;grid, int t) {
        int m = grid.size();
        int n = grid[0].size();
        vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; vis(m, vector&amp;#x3C;int&gt;(n, 0));
        if (grid[0][0] &gt; t) return false;
        q.push({0, 0});
        vis[0][0] = 1;
        while (!q.empty()) {
            auto point = q.front();
            q.pop();
            int h = max(t, grid[point.first][point.second]);
            for (int i = 0; i &amp;#x3C; 4; i++) {
                int nx = point.first + dx[i];
                int ny = point.second + dy[i];
                if (nx &amp;#x3C; 0 || nx &gt;= m || ny &amp;#x3C; 0 || ny &gt;= n || vis[nx][ny] != 0 || h &amp;#x3C; grid[nx][ny])
                    continue;
                vis[nx][ny] = 1;
                q.push({nx, ny});
            }
        }
        return vis[m - 1][n - 1] == 1;
    }
    int swimInWater(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; grid) {
        int n = grid.size();
        queue&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; q;
        int l = INT_MAX, r = INT_MIN;
        for (int i = 0; i &amp;#x3C; n; i++) {
            for (int j = 0; j &amp;#x3C; n; j++) {
                l = min(l, grid[i][j]);
                r = max(r, grid[i][j]);
            }
        }
        int ans = -1;
        while (l &amp;#x3C;= r) {
            int mid = l + (r - l) / 2;
            if (bfs(q, grid, mid)) {
                r = mid - 1;
                ans = mid;
            } else {
                l = mid + 1;
            }
        }
        return ans;
    }
    constexpr static int dx[] = {0, 0, 1, -1};
    constexpr static int dy[] = {1, -1, 0, 0};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(n^2log(n^2))$&lt;/p&gt;
&lt;h2&gt;417. 太平洋大西洋水流问题（2025.10.5）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/pacific-atlantic-water-flow/description/&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;可以把二维矩阵看做一个图，单元格抽象为点，设高度为$h$，针对两个相邻单元格$p_1$，$p_2$，其对应的高度为$h_1$，$h_2$，如果有$h_1 &gt;= h2$，则从$p_1$向$p_2$连接一条边。&lt;/p&gt;
&lt;p&gt;题目要求出能够流向&lt;strong&gt;PO&lt;/strong&gt;和&lt;strong&gt;AO&lt;/strong&gt;的单元格，一种想法是在原先的二维矩阵上围上一圈结点，并设置这些结点的属性（标识不同的海洋），然后从每个结点开始BFS遍历，只要其能够同时到达拥有两种属性的结点，该结点就符合题意，时间复杂度为$O(V(V + E))$，其中，$V$是结点个数，$E$是边数，在本题中都是$10^4$数量级，用于通过题目显然不现实。&lt;/p&gt;
&lt;p&gt;可以反向思考，从&lt;strong&gt;PO&lt;/strong&gt;和&lt;strong&gt;AO&lt;/strong&gt;向单元格开始流，只需要翻转一下原先边的方向：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;显然，第一行第一列是&lt;strong&gt;PO&lt;/strong&gt;能够流向的结点，将这些结点塞入队列，进行一次BFS，就能知道有哪些结点可以流向&lt;strong&gt;PO&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;同理，最后一行最后一列是&lt;strong&gt;AO&lt;/strong&gt;能流向的结点，同样一次BFS，能知道有哪些结点可以流向&lt;strong&gt;AO&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;对于上面求得的两个集合，做一个交集，就是既能流向&lt;strong&gt;PO&lt;/strong&gt;，也能流向&lt;strong&gt;AO&lt;/strong&gt;的结点&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    void bfs(queue&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; &amp;#x26;q, vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; heights, vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; vis) {
        int m = heights.size();
        int n = heights[0].size();
        while (!q.empty()) {
            auto point = q.front();
            q.pop();
            int h = heights[point.first][point.second];
            for (int i = 0; i &amp;#x3C; 4; i++) {
                int nx = point.first + dx[i];
                int ny = point.second + dy[i];
                if (nx &amp;#x3C; 0 || nx &gt;= m || ny &amp;#x3C; 0 || ny &gt;= n || vis[nx][ny] == 1 || heights[nx][ny] &amp;#x3C; h)
                    continue;
                vis[nx][ny] = 1;
                q.push({nx, ny});
            }
        }
    }

    vector&amp;#x3C;vector&amp;#x3C;int&gt; &gt; pacificAtlantic(vector&amp;#x3C;vector&amp;#x3C;int&gt; &gt; &amp;#x26;heights) {
        int m = heights.size();
        int n = heights[0].size();
        vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; pacific_vis(m, vector&amp;#x3C;int&gt;(n, 0));
        vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; altantic_vis(m, vector&amp;#x3C;int&gt;(n, 0));
        queue&amp;#x3C;pair&amp;#x3C;int, int&gt;&gt; q_pacific, q_altantic;
        for (int i = 0; i &amp;#x3C; n; i++) {
            pacific_vis[0][i] = 1;
            q_pacific.push({0, i});
            altantic_vis[m - 1][i] = 1;
            q_altantic.push({m - 1, i});
        }
        for (int i = 0; i &amp;#x3C; m; i++) {
            if (pacific_vis[i][0] == 0) {
                pacific_vis[i][0] = 1;
                q_pacific.push({i, 0});
            }
            if (altantic_vis[i][n - 1] == 0) {
                altantic_vis[i][n - 1] = 1;
                q_altantic.push({i, n - 1});
            }
        }
        bfs(q_pacific, heights, pacific_vis);
        bfs(q_altantic, heights, altantic_vis);
        vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; ans;
        for (int i = 0; i &amp;#x3C; m; i++) {
            for (int j = 0; j &amp;#x3C; n; j++) {
                if (pacific_vis[i][j] &amp;#x26;&amp;#x26; altantic_vis[i][j]) {
                    ans.push_back({i, j});
                }
            }
        }
        return ans;
    }

    constexpr static int dx[] = {-1, 1, 0, 0};
    constexpr static int dy[] = {0, 0, 1, -1};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(V + E)$，其中，$V = mn$，$E = cV$（$c$是一个常数）&lt;/p&gt;
&lt;h2&gt;11. 盛最多水的容器（2025.10.4）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/container-with-most-water/description/?envType=daily-question&amp;#x26;envId=2025-10-04&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;不妨先考虑只有两条线，那么根据短板效应，两条线中高度最短的那个决定了水的高度：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/11_1.DklQZ9gD_Z1pIsrD.webp&quot; alt=&quot;11_1&quot;&gt;&lt;/p&gt;
&lt;p&gt;考虑向中间插入一条线（长或短）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/11_2.CRy8owvb_xCDod.webp&quot; alt=&quot;11_2&quot;&gt;&lt;/p&gt;
&lt;p&gt;分析如下：&lt;/p&gt;
&lt;p&gt;考虑$l_1$，$l_2$中较短的那条线（假设是$l_1$），由于$l_2$在右端点，设在$l_1$和$l_2$之间的为$l_i$，则显然$l_1$与$l_i$组成的容器容量不会超过$l_1$与$l_2$组成的容器容量&lt;/p&gt;
&lt;p&gt;于是可以跳过中间这部分的计算，直接向右移动$l_1$&lt;/p&gt;
&lt;p&gt;同理，如果$l_2$较短，向左移动$l_2$，直到$l_1 &gt;= l_2$&lt;/p&gt;
&lt;p&gt;每移动一次，就计算一次容量，并保存最大容量&lt;/p&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    int maxArea(vector&amp;#x3C;int&gt;&amp;#x26; height) {
        int l = 0, r = static_cast&amp;#x3C;int&gt;(height.size()) - 1;
        int ans = 0;
        while (l &amp;#x3C; r) {
            ans = max(ans, min(height[l], height[r]) * (r - l));
            if (height[l] &amp;#x3C; height[r]) {
                l++;
            } else {
                r--;
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(n)$&lt;/p&gt;
&lt;h2&gt;42. 接雨水&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/trapping-rain-water/description/&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;407. 接雨水 II（2025.10.3）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/trapping-rain-water-ii/description/?envType=daily-question&amp;#x26;envId=2025-10-03&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;该题不能向接雨水一样使用单调栈来做（不能遍历所有的行和列然后取能接雨水的最小值）&lt;/p&gt;
&lt;p&gt;但同样可以考虑木桶效应（因为最外面一圈格子不可能存下雨水），接雨水考虑的是一维中左右两端的木板，本题是二维，则考虑上下左右四端的木板。根据木桶效应，桶中能装多少水由最短的那块木板，那么应该从四端最短的木板找起，由于其是最短的木板，设其高度为$h$，那么：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果与其相邻的格子是木板，不做处理&lt;/li&gt;
&lt;li&gt;如果与其相邻的格子不是木板， 考虑其高度$hn$：
&lt;ul&gt;
&lt;li&gt;如果$hn &gt;= h$，那么该格子保存不了雨水，并且由于其比木板更高，原先的木板就没用了，该格子会代替原木板变为高度为$hn$的木板。&lt;/li&gt;
&lt;li&gt;如果$hn &amp;#x3C; h$，那么该格子能够保存$h - hn$单位的雨水，将这些雨水加入$ans$后，该格子不会能够接更多的雨水了，可以将其看做新的高度为$h$的木板，以此来维护新的木桶。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以使用最小堆来保存四周的木板高度，然后从最低的木板开始遍历，直到堆为空，$ans$中保存的是收集雨水的总和&lt;/p&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    struct Point {
        int x;
        int y;
        int h;
        friend bool operator &gt; (const Point&amp;#x26; lhs, const Point&amp;#x26; rhs) {
            return lhs.h &gt; rhs.h;
        }
    };
    int trapRainWater(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; heightMap) {
        int m = heightMap.size();
        int n = heightMap[0].size();
        if (m &amp;#x3C; 3 || n &amp;#x3C; 3) return 0;
        vector&amp;#x3C;int&gt; visited((m + 1) * (n + 1), 0);
        priority_queue&amp;#x3C;Point, vector&amp;#x3C;Point&gt;, greater&amp;#x3C;&gt;&gt; heap;
        auto arrMap = [n](int i, int j) -&gt; int {
            return i * n + j;
        };
        heap.push({0, 0, heightMap[0][0]}); visited[arrMap(0, 0)] = 1;
        heap.push({0, n - 1, heightMap[0][n - 1]}); visited[arrMap(0, n - 1)] = 1;
        heap.push({m - 1, 0, heightMap[m - 1][0]}); visited[arrMap(m - 1, 0)] = 1;
        heap.push({m - 1, n - 1, heightMap[m - 1][n - 1]}); visited[arrMap(m - 1, n - 1)] = 1;
        for (int i = 1; i &amp;#x3C; n - 1; i++) {
            heap.push({0, i, heightMap[0][i]}); visited[arrMap(0, i)] = 1;
            heap.push({m - 1, i, heightMap[m - 1][i]}); visited[arrMap(m - 1, i)] = 1;
        }
        for (int j = 1; j &amp;#x3C; m - 1; j++) {
            heap.push({j, 0, heightMap[j][0]}); visited[arrMap(j, 0)] = 1;
            heap.push({j, n - 1, heightMap[j][n - 1]}); visited[arrMap(j, n - 1)] = 1;
        }
        int remain = m * n - (2 * m + 2 * n - 4);
        int ans = 0;
        while (remain &gt; 0) {
            auto p = heap.top();
            heap.pop();
            for (int i = 0; i &amp;#x3C; 4; i++) {
                int nx = p.x + dx[i], ny = p.y + dy[i];
                if (nx &gt;= 0 &amp;#x26;&amp;#x26; nx &amp;#x3C; m &amp;#x26;&amp;#x26; ny &gt;= 0 &amp;#x26;&amp;#x26; ny &amp;#x3C; n) {
                    if (visited[arrMap(nx, ny)]) continue;
                    int h = heightMap[nx][ny];
                    visited[arrMap(nx, ny)] = 1;
                    remain--;
                    if (h &gt;= p.h) {
                        heap.push({nx, ny, h});
                    } else {
                        ans += p.h - h;
                        heap.push({nx, ny, p.h});
                    }
                }
            }
        }
        return ans;
    }
    constexpr static int dx[] = {0, 0, 1, -1};
    constexpr static int dy[] = {1, -1, 0, 0};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：二维矩阵$m$行$n$列，时间复杂度为$O(mnlog(mn))$&lt;/p&gt;
&lt;h2&gt;1546. 和为目标值且不重叠的非空子数组的最大值（2025.10.2）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-number-of-non-overlapping-subarrays-with-sum-equals-target/description/&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;子数组是连续的，所以题目要求连续子序列和为target的最大数量（连续子序列相互不重叠），于是可以抽象为以下模型（假设一条线代表从i到j的子序列和为target）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/1546.B0Xu0jgy_Z1icQem.webp&quot; alt=&quot;1546&quot;&gt;&lt;/p&gt;
&lt;p&gt;该模型即求出最多的不相交的线段数量&lt;a href=&quot;https://leetcode.cn/problems/maximum-number-of-non-overlapping-subarrays-with-sum-equals-target/description/&quot;&gt;线段覆盖&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;根据贪心算法，将所有的线段以[start, end]的形式表示，根据end从小到大进行排序，然后依次遍历选择，即哪个线段在前就先选哪个&lt;/p&gt;
&lt;p&gt;对应的本题，从头遍历nums数组，对于一个数nums[i]，如果能找到一个start，使得[start, i]的和为target，就将序列数+1，&lt;strong&gt;问题的关键在于如何求start坐标&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以使用前缀和，当遍历nums时，同时记录前缀和sums，如果存在start，则sums[i] - sums[start - 1] = target&lt;/p&gt;
&lt;p&gt;即sums[start - 1] = sums[i] - target&lt;/p&gt;
&lt;p&gt;将前缀和用哈希表保存即可快速搜寻sums[start - 1]&lt;/p&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    int maxNonOverlapping(vector&amp;#x3C;int&gt;&amp;#x26; nums, int target) {
        int ans = 0;
        int i = 1;
        int n = nums.size();
        vector&amp;#x3C;int&gt; sum(n + 1, 0);
        while (i &amp;#x3C;= n) {
            unordered_set&amp;#x3C;int&gt; s;
            s.insert(0);
            while (i &amp;#x3C;= n) {
                sum[i] = sum[i - 1] + nums[i - 1];
                if (s.contains(sum[i] - target)) {
                    ans++;
                    break;
                }
                s.insert(sum[i]);
                i += 1;
            }
            if (i &gt; n) break;
            sum[i] = 0;
            i = i + 1;
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(n)$&lt;/p&gt;
&lt;h2&gt;1948. 删除系统中的重复文件夹（2025.9.30）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/delete-duplicate-folders-in-system/description/?envType=daily-question&amp;#x26;envId=2025-09-30&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;很容易想到使用Trie结构保存结点，再利用哈希表存储每个结点的子树结构，在遍历树时，如果不同节点有相同的子树结构，就将这些结点删去&lt;/p&gt;
&lt;p&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;/ol&gt;
&lt;p&gt;考虑针对某一个结点，对其子节点的遍历过程，将向下&lt;strong&gt;递&lt;/strong&gt;的动作用一个非字母字符表示，向上&lt;strong&gt;归&lt;/strong&gt;的动作用另一个非字母字符表示，就可以描述一颗树的形状，比如x结点有两个叶子节点y，z，可以表示为x(y)(z)&lt;/p&gt;
&lt;p&gt;一般的，定义如下括号表达式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于叶子结点，设其文件名为S，则其括号表达式为S&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于任意子树x，设x的儿子为y1，y2，...，则子树x的括号表达式为：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;x文件夹名 + (子树y1的表达式) + (子树y2的表达式) + ...&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为了括号表达式的唯一性，在求出y1，y2，...的表达式后，对这些表达式进行排序再拼接，就可以避免下面这种情况（子树相同但表达式不同）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;	  x        x
   / \      / \
  y   z    z   y
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&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;生成每个结点的括号表达式：&lt;/p&gt;
&lt;p&gt;先将每个结点的子树表达式拼接（即&lt;code&gt;(子树y1的表达式) + (子树y2的表达式) + ...&lt;/code&gt;），记录到哈希表中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果该表达式是首次生成，将表达式和对应的子树根节点x保存到哈希表中&lt;/li&gt;
&lt;li&gt;否则文件夹出现重复，把当前根节点x和哈希表中记录的根节点都标记为删除&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DFS遍历Trie树，仅遍历未被删除的结点，同时记录路径上的文件夹名&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    struct Node {
        Node() :name(&quot;&quot;), deleted(false) {}
        Node(string ne) : name(ne), deleted(false) {}

        ~Node() {
            clear();
        }
        void clear() {
            for (auto it = children.begin(); it != children.end(); it++) {
                if (it-&gt;second) {
                    it-&gt;second-&gt;clear();
                    delete it-&gt;second;
                }
                it-&gt;second = nullptr;
            }
        }
        string name;
        unordered_map&amp;#x3C;string, Node*&gt; children;
        bool deleted;
    };
    void constructTrie(Node *node, vector&amp;#x3C;string&gt; &amp;#x26;path) {
        for (string name : path) {
            if (!node-&gt;children.contains(name)) {
                node-&gt;children[name] = new Node(name);
            }
            node = node-&gt;children[name];
        }
    }
    string generateHash(unordered_map&amp;#x3C;string, Node*&gt; &amp;#x26;hash, Node *root) {
        if (root == nullptr) {
            return &quot;&quot;;
        }
        if (root-&gt;children.empty()) {
            return root-&gt;name;
        }
        string res(&quot;&quot;);
        vector&amp;#x3C;string&gt; sub_hash;
        for (auto&amp;#x26;&amp;#x26; child : root-&gt;children) {
            sub_hash.push_back(&quot;(&quot; + generateHash(hash, child.second) + &quot;)&quot;);
        }
        sort(sub_hash.begin(), sub_hash.end());
        for (auto&amp;#x26;&amp;#x26; sub_hash : sub_hash) {
            res += sub_hash;
        }
        // compare
        if (hash.contains(res)) {
            root-&gt;deleted = true;
            hash[res]-&gt;deleted = true;
        } else {
            hash[res] = root;
        }
        res = root-&gt;name + res;
        return res;
    }
    void dfs(Node* root, vector&amp;#x3C;string&gt; &amp;#x26;path, vector&amp;#x3C;vector&amp;#x3C;string&gt;&gt; &amp;#x26;res) {
        if (root == nullptr || root-&gt;deleted) {
            return;
        }
        path.push_back(root-&gt;name);
        res.push_back(path);
        for (auto&amp;#x26;&amp;#x26; child : root-&gt;children) {
            if (!child.second-&gt;deleted)
                dfs(child.second, path, res);
        }
        path.pop_back();
    }
    vector&amp;#x3C;vector&amp;#x3C;string&gt;&gt; deleteDuplicateFolder(vector&amp;#x3C;vector&amp;#x3C;string&gt;&gt;&amp;#x26; paths) {
        Node* node = new Node(&quot;/&quot;);
        for (auto &amp;#x26;&amp;#x26;path : paths) {
            constructTrie(node, path);
        }
        unordered_map&amp;#x3C;string, Node*&gt; hash;
        generateHash(hash, node);
        vector&amp;#x3C;vector&amp;#x3C;string&gt;&gt; res;
        vector&amp;#x3C;string&gt; path;
        for (auto&amp;#x26;&amp;#x26; child : node-&gt;children) {
            dfs(child.second, path, res);
        }
        delete node;
        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1039. 多边形三角剖分的最低得分（2025.9.29）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-score-triangulation-of-polygon/?envType=daily-question&amp;#x26;envId=2025-09-29&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;该题的考点是区间动态规划，仍然从集合的角度来分析&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;状态表示：dp[l, r]表示由边(l, l + 1)， (l + 1, l + 2)，...，(r - 1, r)，(r, l)组成的多边形被剖分成不同的三角形集合&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;ul&gt;
&lt;li&gt;
&lt;p&gt;首先考虑如何划分集合：&lt;/p&gt;
&lt;p&gt;由于多边形最后最划分为多个三角形，那么多边形的边都是各个三角形的边，我们以(r, l)这条边隶属于哪个三角形来划分集合：&lt;/p&gt;
&lt;p&gt;(r, l)的两个端点l, r, 只需要再加一个端点就可以组成三角形，设端点为k，则&lt;strong&gt;k = l + 1, ... r - 1&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​      于是由k和边(r, l)组成的三角形将原多边形划分为了三个部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     1. [l, k]：以(l, l + 1),...,(k, l)边组成的新多边形
     2. [k, r]：以(k, k + 1),...,(r, k)边组成的新多边形
     3. k&amp;#x26;(r, l)：以点k和边(r, l)组成的三角形
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;于是状态转移公式：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
dp[l, r] = min(dp[l, k] + dp[k, r] + v[l] * v[r] * v[k])
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为什么选择(r, l)边作为三角形的一个边处理？&lt;/p&gt;
&lt;p&gt;因为状态表示中无法直接涉及到(r, l)，而直接使用(r, l)边的三角形又能够将集合划分为不同的部分来完成状态的转移&lt;/p&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    int minScoreTriangulation(vector&amp;#x3C;int&gt;&amp;#x26; values) {
        constexpr int N = 50;
        constexpr int INF = 0x3f3f3f3f;
        vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; dp(N, vector&amp;#x3C;int&gt;(N, INF));
        int n = values.size();
        for (int i = 0; i &amp;#x3C; n; i++) {
            dp[i][i] = 0;
            if (i + 1 &amp;#x3C; n) {
                dp[i][i + 1] = 0;
            }
        }
        for (int len = 2; len &amp;#x3C; n; len++) {
            for (int l = 0; l &amp;#x3C; n; l++) {
                int r = l + len;
                if (r &gt;= n) continue;
                // k: l + 1 -&gt; r - 1
                for (int k = l + 1; k &amp;#x3C; r; k++) {
                    dp[l][r] = min(dp[l][r], 
                        dp[l][k] + dp[k][r] + values[l] * values[k] * values[r]);
                }
            }
        }
        return dp[0][n - 1];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(n^3)$&lt;/p&gt;
&lt;h2&gt;2040. 两个有序数组的第K小乘积（2025.9.28）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/kth-smallest-product-of-two-sorted-arrays/description/?envType=daily-question&amp;#x26;envId=2025-09-28&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;直接枚举$n^2$显然是不现实的，注意到两个数组的乘积值域在$[-10^{10}, 10^{10}]$之间&lt;/p&gt;
&lt;p&gt;因此可以考虑对答案进行二分：&lt;/p&gt;
&lt;p&gt;对于一个数&lt;code&gt;mid&lt;/code&gt;，设两个数组的乘积中小于&lt;code&gt;mid&lt;/code&gt;的数有&lt;code&gt;cnt&lt;/code&gt;个，那么：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;cnt &amp;#x3C; k&lt;/code&gt;，则&lt;code&gt;mid&lt;/code&gt;有可能是答案，提高答案的下界&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cnt &gt;= k&lt;/code&gt;，则&lt;code&gt;mid&lt;/code&gt;不可能是答案，降低答案的上界&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;于是问题转化为如何求两个数组的乘积中小于&lt;code&gt;mid&lt;/code&gt;的个数&lt;/p&gt;
&lt;h3&gt;二分&lt;/h3&gt;
&lt;p&gt;由&lt;code&gt;nums1 × nums2 &amp;#x3C; mid&lt;/code&gt;（&lt;code&gt;nums1&lt;/code&gt;不为0），得：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;nums2 &amp;#x3C; mid / nums1&lt;/code&gt;（&lt;code&gt;nums1 &gt; 0&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nums2 &gt; mid / nums1&lt;/code&gt;（&lt;code&gt;nums1 &amp;#x3C; 0&lt;/code&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;两个数组都是排好序的，所以在遍历第一个数组的时候，利用二分求出小于/大于目标数的数字有多少个即可&lt;/p&gt;
&lt;p&gt;code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    using i64 = long long;
    bool check(vector&amp;#x3C;int&gt;&amp;#x26; nums1, vector&amp;#x3C;int&gt; &amp;#x26;nums2, i64 k, i64 num) {
        // 找nums1 * nums2中，&amp;#x3C; num的个数cnt，与k比较
        // cnt &amp;#x3C; k -&gt; true
        // cnt &gt;= k -&gt; false
        i64 cnt = 0;
        for (int n1 : nums1) {
            if (n1 == 0) {
                if (num &gt; 0) cnt += nums2.size();
                continue;
            } 
            i64 n2 = num / n1;
            auto up_it = upper_bound(nums2.begin(), nums2.end(), n2);
            auto low_it = lower_bound(nums2.begin(), nums2.end(), n2);
            if (n1 &gt; 0) {
                int d1 = distance(nums2.begin(), low_it);
                int d2 = distance(nums2.begin(), up_it);
                if (num &gt; 0 &amp;#x26;&amp;#x26; num % n1 != 0) {
                    cnt += max(0, d2);
                } else {
                    cnt += max(0, d1);
                }
            } else {
                int d1 = distance(low_it, nums2.end());
                int d2 = distance(up_it, nums2.end());
                if (num &gt; 0 &amp;#x26;&amp;#x26; num % n1 != 0) {
                    cnt += max(0, d1);
                } else {
                    cnt += max(0, d2);
                }
            }
        }
        return cnt &amp;#x3C; k;
    }
    long long kthSmallestProduct(vector&amp;#x3C;int&gt;&amp;#x26; nums1, vector&amp;#x3C;int&gt;&amp;#x26; nums2, long long k) {
        i64 low = min({1ll * nums1.front() * nums2.front(), 1ll * nums1.front() * nums2.back(),
                                1ll * nums1.back() * nums2.front(), 1ll * nums1.back() * nums2.back()});
        i64 high = max({1ll * nums1.front() * nums2.front(), 1ll * nums1.front() * nums2.back(),
                                1ll * nums1.back() * nums2.front(), 1ll * nums1.back() * nums2.back()});
        i64 ans = 0;
        while (low &amp;#x3C;= high) {
            i64 mid = (low + high) &gt;&gt; 1;
            if (check(nums1, nums2, k, mid)) {
                low = mid + 1;
                ans = mid;
            } else {
                high = mid - 1;
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(n (logn)(logk))$&lt;/p&gt;
&lt;h3&gt;矩阵&lt;/h3&gt;
&lt;p&gt;数组单增，负数正数同时存在，我们可以将分界线列出，假设：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;nums1[0~pos1]&lt;/code&gt;是负数，&lt;code&gt;nums1[pos1~n1]&lt;/code&gt;是正数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nums2[0~pos2]&lt;/code&gt;是负数，&lt;code&gt;nums1[pos1~n2]&lt;/code&gt;是正数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将&lt;code&gt;nums1[0~pos1]&lt;/code&gt;与&lt;code&gt;nums2[0~pos2]&lt;/code&gt;各个数分别相乘，观察性质(以两个数组中元素都大于0为例)：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/2040_1.BKngOqve_Z2uUa3u.webp&quot; alt=&quot;image-20250929174608939&quot;&gt;&lt;/p&gt;
&lt;p&gt;不难发现，左上角是最小值，随行增加，随列增加，右下角为最大值，之后求该矩阵中小于&lt;code&gt;mid&lt;/code&gt;的个数，参考&lt;a href=&quot;https://leetcode.cn/problems/search-a-2d-matrix-ii/description/?envType=study-plan-v2&amp;#x26;envId=top-100-liked&quot;&gt;leetcode-240&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;具体做法是利用双指针，指向矩阵右上角的坐标，计算矩阵中对应的位置元素，如果该元素大于等于&lt;code&gt;mid&lt;/code&gt;，向左移动，否则向下移动，直到找到最后一个小于&lt;code&gt;mid&lt;/code&gt;的元素，移动的过程中，每找到一个小于&lt;code&gt;mid&lt;/code&gt;的元素，就累加该行的个数&lt;/p&gt;
&lt;p&gt;code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    using i64 = long long;
    bool check(vector&amp;#x3C;int&gt;&amp;#x26; nums1, vector&amp;#x3C;int&gt; &amp;#x26;nums2, i64 k, i64 num) {
        // 找nums1 * nums2中，&amp;#x3C; num的个数cnt，与k比较
        // cnt &amp;#x3C; k -&gt; true
        // cnt &gt;= k -&gt; false
        i64 cnt = 0;
        for (int i = 0, j = pos2; i &amp;#x3C;= pos1 &amp;#x26;&amp;#x26; j &gt;= 0;) {
            if (1ll * nums1[i] * nums2[j] &gt;= num) {
                i++;
            }
            else {
                cnt += (pos1 - i + 1);
                j--;
            }
        }
        for (int i = 0, j = pos2 + 1; i &amp;#x3C;= pos1 &amp;#x26;&amp;#x26; j &amp;#x3C; nums2.size();) {
            if (1ll * nums1[i] * nums2[j] &gt;= num) {
                j++;
            } else {
                cnt += (nums2.size() - j);
                i++;
            }
        }
        for (int i = pos1 + 1, j = 0; i &amp;#x3C; nums1.size() &amp;#x26;&amp;#x26; j &amp;#x3C;= pos2;) {
            if (1ll * nums1[i] * nums2[j] &gt;= num) {
                i++;
            } else {
                cnt += (nums1.size() - i);
                j++;
            }
        }
        for (int i = pos1 + 1, j = nums2.size() - 1; i &amp;#x3C; nums1.size() &amp;#x26;&amp;#x26; j &gt; pos2;) {
            if (1ll * nums1[i] * nums2[j] &gt;= num) {
                j--;
            } else {
                cnt += (j - pos2);
                i++;
            }
        }
        return cnt &amp;#x3C; k;
    }
    long long kthSmallestProduct(vector&amp;#x3C;int&gt;&amp;#x26; nums1, vector&amp;#x3C;int&gt;&amp;#x26; nums2, long long k) {
        i64 low = min({1ll * nums1.front() * nums2.front(), 1ll * nums1.front() * nums2.back(),
                                1ll * nums1.back() * nums2.front(), 1ll * nums1.back() * nums2.back()});
        i64 high = max({1ll * nums1.front() * nums2.front(), 1ll * nums1.front() * nums2.back(),
                                1ll * nums1.back() * nums2.front(), 1ll * nums1.back() * nums2.back()});
        i64 ans = 0;
        for (int i = 0; i &amp;#x3C; nums1.size(); i++) {
            if (nums1[i] &amp;#x3C; 0) pos1 = i;            
        }
        for (int i = 0; i &amp;#x3C; nums2.size(); i++) {
            if (nums2[i] &amp;#x3C; 0) pos2 = i;            
        }
        while (low &amp;#x3C;= high) {
            i64 mid = (low + high) &gt;&gt; 1;
            if (check(nums1, nums2, k, mid)) {
                low = mid + 1;
                ans = mid;
            } else {
                high = mid - 1;
            }
        }
        return ans;
    }
private:
    int pos1{-1};
    int pos2{-1};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(nlogk)$&lt;/p&gt;
&lt;h2&gt;976. 三角形的最大周长（2025.9.28）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/largest-perimeter-triangle/description/?envType=daily-question&amp;#x26;envId=2025-09-28&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;枚举三条边a，b，c，三角形三边需要满足：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a + b &gt; c&lt;/li&gt;
&lt;li&gt;a + c &gt; b&lt;/li&gt;
&lt;li&gt;b + c &gt; a&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果按边大小排序，设c &gt;= b &gt;= a，能简化为一个条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a + b &gt; c&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将边从大到小排序，在枚举一个最长边nums[i] = c时，能使得三角形周长最大的合法集合只能为nums[i], nums[i + 1], nums[i + 2]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果nums[i], nums[i + 1], nums[i + 2]合法，则三条边组成的三角形周长最大&lt;/li&gt;
&lt;li&gt;如果不合法，那么以nums[i] = c作为最长边将不能和nums[i + 3]及之后的边组成三角形&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    int largestPerimeter(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        sort(nums.begin(), nums.end(), greater&amp;#x3C;&gt;());
        int ans = 0;
        for (int i = 2; i &amp;#x3C; nums.size(); i++) {
            // a &amp;#x3C;= b &amp;#x3C;= c
            int a = nums[i], b = nums[i - 1], c = nums[i - 2];
            if (c &amp;#x3C; a + b) {
                ans = a + b + c;
                break;
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度： $O(n)$&lt;/p&gt;
&lt;h2&gt;812. 最大三角形面积（2025.9.27）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/largest-triangle-area/description/?envType=daily-question&amp;#x26;envId=2025-09-27&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;枚举三个点A，B，C，设三条边的边长为a，b，c&lt;/p&gt;
&lt;p&gt;根据海伦公式（或者用余弦定理）得出：&lt;/p&gt;
&lt;p&gt;$S = \sqrt {p (p - a) (p - b) (p - c)}$&lt;/p&gt;
&lt;p&gt;其中，$p = \frac {a + b + c} {2}$&lt;/p&gt;
&lt;p&gt;注意，如果三个点x坐标或y坐标相同，三角形不合法&lt;/p&gt;
&lt;p&gt;code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    double getDistance(int x1, int y1, int x2, int y2) {
        double d2 = pow(x1 - x2, 2) + pow(y1 - y2, 2);
        return sqrt(d2);
    }
    double largestTriangleArea(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; points) {
        int n = points.size();
        double ans = INT_MIN;
        for (int i = 0; i &amp;#x3C; n; i++) {
            for (int j = i + 1; j &amp;#x3C; n; j++) {
                for (int k = j + 1; k &amp;#x3C; n; k++) {
                    auto &amp;#x26;pi = points[i];
                    auto &amp;#x26;pj = points[j];
                    auto &amp;#x26;pk = points[k];
                    if (pi[0] == pj[0] &amp;#x26;&amp;#x26; pi[0] == pk[0]) continue;
                    if (pi[1] == pj[1] &amp;#x26;&amp;#x26; pi[1] == pk[1]) continue;
                    double a = getDistance(pi[0], pi[1], pj[0], pj[1]);
                    double b = getDistance(pi[0], pi[1], pk[0], pk[1]);
                    double c = getDistance(pj[0], pj[1], pk[0], pk[1]);
                    double sum = (a + b + c) / 2;
                    ans = max(ans, sqrt(sum * (sum - a) * (sum - b) * (sum - c)));
                }
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度： $O(n^3)$&lt;/p&gt;
&lt;h2&gt;611. 有效三角形的个数（2025.9.26）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/valid-triangle-number/description/?envType=daily-question&amp;#x26;envId=2025-09-26&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;排序 + 二分&lt;/h3&gt;
&lt;p&gt;将边长度按从小到大排序&lt;/p&gt;
&lt;p&gt;因为需要三条边，无论如何都要枚举两条边a，b，不妨从小到大进行枚举（即&lt;code&gt;a &amp;#x3C;= b&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;设第三条边为c，那么c需要满足：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;a + b &gt; c&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a + c &gt; b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b + c &gt; a&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在枚举c的过程中，为了避免重复，c必须满足&lt;code&gt;c &gt;= b&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;故上述条件简化为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;a &amp;#x3C;= b &amp;#x3C;= c&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a + b &gt; c&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;设a，b对应的下标分别为i，j，那么c的下标范围是：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[l, r] = [j + 1, lower_bound(nums.begin() + j + 1, nums.end(), a + b)]&lt;/code&gt;
，如果&lt;code&gt;r &amp;#x3C; l&lt;/code&gt;，则没有与a，b对应的c存在
code：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    int triangleNumber(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        // 枚举a, b
        // c: a + b &gt; c, a + c &gt; b, b + c &gt; a
        // -&gt; c &amp;#x3C; a + b &amp;#x26;&amp;#x26; c &gt; b - a &amp;#x26;&amp;#x26; c &gt; a - b
        sort(nums.begin(), nums.end());
        int ans = 0;
        for (int i = 0; i &amp;#x3C; nums.size(); i++) {
            for (int j = i + 1; j &amp;#x3C; nums.size(); j++) {
                int k = j + 1;
                int start = max(nums[j] - nums[i], nums[i] - nums[j]);
                int end = nums[i] + nums[j];
                auto l = upper_bound(nums.begin() + k, nums.end(), start);
                auto r = lower_bound(nums.begin() + k, nums.end(), end);
                if (r - l &amp;#x3C; 0) continue;
                ans += (r - l);
            }
        }
        if (ans &amp;#x3C; 0) ans = 0;
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度: $O(n^2logn)$&lt;/p&gt;
&lt;h3&gt;排序 + 双指针&lt;/h3&gt;
&lt;p&gt;二分的代码不难发现一个规律：枚举a，b后，寻找c时，只需要找到满足&lt;code&gt;c &amp;#x3C; a + b&lt;/code&gt;的最大的c&lt;/p&gt;
&lt;p&gt;换句话说，对于前一个b，有&lt;code&gt;c &amp;#x3C; a + b&lt;/code&gt;，对于下一个b，也有可能有&lt;code&gt;c &amp;#x3C; a + b&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;因此，我们只需要维护c所在区间的最大值下标&lt;/p&gt;
&lt;p&gt;条件是：只要&lt;code&gt;c &amp;#x3C; a + b&lt;/code&gt;，就让c的下标右移&lt;/p&gt;
&lt;p&gt;c所在区间的最小值下标一定是b的下标+1&lt;/p&gt;
&lt;p&gt;于是可以采用双指针，在枚举b的同时，维护c当前可能的最大值下标&lt;/p&gt;
&lt;p&gt;code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    int triangleNumber(vector&amp;#x3C;int&gt;&amp;#x26; nums) {
        // 枚举a, b
        // c: a + b &gt; c
        // a &amp;#x3C;= b &amp;#x3C;= c
        sort(nums.begin(), nums.end());
        int ans = 0;
        for (int i = 0; i &amp;#x3C; nums.size(); i++) {
            for (int j = i + 1, k = j + 1; j &amp;#x3C; nums.size(); j++) {
                // k是c合法范围内的最大值下标
                k = max(k, j + 1);
                int end = nums[i] + nums[j];
                while (k &amp;#x3C; nums.size() &amp;#x26;&amp;#x26; nums[k] &amp;#x3C; end) {
                    k++;
                }
                ans += max(0, k - j - 1);
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：$O(n^2)$&lt;/p&gt;
&lt;h2&gt;120. 三角形最小路径和（2025.9.25）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/triangle/description/?envType=daily-question&amp;#x26;envId=2025-09-25&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;三角形dp，从集合的角度分析：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;状态表示：dp(i, j)：从triangle(0, 0)走到(i, j)的不同路径组成的集合&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;ul&gt;
&lt;li&gt;
&lt;p&gt;从(i - 1, j)走到(i, j)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从(i - 1, j - 1)走到(i, j)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转移公式：
$$
dp(i, j) =
\begin{cases}
triangle(i, j), &amp;#x26; i = 0, j = 0 \
dp(i - 1, j) + triangle(i, j), &amp;#x26; i &gt; 0, j = 0 \
min(dp(i - 1, j), dp(i - 1, j - 1)) + triangle(i, j) &amp;#x26; i &gt; 0, j &gt; 0 \
\end{cases}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化：注意到在计算dp(i, ...)时，只需要dp(i - 1, ...)，所以可以将状态优化为1维数组，在计算状态时只保留上一层的状态dp(j)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    int minimumTotal(vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt;&amp;#x26; triangle) {
        int ans = INT_MAX;
        vector&amp;#x3C;int&gt; dp(triangle.size(), INT_MAX / 2);
        dp[0] = triangle[0][0];
        for (int i = 1; i &amp;#x3C; triangle.size(); i++) {
            for (int j = triangle[i].size() - 1; j &gt;= 0; j--) {
                int old_dp = INT_MAX / 2;
                if (j &gt;= 1) old_dp = dp[j - 1];
                dp[j] = min(dp[j], old_dp) + triangle[i][j];
            }
        }
        for (int i = 0; i &amp;#x3C; dp.size(); i++) {
            ans = min(ans, dp[i]);
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度： $O(n^2)$&lt;/p&gt;
&lt;h2&gt;166. 分数到小数（2025.9.24）&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/fraction-to-recurring-decimal/description/?envType=daily-question&amp;#x26;envId=2025-09-24&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;问题的难度在于如何判定循环小数的范围&lt;/p&gt;
&lt;p&gt;使用长除法观察示例$\frac {4} {333} = 0.\dot 0 \dot 1 \dot 2$：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/166_1.BYFurA9c_ZG6PDm.webp&quot; alt=&quot;166_1&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;小数点后第1位为0时，对应的被除数为40&lt;/li&gt;
&lt;li&gt;小数点后第2位为1时，对应的被除数为400&lt;/li&gt;
&lt;li&gt;小数点后第3位为2时，对应的被除数为670&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以此类推...， 小数点后第4位开始循环，对应的被除数为40&lt;/p&gt;
&lt;p&gt;不难发现，当小数点后开始循环时，对应的被除数一定已经出现过，可以使用哈希表来保存出现过的被除数，当一个被除数重复出现时，说明有循环小数并已经开始循环&lt;/p&gt;
&lt;p&gt;于是问题来到了如何判断哪一部分是循环小数，我们可以通过存储下面两个信息来解决：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;小数点后第k位对应的被除数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小数点后当前的总位数n&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当被除数重复时，小数点后&lt;strong&gt;第k位~第n位&lt;/strong&gt;就是循环小数所在的部分，前者可以用hashmap解决，后者可以开辟一个额外变量来记录&lt;/p&gt;
&lt;p&gt;由于题目中分子，分母的范围在[INT_MIN, INT_MAX]，所以在计算时最好使用long long，同时将小数点前和小数点后的计算分开，更加清晰&lt;/p&gt;
&lt;p&gt;code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class Solution {
public:
    using i64 = long long;
    void transform_quotient(string &amp;#x26;ans, i64 quotient) {
        string res(&quot;&quot;);
        while (quotient &gt; 0) {
            int mod = quotient % 10;
            quotient /= 10;
            res += static_cast&amp;#x3C;char&gt;(mod + &apos;0&apos;);
        }
        if (res.size() == 0) return;
        for (int i = res.size() - 1; i &gt;= 0; i--) {
            ans += res[i];
        }
    }
    string fractionToDecimal(int numerator, int denominator) {
        bool neg1 = numerator &amp;#x3C; 0;
        bool neg2 = denominator &amp;#x3C; 0;
        i64 numerator1 = abs(numerator * 1ll);
        i64 denominator1 = abs(denominator * 1ll);
        string ans(&quot;&quot;);
        // 小数点前
        if (numerator1 &gt;= denominator1) {
            while (numerator1 &gt;= denominator1) {
                i64 quotient = numerator1 / denominator1;
                numerator1 %= denominator1;
                transform_quotient(ans, quotient);
            }
        } else {
            ans += &apos;0&apos;;
        }
        // 小数点后
        if (numerator1 != 0){
            ans += &apos;.&apos;;
            int index = ans.size();
            vector&amp;#x3C;int&gt; cycle;
            unordered_map&amp;#x3C;int, int&gt; hash;
            while (true) {
                numerator1 *= 10;
                if (hash.contains(numerator1)) {
                    int st = hash[numerator1];
                    string tmp = ans.substr(0, cycle[st]) + &apos;(&apos;;
                    for (int i = cycle[st]; i &amp;#x3C; ans.size(); i++) {
                        tmp += ans[i];
                    }
                    tmp += &apos;)&apos;;
                    ans = tmp;
                    break;
                }
                int quotient = numerator1 / denominator1;
                cycle.push_back(index);
                hash[numerator1] = cycle.size() - 1;
                index++;
                numerator1 %= denominator1;
                ans += static_cast&amp;#x3C;char&gt;(quotient + &apos;0&apos;);
                if (numerator1 == 0) {
                    break;
                }
            }
        }
        if (neg1 != neg2 &amp;#x26;&amp;#x26; numerator != 0) {
            ans = &quot;-&quot; + ans;        
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：设小数点后位数数量级为n，代码时间复杂度为$O(n)$&lt;/p&gt;</content:encoded><h:img src="/_astro/leetcode.BDC3Z14l.png"/><enclosure url="/_astro/leetcode.BDC3Z14l.png"/></item><item><title>java速成</title><link>https://liang-bk.github.io/blog/cpp2java</link><guid isPermaLink="true">https://liang-bk.github.io/blog/cpp2java</guid><description>c++语法快速过渡到java语法</description><pubDate>Mon, 20 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一. 传参&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;java普通类型如&lt;code&gt;int&lt;/code&gt;，&lt;code&gt;double&lt;/code&gt;，&lt;code&gt;char&lt;/code&gt;，与c++相同，都是值传递，方法内修改不影响外部传入的值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ValuePassing {
   public static void main(String[] args) {
       int x = 10;
       modifyValue(x);
       // x仍然为10
   }
   public static void modifyValue(int value) {
       value = 20; // 修改的是副本，不影响原始变量
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;java对象类型如&lt;code&gt;String&lt;/code&gt;，类似c++传递指针参数，传入的是指向对象的指针，可以在方法内修改对象，但对该对象整体重新赋值则相当于c++中让函数内指针指向新内存地址，不会改变外部对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Switch {
    public static void main(String[] args) {
        String x1 = &quot;10&quot;;
        String y1 = &quot;20&quot;;
        System.out.println(&quot;交换前x1 = &quot;+x1);
        System.out.println(&quot;交换前y1 = &quot;+y1);
		// 进行数据交换
        swap(x1, y1);
		// x1, y1不会变化
        System.out.println(&quot;交换后x1 = &quot;+x1);
        System.out.println(&quot;交换后y1 = &quot;+y1);
 
 
    }
 	// 在c++中相当于swap(string* x, string* y)
    public static void swap(String x, String y) {
        // 跟c++类似，传递的指针实际是复制了一份指针指向对象
        // java中重新赋值相当于c++让复制的那份指针指向别的内存，但原指针不变
        String z ;
        z = x;
        x = y;
        y = z;
        System.out.println(&quot;x = &quot;+x);
        System.out.println(&quot;y = &quot;+y);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;void func() {
    string* s1 = new string(&quot;123&quot;);
    string* s2 = new string(&quot;456&quot;);
    swap(s1, s2);
    // s1, s2的指向仍然没变
}
void swap(string* s1, string* s2) {
    string* temp = s1;
    s1 = s2;
    s2 = temp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;java引用传递，即传数组，与c++行为一致&lt;/p&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;c++使用&lt;code&gt;: public ClassName&lt;/code&gt;表示继承，java使用&lt;code&gt;extends ClassName&lt;/code&gt;表示继承&lt;/li&gt;
&lt;li&gt;c++可以多继承，java只有单继承&lt;/li&gt;
&lt;/ol&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;默认继承父类的成员变量和方法&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;可见性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;private&lt;/code&gt;：子类不可直接访问&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protect/public&lt;/code&gt;：子类可直接访问&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.1 方法重写&lt;/h3&gt;
&lt;p&gt;java中子类定义与父类相同的方法（方法名，形参），&lt;strong&gt;默认是重写行为&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;c++中子类与父类相同的方法，默认是两个不同的方法&lt;/p&gt;
&lt;h3&gt;2.2 super&lt;/h3&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在子类构造方法中调用父类的构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// B继承A
public B() {
    // 调用父类无参构造
    super();
}
public B(int x) {
    // 调用父类有参构造
    super(x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;访问父类中的变量/方法&lt;/p&gt;
&lt;p&gt;形式：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;super.var&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;super.method()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三. 多态&lt;/h2&gt;
&lt;h3&gt;3.1 强制方法重写&lt;/h3&gt;
&lt;p&gt;java中在子类方法上使用&lt;code&gt;@Override&lt;/code&gt;注解，可以由编译器检查是否进行了正确的重写&lt;/p&gt;
&lt;p&gt;其行为与c++ &lt;code&gt;override&lt;/code&gt;关键字类似，在子类方法后加上&lt;code&gt;override&lt;/code&gt;，可以重写父类中定义的虚方法（&lt;code&gt;virtual method&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;java重写&lt;strong&gt;Object&lt;/strong&gt;的三个方法（常用）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 显示更有意义的字符串:
@Override
public String toString() {
    return &quot;&quot;;
}

// 比较当前对象和对象o是否相等: 逻辑相等
@Override
public boolean equals(Object o) {
    return false
}

// 计算hash:
@Override
public int hashCode() {
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 抽象类&lt;/h3&gt;
&lt;p&gt;与c++中的纯虚类概念类似，包含抽象方法/纯虚函数的类&lt;/p&gt;
&lt;p&gt;定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;abstract class A {
    private String s;
    public abstract void method();
    public void method2();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;抽象类不能实例化&lt;/li&gt;
&lt;li&gt;类和抽象方法都被&lt;code&gt;abstract&lt;/code&gt;修饰&lt;/li&gt;
&lt;li&gt;可以定义普通变量和方法&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 类型转换&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;子转父：直接强制类型转换即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Child c = new Child();
Father f = (Father) c; // 去掉强制转换也生效
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;父转子：有风险，使用&lt;code&gt;instanceof&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Object obj = &quot;hello&quot;;
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.toUpperCase());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.4 接口&lt;/h3&gt;
&lt;p&gt;一组抽象方法的集合，所有方法默认是&lt;code&gt;public abstract&lt;/code&gt;修饰&lt;/p&gt;
&lt;p&gt;相当于c++的纯虚函数类只定义纯虚方法不定义变量和普通方法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;interface A() {
    void method1();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类实现接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class B implements A {
    @Override
    public void method1() {
        
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;接口可以继承接口&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类可以实现多个接口（&lt;code&gt;implements A, B&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接口可以定义&lt;code&gt;default&lt;/code&gt;方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;interface A() {
    void method1();
    default void method2() {
        method1();
        System.out.println(&quot;&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;default&lt;/code&gt;方法的目的是，当我们需要给接口新增一个方法时，会涉及到修改全部子类。如果新增的是&lt;code&gt;default&lt;/code&gt;方法，那么子类就不必全部修改，只需要在需要覆写的地方去覆写新增方法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当一个类实现了一个接口，就可以使用接口类型指向类的实例，从而调用实际的接口方法来完成对应功能&lt;/p&gt;
&lt;h3&gt;3.5 内部类&lt;/h3&gt;
&lt;p&gt;顾名思义，就是在类A中定义类B，B被称为内部类&lt;/p&gt;
&lt;p&gt;与c++不同，c++在类A中定义类B，除了影响类B的作用域外，两者几乎没有关系&lt;/p&gt;
&lt;p&gt;java中三种内部类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;普通内部类，类A中明确定义类B：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Outer {
    private String name;

    Outer(String name) {
        this.name = name;
    }

    class Inner {
        void hello() {
            System.out.println(&quot;Hello, &quot; + Outer.this.name);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;内部类可以访问外部类的private字段&lt;/li&gt;
&lt;li&gt;内部类依附外部类，持有一个外部类的引用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态内部类（使用&lt;code&gt;static&lt;/code&gt;修饰内部类）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Outer {
    private static String NAME = &quot;OUTER&quot;;

    private String name;

    Outer(String name) {
        this.name = name;
    }

    static class StaticNested {
        void hello() {
            System.out.println(&quot;Hello, &quot; + Outer.NAME);
        }
    }
}
&lt;/code&gt;&lt;/pre&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;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 实现接口的匿名类
Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println(&quot;Hello, &quot; + Outer.this.name);
    }
};
// 继承父类的匿名类
HashMap&amp;#x3C;String, String&gt; map2 = new HashMap&amp;#x3C;&gt;() {
    // 该类继承了HashMap, 可以访问其中的所有变量和方法
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;继承父类或实现接口&lt;/li&gt;
&lt;li&gt;可以访问父类中的变量和方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3.5.1 lambda表达式&lt;/h4&gt;
&lt;p&gt;java与c++中的lambda表达式形式基本相同，但本质不太一样，c++的lambda表达式本质是重载了&lt;code&gt;()&lt;/code&gt;的对象，也可以叫做函数对象，java则是对匿名内部类的一种简化写法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;java lambda：本质是实现接口的方法&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;@FunctionalInterface&lt;/code&gt;来注解一个接口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@FuntionalInterface
interface Swim {
    void swimming();
}

public static void main(String[] args) {
    Swim swim = () -&gt; {
        System.out.println(&quot;swim...&quot;);
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上所示，当某个方法使用&lt;code&gt;Swim&lt;/code&gt;接口做参数时，可以这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void func1(Swim s) {
    ...
}
void call_func1() {
    func1(() -&gt; {
        System.out.println(&quot;swim...&quot;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于lambda表达式是对应接口的实现，所以&lt;code&gt;()&lt;/code&gt;是接口方法的形参列表，函数体返回值也要和接口方法保持一致&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;c++ lambda：&lt;/p&gt;
&lt;p&gt;以&lt;code&gt;std::sort()&lt;/code&gt;为例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;std::vector&amp;#x3C;int&gt; v {1, 2, 5, 4, 3};
std::sort(v.begin(), v.end(), [](int x, int y) -&gt; bool {
    return x &gt; y;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形式为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// -&gt; return_type可省略
auto lambda = [捕获](形参列表) -&gt; return_type {
    函数体
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本质是把对象当函数用，没有java的必须实现接口的限制，还多了可捕获列表，不过二者在写法上大差不差&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3.5.2 方法引用&lt;/h4&gt;
&lt;p&gt;对lambda进一步的简化，用已有的方法来代替lambda表达式成为函数式接口的方法体，参数，返回值等要和接口方法保持一致&lt;/p&gt;
&lt;p&gt;使用形式很像c++带作用域的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Function1 {
    public static int subtraction(int a, int b) {
        return b - a;
    }
}
// 方法引用
Arrays.sort(arr, Function1::subtraction);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;静态方法引用：&lt;code&gt;类名::方法名&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象方法引用：&lt;code&gt;对象::方法名&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引用本类方法：&lt;code&gt;this::方法名&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;引用父类方法：&lt;code&gt;super::方法名&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类名引用成员：跟接口中抽象方法的第一个参数有关&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ArrayList&amp;#x3C;String&gt; list = new ArrayList&amp;#x3C;&gt;();
// 
list.stream.map(String::toUpperCase);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上，&lt;code&gt;map()&lt;/code&gt;里要求实现的抽象方法第一个参数是&lt;code&gt;String str&lt;/code&gt;，那么可以直接引用&lt;code&gt;String&lt;/code&gt;类的方法，其实际含义为调用&lt;code&gt;str.toUpperCase()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造方法引用：&lt;code&gt;类名::new&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;四. 泛型&lt;/h2&gt;
&lt;p&gt;类似c++模板，但是伪泛型（只在编译期检查）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;泛型类&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 可以指定多个类型
public class A&amp;#x3C;T, E&gt; {
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;泛型方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 在返回值前加&amp;#x3C;T&gt;标识方法为泛型方法
public &amp;#x3C;T&gt; void method(T val) {
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;泛型接口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface B&amp;#x3C;E&gt; {
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现类实现接口时，可以指定E为具体类型，也可以延续泛型，创建对象时再确定&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;泛型通配符&lt;/p&gt;
&lt;p&gt;泛型不具备继承性，但数据具有继承性&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;? extends E&lt;/code&gt;：表示可以传递E和E的所有子类类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;? super E&lt;/code&gt;：表示可以传递E和E的所有父类类型&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;五. 常用数据结构和API&lt;/h2&gt;
&lt;h3&gt;5.1 String&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;构造&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public String();
public String(String original);
public String(char[] chs);
public String(byte[] chs);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int length(); // 长度
char charAt(int index); // 指定索引字符
char[] toCharArray(); // 转为字符数组
String substring(int beginIndex, int endIndex); // 截取子字符串
String toUpperCase(); // 大小写转换
boolean equals(Object anObject); // 比较
boolean equalsIgnoreCase(String antherString); // 忽略大小写比较
String[] split(String regex); // 根据指定正则表达式分割字符串
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.2 StringBuilder&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;构造&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public StringBuilder();
public StringBuilder(String str);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;StringBuilder append(任意类型); // 在末尾追加
StringBuilder reverse();	// 反转字符串
int length();	// 当前字符串长度
String toString();	// 转为String类型
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.3 StringJoiner&lt;/h3&gt;
&lt;p&gt;添加字符串之间需要用&lt;strong&gt;间隔符&lt;/strong&gt;分开，并最终需要添加开头和结尾符号&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;构造&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public StringJoiner(间隔符);
public StringJoiner(间隔符, 开始符, 结尾符);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;StringJoiner add(任意类型);	// 追加
int length();		// 长度
String toString();	// 转为String类型 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.4 Math&lt;/h3&gt;
&lt;p&gt;功能和&lt;code&gt;cmath&lt;/code&gt;类似&lt;/p&gt;
&lt;p&gt;使用时一般是&lt;code&gt;Math.func()&lt;/code&gt;的形式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int abs(int);	// 绝对值
double ceil(double);	// 上取整
double floor(double);	// 下取整
int round(float);		// 四舍五入
int max(int, int);		// 最大
double pow(double, double);	// 指数
double random();		// [0.0, 1.0)的随机值
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.5 System&lt;/h3&gt;
&lt;p&gt;三个：退出，当前时间，数组拷贝&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;System.exit(0);
long ms = System.currentTimeMillis();

System.arraycopy(src, index1, dest, index2, len);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.6 Object&lt;/h3&gt;
&lt;p&gt;其中三个在&lt;strong&gt;强制方法重写&lt;/strong&gt;里记过，&lt;code&gt;toString()&lt;/code&gt;，&lt;code&gt;equals()&lt;/code&gt;，&lt;code&gt;hashCode()&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;clone()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;类似c++中的拷贝构造，同样分深克隆和浅克隆&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;浅克隆：重写的&lt;code&gt;clone()&lt;/code&gt;中直接调用&lt;code&gt;super.clone()&lt;/code&gt;，会默认返回一个浅克隆的&lt;code&gt;Object&lt;/code&gt;对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;深克隆：先&lt;code&gt;super.clone()&lt;/code&gt;，返回的对象中的引用数据类型重新从当前对象复制一遍即可&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Objects&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;三个静态方法，&lt;code&gt;equals&lt;/code&gt;，&lt;code&gt;isNull&lt;/code&gt;，&lt;code&gt;nonNull&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;5.6 包装类和BigInteger&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;就是封装一下原始的&lt;code&gt;int&lt;/code&gt;，&lt;code&gt;double&lt;/code&gt;，&lt;code&gt;boolean&lt;/code&gt;数据类型，使其变为类&lt;/p&gt;
&lt;p&gt;在保留了原数据类型的用法上，额外新增类的静态方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Integer valueOf(var); // 将var转为对应的类型
String toBinaryString(int); // 数字转二进制
String toOctalString(int);
String toHexString(int);
int parseInt(String); // 字符串转数字
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，包装类和&lt;code&gt;String&lt;/code&gt;一样是不变类，这意味着：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Integer i = 1000000;
i += 1; // 会新生成一个Integer对象, i将指向这个新对象
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BigInteger&lt;/p&gt;
&lt;p&gt;每一个BigInteger都是一个不可变对象，换句话说每一次计算都会生成新的BigInteger对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造：&lt;/p&gt;
&lt;p&gt;字符串：&lt;code&gt;new BigInteger(&quot;12345678&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;数字（最高支持long）：&lt;code&gt;BigInteger.valueOf(1234)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;BigInteger add(BigInteger val);
BigInteger substract(BigInteger val);
BigInteger multiply(BigInteger val);
BigInteger divide(BigInteger val);
BigInteger[] divideAndRemainder(BigInteger val); // 取余, 返回商和余数
BigInteger equals(BigInteger val);	//	比较两个大整数是否相同
BigInteger pow(int val);		// 次幂
BigInteger max(BigInteger val);	// 返回两个整数中大的一个
int intValue(BigInteger val);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.7 时间&lt;/h3&gt;
&lt;p&gt;一些基本知识：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;时区，以本初子午线为基准线（时间为00:00:00），划分的24个时区，中国在东八区（08:00:00），比基准线快8个小时&lt;/li&gt;
&lt;li&gt;1秒钟的判定，用铯原子的跳动方式来计时&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;传统日期类有：Date（表示日期和时间，可以精确到毫秒）、Calendar（表示日历），但线程不安全&lt;/p&gt;
&lt;p&gt;java之后使用新日期时间类：&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;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5.7.1 日期时间&lt;/h4&gt;
&lt;p&gt;LocalDate：年月日&lt;/p&gt;
&lt;p&gt;LocalTime：时分秒&lt;/p&gt;
&lt;p&gt;LocalDateTime：年月日 时分秒&lt;/p&gt;
&lt;p&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// now
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();

// of
LocalDate date = LocalDate.of(2026, 2, 3);
LocalTime time = LocalTime.of(14, 30, 0);
LocalDateTime dateTime = LocalDateTime.of(2026, 2, 3, 14, 30);

// 加减日期、 时间

// 比较前后

// 获取具体的字段

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ZoneId：指定时区（字符串）&lt;/p&gt;
&lt;p&gt;ZonedDateTime：带时区的时间&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ZoneId zone = ZoneId.of(&quot;Asia/Shanghai&quot;);

ZonedDateTime zdt = ZonedDateTime.now(zone);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.7.2 时间戳&lt;/h4&gt;
&lt;p&gt;Instant：时间线上的一个瞬时点，精确到纳秒&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// now
Instant now = Instant.now();
long millis = now.toEpochMilli();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Duration：时分秒 时间间隔&lt;/p&gt;
&lt;p&gt;Period：年月日 日期间隔&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// duration
Duration d = Duration.between(start, end);
long seconds = d.getSeconds();

// period
Period p = Period.between(startDate, endDate);
int days = p.getDays();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.7.3 格式化&lt;/h4&gt;
&lt;p&gt;DateTimeFormatter：格式化类&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;DateTimeFormatter formatter =
        DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;格式化时间到字符串&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String str = LocalDateTime.now().format(formatter);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;格式化字符串到时间&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;LocalDateTime t = LocalDateTime.parse(
        &quot;2026-02-03 14:30:00&quot;,
        formatter
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.8 Arrays&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String toString([] arr); // 数组拼接为字符串
int binarySearch([] arr, elem); // 数组中找elem
int[] copyOf([] src, destLen); // 拷贝数组
int[] copyOfRange([] src, srcStart, srcEnd);
void fill([] arr, elem); // 填充
void sort([] arr);
void sort([] arr, 排序规则);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.9 Collection&lt;/h3&gt;
&lt;p&gt;通用方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int size();	
boolean isEmpty();
boolean contains(Object o);
boolean add(E e);
boolean remove(Object o);
Object[] toArray();
void clear();
Iterator&amp;#x3C;E&gt; iterator(); // 获取迭代器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三种遍历方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;迭代器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Iterator&amp;#x3C;&gt; it = collection.iterator();
while (it.hasNext()) {
    it.next();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;增强for&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for (type it : collection) {
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;forEach&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;collection.forEach(new Consumer&amp;#x3C;&gt;() {
    @Override
    public void accept(type s) {
		// 
    }
});
// lambda
collection.forEach((type s) -&gt; {
    // 
})
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5.9.1 ArrayList&lt;/h4&gt;
&lt;p&gt;用法类似c++中的&lt;code&gt;vector&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;boolean add(E e); // push_back(e);
E get(int index); // val = v[index];
E set(int index, E element); // v[index] = val;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.9.2 LinkedList&lt;/h4&gt;
&lt;p&gt;链表结构，api稍有不同&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.9.3 HashSet&lt;/h4&gt;
&lt;p&gt;套用Collection api&lt;/p&gt;
&lt;p&gt;自定义类要重写&lt;code&gt;hashCode&lt;/code&gt;方法，否则默认使用地址值&lt;/p&gt;
&lt;h4&gt;5.9.4 TreeSet&lt;/h4&gt;
&lt;p&gt;套用Collection api&lt;/p&gt;
&lt;p&gt;默认升序&lt;/p&gt;
&lt;p&gt;自定义类需要实现&lt;code&gt;Comparable&amp;#x3C;E&gt;&lt;/code&gt;接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class A implements Comparable&amp;#x3C;A&gt; {
    @Override
    public int compareTo(A o) {
        // return 0, -1, 1
        // 0: 相等
        // -1: this 比 o 小
        // 1: this 比 o 大
        // 返回-1, 顺序是this, o
        // 返回1, 顺序是o, this
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者指定TreeSet的排序规则（&lt;code&gt;Comparator&amp;#x3C;E&gt;&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;TreeSet&amp;#x3C;A&gt; = new TreeSet&amp;#x3C;A&gt;(new Comparator&amp;#x3C;A&gt;() {
    @Override
    public int compare(A o1, A o2) {
        // 0: 相等
        // -1: o1 &amp;#x3C; o2
        // 1 : o1 &gt; o2
        // return 0, -1, 1
        // 返回-1, 顺序是o1, o2
        // 返回1, 顺序是o2, o1
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一些其他的方法，第一个/最后一个元素，树上二分查找等&lt;/p&gt;
&lt;h3&gt;5.10 Map&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;通用方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;V put(K key, V val);	// 添加键值对
V get(Object key); 		// 取键对应值
V remove(Object key);	// 删除键
void clear();			// 清空
boolean containsKey(Object key); // 是否有键
boolean containsValue(Object val);	// 是否有值
boolean isEmpty();			// 空
int size();				// 键个数
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遍历&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;keySet，返回一个键集合，遍历键，获取值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Set&amp;#x3C;Object&gt; keySet = map.keySet();
for (Object obj : keySet {
	Object val = map.get(obj);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;entrySet，返回一个键值对集合，直接遍历键值对&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Set&amp;#x3C;Map.Entry&amp;#x3C;Object, Object&gt;&gt; entrySet = map.entrySet();
for (Map.Entry&amp;#x3C;Object, Object&gt; entry : entrySet) {
    entry.getKey();
    entry.getValue();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;forEach，类似Collection，接收键值两个参数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;map.forEach((Object key, Object value) -&gt; {
	//
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5.10.1 TreeMap&lt;/h4&gt;
&lt;p&gt;按键排序，默认升序&lt;/p&gt;
&lt;p&gt;自定义排序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Key类实现&lt;code&gt;Comparable&amp;#x3C;Key&gt;&lt;/code&gt;接口&lt;/li&gt;
&lt;li&gt;指定TreeMap的排序规则（&lt;code&gt;Comparator&amp;#x3C;Key&gt;&lt;/code&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一个，最后一个键值对，树上二分查找等&lt;/p&gt;
&lt;h4&gt;5.10.2 HashMap&lt;/h4&gt;
&lt;p&gt;依赖&lt;code&gt;hashCode()&lt;/code&gt;和&lt;code&gt;equals()&lt;/code&gt;保证键唯一&lt;/p&gt;
&lt;h3&gt;5.11 可变参数&lt;/h3&gt;
&lt;p&gt;本质是数组&lt;/p&gt;
&lt;p&gt;使用方法：&lt;code&gt;func(Object... args)&lt;/code&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;5.12 Stream链式编程&lt;/h3&gt;
&lt;p&gt;目的是简化Collection和数组的操作&lt;/p&gt;
&lt;h4&gt;5.12.1 创建流&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Collection
default Stream&amp;#x3C;E&gt; stream();
// Arrays
public static&amp;#x3C;T&gt; Stream&amp;#x3C;T&gt; stream(T[] array);
// Stream
public static&amp;#x3C;T&gt; Stream&amp;#x3C;T&gt; of (T... values);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.12.2 中间方法&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream&amp;#x3C;T&gt; filter(Predicate&amp;#x3C;? super T&gt; predicate); // 按条件过滤
Stream&amp;#x3C;T&gt; limit(long maxSize);	// 获取前maxSize个元素
Stream&amp;#x3C;T&gt; skip(long n);	// 跳过前n个元素
Stream&amp;#x3C;T&gt; distinct();	// 元素去重，需要hashCode和equals
Stream&amp;#x3C;T&gt; concat(Stream a, Stream b); // 合并a,b流为一个流
Stream&amp;#x3C;T&gt; map(Function&amp;#x3C;T, R&gt; mapper); //
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;filter：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;list.stream().fiter((Object obj) -&gt; {
    // 过滤掉返回值为false的
    // return true; 
    // return false;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;map：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;list.stream().map((Object obj) -&gt; {
    // 对object做操作, 返回新的数据类型
    // 比如将一个String转为Integer, 之后的流上都是Integer类型的数据
    return newObj;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5.12.3 终结方法&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void forEach(Consumer action); // 遍历
long count();	// 统计数据个数
toArray();		// 转为数组
collect(Collector collector); // 转为集合
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;toArray()&lt;/p&gt;
&lt;p&gt;将流中的数据封装到&lt;code&gt;Object[]&lt;/code&gt;中返回&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;toArray(IntFunction&amp;#x3C;A[]&gt; generator)&lt;/p&gt;
&lt;p&gt;返回对应类型的数组&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;list.stream().toArray((int value) -&gt; {
    return new type[value];
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;collect(Collector collector)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;封装到List中返回&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;&gt; list = list.stream().collect(Collectors.toList());
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;封装到Set中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Set&amp;#x3C;&gt; set = list.stream().collect(Collectors.toSet());
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;封装到Map中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map&amp;#x3C;&gt; map = list.stream().collect(Collectors.toMap(
    (Object obj) -&gt; {
        // 返回键, 不能重复
    },
    (Object obj) -&gt; {
        // 返回值
    }
));
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;六. 异常&lt;/h2&gt;
&lt;p&gt;c++异常一坨，java更成体系，没有类比的必要了，东西也不多，直接记住就好了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liang-bk.github.io/_astro/exception.DlzIqupG_2hQgMM.webp&quot; alt=&quot;image-20251204215639127&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编译时异常，编译阶段给提示，必须先手动处理（见6.1-6.2）&lt;/li&gt;
&lt;li&gt;运行时异常，运行程序可能出现异常&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.1 捕获&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;catch&lt;/code&gt;支持多级，支持同时捕获多个异常（用&lt;code&gt;|&lt;/code&gt;分割）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;e.printStackTrace()&lt;/code&gt;：打印异常堆栈信息&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;try {
    
} catch (Exception e) {
    e.printStackTrace();
} finally {
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.2 抛出&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;throws 异常类1, 异常类2&lt;/code&gt;：方法定义后，表示使用方法可能会抛出这些异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;throw 异常&lt;/code&gt;：方法内，手动抛出异常，停止方法运行并将异常返回给调用者&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void method() throws Exception {
    throw Exception();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.3 自定义&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;编译时异常，继承&lt;code&gt;Exception&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;运行时异常，继承&lt;code&gt;RuntimeException&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class MyException extends RuntimeException {
    // 空参
    public MyException() {
        
    }
    // 带参
    public MyException(String messgae) {
        super(message)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.4 异常资源释放&lt;/h3&gt;
&lt;p&gt;类似c++中的RAII思想，自动释放资源（即使出现异常的情况下）&lt;/p&gt;
&lt;p&gt;具体做法是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 基本语法
try (ResourceType resource = new ResourceType()) {
    // 使用资源的代码
} catch (Exception e) {
    // 异常处理
}

// 多个资源
try (ResourceType1 resource1 = new ResourceType1();
     ResourceType2 resource2 = new ResourceType2()) {
    // 使用资源的代码
} catch (Exception e) {
    // 异常处理
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;ResourceType&lt;/code&gt;必须实现&lt;code&gt;AutoCloseable&lt;/code&gt;接口&lt;/p&gt;
&lt;h2&gt;七. IO流&lt;/h2&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;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.1 字节流&lt;/h3&gt;
&lt;p&gt;以Stream结尾的各类对象都是字节流：&lt;/p&gt;
&lt;h3&gt;7.2 字符流&lt;/h3&gt;
&lt;p&gt;以Reader和Writer结尾的各类对象都是字符流&lt;/p&gt;
&lt;h3&gt;7.3 文件&lt;/h3&gt;
&lt;h3&gt;7.4 序列化&lt;/h3&gt;
&lt;h2&gt;八. 多线程&lt;/h2&gt;
&lt;h3&gt;8.1 创建方式：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;继承&lt;code&gt;Thread&lt;/code&gt;类，重写&lt;code&gt;run&lt;/code&gt;方法 （简单任务）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现&lt;code&gt;Runnable&lt;/code&gt;接口，重写&lt;code&gt;run&lt;/code&gt;方法 （要执行的方法与线程分离）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println(&quot;任务执行中...&quot;);
    }
}

// 使用
Thread thread = new Thread(new MyTask());
thread.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现&lt;code&gt;Callable&amp;#x3C;T&gt;&lt;/code&gt;接口，重写&lt;code&gt;call&lt;/code&gt;方法 （支持返回值，能抛异常，配合Future使用）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class MyCallable implements Callable&amp;#x3C;String&gt; {
    @Override
    public String call() throws Exception {
        return &quot;任务执行结果&quot;;
    }
}

// 使用
FutureTask&amp;#x3C;String&gt; task = new FutureTask&amp;#x3C;&gt;(new MyCallable());
Thread thread = new Thread(task);
thread.start();
String result = task.get(); // 获取结果
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8.2 锁&lt;/h3&gt;
&lt;p&gt;在java里面，任何一个对象都可以被当做mutex来使用，&lt;/p&gt;
&lt;h2&gt;九. 反射&lt;/h2&gt;
&lt;p&gt;加载类，并解剖类中的信息&lt;/p&gt;
&lt;h3&gt;9.1 获取Class对象&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Class.forName(&quot;package.className&quot;)&lt;/code&gt;：填全类名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;className.class&lt;/code&gt;：做参数传递&lt;/li&gt;
&lt;li&gt;&lt;code&gt;obj.getClass()&lt;/code&gt;：能创建对象再考虑使用&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;9.2 常用方法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;获取构造器&lt;code&gt;Constructor&lt;/code&gt;对象，并利用其构造类对象&lt;/li&gt;
&lt;li&gt;获取成员变量&lt;code&gt;Field&lt;/code&gt;对象，获取/修改类成员变量&lt;/li&gt;
&lt;li&gt;获取方法&lt;code&gt;Method&lt;/code&gt;对象，执行类方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;获取&lt;code&gt;Constructor&lt;/code&gt;，&lt;code&gt;Field&lt;/code&gt;，&lt;code&gt;Method&lt;/code&gt;类型的对象，大部分签名都很类似，挑重点的记：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getName&lt;/code&gt;：获取类名&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getDeclaredConstructors()&lt;/code&gt;：返回可用的&lt;code&gt;Constructor&lt;/code&gt;数组&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getDeclaredConstructor(xx1.class, xx2.class)&lt;/code&gt;：返回对应有参的&lt;code&gt;Constructor&lt;/code&gt;对象&lt;/p&gt;
&lt;p&gt;其他，要获取对应类的成员变量和方法，其get方法也类似上面&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;Constructor&lt;/code&gt;对象构造原对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class c1 = ArrayList.class;
// 获取无参构造器对象（可能是 private，因此使用 getDeclaredConstructor）
Constructor con = c1.getDeclaredConstructor();
// 临时设置访问权限，使得可以访问 private 构造函数
con.setAccessible(true);
// 构造对象, 需要强转，因为返回的是 Object
ArrayList s1 = (ArrayList) con.newInstance();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;Field&lt;/code&gt;对象设置/获取类成员变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class c1 = ArrayList.class;
// 获取 ArrayList 类中的对应成员变量，例如 &quot;size&quot;
Field fieldSize = c1.getDeclaredField(&quot;size&quot;);
// 临时关闭 Java 权限检查
fieldSize.setAccessible(true);
// arr 是一个 ArrayList 的对象
ArrayList arr = new ArrayList();
// 设置字段值为指定内容
fieldSize.set(arr, 10);
// 获取字段值
int var = (int) fieldSize.get(arr);
System.out.println(&quot;size = &quot; + var);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;Method&lt;/code&gt;对象调用类方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class c1 = String.class;
// 获取 private 方法，如 String 中的 &quot;indexOfSupplementary&quot;
Method method = c1.getDeclaredMethod(&quot;indexOfSupplementary&quot;, int.class, int.class);
// 开放权限
method.setAccessible(true);
// 调用方法，需要传入对象和参数
String s = &quot;hello&quot;;
int result = (int) method.invoke(s, 0x10400, 0);
System.out.println(result);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&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;/li&gt;
&lt;li&gt;
&lt;p&gt;绕过泛型，比如在&lt;code&gt;ArrayList&amp;#x3C;Integer&gt;&lt;/code&gt;添加&lt;code&gt;&quot;123&quot;&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;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;十. 注解&lt;/h2&gt;
&lt;h3&gt;10.1 定义&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public @interface MyAnnotation {
    public String name1() default &quot;123&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本质是一个接口，自定义注解是继承了&lt;code&gt;Annotation&lt;/code&gt;接口的接口&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;/p&gt;
&lt;p&gt;默认情况下可以在类名，方法名，变量名上直接加&lt;code&gt;@MyAnnotation&lt;/code&gt;使用，这代表接口的一个实现类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要指定里面的属性值就用&lt;code&gt;@MyAnnotation(name1=&quot;&quot;, xxx)&lt;/code&gt;，如果注解里面非默认属性只有一个&lt;code&gt;value&lt;/code&gt;，可以直接把&lt;code&gt;value=&lt;/code&gt;省略&lt;/p&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;p&gt;形式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Rentention
@Target
public @interface MyAnnotation {
    public String name1() default &quot;123&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Rentention&lt;/code&gt;：声明注解的保留周期
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@Rentention(RententionPolicy.RUNTIME)&lt;/code&gt;&lt;/strong&gt;：一直保留，&lt;strong&gt;常用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RententionPolicy.SOURCE&lt;/code&gt;：源码保留&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RententionPolicy.CLASS&lt;/code&gt;：字节码保留&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Target&lt;/code&gt;：限制注解的范围
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Target(ElementType.Type)&lt;/code&gt;：该注解可以在类和接口上使用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ElementType.FIELD&lt;/code&gt;：成员变量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ElementType.METHOD&lt;/code&gt;：成员方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ElementType.PARAMETER&lt;/code&gt;：方法参数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;10.2 解析&lt;/h3&gt;
&lt;p&gt;利用反射&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;获取使用注解的类对象&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Class c1 = Demo.class&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;判断对应注解是否存在&lt;/p&gt;
&lt;p&gt;&lt;code&gt;c1.isAnnotationPresent(MyAnnotation.class)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取注解对象并获取其属性值&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MyAnnotation myan = (MyAnnotation) getDeclaredAnnotation(MyAnnotation.class)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;Class&lt;/code&gt;，&lt;code&gt;Method&lt;/code&gt;，&lt;code&gt;Field&lt;/code&gt;，&lt;code&gt;Constructor&lt;/code&gt;等都实现了&lt;code&gt;AnnotatedElement&lt;/code&gt;接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Annotation[] getDeclaredAnnotations();	// 获取当前对象上的注解
public T getDeclaredAnnotation(Class&amp;#x3C;T&gt; annotationClass);	// 获取指定的注解对象
public boolean isAnnotationPresent(Class&amp;#x3C;Annotation&gt; annotationClass); // 判断当前对象是否存在某个注解
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用场景：&lt;/p&gt;
&lt;p&gt;比如JUnit框架定义的&lt;code&gt;@Test&lt;/code&gt;，加上就测试，本质就是底层利用反射，看&lt;code&gt;Method&lt;/code&gt;上是否有&lt;code&gt;@Test&lt;/code&gt;注解，决定是否&lt;code&gt;invoke&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;十一. 动态代理&lt;/h2&gt;
&lt;p&gt;像是一个包装类，使用非侵入式的形式来对被代理的类增添功能代码（底层使用反射）&lt;/p&gt;
&lt;p&gt;核心是&lt;code&gt;Proxy.newPorxyInstance&lt;/code&gt;，简易代码如下，&lt;code&gt;invoke&lt;/code&gt;中的&lt;code&gt;proxy&lt;/code&gt;指的是&lt;code&gt;T proxy&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class ProxyUtil {
    public static &amp;#x3C;T&gt; T createProxy(T obj) {
        T proxy = (T) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
                obj.getClass().getInterfaces(), new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // do something before method...
                        Object result = method.invoke(obj, args);
                        // do something after method...
                        return result; // return method result
                    }
                });
        return proxy;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于&lt;code&gt;Proxy.newPorxyInstance&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Object newProxyInstance(ClassLoader loader,
                                      Class&amp;#x3C;?&gt;[] interfaces,
                                      InvocationHandler h)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ClassLoader loader&lt;/code&gt;：指定加载代理类的类加载器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Class&amp;#x3C;?&gt;[] interfaces&lt;/code&gt;：指定代理要实现的接口，一般被代理的类实现哪些，就填哪些&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InvocationHandler h&lt;/code&gt;：匿名内部类，在内部指定生成的代理类对象要增加的功能&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Hello World</title><link>https://liang-bk.github.io/blog/hello-world</link><guid isPermaLink="true">https://liang-bk.github.io/blog/hello-world</guid><description>第一篇文章</description><pubDate>Thu, 25 Sep 2025 17:20:00 GMT</pubDate><content:encoded>&lt;h2&gt;测试&lt;/h2&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Using MDX</title><link>https://liang-bk.github.io/blog/using-mdx</link><guid isPermaLink="true">https://liang-bk.github.io/blog/using-mdx</guid><description>Learning how to use MDX in Astro</description><pubDate>Sun, 01 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This theme comes with the &lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/mdx/&quot;&gt;@astrojs/mdx&lt;/a&gt; integration installed and configured in your &lt;code&gt;astro.config.mjs&lt;/code&gt; config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file.&lt;/p&gt;
&lt;h2&gt;Why MDX?&lt;/h2&gt;
&lt;p&gt;MDX is a special flavor of Markdown that supports embedded JavaScript &amp;#x26; JSX syntax. This unlocks the ability to &lt;a href=&quot;https://docs.astro.build/en/guides/markdown-content/#mdx-features&quot;&gt;mix JavaScript and UI Components into your Markdown content&lt;/a&gt; for things like interactive charts or alerts.&lt;/p&gt;
&lt;p&gt;If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze.&lt;/p&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;p&gt;Here is how you import and use a UI component inside of MDX.&lt;br&gt;
When you open this page in the browser, you should see the clickable button below.&lt;/p&gt;
&lt;p&gt;import { Button } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;Click Me&lt;/p&gt;
&lt;h2&gt;More Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mdxjs.com/docs/what-is-mdx&quot;&gt;MDX Syntax Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/markdown-content/#markdown-and-mdx-pages&quot;&gt;Astro Usage Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;a href=&quot;https://docs.astro.build/en/reference/directives-reference/#client-directives&quot;&gt;Client Directives&lt;/a&gt; are still required to create interactive components. Otherwise, all components in your MDX will render as static HTML (no JavaScript) by default.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Markdown 语法支持</title><link>https://liang-bk.github.io/blog/markdown-zh</link><guid isPermaLink="true">https://liang-bk.github.io/blog/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/thumbnail.HAXFr_hw.jpg"/><enclosure url="/_astro/thumbnail.HAXFr_hw.jpg"/></item></channel></rss>