<?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>liangbm3&apos;s blog</title><description>总有人间一两风，填我十万八千梦。 feedId:188626016062481462+userId:200940686247224320</description><link>https://liangbm3.site</link><item><title>Debug 记录：一个缺失的“\0”和一个意外的“%”引发的挂死</title><link>https://liangbm3.site/blog/debug-ji-lu-yi-ge-que-shi-de-0-he-yi-ge-yi-wai-de-d-yin-fa-de-gua-si</link><guid isPermaLink="true">https://liangbm3.site/blog/debug-ji-lu-yi-ge-que-shi-de-0-he-yi-ge-yi-wai-de-d-yin-fa-de-gua-si</guid><description>记录在实习过程中遇到的一个由数据源头污染、内存越界读取以及格式化字符串漏洞三者巧合叠加引发的教科书级 Crash</description><pubDate>Thu, 27 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在最近的实习过程中，遇到了一个上线模块的挂死问题，经排查发现这是一次由数据源头污染、内存越界读取以及格式化字符串漏洞三者巧合叠加引发的教科书级 Crash。这里记录一下这个问题的发现与解决过程。&lt;/p&gt;
&lt;h2&gt;1. 案发现场&lt;/h2&gt;
&lt;p&gt;模块挂死，我拿到的是一个 core dump 文件，使用 gdb 查看崩溃堆栈，主要内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Current thread is 1 (Thread 0xffff8effd090 (LWP 9372))]
(gdb) bt
#0  raise (sig=6) ...
#1  &amp;#x3C;signal handler called&gt;
#2  __GI_raise (sig=sig@entry=6) ...
#3  0x0000ffffa117c974 in __GI_abort () at abort.c:79
#4  0x0000ffffa11b572c in __libc_message ...
#5  0x0000ffffa11b5778 in __GI___libc_fatal (message=0xffffa12705d0 &quot;*** %n in writable segment detected ***\n&quot;) at ../sysdeps/posix/libc_fatal.c:191
#6  0x0000ffffa1191288 in _IO_vfprintf_internal (format=... &quot;[tte_app] event_id: ...LK6ADAE45RB756122% not found...\n&quot;, ...) at vfprintf.c:1642
...
#14 0x0000aaaadfe70f48 in tte::OnboardServiceApp::flQueryRecordVoiceCallback ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重点是 &lt;code&gt;frame #5&lt;/code&gt; 的错误信息：&lt;code&gt;%n in writable segment detected&lt;/code&gt;，查阅资料发现，这是 Linux glibc 的一种安全保护机制。它意味着 &lt;code&gt;printf&lt;/code&gt; 系列函数在解析格式化字符串时，发现在可写的内存段（堆或栈）中出现了 &lt;code&gt;%n&lt;/code&gt;。由于 &lt;code&gt;%n&lt;/code&gt; 会将&lt;strong&gt;已打印字符数&lt;/strong&gt;回写到内存，极易被黑客用于覆盖栈数据，所以 &lt;code&gt;glibc&lt;/code&gt; 检测到这种情况会直接让进程自杀。&lt;/p&gt;
&lt;h2&gt;2. 代码回溯&lt;/h2&gt;
&lt;p&gt;根据堆栈，崩溃发生在 &lt;code&gt;flQueryRecordVoiceCallback&lt;/code&gt; 函数中。我找到了对应的业务代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 崩溃所在的业务逻辑
int32_t OnboardServiceApp::flQueryRecordVoiceCallback(
    dcos::ConstMsg&amp;#x3C;sys_msgs::tte_service::VoiceStatusReq&gt; const&amp;#x26; req) {

  // ... 前面是初始化代码 ...

  // 直接强制转换原始字节流为 string
  std::string target_event_id =
      std::string(reinterpret_cast&amp;#x3C;const char*&gt;(req-&gt;event_id));

  LOG_INFO(tte_header, &quot;query event_id: %s\n&quot;, target_event_id.c_str());

  // 在锁内查找 event_id ...
  {
    std::lock_guard&amp;#x3C;std::mutex&gt; l(resource_mutex_);
    for (auto it = process_tree_list_.begin(); it != process_tree_list_.end(); ++it) {
       // ... 省略查找逻辑 ...
       if (event_id == target_event_id) {
         // ... 找到后返回成功 ...
         return 0;
       }
    }
  }

  // [崩溃点] 未找到 ID，打印警告日志
  LOG_WARN(tte_header, &quot;event_id: %s not found, returning error status\n&quot;,
           target_event_id.c_str());

  // ... 返回错误 ...
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结合堆栈信息，崩溃点是在这个函数的 &lt;code&gt;LOG_WARN&lt;/code&gt; 语句处，字符串里混入了一个诡异的 &lt;code&gt;%&lt;/code&gt;，而且它后面紧接着 &lt;code&gt;not found&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;3. 根因分析&lt;/h2&gt;
&lt;p&gt;经过一步步分析，发现这里发生了三个致命巧合，导致了模块的崩溃。&lt;/p&gt;
&lt;h3&gt;3.1 第一环：缺失的终止符 &lt;code&gt;\0&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;首先 &lt;code&gt;target_event_id&lt;/code&gt; 里面出现 &lt;code&gt;%&lt;/code&gt; 是意料之外的，正常来说应该只有数字和字母，&lt;code&gt;VoiceStatusReq&lt;/code&gt; 结构体定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;constexpr int32_t kEventIdSize{128};

struct VoiceStatusReq {
  uint8_t event_id[kEventIdSize];
} __attribute__((packed));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;req-&gt;event_id&lt;/code&gt; 是一个固定大小的数组，发送方在填充这个数组时，没有在实际的 &lt;code&gt;event_id&lt;/code&gt; 字符串末尾添加字符串终止符 &lt;code&gt;\0&lt;/code&gt;。因此当我们直接将其转换为 &lt;code&gt;std::string&lt;/code&gt; 时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::string target_event_id = std::string(reinterpret_cast&amp;#x3C;const char*&gt;(req-&gt;event_id));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在接收端的代码中，&lt;code&gt;std::string&lt;/code&gt; 的构造函数会从首地址一直往后读，直到读到内存里的某一个 &lt;code&gt;0x00&lt;/code&gt; 为止。 这一次刚好读到了栈上的垃圾数据。而在这些垃圾数据中，恰好有一个字节是 &lt;code&gt;0x25&lt;/code&gt;（即 ASCII 码 &lt;code&gt;%&lt;/code&gt;）。是的，发送方一直都没有做这个处理，其实接收方解析出来的 &lt;code&gt;event_id&lt;/code&gt; 很多都是乱码，但是因为没有影响到业务，所以一直没被发现。&lt;/p&gt;
&lt;h3&gt;3.2 第二环：日志函数的“二次格式化”漏洞&lt;/h3&gt;
&lt;p&gt;拿到带有乱码的 &lt;code&gt;target_event_id&lt;/code&gt; 后，程序执行到了最后的 &lt;code&gt;LOG_WARN&lt;/code&gt;。日志库的封装大概是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void Log(dlog::LogLevel level, const std::string&amp;#x26; prefix, const std::string&amp;#x26; fmt, ...) {
  // 1. 第一步格式化：将用户参数拼接到 buffer
  char buffer[8192];
  va_list args;
  va_start(args, fmt);
  vsnprintf(buffer, sizeof(buffer), fmt.c_str(), args); // 这里生成了包含 &quot;%&quot; 的 buffer
  va_end(args);

  std::string formatted_msg = &quot;[&quot; + prefix + &quot;] &quot; + buffer;

  // 2. 第二步格式化：致命错误发生在这里
  // logger-&gt;Print 的底层是 printf 风格的变参函数
  // 这里直接把包含 &quot;%&quot; 的动态内容当成了 format string
  logger-&gt;Print(level, formatted_msg.c_str()); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;vsnprintf&lt;/code&gt; 完成拼接后，&lt;code&gt;buffer&lt;/code&gt; 的内容变成了： &lt;code&gt;...LK6ADAE45RB756122% not found...&lt;/code&gt;。接着，&lt;code&gt;logger-&gt;Print&lt;/code&gt; 被调用，它底层依赖 &lt;code&gt;printf&lt;/code&gt; 逻辑。它拿这个 &lt;code&gt;buffer&lt;/code&gt; 当作格式化字符串去解析。&lt;/p&gt;
&lt;h3&gt;3.3 第三环：致命的 &lt;code&gt;%n&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;当 &lt;code&gt;printf&lt;/code&gt; 解析到 &lt;code&gt;...% not found&lt;/code&gt; 时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它看到了 &lt;code&gt;%&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;它忽略了后面的空格（在某些实现或复杂上下文中），或者因为字符串处于可写段引起了警觉。&lt;/li&gt;
&lt;li&gt;它看到了 &lt;code&gt;n&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;% + n = %n&lt;/code&gt;。 这是一个合法的格式控制符，意为“将已输出字符数量写入指针”。但由于这个字符串是在栈上动态生成的，&lt;code&gt;glibc&lt;/code&gt; 判定这是一次格式化字符串攻击尝试，直接 &lt;code&gt;Abort&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 意外发现&lt;/h2&gt;
&lt;p&gt;在审查日志库底层代码时，我还发现了一个可能导致架构级崩溃的隐患。原有的日志接口定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void Log(dlog::LogLevel level, const std::string&amp;#x26; prefix, const std::string&amp;#x26; fmt, ...) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++ 标准明确规定：传递给 &lt;code&gt;va_start&lt;/code&gt; 的最后一个固定参数如果是引用类型，则是未定义行为。 虽然在 x86 上可能侥幸能跑，但在 ARM64 等架构下，这会导致 &lt;code&gt;va_list&lt;/code&gt; 指针计算错误，无法获取后续参数，甚至导致野指针访问。&lt;/p&gt;
&lt;h2&gt;5. 问题修复&lt;/h2&gt;
&lt;p&gt;首先是修复日志库的格式化漏洞，永远不能信任动态拼接的字符串，必须显式指定格式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// logger-&gt;Print(level, buffer); 错误
logger-&gt;Print(level, &quot;%s&quot;, buffer);// 正确
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时将日志接口的 &lt;code&gt;fmt&lt;/code&gt; 参数类型从 &lt;code&gt;const std::string&amp;#x26;&lt;/code&gt; 改为 &lt;code&gt;const char*&lt;/code&gt;，确保 &lt;code&gt;va_start&lt;/code&gt; 行为符合标准。&lt;/p&gt;
&lt;p&gt;在业务层，不能假设网络发来的数据一定以 &lt;code&gt;\0&lt;/code&gt; 结尾，需要限制一下读取的最大长度：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;size_t len = strnlen(reinterpret_cast&amp;#x3C;const char*&gt;(req-&gt;event_id), kEventIdSize);
std::string target_event_id(reinterpret_cast&amp;#x3C;const char*&gt;(req-&gt;event_id), len);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 总结&lt;/h2&gt;
&lt;p&gt;这个 &lt;code&gt;LOG_WARN&lt;/code&gt; 的崩溃，实际上是一个完美的连锁反应：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发送端少发一个 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;接收端越界读取到了 &lt;code&gt;%&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;日志库错误地对内容进行了二次解析。&lt;/li&gt;
&lt;li&gt;乱码 &lt;code&gt;%&lt;/code&gt; 恰好碰上了固定文案里的 &lt;code&gt;n&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这四个环节缺一不可，才导致了这个崩溃。因此在日常的开发中，要建立防御性编程的意识，不要信任网络包，不要信任内存里的数据，更不要把用户输入直接传给 &lt;code&gt;printf&lt;/code&gt;。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Effective C++: 避免遮掩继承而来的名称</title><link>https://liangbm3.site/blog/effective-c-bi-mian-zhe-yan-ji-cheng-er-lai-de-ming-cheng</link><guid isPermaLink="true">https://liangbm3.site/blog/effective-c-bi-mian-zhe-yan-ji-cheng-er-lai-de-ming-cheng</guid><description>Avoid hiding inherited names</description><pubDate>Sat, 25 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;这是 Effective C++ 的第 33 个条款，Avoid hiding inherited names，中文翻译为：&lt;strong&gt;避免遮掩继承而来的名称。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 C++ 中，&lt;strong&gt;名称遮蔽&lt;/strong&gt;指的是在内层作用域中声明的名称，会遮掩外层作用域中的同名名称。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int x = 10; // Global 作用域

void someFunc() {
    double x = 5.0; // local 作用域
    std::cin &gt;&gt; x;    // 读取的是 local 变量 double x
                      // global 作用域的 int x 在这里被遮掩了
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继承关系也可以看作一种作用域嵌套，&lt;code&gt;Derived&lt;/code&gt; (派生类) 的作用域嵌套在 &lt;code&gt;Base&lt;/code&gt; (基类) 的作用域中。继承中的名称遮蔽规则为当派生类定义了一个与其基类同名的成员（无论是成员变量还是成员函数）时，这个派生类成员会遮掩掉基类中所有同名的成员。注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;遮掩所有同名成员&lt;/strong&gt;： 只要名称相同，基类中所有同名成员（即使它们是不同参数列表的重载函数）都会被遮掩。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与参数和 &lt;code&gt;virtual&lt;/code&gt; 无关&lt;/strong&gt;：遮掩规则只看名称，不看函数的参数列表、返回类型或是否为 &lt;code&gt;virtual&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文中举了一个具体的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Base {
public:
    virtual void mf1() = 0;
    virtual void mf1(int x); // mf1 被重载
    
    void mf3();
    void mf3(double x); // mf3 被重载
    ...
};

class Derived : public Base {
public:
    virtual void mf1(); // 重写(override)了 Base::mf1()
    void mf3();         // 定义了 Derived::mf3()
    ...
};

// --- 客户端代码 ---
Derived d;
int x = 10;

d.mf1();   // 没问题, 调用 Derived::mf1()
d.mf1(x);  // 编译错误
d.mf3();   // 没问题, 调用 Derived::mf3()
d.mf3(x);  // 编译错误 (x 会被隐式转换为 double)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中，&lt;code&gt;Derived&lt;/code&gt; 类中的 &lt;code&gt;mf1()&lt;/code&gt; 和 &lt;code&gt;mf3()&lt;/code&gt; 成员函数分别遮掩了 &lt;code&gt;Base&lt;/code&gt; 类中所有同名的重载版本。因此，尝试调用 &lt;code&gt;d.mf1(x);&lt;/code&gt; 和 &lt;code&gt;d.mf3(x);&lt;/code&gt; 会导致编译错误，因为编译器找不到匹配的函数。在 &lt;code&gt;public&lt;/code&gt; 继承下，这几乎总是不是我们想要的行为。&lt;code&gt;public&lt;/code&gt; 继承意味着 &quot;is-a&quot; (是一种) 关系，派生类理应继承基类的所有接口。名称遮掩破坏了这种关系。&lt;/p&gt;
&lt;p&gt;对于这个问题，主要有两种解决方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;using&lt;/code&gt; 声明。使用 &lt;code&gt;using&lt;/code&gt; 声明可以将基类中所有同名的成员拉到派生类的作用域中，这是最常用且推荐的方法。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Derived : public Base {
public:
    // 将 Base 中所有名为 mf1 的成员引入 Derived 的 public 作用域
    using Base::mf1; 
    
    // 将 Base 中所有名为 mf3 的成员引入 Derived 的 public 作用域
    using Base::mf3; 

    virtual void mf1(); // 依然重写 Base::mf1()
    void mf3();         // 重载(overload)了 Base::mf3() 和 Base::mf3(double)
    ...
};

// --- 客户端代码 (现在可以正常工作) ---
Derived d;
int x = 10;

d.mf1();   // 没问题, 调用 Derived::mf1()
d.mf1(x);  // 没问题，调用 Base::mf1(int)

d.mf3();   // 没问题, 调用 Derived::mf3()
d.mf3(x);  // 没问题，调用 Base::mf3(double)
&lt;/code&gt;&lt;/pre&gt;
&lt;code&gt;using Base::mf1;&lt;/code&gt; 告诉编译器，把 &lt;code&gt;Base&lt;/code&gt; 中所有叫 &lt;code&gt;mf1&lt;/code&gt; 的东西都看作 &lt;code&gt;Derived&lt;/code&gt; 作用域的一部分。现在 &lt;code&gt;Derived&lt;/code&gt; 的作用域中同时有了 &lt;code&gt;Base::mf1()&lt;/code&gt;、&lt;code&gt;Base::mf1(int)&lt;/code&gt; 和 &lt;code&gt;Derived::mf1()&lt;/code&gt;。根据C++的重载和重写规则：&lt;code&gt;Derived::mf1()&lt;/code&gt; 重写了 &lt;code&gt;Base::mf1()&lt;/code&gt;。&lt;code&gt;Base::mf1(int)&lt;/code&gt; 依然可见，成为 &lt;code&gt;Derived&lt;/code&gt; 接口的一部分。&lt;code&gt;mf3&lt;/code&gt; 同理。&lt;/li&gt;
&lt;li&gt;在某些特殊情况下，例如不想继承基类的所有重载版本（比如在 &lt;code&gt;private&lt;/code&gt; 继承中，&lt;code&gt;using&lt;/code&gt; 声明会使基类成员在派生类中变为 &lt;code&gt;public&lt;/code&gt;），你可以使用转交函数。转交函数是派生类中定义的一个内联函数，它只是简单地调用它想继承的那个基类版本。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Base {
public:
    virtual void mf1(int x);
    ...
};

// 假设我们只想暴露 mf1(int) 版本
class Derived : public Base {
public:
    // 只重写无参数版本
    virtual void mf1() { ... } 
    
    // 用转交函数“继承” mf1(int)
    void mf1(int x) {
        Base::mf1(x); // 显式调用 Base 的版本
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Effective C++: 确定你的 public 继承塑模出 is-a 关系</title><link>https://liangbm3.site/blog/effective-c-que-ding-ni-de-public-ji-cheng-su-mo-chu-is-a-guan-xi</link><guid isPermaLink="true">https://liangbm3.site/blog/effective-c-que-ding-ni-de-public-ji-cheng-su-mo-chu-is-a-guan-xi</guid><description>Make sure public inheritance models &apos;is-a&apos;</description><pubDate>Sat, 25 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;这是Effective C++的第 32 条款，Make sure public inheritance models &quot;is-a&quot;，中文翻译为：&lt;strong&gt;确定你的 public 继承塑模出 is-a 关系。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是 C++ 面向对象编程中非常重要的一条规则，它强调 C++ 中的 &lt;code&gt;public&lt;/code&gt; 继承，其唯一的含义就是 &quot;is-a&quot; 关系。当我们写下 &lt;code&gt;class Derived : public Base { ... };&lt;/code&gt; 时，即是在向编译器和所有阅读代码的人做出一个承诺：每一个 &lt;code&gt;Derived&lt;/code&gt; 类型的对象同时也是一个 &lt;code&gt;Base&lt;/code&gt; 类型的对象。&lt;/p&gt;
&lt;p&gt;这个承诺意味着，所有适用于 &lt;code&gt;Base&lt;/code&gt; 对象的行为和属性，也必须同样适用于 &lt;code&gt;Derived&lt;/code&gt; 对象。这通常被称为Liskov 替换原则 (Liskov Substitution Principle, LSP)。即如果程序中任何地方需要一个 &lt;code&gt;Base&lt;/code&gt; 对象，都可以用一个 &lt;code&gt;Derived&lt;/code&gt; 对象去替换它，而程序的行为不应该有任何“意外”的改变。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Person { ... };
class Student : public Person { ... }; // 学生 &quot;is-a&quot; 人

void eat(const Person&amp;#x26; p); // 任何人都会吃饭
void study(const Student&amp;#x26; s); // 只有学生才到校学习

Person p;
Student s;

eat(p); // 正确
eat(s); // 正确，因为学生也是人
study(s); // 正确
// study(p); // 错误，并非所有人都是学生
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文中作者还举了两个经典违法 LSP 的例子。&lt;/p&gt;
&lt;p&gt;第一个例子如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Bird {
public:
    virtual void fly();
};

class Penguin : public Bird { ... }; // 糟糕的设计！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从分类学的角度，企鹅是一种鸟，所以 &lt;code&gt;class Penguin : public Bird { ... }&lt;/code&gt; 看起来很正常。但是如果为 &lt;code&gt;Bird&lt;/code&gt; 定义一个 &lt;code&gt;fly()&lt;/code&gt;方法，这个继承关系承诺了企鹅会飞，但我们都知道这是错误的，违反了 &quot;is-a&quot; 原则。对于这种错误的设计，文中提到了两种修复方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;Penguin&lt;/code&gt; 中重新定义 &lt;code&gt;fly()&lt;/code&gt; 并让它报错。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;virtual void fly() {
    throw &quot;企鹅不会飞！&quot;; // 这是一种糟糕的运行时错误
}
&lt;/code&gt;&lt;/pre&gt;
但这是一个坏主意，因为一个设计层面的逻辑错误，推迟到了运行时才暴露出来。好的设计应该尽可能在编译期就防止错误。&lt;/li&gt;
&lt;li&gt;更合理的继承体系，因为鸟这个概念本身不应该和会飞绑定。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Bird {
    // ... 关于鸟的通用属性，比如有羽毛、是卵生等
    // 没有 fly() 函数
};

class FlyingBird : public Bird {
public:
    virtual void fly();
};

class Penguin : public Bird {
    // ... 企鹅特有的属性
};
&lt;/code&gt;&lt;/pre&gt;
这个设计更加精确。&lt;code&gt;Penguin&lt;/code&gt; is-a &lt;code&gt;Bird&lt;/code&gt;，&lt;code&gt;FlyingBird&lt;/code&gt; is-a &lt;code&gt;Bird&lt;/code&gt;。这个模型是正确的，因为它没有做出任何错误的承诺。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二个例子是正方形与矩形的陷阱。从数学直觉上，正方形是一种特殊的矩形。所以 &lt;code&gt;class Square : public Rectangle&lt;/code&gt; 似乎完全没有问题。但如果从行为上去分析，假设有一个 &lt;code&gt;Rectangle&lt;/code&gt; 类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Rectangle {
public:
    virtual void setWidth(int newWidth);
    virtual void setHeight(int newHeight);
    int width() const;
    int height() const;
    ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Rectangle&lt;/code&gt; 的一个核心行为是：它的长和宽可以被独立修改。假设有一个函数依赖于这个行为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void makeBigger(Rectangle&amp;#x26; r) {
    int oldHeight = r.height();
    r.setWidth(r.width() + 10);
    // 我们断言：修改宽度不应该影响高度
    assert(r.height() == oldHeight);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们让 &lt;code&gt;Square&lt;/code&gt; 公开继承 &lt;code&gt;Rectangle&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Square : public Rectangle { ... };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了维持“正方形”的特性（长宽必须相等），&lt;code&gt;Square&lt;/code&gt; 类的 &lt;code&gt;setWidth&lt;/code&gt; 和 &lt;code&gt;setHeight&lt;/code&gt; 必须被特殊实现，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void Square::setWidth(int newWidth) {
    // 为了保持正方形特性，必须同时修改长和宽
    Rectangle::setWidth(newWidth);
    Rectangle::setHeight(newWidth);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们把一个 &lt;code&gt;Square&lt;/code&gt; 对象传给 &lt;code&gt;makeBigger&lt;/code&gt; 函数时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Square s;
s.setWidth(10); // 此时 s 的长和宽都是10
makeBigger(s);  // s &quot;is-a&quot; Rectangle，所以编译通过
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;makeBigger&lt;/code&gt; 内部，&lt;code&gt;oldHeight&lt;/code&gt; 被记录为 10。&lt;code&gt;s.setWidth(20)&lt;/code&gt; 被调用。根据 &lt;code&gt;Square&lt;/code&gt; 的实现，这会把 &lt;code&gt;s&lt;/code&gt; 的宽和高都设置成 20。&lt;code&gt;assert(s.height() == oldHeight)&lt;/code&gt; 会被触发，因为 &lt;code&gt;s.height()&lt;/code&gt; 现在是 20，而 &lt;code&gt;oldHeight&lt;/code&gt; 是 10。因此这种继承关系是错误的。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Effective C++: 成对使用 new 和 delete 时要采取相同形式</title><link>https://liangbm3.site/blog/effective-c-cheng-dui-shi-yong-new-he-delete-shi-yao-cai-qu-xiang-tong-xing-shi</link><guid isPermaLink="true">https://liangbm3.site/blog/effective-c-cheng-dui-shi-yong-new-he-delete-shi-yao-cai-qu-xiang-tong-xing-shi</guid><description>Use the same form in corresponding uses of new and delete</description><pubDate>Thu, 23 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;这是 Effective C++ 的第 16 个条款，Use the same form in corresponding uses of new and delete，中文翻译为：&lt;strong&gt;成对使用 new 和 delete 时要采取相同形式。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个条款的核心是 &lt;code&gt;new&lt;/code&gt; 和 &lt;code&gt;delete&lt;/code&gt; 的形式必须匹配。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你使用 &lt;code&gt;new&lt;/code&gt; 来分配单个对象，就必须使用 &lt;code&gt;delete&lt;/code&gt; 来释放。&lt;/li&gt;
&lt;li&gt;如果你使用 &lt;code&gt;new[]&lt;/code&gt; 来分配对象数组，就必须使用 &lt;code&gt;delete[]&lt;/code&gt; 来释放。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;之所以要怎么做，是因为编译器需要知道要为多少个对象调用析构函数。当使用 &lt;code&gt;new&lt;/code&gt; 和 &lt;code&gt;delete&lt;/code&gt; 时，底层会发生两件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存的分配/释放 (通过 &lt;code&gt;operator new&lt;/code&gt; 和 &lt;code&gt;operator delete&lt;/code&gt; 函数)。&lt;/li&gt;
&lt;li&gt;对象的构造/析构 (通过构造函数和析构函数)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;对于单个对象&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::string* ptr1 = new std::string(&quot;hello&quot;);
// ...
delete ptr1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;new&lt;/code&gt; 分配足够一个 &lt;code&gt;std::string&lt;/code&gt; 对象的内存，并在这块内存上调用 &lt;code&gt;std::string&lt;/code&gt; 的构造函数。&lt;code&gt;delete&lt;/code&gt; 对 &lt;code&gt;ptr1&lt;/code&gt; 指向的对象调用 &lt;code&gt;std::string&lt;/code&gt; 的析构函数，然后释放内存。这里 &lt;code&gt;delete&lt;/code&gt; 明确知道只需要调用一次析构函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于对象数组&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::string* ptr2 = new std::string[3];
// ...
delete[] ptr2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;new[]&lt;/code&gt;:分配能容纳 3 个 &lt;code&gt;std::string&lt;/code&gt; 对象的内存，并依次调用 3 次默认构造函数。&lt;code&gt;delete[]&lt;/code&gt; 依次对这 3 个对象调用析构函数（通常是按与构造相反的顺序），然后释放整个内存块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;两者的关键区别在于内存布局&lt;/strong&gt;。当使用 &lt;code&gt;new[]&lt;/code&gt; 分配一个数组时，编译器会在分配的内存块中（通常是在指针返回的地址之前）额外存储数组的大小。当调用 &lt;code&gt;delete[]&lt;/code&gt; 时，它会首先查找这个隐藏的 &quot;数组大小&quot; 记录，从而得知需要调用多少次析构函数。当调用 &lt;code&gt;delete&lt;/code&gt; 时，它会假定只有一个对象，不会去查找这个大小，因此只会调用一次析构函数。&lt;/p&gt;
&lt;p&gt;两者混用会导致&lt;strong&gt;未定义行为&lt;/strong&gt;，即程序可能会崩溃、产生错误结果、或者表面上看起来正常运行但实际上已经造成了资源泄漏或内存损坏。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;delete&lt;/code&gt; 释放数组：只会调用第一个对象的析构函数，其他对象的析构函数不会被调用，导致资源泄漏。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;delete[]&lt;/code&gt; 释放单个对象：&lt;code&gt;delete[]&lt;/code&gt; 会试图在指向的内存块之前读取一个 &quot;数组大小&quot;。但那里并没有存储这个信息，它会读取到一块无意义的数据。这可能会导致它尝试在一个完全错误的位置上执行循环，调用若干次析构函数，最终几乎肯定会损坏内存，导致程序崩溃。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在书中，作者还提到了一个常见的陷阱，即 &lt;code&gt;typedef&lt;/code&gt; 可能会隐藏数组的本质，使得我们更容易犯错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;typedef std::string AddressLines[4]; // AddressLines 是一个包含4个string的数组类型

std::string* pal = new AddressLines; // 看起来像是分配单个对象，
                                     // 但实际上等价于 new std::string[4]
delete[] pal; // 正确
// delete pal;   // 错误！将导致资源泄漏
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，&lt;strong&gt;最佳实践为：不要对数组类型使用 &lt;code&gt;typedef&lt;/code&gt; 或 &lt;code&gt;using&lt;/code&gt;，以避免混淆。&lt;/strong&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Effective C++: 以独立语句将 newed 对象置入智能指针</title><link>https://liangbm3.site/blog/effective-c-yi-du-li-yu-ju-jiang-newed-dui-xiang-zhi-ru-zhi-neng-zhi-zhen</link><guid isPermaLink="true">https://liangbm3.site/blog/effective-c-yi-du-li-yu-ju-jiang-newed-dui-xiang-zhi-ru-zhi-neng-zhi-zhen</guid><description>Store newed objects in smart pointers in standalone statements</description><pubDate>Thu, 23 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;这是 Effective C++ 的第 17 个条款，Store newed objects in smart pointers in standalone statements，中文翻译为：&lt;strong&gt;以独立语句将 newed 对象置入智能指针。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;文章举了一个经典的例子来说明这个规则的重要性：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 假设有这两个函数
int priority();
void processWidget(std::shared_ptr&amp;#x3C;Widget&gt; pw, int priority);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用这种看起来很紧凑的调用方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;processWidget(std::shared_ptr&amp;#x3C;Widget&gt;(new Widget()), priority()); // 危险！可能导致内存泄漏
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行代码是非常危险的，因为 &lt;strong&gt;C++ 编译器拥有重新排列函数参数求值顺序的自由&lt;/strong&gt;。在 &lt;code&gt;processWidget&lt;/code&gt; 被真正调用之前，编译器必须完成三项工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行 &lt;code&gt;new Widget()&lt;/code&gt;，在堆上创建一个 &lt;code&gt;Widget&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;priority()&lt;/code&gt; 函数，获取一个 &lt;code&gt;int&lt;/code&gt; 结果。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;std::shared_ptr&lt;/code&gt; 的构造函数，用 &lt;code&gt;new Widget()&lt;/code&gt; 返回的原始指针来创建一个智能指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++ 标准只保证 &lt;code&gt;new Widget()&lt;/code&gt; 一定在 &lt;code&gt;std::shared_ptr&lt;/code&gt; 的构造函数调用之前执行，但对于 &lt;code&gt;priority()&lt;/code&gt; 的调用时机，标准并没有规定。这意味着，编译器可能会选择以下这个危险的执行顺序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new Widget()&lt;/code&gt;：在堆上成功创建了一个 &lt;code&gt;Widget&lt;/code&gt; 对象，一个指向它的原始指针被生成。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;priority()&lt;/code&gt;：接着执行这个函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::shared_ptr&lt;/code&gt; 构造函数：最后才用第 1 步的原始指针去构造智能指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;按照上面这个顺序，如果在第二步，即 &lt;code&gt;priority()&lt;/code&gt; 函数内部抛出了一个异常。根据 C++ 的异常处理机制，函数调用栈会开始回滚，这时第一步中由 &lt;code&gt;new Widget()&lt;/code&gt; 创建的那个原始指针将会丢失，同时第三步 &lt;code&gt;std::shared_ptr&lt;/code&gt; 的构造函数永远也不会执行。最终造成的结果是丢失了唯一指向堆上 &lt;code&gt;Widget&lt;/code&gt; 对象的指针。这块内存再也无法被 &lt;code&gt;delete&lt;/code&gt;，造成了&lt;strong&gt;内存泄漏&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们本想用智能指针来防止资源泄漏，但在这个执行顺序下，资源在被智能指针接管之前就因为异常而泄漏了。&lt;/p&gt;
&lt;p&gt;对于这个问题的解决方案，也就是这个条款所强调的，&lt;strong&gt;要用一条独立的语句完成智能指针的初始化&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 安全的做法
std::shared_ptr&amp;#x3C;Widget&gt; pw(new Widget()); // 第1步：创建对象并立刻用智能指针管理它

processWidget(pw, priority());            // 第2步：将已创建好的智能指针和另一个参数传入函数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++ 保证语句之间是按顺序执行的。第一行代码必须完全执行完毕，才会开始执行第二行。对于这种写法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在第一行 &lt;code&gt;std::shared_ptr&amp;#x3C;Widget&gt; pw(new Widget());&lt;/code&gt; 中。如果 &lt;code&gt;new Widget()&lt;/code&gt; 失败（例如内存不足抛出 &lt;code&gt;std::bad_alloc&lt;/code&gt;），&lt;code&gt;pw&lt;/code&gt; 根本不会被创建，一切正常。如果 &lt;code&gt;new Widget()&lt;/code&gt; 成功，那么返回的原始指针会立即被用来构造 &lt;code&gt;pw&lt;/code&gt;。这个操作在一条语句内是不可分割的。此时，&lt;code&gt;Widget&lt;/code&gt; 资源已经安全地被 &lt;code&gt;pw&lt;/code&gt; 管理起来了。&lt;/li&gt;
&lt;li&gt;程序执行第二行 &lt;code&gt;processWidget(pw, priority());&lt;/code&gt;时。即使 &lt;code&gt;priority()&lt;/code&gt; 抛出异常，栈回滚机制也会正确地销毁局部变量 &lt;code&gt;pw&lt;/code&gt;。&lt;code&gt;pw&lt;/code&gt; 的析构函数会负责 &lt;code&gt;delete&lt;/code&gt; 它所管理的 &lt;code&gt;Widget&lt;/code&gt; 对象，不会发生任何资源泄漏。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际上，现代 C++ 提供了比直接使用 &lt;code&gt;new&lt;/code&gt; 更好的工具来创建智能指针：&lt;code&gt;std::make_unique&lt;/code&gt; 和 &lt;code&gt;std::make_shared&lt;/code&gt;。使用 &lt;code&gt;std::make_shared&lt;/code&gt; 可以完全避免上面讨论的问题，并且写法更简洁、更安全，甚至可能更高效。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 最佳实践：使用 std::make_shared
processWidget(std::make_shared&amp;#x3C;Widget&gt;(), priority()); // 完全安全，并且推荐！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之所以 &lt;code&gt;std::make_shared&lt;/code&gt; 更好，是因为 &lt;code&gt;std::make_shared&lt;/code&gt; 是异常安全的，它像一个函数调用，它在内部完成了对象的创建和智能指针的构造。编译器无法将 &lt;code&gt;priority()&lt;/code&gt; 的调用插入到这两个操作之间，从而从根本上杜绝了资源泄漏的风险。还有一个更重要的原因是效率更高：&lt;code&gt;std::shared_ptr&lt;/code&gt; 需要一个额外的“控制块”来存储引用计数等信息。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::shared_ptr&amp;#x3C;Widget&gt;(new Widget())&lt;/code&gt; 通常会导致两次堆内存分配：一次为 &lt;code&gt;Widget&lt;/code&gt; 对象（在 &lt;code&gt;new&lt;/code&gt; 中），一次为控制块（在 &lt;code&gt;shared_ptr&lt;/code&gt; 构造函数中）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::make_shared&amp;#x3C;Widget&gt;()&lt;/code&gt; 则足够智能，它会进行一次大的堆内存分配，同时容纳 &lt;code&gt;Widget&lt;/code&gt; 对象和控制块。这减少了内存分配的开销，性能更好。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Effective C++: 在资源管理类中提供对原始资源的访问</title><link>https://liangbm3.site/blog/effective-c-zai-zi-yuan-guan-li-lei-zhong-ti-gong-dui-yuan-shi-zi-yuan-de-fang-wen</link><guid isPermaLink="true">https://liangbm3.site/blog/effective-c-zai-zi-yuan-guan-li-lei-zhong-ti-gong-dui-yuan-shi-zi-yuan-de-fang-wen</guid><description>Provide access to raw resources in resource-managing classes</description><pubDate>Mon, 20 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;这是 Effective C++ 的第 15 个条款，Provide access to raw resources in resource-managing classes，中文翻译为：&lt;strong&gt;在资源管理类中提供对原始资源的访问。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个条款解决了一个非常实际的问题：我们已经接受了 RAII 是管理资源的最佳方式，但我们所写的代码需要和大量已有的、只认“原始资源”的 API 交互。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::shared_ptr&amp;#x3C;Investment&gt; pInv(createInvestment());
int daysHeld(const Investment* pi); // 一个只接受裸指针的 API
daysHeld(pInv); // 编译错误！类型不匹配，pInv 不是 Investment*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RAII 对象（&lt;code&gt;pInv&lt;/code&gt;）封装了并管理着原始资源（ &lt;code&gt;Investment*&lt;/code&gt; 指针），但它本身并不是那个原始资源。因此，RAII 类必须提供一个“出口”，允许外界在需要时能够访问到它内部封装的那个原始资源。在书中，作者提到了两种常见的做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;显式转换&lt;/strong&gt;：提供一个专门的成员函数（通常命名为 get），让用户明确地调用它来获取原始资源。&lt;code&gt;std::shared_ptr&lt;/code&gt; 和 &lt;code&gt;std::unique_ptr&lt;/code&gt; 都提供了 &lt;code&gt;get()&lt;/code&gt; 成员函数。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;daysHeld(pInv.get()); // 正确！调用 get() 获取裸指针
&lt;/code&gt;&lt;/pre&gt;
对于我们自定义的 &lt;code&gt;Font&lt;/code&gt; 类，也可以添加一个 &lt;code&gt;get()&lt;/code&gt; 方法：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Font {
public:
    FontHandle get() const { return f; } // 显式转换函数
    // ...
private:
    FontHandle f;
};

Font myFont(getFont());
changeFontSize(myFont.get(), newSize); // 用户需要明确调用 .get()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：代码意图非常明确。任何看到 &lt;code&gt;.get()&lt;/code&gt; 的人都知道这里正在从一个管理类中取出其原始资源。这降低了误用的风险。&lt;/li&gt;
&lt;li&gt;缺点：用户必须时刻记着在需要原始资源的地方调用 &lt;code&gt;.get()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐式转换&lt;/strong&gt;：通过 C++ 的类型转换运算符，可以让类对象可以自动地、不经指明地转换为原始资源类型。实现的方法是在类中定义一个类型转换运算符 &lt;code&gt;operator T()&lt;/code&gt;，其中 &lt;code&gt;T&lt;/code&gt; 是你希望转换的目标类型。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Font {
public:
    operator FontHandle() const { return f; } // 隐式转换函数
    // ...
};

Font myFont(getFont());
changeFontSize(myFont, newSize); // 非常自然！myFont 自动转换为 FontHandle
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：用户代码写起来非常自然流畅，就像直接使用原始资源一样。&lt;/li&gt;
&lt;li&gt;缺点：方便是有代价的。隐式转换可能在你不期望的时候发生，导致难以发现的 bug。书中举了一个绝佳的例子：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Font f1(getFont());
FontHandle f2 = f1;
&lt;/code&gt;&lt;/pre&gt;
程序员的意图可能是想创建一个 &lt;code&gt;Font&lt;/code&gt; 的副本，但这里实际上触发了 &lt;code&gt;operator FontHandle()&lt;/code&gt;，创建了一个原始句柄 &lt;code&gt;f2&lt;/code&gt;。当 &lt;code&gt;f1&lt;/code&gt; 被销毁时，它会释放字体资源，而 &lt;code&gt;f2&lt;/code&gt; 此时就变成了一个悬空句柄，再使用它就会导致程序崩溃或未定义行为。&lt;br&gt;
智能指针的 &lt;code&gt;operator*&lt;/code&gt; 和 &lt;code&gt;operator-&gt;&lt;/code&gt; 也是一种隐式转换，但它们被设计得相对安全，因为它们返回的是对象本身或其成员的引用/指针，而不是资源句柄的副本。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总的来说，安全性通常比便利性更重要，因此：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;首选显式转换&lt;/strong&gt; (&lt;code&gt;get&lt;/code&gt; 方法)。它让代码的意图更加清晰，并且可以有效避免由意想不到的类型转换所引起的严重错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;谨慎使用隐式转换&lt;/strong&gt;。虽然它能让客户代码更漂亮，但它带来的风险（如上述的悬空句柄问题）往往得不偿失。只有在确定这种转换在所有情况下都是安全且符合直觉时，才应该考虑提供它。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Effective C++: 以对象管理资源</title><link>https://liangbm3.site/blog/effective-c-yi-dui-xiang-guan-li-zi-yuan</link><guid isPermaLink="true">https://liangbm3.site/blog/effective-c-yi-dui-xiang-guan-li-zi-yuan</guid><description>Using objects to manage resources</description><pubDate>Sun, 19 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是 Effective C++ 的第 13 个条款，Using objects to manage resources，中文翻译为：&lt;strong&gt;以对象管理资源。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 C++ 中，资源通常指哪些一旦用了它，将来就必须还回去的东西，比如文件句柄、网络连接、互斥锁、动态分配的内存等。以动态分配内存为例，使用 &lt;code&gt;new&lt;/code&gt; 分配的内存必须用 &lt;code&gt;delete&lt;/code&gt; 释放，否则就会发生内存泄漏。&lt;/p&gt;
&lt;p&gt;在书中，作者举了这样一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 一个生产 Investment 对象的工厂函数
Investment* createInvestment();

void f()
{
    Investment* pInv = createInvestment(); // 调用工厂函数，获得资源
    ... // 其他操作
    delete pInv; // 手动释放资源
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码的问题在于，如果中间的&lt;code&gt;...&lt;/code&gt;部分发生了异常，或者函数有多个 &lt;code&gt;return&lt;/code&gt; 路径、&lt;code&gt;continue&lt;/code&gt; 或 &lt;code&gt;goto&lt;/code&gt; 语句提前退出，那么 &lt;code&gt;delete pInv&lt;/code&gt; 就可能永远不会被执行，从而导致资源泄漏（在这个例子中是内存泄漏）。手动管理资源是不可靠且危险的。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，C++ 提供了一种更安全的资源管理方式：&lt;strong&gt;资源获取即初始化（RAII, Resource Acquisition Is Initialization）&lt;/strong&gt;。RAII 的核心思想是将资源的获取和释放绑定到对象的生命周期上。具体做法是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在构造函数中获取资源&lt;/strong&gt;：将资源（如动态分配的内存指针）封装在一个类中，在类的构造函数里完成资源的申请。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在析构函数中释放资源&lt;/strong&gt;：在类的析构函数里执行资源的释放操作（如 delete）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于 C++ 语言保证，当一个对象离开其作用域时（无论是函数正常结束还是因为抛出异常），它的析构函数一定会被自动调用。这样一来，资源的释放就变成了自动行为，永远不会被遗漏。&lt;/p&gt;
&lt;p&gt;在现代 C++ 中，标准库提供了许多 RAII 风格的类来管理各种资源。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::unique_ptr&lt;/code&gt; 和 &lt;code&gt;std::shared_ptr&lt;/code&gt;：用于管理动态分配的内存。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::lock_guard&lt;/code&gt; 和 &lt;code&gt;std::unique_lock&lt;/code&gt;：用于管理互斥锁。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::fstream&lt;/code&gt;：用于管理文件句柄。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过使用这些 RAII 类，我们可以大大简化资源管理的代码，减少错误发生的可能性。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Effective C++: 在资源管理类中小心 copying 行为</title><link>https://liangbm3.site/blog/effective-c-zai-zi-yuan-guan-li-lei-zhong-xiao-xin-copying-xing-wei</link><guid isPermaLink="true">https://liangbm3.site/blog/effective-c-zai-zi-yuan-guan-li-lei-zhong-xiao-xin-copying-xing-wei</guid><description>Think carefully about copying behavior in resource-managing classes</description><pubDate>Sun, 19 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;这是 Effective C++ 的第 14 个条款，Think carefully about copying behavior in resource-managing classes，中文翻译为：&lt;strong&gt;在资源管理类中小心 copying 行为。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当我们自己写一个 RAII 类时，比如书中提到的用于管理互斥锁的 Lock 类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Lock {
public:
    explicit Lock(Mutex* pm) : mutexPtr(pm) {
        lock(mutexPtr); // 在构造函数中获取资源（加锁）
    }
    ~Lock() {
        unlock(mutexPtr); // 在析构函数中释放资源（解锁）
    }
private:
    Mutex* mutexPtr;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个基础的 Lock 类遵循了 RAII 原则，但是当我们复制一个 Lock 对象时，问题就来了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Mutex m; // 一个互斥锁
{
    Lock m1(&amp;#x26;m);    // m1 对 m 加锁
    Lock m2(m1);    // m2 是 m1 的一个副本。这里会发生什么？
} // m1 和 m2 在这里离开作用域，它们的析构函数被调用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们没有手动定义拷贝构造函数的话，编译器会自动生成一个默认的拷贝构造函数，这个默认的拷贝构造函数只会进行浅拷贝操作，即简单地复制成员变量的值。在这个例子中，&lt;code&gt;m2&lt;/code&gt; 会得到和 &lt;code&gt;m1&lt;/code&gt; 相同的 &lt;code&gt;mutexPtr&lt;/code&gt; 指针，这意味着当 &lt;code&gt;m1&lt;/code&gt; 和 &lt;code&gt;m2&lt;/code&gt; 离开作用域时，它们都会尝试解锁同一个互斥锁。这会导致未定义行为，因为同一个互斥锁被解锁了两次。&lt;/p&gt;
&lt;p&gt;因此，作者提到，当设计一个 RAII 类时，对于“复制”操作，通常有以下四种选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;禁止复制&lt;/strong&gt;：这是最简单、也常常是最安全的选择。如果复制一个资源句柄没有意义（比如锁），或者会带来问题，那就直接禁止它。
&lt;ul&gt;
&lt;li&gt;适用场景：资源的逻辑本身就不支持被复制或共享，例如 &lt;code&gt;std::unique_ptr&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;实现方式：通过将拷贝构造函数和拷贝赋值运算符声明为私有（C++11 之前）或删除（C++11 及以后）来禁止复制。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Lock {
public:
    // ...
    Lock(const Lock&amp;#x26;) = delete;
    Lock&amp;#x26; operator=(const Lock&amp;#x26;) = delete;
private:
    // ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;引用计数&lt;/strong&gt;：允许多个对象共享同一个底层资源，并记录“使用者”的数量。只有当最后一个使用者被销毁时，资源才被真正释放。
&lt;ul&gt;
&lt;li&gt;适用场景：需要共享资源所有权的场景，例如 &lt;code&gt;std::shared_ptr&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;实现方式：最简单的方法是在 RAII 类内部使用一个 &lt;code&gt;std::shared_ptr&lt;/code&gt; 来持有资源。&lt;code&gt;shared_ptr&lt;/code&gt; 可以接受一个自定义删除器，这让它不仅能管理内存，还能管理任何资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Lock {
public:
    explicit Lock(Mutex* pm)
        : pMutex(pm, unlock) // 使用 shared_ptr，并提供 unlock 作为删除器
    {
        lock(pMutex.get());
    }
    // 不需要手动写析构函数，shared_ptr 会自动管理！
private:
    std::shared_ptr&amp;#x3C;Mutex&gt; pMutex;
};
&lt;/code&gt;&lt;/pre&gt;
当最后一个 Lock 对象被销毁时，它内部的 &lt;code&gt;shared_ptr&lt;/code&gt; 的引用计数会变为 0，此时它会自动调用我们提供的 &lt;code&gt;unlock&lt;/code&gt; 函数来释放锁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深拷贝&lt;/strong&gt;：在复制对象时，也完整地复制一份底层资源。这样每个对象都拥有一份独立的、内容相同的资源。
&lt;ul&gt;
&lt;li&gt;适用场景：当资源本身可以被安全、完整地复制时。例如，一个管理动态字符数组的字符串类。&lt;/li&gt;
&lt;li&gt;实现方式：手动实现拷贝构造函数和拷贝赋值运算符，在其中创建新资源并复制内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 伪代码示例
String(const String&amp;#x26; other) {
    // 分配新内存
    // 复制 other 中的字符串内容到新内存
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;转移所有权&lt;/strong&gt;：在复制时，将底层资源的所有权从源对象“转移”到目标对象，源对象则不再拥有该资源（通常变为空）。
&lt;ul&gt;
&lt;li&gt;适用场景：需要严格的独占所有权，但又希望能够转移它。例如 &lt;code&gt;std::unique_ptr&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;实现方式：在 C++11 之后，通过实现移动构造函数和移动赋值运算符来完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class UniqueResource {
public:
    UniqueResource(Resource* res) : resourcePtr(res) {}
    // 移动构造函数
    UniqueResource(UniqueResource&amp;#x26;&amp;#x26; other) noexcept
        : resourcePtr(other.resourcePtr) {
        other.resourcePtr = nullptr; // 转移所有权
    }
    // 移动赋值运算符
    UniqueResource&amp;#x26; operator=(UniqueResource&amp;#x26;&amp;#x26; other) noexcept {
        if (this != &amp;#x26;other) {
            release(); // 释放当前资源
            resourcePtr = other.resourcePtr;
            other.resourcePtr = nullptr; // 转移所有权
        }
        return *this;
    }
private:
    Resource* resourcePtr;
    void release() {
        if (resourcePtr) {
            // 释放资源
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Effective C++: 为多态基类声明虚析构函数</title><link>https://liangbm3.site/blog/effective-c-wei-duo-tai-ji-lei-sheng-ming-xu-xi-gou-han-shu</link><guid isPermaLink="true">https://liangbm3.site/blog/effective-c-wei-duo-tai-ji-lei-sheng-ming-xu-xi-gou-han-shu</guid><description>Declare destructors virtual in polymorphic base classes</description><pubDate>Sat, 18 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;这是Effective C++的第 7 条款，Declare destructors virtual in polymorphic base classes，中文翻译为：&lt;strong&gt;为多态基类声明虚析构函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个条款是用来解决一个非常具体且危险的问题：&lt;strong&gt;当通过基类指针删除派生类对象时，如果基类的析构函数不是虚函数，派生类的析构函数将不会被调用，导致资源泄漏和未定义行为。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;以下代码为例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 基类
class TimeKeeper {
public:
    TimeKeeper();
    ~TimeKeeper(); // 注意：这是一个非虚 (non-virtual) 析构函数
    // ...
};

// 派生类
class AtomicClock : public TimeKeeper { ... };

// 工厂函数，返回一个指向派生类对象的基类指针
TimeKeeper* getTimeKeeper() {
    return new AtomicClock();
}

// 客户端代码
TimeKeeper* ptk = getTimeKeeper();
// ... 使用 ptk ...
delete ptk; // 危险！问题就在这里！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当调用 &lt;code&gt;delete ptk;&lt;/code&gt; 时，由于 &lt;code&gt;TimeKeeper&lt;/code&gt; 的析构函数不是虚函数，只有 &lt;code&gt;TimeKeeper&lt;/code&gt; 的析构函数会被调用，而 &lt;code&gt;AtomicClock&lt;/code&gt; 的析构函数不会被调用。这会导致 &lt;code&gt;AtomicClock&lt;/code&gt; 中分配的资源（如动态内存、文件句柄等）无法正确释放，进而引发资源泄漏和未定义行为。而解决方法就是将基类的析构函数声明为虚函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class TimeKeeper {
public:
    TimeKeeper();
    virtual ~TimeKeeper(); 
    // ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是作者又指出，无端地将所有类的析构函数声明为虚函数也是错误的做法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能与成本&lt;/strong&gt;：最直接和最主要的原因是性能和内存成本的考虑，当为一个类添加虚函数时，会给该类的每个对象引入一个虚函数指针(vptr)，这会导致对象体积的增大，文中举了一个非常形象的例子：一个用于表示二维空间点坐标的 Point 类，原本可能只包含两个 int，在64位系统上占用64位（8字节）。一旦为其添加了 vptr，整个对象的体积可能会增大50%~100%，达到96位（12字节）甚至128位（16字节）。另一方面，引入 vptr 会破坏对象的内存布局，使其不再是纯粹的数据集合，这样的对象将“不再可能把它们传递至（或接收自）其他语言所写的函数”，不再具有移植性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设计意图&lt;/strong&gt;：一个类不含任何虚函数，通常是在明确地表示“它并不意图被用作一个基类”，或者至少不是一个“多态基类”。文中用 &lt;code&gt;std::string&lt;/code&gt; 举例。&lt;code&gt;std::string&lt;/code&gt; 没有任何虚函数，包括析构函数。这清楚地表明了它的设计者不希望用户继承它并进行多态操作。如果你错误地这样做了（例如，&lt;code&gt;class SpecialString : public std::string&lt;/code&gt;），并且通过 &lt;code&gt;std::string&lt;/code&gt; 指针去删除 &lt;code&gt;SpecialString&lt;/code&gt; 对象，就会导致未定义行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文中还提到了纯虚析构函数，有时候你想让一个类成为抽象基类（不能被实例化），但这个类里又恰好没有任何其他适合声明为纯虚的函数，这时候就可以将析构函数声明为纯虚析构函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class AWOW { // Abstract w/o Virtuals
public:
    virtual ~AWOW() = 0; // 声明一个纯虚析构函数
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，纯虚析构函数必须在类外进行定义，即使它是纯虚的，因为在派生类对象被销毁时，基类的析构函数仍然需要被调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;AWOW::~AWOW() { } // 必须提供，哪怕是空的
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Effective C++: 视C++为一个语言联邦</title><link>https://liangbm3.site/blog/effective-c-shi-c-wei-yi-ge-yu-yan-lian-bang</link><guid isPermaLink="true">https://liangbm3.site/blog/effective-c-shi-c-wei-yi-ge-yu-yan-lian-bang</guid><description>View C++ as a federation of languages</description><pubDate>Sat, 18 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是Effective C++的第一个条款，View C++ as a federation of languages，中文翻译为：&lt;strong&gt;视C++为一个语言联邦。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于这个条款的理解，可以从 C++ 的历史发展来看。&lt;/p&gt;
&lt;p&gt;C++ 的故事始于 1979 年，在美国新泽西州的贝尔实验室（Bell Labs）。一位名叫 本贾尼·斯特劳斯特卢普 (Bjarne Stroustrup) 的丹麦计算机科学家，希望将他博士期间使用的 Simula 语言的“类”和面向对象思想，与 C 语言的高效和底层控制能力结合起来。 最初的产物，被称为“C with Classes”。主要是在C的基础上增加了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类（Classes）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;继承（Inheritance）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内联函数（Inline Functions）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认参数（Default Arguments）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;到了1983年，C with Classes 发展成为 C++，在这个时期，C++ 加入了许多至今仍是核心的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;虚函数（Virtual Functions）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数重载（Function Overloading）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;引用（References）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;new 和 delete 操作符&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模板（Templates）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;随着 C++ 被越来越广泛地使用，不同的编译器厂商实现了各自的版本，导致了代码的可移植性问题。为了解决这个问题，一个标准化的进程启动了。1998年，第一个 C++ 标准发布，通常称为 C++98。这是一个极其重要的版本，它标志着 C++ 从一门“语言”正式成为一个有国际标准的“工业级语言”。C++98 主要贡献：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;语言核心的规范化&lt;/li&gt;
&lt;li&gt;标准库的诞生&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2011年至今，C++经历了多次重大更新：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C++11&lt;/strong&gt;：被誉为“现代C++”的开端，引入了自动类型推导（auto）、范围for循环（range-based for loops）、智能指针（smart pointers）、右值引用（rvalue references）、lambda表达式、并发编程支持等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C++14&lt;/strong&gt;：对C++11的补充和改进，增加了泛型lambda、auto返回类型推导等特性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C++17&lt;/strong&gt;：引入了结构化绑定（structured bindings）、std::optional、std::variant等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C++20&lt;/strong&gt;：带来了概念（concepts）、范围（ranges）、协程（coroutines）、模块（modules）等重大特性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C++23&lt;/strong&gt;：继续在语言和库方面进行改进，增强了对现代编程范式的支持。例如 std::print 、std::expect 等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过回顾 C++ 的发展历程，我们可以清晰地看到 C++ 并非一成不变的单一语言，而是一个不断演进、融合多种编程范式和特性的“语言联邦”。每一个版本的更新都引入了新的概念和工具，使得 C++ 能够适应不同的编程需求和挑战。&lt;/p&gt;
&lt;p&gt;书中，作者提出，可以把 C++ 看成 四个子语言，每个子语言都有自己的一套规则、惯例和最佳实践。当你从一个子语言切换到另一个时，高效编程的策略可能也需要随之改变。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt;： 这部分完全继承了 C 的所有特性，是 C++ 的基石，包括：
&lt;ul&gt;
&lt;li&gt;核心语法：块（blocks）、语句（statements）、预处理器（preprocessor）、内置数据类型（built-in data types）、指针（pointers）、数组（arrays）等。&lt;/li&gt;
&lt;li&gt;编程范式：面向过程&lt;/li&gt;
&lt;li&gt;高效编程规则：在 C 的世界里，一切原始且高效，需要手动管理内存，注重性能和资源利用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Object-Oriented C++&lt;/strong&gt;：这部分也就是 C with Classes 所诉求的，主要围绕类和对象来展开，包括：
&lt;ul&gt;
&lt;li&gt;核心概念：类（classes）、封装（encapsulation）、继承（inheritance）、多态（polymorphism）、构造函数（constructors）和析构函数（destructors）。&lt;/li&gt;
&lt;li&gt;编程范式：面向对象编程（OOP）&lt;/li&gt;
&lt;li&gt;高效编程规则：在这个子语言中，关注点转向了设计模式、对象生命周期管理、虚函数的使用等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Template C++&lt;/strong&gt;：这是 C++ 泛型编程部分，运行我们编写不依赖具体类型的代码，提高代码的复用性，包括：
&lt;ul&gt;
&lt;li&gt;核心概念：模板（templates）、模板特化（template specialization）、模板元编程（template metaprogramming）。&lt;/li&gt;
&lt;li&gt;编程范式：泛型编程&lt;/li&gt;
&lt;li&gt;高效编程规则：模板元编程是这个语言的高阶玩法，可以将原本在运行时完成的工作提前到编译阶段，意味着某些运行时错误可以在编译时发现，能够生成极其高效的代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;STL&lt;/strong&gt;：STL是一个基于模板构建、高度协同的程序库，包括：
&lt;ul&gt;
&lt;li&gt;核心组件：容器（containers）、迭代器（iterators）、算法（algorithms）、函数对象（function objects）。&lt;/li&gt;
&lt;li&gt;编程范式：基于容器、迭代器和算法的编程&lt;/li&gt;
&lt;li&gt;高效编程规则：STL有一套自己的惯例，例如使用迭代器遍历容器而不是指针，优先使用标准算法而不是手写循环等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，当使用 C++ 编程的时候，理解并区分这四个子语言是非常重要的。每个子语言都有其独特的规则和最佳实践，只有正确地应用这些规则，才能编写出高效、可靠的 C++ 代码。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>C++ 中使用 SQLite3 API 的经典流程</title><link>https://liangbm3.site/blog/c-zhong-shi-yong-sqlite3-api-de-jing-dian-liu-cheng</link><guid isPermaLink="true">https://liangbm3.site/blog/c-zhong-shi-yong-sqlite3-api-de-jing-dian-liu-cheng</guid><description>总结一下在 C++ 中使用 SQLite3 API 的经典流程</description><pubDate>Thu, 16 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 核心概念&lt;/h2&gt;
&lt;p&gt;在使用 C++ API 之前，需要理解 SQLite3 的几个核心对象和函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sqlite3&lt;/code&gt;: 代表一个打开的数据库连接的句柄（handle）。所有针对该数据库的操作都需要这个对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite3_stmt&lt;/code&gt;: 代表一个已经编译（prepared）的 SQL 语句。这是执行复杂查询和防止 SQL 注入的关键。&lt;/li&gt;
&lt;li&gt;返回码: SQLite3 的几乎所有函数都会返回一个整型状态码，例如 &lt;code&gt;SQLITE_OK&lt;/code&gt; 表示成功，&lt;code&gt;SQLITE_ERROR&lt;/code&gt; 表示通用错误，&lt;code&gt;SQLITE_BUSY&lt;/code&gt; 表示数据库被锁定等。检查这些返回码是保证程序健壮性的关键。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 经典使用流程&lt;/h2&gt;
&lt;h3&gt;2.1 包含头文件和定义必要变量&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() {
  sqlite3 *db; // 数据库句柄
  char *errMsg = nullptr;
  int rc; // 返回值
  //...其他代码
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 打开或创建数据库&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;函数&lt;/strong&gt;: &lt;code&gt;int sqlite3_open(const char *filename, sqlite3 **ppDb)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;: 打开一个数据库连接。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参数&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;filename&lt;/code&gt;: 数据库文件的路径。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ppDb&lt;/code&gt;: 一个指向 &lt;code&gt;sqlite3*&lt;/code&gt; 指针的指针，函数会把创建的数据库句柄写入这里。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 创建或打开数据库
rc = sqlite3_open(&quot;test.db&quot;, &amp;#x26;db);
if (rc) {
  std::cerr &amp;#x3C;&amp;#x3C; &quot;Can&apos;t open database: &quot; &amp;#x3C;&amp;#x3C; sqlite3_errmsg(db) &amp;#x3C;&amp;#x3C; std::endl;
  return rc;
} else {
  std::cout &amp;#x3C;&amp;#x3C; &quot;Opened database successfully&quot; &amp;#x3C;&amp;#x3C; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sqlite3_errmsg(db)&lt;/code&gt; 是一个非常有用的函数，它可以获取最近一次操作失败的错误信息。&lt;/p&gt;
&lt;h3&gt;2.3 执行SQL语句（非查询类）&lt;/h3&gt;
&lt;p&gt;对于不需要返回结果集的 SQL 语句，比如 &lt;code&gt;CREATE TABLE&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;，最简单的方式是使用 &lt;code&gt;sqlite3_exec()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;函数&lt;/strong&gt;: &lt;code&gt;int sqlite3_exec(sqlite3 *db, const char *sql, int (*callback)(void*,int,char**,char**), void *arg, char **errmsg)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;: 一次性执行一个或多个 SQL 语句。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参数&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;db&lt;/code&gt;: 数据库句柄。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sql&lt;/code&gt;: 要执行的 SQL 字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;callback&lt;/code&gt;: 回调函数，每当查询产生一行结果时被调用（对于非查询语句，设为 &lt;code&gt;NULL&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arg&lt;/code&gt;: 传递给回调函数的第一个参数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;errmsg&lt;/code&gt;: 用于存放错误信息的指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 创建一张表
const char *sql_create_table = &quot;CREATE TABLE IF NOT EXISTS USERS(&quot;
                             &quot;ID INT PRIMARY KEY     NOT NULL,&quot;
                             &quot;NAME           TEXT    NOT NULL,&quot;
                             &quot;AGE            INT     NOT NULL);&quot;;
rc = sqlite3_exec(db, sql_create_table, nullptr, nullptr, &amp;#x26;errMsg);
if (rc != SQLITE_OK) {
  std::cerr &amp;#x3C;&amp;#x3C; &quot;SQL error: &quot; &amp;#x3C;&amp;#x3C; errMsg &amp;#x3C;&amp;#x3C; std::endl;
  sqlite3_free(errMsg); // 释放错误信息内存
} else {
  std::cout &amp;#x3C;&amp;#x3C; &quot;Table created successfully&quot; &amp;#x3C;&amp;#x3C; std::endl;
}
// 插入数据
const char *sql_insert =
  &quot;INSERT INTO USERS (ID,NAME,AGE) VALUES (1, &apos;Alice&apos;, 25);&quot;
  &quot;INSERT INTO USERS (ID,NAME,AGE) VALUES (2, &apos;Bob&apos;, 30);&quot;;
rc = sqlite3_exec(db, sql_insert, nullptr, nullptr, &amp;#x26;errMsg);
if (rc != SQLITE_OK) {
  std::cerr &amp;#x3C;&amp;#x3C; &quot;SQL error: &quot; &amp;#x3C;&amp;#x3C; errMsg &amp;#x3C;&amp;#x3C; std::endl;
  sqlite3_free(errMsg); // 释放错误信息内存
} else {
  std::cout &amp;#x3C;&amp;#x3C; &quot;Records created successfully&quot; &amp;#x3C;&amp;#x3C; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 执行查询并处理结果&lt;/h3&gt;
&lt;p&gt;对于 &lt;code&gt;SELECT&lt;/code&gt; 查询，虽然也可以使用 &lt;code&gt;sqlite3_exec&lt;/code&gt; 配合回调函数，但更&lt;strong&gt;经典和推荐&lt;/strong&gt;的做法是使用 &lt;strong&gt;prepare - step - finalize&lt;/strong&gt; 流程。这种方式更灵活、更安全（可以防止 SQL 注入），并且效率更高。&lt;/p&gt;
&lt;h4&gt;2.4.1 准备SQL语句&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;sqlite3_prepare_v2()&lt;/code&gt; 将 SQL 语句编译成一个字节码对象 (&lt;code&gt;sqlite3_stmt&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;函数&lt;/strong&gt;: &lt;code&gt;int sqlite3_prepare_v2(sqlite3 *db, const char *zSql, int nByte, sqlite3_stmt **ppStmt, const char **pzTail)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;: 编译 SQL 语句，为执行做准备。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参数：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;db&lt;/code&gt;: 数据库连接句柄&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zSql&lt;/code&gt;: 要编译的SQL语句字符串&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bByte&lt;/code&gt;:SQL字符串的字节长度，如果为 -1 SQLite 会自动计数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ppStmt&lt;/code&gt;: 输出参数，用于接收编译后的语句句柄&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pzTail&lt;/code&gt;: 输出参数，指向SQL字符串中未被处理的部分，如果SQL字符串中包含多个语句，会返回第一个语句后剩余的部分，如果只有一个语句或者不关心剩余的部分，可以传入&lt;code&gt;nullptr&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;sqlite3_stmt *stmt; // 语句句柄
const char *sql_select = &quot;SELECT ID, NAME, AGE FROM USERS WHERE AGE &gt; ?;&quot;;
// 准备SQL语句
rc = sqlite3_prepare_v2(db, sql_select, -1, &amp;#x26;stmt, nullptr);
if (rc != SQLITE_OK) {
  std::cerr &amp;#x3C;&amp;#x3C; &quot;Failed to prepare statement: &quot; &amp;#x3C;&amp;#x3C; sqlite3_errmsg(db) &amp;#x3C;&amp;#x3C; std::endl;
  sqlite3_close(db);
  return rc;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.4.2 绑定参数&lt;/h4&gt;
&lt;p&gt;如果 SQL 语句中使用了占位符 (&lt;code&gt;?&lt;/code&gt;)，需要使用 &lt;code&gt;sqlite3_bind_*()&lt;/code&gt; 系列函数来绑定具体的值。这可以有效防止 SQL 注入攻击。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 绑定整数
int sqlite3_bind_int(sqlite3_stmt*, int, int);

// 绑定64位整数
int sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64);

// 绑定双精度浮点数
int sqlite3_bind_double(sqlite3_stmt*, int, double);

// 绑定空值
int sqlite3_bind_null(sqlite3_stmt*, int);

// 绑定文本字符串
int sqlite3_bind_text(sqlite3_stmt*, int, const char*, int, void(*)(void*));

// 绑定二进制数据
int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int, void(*)(void*));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一系列的函数第一个参数为语句句柄，第二个参数为占位符索引（从1开始），第三个参数为值，第四个参数为一个析构函数指针，用于指定字符串的生命周期类型。这个参数可以为&lt;code&gt;SQLITE_STATIC&lt;/code&gt;和&lt;code&gt;SQLITE_TRANSIENT&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;SQLITE_STATIC&lt;/code&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;: 向 SQLite 保证，传入的这个字符串 (&lt;code&gt;zData&lt;/code&gt;) 的内存是&lt;strong&gt;静态的、全局的&lt;/strong&gt;、或者至少在 &lt;code&gt;sqlite3_stmt&lt;/code&gt; 整个生命周期内（直到 &lt;code&gt;sqlite3_finalize&lt;/code&gt; 被调用）都是有效且不会改变的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQLite 的行为&lt;/strong&gt;: 既然做了这个保证，SQLite 为了追求极致的性能，会直接使用传入的指针，而不会自己复制一份字符串数据。它只是持有了这个指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风险&lt;/strong&gt;: 如果传入了一个局部变量（比如一个函数内的 &lt;code&gt;std::string.c_str()&lt;/code&gt; 或者一个栈上的字符数组），当函数返回后，这个局部变量被销毁，内存被释放。但 SQLite 里的语句句柄 &lt;code&gt;stmt&lt;/code&gt; 还傻傻地存着那个已经无效的野指针。后面调用 &lt;code&gt;sqlite3_step()&lt;/code&gt; 去执行它时，它会尝试读取一个无效的内存地址，从而导致&lt;strong&gt;未定义行为&lt;/strong&gt;，通常表现为程序崩溃、数据损坏或奇怪的错误。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SQLITE_TRANSIENT&lt;/code&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;: 这个宏告诉 SQLite，传入的这个字符串 (&lt;code&gt;zData&lt;/code&gt;) 是临时的、短暂的 (transient)，它的生命周期可能很短，在 &lt;code&gt;sqlite3_bind_text&lt;/code&gt; 函数调用返回后可能就失效了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQLite 的行为&lt;/strong&gt;: 收到这个信号后，SQLite 会变得很谨慎。它会在函数内部&lt;strong&gt;立即将字符串数据完整地复制一份&lt;/strong&gt;，并由自己来管理这份拷贝的内存。这样，即使原始的字符串变量超出了作用域被销毁，也完全没关系，因为 SQLite 使用的是它自己的安全拷贝。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代价&lt;/strong&gt;: 需要一次额外的内存分配和数据拷贝，相比 &lt;code&gt;SQLITE_STATIC&lt;/code&gt; 会有微小的性能开销。但它换来的是&lt;strong&gt;安全和省心&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int age_limit = 20;
rc = sqlite3_bind_int(stmt, 1, age_limit);
if (rc != SQLITE_OK) {
  std::cerr &amp;#x3C;&amp;#x3C; &quot;Failed to bind parameter: &quot; &amp;#x3C;&amp;#x3C; sqlite3_errmsg(db) &amp;#x3C;&amp;#x3C; std::endl;
  sqlite3_finalize(stmt);
  sqlite3_close(db);
  return rc;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.4.3 执行语句并逐行获取结果&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;sqlite3_step()&lt;/code&gt; 来执行预处理语句。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;函数&lt;/strong&gt;: &lt;code&gt;int sqlite3_step(sqlite3_stmt *pStmt)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;: 执行一次编译好的 SQL 语句。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回值&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SQLITE_ROW&lt;/code&gt;: 表示成功获取了一行数据，可以提取结果。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SQLITE_DONE&lt;/code&gt;: 表示所有行都已处理完毕。&lt;/li&gt;
&lt;li&gt;其他错误码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当 &lt;code&gt;sqlite3_step()&lt;/code&gt; 返回 &lt;code&gt;SQLITE_ROW&lt;/code&gt; 时，使用 &lt;code&gt;sqlite3_column_*()&lt;/code&gt; 系列函数来获取当前行各个字段的值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;函数&lt;/strong&gt;: &lt;code&gt;sqlite3_column_int()&lt;/code&gt;, &lt;code&gt;sqlite3_column_text()&lt;/code&gt;, &lt;code&gt;sqlite3_column_double()&lt;/code&gt; 等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;: 获取查询结果中当前行的某一列数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 循环执行step，逐行获取数据
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
  int id = sqlite3_column_int(stmt, 0);
  const unsigned char *name = sqlite3_column_text(stmt, 1);
  int age = sqlite3_column_int(stmt, 2);
  std::cout &amp;#x3C;&amp;#x3C; &quot;ID: &quot; &amp;#x3C;&amp;#x3C; id &amp;#x3C;&amp;#x3C; &quot;, Name: &quot; &amp;#x3C;&amp;#x3C; name &amp;#x3C;&amp;#x3C; &quot;, Age: &quot; &amp;#x3C;&amp;#x3C; age &amp;#x3C;&amp;#x3C; std::endl;
}
if (rc != SQLITE_DONE) {
  std::cerr &amp;#x3C;&amp;#x3C; &quot;Failed to execute statement: &quot; &amp;#x3C;&amp;#x3C; sqlite3_errmsg(db) &amp;#x3C;&amp;#x3C; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.5 销毁语句句柄&lt;/h3&gt;
&lt;p&gt;查询结束后，必须使用 &lt;code&gt;sqlite3_finalize()&lt;/code&gt; 来销毁 &lt;code&gt;sqlite3_stmt&lt;/code&gt; 对象，释放资源。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;函数&lt;/strong&gt;: &lt;code&gt;int sqlite3_finalize(sqlite3_stmt *pStmt)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;: 销毁预处理语句对象，回收内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 释放语句句柄
sqlite3_finalize(stmt);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.6 关闭数据库&lt;/h3&gt;
&lt;p&gt;所有操作完成后，使用 &lt;code&gt;sqlite3_close()&lt;/code&gt; 关闭数据库连接，释放所有与该连接相关的资源。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;函数&lt;/strong&gt;: &lt;code&gt;int sqlite3_close(sqlite3 *db)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;: 关闭数据库连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 关闭数据库
sqlite3_close(db);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 总结&lt;/h2&gt;
&lt;p&gt;| 步骤 | 核心函数 | 主要作用 |
| --- | --- | --- |
| &lt;strong&gt;1. 打开数据库&lt;/strong&gt; | &lt;code&gt;sqlite3_open()&lt;/code&gt; | 获取数据库连接句柄 &lt;code&gt;sqlite3*&lt;/code&gt; |
| &lt;strong&gt;2. 执行非查询SQL&lt;/strong&gt; | &lt;code&gt;sqlite3_exec()&lt;/code&gt; | 简单快速地执行 &lt;code&gt;CREATE&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt; 等操作|
| &lt;strong&gt;3. 准备查询&lt;/strong&gt; | &lt;code&gt;sqlite3_prepare_v2()&lt;/code&gt; | 编译 &lt;code&gt;SELECT&lt;/code&gt; 语句，获取语句句柄 &lt;code&gt;sqlite3_stmt*&lt;/code&gt;|
| &lt;strong&gt;4. 绑定参数&lt;/strong&gt; | &lt;code&gt;sqlite3_bind_*()&lt;/code&gt; | (可选) 为 &lt;code&gt;?&lt;/code&gt; 占位符安全地绑定值。 |
| &lt;strong&gt;5. 逐行执行&lt;/strong&gt; | &lt;code&gt;sqlite3_step()&lt;/code&gt; | 在循环中调用，每次获取一行结果 (&lt;code&gt;SQLITE_ROW&lt;/code&gt;)。 |
| &lt;strong&gt;6. 提取数据&lt;/strong&gt; | &lt;code&gt;sqlite3_column_*()&lt;/code&gt; | 从当前行中按列索引提取数据。 |
| &lt;strong&gt;7. 终结语句&lt;/strong&gt; | &lt;code&gt;sqlite3_finalize()&lt;/code&gt; | 释放语句句柄 &lt;code&gt;sqlite3_stmt*&lt;/code&gt; 的资源。 |
| &lt;strong&gt;8. 关闭数据库&lt;/strong&gt; | &lt;code&gt;sqlite3_close()&lt;/code&gt; | 释放数据库连接句柄 &lt;code&gt;sqlite3*&lt;/code&gt; 的所有资源。 |&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>SQLite 踩坑记录</title><link>https://liangbm3.site/blog/sqlite-cai-keng-ji-lu</link><guid isPermaLink="true">https://liangbm3.site/blog/sqlite-cai-keng-ji-lu</guid><description>最近在实习中排查了一个相当棘手的 Bug，这里记录一下</description><pubDate>Wed, 15 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 问题背景&lt;/h2&gt;
&lt;p&gt;最近在实习中排查了一个相当棘手的 Bug，这里记录一下。这个Bug的表象是一个模块将 JSON 数据存入 SQLite 数据库后，再次读取出来时会发生数据损坏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;排查记录：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;检查日志&lt;/strong&gt;：首先查看了模块的日志，很快定位到了错误，程序在解析从数据库中读出的 JSON 字符串时抛出了异常。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打印原始数据&lt;/strong&gt;：然后我直接打印了从数据库读出的原始字符串，发现内容要么是乱码，要么干脆是空的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查写入过程&lt;/strong&gt;：我转头去检查数据写入时的日志，却发现一切正常，没有任何错误或警告信息。程序似乎认为它已经成功地将数据写入了数据库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;直击数据库&lt;/strong&gt;：既然日志无法提供更多线索，我便使用命令行工具直接查询数据库。真相大白——数据库里存储的数据本身就是有问题的。这说明问题出在写入阶段，而不是读取或解析阶段。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;经过层层排查，我最终锁定了一个在使用 SQLite C++ API 时非常经典、但也极易被忽视的“坑”。&lt;/p&gt;
&lt;h2&gt;2. 问题根源&lt;/h2&gt;
&lt;p&gt;最后排查出来的问题是&lt;code&gt;sqlite3_bind_text()&lt;/code&gt;这个API使用错误。API的&lt;strong&gt;函数原型&lt;/strong&gt;如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int sqlite3_bind_text(
  sqlite3_stmt *pStmt,
  int i,
  const char *zData,
  int nData,
  void (*xDel)(void*)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;参数解释：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pStmt&lt;/code&gt;: 语句句柄。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt;: 参数索引 (从1开始)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zData&lt;/code&gt;: 要绑定的字符串数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nData&lt;/code&gt;: 字符串长度 (&lt;code&gt;-1&lt;/code&gt; 表示自动计算)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xDel&lt;/code&gt;: &lt;strong&gt;这是一个析构函数指针&lt;/strong&gt;，用于指定字符串 &lt;code&gt;zData&lt;/code&gt; 的生命周期类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;xDel&lt;/code&gt; 参数可以传入两个特殊的宏：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;SQLITE_STATIC&lt;/code&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;: 向 SQLite 保证，传入的这个字符串 (&lt;code&gt;zData&lt;/code&gt;) 的内存是&lt;strong&gt;静态的、全局的&lt;/strong&gt;、或者至少在 &lt;code&gt;sqlite3_stmt&lt;/code&gt; 整个生命周期内（直到 &lt;code&gt;sqlite3_finalize&lt;/code&gt; 被调用）都是有效且不会改变的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQLite 的行为&lt;/strong&gt;: 既然做了这个保证，SQLite 为了追求极致的性能，会直接使用传入的指针，而不会自己复制一份字符串数据。它只是持有了这个指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风险&lt;/strong&gt;: 如果传入了一个局部变量（比如一个函数内的 &lt;code&gt;std::string.c_str()&lt;/code&gt; 或者一个栈上的字符数组），当函数返回后，这个局部变量被销毁，内存被释放。但 SQLite 里的语句句柄 &lt;code&gt;stmt&lt;/code&gt; 还傻傻地存着那个已经无效的野指针。后面调用 &lt;code&gt;sqlite3_step()&lt;/code&gt; 去执行它时，它会尝试读取一个无效的内存地址，从而导致&lt;strong&gt;未定义行为&lt;/strong&gt;，通常表现为程序崩溃、数据损坏或奇怪的错误。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SQLITE_TRANSIENT&lt;/code&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;: 这个宏告诉 SQLite，传入的这个字符串 (&lt;code&gt;zData&lt;/code&gt;) 是临时的、短暂的 (transient)，它的生命周期可能很短，在 &lt;code&gt;sqlite3_bind_text&lt;/code&gt; 函数调用返回后可能就失效了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQLite 的行为&lt;/strong&gt;: 收到这个信号后，SQLite 会变得很谨慎。它会在函数内部&lt;strong&gt;立即将字符串数据完整地复制一份&lt;/strong&gt;，并由自己来管理这份拷贝的内存。这样，即使原始的字符串变量超出了作用域被销毁，也完全没关系，因为 SQLite 使用的是它自己的安全拷贝。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代价&lt;/strong&gt;: 需要一次额外的内存分配和数据拷贝，相比 &lt;code&gt;SQLITE_STATIC&lt;/code&gt; 会有微小的性能开销。但它换来的是&lt;strong&gt;安全和省心&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 总结&lt;/h2&gt;
&lt;p&gt;在使用这个API的时候，应该根据数据的声明周期来选择正确的宏。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对于&lt;strong&gt;局部变量、临时拼接的字符串、&lt;code&gt;std::string&lt;/code&gt; 对象&lt;/strong&gt;等，它们的生命周期很短，必须使用 &lt;code&gt;SQLITE_TRANSIENT&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::string user_name = &quot;Alice&quot;;
// 正确的做法：告诉 SQLite 这是临时数据，请它自己复制
sqlite3_bind_text(stmt, 1, user_name.c_str(), -1, SQLITE_TRANSIENT);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于&lt;strong&gt;字符串字面量、全局变量、或能确保其生命周期超过 &lt;code&gt;stmt&lt;/code&gt; 的数据&lt;/strong&gt;，可以使用 &lt;code&gt;SQLITE_STATIC&lt;/code&gt; 来获得一点性能优势。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 字符串字面量是静态存储的，整个程序运行期间都有效
sqlite3_bind_text(stmt, 1, &quot;Alice&quot;, -1, SQLITE_STATIC); // 这是安全的
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4. 拓展&lt;/h2&gt;
&lt;p&gt;深入探究会发现，SQLite 的 &lt;code&gt;sqlite3_bind_*&lt;/code&gt; 系列 API 在设计上体现了 C/C++ 核心的数据传递思想，主要分为两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;传值
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sqlite3_bind_int()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite3_bind_double()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite3_bind_int64()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;传指针
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sqlite3_bind_text()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite3_bind_blob()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;对于传值&lt;/strong&gt;，SQLite 接收到的是这个数字的&lt;strong&gt;副本&lt;/strong&gt;。它会立即将这个副本存放到它自己管理的内存中（通常是 &lt;code&gt;sqlite3_stmt&lt;/code&gt; 对象内部的某个字段）。这个过程非常快，因为整数和浮点数都是固定大小的、很小的数据类型。一旦函数调用完成，SQLite 已经拿到了它需要的&lt;strong&gt;值的拷贝&lt;/strong&gt;。原来的变量之后是超出作用域被销毁，还是被修改，都与 SQLite 毫无关系了。SQLite 内部已经有了一个安全、独立的副本，所以&lt;strong&gt;不存在野指针或数据失效的风险&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于传指针&lt;/strong&gt;，SQLite 拿到的只是一个地址，它并不知道这个地址指向的内存能有效多久。这段内存是由调用者管理的。这就带来了我们之前讨论的问题：如果 SQLite &lt;strong&gt;不复制&lt;/strong&gt;，它就需要你&lt;strong&gt;保证&lt;/strong&gt;这个地址在未来（&lt;code&gt;sqlite3_step&lt;/code&gt; 执行时）依然有效。这就是 &lt;code&gt;SQLITE_STATIC&lt;/code&gt; 的含义。如果 SQLite &lt;strong&gt;自己复制&lt;/strong&gt;，它就需要分配内存，然后把指针指向的数据拷贝过来。这样就安全了，但有性能开销。这就是 &lt;code&gt;SQLITE_TRANSIENT&lt;/code&gt; 的含义。&lt;strong&gt;生命周期参数是必需的&lt;/strong&gt;: 因为 C API 无法自动判断指针的生命周期，所以 SQLite 的设计者选择将这个决定权通过最后一个参数交还给开发者，让你明确告知它应该如何处理这个指针。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>单源最短路径</title><link>https://liangbm3.site/blog/dan-yuan-zui-duan-lu-jing</link><guid isPermaLink="true">https://liangbm3.site/blog/dan-yuan-zui-duan-lu-jing</guid><description>介绍一下单源最短路径算法：Dijkstra 算法、Bellman-Ford 算法和 SPFA 算法</description><pubDate>Wed, 09 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;1. 简介&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;单源最短路径&lt;/strong&gt;问题是图论中的一个非常经典的问题，它的目标是在一个带权的有向图或无向图中，找到从一个指定的“源”顶点到图中所有其他顶点的最短路径（路径上所有边的权重之和最小）。这个问题在实际生活中有很多重要的应用，比如网络路由、地图导航、通信网络优化等。&lt;/p&gt;
&lt;p&gt;主要的单源最短路径算法主要有以下三个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dijkstra 算法&lt;/strong&gt;：最著名和最常用的算法，但前提是图中所有边的权重都必须是非负的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bellman-Ford 算法&lt;/strong&gt;：比 Dijkstra 算法更具普适性，可以处理带有负权重边的图。同时，它还能检测图中是否存在“负权环”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SPFA 算法 (Shortest Path Faster Algorithm)&lt;/strong&gt;：这是 Bellman-Ford 算法的一种优化版本，在很多情况下（特别是稀疏图）比 Bellman-Ford 更快，同样可以处理负权边和检测负权环。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. Dijkstra 算法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Dijkstra 算法（迪杰斯特拉算法）&lt;/strong&gt; 是一种贪心算法。它的核心思想是，每次都从“尚未确定最短路径”的顶点中，选择一个距离源点最近的顶点，并且认为这个顶点的最短路径已经确定，然后用这个顶点去更新其他相邻顶点的距离。&lt;/p&gt;
&lt;p&gt;算法步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;创建一个距离数组 &lt;code&gt;dist&lt;/code&gt;，&lt;code&gt;dist[i]&lt;/code&gt; 表示从源点 &lt;code&gt;startNode&lt;/code&gt; 到顶点 &lt;code&gt;i&lt;/code&gt; 的当前最短距离。将 &lt;code&gt;dist[startNode]&lt;/code&gt; 初始化为 &lt;code&gt;0&lt;/code&gt;，其他所有顶点的 &lt;code&gt;dist&lt;/code&gt; 初始化为无穷大 (&lt;code&gt;INF&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;创建一个优先队列 &lt;code&gt;pq&lt;/code&gt;，并将 &lt;code&gt;{0, startNode}&lt;/code&gt;（表示距离为0，顶点为 startNode）加入队列。优先队列会根据距离从小到大排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主循环&lt;/strong&gt;：当优先队列不为空时，执行以下操作：
&lt;ul&gt;
&lt;li&gt;从优先队列中取出距离最小的顶点 &lt;code&gt;u&lt;/code&gt;（以及其距离 &lt;code&gt;d&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;d&lt;/code&gt; 大于 &lt;code&gt;dist[u]&lt;/code&gt;，说明已经有更短的路径到达 &lt;code&gt;u&lt;/code&gt; 了（这个旧的、较长的路径信息是冗余的），直接跳过本次循环。&lt;/li&gt;
&lt;li&gt;遍历 &lt;code&gt;u&lt;/code&gt; 的所有邻接点 &lt;code&gt;v&lt;/code&gt;（权重为 &lt;code&gt;w&lt;/code&gt;），进行松弛操作：如果通过 &lt;code&gt;u&lt;/code&gt; 到达 &lt;code&gt;v&lt;/code&gt; 的路径比已知的到 &lt;code&gt;v&lt;/code&gt; 的最短路径还要短（即 &lt;code&gt;dist[u] + w &amp;#x3C; dist[v]&lt;/code&gt;），那么就更新 &lt;code&gt;dist[v]&lt;/code&gt; 为 &lt;code&gt;dist[u] + w&lt;/code&gt;，并将新的、更短的路径信息 &lt;code&gt;{dist[v], v} &lt;/code&gt;加入优先队列。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结束&lt;/strong&gt;：循环结束后，dist 数组中就存储了从源点到所有其他顶点的最短路径长度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++ 代码实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;queue&gt;
#include &amp;#x3C;limits&gt;

const int INF = std::numeric_limits&amp;#x3C;int&gt;::max(); // 代表无穷大

struct Edge
{
    int u, v, weight; // 边的起点、终点和权重
};

//{权重，顶点}
using ipair = std::pair&amp;#x3C;int, int&gt;; // 用于存储权重和顶点的对

// Dijkstra 算法实现
// V: 顶点数量
// startNode: 源点
// Edge: 图的边列表{u, v, weight}
void dijkstra(int V, int startNode, std::vector&amp;#x3C;Edge&gt; edges)
{
    // 距离数组，初始化为无穷大
    std::vector&amp;#x3C;int&gt; dist(V, INF);
    dist[startNode] = 0;

    // 邻接表表示图
    std::vector&amp;#x3C;std::vector&amp;#x3C;ipair&gt;&gt; adj(V);
    for (const auto &amp;#x26;edge : edges)
    {
        // 有向图
        adj[edge.u].push_back({edge.weight, edge.v});
        // 如果是无向图，则添加以下行
        // adj[edge.v].push_back({edge.weight, edge.u});
    }

    // 优先队列（最小堆），存储 {距离, 顶点}
    // std::greater 使其成为最小堆
    std::priority_queue&amp;#x3C;ipair, std::vector&amp;#x3C;ipair&gt;, std::greater&amp;#x3C;ipair&gt;&gt; pq;

    pq.push({0, startNode});

    while (!pq.empty())
    {
        // 取出当前已知最短路径的顶点
        int u = pq.top().second;
        int d = pq.top().first;
        pq.pop();

        // 如果这个顶点的最短路径已经被更新过，则跳过
        if (d &gt; dist[u])
        {
            continue;
        }

        // 遍历所有邻接点
        for (auto &amp;#x26;edge : adj[u])
        {
            int v = edge.second;
            int weight = edge.first;

            // 松弛操作
            if (dist[u] + weight &amp;#x3C; dist[v])
            {
                dist[v] = dist[u] + weight;
                pq.push({dist[v], v});
            }
        }
    }

    // 打印从源点到所有顶点的最短距离
    std::cout &amp;#x3C;&amp;#x3C; &quot;Dijkstra 算法的结果：&quot; &amp;#x3C;&amp;#x3C; std::endl;
    for (int i = 0; i &amp;#x3C; V; ++i)
    {
        if (dist[i] == INF)
        {
            std::cout &amp;#x3C;&amp;#x3C; &quot;顶点 &quot; &amp;#x3C;&amp;#x3C; startNode &amp;#x3C;&amp;#x3C; &quot; -&gt; &quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot;: No path&quot; &amp;#x3C;&amp;#x3C; std::endl;
        }
        else
        {
            std::cout &amp;#x3C;&amp;#x3C; &quot;顶点 &quot; &amp;#x3C;&amp;#x3C; startNode &amp;#x3C;&amp;#x3C; &quot; -&gt; &quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot;: &quot; &amp;#x3C;&amp;#x3C; dist[i] &amp;#x3C;&amp;#x3C; std::endl;
        }
    }
}

int main()
{
    int V = 7; // 顶点数
    int startNode = 0;

    std::vector&amp;#x3C;Edge&gt; edges =
        {
            {0, 1, 8},
            {0, 2, 12},
            {0, 3, 19},
            {0, 4, 21},
            {1, 2, 13},
            {1, 3, 4},
            {1, 4, 5},
            {1, 5, 5},
            {2, 5, 21},
            {3, 4, 19},
            {3, 6, 11},
            {5, 3, 9},
            {5, 6, 3}};

    dijkstra(V, startNode, edges);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Dijkstra 算法的结果：
顶点 0 -&gt; 0: 0
顶点 0 -&gt; 1: 8
顶点 0 -&gt; 2: 12
顶点 0 -&gt; 3: 12
顶点 0 -&gt; 4: 13
顶点 0 -&gt; 5: 13
顶点 0 -&gt; 6: 16
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动画演示过程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/%E5%8A%A8%E7%94%BB1.BQ1hNw5b_1z6gY5.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3. Bellman-Ford 算法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Bellman-Ford 算法（贝尔曼-福特算法）&lt;/strong&gt; 同样用于解决单源最短路径问题，但它比 Dijkstra 更强大，因为它可以处理带负权边的图，并且能够检测负权环。&lt;/p&gt;
&lt;p&gt;Bellman-Ford 的核心思想是动态规划。它通过对图中所有的边进行 &lt;code&gt;V-1&lt;/code&gt; 轮松弛操作来逐步逼近最短路径。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;经过第 1 轮松弛，可以找到从源点出发、最多经过 1 条边的最短路径。&lt;/li&gt;
&lt;li&gt;经过第 2 轮松弛，可以找到从源点出发、最多经过 2 条边的最短路径。&lt;/li&gt;
&lt;li&gt;经过 &lt;code&gt;V-1&lt;/code&gt; 轮松弛，可以找到从源点出发、最多经过 &lt;code&gt;V-1&lt;/code&gt; 条边的最短路径。因为在一个不包含负权环的图中，任意两点间的最短路径最多包含 &lt;code&gt;V-1&lt;/code&gt; 条边。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;算法步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;创建一个距离数组 &lt;code&gt;dist&lt;/code&gt;，将源点 &lt;code&gt;startNode&lt;/code&gt; 的距离 &lt;code&gt;dist[startNode]&lt;/code&gt; 初始化为 &lt;code&gt;0&lt;/code&gt;，其他所有顶点的 &lt;code&gt;dist&lt;/code&gt; 初始化为无穷大 (&lt;code&gt;INF&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代松弛&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;重复 &lt;code&gt;V-1&lt;/code&gt; 次以下操作：&lt;/li&gt;
&lt;li&gt;遍历图中的每一条边 &lt;code&gt;(u, v)&lt;/code&gt;，其权重为 &lt;code&gt;w&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;松弛操作：如果 &lt;code&gt;dist[u] + w &amp;#x3C; dist[v]&lt;/code&gt;，则更新 &lt;code&gt;dist[v]&lt;/code&gt; 为 &lt;code&gt;dist[u] + w&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检测负权环&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;完成 &lt;code&gt;V-1&lt;/code&gt; 轮松弛后，再进行第 &lt;code&gt;V&lt;/code&gt; 轮松弛：&lt;/li&gt;
&lt;li&gt;遍历图中的每一条边 &lt;code&gt;(u, v)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果仍然可以进行松弛操作（即 &lt;code&gt;dist[u] + w &amp;#x3C; dist[v]&lt;/code&gt;），则说明图中存在一个从源点可达的负权环。因为在没有负权环的情况下，&lt;code&gt;V-1&lt;/code&gt; 轮后所有最短路径都已确定，不应再有更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++ 代码实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;queue&gt;
#include &amp;#x3C;limits&gt;

const int INF = std::numeric_limits&amp;#x3C;int&gt;::max(); // 代表无穷大

struct Edge
{
    int u, v, w; // 边的起点、终点和权重
};

// Bellman-Ford 算法实现
// V: 顶点数量, E: 边的数量
// edges: 边的列表
// startNode: 源点
void bellmanFord(int V, int startNode, const std::vector&amp;#x3C;Edge&gt; &amp;#x26;edges)
{
    std::vector&amp;#x3C;int&gt; dist(V, INF);
    dist[startNode] = 0;

    // 1. 进行 V-1 轮松弛操作
    for (int i = 0; i &amp;#x3C; V - 1; ++i)
    {
        for (const auto &amp;#x26;edge : edges)
        {
            if (dist[edge.u] != INF &amp;#x26;&amp;#x26; dist[edge.u] + edge.w &amp;#x3C; dist[edge.v])
            {
                dist[edge.v] = dist[edge.u] + edge.w;
            }
        }
    }

    // 2. 检测负权环
    bool hasNegativeCycle = false;
    for (const auto &amp;#x26;edge : edges)
    {
        if (dist[edge.u] != INF &amp;#x26;&amp;#x26; dist[edge.u] + edge.w &amp;#x3C; dist[edge.v])
        {
            hasNegativeCycle = true;
            break;
        }
    }

    // 3. 打印结果
    std::cout &amp;#x3C;&amp;#x3C; &quot;Bellman-Ford 算法的结果：&quot; &amp;#x3C;&amp;#x3C; std::endl;
    if (hasNegativeCycle)
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;图中包含负权重环！&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
    else
    {
        for (int i = 0; i &amp;#x3C; V; ++i)
        {
            if (dist[i] == INF)
            {
                std::cout &amp;#x3C;&amp;#x3C; &quot;顶点 &quot; &amp;#x3C;&amp;#x3C; startNode &amp;#x3C;&amp;#x3C; &quot; -&gt; &quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot;: No path&quot; &amp;#x3C;&amp;#x3C; std::endl;
            }
            else
            {
                std::cout &amp;#x3C;&amp;#x3C; &quot;顶点 &quot; &amp;#x3C;&amp;#x3C; startNode &amp;#x3C;&amp;#x3C; &quot; -&gt; &quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot;: &quot; &amp;#x3C;&amp;#x3C; dist[i] &amp;#x3C;&amp;#x3C; std::endl;
            }
        }
    }
}
int main()
{
    int V = 6; // 顶点数
    int startNode = 0;

    std::vector&amp;#x3C;Edge&gt; edges =
        {
            {0, 1, -5},
            {0, 2, 2},
            {1, 2, 3},
            {1, 3, 4},
            {1, 4, 5},
            {2, 5, 1},
            {3, 4, 2},
            {3, 5, 2},
            {4, 5, 3},
            {5, 1, -2}};

    bellmanFord(V, startNode, edges);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Bellman-Ford 算法的结果：
顶点 0 -&gt; 0: 0
顶点 0 -&gt; 1: -5
顶点 0 -&gt; 2: -2
顶点 0 -&gt; 3: -1
顶点 0 -&gt; 4: 0
顶点 0 -&gt; 5: -1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. SPFA 算法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;SPFA 算法&lt;/strong&gt;是 Bellman-Ford 算法的一种队列优化版本，同样用于解决单源最短路径问题，并且能够处理负权边和检测负权环。&lt;/p&gt;
&lt;p&gt;Bellman-Ford 算法的瓶颈在于它每一轮迭代都会盲目地尝试松弛所有的边，而实际上，只有那些在上一轮中距离被缩短了的顶点，才有可能去更新它们的邻接点。而SPFA 算法正是基于这个观察进行优化。它维护一个队列，队列中存放的是“刚刚被成功松弛过”的顶点。算法每次从队列中取出一个顶点 u，然后用 u 去松弛它的所有邻接点 v。如果 v 被成功松弛，并且 v 当前不在队列中，就将 v 加入队列。这个过程不断重复，直到队列为空。&lt;/p&gt;
&lt;p&gt;由于只有“可能引发更新”的顶点才会入队并被用于松弛操作，SPFA 在大多数情况下（尤其是稀疏图）避免了 Bellman-Ford 中的大量冗余计算，因此平均效率要高得多。&lt;/p&gt;
&lt;p&gt;算法步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化：
&lt;ul&gt;
&lt;li&gt;创建一个距离数组 &lt;code&gt;dist&lt;/code&gt;，将源点 &lt;code&gt;startNode&lt;/code&gt; 的距离 &lt;code&gt;dist[startNode]&lt;/code&gt; 初始化为 0，其他所有顶点的 &lt;code&gt;dist&lt;/code&gt; 初始化为无穷大 (&lt;code&gt;INF&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;创建一个队列 &lt;code&gt;q&lt;/code&gt;，并将源点 &lt;code&gt;startNode&lt;/code&gt; 入队。&lt;/li&gt;
&lt;li&gt;创建一个布尔数组 &lt;code&gt;in_queue&lt;/code&gt;，用于标记顶点是否在队列中。将 &lt;code&gt;in_queue[startNode]&lt;/code&gt; 设为 true。&lt;/li&gt;
&lt;li&gt;创建一个计数器数组 &lt;code&gt;count&lt;/code&gt;，&lt;code&gt;count[i]&lt;/code&gt; 记录顶点 &lt;code&gt;i&lt;/code&gt; 入队的次数，用于检测负权环。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;主循环：当队列 &lt;code&gt;q&lt;/code&gt; 不为空时：
&lt;ul&gt;
&lt;li&gt;从队首取出一个顶点 &lt;code&gt;u&lt;/code&gt;，并将其出队。将 &lt;code&gt;in_queue[u]&lt;/code&gt; 设为 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;遍历 &lt;code&gt;u&lt;/code&gt; 的所有邻接点 &lt;code&gt;v&lt;/code&gt;（权重为 &lt;code&gt;w&lt;/code&gt;）：
&lt;ul&gt;
&lt;li&gt;松弛操作：如果 &lt;code&gt;dist[u] + w &amp;#x3C; dist[v]&lt;/code&gt;，则更新 &lt;code&gt;dist[v]&lt;/code&gt; 为 &lt;code&gt;dist[u] + w&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;v&lt;/code&gt; 被成功松弛，且 &lt;code&gt;v&lt;/code&gt; 当前不在队列中：&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;v&lt;/code&gt; 入队，并设置 &lt;code&gt;in_queue[v]&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;增加 &lt;code&gt;v&lt;/code&gt; 的入队计数 &lt;code&gt;count[v]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;检测负权环：如果 &lt;code&gt;count[v]&lt;/code&gt; 大于等于图的顶点总数 &lt;code&gt;V&lt;/code&gt;，则说明从源点到 &lt;code&gt;v&lt;/code&gt; 的路径上存在一个负权环。算法可以立即终止并报告。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;结束：如果队列为空且没有检测到负权环，&lt;code&gt;dist&lt;/code&gt; 数组就包含了源点到所有其他顶点的最短路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++ 代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;queue&gt;
#include &amp;#x3C;limits&gt;

const int INF = std::numeric_limits&amp;#x3C;int&gt;::max(); // 代表无穷大

struct Edge
{
    int to;     // 边的终点
    int weight; // 边的权重
};

// SPFA 算法实现
// V: 顶点数量
// adj: 邻接表表示的图
// startNode: 源点
void spfa(int V, int startNode, const std::vector&amp;#x3C;std::vector&amp;#x3C;Edge&gt;&gt; &amp;#x26;adj)
{
    std::vector&amp;#x3C;int&gt; dist(V, INF);
    std::vector&amp;#x3C;int&gt; inQueueCount(V, 0); // 记录每个顶点入队的次数
    std::vector&amp;#x3C;bool&gt; inQueue(V, false); // 标记顶点是否在队列中
    std::queue&amp;#x3C;int&gt; q;

    dist[startNode] = 0;
    q.push(startNode);
    inQueue[startNode] = true;
    inQueueCount[startNode]++;

    bool hasNegativeCycle = false;

    while (!q.empty())
    {
        int u = q.front();
        q.pop();
        inQueue[u] = false;

        for (const auto &amp;#x26;edge : adj[u])
        {
            int v = edge.to;
            int w = edge.weight;

            if (dist[u] != INF &amp;#x26;&amp;#x26; dist[u] + w &amp;#x3C; dist[v])
            {
                dist[v] = dist[u] + w;
                if (!inQueue[v])
                {
                    q.push(v);
                    inQueue[v] = true;
                    inQueueCount[v]++;
                    // 如果一个顶点入队次数超过 V，则存在负权环
                    if (inQueueCount[v] &gt; V)
                    {
                        hasNegativeCycle = true;
                        goto end_loop; // 跳出外层循环
                    }
                }
            }
        }
    }

end_loop:;

    // 3. 打印结果
    std::cout &amp;#x3C;&amp;#x3C; &quot;SPFA 算法的结果：&quot; &amp;#x3C;&amp;#x3C; std::endl;
    if (hasNegativeCycle)
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;图中包含负权重环！&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
    else
    {
        for (int i = 0; i &amp;#x3C; V; ++i)
        {
            if (dist[i] == INF)
            {
                std::cout &amp;#x3C;&amp;#x3C; &quot;顶点 &quot; &amp;#x3C;&amp;#x3C; startNode &amp;#x3C;&amp;#x3C; &quot; -&gt; &quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot;: No path&quot; &amp;#x3C;&amp;#x3C; std::endl;
            }
            else
            {
                std::cout &amp;#x3C;&amp;#x3C; &quot;顶点 &quot; &amp;#x3C;&amp;#x3C; startNode &amp;#x3C;&amp;#x3C; &quot; -&gt; &quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot;: &quot; &amp;#x3C;&amp;#x3C; dist[i] &amp;#x3C;&amp;#x3C; std::endl;
            }
        }
    }
}

int main()
{
    int V = 6; // 顶点数
    int startNode = 0;

    // 为了使用 SPFA，最好将边列表转换为邻接表
    std::vector&amp;#x3C;std::vector&amp;#x3C;Edge&gt;&gt; adj(V);
    std::vector&amp;#x3C;std::pair&amp;#x3C;int, std::pair&amp;#x3C;int, int&gt;&gt;&gt; edges =
        {
            {0, {1, -5}},
            {0, {2, 2}},
            {1, {2, 3}},
            {1, {3, 4}},
            {1, {4, 5}},
            {2, {5, 1}},
            {3, {4, 2}},
            {3, {5, 2}},
            {4, {5, 3}},
            {5, {1, -2}}};

    for (const auto &amp;#x26;edge_info : edges)
    {
        int u = edge_info.first;
        int v = edge_info.second.first;
        int w = edge_info.second.second;
        adj[u].push_back({v, w});
    }

    spfa(V, startNode, adj);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;SPFA 算法的结果：
顶点 0 -&gt; 0: 0
顶点 0 -&gt; 1: -5
顶点 0 -&gt; 2: -2
顶点 0 -&gt; 3: -1
顶点 0 -&gt; 4: 0
顶点 0 -&gt; 5: -1
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>并查集</title><link>https://liangbm3.site/blog/bing-cha-ji</link><guid isPermaLink="true">https://liangbm3.site/blog/bing-cha-ji</guid><description>介绍一下图论中的并查集</description><pubDate>Tue, 08 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;1. 简介&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;并查集（Disjoint Set Union, DSU）&lt;/strong&gt; 是一种用来高效处理一些不相交集合的合并及查找问题的数据结构，它可以高效地完成两件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查找（Find）&lt;/strong&gt;：确定一个元素属于哪个集合&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合并（Union）&lt;/strong&gt;：将两个不同集合合并成一个大的集合&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为这两个核心操作，它也被称为“Union-Find”数据结构。&lt;/p&gt;
&lt;h2&gt;2. 实现&lt;/h2&gt;
&lt;p&gt;并查集的实现思路可以分为两种：&lt;strong&gt;Quick Find&lt;/strong&gt; 和 &lt;strong&gt;Quick Union&lt;/strong&gt;。这两种方式在如何组织数据以及执行 Find 和 Union 操作上有所不同。&lt;/p&gt;
&lt;h3&gt;2.1 Quick Find&lt;/h3&gt;
&lt;p&gt;Quick Find 的核心思想是用一个数组 &lt;code&gt;id[]&lt;/code&gt; 来存储每个元素所属集合的标识符。&lt;code&gt;id[i]&lt;/code&gt; 的值就是元素 i 所在集合的 &quot;名字&quot;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;：&lt;code&gt;id[i] = i&lt;/code&gt;。每个元素都属于一个以自己为标识的独立集合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Find (查找)&lt;/strong&gt;：&lt;code&gt;Find(p, q)&lt;/code&gt; 操作就是检查 &lt;code&gt;id[p]&lt;/code&gt; 是否等于 &lt;code&gt;id[q]&lt;/code&gt;。这是一个 O(1) 的操作，非常快，这也是它“Quick Find”名字的由来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Union (合并)&lt;/strong&gt;：&lt;code&gt;Union(p, q)&lt;/code&gt; 操作稍微复杂。要将 &lt;code&gt;p&lt;/code&gt; 和 &lt;code&gt;q&lt;/code&gt; 所在的集合合并，需要遍历整个 &lt;code&gt;id&lt;/code&gt; 数组。将所有与 &lt;code&gt;id[p]&lt;/code&gt; 相同的元素的集合标识符，全部修改为 &lt;code&gt;id[q]&lt;/code&gt; 的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++代码实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;vector&gt;

class QuickFind 
{
public:
    std::vector&amp;#x3C;int&gt; id;

    // 初始化并查集
    QuickFind(int size) 
    {
        id.resize(size);
        for (int i = 0; i &amp;#x3C; size; ++i) 
        {
            id[i] = i;
        }
    }

    // 查找p和q是否在同一个集合
    bool Find(int p, int q) 
    {
        return id[p] == id[q];
    }

    // 合并p和q所在的集合
    void Union(int p, int q) 
    {
        int p_id = id[p];
        int q_id = id[q];

        if (p_id == q_id) 
        {
            return;
        }

        // 遍历整个数组，将所有属于p集合的元素，归入q集合
        for (int i = 0; i &amp;#x3C; id.size(); ++i) 
        {
            if (id[i] == p_id) 
            {
                id[i] = q_id;
            }
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：&lt;code&gt;Find&lt;/code&gt; 操作极快，时间复杂度为 &lt;code&gt;O(1)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;缺点：&lt;code&gt;Union&lt;/code&gt; 操作非常慢。每次合并都需要遍历整个数组，时间复杂度为 O(N)。如果进行 N 次合并操作，总时间复杂度会达到 O($N^2$)，在处理大规模数据时效率很低。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 Quick Union&lt;/h3&gt;
&lt;p&gt;Quick Union 是主流的并查集实现方式，它将数据组织成一个森林（多棵树），每个集合是一棵树。数组 &lt;code&gt;parent[i]&lt;/code&gt; 存储的是元素 &lt;code&gt;i&lt;/code&gt; 的父节点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;：&lt;code&gt;parent[i] = i&lt;/code&gt;。每个元素都是一棵单节点树的根。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Find (查找)&lt;/strong&gt;：&lt;code&gt;find(p)&lt;/code&gt; 操作是从节点 &lt;code&gt;p&lt;/code&gt; 开始，沿着父节点指针一路向上，直到找到根节点（即 &lt;code&gt;parent[i] == i&lt;/code&gt; 的节点）。这个根节点就是集合的代表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Union (合并)&lt;/strong&gt;：&lt;code&gt;union(p, q)&lt;/code&gt; 操作是先分别找到 &lt;code&gt;p&lt;/code&gt; 和 &lt;code&gt;q&lt;/code&gt; 的根节点 &lt;code&gt;rootP&lt;/code&gt; 和 &lt;code&gt;rootQ&lt;/code&gt;。如果它们不相同，只需将一棵树的根节点设置为另一棵树的子节点即可。例如，&lt;code&gt;parent[rootP] = rootQ&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++ 代码实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;vector&gt;

class QuickUnion 
{
public:
    std::vector&amp;#x3C;int&gt; parent;

    // 初始化并查集
    QuickUnion(int size) 
    {
        parent.resize(size);
        for (int i = 0; i &amp;#x3C; size; ++i) 
        {
            parent[i] = i;
        }
    }

    // 查找p的根节点
    int Find(int p) 
    {
        while (p != parent[p]) 
        {
            p = parent[p];
        }
        return p;
    }

    // 合并p和q所在的集合
    void Union(int p, int q) 
    {
        int rootP = Find(p);
        int rootQ = Find(q);

        if (rootP != rootQ) 
        {
            parent[rootP] = rootQ;
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：&lt;code&gt;Union&lt;/code&gt; 操作非常快。只需要找到两个根节点并修改一次指针，时间复杂度主要取决于 &lt;code&gt;Find&lt;/code&gt; 操作。&lt;/li&gt;
&lt;li&gt;缺点：&lt;code&gt;Find&lt;/code&gt; 操作可能很慢。在最坏的情况下，树可能退化成一条长链，导致 &lt;code&gt;Find&lt;/code&gt; 的时间复杂度达到 O(N)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看到虽然 Quick Union 解决了 Union 慢的问题，但引入了 Find 可能慢的新问题。为了解决 Find 的瓶颈，通常会引入&lt;strong&gt;路径压缩 (Path Compression)&lt;/strong&gt; 和 &lt;strong&gt;按秩/大小合并 (Union by Rank/Size)&lt;/strong&gt; 这两大优化。&lt;/p&gt;
&lt;h3&gt;2.3 路径压缩&lt;/h3&gt;
&lt;p&gt;路径压缩是在 Find 操作中进行优化的，核心思想是当我们要查找一个节点 &lt;code&gt;x&lt;/code&gt; 的根节点时，我们会从 &lt;code&gt;x&lt;/code&gt; 出发，沿着父节点指针一路向上，直到找到根。在这个过程中，我们经过了一条路径。路径压缩的精髓在于：在找到根之后，将这条路径上的所有节点都直接连接到根节点上。下次再查找这条路径上的任何一个节点时，只需一步就能直接访问到根节点。这极大地降低了树的高度。&lt;/p&gt;
&lt;p&gt;这通常是用递归来实现的，C++ 代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 查找p的根节点，并进行路径压缩
int Find(int p) 
{
    if (parent[p] != p) 
    {
        // 递归向上查找根节点，
        // 并且在回溯时，将p的父节点直接设置为最终的根节点
        parent[p] = Find(parent[p]);
    }
    return parent[p];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 按秩合并/按大小合并&lt;/h3&gt;
&lt;p&gt;按秩合并是在 Union 操作中进行的，核心思想是为每个集合（即每棵树的根节点）维护一个叫做“秩” (rank) 的属性。rank 可以理解为树的高度的上界。在合并两个集合时，总是将秩较小的树的根节点，连接到秩较大的树的根节点上。&lt;/p&gt;
&lt;p&gt;按大小合并与按秩合并非常相似，但维护的不是 rank，而是每个集合中元素的数量 (size)。在合并时，总是将节点数较少的树，合并到节点数较多的树上。&lt;/p&gt;
&lt;p&gt;最终使用两种优化方式的并查集的 C++ 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;vector&gt;

class UnionFind 
{
public:
    std::vector&amp;#x3C;int&gt; parent;
    std::vector&amp;#x3C;int&gt; size;

    // 初始化并查集
    UnionFind(int n) 
    {
        parent.resize(n);
        size.resize(n, 1);
        for (int i = 0; i &amp;#x3C; n; ++i) 
        {
            parent[i] = i;
        }
    }

    // 查找元素i的根节点，并进行路径压缩
    int find(int i) {
        if (parent[i] == i) 
        {
            return i;
        }
        // 递归查找根，并将路径上所有节点直接指向根
        parent[i] = find(parent[i]);
        return parent[i];
    }

    // 按大小合并元素i和j所在的集合
    void unionElements(int i, int j) 
    {
        int rootI = find(i);
        int rootJ = find(j);

        if (rootI != rootJ) 
        {
            // 按大小合并：将小树合并到大树上
            if (size[rootI] &amp;#x3C; size[rootJ]) 
            {
                parent[rootI] = rootJ;
                size[rootJ] += size[rootI];
            } 
            else 
            {
                parent[rootJ] = rootI;
                size[rootI] += size[rootJ];
            }
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>最小生成树</title><link>https://liangbm3.site/blog/zui-xiao-sheng-cheng-shu</link><guid isPermaLink="true">https://liangbm3.site/blog/zui-xiao-sheng-cheng-shu</guid><description>介绍最小生成树中的 Kruskal 算法和 Prim 算法</description><pubDate>Tue, 08 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;1. 简介&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;最小生成树（Minimum Spanning Tree，简称 MST）&lt;/strong&gt; 是图论中的一个重要概念，主要应用于&lt;strong&gt;带权无向连通图&lt;/strong&gt;中。给定一个带权无向连通图，它的最小生成树包含图中所有的顶点，但只包含一部分边，并且满足以下三个条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是一棵树：这意味着图中没有任何环路&lt;/li&gt;
&lt;li&gt;是一棵生成树：这意味着包含图中的所有顶点&lt;/li&gt;
&lt;li&gt;权重之和最小：在所有可能的生成树中，它的所有边的权重之和是最小的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如对于下面的带权无向连通图：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;它的最小生成树为：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;最小生成树有两个非常重要的性质，是构建算法的基础：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;切割性质（Cut Property）&lt;/strong&gt;：将图中的所有顶点分成两个不相交的集合。在所有横跨这两个集合的边中（即一个端点在一个集合，另一个端点在另一个集合），权重最小的那条边，必定属于图的某一个最小生成树。例如按蓝色这条线切割，权重为5这条边肯定属于最小生成树：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;环路性质（Cycle Property）&lt;/strong&gt;：对于图中的任意一个环路。这个环路中权重最大的那条边，一定不属于任何一个最小生成树。例如对于红色这个环路，权重为4这条边肯定不属于最小生成树：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 应用场景&lt;/h2&gt;
&lt;p&gt;最小生成树在日常有着广泛的应用，主要用于解决如何以最低成本连接所有点的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;网络设计&lt;/strong&gt;：铺设通信电缆、输水管道、电网、公路网等，目标是覆盖所有地点且总长度或成本最低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;聚类分析&lt;/strong&gt;：在数据科学中，可以用来帮助识别数据点簇。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;电路设计&lt;/strong&gt;：在设计电路板时，确保所有引脚都连接起来，并且使用的导线总长度最短。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;近似算法&lt;/strong&gt;：在解决一些更复杂的问题（如旅行商问题）时，最小生成树可以作为一个很好的近似解或解决过程中的一个步骤。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 经典算法&lt;/h2&gt;
&lt;p&gt;寻找最小生成树主要有三种经典算法，分别是：&lt;strong&gt;Prim 算法&lt;/strong&gt;、&lt;strong&gt;Kruskal 算法&lt;/strong&gt;和 &lt;strong&gt;Boruvka 算法&lt;/strong&gt;。其中 Boruvka 的并行友好特性使其在高性能计算和处理大规模图数据时具有不可替代的优势。我们这里主要讨论的是Prim 算法和Kruskal 算法，它们两个都是属于贪心算法，即在每一步都做出当前的最优选择，最终得到全局最优解。&lt;/p&gt;
&lt;h3&gt;3.1 Kruskal 算法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kruskal 算法（克鲁斯卡尔算法）&lt;/strong&gt; 的思路是：从权重最小的边开始，依次考察所有边。如果当前这条边连接的两个顶点还没有被连通（即加入这条边不会形成环路），就将它加入到最小生成树中。重复这个过程，直到加入了 n-1 条边为止（对于一个有 n 个顶点的图，其生成树恰好有 n-1 条边）。具体步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将图中所有的边按照权重从小到大进行排序，然后创建一个只包含所有顶点但没有边的图，每个顶点初始时都各自属于一个独立的集合。&lt;/li&gt;
&lt;li&gt;从排序好的边中，依次取出权重最小的边。&lt;/li&gt;
&lt;li&gt;判断这条边的两个顶点是否属于同一个集合（如果属于，说明它们已经连通，加入这条边会形成环路）。属于则跳过，不属于则加入到最小生成树中&lt;/li&gt;
&lt;li&gt;重复步骤4，直到最小生成树中有 n-1 条边。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Kruskal 算法的 C++ 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;algorithm&gt;
#include &amp;#x3C;numeric&gt;

// 定义边的结构体
struct Edge
{
    int src, dest, weight;
};

// 并查集结构体
struct UnionFind
{
    std::vector&amp;#x3C;int&gt; parent;
    UnionFind(int n)
    {
        parent.resize(n);
        // 初始化，每个元素的父节点都是它自己
        for (int i = 0; i &amp;#x3C; n; ++i)
        {
            parent[i] = i;
        }
    }

    // 查找元素i所在集合的根节点（带路径压缩）
    int Find(int i)
    {
        if (parent[i] == i)
            return i;
        return parent[i] = Find(parent[i]);
    }

    // 合并i和j所在的集合
    void Union(int i, int j)
    {
        int root_i = Find(i);
        int root_j = Find(j);
        if (root_i != root_j)
        {
            parent[root_i] = root_j;
        }
    }
};

// 比较函数，用于排序
bool compareEdges(const Edge &amp;#x26;a, const Edge &amp;#x26;b)
{
    return a.weight &amp;#x3C; b.weight;
}

// Kruskal算法主函数
void kruskalMST(int V, std::vector&amp;#x3C;Edge&gt; &amp;#x26;edges)
{
    // 1. 按权重对所有边进行升序排序
    std::sort(edges.begin(), edges.end(), compareEdges);

    UnionFind union_find(V);
    std::vector&amp;#x3C;Edge&gt; resultMST;
    int totalWeight = 0;

    // 2. 遍历排序后的边
    for (const auto &amp;#x26;edge : edges)
    {
        int u = edge.src;
        int v = edge.dest;

        // 3. 如果加入这条边不会形成环路
        if (union_find.Find(u) != union_find.Find(v))
        {
            // 将这条边加入结果集
            resultMST.push_back(edge);
            totalWeight += edge.weight;

            // 合并两个顶点的集合
            union_find.Union(u, v);
        }

        // 当MST中的边数达到 V-1 时，可以提前结束
        if (resultMST.size() == V - 1)
            break;
    }

    // 打印结果
    std::cout &amp;#x3C;&amp;#x3C; &quot;Kruskal 算法找到的最小生成树如下:&quot; &amp;#x3C;&amp;#x3C; std::endl;
    for (const auto &amp;#x26;edge : resultMST)
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;边 &quot; &amp;#x3C;&amp;#x3C; edge.src &amp;#x3C;&amp;#x3C; &quot; - &quot; &amp;#x3C;&amp;#x3C; edge.dest &amp;#x3C;&amp;#x3C; &quot;   权重: &quot; &amp;#x3C;&amp;#x3C; edge.weight &amp;#x3C;&amp;#x3C; std::endl;
    }
    std::cout &amp;#x3C;&amp;#x3C; &quot;最小生成树的总权重: &quot; &amp;#x3C;&amp;#x3C; totalWeight &amp;#x3C;&amp;#x3C; std::endl;
}

int main()
{
    int V = 5; // 顶点数
    std::vector&amp;#x3C;Edge&gt; edges =
        {
            {0, 1, 2},
            {0, 3, 6},
            {1, 2, 3},
            {1, 3, 8},
            {1, 4, 5},
            {2, 4, 7},
            {3, 4, 9}
        };

    kruskalMST(V, edges);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;算法输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Kruskal 算法找到的最小生成树如下:
边 0 - 1   权重: 2
边 1 - 2   权重: 3
边 1 - 4   权重: 5
边 0 - 3   权重: 6
最小生成树的总权重: 16
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动画演示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/%E5%8A%A8%E7%94%BB1.D7TsD15S_2sp3SA.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;3.2 Prim 算法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Prim 算法（普里姆算法）&lt;/strong&gt; 的思路是：从任意一个顶点开始，逐步扩大一棵正在生长的树。在每一步，都寻找一条连接树内顶点和树外顶点的、权重最小的边，并将这条边和它所连接的树外顶点加入到树中。重复这个过程，直到所有顶点都被加入。具体步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选择任意一个顶点作为起始点，将其加入到一个集合 U 中（表示已经是最小生成树的一部分）。&lt;/li&gt;
&lt;li&gt;寻找所有一个端点在集合 U 中，另一个端点不在 U 中的边。在这些边中，找到权重最小的那一条。&lt;/li&gt;
&lt;li&gt;将这条权重最小的边和它连接的那个不在 U 中的顶点加入到最小生成树和集合 U 中。&lt;/li&gt;
&lt;li&gt;重复步骤2-3，直到所有顶点都加入了集合 U&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Prim 算法的 C++ 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;queue&gt;
#include &amp;#x3C;limits&gt;

// 定义一个类型别名，方便使用
// pair&amp;#x3C;权重, 顶点&gt;
using iPair = std::pair&amp;#x3C;int, int&gt;;

// Prim算法主函数
void primMST(int V, const std::vector&amp;#x3C;std::vector&amp;#x3C;iPair&gt;&gt; &amp;#x26;adj)
{
    // 使用优先队列作为最小堆，存储 {权重, 顶点}
    // std::greater 使其成为最小堆
    // 优先队列会使用pair的第一个元素（权重）进行排序，如果第一个元素相同再排序第二个元素（顶点）
    std::priority_queue&amp;#x3C;iPair, std::vector&amp;#x3C;iPair&gt;, std::greater&amp;#x3C;iPair&gt;&gt; pq;

    // 选择顶点0作为起始点
    int src = 0;

    // 存储连接到MST的最小权重，初始化为无穷大
    // 如果为无穷大，表示这个权重对应的边的两个顶点都没有在最小生成树中
    std::vector&amp;#x3C;int&gt; minWeight(V, std::numeric_limits&amp;#x3C;int&gt;::max());
    // 存储父节点，用于构建最小生成树（MST），便于打印输出结果
    std::vector&amp;#x3C;int&gt; parent(V, -1);
    // 标记顶点是否访问过
    std::vector&amp;#x3C;bool&gt; visited(V, false);

    // 初始化起始点
    pq.push({0, src});
    minWeight[src] = 0;

    int totalWeight = 0;

    while (!pq.empty())
    {
        // 从优先队列中取出权重最小的顶点
        int u = pq.top().second;
        // 取出顶点的权重
        int w = pq.top().first;
        pq.pop();

        // 如果顶点已经处理过，则跳过，防止环路
        if (visited[u])
        {
            continue;
        }

        // 将顶点标记为已访问
        visited[u] = true;
        totalWeight += w;

        // 遍历u的所有邻接点
        for (const auto &amp;#x26;edge : adj[u])
        {
            int weight = edge.first;
            int v = edge.second;

            // 如果v没有访问过，并且通过u连接v的权重更小
            if (!visited[v] &amp;#x26;&amp;#x26; minWeight[v] &gt; weight)
            {
                // 更新v的minWeight值和父节点
                minWeight[v] = weight;
                pq.push({weight, v});
                parent[v] = u;
            }
        }
    }

    // 打印结果
    std::cout &amp;#x3C;&amp;#x3C; &quot;Prim 算法找到的最小生成树如下:&quot; &amp;#x3C;&amp;#x3C; std::endl;
    for (int i = 1; i &amp;#x3C; V; ++i)
    {
        // minWeight[i] 就是连接 parent[i] 和 i 的边的权重
        std::cout &amp;#x3C;&amp;#x3C; &quot;边 &quot; &amp;#x3C;&amp;#x3C; parent[i] &amp;#x3C;&amp;#x3C; &quot; - &quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot;   权重: &quot; &amp;#x3C;&amp;#x3C; minWeight[i] &amp;#x3C;&amp;#x3C; std::endl;
    }
    std::cout &amp;#x3C;&amp;#x3C; &quot;最小生成树的总权重: &quot; &amp;#x3C;&amp;#x3C; totalWeight &amp;#x3C;&amp;#x3C; std::endl;
}

int main()
{
    int V = 5;
    // 使用邻接表表示图
    std::vector&amp;#x3C;std::vector&amp;#x3C;iPair&gt;&gt; adj(V);

    //使用lambda表达式构建邻接表
    auto addEdge = [&amp;#x26;](int u, int v, int w)
    {
        adj[u].push_back({w, v});
        adj[v].push_back({w, u});
    };

    addEdge(0, 1, 2);
    addEdge(0, 3, 6);
    addEdge(1, 2, 3);
    addEdge(1, 3, 8);
    addEdge(1, 4, 5);
    addEdge(2, 4, 7);
    addEdge(3, 4, 9);

    primMST(V, adj);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动画演示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/%E5%8A%A8%E7%94%BB2.CgdMVog-_Zoey1h.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>堆及其相关应用</title><link>https://liangbm3.site/blog/dui-ji-qi-xiang-guan-ying-yong</link><guid isPermaLink="true">https://liangbm3.site/blog/dui-ji-qi-xiang-guan-ying-yong</guid><description>本文主要介绍堆这种数据结构，以及堆的相关应用，包括使用堆进行排序和使用堆来实现优先队列。</description><pubDate>Mon, 07 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;1. 堆简介&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;堆&lt;/strong&gt;本质上是一棵树结构，其每个节点的键值都大于等于或者小于等于其父节点的键值。这里主要讨论的是&lt;strong&gt;二叉堆&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二叉堆&lt;/strong&gt;是一种特殊的&lt;strong&gt;完全二叉树&lt;/strong&gt;。完全二叉树是指，除了最后一层外，其他各层的节点数都达到最大，并且最后一层的节点都连续集中在左侧。除了结构上的要求，堆中的节点还需要满足&lt;strong&gt;堆序性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;最大堆（Max Heap）&lt;/strong&gt;：每个节点的值都大于或等于其子节点的值。因此，堆顶（根节点）的元素是整个堆中的最大值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最小堆（Min Heap）&lt;/strong&gt;：每个节点的值都小于或等于其子节点的值。因此，堆顶（根节点）的元素是整个堆中的最小值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最大堆：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;最小堆：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;2. 堆的存储&lt;/h2&gt;
&lt;p&gt;尽管堆是树形结构，但它通常用数组来实现高效存储，堆节点在数组中的存储顺序通常是层序遍历的顺序，因此堆中的树节点和它的子节点在数组的索引是有数学关系的。对于数组中索引为 &lt;code&gt;i&lt;/code&gt; 的节点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其父节点的索引为 &lt;code&gt;floor((i-1)/2)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;其左子节点的索引为 &lt;code&gt;2*i + 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;其右子节点的索引为 &lt;code&gt;2*i + 2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 堆的操作&lt;/h2&gt;
&lt;p&gt;堆的操作主要有两种：&lt;strong&gt;插入&lt;/strong&gt;和&lt;strong&gt;删除&lt;/strong&gt;。插入和删除后都有可能不再满足堆的定义，需要重新进行调整，使其重新满足堆的特性，这个过程叫做&lt;strong&gt;堆化&lt;/strong&gt;。堆化分为两种：&lt;strong&gt;上浮&lt;/strong&gt;和&lt;strong&gt;下沉&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;3.1 插入上浮&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;上浮&lt;/strong&gt;主要用于&lt;strong&gt;插入&lt;/strong&gt;操作中，执行插入时通常会把一个新元素添加到堆的末尾，这是它可能会比它的父节点更大（在最大堆中）或更小（在最小堆中），从而违反了堆属性。上浮操作就是将这个新元素与其父节点比较并交换，直到它找到合适的位置，恢复堆属性。以最大堆为例，插入的过程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将新元素放在数组的末尾&lt;/li&gt;
&lt;li&gt;比较新元素和它的父节点&lt;/li&gt;
&lt;li&gt;如果新元素比父节点大，则交换它们&lt;/li&gt;
&lt;li&gt;重复步骤 2 和 3，直到新元素不再比它的父节点大，或者它已经到达了堆顶&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上浮操作的 C++ 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void siftUp(vector&amp;#x3C;int&gt;&amp;#x26; heap, int idx)
{
    while (idx &gt; 0)
    {
        int parent = (idx - 1) / 2;
        if (heap[idx] &amp;#x3C;= heap[parent]) break;  // 最大堆：子 ≤ 父，则停止
        swap(heap[idx], heap[parent]);
        idx = parent;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;插入操作的 C++ 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void insert(vector&amp;#x3C;int&gt;&amp;#x26; heap, int value)
{
    heap.push_back(value);
    siftUp(heap, heap.size() - 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;插入操作的动画：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/%E5%8A%A8%E7%94%BB2.Ch5GE_Rb_Z1Iwk2q.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;3.2 删除下沉&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;下沉&lt;/strong&gt;主要用于&lt;strong&gt;删除&lt;/strong&gt;操作中，执行删除操作将某个节点元素移除后，通常会用堆的最后一个元素来填补空缺。填补元素后很可能违反堆属性。下沉操作就是将这个元素与其子节点中较大者（在最大堆中）或较小者（在最小堆中）比较并交换，直到它找到合适的位置。以最大堆为例，删除的过程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将堆的最后一个元素移动到删除位置&lt;/li&gt;
&lt;li&gt;比较当前节点和它的左右子节点，找出最大的那个子节点&lt;/li&gt;
&lt;li&gt;如果当前节点比它最大的子节点要小，则交换它们&lt;/li&gt;
&lt;li&gt;重复步骤 2 和 3，顺着交换路径继续向下比较和交换，直到当前节点不再比它的任何子节点小，或者它已经成为一个叶子节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下沉操作的 C++ 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void siftDown(vector&amp;#x3C;int&gt;&amp;#x26; heap, int idx)
{
    int n = heap.size();
    while (true)
    {
        int left = 2 * idx + 1;
        int right = 2 * idx + 2;
        int largest = idx;
        if (left &amp;#x3C; n &amp;#x26;&amp;#x26; heap[left] &gt; heap[largest]) largest = left;
        if (right &amp;#x3C; n &amp;#x26;&amp;#x26; heap[right] &gt; heap[largest]) largest = right;
        if (largest == idx) break;
        swap(heap[idx], heap[largest]);
        idx = largest;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;弹出（删除）堆顶点的 C++ 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int popHeap(vector&amp;#x3C;int&gt;&amp;#x26; heap)
{
    if (heap.empty()) throw out_of_range(&quot;Heap is empty&quot;);
    int top = heap[0];
    heap[0] = heap.back();
    heap.pop_back();
    if (!heap.empty()) siftDown(heap, 0);
    return top;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;弹出堆顶元素的动画：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/%E5%8A%A8%E7%94%BB3.C-BJuVeN_ZnxFsO.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;4. 堆的构建&lt;/h2&gt;
&lt;p&gt;从一个无序的元素集合构建一个完整堆的过程，通常被称为建堆，构建堆通常有两种方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;逐个插入法&lt;/li&gt;
&lt;li&gt;原地建堆法&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.1 逐个插入法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;逐个插入法&lt;/strong&gt;的思路非常直观，首先初始化一个空的堆，然后遍历数组，将每个元素依次插入进堆里面，遍历完数组后即建堆完成。每次插入操作的时间复杂度是 O(log k)，其中 k 是堆中当前的元素数量。当插入 N 个元素时，总的时间复杂度为所有插入操作的总和，即 O(log 1) + O(log 2) + ... + O(log N)，这最终可以近似为 O(N log N)。&lt;/p&gt;
&lt;p&gt;C++ 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void buildHeap(vector&amp;#x3C;int&gt;&amp;#x26; heap)
{
    vector&amp;#x3C;int&gt; new_heap(heap.size());
    for(int val:heap)
    {
        insert(new_heap, val);
    }
    heap = new_heap;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建的动画如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/%E5%8A%A8%E7%94%BB4.DJnhmF5G_18875a.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;4.2 原地建堆法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原地建堆法&lt;/strong&gt;则是构建堆的标准算法，也叫 Floyd 算法，它比逐个插入法更加高效，它是一个自底向上的方法。构建的核心思想是将每一个叶子节点视为一个堆，因此只需要处理所有的非叶子节点，确保它们满足堆的属性即可。我们需要从最后一个非叶子节点开始，向前遍历直到根节点，对每个节点执行&lt;strong&gt;下沉&lt;/strong&gt;操作。&lt;/p&gt;
&lt;p&gt;算法的具体过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将所有元素直接放入一个数组中，此时它只是一个完全二叉树，不一定是堆。&lt;/li&gt;
&lt;li&gt;找到最后一个非叶子节点的索引。对于一个大小为 N 的数组，该索引是 &lt;code&gt;floor(N / 2) - 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;从这个索引开始，向前遍历到根节点（索引 0）。&lt;/li&gt;
&lt;li&gt;对遍历到的每个节点，执行下沉（siftDown）操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度分析&lt;/strong&gt;：虽然这个算法看起来像是执行了 N/2 次 siftDown 操作（每次 O(log N)），因此总的时间复杂度为 O(N log N)，但是这个直觉是错误的。更精确的数学分析表明，大部分节点的下沉路径都很短。越靠近底部的节点越多，但它们下沉的距离也越短。总的操作数可以被证明是一个与 N 呈线性关系的级数，因此时间复杂度为 O(N)。&lt;/p&gt;
&lt;p&gt;C++ 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void buildHeap(vector&amp;#x3C;int&gt;&amp;#x26; heap)
{
    int n = heap.size();
    // 最后一个非叶节点索引 = (n/2) - 1
    for (int i = n / 2 - 1; i &gt;= 0; --i)
    {
        siftDown(heap, i);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建动画如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/%E5%8A%A8%E7%94%BB5.BiqO_XNz_25krvM.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;int main()
{
std::vector data = {3, 19, 1, 14, 8, 7};&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用 std::make_heap 将 vector 原地转换成一个最大堆
// 它接受一对迭代器，定义了要操作的范围
std::make_heap(data.begin(), data.end());

std::cout &amp;#x3C;&amp;#x3C; &quot;Heap built with std::make_heap: &quot;;
for (int val : data) 
{
    std::cout &amp;#x3C;&amp;#x3C; val &amp;#x3C;&amp;#x3C; &quot; &quot;;
}
std::cout &amp;#x3C;&amp;#x3C; std::endl;
// 输出可能为：19 14 7 3 8 1 (或其他合法的最大堆排列)

return 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;/Aside&gt;

## 5. 堆排序

**堆排序**是借助堆结构所实现的排序算法，通常使用大顶堆来实现升序排序，小顶堆来实现降序排序。核心思想是将数组转化为大顶堆（小顶堆）后，重复从大顶堆（小顶堆）中取出数值最大（数值最小）的节点，并让剩余的堆结构继续维持大顶堆（小顶堆）的性质。以大顶堆为例，堆排序算法的步骤如下：
- **构建大顶堆**：从最后一个非叶子节点开始，向前逐个对节点进行“下沉”调整，确保以该节点为根的子树满足最大堆的性质。经过一系列调整后，整个数组就变成了一个最大堆。
- **执行排序**：循环地将堆顶元素（当前最大值）与堆末尾的元素交换，然后缩小堆的范围并重新调整堆，使其继续满足最大堆的性质。
- 当所有元素都从堆中移除后，数组就变成了一个有序的升序序列。

**时间复杂度**：堆排序的时间复杂度为 O(nlogn)。其中，建堆的过程需要 O(n) 的时间，而之后每次调整堆需要 O(logn) 的时间，共进行 n-1 次。

**空间复杂度**：堆排序是一种原地排序算法，它只需要一个常数级别的额外空间来存储临时变量，因此空间复杂度为 O(1)。

C++ 实现如下：
```cpp
class maxHeap
{
private:
    vector&amp;#x3C;int&gt; heap;
    void siftdown(int idx,int n)
    {
        while(true)
        {
            int left=2*idx+1;
            int right=2*idx+2;
            int largest=idx;
            if(left&amp;#x3C;n &amp;#x26;&amp;#x26; heap[left]&gt;heap[largest]) largest=left;
            if(right&amp;#x3C;n &amp;#x26;&amp;#x26; heap[right]&gt;heap[largest]) largest=right;
            if(largest==idx) break;
            swap(heap[idx],heap[largest]);
            idx=largest;
        }
    }
public:
    void buildMaxHeap(const vector&amp;#x3C;int&gt;&amp;#x26; arr)
    {
        heap=arr;
        int n=heap.size();
        for(int i=(n-2)/2;i&gt;=0;i--)
        {
            siftdown(i,n);
        }
    }
    //堆排序
    vector&amp;#x3C;int&gt; maxHeapSort(const vector&amp;#x3C;int&gt;&amp;#x26; nums)
    {
        //建堆
        buildMaxHeap(nums);
        int n=heap.size();
        //从最后一个元素开始
        for(int i=n-1;i&gt;0;i--)
        {
            //将堆顶元素放在尾部
            swap(heap[0],heap[i]);
            //缩小堆结构的范围并进行下沉
            siftdown(0,i);
        }
        return heap;
    }
};

class Solution
{
public:
    vector&amp;#x3C;int&gt; sort(vector&amp;#x3C;int&gt; nums)
    {
        return maxHeap().maxHeapSort(nums);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 优先队列&lt;/h2&gt;
&lt;h3&gt;6.1 简介&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;优先队列&lt;/strong&gt;是一种特殊的队列，优先队列的元素都会被赋予一个优先级，元素的出队顺序由优先级决定：高优先级的元素先于低优先级的元素出队。如果多个元素具有相同的优先级，则它们的相对顺序没有明确规定。&lt;/p&gt;
&lt;p&gt;一个最小优先队列可以很容易地通过将元素的优先级取反来当做最大优先队列使用，反之亦然。&lt;/p&gt;
&lt;h3&gt;6.2 应用场景&lt;/h3&gt;
&lt;p&gt;优先队列具有广泛的应用场景，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;操作系统任务调度&lt;/strong&gt;: 操作系统需要管理多个同时运行的进程。优先队列被用来调度 CPU 时间，高优先级的进程（如系统进程）会比低优先级的用户进程先获得执行权。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图算法&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dijkstra 最短路径算法&lt;/strong&gt;: 在寻找图中从一个顶点到所有其他顶点的最短路径时，Dijkstra 算法使用一个最小优先队列来存储待访问的顶点。队列根据已知的从源点到该顶点的距离进行排序，每次从未访问的顶点中选择距离最短的一个进行处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prim 最小生成树算法&lt;/strong&gt;: Prim 算法使用最小优先队列来选择连接到当前生成树的、权重最小的边，从而逐步构建最小生成树。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事件驱动模拟 (Event-Driven Simulation)&lt;/strong&gt;: 在模拟系统中，事件被存储在一个以时间为优先级的最小优先队列中。模拟器总是从队列中取出时间最早的事件进行处理，处理该事件可能会触发新的未来事件加入队列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据压缩 (Huffman Coding)&lt;/strong&gt;: 在构建霍夫曼树时，使用一个最小优先队列来存储字符及其频率。算法重复地从队列中取出两个频率最低的节点，合并它们，然后将新生成的父节点放回队列，直到队列中只剩一个节点（即霍夫曼树的根）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A* 搜索算法&lt;/strong&gt;: 这是广泛用于寻路和图遍历的算法。它使用一个优先队列来决定下一步要探索的节点，其优先级由“已付出的代价”（g-score）和“到目标的预估代价”（h-score）共同决定。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3 实现&lt;/h3&gt;
&lt;p&gt;优先队列的实现方法有很多种：&lt;strong&gt;基于数组或链表的实现&lt;/strong&gt;方式如下：
- &lt;strong&gt;无序数组或链表&lt;/strong&gt;： 插入操作非常简单，只需将新元素添加到数组末尾或链表头部即可，时间复杂度为 O(1)。但要查找并删除最高优先级的元素，则需要遍历整个数据结构，找到目标元素，这需要 O(n) 的时间。
- &lt;strong&gt;有序数组或链表&lt;/strong&gt;： 如果我们维持一个按优先级排序的数组或链表，那么查看和删除最高优先级的元素（位于数组或链表的一端）将非常快，仅需 O(1)。然而，为了在插入新元素时保持有序性，可能需要移动大量元素，导致插入操作的时间复杂度为 O(n)。&lt;/p&gt;
&lt;p&gt;更加常用的且高效是&lt;strong&gt;基于二叉堆的实现&lt;/strong&gt;方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;插入&lt;/strong&gt;: 将新元素添加到堆的末尾，然后通过与其父节点比较和交换，一路“上浮”到正确的位置以维持堆属性。这个过程最多移动树的高度，即 O(logn)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;删除&lt;/strong&gt;: 将根节点（最高优先级元素）与最后一个元素交换，然后移除根节点。接着，新的根节点通过与子节点比较和交换，一路“下沉”到正确的位置以恢复堆属性。这个过程的时间复杂度也是 O(logn)。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.4 C++ 中的优先队列&lt;/h3&gt;
&lt;p&gt;在 C++ 中，&lt;code&gt;&amp;#x3C;priority_queue&gt;&lt;/code&gt; 是标准模板库（STL）的一个容器适配器，，用于实现优先队列。默认情况下，队列中最大的元素拥有最高的优先级。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::priority_queue&lt;/code&gt; 是一个模板类，其声明格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;template &amp;#x3C;
    class T, // 元素类型
    class Container = std::vector&amp;#x3C;T&gt;, // 底层容器类型，必须支持随机访问迭代器
    class Compare = std::less&amp;#x3C;T&gt; // 比较函数对象，决定优先级
&gt; class priority_queue;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常用操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;empty()&lt;/code&gt;: 检查队列是否为空。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt;: 返回队列中的元素数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;top()&lt;/code&gt;: 返回队列顶部的元素（不删除它）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push()&lt;/code&gt;: 向队列添加一个元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop()&lt;/code&gt;: 移除队列顶部的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++ 的 &lt;code&gt;priority_queue&lt;/code&gt; 默认使用 &lt;code&gt;std::less&amp;#x3C;T&gt;&lt;/code&gt; 比较器，队列中最大的元素具有最高优先级，如果要最小的元素具有最低优先级，可以传入&lt;code&gt;std::greater&amp;#x3C;T&gt;&lt;/code&gt;比较器。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::priority_queue&amp;#x3C;int, std::vector&amp;#x3C;int&gt;, std::greater&amp;#x3C;int&gt;&gt; min_heap;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是在优先队列中存储自定义的结构体或类时，则需要告诉 &lt;code&gt;std::priority_queue&lt;/code&gt; 如何比较它们。有以下几种常用方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重载 &lt;code&gt;&amp;#x3C;&lt;/code&gt; 运算符。这种方法比较直接，例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Task 
{
    int priority;
    std::string name;

    // 重载 &amp;#x3C; 运算符，priority 值越大，优先级越高
    bool operator&amp;#x3C;(const Task&amp;#x26; other) const {
        return this-&gt;priority &amp;#x3C; other.priority;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;提供自定义比较函数对象。这种方法更加灵活，可以不修改类本身，例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Task 
{
    int priority;
    std::string name;
};

// 自定义比较函数对象
struct CompareTask 
{
    // 返回 true 表示 a 的优先级比 b 低
    bool operator()(const Task&amp;#x26; a, const Task&amp;#x26; b) 
    {
        return a.priority &amp;#x3C; b.priority; // 最大堆
        // return a.priority &gt; b.priority; // 若要最小堆，则反向比较
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;使用 Lambda 表达式。对于局部的、一次性的比较逻辑，使用 Lambda 表达式非常方便。例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Point 
{
    int x, y;
    int dist_from_origin() const { return x*x + y*y; }
};

auto compare_points = [](const Point&amp;#x26; a, const Point&amp;#x26; b) {
    // 距离原点越近，优先级越高 (最小堆)
    return a.dist_from_origin() &gt; b.dist_from_origin();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>拓扑排序</title><link>https://liangbm3.site/blog/tuo-bu-pai-xu</link><guid isPermaLink="true">https://liangbm3.site/blog/tuo-bu-pai-xu</guid><description>介绍一下实现拓扑排序以及实现拓扑排序的两种算法</description><pubDate>Sun, 06 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;1. 拓扑排序简介&lt;/h2&gt;
&lt;p&gt;拓扑排序是对 &lt;strong&gt;有向无环图 (Directed Acyclic Graph, DAG)&lt;/strong&gt; 的顶点进行的一种线性排序。简单来说，就是将一个 DAG 中所有的顶点排成一个线性序列，使得图中任意一对顶点 &lt;code&gt;u&lt;/code&gt; 和 &lt;code&gt;v&lt;/code&gt;，若存在一条从 &lt;code&gt;u&lt;/code&gt; 指向 &lt;code&gt;v&lt;/code&gt; 的边 (&lt;code&gt;u -&gt; v&lt;/code&gt;)，那么在这个序列中 &lt;code&gt;u&lt;/code&gt; 必须出现在 &lt;code&gt;v&lt;/code&gt; 的前面。&lt;/p&gt;
&lt;p&gt;拓扑排序主要用于解决依赖关系和执行顺序的问题，主要的应用场景如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;任务调度或工作流&lt;/strong&gt;：在一个复杂的项目中，很多任务之间有依赖关系。例如，编译代码时的文件依赖关系 (Makefile)。拓扑排序可以确定任务的执行顺序，确保依赖的任务先完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;课程的先修关系&lt;/strong&gt;。例如必须先修完《程序设计》才能修读《数据结构》，拓扑排序可以确定一个满足所有先修课要求的学习计划。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;软件包的依赖管理&lt;/strong&gt;。在安装或卸载软件包时，系统需要解决包之间的依赖关系，例如 Linux 的 &lt;code&gt;apt-get&lt;/code&gt; 命令在安装一个包之前，会安装它的所有依赖包，这个依赖解析的过程就用到了拓扑排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 算法及实现&lt;/h2&gt;
&lt;p&gt;实现拓扑排序有两种经典的算法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kahn 算法 (基于入度的算法)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基于深度优先搜索 (DFS) 的算法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以下面的这个有向无环图为例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image.BwowPyBY_2vBYwn.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2.1 Kahn 算法&lt;/h3&gt;
&lt;p&gt;Kahn 算法的思路非常直观，就像模拟 &quot;完成任务&quot; 的过程。一个任务只有在它所有的前置任务都完成后，才能开始执行。算法步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;计算所有顶点的入度。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;入度是指有多少条边指向该顶点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;初始化一个队列，并将所有入度为 0 的顶点入队。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;入度为 0 的顶点意味着它没有任何前置依赖，可以作为起点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;循环执行以下操作，直到队列为空：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;从队列中取出一个顶点 &lt;code&gt;u&lt;/code&gt;，并将其加入到最终的拓扑排序结果序列中。&lt;/li&gt;
&lt;li&gt;遍历 &lt;code&gt;u&lt;/code&gt; 的所有邻接顶点 &lt;code&gt;v&lt;/code&gt;（即所有 &lt;code&gt;u&lt;/code&gt; 指向的顶点）。&lt;/li&gt;
&lt;li&gt;对于每一个邻接顶点 &lt;code&gt;v&lt;/code&gt;，将其入度减 1 (相当于 u 这个前置任务已经完成了)。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;v&lt;/code&gt; 的入度变为 0，则将 &lt;code&gt;v&lt;/code&gt; 入队。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查结果：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;如果最终结果序列中的顶点数量与图中顶点总数相等，则说明拓扑排序成功。&lt;/li&gt;
&lt;li&gt;如果数量不相等，则说明图中存在环，无法进行拓扑排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Kahn 算法的 C++ 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;queue&gt;
#include &amp;#x3C;string&gt;

// Kahn 算法实现拓扑排序
std::vector&amp;#x3C;int&gt; topologicalSort_Kahn(int n, const std::vector&amp;#x3C;std::vector&amp;#x3C;int&gt;&gt;&amp;#x26; adj) 
{
    // 1. 计算所有顶点的入度
    std::vector&amp;#x3C;int&gt; in_degree(n, 0);
    for (int i = 0; i &amp;#x3C; n; ++i) 
    {
        for (int neighbor : adj[i]) 
        {
            in_degree[neighbor]++;
        }
    }

    // 2. 将所有入度为 0 的顶点加入队列
    std::queue&amp;#x3C;int&gt; q;
    for (int i = 0; i &amp;#x3C; n; ++i) 
    {
        if (in_degree[i] == 0) 
        {
            q.push(i);
        }
    }

    std::vector&amp;#x3C;int&gt; result; // 存储拓扑排序结果

    // 3. 处理队列中的顶点
    while (!q.empty()) 
    {
        int u = q.front();
        q.pop();
        result.push_back(u);

        // 遍历 u 的所有邻接点，并将其入度减 1
        for (int v : adj[u]) 
        {
            in_degree[v]--;
            // 如果邻接点 v 的入度变为 0，则将其入队
            if (in_degree[v] == 0) 
            {
                q.push(v);
            }
        }
    }

    // 4. 检查结果
    // 如果结果序列中的顶点数不等于总顶点数，说明图中存在环
    if (result.size() != n) 
    {
        return {}; // 返回空向量表示失败
    }

    return result;
}

int main() 
{
    int n = 6; //节点数
    std::vector&amp;#x3C;std::vector&amp;#x3C;int&gt;&gt; adj(n);

    // 添加依赖关系 (边)
    adj[0].push_back(1); 
    adj[0].push_back(2); 
    adj[1].push_back(2); 
    adj[2].push_back(5); 
    adj[2].push_back(3);
    adj[3].push_back(4);
    adj[4].push_back(5);

    std::cout &amp;#x3C;&amp;#x3C; &quot;--- Kahn 算法 ---&quot; &amp;#x3C;&amp;#x3C; std::endl;
    std::vector&amp;#x3C;int&gt; sorted_order = topologicalSort_Kahn(n, adj);

    if (sorted_order.empty()) 
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;图中存在环，无法进行拓扑排序。&quot; &amp;#x3C;&amp;#x3C; std::endl;
    } 
    else 
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;一个可行的拓扑排序是: &quot; &amp;#x3C;&amp;#x3C; std::endl;
        for (int i = 0; i &amp;#x3C; sorted_order.size(); ++i) 
        {
            std::cout &amp;#x3C;&amp;#x3C; sorted_order[i] &amp;#x3C;&amp;#x3C; (i == sorted_order.size() - 1 ? &quot;&quot; : &quot; -&gt; &quot;);
        }
        std::cout &amp;#x3C;&amp;#x3C; std::endl;
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--- Kahn 算法 ---
一个可行的拓扑排序是: 
0 -&gt; 1 -&gt; 2 -&gt; 3 -&gt; 4 -&gt; 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;排序的动画如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/%E5%8A%A8%E7%94%BB.D5KlAFVY_17oVGf.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2.2 DFS 算法&lt;/h3&gt;
&lt;p&gt;基于 DFS 的算法主要利用了 DFS 遍历图的特性。其核心思想是，一个顶点只有在它的所有后续顶点（它能到达的所有顶点）都被访问过之后，才算真正 &quot;完成&quot; 访问。算法步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个栈 (Stack) 来存储拓扑排序的结果。&lt;/li&gt;
&lt;li&gt;创建一个集合 (Set) 或布尔数组 &lt;code&gt;visited&lt;/code&gt; 来记录已访问过的顶点。&lt;/li&gt;
&lt;li&gt;遍历图中的每一个顶点 &lt;code&gt;u&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;u&lt;/code&gt; 没有被访问过，则以 &lt;code&gt;u&lt;/code&gt; 为起点进行深度优先搜索。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;深度优先搜索 (DFS) 函数 &lt;code&gt;dfs(u)&lt;/code&gt; 的逻辑：
&lt;ul&gt;
&lt;li&gt;将当前顶点 &lt;code&gt;u&lt;/code&gt; 标记为已访问。&lt;/li&gt;
&lt;li&gt;遍历 &lt;code&gt;u&lt;/code&gt; 的所有邻接顶点 &lt;code&gt;v&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;v&lt;/code&gt; 没有被访问过，则递归调用 &lt;code&gt;dfs(v)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;当从 &lt;code&gt;u&lt;/code&gt; 出发的所有路径都探索完毕后（即所有邻接点都已被访问），将顶点 &lt;code&gt;u&lt;/code&gt; 压入栈中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最终结果：
&lt;ul&gt;
&lt;li&gt;当所有顶点都已被访问后，依次从栈中弹出所有元素，得到的序列就是拓扑排序的结果（逆序）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果想要在 DFS 中检测环，需要维护三种状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;UNVISITED&lt;/code&gt;: 未访问&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VISITING&lt;/code&gt;: 正在访问（当前递归栈中）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VISITED&lt;/code&gt;: 已访问完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果在 DFS 过程中，遇到了一个状态为 VISITING 的顶点，就说明图中存在环。&lt;/p&gt;
&lt;p&gt;DFS 算法的 C++ 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;stack&gt;
#include &amp;#x3C;string&gt;
#include &amp;#x3C;algorithm&gt;

// 顶点的访问状态
enum State { UNVISITED, VISITING, VISITED };

// DFS 辅助函数，发现环返回true，未发现则返回false
bool dfs(int u, const std::vector&amp;#x3C;std::vector&amp;#x3C;int&gt;&gt;&amp;#x26; adj, std::vector&amp;#x3C;State&gt;&amp;#x26; visited, std::stack&amp;#x3C;int&gt;&amp;#x26; s) 
{
    visited[u] = VISITING; // 标记为正在访问

    for (int v : adj[u]) 
    {
        if (visited[v] == UNVISITED) 
        {
            if (dfs(v, adj, visited, s)) 
            {
                return true; // 如果在子调用中发现了环，立即返回
            }
        } 
        else if (visited[v] == VISITING) 
        {
            // 如果访问到一个正在访问的节点，说明存在环
            return true; 
        }
    }

    visited[u] = VISITED; // 节点 u 的所有邻接点都已访问完毕
    s.push(u); // 将 u 压入栈中
    return false; // 未发现环
}

// 基于 DFS 的拓扑排序实现
std::vector&amp;#x3C;int&gt; topologicalSort_DFS(int n, const std::vector&amp;#x3C;std::vector&amp;#x3C;int&gt;&gt;&amp;#x26; adj) 
{
    std::stack&amp;#x3C;int&gt; s;
    std::vector&amp;#x3C;State&gt; visited(n, UNVISITED);
    
    // 遍历所有顶点，以处理不连通的图
    for (int i = 0; i &amp;#x3C; n; ++i) 
    {
        if (visited[i] == UNVISITED) 
        {
            if (dfs(i, adj, visited, s)) 
            {
                // 如果 dfs 函数返回 true，说明检测到了环
                return {}; // 返回空向量表示失败
            }
        }
    }

    // 从栈中弹出元素，构造拓扑序列
    std::vector&amp;#x3C;int&gt; result;
    while (!s.empty()) 
    {
        result.push_back(s.top());
        s.pop();
    }
    
    return result;
}


int main() {
    int n = 6; // 6 个节点
    std::vector&amp;#x3C;std::vector&amp;#x3C;int&gt;&gt; adj(n);

    // 添加依赖关系 (边)
    adj[0].push_back(1); 
    adj[0].push_back(2); 
    adj[1].push_back(2); 
    adj[2].push_back(3); 
    adj[2].push_back(5);
    adj[3].push_back(4);
    adj[4].push_back(5);

    std::cout &amp;#x3C;&amp;#x3C; &quot;--- DFS 算法 ---&quot; &amp;#x3C;&amp;#x3C; std::endl;
    std::vector&amp;#x3C;int&gt; sorted_order = topologicalSort_DFS(n, adj);

    if (sorted_order.empty()) 
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;图中存在环，无法进行拓扑排序。&quot; &amp;#x3C;&amp;#x3C; std::endl;
    } 
    else 
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;一个可行的拓扑排序是: &quot; &amp;#x3C;&amp;#x3C; std::endl;
        for (int i = 0; i &amp;#x3C; sorted_order.size(); ++i) 
        {
            std::cout &amp;#x3C;&amp;#x3C; sorted_order[i] &amp;#x3C;&amp;#x3C; (i == sorted_order.size() - 1 ? &quot;&quot; : &quot; -&gt; &quot;);
        }
        std::cout &amp;#x3C;&amp;#x3C; std::endl;
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--- DFS 算法 ---
一个可行的拓扑排序是: 
0 -&gt; 1 -&gt; 2 -&gt; 3 -&gt; 4 -&gt; 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;排序的动画如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/%E5%8A%A8%E7%94%BB2.CWtOCH-1_T9Txm.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3. 复杂度分析&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度&lt;/strong&gt;： 两种算法都需要遍历所有的顶点和所有的边一次，时间复杂度为 O(V+E)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;空间复杂度&lt;/strong&gt;：Kahn 算法需要一个队列和数组来存储入度，DFS 算法需要递归栈空间和使用 &lt;code&gt;visited&lt;/code&gt; 数组来记录数组的访问状态，空间复杂度都为 O(V)。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>C++ 中静态局部变量的使用场景</title><link>https://liangbm3.site/blog/c-zhong-jing-tai-ju-bu-bian-liang-de-shi-yong-chang-jing</link><guid isPermaLink="true">https://liangbm3.site/blog/c-zhong-jing-tai-ju-bu-bian-liang-de-shi-yong-chang-jing</guid><description>记录一下 C++ 中静态局部变量常见的使用场景</description><pubDate>Sat, 05 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这里之所以特地写一篇文章记录一下 C++ 中静态局部变量的应用场景，是因为静态局部变量是一种非常特殊的变量：静态局部变量的生命周期是整个程序的运行期间，它在第一次调用所在的函数时初始化，并且到程序结束时才会被销毁。它的作用域仅限于定义它的函数或代码块内部，和普通的局部变量相同。&lt;/p&gt;
&lt;h2&gt;1. 函数调用计数器&lt;/h2&gt;
&lt;p&gt;静态局部变量的一个经典应用就是&lt;strong&gt;函数调用计数器&lt;/strong&gt;，由于变量在函数调用结束后不会被销毁，我们可以用它来轻松记录一个函数被调用了多少次。这在调试的时候非常有用。示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

void someFunction() 
{
    static int callCount = 0; // 仅在第一次调用时初始化为 0
    callCount++;
    std::cout &amp;#x3C;&amp;#x3C; &quot;该函数已被调用 &quot; &amp;#x3C;&amp;#x3C; callCount &amp;#x3C;&amp;#x3C; &quot; 次。&quot; &amp;#x3C;&amp;#x3C; std::endl;
}

int main() 
{
    someFunction(); // 输出: 该函数已被调用 1 次。
    someFunction(); // 输出: 该函数已被调用 2 次。
    someFunction(); // 输出: 该函数已被调用 3 次。
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在调试的过程中，如果我们不是想知道一个函数被调用了多少次，而是想知道一个函数是否被调用过。例如我们想在一个循环调用的函数中打印一条一次性的进入消息，也可以使用静态局部变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void someFunction() 
{
    static bool first_run = true;
    if (first_run) 
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Debug: Performing one-time setup inside someFunction().\n&quot;;
        // 在这里可以放置任何你只想执行一次的调试代码或检查
        first_run = false;
    }

    // ... 函数的其余逻辑 ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 实现缓存&lt;/h2&gt;
&lt;p&gt;静态局部变量还可以用来&lt;strong&gt;实现缓存&lt;/strong&gt;，当一个函数的计算成本很高时，并且对于相同的输入总是产生相同的输出时，可以使用静态局部变量来缓存计算结果，避免重复计算。示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;map&gt;
#include &amp;#x3C;thread&gt;
#include &amp;#x3C;chrono&gt;

// 模拟一个耗时的计算
int expensiveComputation(int input) 
{
    static std::map&amp;#x3C;int, int&gt; cache; // 静态缓存

    // 检查结果是否已在缓存中
    if (cache.count(input)) 
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;从缓存中命中: &quot; &amp;#x3C;&amp;#x3C; input &amp;#x3C;&amp;#x3C; std::endl;
        return cache[input];
    }

    std::cout &amp;#x3C;&amp;#x3C; &quot;正在计算: &quot; &amp;#x3C;&amp;#x3C; input &amp;#x3C;&amp;#x3C; std::endl;
    // 模拟耗时操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
    int result = input * input; // 假设这是复杂计算
    cache[input] = result;
    return result;
}

int main() 
{
    expensiveComputation(5); // 第一次调用，会计算
    expensiveComputation(10); // 第一次调用，会计算
    expensiveComputation(5);  // 第二次调用，直接从缓存返回
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 在递归中保持状态&lt;/h2&gt;
&lt;p&gt;有时候我们在递归的过程中，需要保持某些状态，除了使用全局变量的方法，还可以使用静态局部变量，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;stdio.h&gt;

void recursiveSum(int n) 
{
    static int sum = 0;
    if (n == 0) {
        printf(&quot;Total sum: %d\n&quot;, sum);
        return;
    }
    sum += n;
    recursiveSum(n - 1);
}

int main() 
{
    recursiveSum(5); // 输出 15
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 生成唯一的 ID&lt;/h2&gt;
&lt;p&gt;在有一些场景中，我们需要递增生成一些编号作为 ID，这时候就可以使用静态局部变量，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int generateID() 
{
    static int id = 0;
    return ++id;
}

int main() 
{
    printf(&quot;%d\n&quot;, generateID());
    printf(&quot;%d\n&quot;, generateID());
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数每调用一次，就会返回一个递增的唯一 ID。&lt;/p&gt;
&lt;h2&gt;5. 有限状态机&lt;/h2&gt;
&lt;p&gt;在有一些业务场景中，我们需要使用有限状态机来处理一些流程，例如解析器、协议处理等，这就需要一个函数能够记住它上一次执行的状态，这时候可以使用静态局部变量。例如一个处理数据流的函数，需要根据上一次处理的数据块来决定如何处理当前的数据块：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;stdio.h&gt;

typedef enum 
{
    STATE_A,
    STATE_B,
    STATE_C
} State;

void process_char(char c) 
{
    static State currentState = STATE_A; // 维护当前状态

    switch (currentState) 
    {
        case STATE_A:
            printf(&quot;当前在状态 A, 处理字符: %c\n&quot;, c);
            if (c == &apos;1&apos;) 
            {
                currentState = STATE_B; // 转换到状态 B
            }
            break;
        case STATE_B:
            printf(&quot;当前在状态 B, 处理字符: %c\n&quot;, c);
            if (c == &apos;2&apos;) 
            {
                currentState = STATE_C; // 转换到状态 C
            }
            break;
        case STATE_C:
            printf(&quot;当前在状态 C, 处理字符: %c\n&quot;, c);
            if (c == &apos;3&apos;) 
            {
                currentState = STATE_A; // 回到状态 A
            }
            break;
    }
}

int main() 
{
    process_char(&apos;a&apos;);
    process_char(&apos;1&apos;);
    process_char(&apos;b&apos;);
    process_char(&apos;2&apos;);
    process_char(&apos;c&apos;);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 实现单例模式&lt;/h2&gt;
&lt;p&gt;在 C++ 实现单例模式时，为了实现线程安全，需要使用锁，同时还需要进行双重判空，我们可以利用 C++11 可以保证局部静态变量的初始化是线程安全的性质，来实现简洁的线程安全的单例模式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Singleton 
{
private:
    Singleton() 
    { 
        std::cout &amp;#x3C;&amp;#x3C; &quot;Singleton Instance Created\n&quot;; 
    }
    ~Singleton() = default;
    Singleton(const Singleton&amp;#x26;) = delete;
    Singleton&amp;#x26; operator=(const Singleton&amp;#x26;) = delete;

public:
    static Singleton&amp;#x26; getInstance() 
    {
        static Singleton instance;  // C++11 保证线程安全
        return instance;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. 调试观察点&lt;/h2&gt;
&lt;p&gt;这是静态变量一个极其强大的调试优势。因为静态局部变量的内存地址在整个程序运行期间是固定的，而普通局部变量存储在栈上，每次函数调用其地址都可能不同。在调试时如果我们怀疑一个变量的值被异常修改，但不知道是哪里的代码干的，调试技巧如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在调试器中运行程序，让函数至少执行一次，这样静态变量就被创建了。&lt;/li&gt;
&lt;li&gt;在变量窗口中找到该静态变量，右键点击并选择“设置数据断点”或“在此地址设置断点”。&lt;/li&gt;
&lt;li&gt;之后，只要这个变量的值发生改变，调试器就会立即中断，并指向导致修改的代码行，我们就可以知道这个变量的值是在哪里更改的了。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>C++ 的 static 和 extern 关键字以及相关的概念</title><link>https://liangbm3.site/blog/c-de-static-he-extern-guan-jian-zi-yi-ji-xiang-guan-de-gai-nian</link><guid isPermaLink="true">https://liangbm3.site/blog/c-de-static-he-extern-guan-jian-zi-yi-ji-xiang-guan-de-gai-nian</guid><description>介绍一下 C++ 的作用域和声明周期的概念，以及链接属性的概念，最后再讲解一下 static 和 extern 的用法</description><pubDate>Fri, 04 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;static&lt;/code&gt; 和 &lt;code&gt;extern&lt;/code&gt; 都是C++中常见的关键字，主要用于控制变量和函数的&lt;strong&gt;存储期&lt;/strong&gt;和&lt;strong&gt;链接性&lt;/strong&gt;，从而决定了标识符在内存中的生命周期以及在不同文件之间的可见性。本文会先介绍&lt;strong&gt;作用域&lt;/strong&gt;和&lt;strong&gt;生命周期&lt;/strong&gt;的概念，然后说明 C++ 中&lt;strong&gt;链接属性&lt;/strong&gt;，最后给出&lt;code&gt;static&lt;/code&gt; 和 &lt;code&gt;extern&lt;/code&gt; 这两个 C++ 关键字的常见用法。&lt;/p&gt;
&lt;h2&gt;1. 作用域与生命周期&lt;/h2&gt;
&lt;h3&gt;1.1 作用域&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;作用域&lt;/strong&gt;是一个编译时的概念，定义了一个标识符在代码中可以被有效访问的区域，通常会有以下几种作用域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;块作用域&lt;/strong&gt;：也称局部作用域，任何在花括号 &lt;code&gt;{}&lt;/code&gt; 内声明的标识符，其作用域都从声明点开始，到包含它的右花括号 &lt;code&gt;}&lt;/code&gt; 结束&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数作用域&lt;/strong&gt;：标签在整个函数体内都是可见的，无论在何处定义，通常仅适用于 &lt;code&gt;goto&lt;/code&gt; 语句中的 &lt;code&gt;label&lt;/code&gt; 标签&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局作用域&lt;/strong&gt;：也称文件作用域，在所有函数、类和代码块之外声明的标识符用于全局作用域，它的作用域从声明点开始，直到当前源文件的末尾。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类作用域&lt;/strong&gt;：在类（&lt;code&gt;class&lt;/code&gt; 或 &lt;code&gt;struct&lt;/code&gt;）内部声明的成员拥有类作用域。在类的外部访问它们需要通过类的对象、引用、指针使用成员访问运算符（&lt;code&gt;.&lt;/code&gt; 或 &lt;code&gt;-&gt;&lt;/code&gt;），对于静态成员要通过类名使用作用域解析运算符 &lt;code&gt;::&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;命名空间作用域&lt;/strong&gt;：在 &lt;code&gt;namespace&lt;/code&gt; 中声明的标识符的作用域限定在该命名空间内。访问它们需要使用命名空间名称和作用域解析运算符 &lt;code&gt;::&lt;/code&gt;，或者使用 &lt;code&gt;using&lt;/code&gt; 声明。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 生命周期&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;生命周期&lt;/strong&gt;是一个运行时的概念，它指的是一个对象从被创建到被销毁的时间段，当一个对象的生命周期结束后，它所占用的内存可能会被回收，访问该对象将导致未定义行为。C++ 中对象的生命周期主要由它的存储期决定的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自动存储期&lt;/strong&gt;：局部变量具有自动存储期。当程序的执行流进入其声明所在的块时，对象被创建，当程序的执行流离开该块时，对象被自动销毁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态存储期&lt;/strong&gt;：全局变量、静态局部变量和静态类成员具有静态存储期。静态存储期的变量在 &lt;code&gt;main&lt;/code&gt; 函数执行前就被创建和初始化，在 &lt;code&gt;main&lt;/code&gt; 函数执行后或者调用 &lt;code&gt;exit&lt;/code&gt; 时被销毁。生命周期从程序开始，到程序结束。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态存储期&lt;/strong&gt;：使用 &lt;code&gt;new&lt;/code&gt; 或者 &lt;code&gt;malloc&lt;/code&gt; 在堆或自由存储区上分配的内存中的对象，具有动态存储期。它在 &lt;code&gt;new&lt;/code&gt; 时被创建，在 &lt;code&gt;delete&lt;/code&gt; 时被销毁，生命周期与作用域无关。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程存储期&lt;/strong&gt;：使用 &lt;code&gt;thread_local&lt;/code&gt; 修饰的变量，其生命周期和创建的线程一致，线程结束时自动销毁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 链接属性&lt;/h2&gt;
&lt;p&gt;在 C++ 中，一个程序可以由多个源文件组成的，每个源文件被称为一个翻译单元，编译器会独立地将每个翻译单元编译成一个目标文件，最后链接器会将这些目标文件链接在一起，生成最终的可执行文件。标识符的&lt;strong&gt;链接&lt;/strong&gt;属性会决定该标识符在不同的翻译单元中是否指向同一个实体。C++ 主要由三种链接属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;外部链接&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内部链接&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无链接&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中无链接指的是标识符仅存在于当前作用域，完全无法被其他地方访问，例如局部变量、函数参数、非静态类成员等。我们这里讨论的主要是外部链接和内部链接。&lt;/p&gt;
&lt;h3&gt;2.1 外部链接&lt;/h3&gt;
&lt;p&gt;具有外部链接的标识符，无论在多少个翻译单元中被声明，都指向同一个唯一的实体，因此可以在一个文件中定义它，然后在其他文件中声明并使用它。&lt;/p&gt;
&lt;p&gt;具有外部链接的情况如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在函数、类和命名空间之外定义的函数&lt;/li&gt;
&lt;li&gt;默认情况下全局变量具有外部链接&lt;/li&gt;
&lt;li&gt;类的定义本身具有外部链接，可以在一个文件中定义类，在另一个文件中创建它的实例&lt;/li&gt;
&lt;li&gt;类的静态成员变量具有外部链接。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面是一个很常见的例子：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;helper.h&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#ifndef HELPER_H
#define HELPER_H

// 声明一个具有外部链接的变量
// &apos;extern&apos; 关键字表明 g_shared_data 的定义在别处
extern int g_shared_data;

// 声明一个具有外部链接的函数
void shared_function();

#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;helper.cpp&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &quot;helper.h&quot;
#include &amp;#x3C;iostream&gt;

// 定义（创建并初始化）这个全局变量
// 这是唯一的定义点
int g_shared_data = 100;

// 定义这个全局函数
void shared_function() 
{
    std::cout &amp;#x3C;&amp;#x3C; &quot;shared_function() called. g_shared_data is: &quot; &amp;#x3C;&amp;#x3C; g_shared_data &amp;#x3C;&amp;#x3C; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main.cpp&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &quot;helper.h&quot;
#include &amp;#x3C;iostream&gt;

int main() 
{
    std::cout &amp;#x3C;&amp;#x3C; &quot;main() started. g_shared_data is: &quot; &amp;#x3C;&amp;#x3C; g_shared_data &amp;#x3C;&amp;#x3C; std::endl;
    g_shared_data = 200; // 修改共享数据
    shared_function(); // 调用共享函数
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终会输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main() started. g_shared_data is: 100
shared_function() called. g_shared_data is: 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这表明 &lt;code&gt;main.cpp&lt;/code&gt; 和 &lt;code&gt;helper.cpp&lt;/code&gt; 访问和修改的是同一个 &lt;code&gt;g_shared_data&lt;/code&gt; 变量。&lt;/p&gt;
&lt;h3&gt;2.2 内部链接&lt;/h3&gt;
&lt;p&gt;具有内部链接的标识符，在每个翻译单元中都指向一个独立的、唯一的实体。即使在不同的文件中声明了同名的标识符，它们也是完全不同、互不相干的实体。&lt;/p&gt;
&lt;p&gt;具有内部链接的情况如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;static&lt;/code&gt; 关键字修饰的全局变量和函数，这是 C 语言中实现内部链接的传统方式。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 此变量和函数只在本文件内部可见
static int file_local_count = 10;

static void print_message() 
{
    std::cout &amp;#x3C;&amp;#x3C; &quot;Message from file1. Count: &quot; &amp;#x3C;&amp;#x3C; file_local_count &amp;#x3C;&amp;#x3C; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;匿名命名空间，这是 C++ 中实现内部链接的现代且推荐的方式，使用匿名空间可以避免为每个实体都单独写 &lt;code&gt;static&lt;/code&gt;。例如一个名为 &lt;code&gt;greeter.cpp&lt;/code&gt; 的内容如下：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

// greeter.cpp 的匿名命名空间
namespace {
    // 这个 greet() 函数只在 greeter.cpp 文件内有效
    void greet() {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Hello from greeter.cpp!&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
}

// 这是一个普通的、具有外部链接的函数，可以被其他文件调用
void say_hello_from_greeter() {
    // 它调用的是自己文件内部的 greet()
    greet();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;code&gt;main.cpp&lt;/code&gt; 的内容如下：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

// main.cpp 的匿名命名空间
namespace {
    // 这个 greet() 函数与 greeter.cpp 中的同名函数完全无关
    // 它只在 main.cpp 文件内有效
    void greet() {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Hello from main.cpp!&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
}

// 声明在 greeter.cpp 中定义的函数
void say_hello_from_greeter();

int main() {
    // 1. 调用当前文件 (main.cpp) 的 greet()
    greet();

    // 2. 调用另一个文件的函数，那个函数会调用它自己的 greet()
    say_hello_from_greeter();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
最终会输出：
&lt;pre&gt;&lt;code&gt;Hello from main.cpp!
Hello from greeter.cpp!
&lt;/code&gt;&lt;/pre&gt;
程序顺利运行，没有出现命名冲突的情况，因为不同文件里面的相同名称的函数被各自的匿名空间隐藏了，链接器认为它们是各自私有的，不会发生冲突。&lt;/li&gt;
&lt;li&gt;默认情况下 &lt;code&gt;const&lt;/code&gt; 全局变量具有内部链接，这个性质是非常重要的，因为这让在头文件中定义常量成为可能。否则多个源文件引用同一个头文件时会有命名冲突。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. &lt;code&gt;static&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;static&lt;/code&gt; 关键字在 C++ 中有多种用途，主要用途包括以下几个方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用于修饰静态局部变量&lt;/strong&gt;：用 &lt;code&gt;static&lt;/code&gt; 修饰函数内部的局部变量会改变该变量的存储期。普通局部变量在函数返回时销毁，而静态局部变量是在整个生命周期内都存在的，即使函数退出了也不会被销毁。静态局部变量只有在第一次执行到声明时初始化一次，后续的函数调用会跳过初始化语句。在 C++11 之后的多线程环境下，静态局部变量的初始化过程是&lt;strong&gt;线程安全&lt;/strong&gt;的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用于修饰静态全局变量或函数&lt;/strong&gt;：普通的全局变量或函数是具有外部链接性的，可以在其他的项目中通过 &lt;code&gt;extern&lt;/code&gt; 关键字访问。如果使用 &lt;code&gt;static&lt;/code&gt; 修饰全局变量或函数，则它们的作用域会被限制在单个源文件的内部，变成内部链接。这通常可以用来解决不同文件间因同名全局变量或函数导致的命名冲突问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用于修饰类成员&lt;/strong&gt;：当用 &lt;code&gt;static&lt;/code&gt; 修饰类的成员时，它表示该成员属于类本身，而不是特定的对象，所有类的对象共享同一个静态数据成员的实例。静态成员变量必须在类定义之外进行初始化和定义，静态成员函数内部是没有 &lt;code&gt;this&lt;/code&gt; 指针，它们只能直接访问类的静态成员变量，不能直接访问非静态成员变量。示例：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

class Player {
public:
    Player() {
        active_players_++; // 每当创建新玩家时，计数器加一
    }

    ~Player() {
        active_players_--; // 玩家被销毁时，计数器减一
    }

    // 静态成员函数
    static int get_active_players_count() {
        return active_players_;
    }

private:
    // 静态数据成员声明
    static int active_players_;
};

// 静态数据成员的定义和初始化
int Player::active_players_ = 0;
int main() {
    std::cout &amp;#x3C;&amp;#x3C; &quot;Active players at start: &quot; &amp;#x3C;&amp;#x3C; Player::get_active_players_count() &amp;#x3C;&amp;#x3C; std::endl;

    Player p1;
    Player p2;
    std::cout &amp;#x3C;&amp;#x3C; &quot;Active players now: &quot; &amp;#x3C;&amp;#x3C; Player::get_active_players_count() &amp;#x3C;&amp;#x3C; std::endl;

    {
        Player p3;
        std::cout &amp;#x3C;&amp;#x3C; &quot;Active players inside scope: &quot; &amp;#x3C;&amp;#x3C; Player::get_active_players_count() &amp;#x3C;&amp;#x3C; std::endl;
    } // p3 在这里被销毁

    std::cout &amp;#x3C;&amp;#x3C; &quot;Active players after scope: &quot; &amp;#x3C;&amp;#x3C; Player::get_active_players_count() &amp;#x3C;&amp;#x3C; std::endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. &lt;code&gt;extern&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;extern&lt;/code&gt; 关键字的主要作用是声明一个变量或函数是在别处定义的，主要用于在多个源文件之间共享全局变量和函数。&lt;/p&gt;
&lt;p&gt;下面是一个使用 &lt;code&gt;extern&lt;/code&gt; 关键字共享全局变量的例子：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;config.cpp&lt;/code&gt;（定义）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 定义一个全局配置变量
int global_log_level = 2; // Definition
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;config.h&lt;/code&gt;（声明）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#ifndef CONFIG_H
#define CONFIG_H

// 使用 extern 声明全局变量，供其他文件使用
extern int global_log_level; // Declaration

#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main.cpp&lt;/code&gt;（使用）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &quot;config.h&quot; // 包含声明

void some_function() {
    if (global_log_level &gt; 1) {
        std::cout &amp;#x3C;&amp;#x3C; &quot;High detail logging enabled.&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
}

int main() {
    some_function();
    global_log_level = 0; // 可以在这里修改它
    std::cout &amp;#x3C;&amp;#x3C; &quot;Log level set to: &quot; &amp;#x3C;&amp;#x3C; global_log_level &amp;#x3C;&amp;#x3C; std::endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>C++ 的函数指针与函数对象</title><link>https://liangbm3.site/blog/c-de-han-shu-zhi-zhen-yu-han-shu-dui-xiang</link><guid isPermaLink="true">https://liangbm3.site/blog/c-de-han-shu-zhi-zhen-yu-han-shu-dui-xiang</guid><description>总结了一下C++中的函数指针和函数对象，还有std::function和std::bind</description><pubDate>Thu, 03 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;1. 函数指针&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;函数指针&lt;/strong&gt;就是一个指向函数的指针变量，函数是是存放在代码段的，函数指针指向的就是函数在代码段中的入口地址。通过函数指针，我们可以间接地调用这个函数。&lt;/p&gt;
&lt;p&gt;语法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;返回类型 (*指针名称)(参数类型1, 参数类型2, ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设我们有一个普通函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int add(int a, int b) 
{
    return a + b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以声明一个指向该函数的函数指针：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int (*p_func)(int, int);
p_func = &amp;#x26;add; // &amp;#x26; 是可选的，直接写函数名也可以
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过解引用函数指针，或者直接使用指针名，就可以调用它所指向的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 使用函数指针调用函数
int result1 = (*p_func)(10, 5); // 解引用方式 (C-style)
int result2 = p_func(10, 5);   // 直接使用 (更现代的 C++ 风格)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于普通函数，&lt;code&gt;add&lt;/code&gt; 是函数名，本质是一个标签，指向该函数的入口地址，是静态的，不可改变的。而函数指针是一个变量，它存储的函数的地址，可以通过这个指针去调用函数，是动态的，可以指向不同的函数。基于这个特性，函数指针通常的用途如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;回调函数 (Callbacks)&lt;/strong&gt;: 一个函数接受另一个函数作为参数，在适当的时候调用它。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void performOperation(int x, int y, int (*op)(int, int)) 
{
std::cout &amp;#x3C;&amp;#x3C; &quot;Result: &quot; &amp;#x3C;&amp;#x3C; op(x, y) &amp;#x3C;&amp;#x3C; std::endl;
}

// 调用
performOperation(20, 10, &amp;#x26;add);      // 传入 add 函数
performOperation(20, 10, &amp;#x26;subtract); // 传入另一个兼容的函数
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现函数表/跳转表 (Jump Tables)&lt;/strong&gt;: 可以用函数指针数组来代替一长串的 &lt;code&gt;if-else&lt;/code&gt; 或 &lt;code&gt;switch-case&lt;/code&gt; 语句，根据输入选择要执行的函数。函数指针数组的声明语法如下：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;返回类型 (*数组名[数组大小])(参数列表);
&lt;/code&gt;&lt;/pre&gt;
这种声明形式比较晦涩难懂，我们可以使用&lt;code&gt;typedef&lt;/code&gt;或&lt;code&gt;using&lt;/code&gt;来进行简化：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;typedef int (*FuncPtr)(int, int);
// 或 C++11 写法：using FuncPtr = int(*)(int, int);

FuncPtr funcArr[3] = {add, sub, mul};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;函数指针是C语言中的用法，实现简单，调用的开销小。但是在C++中是不推荐使用函数指针的，因为函数指针既不能携带状态信息，也不能捕获或者存储任何上下文的信息。在C++中，通常使用的是函数对象。&lt;/p&gt;
&lt;h2&gt;2. 函数对象&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;函数对象&lt;/strong&gt;又叫&lt;strong&gt;仿函数&lt;/strong&gt;，本质上是一个类或者结构体的实例，由于它重载了&lt;code&gt;operator()&lt;/code&gt;，因此我们可以像调用普通函数一样使用这个对象。&lt;/p&gt;
&lt;p&gt;一个示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Adder 
{
public:
    int operator()(int a, int b) const 
    {
        return a + b;
    }
};
Adder add; // 创建一个 Adder 对象
int sum1 = add(5, 3);       // 像调用函数一样调用对象 add
// 也可以直接创建临时对象并调用
int sum2 = Adder()(10, 20);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数对象的核心优势在于可以拥有独立的状态，而函数指针和普通函数不具备这个能力。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;

class Counter 
{
    int count = 0;  // 每个对象独有的状态
public:
    int operator()(int x) {
        return ++count + x;
    }
};

int main() 
{
    Counter c1, c2;
    cout &amp;#x3C;&amp;#x3C; c1(5) &amp;#x3C;&amp;#x3C; endl;  // count = 1
    cout &amp;#x3C;&amp;#x3C; c1(5) &amp;#x3C;&amp;#x3C; endl;  // count = 2
    cout &amp;#x3C;&amp;#x3C; c2(5) &amp;#x3C;&amp;#x3C; endl;  // c2独立，count = 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数模板允许我们编写与类型无关的泛型代码，当函数模板的行为需要某种可定制的操作时，函数对象便是最理想的参数类型。例如可以写一个通用的&lt;code&gt;getMax&lt;/code&gt;函数，可以根据传入的比较策略（函数对象）返回两个值中的较大者。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;

// 通用比较模板函数
template &amp;#x3C;typename T, typename Compare&gt;
T getMax(T a, T b, Compare comp) 
{
    return comp(a, b) ? b : a;
}

// 函数对象：比较大小
struct Less 
{
    bool operator()(int a, int b) 
    {
        return a &amp;#x3C; b; // 返回 true 时，b 是较大者
    }
};

// 函数对象：比较绝对值大小
struct AbsLess 
{
    bool operator()(int a, int b) 
    {
        return abs(a) &amp;#x3C; abs(b);
    }
};

int main() {
    int x = -5, y = 3;

    // 使用 Less 比较器
    cout &amp;#x3C;&amp;#x3C; &quot;Max: &quot; &amp;#x3C;&amp;#x3C; getMax(x, y, Less()) &amp;#x3C;&amp;#x3C; endl;

    // 使用 AbsLess 比较器
    cout &amp;#x3C;&amp;#x3C; &quot;Max by abs: &quot; &amp;#x3C;&amp;#x3C; getMax(x, y, AbsLess()) &amp;#x3C;&amp;#x3C; endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在C++的STL算法中，和上面所举例子一样，大量使用了函数对象作为可传递的比较器或操作器，因此STL的使用非常灵活，是泛型编程的典范。例如，C++ 的STL算法&lt;code&gt;std::sort&lt;/code&gt;默认的是&lt;code&gt;std::less&amp;#x3C;T&gt;&lt;/code&gt;来进行升序排序，可以使用预定义的函数对象&lt;code&gt;std::greater&amp;#x3C;T&gt;&lt;/code&gt;来进行降序排序：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;algorithm&gt;
#include &amp;#x3C;functional&gt; // 包含预定义函数对象

int main() {
    std::vector&amp;#x3C;int&gt; nums = {3, 1, 4, 1, 5, 9};

    // 使用 std::greater&amp;#x3C;&gt; 进行降序排序
    std::sort(nums.begin(), nums.end(), std::greater&amp;#x3C;int&gt;());

    for (int n : nums) {
        std::cout &amp;#x3C;&amp;#x3C; n &amp;#x3C;&amp;#x3C; &quot; &quot;; // 输出: 9 5 4 3 1 1
    }
    std::cout &amp;#x3C;&amp;#x3C; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Lambda 表达式&lt;/h2&gt;
&lt;p&gt;C++11 引入的 Lambda 表达式在很多情况下可以看作是创建匿名函数对象的便捷语法，通常编译器会将 Lambda 表达式转换为一个未命名的函数对象。Lambda 表达式的基本格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;[捕获列表](参数列表) -&gt; 返回类型 {
    函数体
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[]&lt;/code&gt; 捕获列表（Capture List）：捕获列表决定了 Lambda 如何访问外部作用域的变量
&lt;ul&gt;
&lt;li&gt;按值捕获 &lt;code&gt;[=]&lt;/code&gt; 或显式 &lt;code&gt;[x, y]&lt;/code&gt;：值捕获的变量在 Lambda 表达式定义时就已经确定，不会随着外部变量的变化而变化，例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int x = 10;
auto f = [x] (int y) -&gt; int { return x + y; }; // 值捕获 x
x = 20; // 修改外部的 x
cout &amp;#x3C;&amp;#x3C; f(5) &amp;#x3C;&amp;#x3C; endl; // 输出 15，不受外部 x 的影响
&lt;/code&gt;&lt;/pre&gt;
默认情况下，值捕获的变量在lambda内部是只读的，但是可以通过&lt;code&gt;mutable&lt;/code&gt;修改，例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int a = 10;
auto f = [a]() mutable { a++; std::cout &amp;#x3C;&amp;#x3C; a &amp;#x3C;&amp;#x3C; std::endl; };
f(); // 输出 11，但外部a仍然是10
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;按引用捕获 &lt;code&gt;[&amp;#x26;]&lt;/code&gt; 或显式 &lt;code&gt;[&amp;#x26;x, &amp;#x26;y]&lt;/code&gt;：引用表达式的变量在 Lambda 表达式调用时才确定，会随着外部变量变化而变化，反之亦然。使用引用捕获要注意生命周期的问题，避免悬空引用。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int x = 10;
auto f = [&amp;#x26;x] (int y) -&gt; int { return x + y; }; // 引用捕获 x
x = 20; // 修改外部的 x
cout &amp;#x3C;&amp;#x3C; f(5) &amp;#x3C;&amp;#x3C; endl; // 输出 25，受外部 x 的影响
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;混合捕获：&lt;code&gt;[=, &amp;#x26;x]&lt;/code&gt;（&lt;code&gt;x&lt;/code&gt;按引用，其余按值）或 &lt;code&gt;[&amp;#x26;, y]&lt;/code&gt;（&lt;code&gt;y&lt;/code&gt;按值，其余按引用）。例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int x = 10;
int y = 20;
auto f = [=, &amp;#x26;y] (int z) -&gt; int { return x + y + z; }; // 隐式按值捕获 x，显式按引用捕获 y
x = 30; // 修改外部的 x
y = 40; // 修改外部的 y
cout &amp;#x3C;&amp;#x3C; f(5) &amp;#x3C;&amp;#x3C; endl; // 输出 55，不受外部 x 的影响，受外部 y 的影响
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;初始化捕获（C++14）：它允许在捕获列表中使用初始化表达式，从而在捕获列表中创建并初始化一个新的变量，而不是捕获一个已存在的变量。这种方式可以使用 &lt;code&gt;auto&lt;/code&gt; 关键字来推导类型，也可以显式指定类型。这种方式可以用来捕获只移动的变量，或者捕获 &lt;code&gt;this&lt;/code&gt; 指针的值。下面是一个捕获&lt;code&gt;unique_ptr&lt;/code&gt;的例子：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto lambda = [p = std::make_unique&amp;#x3C;int&gt;(42)]() 
{
    std::cout &amp;#x3C;&amp;#x3C; &quot;Value: &quot; &amp;#x3C;&amp;#x3C; *p &amp;#x3C;&amp;#x3C; std::endl;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;捕获&lt;code&gt;this&lt;/code&gt;指针，&lt;code&gt;[this]&lt;/code&gt; 或 &lt;code&gt;[*this]&lt;/code&gt;：这允许在 Lambda 内部访问当前类的成员变量和成员函数。&lt;code&gt;this&lt;/code&gt;是C++11引入的，这种方式会按值捕获&lt;code&gt;this&lt;/code&gt;指针，在 Lambda 表达式的内部会有一份&lt;code&gt;this&lt;/code&gt;指针的拷贝，实际上这相当于按引用捕获了对象本身，Lambda 内部对成员变量的修改会直接影响到原始对象。而&lt;code&gt;*this&lt;/code&gt;是C++17引入的，这种方式是对当前对象进行值捕获，这就意味着 Lambda 表达式的内部操作的是一个独立的副本，对副本成员的任何修改都不会影响到原始对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;()&lt;/code&gt; 参数列表（Parameter List）：可选，类似于普通函数的参数。
&lt;ul&gt;
&lt;li&gt;与普通函数参数类似，支持默认参数（C++14起）。&lt;/li&gt;
&lt;li&gt;泛型Lambda（C++14）：使用 auto 声明参数，使 Lambda 可接受任意类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-&gt;&lt;/code&gt; 返回类型（Return Type）：可选，返回类型通常可以省略，由编译器推导。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{}&lt;/code&gt; 函数体（Function Body）：Lambda 的执行逻辑。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lambda 表达式为我们创建匿名函数对象提供了便捷的语法糖，当我们编写一个Lambda表达式时，C++编译器会自动生成一个独一无二、匿名的类（这个类被称为闭包类型），然后根据这个类创建一个对象，这个对象就是我们所说的函数对象。例如这个 Lambda 表达式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto myLambda = [](int x) { return x * 2; };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器内部通常会生成如下的匿名类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 编译器在内部生成的匿名类
class __SomeInternalUniqueNameForLambda {
public:
    // 1. operator() 的重载
    // Lambda的参数列表和返回类型决定了这个成员函数的签名
    // 默认情况下，这个operator()是const的，因为Lambda默认是不可变的
    int operator()(int x) const {
        return x * 2;
    }
};

// 2. 创建该类的实例（函数对象）
auto myLambda = __SomeInternalUniqueNameForLambda{};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;int main() {
// 相当于 outer(10)
int x = 10;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义闭包，捕获外部变量 x
auto f = [x](int y) { return x + y; };

cout &amp;#x3C;&amp;#x3C; f(5) &amp;#x3C;&amp;#x3C; endl;   // 输出 15
cout &amp;#x3C;&amp;#x3C; f(20) &amp;#x3C;&amp;#x3C; endl;  // 输出 30

return 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;/Aside&gt;

## 4. 函数包装器和函数适配器

函数包装器`std::function`和函数适配器`std::bind`是C++11引入的两大处理**可调用对象**的强大工具，可以看作是函数指针和函数对象的现代化、类型安全的替代方案。

&amp;#x3C;Aside type=&quot;tip&quot;&gt;
**可调用对象**：能用`()`运算符直接调用的对象，都是可调用对象，包括：
- 普通函数
- 函数指针
- 函数对象
- Lambda 表达式
- `std::function` 封装的对象
- `std::bin` 生成的对象

&amp;#x3C;/Aside&gt;

### 4.1 `std::function`

`std::function` 是一个定义在 `&amp;#x3C;functional&gt;` 头文件中的类模板。它是一个通用的、多态的函数包装器，它可以存储、复制和调用任何签名兼容的可调用对象。`std::function` 的模板参数就是它要包装的函数的签名，语法如下：
```cpp
#include &amp;#x3C;functional&gt;

std::function&amp;#x3C;返回类型(参数类型1, 参数类型2, ...)&gt; func;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是简单的使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;functional&gt;

// 1. 普通函数
int add(int a, int b) { return a + b; }

// 2. 函数对象 (Functor)
struct Subtract 
{
    int operator()(int a, int b) const { return a - b; }
};

int main() 
{
    std::function&amp;#x3C;int(int, int)&gt; operation;

    // a. 包装普通函数
    operation = add; // 或者 &amp;#x26;add
    std::cout &amp;#x3C;&amp;#x3C; &quot;add(10, 5) = &quot; &amp;#x3C;&amp;#x3C; operation(10, 5) &amp;#x3C;&amp;#x3C; std::endl; // 输出 15

    // b. 包装函数对象
    Subtract sub;
    operation = sub;
    std::cout &amp;#x3C;&amp;#x3C; &quot;subtract(10, 5) = &quot; &amp;#x3C;&amp;#x3C; operation(10, 5) &amp;#x3C;&amp;#x3C; std::endl; // 输出 5

    // c. 包装 Lambda 表达式
    operation = [](int a, int b) { return a * b; };
    std::cout &amp;#x3C;&amp;#x3C; &quot;multiply(10, 5) = &quot; &amp;#x3C;&amp;#x3C; operation(10, 5) &amp;#x3C;&amp;#x3C; std::endl; // 输出 50
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，它提供了一个统一的接口来存储不同种类的可调用对象，&lt;code&gt;std::function&lt;/code&gt;会擦除原始可调用对象的具体类型，只保留其调用的接口。这在设计需要回调的 API 时非常有用。API 的设计者不必为函数指针、函数对象等分别设计接口，只需使用 &lt;code&gt;std::function&lt;/code&gt; 即可。由于会进行类型擦除以及可能进行堆分配，&lt;code&gt;std::function&lt;/code&gt;的调用可能比直接调用或者通过函数指针调用有轻微的性能开销，&lt;code&gt;std::function&lt;/code&gt;对象本身的大小可能比函数指针大。&lt;/p&gt;
&lt;h3&gt;4.2 &lt;code&gt;std::bind&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;std::bind&lt;/code&gt; 也是一个定义在 &lt;code&gt;&amp;#x3C;functional&gt;&lt;/code&gt; 头文件中的函数模板。它的作用是将一个可调用对象（如函数、成员函数、函数对象）与其部分或全部参数进行绑定，从而创建一个新的可调用对象（一个函数对象）。语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto new_callable = std::bind(callable, arg1, arg2, ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;callable_object&lt;/code&gt;: 原始的可调用对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arg1, arg2, ...&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;可以是具体的值，这些值将被“绑定”到 &lt;code&gt;callable_object&lt;/code&gt; 对应位置的参数。&lt;/li&gt;
&lt;li&gt;可以是占位符 (Placeholders)，如 &lt;code&gt;std::placeholders::_1&lt;/code&gt;, &lt;code&gt;std::placeholders::_2&lt;/code&gt; 等。这些占位符表示新生成的可调用对象的参数。&lt;code&gt;_1&lt;/code&gt; 对应新可调用对象的第一个参数，&lt;code&gt;_2&lt;/code&gt; 对应第二个，以此类推。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一些使用示例如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数绑定（固定部分参数，得到一个参数更少的新函数）：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int add(int a, int b, int c) 
{
    return a + b + c;
}
// 绑定第一个参数为 10
auto add_10 = std::bind(add, 10, std::placeholders::_1, std::placeholders::_2);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;重新排列参数顺序：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void print_ordered(int a, const std::string&amp;#x26; b, double c) 
{
    std::cout &amp;#x3C;&amp;#x3C; &quot;Int: &quot; &amp;#x3C;&amp;#x3C; a &amp;#x3C;&amp;#x3C; &quot;, String: &quot; &amp;#x3C;&amp;#x3C; b &amp;#x3C;&amp;#x3C; &quot;, Double: &quot; &amp;#x3C;&amp;#x3C; c &amp;#x3C;&amp;#x3C; std::endl;
}

// 原本参数顺序: int, string, double
// 新函数参数顺序: string, double, int (传入_3)
auto print_reordered = std::bind(print_ordered,
                                std::placeholders::_3, // 第三个参数给 a
                                std::placeholders::_1, // 第一个参数给 b
                                std::placeholders::_2  // 第二个参数给 c
                                );
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;绑定成员函数：这是&lt;code&gt;std::bind&lt;/code&gt;的一个经典用途，用于将成员函数适配为可以独立调用的函数对象。对于非静态成员函数，第一个绑定的参数必须是对象实例的指针或引用，或者是一个包装了对象的&lt;code&gt;std::shared_ptr&lt;/code&gt;或&lt;code&gt;std::reference_wrapper&lt;/code&gt;。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Greeter 
{
    std::string name;
    Greeter(std::string n) : name(n) {}
    void greet(const std::string&amp;#x26; salutation) 
    {
        std::cout &amp;#x3C;&amp;#x3C; salutation &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; name &amp;#x3C;&amp;#x3C; &quot;!&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
};

Greeter world_greeter(&quot;World&quot;);
Greeter cpp_greeter(&quot;C++&quot;);

// 绑定到 world_greeter 实例的 greet 方法，并预设 salutation 参数
auto greet_world_morning = std::bind(&amp;#x26;Greeter::greet, &amp;#x26;world_greeter, &quot;Good morning&quot;);
greet_world_morning(); // 输出: Good morning, World!

// 绑定到 cpp_greeter 实例的 greet 方法，salutation 通过占位符传入
auto greet_cpp = std::bind(&amp;#x26;Greeter::greet, &amp;#x26;cpp_greeter, std::placeholders::_1);
greet_cpp(&quot;Hello&quot;); // 输出: Hello, C++!
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>C++ 中的静态库和动态库</title><link>https://liangbm3.site/blog/c-zhong-de-jing-tai-ku-he-dong-tai-ku</link><guid isPermaLink="true">https://liangbm3.site/blog/c-zhong-de-jing-tai-ku-he-dong-tai-ku</guid><description>总结一下C++中的静态库和动态库，以及如何创建和使用</description><pubDate>Wed, 02 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在编写程序时，我们为了实现代码复用，通常会把通用的功能模块化，编译成库文件，供其他程序使用。在 C++ 中，库文件分为静态库和动态库两种。&lt;/p&gt;
&lt;h2&gt;1. 静态库简介&lt;/h2&gt;
&lt;p&gt;静态库在 Windows 上通常是 &lt;code&gt;.lib&lt;/code&gt; 文件，在 Linux 或 macOS 上是&lt;code&gt;.a&lt;/code&gt; (archive) 文件。它是一组目标文件（&lt;code&gt;.o&lt;/code&gt; 或 &lt;code&gt;.obj&lt;/code&gt; 文件）的集合。在程序的链接阶段，链接器会从静态库中提取出程序所需要的所有代码和数据，并将它们完整复制到最终的可执行文件中。编译完成后，即使静态库不存在了，程序也是可以正常运行的。&lt;/p&gt;
&lt;p&gt;静态库的优点是独立性强，发布简单。由于所有依赖的代码都被复制到了可执行文件中，所以程序在运行时不再需要外部库文件，可以独立运行，程序的部署和分发更加简单。因为所有代码都在一个文件中，所以可以减少程序在启动时或者调用库函数时的开销。&lt;/p&gt;
&lt;p&gt;静态库也有明显的缺点，那就是生成的可执行文件体积较大。因为每个使用该库的程序都包含了一份完整的库代码副本，如果多个程序都使用了同一个静态库，那么在磁盘和内存中就会有多份重复的代码，造成空间浪费。如果静态库进行更新，那么所有使用这个旧版本静态库的程序都必须重新编译、链接，才能得到更新后的代码，这是非常麻烦的。所以静态库在小型软件用得比较多。&lt;/p&gt;
&lt;h2&gt;2. 动态库简介&lt;/h2&gt;
&lt;p&gt;动态库在 Windows 上是&lt;code&gt;.dll&lt;/code&gt;文件，在 Linux 上是&lt;code&gt;.so&lt;/code&gt;文件，在 macOS 上是 &lt;code&gt;.dylib&lt;/code&gt; 文件。动态库在程序的链接阶段不会被复制到可执行文件中，链接器只是在可执行文件中记录下对该动态库的引用信息（比如库的名字和函数签名）。当程序运行时，操作系统会根据这些记录的信息，在内存中找到并加载这个动态库。因此所有使用该动态库的程序在内存中是共享同一份库的实例。&lt;/p&gt;
&lt;p&gt;多个程序共享同一个动态库，磁盘和内存中只需要存在一份库的副本，大大减少了资源的占用。如果动态库更新了，只需要替换掉系统中的旧库文件即可，所有依赖于它的程序在它下次运行时，就会加载新版本的库，而不需要编译程序本身，但前提是要保持接口兼容。因此大型程序中，通常会拆分出多个较小的动态库模块，按需加载，实现插件化功能。&lt;/p&gt;
&lt;p&gt;当然，动态库也是有缺点的。发布程序时，除了可执行文件本身，还必须同时提供其依赖的所有动态库文件，如果用户系统中缺少某个动态库或者版本不兼容，程序将无法运行。这就是我们在 Windows 中有时会碰到“缺少 xxx.dll”的错误。程序启动时需要由操作系统去查找、加载和链接动态库，这会带来一定的性能开销。&lt;/p&gt;
&lt;h2&gt;3. 静态库的创建&lt;/h2&gt;
&lt;p&gt;以在 Linux 环境中使用g++编译器创建一个简单的数学计算库为例，静态库的创建一般流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编写库文件代码，这通常需要包含头文件和源文件。头文件用来声明库对外提供的接口（函数原型），使用我们编写的库则需要包含这个头文件：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#ifndef MATH_LIB_H
#define MATH_LIB_H

// 函数声明：一个加法函数
int add(int a, int b);

// 函数声明：一个减法函数
int subtract(int a, int b);

#endif // MATH_LIB_H
&lt;/code&gt;&lt;/pre&gt;
同时，还需要包含源文件，源文件是库函数的具体实现，是不对外暴露的，使用这个静态库的用户无须关心这部分的代码：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &quot;math_lib.h&quot;

// 加法函数的实现
int add(int a, int b) {
    return a + b;
}

// 减法函数的实现
int subtract(int a, int b) {
    return a - b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;将源文件编译成目标文件(&lt;code&gt;.o&lt;/code&gt;)，我们使用&lt;code&gt;g++&lt;/code&gt;的&lt;code&gt;-c&lt;/code&gt;选项，这个选项告诉编译器只进行编译，生成目标文件，而不进行链接。
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;g++ -c math_lib.cpp -o math_lib.o
&lt;/code&gt;&lt;/pre&gt;
执行成功后，目录下会多一个&lt;code&gt;math_lib.o&lt;/code&gt;文件。如果有多个&lt;code&gt;.cpp&lt;/code&gt;文件，则需要都将它们编译成对应的&lt;code&gt;.o&lt;/code&gt;文件。&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;ar&lt;/code&gt;工具将一个或者多个目标文件打包成静态库，静态库的命名一般遵循&lt;code&gt;lib&amp;#x3C;库名&gt;.a&lt;/code&gt;的规范，例如我们的库叫&lt;code&gt;math&lt;/code&gt;，那么库文件应该叫&lt;code&gt;libmath.a&lt;/code&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# ar 是一个用于创建和管理归档文件的工具
# rcs 是三个选项的组合：
#   r: replace，若库中已存在同名目标文件，则替换它；若不存在，则添加。
#   c: create，如果库文件不存在，就创建一个。
#   s: create symbol index，为库创建一个索引，可以加快链接速度。
# libmath.a 是我们想要创建的静态库文件名
# math_lib.o 是要打包进去的目标文件
ar rcs libmath.a math_lib.o
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;将静态库拷贝到系统默认存放库目录下（&lt;code&gt;/usr/lib&lt;/code&gt;或&lt;code&gt;/usr/local/lib&lt;/code&gt;）这部分是可选的，这通常只用于安装系统级的、供所有用户和程序使用的共享库。如果不拷贝到系统目录，可以在编译主程序时，使用&lt;code&gt;-L&lt;/code&gt;选项指定库文件所在的路径。&lt;/li&gt;
&lt;li&gt;编写主程序并使用静态库，我们编写如下主程序：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &quot;math_lib.h&quot; // 包含我们库的头文件

int main() {
    int x = 20;
    int y = 10;

    // 调用静态库中的函数
    int sum = add(x, y);
    int diff = subtract(x, y);

    std::cout &amp;#x3C;&amp;#x3C; &quot;The sum is: &quot; &amp;#x3C;&amp;#x3C; sum &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;The difference is: &quot; &amp;#x3C;&amp;#x3C; diff &amp;#x3C;&amp;#x3C; std::endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
在编译时，如果我们编写的库被拷贝到系统目录中，那我们通常需要使用&lt;code&gt;-L&lt;/code&gt;来指定：
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# g++: 编译器
# main.cpp: 主程序源文件
# -o main_app: 指定输出的可执行文件名为 main_app
# -I. : -I (大写i) 选项用于指定头文件的搜索路径。&apos;.&apos; 表示当前目录。
#       这样编译器才能找到 #include &quot;math_lib.h&quot;。
# -L. : -L 选项用于指定库文件的搜索路径。&apos;.&apos; 表示当前目录。
#       这样链接器才能找到 libmath.a。
# -lmath: -l (小写L) 选项用于指定要链接的库的名称。
#         编译器会自动在库名前加上 &quot;lib&quot;，在库名后加上 &quot;.a&quot; 或 &quot;.so&quot;。
#         所以 -lmath 就对应着 libmath.a。
g++ main.cpp -o main_app -I. -L. -lmath
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 动态库的创建&lt;/h2&gt;
&lt;p&gt;动态库和静态库的创建流程相似，动态库的一般创建流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;和静态库一样，也是需要编写库文件，包括头文件和源文件。&lt;/li&gt;
&lt;li&gt;因为动态库运行时会被加载到内存的任意位置，所以它的代码和数据引用不能使用绝对地址，必须使用相对地址，这种代码被称为“位置无关码”（PIC）。需要使用 &lt;code&gt;g++&lt;/code&gt; 的 &lt;code&gt;-fPIC&lt;/code&gt; 标志来生成这种代码。
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;g++ -fPIC -c math_lib.cpp -o math_lib.o
&lt;/code&gt;&lt;/pre&gt;
执行后，会生成名为 &lt;code&gt;math_lib.o&lt;/code&gt; 的目标文件。&lt;/li&gt;
&lt;li&gt;将刚才生成的位置无关的目标文件链接成一个共享库文件。假设库名为&lt;code&gt;math&lt;/code&gt;，仍然遵循&lt;code&gt;lib&amp;#x3C;name&gt;.so&lt;/code&gt;的命名约定。
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;g++ -shared -o libmath.so math_lib.o
&lt;/code&gt;&lt;/pre&gt;
执行完毕后，当前目录下就会生成 &lt;code&gt;libmath.so&lt;/code&gt; 文件，就是我们成功创建的动态库。&lt;/li&gt;
&lt;li&gt;编写主程序并使用动态库，我们编写如下主程序：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &quot;math_lib.h&quot; // 包含我们库的头文件

int main() {
    int x = 20;
    int y = 10;

    // 调用动态库中的函数
    int sum = add(x, y);
    int diff = subtract(x, y);

    std::cout &amp;#x3C;&amp;#x3C; &quot;The sum is: &quot; &amp;#x3C;&amp;#x3C; sum &amp;#x3C;&amp;#x3C; std::endl;
    std::cout &amp;#x3C;&amp;#x3C; &quot;The difference is: &quot; &amp;#x3C;&amp;#x3C; diff &amp;#x3C;&amp;#x3C; std::endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
进行编译：
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# -I. 指定头文件搜索路径为当前目录
# -L. 指定库文件搜索路径为当前目录
# -lmath 指定链接名为 &quot;math&quot; 的库 (即 libmath.so)
g++ main.cpp -o main_app -I. -L. -lmath
&lt;/code&gt;&lt;/pre&gt;
运行时可能会看到一个这样的错误：
&lt;pre&gt;&lt;code&gt;error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory
&lt;/code&gt;&lt;/pre&gt;
因为在运行时，操作系统需要知道去哪里找 &lt;code&gt;libmath.so&lt;/code&gt;。编译时使用的 &lt;code&gt;-L.&lt;/code&gt; 只是告诉编译器在编译期间去哪里检查库，这个路径信息默认不会被保存到最终的可执行文件中。一般来说有以下三种方法解决这个问题：
&lt;ul&gt;
&lt;li&gt;将库所在的目录临时添加到 &lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt; 环境变量中。
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 将当前目录(.)添加到该环境变量中
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

# 然后再次运行程序
./main_app
&lt;/code&gt;&lt;/pre&gt;
&lt;code&gt;export&lt;/code&gt; 命令只在当前的终端会话中有效。关闭终端后需要重新设置。因此这是一个临时性的设置，常用于开发当中。&lt;/li&gt;
&lt;li&gt;我们可以在编译 &lt;code&gt;main_app&lt;/code&gt; 时，把库的路径信息直接“写死”到可执行文件里。这样运行时，程序就知道去哪里找库了。我们使用链接器选项 &lt;code&gt;-rpath&lt;/code&gt;。
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# -Wl,&amp;#x3C;option&gt; 是告诉g++将&amp;#x3C;option&gt;传递给链接器(ld)
# -rpath,. 表示将当前目录(.)作为运行时库的搜索路径
g++ main.cpp -o main_app -I. -L. -lmath -Wl,-rpath,.
&lt;/code&gt;&lt;/pre&gt;
这样程序在运行时会永远在这个路径寻找动态库，比较适用于发布的程序当中。&lt;/li&gt;
&lt;li&gt;对于要被系统上很多程序使用的通用库，可以将其安装到标准的系统库目录中，如 &lt;code&gt;/usr/local/lib&lt;/code&gt;、&lt;code&gt;/usr/lib&lt;/code&gt;和&lt;code&gt;/lib&lt;/code&gt;。
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 1. 将库文件拷贝到系统目录 (需要root权限)
sudo cp libmath.so /usr/local/lib/

# 2. 更新系统的动态链接器缓存
sudo ldconfig

# 3. 现在，你可以在任何地方编译和运行你的程序了（编译时仍需-l，但无需-L）
g++ main.cpp -o main_app -lmath
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 动态库的加载与执行过程&lt;/h2&gt;
&lt;p&gt;当一个文件被执行时，其内部对动态库的依赖关系必须在运行时得到解析和满足，这个过程是由操作系统的动态链接器来完成的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;启动加载器&lt;/strong&gt;：操作系统内核首先会读取可执行文件（Linux中为ELF格式）的头部，里面会有一个叫&lt;code&gt;.interp&lt;/code&gt;的段，该段指定了动态链接的解释器路径，内核会加载并启动这个动态链接器，并将程序的控制权和相关信息移交给它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖发现与库搜索&lt;/strong&gt;：动态链接器接管后，会读取可执行文件的&lt;code&gt;.dynamic&lt;/code&gt;段，此段包含一系列的&lt;code&gt;DT_NEEDED&lt;/code&gt;条目，每个条目都制定了一个必须的共享库名称，动态链接器会按一下顺序去搜索：
&lt;ul&gt;
&lt;li&gt;运行时搜索路径 (&lt;code&gt;rpath&lt;/code&gt;、&lt;code&gt;runpath&lt;/code&gt;)：这是通过在链接时使用&lt;code&gt;-rpath&lt;/code&gt;或&lt;code&gt;-runpath&lt;/code&gt;标志指定的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt; 环境变量：用户可以通过设置此变量，临时性地将一个或多个目录加入到搜索路径中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ld.so.cache&lt;/code&gt; 缓存：搜索 &lt;code&gt;/etc/ld.so.cache&lt;/code&gt; 文件。这是一个由 &lt;code&gt;ldconfig&lt;/code&gt; 命令生成的二进制缓存，它索引了配置文件 &lt;code&gt;/etc/ld.so.conf&lt;/code&gt; 及其包含的子目录中所有已知的库及其路径。通过缓存进行查找的速度极快。&lt;/li&gt;
&lt;li&gt;默认库路径：如果以上路径均未找到，链接器会搜索标准的系统默认路径，通常包括 &lt;code&gt;/lib&lt;/code&gt;、&lt;code&gt;/usr/lib&lt;/code&gt;、&lt;code&gt;/lib64&lt;/code&gt;、&lt;code&gt;/usr/lib64&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;符号解析与重定位&lt;/strong&gt;：由于程序代码中对动态库函数或变量的引用只是一个符号名称或者占位符，动态链接器需要将这些符号引用修正为它们在虚拟内存中的实际地址，这个过程主要依赖以下两个机制：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全局偏移表 (GOT - Global Offset Table)&lt;/strong&gt;: 存储外部全局变量和函数的绝对地址，代码通过访问GOT来间接访问这些外部符号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;过程链接表 (PLT - Procedure Linkage Table)&lt;/strong&gt;: 对外部函数的第一次调用会通过PLT跳转到动态链接器的一段代码，该代码负责查找函数的实际地址，并填入GOT中，后续对同一函数的调用就可以通过PLT直接跳转到GOT记录的实际地址，无需再经过链接器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;所有的库成功加载、重定向和初始化后，动态链接器就会将执行控制权交给可执行文件的主入口点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. 动态依赖与符号问题排查指南&lt;/h2&gt;
&lt;p&gt;在我们平时的开发当中，经常会遇到找不到动态库或者找不到符号的问题，下面给出一些常见的排查方法。&lt;/p&gt;
&lt;p&gt;运行时找不到动态库文件会报如下信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error while loading shared libraries: libxxx.so: cannot open shared object file: No such file or directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;遇到这种问题，我们可以使用&lt;code&gt;ldd&lt;/code&gt;命令来列出一个可执行文件或另一个动态库所依赖的所有共享库，并显式动态链接器找到的库路径。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ldd /path/to/your_executable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们可以使用&lt;code&gt;find&lt;/code&gt;命令在整个系统中搜索该库文件，并确认它是否真实存在于磁盘上，以及它的具体位置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 2&gt;/dev/null 是为了忽略权限不足的错误提示
find / -name &quot;libxxx.so&quot; 2&gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果发现库文件存在但是&lt;code&gt;ldd&lt;/code&gt;找不到，说明其所在的目录不在动态链接器的搜索路径中，可以使用上面介绍的方法将其加入链接器的搜索路径。&lt;/p&gt;
&lt;p&gt;还有一种常见的错误是库文件被成功加载了，但是在执行的过程中，程序需要调用某个函数或者访问某个变量时，发现该符号在库中不存在，典型的报错信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./your_executable: symbol lookup error: ./your_executable: undefined symbol: _Zxyz...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者在编译的时候出现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;undefined reference to ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;遇到这种问题，可以使用&lt;code&gt;nm&lt;/code&gt;工具列出目标文件或者库文件中的符号，关键是使用 &lt;code&gt;-D&lt;/code&gt; 选项来查看动态符号表，因为只有这里的符号才能被外部链接。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 检查 libxxx.so 是否导出了你需要的符号
nm -D /path/to/libxxx.so | grep &apos;your_function_name&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++为了支持函数重载，会把函数名修饰成复杂的符号，因此&lt;code&gt;nm&lt;/code&gt;显式的是修饰后的名字，为了查看原始的函数签名，可以使用&lt;code&gt;c++fit&lt;/code&gt;解码，可以将 &lt;code&gt;nm&lt;/code&gt; 的输出通过管道传给 &lt;code&gt;c++filt&lt;/code&gt; 来查看原始的函数签名。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# -n 表示 c++filt 尝试将输入作为符号名处理
nm -D /path/to/libxxx.so | c++filt -n
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>C++ 中的 malloc、free 和 new、delete</title><link>https://liangbm3.site/blog/c-zhong-de-malloc-free-he-new-delete</link><guid isPermaLink="true">https://liangbm3.site/blog/c-zhong-de-malloc-free-he-new-delete</guid><description>总结一下C++的malloc、free 和 new、delete这些关键字常见的问题</description><pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Collapse } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;在C++中，&lt;code&gt;malloc&lt;/code&gt;、&lt;code&gt;new&lt;/code&gt;、&lt;code&gt;free&lt;/code&gt;、&lt;code&gt;delete&lt;/code&gt;经常放在一起对比，这里以问答的形式来看看这些关键字的一些细节。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    // 2. 检查分配是否成功
    if (p_int == NULL) 
    {
        printf(&quot;内存分配失败！\n&quot;);
        return 1; // 返回错误码
    }
    ```
    `free`只需要指定刚刚返回的指针即可，不需要指定大小。
    ```cpp
    free(p_int);
    p_int = NULL; 
    ```
    下面是一个分配数组的例子：
    ```cpp
    int *arr = NULL;
    int n = 5;

    // 1. 为一个包含 n 个整数的数组分配内存
    arr = (int*)malloc(n * sizeof(int));

    // 2. 检查分配是否成功
    if (arr == NULL) 
    {
        printf(&quot;数组内存分配失败！\n&quot;);
        return 1;
    }

    printf(&quot;为 %d 个整数分配了内存。\n&quot;, n);

    // 3. 使用内存

    // 4. 释放整个数组的内存
    free(arr);
    printf(&quot;数组内存已释放。\n&quot;);
    
    arr = NULL;
    ```
- `new`在使用时直接指定要分配的数据类型即可，编译器会自动计算所需的大小，成功时会返回一个对应类型的指针，无需任何转换，如果分配失败则会默认抛出`std::bad_alloc`异常，我们可以使用`try-catch`去捕捉异常。
    ```cpp
    int* p_int1 = new int;//*p_int1 的值是未定义的
    int* p_int2 = new int();//申请内存，并进行初始化
    int* p_int3 = new int(123);//申请内存，并指定一个具体的初始值
    ```
    `new`申请的内存，我们需要用`delete`去释放，只需指定变量名即可：
    ```cpp
    delete p_int1;
    delete p_int2;
    delete p_int3;
    delete p_double;
    ```
    我们可以使用`new[]`去申请一个数组：
    ```cpp
    int* p_array1 = new int[SIZE];// 默认情况下，数组中的元素是未初始化的。
    int* p_array2 = new int[SIZE]{};//可以使用花括号 {} 对数组进行值初始化（所有元素为 0）
    ```
    使用`new[]`申请的数组需要使用`delete[]`去进行释放：
    ```cpp
    delete[] p_array1;
    delete[] p_array2;
    ```
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;内存处理的差别：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;malloc&lt;/code&gt;分配的内存是未初始化的，它仅仅是一块原始的、内容不确定的内存空间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new&lt;/code&gt;分配的内存是初始化的。对于基本数据类型，初始化的行为取决于&lt;code&gt;new&lt;/code&gt;的使用形式，例如&lt;code&gt;new int()&lt;/code&gt;会进行值初始化为0，而&lt;code&gt;new int&lt;/code&gt;则不保证。对于类类型对象，&lt;code&gt;new&lt;/code&gt;会在分配内存后自动调用该类的构造函数，完成对象的初始化工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;可重载性：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;malloc&lt;/code&gt;和&lt;code&gt;free&lt;/code&gt;作为C库函数，通常是不可重载的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new&lt;/code&gt;和&lt;code&gt;delete&lt;/code&gt;作为C++的操作符，是可以进行重载的，所以我们可以很方便地定制内存分配策略，比如使用内存池。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;// 简洁且安全的写法
delete ptr; // 如果 ptr 是 nullptr，这里什么也不做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;/Collapse&gt;

&amp;#x3C;Collapse title=&apos;对一个指针 delete 两次会发生什么？&apos;&gt; 
对一个指针delete两次会导致未定义行为，程序可能会因为内存访问冲突（段错误）而终止，第二次delete可能会破坏内存管理器的内部数据结构，程序不会立即崩溃，后续再申请内存时，由于堆内存结构已经被破坏，会导致莫名的崩溃或者数据错乱。

双重删除避免的传统方法：
```cpp
MyClass* p = new MyClass();

delete p;
p = nullptr;//把指针置为空，这是一个好习惯

// 后续如果不小心再次 delete
delete p; // 这里 p 是 nullptr，所以这行代码是安全的，什么也不会发生。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用指针指针，能够避免手动管理内存，避免发生这种问题：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;memory&gt;

void myFunction() {
    std::unique_ptr&amp;#x3C;MyClass&gt; p = std::make_unique&amp;#x3C;MyClass&gt;();
    // ... 使用 p ...
} // p 离开作用域，MyClass 对象被自动删除，绝不会发生双重删除
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>C++ 中的 RAII 思想</title><link>https://liangbm3.site/blog/c-zhong-de-raii-si-xiang</link><guid isPermaLink="true">https://liangbm3.site/blog/c-zhong-de-raii-si-xiang</guid><description>总结一下C++中的RAII思想</description><pubDate>Mon, 30 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RAII&lt;/strong&gt; 是 &lt;strong&gt;“Resource Acquisition Is Initialization”&lt;/strong&gt; 的缩写，中文通常翻译为 &lt;strong&gt;“资源获取即初始化”&lt;/strong&gt;。这是 C++ 语言中的一种重要编程范式。&lt;/p&gt;
&lt;h2&gt;1. 核心思想&lt;/h2&gt;
&lt;p&gt;RAII的核心思想是将资源的生命周期与对象的生命周期绑定起来。具体来说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;获取资源&lt;/strong&gt;：在对象的构造函数中获取资源。当对象被成功创建时，资源也就被成功获取了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用资源&lt;/strong&gt;：在对象的生命周期内，可以通过对象的方法来使用该资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放资源&lt;/strong&gt;：在对象的析构函数中释放资源。当对象离开其作用域（例如，函数返回、对象被 delete、栈展开等）时，其析构函数会自动被调用，从而确保资源被释放。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. RAII的优点&lt;/h2&gt;
&lt;p&gt;由于C/C++是需要手动管理资源的语言，保证在任何情况下都能正确释放资源是一个非常大的挑战，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;函数提前返回&lt;/strong&gt;：一个函数可能有多个返回路径，函数可能提前返回，导致资源没有被正确释放。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void process_data_bad(int size) 
{
    // 获取资源
    int* data = (int*)malloc(size * sizeof(int));
    // 检查一个条件，可能导致提前返回
    if (size &amp;#x3C;= 0) 
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Invalid size, returning early.\n&quot;;
        // 错误：在这里返回，但没有释放内存！
        return;
    }
    // ... 使用 data ...
    std::cout &amp;#x3C;&amp;#x3C; &quot;Processing data...\n&quot;;
    //手动释放资源
    free(data);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常抛出&lt;/strong&gt;：如果在获取资源后，在释放前发生了异常，正常的清理代码会被跳过，导致资源发生泄露。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void process_with_exception_bad() 
{
    // 获取资源
    char* buffer = new char[1024];
    std::cout &amp;#x3C;&amp;#x3C; &quot;Buffer allocated.\n&quot;;
    try 
    {
        // ... 使用 buffer ...
        do_some_work(true); // 模拟一个操作抛出异常
        // 异常发生后，下面的代码不会执行
        std::cout &amp;#x3C;&amp;#x3C; &quot;This line will not be printed.\n&quot;;
        delete[] buffer; // 释放代码被跳过！
    } 
    catch (const std::exception&amp;#x26; e) 
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Caught exception: &quot; &amp;#x3C;&amp;#x3C; e.what() &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
        // 我们在这里捕获了异常，但 buffer 已经泄漏了！
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂的逻辑分支&lt;/strong&gt;：在多层的&lt;code&gt;if-else&lt;/code&gt;循环中，要确保所有可能的代码路径上都释放资源，代码会变得非常混乱且容易出错。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void complex_logic_bad(const char* filename, int condition) 
{
    // 获取资源
    FILE* file = fopen(filename, &quot;w&quot;);
    if (!file) 
    {
        return;
    }
    if (condition == 1) 
    {
        fprintf(file, &quot;Condition 1&quot;);
        // ... 更多操作
        if (/* 某个子条件 */ true) {
            fclose(file); // 清理点 1
            return;
        }
    } 
    else if (condition == 2) 
    {
        for (int i = 0; i &amp;#x3C; 5; ++i) 
        {
            if (i == 3) 
            {
                fprintf(file, &quot;Breaking loop&quot;);
                fclose(file); // 清理点 2
                return; // 用 return 代替 break
            }
        }
    } 
    else 
    {
        fprintf(file, &quot;Default condition&quot;);
        // 假设这里一切正常
    }
    // 最终的清理点
    fclose(file); // 清理点 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这些情况下，忘记或者无法调用释放资源的代码都会导致资源泄漏。而通过RAII的思想可以避免以上情况的发生，RAII主要有以下优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自动资源管理和防止泄漏&lt;/strong&gt;：这是 RAII最核心的优点。资源在对象销毁时自动释放，作为程序员的我们不需要手动调用释放资源的函数，如&lt;code&gt;free()&lt;/code&gt;、&lt;code&gt;fclose()&lt;/code&gt;、&lt;code&gt;close_socket()&lt;/code&gt;、&lt;code&gt;pthread_mutex_unlock()&lt;/code&gt;等，这可以极大降低忘记释放资源导致资源泄漏的风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常安全&lt;/strong&gt;：如果在资源被获取后、显示释放代码执行前发生异常，传统的&lt;code&gt;try-catch-finally&lt;/code&gt;很容易出错或者发生遗漏。如果我们使用RAII，异常发生会导致栈展开，作用域内的析构函数会被异常调用，保证了资源能够被正确释放。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码简洁和可读性&lt;/strong&gt;：RAII把资源管理都封装在了专门的类中，使得业务代码逻辑代码更加干净整洁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. RAII的经典例子&lt;/h2&gt;
&lt;p&gt;C++标准库本身就是RAII思想的最佳实践者，下面是一些经典示例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;智能指针&lt;/strong&gt;：&lt;code&gt;std::unique_ptr&lt;/code&gt;，&lt;code&gt;std::shared_ptr&lt;/code&gt;和&lt;code&gt;std::weak_ptr&lt;/code&gt;是RAII的完美体现。它们在构造时获取动态分配的内存（或接管已分配的内存），并在析构时自动释放这块内存。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;memory&gt;
#include &amp;#x3C;stdexcept&gt;

void good_function() 
{
    // 资源获取（内存分配）即初始化
    std::unique_ptr&amp;#x3C;int&gt; ptr(new int(42));

    // ... 其它操作
    if (/* 某个条件失败 */) 
    {
        // ptr 离开作用域，其析构函数被调用，自动 delete 内存
        return;
    }
    if (/* 某个操作抛出异常 */) 
    {
        // 发生异常，栈展开，ptr 的析构函数被调用，自动 delete 内存
        throw std::runtime_error(&quot;Something went wrong!&quot;);
    }
    // ... 其它操作

} // 函数正常结束，ptr 离开作用域，析构函数被调用，自动 delete 内存
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件处理&lt;/strong&gt;：&lt;code&gt;std::fstream&lt;/code&gt;，&lt;code&gt;std::ifstream&lt;/code&gt;，&lt;code&gt;std::ofstream&lt;/code&gt;类就是RAII的例子。它们在构造时打开文件，在析构时自动关闭文件。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;fstream&gt;
#include &amp;#x3C;string&gt;
#include &amp;#x3C;iostream&gt;

void write_to_file(const std::string&amp;#x26; filename) 
{
    // 构造 ofstream 对象时，文件被打开（获取资源）
    std::ofstream file(filename);
    if (!file.is_open()) 
    {
        std::cerr &amp;#x3C;&amp;#x3C; &quot;Failed to open file.\n&quot;;
        return;
    }

    file &amp;#x3C;&amp;#x3C; &quot;Hello, RAII!\n&quot;;
    // ... 可能发生异常

} // 函数结束，file 对象被销毁，其析构函数自动调用 file.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;互斥锁&lt;/strong&gt;：&lt;code&gt;std::lock_guard&lt;/code&gt;和&lt;code&gt;std::unique_lock&lt;/code&gt;是用于管理互斥锁的RAII包装器。它们在构造时锁定互斥锁，在析构时自动解锁互斥锁，确保即使在临界区发生异常，锁也会被释放，避免死锁。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;mutex&gt;

std::mutex mtx;

void safe_critical_section() 
{
    // 构造 lock_guard 时，自动调用 mtx.lock()
    std::lock_guard&amp;#x3C;std::mutex&gt; guard(mtx);

    // ... 执行临界区代码
    if (/* 某个条件 */) 
    {
        // guard 离开作用域，其析构函数自动调用 mtx.unlock()
        return;
    }
    // ... 即使这里抛出异常，析构函数也会被调用

} // 函数结束，guard 被销毁，析构函数自动调用 mtx.unlock()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. RAII类的实现&lt;/h2&gt;
&lt;p&gt;如果我们想实现一个RAII类，一般的步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;确定资源&lt;/strong&gt;：明确要使用这个类管理什么资源，这个资源必须有清晰的获取和释放操作，例如：&lt;code&gt;fopen/fclose&lt;/code&gt;、&lt;code&gt;new/delete&lt;/code&gt;、&lt;code&gt;lock/unlock&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定义类骨架&lt;/strong&gt;：定义一个类，并将代表资源的句柄作为其私有成员变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现构造函数&lt;/strong&gt;：在构造函数中执行获取资源的操作，构造函数应该接收所有获取资源所必须的参数，获取资源后将句柄存放在成员变量中。同时要处理资源获取失败的情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现析构函数&lt;/strong&gt;：在析构函数中执行释放资源的操作，析构函数应该检查句柄是否有效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理所有权&lt;/strong&gt;：通常来说，应该RAII对象应该独占它所管理的资源，所以我们应该禁止拷贝构造函数和拷贝赋值运算符，防止两个对象管理同一个资源，避免一个资源被释放两次。通常我们还会实现移动构造函数和移动赋值运算符，允许资源所有权从一个对象转移到另一个对象，既高效又安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供访问接口&lt;/strong&gt;：RAII类是资源的“管理者”，而不是资源本身。需要提供方法让外部能够使用被管理的资源，例如提供一个&lt;code&gt;get()&lt;/code&gt;方法返回原始句柄，或者重载 &lt;code&gt;-&gt;&lt;/code&gt;和&lt;code&gt;*&lt;/code&gt;运算符。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是创建一个RAII类管理C风格的文件句柄的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;cstdio&gt;    //管理文件的C函数：FILE、fopen、fclose、fputs
#include &amp;#x3C;utility&gt;   //引入std::move
#include &amp;#x3C;stdexcept&gt; //异常处理

class FileGuard
{
private:
    FILE *m_file_handle = nullptr; // 用来存储文件句柄
public:
    // 在构造函数中获取资源，析构函数接收文件名和打开模式作为参数
    explicit FileGuard(const char *filename, const char *mode)
    {
        // 获取资源
        m_file_handle = fopen(filename, mode);
        // 如果fopen失败，抛出异常
        if (m_file_handle == nullptr)
        {
            throw std::runtime_error(&quot;Failed to open file: &quot; + std::string(filename));
        }
        printf(&quot;FileGuard: File &apos;%s&apos; opened.\n&quot;, filename);
    }
    // 在析构函数中释放资源
    ~FileGuard()
    {
        if (m_file_handle != nullptr)
        {
            printf(&quot;FileGuard: Closing file.\n&quot;);
            fclose(m_file_handle);
            m_file_handle = nullptr; // 将句柄设为nullptr为了防止意外使用
        }
    }
    // 删除拷贝构造函数
    FileGuard(const FileGuard &amp;#x26;) = delete;
    // 删除拷贝赋值运算符
    FileGuard &amp;#x26;operator=(const FileGuard &amp;#x26;) = delete;
    // 实现移动构造函数
    FileGuard(FileGuard &amp;#x26;&amp;#x26;other) noexcept : m_file_handle(other.m_file_handle)
    {
        // 这里把源对象的析构函数的句柄置为nullptr，源对象析构的时候就不会关闭文件了
        other.m_file_handle = nullptr;
        printf(&quot;FileGuard: Ownership moved (move constructor).\n&quot;);
    }
    // 实现移动赋值运算符
    FileGuard &amp;#x26;operator=(FileGuard &amp;#x26;&amp;#x26;other) noexcept
    {
        // 防止自我赋值，例如：my_file=std::move(my_file)
        if (this != &amp;#x26;other)
        {
            // 释放当前对象可能持有的资源
            if (m_file_handle != nullptr)
            {
                fclose(m_file_handle);
            }
            // 转移资源所有权
            m_file_handle = other.m_file_handle;
            other.m_file_handle = nullptr;
            printf(&quot;FileGuard: Ownership moved (move assignment).\n&quot;);
        }
        return *this;
    }
    // 提供访问结果，返回文件句柄，便于进行文件操作
    FILE *get() const
    {
        return m_file_handle;
    }
    // 提供一个简单的方法，判断文件是否成功打开
    explicit operator bool() const
    {
        return m_file_handle != nullptr;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://csguide.cn/cpp/memory/raii_in_cpp.html&quot;&gt;C++ RAII 思想机制详解 | 编程指北-计算机学习指南&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>C++ 的 sizeof 运算符</title><link>https://liangbm3.site/blog/c-de-sizeof-yun-suan-fu</link><guid isPermaLink="true">https://liangbm3.site/blog/c-de-sizeof-yun-suan-fu</guid><description>总结了一下C++的sizeof运算符</description><pubDate>Sun, 29 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;1. &lt;code&gt;sizeof&lt;/code&gt;运算符简介&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sizeof&lt;/code&gt;是C++中的一个关键字和一元运算符，其作用是取得数据类型或者表达式的大小（以Byte为单位），求得结果的类型是&lt;code&gt;std::size_t&lt;/code&gt;。它是一个编译时运算符，在绝大多数情况下是在编译时进行运算（在运行时运算的特例后面会提及）。&lt;code&gt;sizeof&lt;/code&gt;运算符的语法有两种形式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对类型使用：必须使用括号
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;sizeof(type)
&lt;/code&gt;&lt;/pre&gt;
例如：&lt;code&gt;sizeof(int)&lt;/code&gt;、&lt;code&gt;sizeof(double)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对表达式使用：括号是可选的
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;sizeof expression
// 或者
sizeof(expression)
&lt;/code&gt;&lt;/pre&gt;
例如：&lt;code&gt;sizeof(x)&lt;/code&gt;、&lt;code&gt;sizeof(x+1)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. &lt;code&gt;sizeof&lt;/code&gt;运算符的常见用法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sizeof&lt;/code&gt;运算符可以求得基本数据类型的大小，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;sizeof(char);//结果为1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对变量使用&lt;code&gt;sizeof&lt;/code&gt;，可以求得该变量类型的大小，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int a;
sizeof a;//结果为4
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对数组使用&lt;code&gt;sizeof&lt;/code&gt;，可以求得整个数组的长度，一个常见的用法是计算数组中元素的数量，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int arr[] = {1, 2, 3, 4, 5, 6, 7};
int numElements = sizeof(arr) / sizeof(arr[0]);
std::cout &amp;#x3C;&amp;#x3C; &quot;Number of elements: &quot; &amp;#x3C;&amp;#x3C; numElements &amp;#x3C;&amp;#x3C; std::endl; // 输出: 7
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对指针变量使用&lt;code&gt;sizeof&lt;/code&gt;，可以求得指针本身的大小。对引用使用&lt;code&gt;sizeof&lt;/code&gt;，可以求得被引用对象的大小，而不是引用本身的大小，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int* p = nullptr;
sizeof p;//结果为8（64位系统）或4（32位系统）

int x = 10;
int&amp;#x26; ref = x;
sizeof ref;//结果为4
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对类或者结构体使用&lt;code&gt;sizeof&lt;/code&gt;，可以求得该类或者结构体的一个对象占用的总字节数，通常包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非静态成员变量的大小&lt;/li&gt;
&lt;li&gt;编译器为了内存对齐而额外添加的字节&lt;/li&gt;
&lt;li&gt;如果存在虚函数，会包含一个指向虚函数表的虚指针大小&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态成员变量的大小，因为静态成员变量不属于任何一个对象，而是由所有对象共享&lt;/li&gt;
&lt;li&gt;成员函数的大小，因为成员函数存储在代码段，不占用对象的内存空间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;// 在一个典型的64位系统上，sizeof(MyClass) 可能是 24 字节
// (vptr: 8) + (int a: 4) + (char b: 1) + (padding: 3) + (double c: 8) = 24&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;在C++中，由于任何对象都必须有一个唯一地址，因此空类和结构体的大小至少为1字节，例如：
```cpp
class Empty {};
std::cout &amp;#x3C;&amp;#x3C; &quot;Size of Empty class: &quot; &amp;#x3C;&amp;#x3C; sizeof(Empty) &amp;#x3C;&amp;#x3C; std::endl; // 输出: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. &lt;code&gt;sizeof&lt;/code&gt;运算符的一些特性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sizeof&lt;/code&gt;是运算符而不是函数，这是&lt;code&gt;sizeof&lt;/code&gt;最基本的特性。因此我们通常不会采用参数和返回值的说法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sizeof&lt;/code&gt;不能求得&lt;code&gt;void&lt;/code&gt;类型的长度，因为&lt;code&gt;void&lt;/code&gt;类型是没有大小的，我们也无法声明&lt;code&gt;void&lt;/code&gt;类型的变量。&lt;code&gt;sizeof&lt;/code&gt;能够求得&lt;code&gt;void&lt;/code&gt;类型的指针的长度，因为&lt;code&gt;void&lt;/code&gt;类型的指针是有大小的。
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;sizeof(void);//非法
sizeof(void*)//合法
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;对任何类型的指针变量使用&lt;code&gt;sizeof&lt;/code&gt;，得到的永远是指针本身的大小，而不是它所指向的对象或内存块的大小。例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int arr[10];
int *p = arr;
std::cout &amp;#x3C;&amp;#x3C; sizeof(arr) &amp;#x3C;&amp;#x3C; std::endl; // 输出：10 * sizeof(int) = 40
std::cout &amp;#x3C;&amp;#x3C; sizeof(p) &amp;#x3C;&amp;#x3C; std::endl;   // 输出：指针大小，通常是 4 或 8 字节
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;在函数参数中，数组会退化成指针，因为在函数参数传递时，数组被退化成指针了，例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void func(int arr[10]) 
{
    std::cout &amp;#x3C;&amp;#x3C; sizeof(arr) &amp;#x3C;&amp;#x3C; std::endl; // 实际是 sizeof(int*)，而不是数组大小
}
int main() {
    int arr[10];
    func(arr);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;表达式不会被求值，但是会返回表达式计算结果的类型大小。例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int x = 5;
double y = 5.0;
std::cout &amp;#x3C;&amp;#x3C; sizeof(x++) &amp;#x3C;&amp;#x3C; std::endl; // 结果为4，x 仍然等于 5
std::cout &amp;#x3C;&amp;#x3C; sizeof(x+y) &amp;#x3C;&amp;#x3C; std::endl; //结果为8，求得的是double的大小
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;动态内存只能求得指针大小，无法获取内存块的大小，例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int *p = new int[10];
std::cout &amp;#x3C;&amp;#x3C; sizeof(p) &amp;#x3C;&amp;#x3C; std::endl; // 结果是 4 或 8，无法获取数组总大小
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sizeof&lt;/code&gt;用在函数调用上时，实际上不会调用函数，只会求得函数的返回类型所占用的字节数，例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int foo() 
{
    std::cout &amp;#x3C;&amp;#x3C; &quot;foo() 被调用&quot; &amp;#x3C;&amp;#x3C; std::endl;
    return 42;
}

int main() {
    std::cout &amp;#x3C;&amp;#x3C; sizeof(foo()) &amp;#x3C;&amp;#x3C; std::endl;//输出结果为4
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
这是因为&lt;code&gt;sizeof&lt;/code&gt;的运算是在编译器完成的，不会产生任何的运行时行为。注意&lt;code&gt;sizeof&lt;/code&gt;不能对&lt;code&gt;void&lt;/code&gt;函数使用，因为返回值&lt;code&gt;void&lt;/code&gt;是没有大小的，编译会报错。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sizeof&lt;/code&gt;不能用来获取结构体中单个位域成员的大小，&lt;code&gt;sizeof&lt;/code&gt;只能针对类型或者整个结构体，不能直接用于单个位域成员，例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Test 
{
    unsigned int a : 3;  // 3 bits
    unsigned int b : 5;  // 5 bits
};

int main() 
{
    Test t;
    std::cout &amp;#x3C;&amp;#x3C; sizeof(t.a) &amp;#x3C;&amp;#x3C; std::endl; // 输出 4（因为 a 是 unsigned int 类型）
    std::cout &amp;#x3C;&amp;#x3C; sizeof(t) &amp;#x3C;&amp;#x3C; std::endl;   // 输出结构体的总大小，通常会考虑对齐
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sizeof&lt;/code&gt;不能对不完整类型的数组求长度，会导致编译错误，例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int arr[];  // 只声明，不定义（大小未知）
int main() {
    std::cout &amp;#x3C;&amp;#x3C; sizeof(arr) &amp;#x3C;&amp;#x3C; std::endl;  // 编译错误，类型不完整
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;对字面量使用&lt;code&gt;sizeof&lt;/code&gt;会包含&lt;code&gt;\0&lt;/code&gt;，例如：
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::cout &amp;#x3C;&amp;#x3C; sizeof(&quot;hello&quot;) &amp;#x3C;&amp;#x3C; std::endl; // 输出 6，包括 &apos;\0&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. &lt;code&gt;sizeof&lt;/code&gt;的其他常见问题&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sizeof&lt;/code&gt;是有可能是在运行时计算的，这只发生在一种非常特殊的情况下，即对变长数组使用&lt;code&gt;sizeof&lt;/code&gt;时。在标准的C++中，&lt;code&gt;sizeof&lt;/code&gt;的结果始终都是一个编译时常量，编译器在编译代码时就已经确定了所有类型的大小，并将&lt;code&gt;sizeof&lt;/code&gt;的结果替换成为一个具体数值。&lt;/p&gt;
&lt;p&gt;变成数组是C99语言的一个特性，它允许使用一个变量来指定数组的长度：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 这是 C 语言代码，不是标准 C++
void foo(int n) {
    int array[n]; // n 的值在运行时才知道，所以 array 是一个变长数组
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是我们会发现，我们在C++这样写不会报错，因为一些主流的C++编译器（特别是GCC和Clang）为了向后兼容C语言，把VLA作为一种非标准的语言扩展也引入到了C++中。所以在这种情况下，对变长数组使用&lt;code&gt;sizeof&lt;/code&gt;是运行时进行计算的。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/ybqjymy/p/16494352.html&quot;&gt;C/C++中 sizeof 详解 - 一杯清酒邀明月 - 博客园&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>C++ 中的内存对齐</title><link>https://liangbm3.site/blog/c-zhong-de-nei-cun-dui-qi</link><guid isPermaLink="true">https://liangbm3.site/blog/c-zhong-de-nei-cun-dui-qi</guid><description>介绍计算机中的内存对齐原理和C++中结构体的内存对齐</description><pubDate>Sat, 28 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;1. 内存对齐简介&lt;/h2&gt;
&lt;p&gt;在讨论内存对齐之前，我们先来看一下计算机底层是怎么对数据进行存取的，这涉及到计算机组成原理的知识。&lt;/p&gt;
&lt;p&gt;这主要涉及计算机中的三大总线：地址总线、数据总线和控制总线。地址总线决定数据的地址，数据总线里面传输的是数据，控制总线传输的是命令和状态信号。数据总线的宽度直接决定了一个CPU周期可以同时传送多少位。CPU的设计是高度协同的，寄存器和数据总线的宽度一般相同（Intel早期的8086的寄存器宽度为16位，总线宽度为8位），我们平时所说的xx位的计算机实际上指的是CPU寄存器的宽度，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;32位计算机：CPU寄存器和数据总线的宽度都为32位&lt;/li&gt;
&lt;li&gt;64位计算机：CPU寄存器和数据总线的宽度都为64位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现代计算机中，内存空间的最小单位是字节，因为CPU的地址总线以字节为&lt;strong&gt;最小寻址粒度&lt;/strong&gt;。但是，大部分的处理器并不是按字节来存取内存的，一般会以2字节，4字节，8字节来存取内存，我们称之为&lt;strong&gt;内存存取粒度&lt;/strong&gt;。这个存取的宽度就取决于我们上面介绍的总线宽度，例如32位计算机每次传输的是32位（4字节），而64位计算机每次传输的是64位（8字节）。&lt;/p&gt;
&lt;p&gt;以32位计算机为例，假设我们要在&lt;code&gt;0x0001&lt;/code&gt;这个地址中写入一个字符&lt;code&gt;&apos;A&apos;&lt;/code&gt;（1字节）。CPU会将&lt;code&gt;0X0000&lt;/code&gt;放在地址总线，让内存控制器知道目标在&lt;code&gt;0x0000&lt;/code&gt;到&lt;code&gt;0x0003&lt;/code&gt;那个内存块，然后会将字符&lt;code&gt;&apos;A&apos;&lt;/code&gt;放在数据总线的对应位置，即4字节块中的第二个字节，CPU会使能第二天字节使能线，这个控制指令放在控制总线中，这样CPU可以在对应的位置写入而不影响内存块中的其他字节。&lt;/p&gt;
&lt;p&gt;再举个例子，假设在一个32位的计算机中，要读取一个地址为&lt;code&gt;0x1001&lt;/code&gt;的&lt;code&gt;int&lt;/code&gt;变量，那么这个4字节的&lt;code&gt;int&lt;/code&gt;变量占用的内存是&lt;code&gt;0x1001&lt;/code&gt; - &lt;code&gt;0x1005&lt;/code&gt;。由于32位计算机的内存存取粒度是4，因此该处理器只能从地址为4的倍数的内存开始读取数据。第一次会读取&lt;code&gt;0x1000&lt;/code&gt; - &lt;code&gt;0x1003&lt;/code&gt;，剔除不需要的字节（&lt;code&gt;0x1000&lt;/code&gt;），然后第二次读取&lt;code&gt;0x1004&lt;/code&gt; - &lt;code&gt;0x1007&lt;/code&gt;，剔除不要的数据（&lt;code&gt;0x1005&lt;/code&gt;、&lt;code&gt;0x1006&lt;/code&gt;、&lt;code&gt;0x1007&lt;/code&gt;），最后将数据合并完成读取。&lt;/p&gt;
&lt;p&gt;那&lt;strong&gt;内存对齐&lt;/strong&gt;是什么呢？其实内存对齐是一种优化技术，旨在提高CPU访问内存的速度，从以上的例子可以看出，虽然&lt;code&gt;int&lt;/code&gt;的大小等于内存存取粒度，但是由于存放的位置，CPU需要进行两次读取。而内存对齐则是对数据再内存中存放的位置的限制，通常会要求数据的首地址的值是某个数k的倍数。例如如果规定&lt;code&gt;int&lt;/code&gt;的首地址只能是4的倍数，那么对int的读取就可以一次性完成。&lt;/p&gt;
&lt;h2&gt;2. 内存对齐规则&lt;/h2&gt;
&lt;p&gt;一些核心概念如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自身对齐值&lt;/strong&gt;：这是数据类型本身固有的对齐要求，通常等于其 sizeof 的大小。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;char&lt;/code&gt;: 1 字节&lt;/li&gt;
&lt;li&gt;&lt;code&gt;short&lt;/code&gt;: 2 字节&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int&lt;/code&gt;: 4 字节&lt;/li&gt;
&lt;li&gt;&lt;code&gt;float&lt;/code&gt;: 4 字节&lt;/li&gt;
&lt;li&gt;&lt;code&gt;double&lt;/code&gt;: 8 字节&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long&lt;/code&gt;: 4 字节（32为系统）或 8 字节（64位系统）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long long&lt;/code&gt;: 8 字节&lt;/li&gt;
&lt;li&gt;指针: 4 字节（32位系统）或 8 字节（64位系统）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指定对齐值&lt;/strong&gt;：这是程序员通过编译器指令（如 C/C++ 中的 &lt;code&gt;#pragma pack(n)&lt;/code&gt;） 手动设定的对齐值。n 的值通常是1, 2, 4, 8等2的幂。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有效对齐值 (Effective Alignment)&lt;/strong&gt;：这是编译器最终为某个数据成员采用的对齐值。它的计算规则是：
有效对齐值 = Min(自身对齐值, 指定对齐值)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有的数据结构，无论是&lt;code&gt;struct&lt;/code&gt;还是&lt;code&gt;union&lt;/code&gt;，在布局时都遵循以下三条规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;结构体中每个成员相对于结构体起始地址的偏移量，必须是其&lt;strong&gt;有效对齐值&lt;/strong&gt;的整数倍。如果不是，编译器会在前一个成员后面填充若干字节以满足要求。&lt;/li&gt;
&lt;li&gt;结构体（或联合体）的总大小，必须是其所有成员中&lt;strong&gt;最大的有效对齐值&lt;/strong&gt;的整数倍。如果不是，编译器会在最后一个成员后面填充若干字节以满足要求。&lt;/li&gt;
&lt;li&gt;如果一个结构体包含了另一个嵌套结构体，那么这个嵌套结构体的起始偏移量，必须是其内部所有成员中&lt;strong&gt;最大的有效对齐值&lt;/strong&gt;的整数倍。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 内存对齐举例&lt;/h2&gt;
&lt;p&gt;在64位系统中，一个结构体定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct MyStruct {
    char a;         // 自身对齐1
    int b;          // 自身对齐4
    double c;       // 自身对齐8
    short d;        // 自身对齐2
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一个成员&lt;code&gt;a&lt;/code&gt;，有效对齐值为1，当前偏移量为0，是1的倍数，因此当前大小为1字节。&lt;/li&gt;
&lt;li&gt;第二个成员&lt;code&gt;b&lt;/code&gt;，有效对齐值4，当前偏移量为1，不是4的倍数，前面要填充3字节，因此当前大小为8字节。&lt;/li&gt;
&lt;li&gt;第三个成员&lt;code&gt;c&lt;/code&gt;，有效对齐值为8，当前偏移量为8，是8的倍数，因此当前大小为16字节。&lt;/li&gt;
&lt;li&gt;第四个成员&lt;code&gt;d&lt;/code&gt;，有效对齐值为2，当前偏移量为10，是2的倍数，因此当前大小为18字节。&lt;/li&gt;
&lt;li&gt;在结构体中，最大有效对齐值为8，18不是8的倍数，需要填充到24，后面填充6个字节。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此这个结构体在内存中的布局如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image.BC6sshNO_1NvDGo.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;下面举一个指定对齐值的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#pragma pack(push, 2) // 设置指定对齐值为2
struct MyStructPacked {
    char a;         // 自身对齐1-&gt;有效对齐min(1,2)=1
    int b;          // 自身对齐4-&gt;有效对齐min(4,2)=2
    double c;       // 自身对齐8-&gt;有效对齐min(8,2)=2
    short d;        // 自身对齐2-&gt;有效对齐min(2,2)=2
};
#pragma pack(pop) // 恢复默认
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一个成员&lt;code&gt;a&lt;/code&gt;，有效对齐值为1，当前偏移量为0，是1的倍数，因此当前大小为1字节。&lt;/li&gt;
&lt;li&gt;第二个成员&lt;code&gt;b&lt;/code&gt;，有效对齐值为2，当前偏移量为1，不是2的倍数，前面要填充1字节，因此当前大小为6字节。&lt;/li&gt;
&lt;li&gt;第三个成员&lt;code&gt;c&lt;/code&gt;，有效对齐值为2，当前偏移量为6，是2的倍数，因此当前大小为14字节。&lt;/li&gt;
&lt;li&gt;第四个成员&lt;code&gt;d&lt;/code&gt;，有效对齐值为2，当前偏移量为14，是2的倍数，因此当前大小为16字节。&lt;/li&gt;
&lt;li&gt;在结构体中，最大有效对齐值为2，16是8的倍数，不需要再填充，因此总大小为16字节。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此这个结构体在内存中的布局如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-1.c5CYaYFp_Z2vUm1E.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>二叉树的遍历小结</title><link>https://liangbm3.site/blog/er-cha-shu-de-bian-li-xiao-jie</link><guid isPermaLink="true">https://liangbm3.site/blog/er-cha-shu-de-bian-li-xiao-jie</guid><description>总结一下二叉树的遍历</description><pubDate>Thu, 19 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;1. 二叉树的遍历方式&lt;/h2&gt;
&lt;p&gt;二叉树的遍历方式可以分为深度遍历和广度遍历两种。&lt;/p&gt;
&lt;p&gt;所谓的深度遍历，就是深度优先的原则，先把一条路径走到头再回头，对于二叉树就是选择一条路径走到叶子节点之后再考虑其他路径。二叉树的深度遍历又可以分为前序遍历、中序遍历、后序遍历三种，这三种遍历方式的访问顺序分别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前序遍历：根节点-&gt;左子树-&gt;右子树&lt;/li&gt;
&lt;li&gt;中序遍历：左子树-&gt;根节点-&gt;右子树&lt;/li&gt;
&lt;li&gt;后序遍历：左子树-&gt;右子树-&gt;根节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而所谓的广度遍历，就是广度优先的原则。对于二叉树来说，就是一层一层地访问，所以又叫层序遍历。&lt;/p&gt;
&lt;p&gt;以下面这个二叉树为例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image.Ct1ZfTvk_Z176NHD.webp&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前序遍历：&lt;code&gt;1-&gt;2-&gt;4-&gt;5-&gt;6-&gt;7-&gt;3-&gt;8-&gt;9&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;中序遍历：&lt;code&gt;4-&gt;2-&gt;6-&gt;5-&gt;7-&gt;1-&gt;3-&gt;9-&gt;8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;后序遍历：&lt;code&gt;4-&gt;6-&gt;7-&gt;5-&gt;2-&gt;9-&gt;8-&gt;3-&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;层序遍历：&lt;code&gt;1-&gt;2-&gt;3-&gt;4-&gt;5-&gt;8-&gt;6-&gt;7-&gt;9&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 二叉树遍历的代码实现&lt;/h2&gt;
&lt;p&gt;在遍历之前，先定义节点，以及构建上面示例的二叉树。&lt;/p&gt;
&lt;p&gt;节点定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//节点定义
struct TreeNode
{
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int val):val(val),left(nullptr),right(nullptr){}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建二叉树：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;TreeNode* root = new TreeNode(1);
root-&gt;left = new TreeNode(2);
root-&gt;right = new TreeNode(3);
root-&gt;left-&gt;left = new TreeNode(4);
root-&gt;left-&gt;right = new TreeNode(5);
root-&gt;right-&gt;right = new TreeNode(8);
root-&gt;left-&gt;right-&gt;left=new TreeNode(6);
root-&gt;left-&gt;right-&gt;right=new TreeNode(7);
root-&gt;right-&gt;right-&gt;left=new TreeNode(9);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在下面的代码中，我们打印出节点的值来表示&lt;strong&gt;真正的访问&lt;/strong&gt;（下面统称为&lt;strong&gt;记录&lt;/strong&gt;）。那什么不是真正的访问呢？对于上面举例的那棵二叉树，后序遍历先访问节点4，但是在代码中我们需要先经过1-&gt;2-&gt;4这样的路径到达4。&lt;/p&gt;
&lt;p&gt;但是在经过1和2的时候，我们没有记录节点的值，并不是“真正的访问”。&lt;/p&gt;
&lt;h3&gt;2.1 前序遍历&lt;/h3&gt;
&lt;p&gt;先来看递归版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//前序遍历递归版本
void preorder_recursion(TreeNode* root)
{
    if(root==nullptr)
    {
        return ;
    }
    cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;
    preorder_recursion(root-&gt;left);
    preorder_recursion(root-&gt;right);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为深度优先的的原则通常是用递归来实现的，因此递归的代码非常简单。当到达某个节点时，我们将它的&lt;code&gt;val&lt;/code&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-cpp&quot;&gt;//前序遍历迭代版本
void preorder_iteration(TreeNode* root)
{
    stack&amp;#x3C;TreeNode*&gt; st;
    while(!st.empty()||root!=nullptr)
    {
        while(root!=nullptr)
        {
            cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;//记录根节点的值
            //将根节点入栈，这里入栈是为了访问完左子树之后，能够根据根节点去访问右子树
            st.push(root);
            root=root-&gt;left;//访问左子树，
        }
        //左子树访问完之后，根节点出栈
        root=st.top();
        st.pop();
        root=root-&gt;right;//访问右子树
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.1 中序遍历&lt;/h3&gt;
&lt;p&gt;先来看递归版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//中序遍历递归版本
void inorder_recursion(TreeNode* root)
{
    if(root==nullptr)
    {
        return ;
    }
    inorder_recursion(root-&gt;left);
    cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;
    inorder_recursion(root-&gt;right);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;中序遍历的递归版本的代码和前序遍历非常相似，只是&lt;strong&gt;记录&lt;/strong&gt;节点的位置不同。根据定义，先访问左子树，再访问根节点，最后再访问右子树，因此记录根节点的位置在中间。&lt;/p&gt;
&lt;p&gt;中序遍历的迭代版本如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//中序遍历迭代版本
void inorder_iteration(TreeNode* root)
{
    stack&amp;#x3C;TreeNode*&gt; st;
    while(!st.empty()||root!=nullptr)
    {
        while(root!=nullptr)
        {
            //根节点入栈，为了能够访问完左子树之后，能够访问根节点和右子树
            st.push(root);
            root=root-&gt;left;//访问左子树
        }
        root=st.top();
        st.pop();
        cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;//记录根节点的值
        root=root-&gt;right;//访问右子树
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;中序遍历和前序遍历的迭代版本也非常相似，只是&lt;strong&gt;记录&lt;/strong&gt;根节点的顺序不同。根据定义，中序遍历先访问左子树再访问根节点，因此记录根节点的位置放在了出栈的时候而不是入栈的时候。&lt;/p&gt;
&lt;p&gt;其实这可以结合递归版本来理解的。前序遍历的递归版本在调用递归函数访问左子树之前，已经&lt;strong&gt;记录&lt;/strong&gt;了根节点的值，而调用递归函数恰恰是入栈的过程。而中序遍历的递归版本在调用递归函数访问左子树之后，再&lt;strong&gt;记录&lt;/strong&gt;根节点的值，即出栈之后。&lt;/p&gt;
&lt;h3&gt;2.3 后序遍历&lt;/h3&gt;
&lt;p&gt;先来看递归版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//后序遍历递归版本
void postorder_recursion(TreeNode* root)
{
    if(root==nullptr)
    {
        return ;
    }
    postorder_recursion(root-&gt;left);
    postorder_recursion(root-&gt;right);
    cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;递归版本非常好理解，递归访问完左子树和右子树之后，&lt;strong&gt;记录&lt;/strong&gt;根节点的值。&lt;/p&gt;
&lt;p&gt;迭代版本如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//后序遍历迭代版本
void postorder_iteration(TreeNode* root)
{
    stack&amp;#x3C;TreeNode*&gt; st;
    TreeNode* prev=nullptr;
    while (!st.empty()||root!=nullptr)
    {
        while(root!=nullptr)//递归访问左子树
        {
            st.push(root);
            root=root-&gt;left;
        }
        //访问完左子树之后
        root=st.top();//根节点出栈
        st.pop();
        //判断右子树是否已经访问了，如果右子树为空或者右子树已经访问了
        //则记录根节点的值。
        if(root-&gt;right==nullptr||prev==root-&gt;right)
        {
            cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;
            prev=root;
            //这里的置空是为了在下一次循环中从栈中获取节点进行处理
            //而不是继续处理当前节点，因为记录了当前节点之后，
            //说明左右子树的节点都已经被处理完了
            root=nullptr;
        }
        //否则访问右子树
        else
        {
            st.push(root);
            root=root-&gt;right;
        }
    }
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后序遍历的迭代版本是所有的遍历中最难理解的。我们先从后序遍历的递归版本看起，递归版本先递归访问左子树，再递归访问右子树后，再&lt;strong&gt;记录&lt;/strong&gt;根节点的值。
递归访问左子树在迭代中的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;while(root!=nullptr)
{
    st.push(root);
    root=root-&gt;left;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这部分代码就是实现了递归访问左子树。后序遍历为什么和前序遍历和中序遍历有这么大差别呢？主要原因是记录值的位置不同，前序遍历在递归访问左子树前记录根节点的值，中序遍历在递归访问左子树后记录根节点的值。而后序遍历，在递归访问完左子树后，再递归访问右子树和记录节点的值。&lt;/p&gt;
&lt;p&gt;在递归代码中，&lt;strong&gt;“再递归访问右子树和记录节点的值”&lt;/strong&gt; 这个是很容易实现的，而在迭代版本中，递归的返回即是根节点的出栈，这时我们就需要判断右子树是否已经访问过了。如何判断呢？这里的方法是引入一个名为&lt;code&gt;prev&lt;/code&gt;的辅助变量，记录上一次访问的节点。如果从栈弹出的节点的右孩子等于 &lt;code&gt;prev&lt;/code&gt; 这个辅助变量，
说明访问的就是右子树，可以对当前节点进行&lt;strong&gt;记录&lt;/strong&gt;了，否则需要访问右子树。&lt;/p&gt;
&lt;p&gt;对于后序遍历，网上还有一种做法利用一种异构的前序遍历(中-&gt;右-&gt;左)遍历二叉树后，再将结果翻转得到后序遍历的结果。这种做法看似优雅，其实本质上还是前序遍历，而不是后序遍历。&lt;strong&gt;遍历算法的本质，不是看最后输出的那一串有序的数字的顺序，而是一种有序的内存访问顺序。&lt;/strong&gt; 失去了访问顺序，即使用各种数据结构恢复这个次序，遍历的本身也毫无意义。&lt;/p&gt;
&lt;h3&gt;2.4 层序遍历&lt;/h3&gt;
&lt;p&gt;层序遍历是广度优先的思想，广度优先遍历一般借助队列实现，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//层序遍历
void levelorder(TreeNode* root)
{
    if(root==nullptr)
    {
        return;
    }
    queue&amp;#x3C;TreeNode*&gt; q;
    q.push(root);
    while(!q.empty())
    {
        int sz=q.size();//获取当前层的节点个数
        for(int i=0;i&amp;#x3C;sz;i++)//对当前层进行遍历
        {
            TreeNode* node=q.front();
            q.pop();
            cout&amp;#x3C;&amp;#x3C;node-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;
            if(node-&gt;left) q.push(node-&gt;left);
            if(node-&gt;right) q.push(node-&gt;right);
        }
        cout&amp;#x3C;&amp;#x3C;endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 代码汇总&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;stack&gt;
#include &amp;#x3C;queue&gt;

using namespace std;

//节点定义
struct TreeNode
{
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int val):val(val),left(nullptr),right(nullptr){}
};

//前序遍历递归版本
void preorder_recursion(TreeNode* root)
{
    if(root==nullptr)
    {
        return ;
    }
    cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;
    preorder_recursion(root-&gt;left);
    preorder_recursion(root-&gt;right);
}

//前序遍历迭代版本
void preorder_iteration(TreeNode* root)
{
    stack&amp;#x3C;TreeNode*&gt; st;
    while(!st.empty()||root!=nullptr)
    {
        while(root!=nullptr)
        {
            cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;//记录根节点的值
            //将根节点入栈，这里入栈是为了访问完左子树之后，能够根据根节点去访问右子树
            st.push(root);
            root=root-&gt;left;//访问左子树，
        }
        //左子树访问完之后，根节点出栈
        root=st.top();
        st.pop();
        root=root-&gt;right;//访问右子树
    }
}

//中序遍历递归版本
void inorder_recursion(TreeNode* root)
{
    if(root==nullptr)
    {
        return ;
    }
    inorder_recursion(root-&gt;left);
    cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;
    inorder_recursion(root-&gt;right);
}

//中序遍历迭代版本
void inorder_iteration(TreeNode* root)
{
    stack&amp;#x3C;TreeNode*&gt; st;
    while(!st.empty()||root!=nullptr)
    {
        while(root!=nullptr)
        {
            //根节点入栈，为了能够访问完左子树之后，能够访问根节点和右子树
            st.push(root);
            root=root-&gt;left;//访问左子树
        }
        root=st.top();
        st.pop();
        cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;//访问根节点
        root=root-&gt;right;//访问右子树
    }
}

//后序遍历递归版本
void postorder_recursion(TreeNode* root)
{
    if(root==nullptr)
    {
        return ;
    }
    postorder_recursion(root-&gt;left);
    postorder_recursion(root-&gt;right);
    cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;
}

//后序遍历迭代版本
void postorder_iteration(TreeNode* root)
{
    stack&amp;#x3C;TreeNode*&gt; st;
    TreeNode* prev=nullptr;
    while (!st.empty()||root!=nullptr)
    {
        while(root!=nullptr)//递归访问左子树
        {
            st.push(root);
            root=root-&gt;left;
        }
        //访问完左子树之后
        root=st.top();//根节点出栈
        st.pop();
        //判断右子树是否已经访问了，如果右子树为空或者右子树已经访问了
        //则记录根节点的值。
        if(root-&gt;right==nullptr||prev==root-&gt;right)
        {
            cout&amp;#x3C;&amp;#x3C;root-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;
            prev=root;
            //这里的置空是为了在下一次循环中从栈中获取节点进行处理
            //而不是继续处理当前节点，因为记录了当前节点之后，
            //说明左右子树的节点都已经被处理完了
            root=nullptr;
        }
        //否则访问右子树
        else
        {
            st.push(root);
            root=root-&gt;right;
        }
    }
    
}

//层序遍历
void levelorder(TreeNode* root)
{
    if(root==nullptr)
    {
        return;
    }
    queue&amp;#x3C;TreeNode*&gt; q;
    q.push(root);
    while(!q.empty())
    {
        int sz=q.size();//获取当前层的节点个数
        for(int i=0;i&amp;#x3C;sz;i++)//对当前层进行遍历
        {
            TreeNode* node=q.front();
            q.pop();
            cout&amp;#x3C;&amp;#x3C;node-&gt;val&amp;#x3C;&amp;#x3C;&quot; &quot;;
            if(node-&gt;left) q.push(node-&gt;left);
            if(node-&gt;right) q.push(node-&gt;right);
        }
        cout&amp;#x3C;&amp;#x3C;endl;
    }
}

//删除二叉树
void deleteTree(TreeNode* node) {
    if (node == nullptr) return;
    
    deleteTree(node-&gt;left);
    deleteTree(node-&gt;right);
    
    delete node;
}

int main() {
    // 构建二叉树
    TreeNode* root = new TreeNode(1);
    root-&gt;left = new TreeNode(2);
    root-&gt;right = new TreeNode(3);
    root-&gt;left-&gt;left = new TreeNode(4);
    root-&gt;left-&gt;right = new TreeNode(5);
    root-&gt;right-&gt;right = new TreeNode(8);
    root-&gt;left-&gt;right-&gt;left=new TreeNode(6);
    root-&gt;left-&gt;right-&gt;right=new TreeNode(7);
    root-&gt;right-&gt;right-&gt;left=new TreeNode(9);

    cout&amp;#x3C;&amp;#x3C;&quot;前序遍历递归版本：&quot;&amp;#x3C;&amp;#x3C;endl;
    preorder_recursion(root);
    cout&amp;#x3C;&amp;#x3C;endl;

    cout&amp;#x3C;&amp;#x3C;&quot;前序遍历迭代版本：&quot;&amp;#x3C;&amp;#x3C;endl;
    preorder_iteration(root);
    cout&amp;#x3C;&amp;#x3C;endl;

    cout&amp;#x3C;&amp;#x3C;&quot;中序遍历递归版本：&quot;&amp;#x3C;&amp;#x3C;endl;
    inorder_recursion(root);
    cout&amp;#x3C;&amp;#x3C;endl;

    cout&amp;#x3C;&amp;#x3C;&quot;中序遍历迭代版本：&quot;&amp;#x3C;&amp;#x3C;endl;
    inorder_iteration(root);
    cout&amp;#x3C;&amp;#x3C;endl;

    cout&amp;#x3C;&amp;#x3C;&quot;后序遍历递归版本：&quot;&amp;#x3C;&amp;#x3C;endl;
    postorder_recursion(root);
    cout&amp;#x3C;&amp;#x3C;endl;

    cout&amp;#x3C;&amp;#x3C;&quot;后序遍历迭代版本：&quot;&amp;#x3C;&amp;#x3C;endl;
    postorder_iteration(root);
    cout&amp;#x3C;&amp;#x3C;endl;

    //层序遍历
    cout&amp;#x3C;&amp;#x3C;&quot;层序遍历版本：&quot;&amp;#x3C;&amp;#x3C;endl;
    levelorder(root);
    cout&amp;#x3C;&amp;#x3C;endl;

    deleteTree(root);

    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;前序遍历递归版本：
1 2 4 5 6 7 3 8 9 
前序遍历迭代版本：
1 2 4 5 6 7 3 8 9 
中序遍历递归版本：
4 2 6 5 7 1 3 9 8 
中序遍历迭代版本：
4 2 6 5 7 1 3 9 8 
后序遍历递归版本：
4 6 7 5 2 9 8 3 1 
后序遍历迭代版本：
4 6 7 5 2 9 8 3 1 
层序遍历版本：
1 
2 3 
4 5 8 
6 7 9 
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>原码、补码和反码</title><link>https://liangbm3.site/blog/yuan-ma-bu-ma-he-fan-ma</link><guid isPermaLink="true">https://liangbm3.site/blog/yuan-ma-bu-ma-he-fan-ma</guid><description>本文主要介绍原码、反码和补码以及它们之间的关系</description><pubDate>Sun, 26 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;在计算机中，数字以二进制的形式存储，分为有&lt;strong&gt;符号数&lt;/strong&gt;和&lt;strong&gt;无符号数&lt;/strong&gt;。顾名思义，有符号数是带符号的，比如正负。无符号数是不带符号的，其范围为：$0\le x\le 2^n-1$。&lt;/p&gt;
&lt;p&gt;为了表示一个有符号数，除了数值位以外还应指定符号位，通常以这个数的最高位为符号位，并规定0表示正数的符号“+”；1表示负数的符号“-”。这样，数的符号在机器中也数字化了，符号位和数值位一起完整地表示有符号二进制数。&lt;/p&gt;
&lt;p&gt;我们把一个数在机器中的表示形式称为机器数，而把它们的实际数值称为机器数的&lt;strong&gt;真值&lt;/strong&gt;。计算机中，有符号的二进制数可由不同的编码形式表示，它们是原码、补码与反码。下面来详细讨论它们。&lt;/p&gt;
&lt;h2&gt;2. 原码&lt;/h2&gt;
&lt;p&gt;原码表示法是一种简单的机器数表示法，即符号和数值表示法。&lt;strong&gt;原码就是符号位加上真值的绝对值&lt;/strong&gt;。以&lt;code&gt;+66&lt;/code&gt;和&lt;code&gt;-66&lt;/code&gt;为例：&lt;/p&gt;
&lt;p&gt;两者的真值均为：&lt;code&gt;1000010&lt;/code&gt;，由于以0表示符号为正，以1表示符号为负。因此&lt;code&gt;+66&lt;/code&gt;的原码为&lt;code&gt;01000010&lt;/code&gt;，&lt;code&gt;-66&lt;/code&gt;的原码为&lt;code&gt;11000010&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;原码的表示范围为：
$$
-(2^{n-1}-1)\le X \le +(2^{n-1}-1)
$$
原码简单易于理解，但是有一个缺点，那就是进行加减运算时很麻烦。参加运算的数可能为正，也可能为负。不仅要考虑运算是加还是减，还要考虑数的绝对值大小。例如，在进行两数相加时，我们要判断：&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;00000000&lt;/code&gt;和&lt;code&gt;10000000&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;正是存在以上种种弊端，我们才引入补码表示法，能够使正、负数的加法和减法运算简化成单一的相加运算。&lt;/p&gt;
&lt;h2&gt;3. 反码&lt;/h2&gt;
&lt;p&gt;正数的反码就是真值本身，负数的反码在原码的基础上，对符号位之外的数按位求反即可。反码的表示范围和原码相同：
$$
-(2^{n-1}-1)\le X \le +(2^{n-1}-1)
$$
在反码中，0也有两个编码，反码一般来说很少使用。&lt;/p&gt;
&lt;h2&gt;4. 补码&lt;/h2&gt;
&lt;p&gt;正数的原码、反码和补码都是相同的。求补码的步骤如下（以&lt;code&gt;-66&lt;/code&gt;为例）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表示成原码：&lt;code&gt;11000010&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;求反码：&lt;code&gt;10111101&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;加1：&lt;code&gt;10111110&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补码具有如下性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$X$为正数时，补码和原码相同；当$X$为负数时，负数的补码等于$2^n+X=2^n-|X|$&lt;/li&gt;
&lt;li&gt;+0和-0的补码是相同的，因此在补码中，0的表示是唯一的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补码的表示范围为：
$$
-2^{n-1}\le X \le 2^{n-1}
$$
补码具有以下运算性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加法：$[X+Y]&lt;em&gt;{补}=[X]&lt;/em&gt;{补}+[Y]_{补}$&lt;/li&gt;
&lt;li&gt;减法：$[X-Y]&lt;em&gt;{补}=[X]&lt;/em&gt;{补}-[Y]&lt;em&gt;{补}$，或$[X-Y]&lt;/em&gt;{补}=[X]&lt;em&gt;{补}+[-Y]&lt;/em&gt;{补}$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 参考资料&lt;/h2&gt;
&lt;p&gt;《微机原理、汇编语言与接口技术》人民邮电出版社&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>进位计数制及数制之间的互转</title><link>https://liangbm3.site/blog/jin-wei-ji-shu-zhi-ji-shu-zhi-zhi-jian-de-hu-zhuan</link><guid isPermaLink="true">https://liangbm3.site/blog/jin-wei-ji-shu-zhi-ji-shu-zhi-zhi-jian-de-hu-zhuan</guid><description>本文主要介绍进位计数制和数制之间的相互转换。</description><pubDate>Sat, 25 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1.前言&lt;/h2&gt;
&lt;p&gt;本文主要介绍进位计数制和数制之间的相互转换。&lt;/p&gt;
&lt;h2&gt;2. 进位计数制&lt;/h2&gt;
&lt;p&gt;按照进位的方法进行计数，称为&lt;strong&gt;进位计数制&lt;/strong&gt;。在进位制中每个数规定使用的数码符号的数量，称为&lt;strong&gt;进位基数&lt;/strong&gt;，例如十进制的进位基数是10，二进制的进位基数是2。使用$R$为基数的计数制称为$R$进制数，常用的有十进制数、二进制数、十六进制数和八进制数。&lt;/p&gt;
&lt;p&gt;我们习惯把最右边一位称为最低位，最左边一位称为最高位。权值是进位基数从右至左呈指数规律增加，最低位权值位$R^0$，第$i$位的权值为$R^i$。这样就可以把$R$进制中数$N$写成按权展开的多项式：
$$
N_R = a_{n-1}R^{n-1} + \cdots + a_iR^i + \cdots + a_0R^0 + a_{-1}R^{-1} + a_{-2}R^{-2} + \cdots + a_{-m}R^{-m}
$$&lt;/p&gt;
&lt;h3&gt;2.1 十进制数(Decimal)&lt;/h3&gt;
&lt;p&gt;十进制是我们非常熟悉的数制，它的进位基数是10。各位的权值就是我们通常所说的“个”、“十”、“百”、“千”、“万”等。以&lt;code&gt;520.1314&lt;/code&gt;为例：&lt;/p&gt;
&lt;p&gt;| 数码 |   5    |   2    |   0    |     1     |     3     |     1     |     4     |
| :--: | :----: | :----: | :----: | :-------: | :-------: | :-------: | :-------: |
| 权值 | $10^2$ | $10^1$ | $10^0$ | $10^{-1}$ | $10^{-2}$ | $10^{-3}$ | $10^{-4}$ |
| 总计 |  500   |   20   |   0    |    0.1    |   0.03    |   0.001   |  0.0004   |&lt;/p&gt;
&lt;h3&gt;2.2 二进制数(Binary)&lt;/h3&gt;
&lt;p&gt;计算机采用的是“0”和“1”两个基本符号组成的二进制码，因为计算机内部记忆信息的设备由两个状态的器件组成，因而计算机内部的任何信息只能用“0”或“1”这两个状态来表示。17世纪至18世纪的德国数学家莱布尼茨，是世界上第一个提出二进制记数法的人。&lt;/p&gt;
&lt;p&gt;二进制的基数为2，逢2进1。以&lt;code&gt;0101.1110&lt;/code&gt;为例：&lt;/p&gt;
&lt;p&gt;| 数码 |    0    |    1    |    0    |    1    |    1     |    1     |    1     |    0     |
| ---- | :-----: | :-----: | :-----: | :-----: | :------: | :------: | :------: | :------: |
| 权值 | $2^{3}$ | $2^{2}$ | $2^{1}$ | $2^{0}$ | $2^{-1}$ | $2^{-2}$ | $2^{-3}$ | $2^{-4}$ |
| 总计 |    0    |    4    |    0    |    1    |   0.5    |   0.25   |  0.125   |    0     |&lt;/p&gt;
&lt;p&gt;所以该数表示的是10进制中的：0+4+0+1+0.5+0.25+0.125+0=5.875&lt;/p&gt;
&lt;p&gt;同时，二进制的运算具有如下法则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加法
&lt;ul&gt;
&lt;li&gt;0 + 0 = 0&lt;/li&gt;
&lt;li&gt;0 + 1 = 1&lt;/li&gt;
&lt;li&gt;1 + 0 = 1&lt;/li&gt;
&lt;li&gt;1 + 1 = 0, 向下一位进1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;乘法
&lt;ul&gt;
&lt;li&gt;0×0=0&lt;/li&gt;
&lt;li&gt;1×0=0&lt;/li&gt;
&lt;li&gt;0×1=0&lt;/li&gt;
&lt;li&gt;1×1=1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;减法
&lt;ul&gt;
&lt;li&gt;0 - 0=0&lt;/li&gt;
&lt;li&gt;1 - 0=1&lt;/li&gt;
&lt;li&gt;1 - 1=0，&lt;/li&gt;
&lt;li&gt;0 - 1=1，向前一位借1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;除法（除数只能为1）
&lt;ul&gt;
&lt;li&gt;0÷1=0&lt;/li&gt;
&lt;li&gt;1÷1=1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 十六进制(Hexadecimal)&lt;/h3&gt;
&lt;p&gt;十六进制的基数为16，是一种逢16进1的进位制。通常用数字0、1、2、3、4、5、6、7、8、9和字母A、B、C、D、E、F（a、b、c、d、e、f）表示，其中:A ~ F表示10 ~ 15。以&lt;code&gt;58C.B2&lt;/code&gt;为例：&lt;/p&gt;
&lt;p&gt;| 数码 |   5    |   8    |   C    |     B     |     2     |
| :--: | :----: | :----: | :----: | :-------: | :-------: |
| 权值 | $16^2$ | $16^1$ | $16^0$ | $16^{-1}$ | $16^{-2}$ |
| 总计 |  1280  |  128   |   12   |  0.6875   | 0.0078125 |&lt;/p&gt;
&lt;p&gt;所以该数表示的是十进制中的：1280+128+12+0.6875+0.0078125=1420.6953125&lt;/p&gt;
&lt;h3&gt;2.4 八进制(Octal)&lt;/h3&gt;
&lt;p&gt;八进制的基数为8，采用0，1，2，3，4，5，6，7八个数字，逢八进1。以&lt;code&gt;520.1&lt;/code&gt;为例：&lt;/p&gt;
&lt;p&gt;| 数码 |   5   |   2   |   0   |    1     |
| :--: | :---: | :---: | :---: | :------: |
| 权值 | $8^2$ | $8^1$ | $8^0$ | $8^{-1}$ |
| 总计 |  320  |  16   |   0   |  0.125   |&lt;/p&gt;
&lt;p&gt;所以该数表示的是10进制中的：320+16+0+0.125=336.125&lt;/p&gt;
&lt;h2&gt;3. 数制间的转换&lt;/h2&gt;
&lt;p&gt;其实上面在介绍数制时，我们已经可以窥见它们之间的转换关系，下面进一步明确和总结。&lt;/p&gt;
&lt;h3&gt;3.1 非十进制数转换为十进制数&lt;/h3&gt;
&lt;p&gt;这一过程非常简单，由上面的介绍我们可以知道如下的关系式：
$$
a_n R^n + a_{n-1} R^{n-1} + \cdots + a_i R^i + \cdots + a_0 = d_m 10^m + d_{m-1} 10^{m-1} + \cdots + d_i 10^i + \cdots + d_1 10 + d_0
$$
我们只需要将$R^i$和$a^i$用十进制表示，然后作十进制运算即可。这样描述可能比较难以理解，我们举个例子：
$$
(1011.101)&lt;em&gt;2 = 1 \times 2^3 + 1 \times 2^1 + 1 \times 2^0 + 1 \times 2^{-1} + 1 \times 2^{-3} = (11.625)&lt;/em&gt;{10}
$$
其他进制也同理：
$$
(3B6)&lt;em&gt;{16} = 3 \times 16^2 + 11 \times 16^1 + 6 = 768 + 176 + 6 = (950)&lt;/em&gt;{10}
$$&lt;/p&gt;
&lt;h3&gt;3.2 十进制数转换为非十进制数&lt;/h3&gt;
&lt;p&gt;最常用的方法是“除R取余法”：即只需将要被转换的十进制数，连续除以R，直至商等于零为止。第一次除法的余数是$a_0$，而最后一次除法的余数为$a_n$，将$a_n$~$a_1$从高到低排列得$a_n...a_1$，即为所求R进制数。&lt;/p&gt;
&lt;p&gt;将$(251)_{10}$转换为二进制数的过程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20250125110158089.DtBjXIKK_Z1nHz2E.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;结果为：$(11111011)_2$&lt;/p&gt;
&lt;p&gt;将$(251)_{10}$转换为十六进制数的过程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20250125105454708.nv3s6Zz4_23rjfU.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;结果为：$(FB)_{16}$&lt;/p&gt;
&lt;p&gt;把十进制小数转换为相应R进制小数时，可以采用“乘R取整法”：即对该小数或乘以基数R后所得的新的小数部分进行乘以基数R的操作，所得整数为R进制的小数位，第一次乘法的余数是$K_{-1}$，而最后一次乘法的余数为$K_{-n}$，将$K_{-1}$~$K_{-n}$，从高到低排列得$K_{-1}…K_{-n}$，即为所求R进制小数。&lt;/p&gt;
&lt;p&gt;将$(0.6875)_{10}$转换为二进制数过程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20250125110217953.Cwxy5bT3_Z2wuwqe.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;结果为：$(0.1011)_2$&lt;/p&gt;
&lt;p&gt;将$(0.6875)_{10}$转换为十六进制数过程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20250125110434438.BWhhoX66_e87AN.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;结果为：$(0.BH)_{16}$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;并不是所有的小数都能在不同的进制中进行转换，因为不同进制的精度是不一样的，有时候会出现除不尽的情况。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;将$(0.734)_{10}$转换为二进制数过程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20250125123611984.BjFWWzhA_1Uu6pW.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;结果为：$(0.101110)_2$（无穷小数）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;综上，如果任意一个十进制数要转换为非十进制数，可以把整数部分和小数部分分别加以转换，然后把转换后的整数部分和小数部分相加即可。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;上述的办法不是唯一的转换方法，上述方法非常利于人类进行计算，因为它只涉及除以2，但是这种方法对于机器来说是不理想的。首先这种方法运用的是除法，机器在进行除法运算的时候是非常耗时的；其次机器需要存储计算出的比特，以便稍后以相反的顺序打印。下面以将$(148)_{10}$转换为二进制数为例，说明另一种方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先从小于148的2的最大次幂——128开始比较&lt;/li&gt;
&lt;li&gt;148 $\ge$ 128，因此该位为1，148-128=20&lt;/li&gt;
&lt;li&gt;20&amp;#x3C;64，因此该位为0，20&lt;/li&gt;
&lt;li&gt;20&amp;#x3C;32，因此该位为0，20&lt;/li&gt;
&lt;li&gt;20 $\ge$ 16，因此该位为1，20-16=4&lt;/li&gt;
&lt;li&gt;4&amp;#x3C;8，因此该位为0，4&lt;/li&gt;
&lt;li&gt;4 $\ge$ 4，因此该位为1，4-4=0&lt;/li&gt;
&lt;li&gt;0&amp;#x3C;2，因此该位为0，0&lt;/li&gt;
&lt;li&gt;0&amp;#x3C;1，因此该位为0，0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以最后的结果为：10010100&lt;/p&gt;
&lt;p&gt;&lt;em&gt;如果含有小数部分，也可以仿照该方法进行逐级比较。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这种方法对于人类来说在小数字时（例如 8 位二进制数）相当简单。对于机器来说也很高效，因为每个位只需要需要比较、减法和赋值。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当数字较小，我们手动计算进行转换时，两种方法都可。如果数字较大，我们进行手动计算时选用第一种方法，编写代码时选用第二种方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.3 二进制转换为八进制、十六进制&lt;/h3&gt;
&lt;p&gt;二进制转换为八进制、十六进制的方法为&lt;strong&gt;分组转换&lt;/strong&gt;。如果转换为八进制，则从左开始，三位一组，不足补零；如果转换为十六进制，则从右开始，四位一组，不足补零。下面以将$(10110110)_2$转换为八进制和十六进制为例。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;转换为八进制：
$$
(\underline{010}:\underline{110}:\underline{110})_2
$$&lt;/p&gt;
&lt;p&gt;$$
(\underline{2}:\underline{6}:\underline{6})_{8}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转换为十六进制：
$$
(\underline{1011}:\underline{0110})_2
$$&lt;/p&gt;
&lt;p&gt;$$
(\underline{B}:\underline{6})_{16}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.4 八进制、十六进制转换为二进制&lt;/h3&gt;
&lt;p&gt;八进制、十六进制转换为二进制的方法也非常简单。如果是八进制转换为二进制，只需要将八进制的每一位用3位的二进制表示即可；如果是十六进制转换位二进制，只需要将十六进制的每一位用4位的二进制表示即可。&lt;/p&gt;
&lt;p&gt;将$(57)_{8}$转换为二进制：
$$
(\underline{5}:\underline{7})_8
$$&lt;/p&gt;
&lt;p&gt;$$
(\underline{101}:\underline{111})_2
$$&lt;/p&gt;
&lt;p&gt;将$(A9)$转换为二进制：
$$
(\underline{A}:\underline{9})_{16}
$$&lt;/p&gt;
&lt;p&gt;$$
(\underline{1011}:\underline{1010})_{2}
$$&lt;/p&gt;
&lt;h3&gt;3.5 八进制和十六进制的相互转换&lt;/h3&gt;
&lt;p&gt;八进制和十六进制的相互转换可以通过二进制为中介进行转换。非常简单，这里不再举例。&lt;/p&gt;
&lt;h2&gt;4. 参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;《微机原理、汇编语言与接口技术》人民邮电出版社&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.learncpp.com/cpp-tutorial/converting-integers-between-binary-and-decimal-representation/&quot;&gt;O.4 — Converting integers between binary and decimal representation – Learn C++&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>四旋翼无人机基础知识和基本概念</title><link>https://liangbm3.site/blog/si-xuan-yi-wu-ren-ji-ji-chu-zhi-shi-he-ji-ben-gai-nian</link><guid isPermaLink="true">https://liangbm3.site/blog/si-xuan-yi-wu-ren-ji-ji-chu-zhi-shi-he-ji-ben-gai-nian</guid><description>本文主要介绍四旋翼无人机的一些基础知识和基本概念</description><pubDate>Mon, 13 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 飞行器的分类&lt;/h2&gt;
&lt;p&gt;飞行器主要分为固定翼、直升机和多旋翼，这里介绍的是多旋翼&lt;/p&gt;
&lt;h2&gt;2. 四旋翼的总体组成&lt;/h2&gt;
&lt;p&gt;四旋翼主要组成部分包括机架、动力系统和指挥控制系统。下面进行一一介绍。&lt;/p&gt;
&lt;h3&gt;2.1 机架&lt;/h3&gt;
&lt;h4&gt;2.1.1 机身&lt;/h4&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;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;材料：以碳纤维居多&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.1.2 起落架&lt;/h4&gt;
&lt;p&gt;具有支撑作用，可以避免起飞离地太近，着陆时可以起缓冲作用&lt;/p&gt;
&lt;h4&gt;2.1.3 涵道&lt;/h4&gt;
&lt;p&gt;保护螺旋桨和人身安全，提高桨叶拉力效率和减少噪音（伯努利原理）&lt;/p&gt;
&lt;h3&gt;2.2 动力系统&lt;/h3&gt;
&lt;h4&gt;2.2.1 螺旋桨&lt;/h4&gt;
&lt;p&gt;螺旋桨的参数有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;型号&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设螺旋桨在一种不能流动的介质中旋转，那么螺旋桨每转一圈,就会向前进一个距离，就称为螺距或桨距(Propeller Pitch)。&lt;/li&gt;
&lt;li&gt;螺旋桨一般用4个数字表示，其中前面2位是螺旋桨的直径，后面2位是螺旋桨的螺距。比如：1045桨的直径为10英寸，而螺距为4.5英寸。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;弦长：一般选择半径2/3处的弦长作为标称弦长&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转动惯量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;桨叶数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全转速&lt;/p&gt;
&lt;/li&gt;
&lt;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;/li&gt;
&lt;li&gt;动平衡是指螺旋桨重心与其惯性中心重合时的平衡状态&lt;/li&gt;
&lt;li&gt;进行静平衡和动平衡的目的是减少振动&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2.2 电机&lt;/h4&gt;
&lt;p&gt;电机以无刷电机为主，无刷电机的原理：电机里面一般有多匝线圈，对线圈交替通电可以周而复始驱动转子转动。&lt;/p&gt;
&lt;p&gt;电机的参数有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尺寸：电机的尺寸取决于定子的大小，由一个四位数字来表示。例如2212(或写成22×12)电机，前两个数字代表定子直径(单位mm),后两个数字代表定子高度(单位mm)，因此2212电机表示电机定子直径是22mm，定子高度为12mm。&lt;/li&gt;
&lt;li&gt;标称空载KV：空载情况下，外加1V电压得到的转速值，大型螺旋桨可以选用KV值较小的电机，小型螺旋桨可选用KV值较大的电机。&lt;/li&gt;
&lt;li&gt;标称空载电流和电压&lt;/li&gt;
&lt;li&gt;最大电流/功率&lt;/li&gt;
&lt;li&gt;内阻&lt;/li&gt;
&lt;li&gt;电机效率&lt;/li&gt;
&lt;li&gt;总力效&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2.3 电调&lt;/h4&gt;
&lt;p&gt;电调全称电子调速器，英文Electronic Speed Control，简称ESC。最基本的功能是电机调速，同时为遥控接收器其他通道的舵机供电。&lt;/p&gt;
&lt;p&gt;电调的参数有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最大持续/峰值电流：不同电机需要配备不同安数的电调，安数不足会导致电调甚至电机烧毁&lt;/li&gt;
&lt;li&gt;电压范围：一般在电调说明书上可以看到标注例如“3-4S LiPo”字样，表示这个电调适用于3到 4节电芯串联的锂聚合物电池，也就是说它的电压范围为11.1V~14.8V&lt;/li&gt;
&lt;li&gt;内阻&lt;/li&gt;
&lt;li&gt;刷新效率&lt;/li&gt;
&lt;li&gt;可编程特性&lt;/li&gt;
&lt;li&gt;兼容性&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2.4 电池&lt;/h4&gt;
&lt;p&gt;常见的有锂聚合物电池（LiPo）和镍氢电池（NiMh）&lt;/p&gt;
&lt;p&gt;电池的参数如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;电压
&lt;ul&gt;
&lt;li&gt;锂电池组包含两部分：电池和锂电池保护线路。单节电压3.7V。&lt;/li&gt;
&lt;li&gt;3S1P表示3片锂聚合物电池的串联，电压是11.1V，其中：S是串联，P表示并联。&lt;/li&gt;
&lt;li&gt;2S2P电池表示2片锂聚合物电池的串联，然后两个这样的串联结构并联，总电压是7.4V，容量是单个电池的两倍。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;容量：在实际多旋翼飞行过程中，有两种方式检测电池的剩余容量是否满足飞行安全的要求。一种方式是检测电池单节电压，另一种方式是实时检测电池输出电流做积分计算。&lt;/li&gt;
&lt;li&gt;放电倍率&lt;/li&gt;
&lt;li&gt;内阻&lt;/li&gt;
&lt;li&gt;能量密度&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 指挥控制系统&lt;/h3&gt;
&lt;h4&gt;2.3.1 遥控器和接收器&lt;/h4&gt;
&lt;p&gt;指标参数如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;频率：常用的无线电频率是72MHz与2.4GHz，目前采用的最多的是2.4GHz遥控器。2.4G微波的直线性很好，尽量避免遥控模型与发射机之间有很大的障碍物（如房屋及仓库等）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调制方式：PCM&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通道：一般有六通道、十通道等。多旋翼在控制过程中需要控制的动作路数有：油门、偏航、俯仰、滚转，所以至少得四个通道遥控器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制模式：美国手和日本手&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;油门&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;称直接式油门：油门杆不会自动回中，最低点为0%油门，最高点为100%油门。这种油门主要对应的是期望的推力的大小。&lt;/li&gt;
&lt;li&gt;增量式油门：松手油门自动回中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遥控距离&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.3.2 自动驾驶仪&lt;/h4&gt;
&lt;p&gt;组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全球定位系统（GPS）接收器&lt;/li&gt;
&lt;li&gt;惯性测量单元（IMU），包括三轴加速度计、三轴陀螺仪、电子罗盘（或磁力计），目的是得到多旋翼的姿态信息&lt;/li&gt;
&lt;li&gt;气压计和超声波测距模块&lt;/li&gt;
&lt;li&gt;微型计算机&lt;/li&gt;
&lt;li&gt;接口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;感知。导航就是解决“多旋翼在哪”的问题&lt;/li&gt;
&lt;li&gt;控制。控制就是解决“多旋翼怎么去” 的问题&lt;/li&gt;
&lt;li&gt;决策。决策就是解决“多旋翼去哪儿” 的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.3.3 地面站&lt;/h4&gt;
&lt;p&gt;操作员通过地面站系统提供的鼠标、键盘、按钮和操控手柄等外设来与地面站软件进行交互。预先规划好本次任务的航迹，对多旋翼的飞行过程中飞行状况进行实时监控和修改任务设置以干预多旋翼飞行。任务完成后还可以对任务的执行记录进行回放分析。&lt;/p&gt;
&lt;h4&gt;2.3.4 数传&lt;/h4&gt;
&lt;p&gt;数传电台一端接入计算机（地面站软件），一端接入多旋翼自驾仪，通讯采用一定协议进行，从而保持自驾仪与地面站的双向通讯。&lt;/p&gt;
&lt;p&gt;数传指标如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;频率：可选择：433MHz或915MHz&lt;/li&gt;
&lt;li&gt;传输距离&lt;/li&gt;
&lt;li&gt;传输速率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通信协议：MAVLink通讯协议是一个为微型飞行器设计的非常轻巧的、只由头文件构成的信息编组库。MAVLink最初由劳伦兹·迈耶根据LGPL（Lesser GeneralPublic License）许可在2009年初发表。&lt;/p&gt;
&lt;h2&gt;3. 四旋翼的坐标系和姿态表示&lt;/h2&gt;
&lt;h3&gt;3.1 坐标系&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;地球固连坐标系 用于研究多旋翼飞行器相对于地面的运动状态，确定机体的空间位置坐标。$x_e$轴水平面指定某一方向，$z_e$轴垂直地面向下，通过右手定则确定$y_e$轴&lt;/li&gt;
&lt;li&gt;机体坐标系 与多旋翼固连，其原点取在多旋翼的重心位置上。$x_b$轴在多旋翼对称平面内指向机头。$z_b$轴在飞机对称平面内，垂直$x_b$轴向下。然后，按右手定则确定$y_b$轴。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两者关系如下图：&lt;/p&gt;
&lt;h3&gt;3.2 欧拉角&lt;/h3&gt;
&lt;p&gt;欧拉角是一种直观的姿态表示方法，在姿态控制中被广泛使用。可以通过绕$\mathbf{e}_3,\mathbf{k}_2,\mathbf{n}_1$轴分别旋转欧拉角$\psi,\theta,\phi$将地球固联坐标系转动到机体坐标系。如图&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;俯仰角$\theta$：机体轴与地平面（水平面）之间的夹角，飞机抬头为正。&lt;/li&gt;
&lt;li&gt;偏航角$\psi$：机体轴在水平面上的投影与地轴之间的夹角，以机头右偏为正。&lt;/li&gt;
&lt;li&gt;滚转角$\phi$：飞机对称面绕机体轴转过的角度，右滚为正。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 旋转矩阵&lt;/h3&gt;
&lt;p&gt;从地球固联坐标系到机体坐标系的旋转可以通过三步来完成&lt;/p&gt;
&lt;p&gt;$$
\begin{bmatrix}\mathbf{e}_1\\mathbf{e}_2\\mathbf{e}_3\end{bmatrix}
\xrightarrow{\mathbf{R}_z(\psi)}
\begin{bmatrix}\mathbf{k}_1\\mathbf{k}_2\\mathbf{k}_3=\mathbf{e}_3\end{bmatrix}
\xrightarrow{\mathbf{R}_y(\theta)}
\begin{bmatrix}\mathbf{n}_1\\mathbf{n}_2=\mathbf{k}_2\\mathbf{n}_3\end{bmatrix}
\xrightarrow{\mathbf{R}_x(\phi)}
\begin{bmatrix}\mathbf{b}_1=\mathbf{n}_1\\mathbf{b}_2\\mathbf{b}_3\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;其中
$$\mathbf{R}_z\begin{pmatrix}\psi\end{pmatrix}\triangleq\begin{bmatrix}\cos\psi&amp;#x26;\sin\psi&amp;#x26;0\-\sin\psi&amp;#x26;\cos\psi&amp;#x26;0\0&amp;#x26;0&amp;#x26;1\end{bmatrix},\mathbf{R}_y\begin{pmatrix}\theta\end{pmatrix}\triangleq\begin{bmatrix}\cos\theta&amp;#x26;0&amp;#x26;-\sin\theta\0&amp;#x26;1&amp;#x26;0\\sin\theta&amp;#x26;0&amp;#x26;\cos\theta\end{bmatrix},\mathbf{R}_x\begin{pmatrix}\phi\end{pmatrix}\triangleq\begin{bmatrix}1&amp;#x26;0&amp;#x26;0\0&amp;#x26;\cos\phi&amp;#x26;\sin\phi\0&amp;#x26;-\sin\phi&amp;#x26;\cos\phi\end{bmatrix}$$&lt;/p&gt;
&lt;p&gt;因此从机体坐标系到地球固联坐标系的旋转矩阵$\mathbf{R_b^e}$可表示为
$$
\begin{aligned}
\mathbf{R_b^{e}}&amp;#x26;=\left(\mathbf{R}_\mathrm{e}^\mathrm{b}\right)^{-1}\
&amp;#x26;=\mathbf{R}_z^{-1}\left(\psi\right)\mathbf{R}_y^{-1}\left(\theta\right)\mathbf{R}_x^{-1}\left(\phi\right)\
&amp;#x26;=\mathbf{R}_z^\mathrm{T}\left(\psi\right)\mathbf{R}_y^\mathrm{T}\left(\theta\right)\mathbf{R}_x^\mathrm{T}\left(\phi\right)\
&amp;#x26;=\begin{bmatrix}\cos\theta\cos\psi&amp;#x26;\cos\psi\sin\theta\sin\phi-\sin\psi\cos\phi&amp;#x26;\cos\psi\sin\theta\cos\phi+\sin\psi\sin\phi\\cos\theta\sin\psi&amp;#x26;\sin\psi\sin\theta\sin\phi+\cos\psi\cos\phi&amp;#x26;\sin\psi\sin\theta\cos\phi-\cos\psi\sin\phi\-\sin\theta&amp;#x26;\sin\phi\cos\theta&amp;#x26;\cos\phi\cos\theta\end{bmatrix}&amp;#x26;\mathrm{I}
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;当然知道了旋转矩阵我们也可以反过来求欧拉角。&lt;/p&gt;
&lt;h3&gt;3.4 四元数&lt;/h3&gt;
&lt;p&gt;四元数是目前应用最广泛的姿态表示方法之一，四元数本质上是一种高阶复数，是一个四维空间，一般表示为：
$$
\mathbf{q} \triangleq\left[\begin{array}{l}q_0 \ \mathbf{q}_{\mathrm{v}}\end{array}\right]
$$
其中$q_0$表示标量部分，$\mathbf{q_v}$表示向量部分。&lt;/p&gt;
&lt;p&gt;四元数有如下基本运算法则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加减法
$$
\mathbf{p} \pm \mathbf{q}=\left[\begin{array}{l}
p_0 \
\mathbf{p}&lt;em&gt;{\mathrm{v}}
\end{array}\right] \pm\left[\begin{array}{l}
q_0 \
\mathbf{q}&lt;/em&gt;{\mathrm{v}}
\end{array}\right]=\left[\begin{array}{l}
p_0 \pm q_0 \
\mathbf{p}&lt;em&gt;{\mathrm{v}} \pm \mathbf{q}&lt;/em&gt;{\mathrm{v}}
\end{array}\right]
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;乘法
$$
\mathbf{p} \otimes \mathbf{q}=\left[\begin{array}{l}
p_0 \
\mathbf{p}&lt;em&gt;{\mathrm{v}}
\end{array}\right] \otimes\left[\begin{array}{l}
q_0 \
\mathbf{q}&lt;/em&gt;{\mathrm{v}}
\end{array}\right]=\left[\begin{array}{c}
p_0 q_0-\mathbf{q}&lt;em&gt;{\mathrm{v}}^{\mathrm{T}} \mathbf{p}&lt;/em&gt;{\mathrm{v}} \
\mathbf{p}&lt;em&gt;{\mathrm{v}} \times \mathbf{q}&lt;/em&gt;{\mathrm{v}}+p_0 \mathbf{q}&lt;em&gt;{\mathrm{v}}+q_0 \mathbf{p}&lt;/em&gt;{\mathrm{v}}
\end{array}\right]
$$
乘法不满足交换律，但是满足分配率和结合律。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;共轭
$$
\mathbf{q}^*=\left[\begin{array}{c}
q_0 \
-\mathbf{q}_{\mathbf{v}}
\end{array}\right]
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;范数
$$
\begin{aligned}
|\mathbf{q}|^2 &amp;#x26; =\left|\mathbf{q} \otimes \mathbf{q}^&lt;em&gt;\right| \
&amp;#x26; =\left|\mathbf{q}^&lt;/em&gt; \otimes \mathbf{q}\right| \
&amp;#x26; =q_0^2+\mathbf{q}&lt;em&gt;{\mathbf{v}}^{\mathrm{T}} \mathbf{q}&lt;/em&gt;{\mathbf{v}} \
&amp;#x26; =q_0^2+q_1^2+q_2^2+q_3^2 .
\end{aligned}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;求逆&lt;/p&gt;
&lt;p&gt;四元数的逆$\mathbf{q^{-1}}$
$$
\mathbf{q} \otimes \mathbf{q}^{-1}=\left[\begin{array}{c}
1 \
\mathbf{0}_{3 \times 1}
\end{array}\right]
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;除法&lt;/p&gt;
&lt;p&gt;如果$\mathbf{r}\otimes \mathbf{p}=\mathbf{m}$，则有
$$
\begin{aligned}
\mathbf{r} \otimes \mathbf{p} \otimes \mathbf{p}^{-1} &amp;#x26; =\mathbf{m} \otimes \mathbf{p}^{-1} \
\mathbf{r} &amp;#x26; =\mathbf{m} \otimes \mathbf{p}^{-1}
\end{aligned}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以使用四元数来表示旋转&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;向量旋转
$$
\left[\begin{array}{c}
0 \
\mathbf{v}_1^{\prime}
\end{array}\right]=\mathbf{q} \otimes\left[\begin{array}{l}
0 \
\mathbf{v}_1
\end{array}\right] \otimes \mathbf{q}^{-1}
$$
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;坐标系旋转
$$
\left[\begin{array}{c}
0 \
\mathbf{v}_1^{\prime}
\end{array}\right]=\mathbf{q}^{-1} \otimes\left[\begin{array}{c}
0 \
\mathbf{v}_1
\end{array}\right] \otimes \mathbf{q}
$$
&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于四元数更多内容可参考：&lt;a href=&quot;https://krasjet.github.io/quaternion/&quot;&gt;https://krasjet.github.io/quaternion/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;4. 四旋翼的运动&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;悬停：转速相同，1和3逆时针旋转，2和4顺时针旋转，俯仰力矩和翻滚力矩相互抵消，拉力抵消重力&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上下运动：同时改变四个螺旋桨转速，力矩和仍然为0，但是总拉力会变化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;前后运动：减小1和4转速，拉力会产生向前分量，同时拉力垂直分量会减小，需要同量增加四个螺旋桨转速来补偿向后运动同理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;左右运动：同量减小1和2的转速，同量增加3和4的转速，会产生向右翻滚倾斜的力矩，拉力会产生向右分量，同时拉力垂直分量会减小，需要同量增加四个螺旋桨转速来补偿，向左运动同理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;偏航运动：同量减小2和4的转速，同量增加1和3的转速，会产生一个顺时针的偏航力矩&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考资料：&lt;/h2&gt;
&lt;p&gt;电子工业出版社《多旋翼飞行器设计与控制》&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>D435i 配置教程</title><link>https://liangbm3.site/blog/d435i-pei-zhi-jiao-cheng</link><guid isPermaLink="true">https://liangbm3.site/blog/d435i-pei-zhi-jiao-cheng</guid><description>Intel RealSense D435i 的一个配置教程🤗</description><pubDate>Sun, 12 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;Intel RealSense D435i是英特尔公司推出的一款消费级深度相机。它主要包含一个RGB相机、两个红外相机以及一个红外发射器，此外还有一个IMU单元(这也就是D435i和D435的区别，i就表示imu)。本教程的SDK版本为v2.50.0，对应的固件版本为v5.13.0.50，需要使用3.0及3.0以上数据线和数据接口，不然会报错。&lt;/p&gt;
&lt;h2&gt;2. 驱动安装&lt;/h2&gt;
&lt;h3&gt;2.1 驱动安装&lt;/h3&gt;
&lt;p&gt;摄像头官方文档：https://dev.intelrealsense.com/docs/docs-get-started&lt;/p&gt;
&lt;p&gt;驱动仓库：https://github.com/IntelRealSense/librealsense&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;克隆驱动仓库（指定版本，踩过坑，驱动版本需要和固件相对应）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone -b v2.50.0 https://github.com/IntelRealSense/librealsense  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​	克隆可能会发生错误，可以直接在该页面下载压缩包：https://github.com/IntelRealSense/librealsense/tree/v2.50.0&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;安装依赖&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get install libudev-dev pkg-config libgtk-3-dev
sudo apt-get install libusb-1.0-0-dev pkg-config
sudo apt-get install libglfw3-dev
sudo apt-get install libssl-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装权限脚本(进入&lt;code&gt;librealsense-2.50.0&lt;/code&gt;文件夹执行以下命令)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo cp config/99-realsense-libusb.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules &amp;#x26;&amp;#x26; udevadm trigger 
&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-bash&quot;&gt;mkdir build
cd build
cmake ../ -DBUILD_EXAMPLES=true
make
sudo make install
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果编译过慢可以使用&lt;code&gt;make -j8&lt;/code&gt;命令&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试安装&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;realsense-viewer
modinfo uvcvideo | gr
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;插入摄像头屏幕可能发生翻转：重启即可&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：
USB线必须使用3.0以上的，同时如果在虚拟机中需要将USB兼容性改为3.1，参考链接：&lt;a href=&quot;https://blog.csdn.net/qq_28872655/article/details/131452813&quot;&gt;https://blog.csdn.net/qq_28872655/article/details/131452813&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;显示如图界面则成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image.qgTBQho5_Z1pdbDg.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2.2 ROS接口安装&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建ROS工作区&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/realsense_ros_ws/src
cd ~/realsense_ros_ws/src
&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-bash&quot;&gt;git clone https://github.com/IntelRealSense/realsense-ros.git
git clone https://github.com/pal-robotics/ddynamic_reconfigure.git
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;列出仓库中的所有标签，切换到2.x.x的最新版本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd realsense-ros/
git checkout `git tag | sort -V | grep -P &quot;^2.\d+\.\d+&quot; | tail -1`
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化ROS工作区&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ../../
cd ~/realsense_ros_ws/src
catkin_init_workspace
&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-bash&quot;&gt;cd ..
catkin_make clean
catkin_make -DCATKIN_ENABLE_TESTING=False -DCMAKE_BUILD_TYPE=Release
catkin_make install
&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-bash&quot;&gt;echo &quot;source ~/realsense_ros_ws/devel/setup.bash&quot; &gt;&gt; ~/.bashrc
source ~/.bashrc
&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-bash&quot;&gt;roslaunch realsense2_camera rs_camera.launch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时可以用&lt;code&gt;rostopic&lt;/code&gt;来查看相关话题&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/Hacker_MAI/article/details/107976049&quot;&gt;Melodic + Realsense D435i 配置及错误问题解决&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhaoxuhui.top/blog/2020/09/09/intel-realsense-d435i-installation-and-use.html&quot;&gt;Intel RealSense D435i:简介、安装与使用(ROS、Python)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Windows 的 PowerShell 美化教程</title><link>https://liangbm3.site/blog/windows-de-powershell-mei-hua-jiao-cheng</link><guid isPermaLink="true">https://liangbm3.site/blog/windows-de-powershell-mei-hua-jiao-cheng</guid><description>记录在一次Windows中升级PowerShell 5.1至PowerShell 7的，并使用 oh my posh 对终端进行美化的折腾过程。</description><pubDate>Sat, 23 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;记录在一次Windows中升级PowerShell 5.1至PowerShell 7的，并使用 oh my posh 对终端进行美化的折腾过程。&lt;/p&gt;
&lt;h2&gt;2. 升级PowerShell&lt;/h2&gt;
&lt;p&gt;参考&lt;a href=&quot;https://learn.microsoft.com/zh-cn/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4&quot;&gt;在 Windows 上安装 PowerShell - PowerShell | Microsoft Learn&lt;/a&gt;，在PowerShell中直接运行如下命令安装最新版本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;winget install --id Microsoft.Powershell --source winget
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后需要修改默认终端，随便打开一个终端，点击设置，然后将默认启动改为PowerShell 7，如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241123193847041.CkqZEVoz_gMr7S.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3. 使用 oh my posh 进行美化&lt;/h2&gt;
&lt;p&gt;在PowerShell中进行安装：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;Set-ExecutionPolicy Bypass -Scope Process -Force; Invoke-Expression ((New-Object System.Net.WebClient).DownloadString(&apos;https://ohmyposh.dev/install.ps1&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启终端，输入&lt;code&gt;oh-my-posh&lt;/code&gt;，若打印如下内容，则说明安装成功：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241123194432500.DcR4KEAd_1ivb0p.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;更改PowerShell的安全策略：以管理员身份运行，然后输入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;notepad $PROFILE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果提示该文件不存在，可以使用如下命令创建&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;New-Item -Path $PROFILE -Type File -Force
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在打开的配置文件添加以下代码并保存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;oh-my-posh init pwsh | Invoke-Expression
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新打开终端会加载默认主题。&lt;/p&gt;
&lt;p&gt;在&lt;a href=&quot;https://www.nerdfonts.com/font-downloads&quot;&gt;Nerd Fonts 官网&lt;/a&gt;下载一款自己喜欢的字体，这里以&lt;code&gt;Cousine Nerd Font&lt;/code&gt;为例，解压压缩包后，右键安装所有字体&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241123194822118.BYglDj90_IrQU.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后在外观中配置刚刚下载的字体，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241123202608377.W9-dhJxq_2bmidj.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;显示可用的主题：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;Get-PoshThemes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记住好自己要切换的主题名称，然后&lt;code&gt;ctrl+左键&lt;/code&gt;打开主题文件夹，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241123202959183.DkpOzXPb_Z1LkfXl.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;打开配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;notepad $PROFILE 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置主题路径，以&lt;code&gt;wholespace&lt;/code&gt;主题为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;oh-my-posh init pwsh  --config &quot;Senv:POSH_THEMES_PATH/wholespace.omp.json&quot; | Invoke-Expression
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重载主题，命令如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;. $PROFILE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，还可以在终端外观设置中设置背景和透明度。&lt;/p&gt;
&lt;h2&gt;4. Vsocde中的乱码配置&lt;/h2&gt;
&lt;p&gt;使用的时候发现在VScode中会乱码，安装下面步骤更改终端字体即可：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开设置&lt;/li&gt;
&lt;li&gt;搜索&lt;code&gt;Integrated:Font Family&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将字体修改为上面设置的字体&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;5. 参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/KWlN5s_rdLn8oO_iL2vbgg&quot;&gt;https://mp.weixin.qq.com/s/KWlN5s_rdLn8oO_iL2vbgg&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Git 中对提交历史进行处理</title><link>https://liangbm3.site/blog/git-zhong-dui-ti-jiao-li-shi-jin-xing-chu-li</link><guid isPermaLink="true">https://liangbm3.site/blog/git-zhong-dui-ti-jiao-li-shi-jin-xing-chu-li</guid><description>这篇博客将对Git中的版本管理操作作进一步总结。</description><pubDate>Fri, 15 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;在之前的&lt;a href=&quot;/blog/git-de-shi-yong-zong-jie&quot;&gt;Git 的使用总结&lt;/a&gt;这篇博客中，总结了一些Git的基础命令和用法，这篇博客将对Git中的版本管理操作作进一步总结。&lt;/p&gt;
&lt;h2&gt;2. 场景&lt;/h2&gt;
&lt;p&gt;我们使用Git来管理我们的项目时，有时候会不小心提交一些不该提交的文件，如果我们直接将这些文件删除，会留下提交记录。所以我们需要删除那次的提交记录，主要的方法有&lt;code&gt;git reset&lt;/code&gt;和&lt;code&gt;git rebase&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;3. 操作流程&lt;/h2&gt;
&lt;h3&gt;3.1 使用git reset 命令&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;git log --oneline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认需要删除的提交的位置，假设提交历史如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;abc1234 Third commit (最新提交)
def5678 Second commit (需要删除的提交)
ghi9101 First commit
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将指针重置到&lt;code&gt;First commit&lt;/code&gt;，并将更改保留在暂存区。可以使用如下命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset --soft HEAD~3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或使用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset --soft ghi9101
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从暂存区删除我们错误提交的文件，然后重新commit&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后执行以下命令推送到远程仓库&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git push origin main --force
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之所以要使用&lt;code&gt;--force&lt;/code&gt;，是因为通常Git 在推送时会检查你本地的提交历史是否与远程仓库的历史一致。如果本地和远程仓库的提交历史有冲突（例如你使用 &lt;code&gt;git rebase&lt;/code&gt; 或 &lt;code&gt;git reset&lt;/code&gt; 修改了本地历史），Git 默认会拒绝推送，以防止你覆盖远程仓库中的提交历史。当你想要覆盖远程仓库的历史时（例如修改或删除提交），必须使用 &lt;code&gt;--force&lt;/code&gt; 选项，告诉 Git 你明确地希望覆盖远程历史。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;完成&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.2 使用git rebase 命令&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;git log --oneline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认需要删除的提交的位置，假设提交历史如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;abc1234 Third commit (最新提交)
def5678 Second commit (需要删除的提交)
ghi9101 First commit
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开一个编辑器，并显示最近 3 个提交的列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git rebase -i HEAD~3
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在编辑器中找到需要删除的提交，将其对应的 &lt;code&gt;pick&lt;/code&gt; 改成 &lt;code&gt;drop&lt;/code&gt;，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pick abc1234 Third commit
drop def5678 Second commit
pick ghi9101 First commit
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;完成 Rebase 后强制推送更改：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git push origin main --force
&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;h2&gt;4. git reset 和 git rebase 的不同&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git reset&lt;/code&gt; 和 &lt;code&gt;git rebase&lt;/code&gt; 是 Git 中两个常用的命令，它们都可以改变历史记录，但它们的用途和效果有所不同。下面是它们的主要区别和使用场景：&lt;/p&gt;
&lt;h3&gt;4.1. git reset&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git reset&lt;/code&gt; 主要用于重置当前分支的 HEAD（当前提交的指针），它可以用于修改 Git 提交历史的指针位置，并根据不同的模式（如 &lt;code&gt;--soft&lt;/code&gt;、&lt;code&gt;--mixed&lt;/code&gt;、&lt;code&gt;--hard&lt;/code&gt;）决定如何处理工作区和暂存区的内容。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常用选项：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git reset --soft &amp;#x3C;commit&gt;&lt;/code&gt;：将 &lt;code&gt;HEAD&lt;/code&gt; 移动到指定的提交位置，并将所有更改保留在暂存区（staging area），即重新准备提交这些更改。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git reset --mixed &amp;#x3C;commit&gt;&lt;/code&gt;：将 &lt;code&gt;HEAD&lt;/code&gt; 移动到指定的提交位置，并将更改保留在工作区，但从暂存区移除这些更改（相当于 &lt;code&gt;git rm --cached&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git reset --hard &amp;#x3C;commit&gt;&lt;/code&gt;：将 &lt;code&gt;HEAD&lt;/code&gt; 和工作区都移动到指定提交的位置，丢弃所有更改。所有的修改（无论是暂存区还是工作区的）都会丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;撤销提交：你可以用 &lt;code&gt;git reset&lt;/code&gt; 撤销最近的提交（&lt;code&gt;git reset --soft HEAD~1&lt;/code&gt; 或 &lt;code&gt;git reset --mixed HEAD~1&lt;/code&gt;），并选择保留更改在工作区或暂存区。&lt;/li&gt;
&lt;li&gt;撤销修改：通过 &lt;code&gt;git reset --hard&lt;/code&gt; 可以彻底丢弃所有本地修改（包括工作区和暂存区），回到某个提交的状态。&lt;/li&gt;
&lt;li&gt;调整历史记录指针：它通常用于调整 HEAD 指针的位置，可以方便地回到之前的某个提交。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 撤销最后一次提交并将更改保留在暂存区
git reset --soft HEAD~1

# 撤销最后一次提交并将更改保留在工作区
git reset --mixed HEAD~1

# 完全撤销最后一次提交并丢弃所有更改
git reset --hard HEAD~1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2. git rebase&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git rebase&lt;/code&gt; 是一个更为强大的命令，用于将一系列提交“重放”到另一分支上。&lt;code&gt;git rebase&lt;/code&gt; 会修改分支的历史记录，它会“将你的提交移到另一个分支的后面”，就像你从一个新的基础上开始工作一样。&lt;/p&gt;
&lt;p&gt;常用选项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git rebase &amp;#x3C;branch&gt;&lt;/code&gt;：将当前分支的所有提交“重放”到 &lt;code&gt;&amp;#x3C;branch&gt;&lt;/code&gt; 分支的末尾。例如，&lt;code&gt;git rebase main&lt;/code&gt; 将当前分支的提交应用到 &lt;code&gt;main&lt;/code&gt; 分支的末尾。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git rebase -i &amp;#x3C;commit&gt;&lt;/code&gt;：交互式 rebase，用于编辑提交历史、删除、合并、修改提交信息等。你可以选择哪些提交进行修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;清理历史提交：&lt;code&gt;git rebase -i&lt;/code&gt; 允许你对提交进行重写、合并、修改提交信息等，非常适合清理提交历史。&lt;/li&gt;
&lt;li&gt;合并提交：通过 &lt;code&gt;git rebase&lt;/code&gt; 将多个提交合并成一个提交（&lt;code&gt;squash&lt;/code&gt;），让历史更加整洁。&lt;/li&gt;
&lt;li&gt;保持历史线性：使用 &lt;code&gt;git rebase&lt;/code&gt; 来避免合并提交（&lt;code&gt;merge commits&lt;/code&gt;），让提交历史更干净。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 将当前分支的提交重放到 main 分支上
git rebase main

# 交互式 rebase，用于修改提交历史
git rebase -i HEAD~3
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Obsidian 复刻 Typora 的图片操作</title><link>https://liangbm3.site/blog/obsidian-fu-ke-typora-de-tu-pian-cao-zuo</link><guid isPermaLink="true">https://liangbm3.site/blog/obsidian-fu-ke-typora-de-tu-pian-cao-zuo</guid><description>这篇博客主要记录一些图片相关的问题，例如：图片的自动居中，图片的缩放，图床的设置，如何利用脚本工具将图片从Markdown自动转换成HTML语法。</description><pubDate>Thu, 14 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;最近入坑了Obsidian🤗🤗🤗，由于学习成本太高，之前一直没有入坑，因为Typora真的挺好的。但是由于Obsidian可自定义程度非常高，所以还是决定从Typora迁移到Obsidian。这篇博客主要记录一些图片相关的问题，例如：图片的自动居中，图片的缩放，图床的设置，如何利用脚本工具将图片从Markdown自动转换成HTML语法。&lt;/p&gt;
&lt;h2&gt;2. 图片的自动居中&lt;/h2&gt;
&lt;p&gt;正常来说直接插入Markdown的图片是占满整个页宽的，所以居中不居中看不出来，但是如果对图片进行缩放时，图片会自动靠左，其实这主要取决于Markdown编辑器，例如在vscode中图片就是自动居中的，要在Typora中实现图片自动居中，可以通过添加CSS来实现，具体步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在设置界面，点击&lt;code&gt;外观&lt;/code&gt;，然后打开CSS文件夹&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/images/obsidian-fu-ke-typora-de-tu-pian-cao-zuo/image-20241113231228686.png&quot; alt=&quot;image-20241113231228686&quot;&gt;&lt;/p&gt;
&lt;p&gt;在该文件夹下，新建一个&lt;code&gt;center.css&lt;/code&gt;，复制粘贴以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;img {
    display: block !important;
    margin-left: auto !important;
    margin-right: auto !important;
}

 .markdown-source-view.mod-cm6 .cm-content &gt; * {
    margin: auto auto !important;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;回到刚才的页面，开启该代码片段，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241113232256088.0Bwdv_0M_14dFhm.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 图片的缩放&lt;/h2&gt;
&lt;p&gt;如果是Markdown语法的缩放，可以使用插件 mousewheel-image-zoom ，但是这个语法不是Markdown的通用语法，一般在Markdown中图片的缩放是使用HTML语法的，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;img src=&quot;https://raw.githubusercontent.com/liangbm3/photos/main/Typora/20241113224150.png&quot; style=&quot;zoom:67%;&quot;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个语句可以实现图片缩放67%，所以只要我们只要将图片写成HTML的表达方式，就可以很方便地缩放了。&lt;/p&gt;
&lt;h2&gt;4. 图床的设置&lt;/h2&gt;
&lt;p&gt;为什么要设置图床？一方面当然是方便Markdown文件的传播，还有一个重要的原因是Obsidian图片的HTML语法不支持相对路径（🙃🙃🙃），我们写Markdown一般不会写绝对路径，使用图床我们可以得到的是一个链接，这是一个非常完美的解决方案。关于图床，网上有很多种，这里以Github+PicGo-Core为例：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装了 node 或 yarn 之后，在终端中以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;npm install picgo -g

# or

yarn global add picgo
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行配置&lt;/p&gt;
&lt;p&gt;picgo 的默认配置文件为&lt;code&gt;~/.picgo/config.json&lt;/code&gt;。其中&lt;code&gt;~&lt;/code&gt;为用户目录。不同系统的用户目录不太一样。&lt;/p&gt;
&lt;p&gt;Linux 和 macOS 均为&lt;code&gt;~/.picgo/config.json&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;windows 则为&lt;code&gt;C:\Users\你的用户名\.picgo\config.json&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;一个示例如下图：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;picBed&quot;: {
    &quot;uploader&quot;: &quot;github&quot;,
    &quot;current&quot;: &quot;github&quot;,
    &quot;github&quot;: {
      &quot;repo&quot;: &quot;&quot;,
      &quot;branch&quot;: &quot;&quot;,
      &quot;token&quot;: &quot;&quot;,
      &quot;path&quot;: &quot;&quot;,
      &quot;customUrl&quot;: &quot;&quot;
    },
    &quot;transformer&quot;: &quot;path&quot;
  },
  &quot;picgoPlugins&quot;: {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注释如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;repo&quot;: &quot;&quot;, // 仓库名，格式是 username/reponame
  &quot;token&quot;: &quot;&quot;, // github token
  &quot;path&quot;: &quot;&quot;, // 自定义存储路径，比如 img/
  &quot;customUrl&quot;: &quot;&quot;, // 自定义域名，注意要加 http://或者 https://
  &quot;branch&quot;: &quot;&quot; // 分支名，默认是 main
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Github 获取个人 token&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;访问：&lt;a href=&quot;https://github.com/settings/tokens&quot;&gt;settings-tokens&lt;/a&gt; ，点击&lt;strong&gt;Generate new token&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;设置 token 属性
&lt;ul&gt;
&lt;li&gt;Expiration：&lt;code&gt;no expiration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Select scopes：&lt;code&gt;repo&lt;/code&gt; 选择存放的仓库&lt;/li&gt;
&lt;li&gt;点击&lt;code&gt;Generate token&lt;/code&gt;，生成 token。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;这个 token 生成后只会显示这一次！注意复制、保存到其他地方以备后续使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将信息填入上述json文件即可&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;也可以通过在终端输入&lt;code&gt;picgo set uploader&lt;/code&gt;按照提示自动生成json文件&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置好picgo后进入Obsidian中下载插件 Image auto upload Plugin&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241113234933073.Cn6S92z4_URCiL.webp&quot; alt=&quot;image-20241113234933073&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://liangbm3.site/_astro/image-20241113235049992.BjbwvqFS_Z1LSBwM.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;到这里图床已经完成了。&lt;/p&gt;
&lt;h2&gt;5. Markdown自动转换成HTML语法&lt;/h2&gt;
&lt;p&gt;这个是Typora中的一个特色功能，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/71d6f77faabf7457985405753aefb65.t5G-QQtj_Z1s1xfQ.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;然而似乎Obsidian中没有类似的插件实现这个功能。&lt;/p&gt;
&lt;p&gt;在Windows中，我们可以借助自动化工具 AutoHotkey 实现，步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;进入&lt;a href=&quot;https://www.autohotkey.com/&quot;&gt;AutoHotkey &lt;/a&gt;官网，下载安装包，安装。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在合适的位置，右键，新建一个&lt;code&gt;.ahk&lt;/code&gt;脚本&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241113235818365.DZwNPD-L_ZtixPd.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用文本编辑器打开，复制粘贴如下内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;^p:: ; 按下 Ctrl + P 触发
    Clipboard := &quot;&quot; ; 清空剪贴板
    Send ^c ; 复制当前选中内容
    ClipWait, 1 ; 等待内容进入剪贴板

    ; 检查是否是 Markdown 图片格式并替换为 HTML 图片标签
    if (Clipboard ~= &quot;!\[.*?\]\((.*?)\)&quot;) {
        ; 使用正则表达式将 Markdown 图片语法转换为 HTML &amp;#x3C;img&gt; 标签
        newContent := RegExReplace(Clipboard, &quot;!\[.*?\]\((.*?)\)&quot;, &quot;&amp;#x3C;img src=&quot;&quot;&quot; . &quot;$1&quot; . &quot;&quot;&quot; style=&quot;&quot;zoom:67%;&quot;&quot;&gt;&quot;)
        
        ; 将新内容设置为剪贴板内容
        Clipboard := newContent
        ClipWait, 1 ; 等待剪贴板更新
        Send ^v ; 粘贴替换后的内容
    }
return

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保存退出，然后我们双击运行该脚本，运行之后可以在系统托盘中看到一个&lt;code&gt;H&lt;/code&gt;图标&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;之后我们只要在Obsidian里面选中要转换的Markdown图片语法，按住&lt;code&gt;Ctrl + P&lt;/code&gt;，即可自动转换，非常方便。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;6. 参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://picgo.github.io/PicGo-Core-Doc/zh/guide/config.html&quot;&gt;配置文件 | PicGo-Core&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://forum-zh.obsidian.md/t/topic/15964&quot;&gt;4.1.230226 obsidian的图片如何居中显示？ - 经验分享 - Obsidian 中文论坛&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.haoyep.com/posts/github-graph-beds&quot;&gt;使用PicGo + GitHub 搭建 Obsidian 图床 | Leehow的小站&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>使用 scp 在计算机和远程服务器直接传输文件</title><link>https://liangbm3.site/blog/shi-yong-scp-zai-ji-suan-ji-he-yuan-cheng-fu-wu-qi-zhi-jie-chuan-shu-wen-jian</link><guid isPermaLink="true">https://liangbm3.site/blog/shi-yong-scp-zai-ji-suan-ji-he-yuan-cheng-fu-wu-qi-zhi-jie-chuan-shu-wen-jian</guid><description>SCP（Secure Copy Protocol）是一种用于在计算机之间安全传输文件的协议，通常用于在本地计算机和远程服务器之间复制文件。</description><pubDate>Tue, 05 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 简介&lt;/h2&gt;
&lt;p&gt;SCP（Secure Copy Protocol）是一种用于在计算机之间安全传输文件的协议，通常用于在本地计算机和远程服务器之间复制文件。&lt;/p&gt;
&lt;h2&gt;2. 使用说明&lt;/h2&gt;
&lt;p&gt;语法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;scp [可选参数] file_source file_target 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可选参数如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-1&lt;/code&gt;： 强制scp命令使用协议ssh1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-2&lt;/code&gt;： 强制scp命令使用协议ssh2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-4&lt;/code&gt;： 强制scp命令只使用IPv4寻址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-6&lt;/code&gt;： 强制scp命令只使用IPv6寻址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-C&lt;/code&gt;： 允许压缩。（将-C标志传递给ssh，从而打开压缩功能）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p&lt;/code&gt;：保留原文件的修改时间，访问时间和访问权限。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-q&lt;/code&gt;： 不显示传输进度条。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-r&lt;/code&gt;： 递归复制整个目录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-l limit&lt;/code&gt;： 限定用户所能使用的带宽，以Kbit/s为单位。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-P port&lt;/code&gt;：注意是大写的P, port是指定数据传输用到的端口号&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 使用示例&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将本地一个文件复制到远程服务器的桌面&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;scp file.zip root@192.168.2.1:/home/Desktop
&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-powershell&quot;&gt;scp -r file root@192.168.2.1:/home/Desktop
&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-powershell&quot;&gt;scp -r 端口号 file root@192.168.2.1:/home/Desktop
&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-powershell&quot;&gt;scp root@192.168.2.1:/home/Desktop/file.zip &quot;D:\Downloads\Compressed&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4. 参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.runoob.com/linux/linux-comm-scp.html&quot;&gt;Linux scp命令 | 菜鸟教程&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Python 多线程、多进程和协程</title><link>https://liangbm3.site/blog/python-duo-xian-cheng-duo-jin-cheng-he-xie-cheng</link><guid isPermaLink="true">https://liangbm3.site/blog/python-duo-xian-cheng-duo-jin-cheng-he-xie-cheng</guid><description>在Python中，多线程、多进程和协程是处理并发任务的三种常用方式。它们各有优缺点，适合不同的场景。</description><pubDate>Mon, 04 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;在Python中，多线程、多进程和协程是处理并发任务的三种常用方式。它们各有优缺点，适合不同的场景。&lt;/p&gt;
&lt;h2&gt;2. python多线程&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;threading&lt;/code&gt;模块，&lt;code&gt;threading&lt;/code&gt;模块通过共享内存来实现多线程，所有线程共享一样的变量，适用于I/O密集型任务。&lt;/p&gt;
&lt;h3&gt;2.1. 一个线程的完整周期&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;新建：新建一个线程对象&lt;/li&gt;
&lt;li&gt;就绪：调用start方法后，线程对象等待运行，什么时候开始取决于调度&lt;/li&gt;
&lt;li&gt;运行：线程处于运行状态&lt;/li&gt;
&lt;li&gt;阻塞：处于运行状态的线程被阻塞&lt;/li&gt;
&lt;li&gt;死亡：线程执行完毕或异常退出，线程对象销毁并释放内存&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.2. 一些使用实例&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;直接使用Thread创建线程对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from threading import Thread

def print_numbers(range_start, range_end):
    for i in range(range_start, range_end):
        print(i)
        
t1 = Thread(target=print_numbers, args=(1, 10))
t2 = Thread(target=print_numbers, args=(11, 20))

t1.start()
t2.start()

t1.join()
t2.join()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重写父类&lt;code&gt;threading.Thread&lt;/code&gt;创建线程&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import threading 

#继承Thread并重写
class myThread(threading.Thread):
    #重写构造函数
    def __init__(self,start_num,end_num):
        threading.Thread.__init__(self)
        self.start_num = start_num
        self.end_num = end_num
    #重写run函数
    def run(self):
        print(f&quot;线程开始{self.name}&quot;)
        for i in range(self.start_num,self.end_num):
            print(i)
        print(f&quot;线程结束{self.name}&quot;)
        
thread1 = myThread(1,10)
thread2 = myThread(11,20)

thread1.start()
thread2.start()

thread1.join()
thread2.join()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. python多进程&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;multiprocessing&lt;/code&gt;模块，用法和&lt;code&gt;threading&lt;/code&gt;类似。一些使用实例如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;直接使用&lt;code&gt;Process&lt;/code&gt;创建进程对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import multiprocessing

def print_numbers(range_start, range_end):
    for i in range(range_start, range_end):
        print(i)

if __name__ == &apos;__main__&apos;:      
    p1 = multiprocessing.Process(target=print_numbers, args=(1, 100))
    p2 = multiprocessing.Process(target=print_numbers, args=(110, 200))

    p1.start()
    p2.start()
    p1.join()
    p2.join()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重写父类&lt;code&gt;multiprocessing.Process&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import multiprocessing

#继承Thread类并重写
class myProcess(multiprocessing.Process):
    #重写构造函数
    def __init__(self,start_num,end_num):
        multiprocessing.Process.__init__(self)
        self.start_num = start_num
        self.end_num = end_num
    #重写run函数
    def run(self):
        print(f&quot;进程开始{self.name}&quot;)
        for i in range(self.start_num,self.end_num):
            print(i)
        print(f&quot;进程结束{self.name}&quot;)
if __name__ == &apos;__main__&apos;:
    process1 = myProcess(1,100)
    process2 = myProcess(110,200)
    
    process1.start()
    process2.start()
    
    process1.join()
    process2.join()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. python线程池和进程池&lt;/h2&gt;
&lt;p&gt;系统启动一个新线程的成本是比较高的，因为它涉及与操作系统的交互。在这种情形下，使用线程池可以很好地提升性能，尤其是当程序中需要创建大量生存期很短暂的线程时，更应该考虑使用线程池。&lt;/p&gt;
&lt;p&gt;线程池在系统启动时即创建大量空闲的线程，程序只要将一个函数提交给线程池，线程池就会启动一个空闲的线程来执行它。当该函数执行结束后，该线程并不会死亡，而是再次返回到线程池中变成空闲状态，等待执行下一个函数。&lt;/p&gt;
&lt;p&gt;此外，使用线程池可以有效地控制系统中并发线程的数量。当系统中包含有大量的并发线程时，会导致系统性能急剧下降，甚至导致Python 解释器崩溃，而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数。&lt;/p&gt;
&lt;p&gt;使用实例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from concurrent.futures import ThreadPoolExecutor, as_completed
import time

def spider(page):
    time.sleep(page)
    print(f&quot;crawl task{page} finished&quot;)
    return page

if __name__ == &apos;__main__&apos;:
    with ThreadPoolExecutor(max_workers=5) as t:
        obj_list = []
        for page in range(1, 5):
            obj = t.submit(spider, page)
            obj_list.append(obj)

        for future in as_completed(obj_list):
            data = future.result()
            print(f&quot;main: {data}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序将 &lt;code&gt;task&lt;/code&gt; 函数提交（submit）给线程池后，&lt;code&gt;submit&lt;/code&gt; 方法会返回一个 &lt;code&gt;Future&lt;/code&gt; 对象，&lt;code&gt;Future&lt;/code&gt; 类主要用于获取线程任务函数的返回值。由于线程任务会在新线程中以异步方式执行，因此，线程执行的函数相当于一个“将来完成”的任务，所以 Python 使用 &lt;code&gt;Future&lt;/code&gt; 来代表。&lt;/p&gt;
&lt;h2&gt;4. 协程&lt;/h2&gt;
&lt;p&gt;协程（Coroutines）是一种特殊的软件构造，它允许程序在执行过程中暂停并恢复执行，而不会丢失当前的执行上下文。与线程和进程不同，协程在单个线程中运行，通过调度机制实现并发，降低了上下文切换的开销，提高了程序的执行效率。协程通常用于处理I/O密集型任务，如网络请求、文件读写等。&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;asyncio&lt;/code&gt;库来实现协程示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import asyncio

async def task(num):
    print(f&quot;Task {num} started&quot;)
    await asyncio.sleep(1)
    print(f&quot;Task {num} completed&quot;)

async def main():
    tasks = [task(i) for i in range(3)]  # 创建多个任务
    await asyncio.gather(*tasks)  # 并行运行所有任务，并等待它们完成

if __name__ == &quot;__main__&quot;:
    asyncio.run(main())  # 运行主函数

&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ROS1 基础与常用命令</title><link>https://liangbm3.site/blog/ros1-ji-chu-yu-chang-yong-ming-ling</link><guid isPermaLink="true">https://liangbm3.site/blog/ros1-ji-chu-yu-chang-yong-ming-ling</guid><description>接触ROS差不多有一年多了，最近由于科研的需要，又重新捡了起来。个人觉得快速上手ROS的方法是在理解大概原理的前提下，快速把常用的操作过一遍。在使用ROS的时候，有时一些命令常常想不起来，上网查阅又需要花费大量时间，这里一并总结了一下。</description><pubDate>Sun, 03 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;接触ROS差不多有一年多了，最近由于科研的需要，又重新捡了起来。个人觉得快速上手ROS的方法是在理解大概原理的前提下，快速把常用的操作过一遍。在使用ROS的时候，有时一些命令常常想不起来，上网查阅又需要花费大量时间，这里一并总结了一下。&lt;/p&gt;
&lt;h2&gt;2. ROS的安装&lt;/h2&gt;
&lt;p&gt;一般的使用场景都是在Ubuntu 20.04 上安装Noetic版本，一般在有代理的情况下，按照&lt;a href=&quot;https://wiki.ros.org/noetic/Installation/Ubuntu&quot;&gt;官方的教程&lt;/a&gt;，可以一次性安装成功，步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;设置源列表使计算机接受来自packages.ros.org的软件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo sh -c &apos;echo &quot;deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main&quot; &gt; /etc/apt/sources.list.d/ros-latest.list&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置Key&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install curl # 安装curl，若已安装则跳过
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
&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-bash&quot;&gt;sudo apt update #更新软件包索引
sudo apt install ros-noetic-desktop-full #安装完整版
&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-bash&quot;&gt;echo &quot;source /opt/ros/noetic/setup.bash&quot; &gt;&gt; ~/.bashrc
source ~/.bashrc
&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-bash&quot;&gt;sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool build-essential
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装和初始化rosdep&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install python3-rosdep
sudo rosdep init
rosdep update
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;安装完成后，后续如果要安装其他软件包，可以在&lt;a href=&quot;https://index.ros.org/packages/page/1/time/#noetic&quot;&gt;ROS Packages&lt;/a&gt;中寻找，或者使用命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;apt search ros-noetic
&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 install ros-noetic-PACKAGE
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. ROS简介&lt;/h2&gt;
&lt;p&gt;ROS（Robot Operating System）是一个用于机器人开发的开源框架，旨在简化机器人软件的开发。它提供了一系列工具、库和约定，以便于开发者创建复杂的机器人应用。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;节点（Node）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;节点是ROS系统中的基本计算单元。每个节点都是一个独立的进程，可以执行特定的任务。不同的节点可以用不同的语言编写。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;话题（Topic）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;话题是ROS中节点之间通信的方式。节点可以发布（publish）和订阅（subscribe）话题。发布者将消息发送到特定的话题，而订阅者接收该话题的消息。这样，节点之间的耦合度降低，增加了系统的灵活性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;服务（Service）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;服务是ROS中另一种通信机制，允许节点通过请求-响应模式进行交互。一个节点提供服务，另一个节点调用该服务。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;消息（Message）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;消息是ROS中传递的数据结构。每种类型的消息都有其特定的格式，定义了节点之间交换的数据。ROS内置了多种基本消息类型，也支持用户自定义消息类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;参数服务器（Parameter Server）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;参数服务器是一个共享的全局数据库，用于存储配置参数。节点可以读取和写入这些参数，以便动态调整系统的行为。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;工作空间（Workspace）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;工作空间是ROS开发环境的一个目录，包含了ROS包、库和配置文件。我们可以在工作空间中编写、测试和管理机器人应用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;包（Package）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ROS的功能以包的形式组织。每个包可以包含节点、库、配置文件、文档等。包的结构使得代码的复用和共享变得更加方便。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4. 构建工作区&lt;/h2&gt;
&lt;p&gt;我们这里以在&lt;code&gt;home&lt;/code&gt;目录中创建一个名为&lt;code&gt;catkin_ws&lt;/code&gt;的工作区为例&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p catkin_ws/src
cd catkin_ws
catkin_make
catkin_make install #添加install文件夹
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里有一个坑：如果之前在Ubuntu中安装了Anaconda或者miniconda，会出现找不到&lt;code&gt;empy&lt;/code&gt;包的错误。&lt;/p&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置启动终端时不自动进入base环境，命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;conda config --set auto_activate_base false
&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-bash&quot;&gt;rm -rf build devel
catkin_make
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;在每次运行软件包时，我们通常要先设置环境变量，为了避免频繁操作，可以将其写入&lt;code&gt;.bashrc&lt;/code&gt;文件中，这样每次启动终端时会自动设置。&lt;/p&gt;
&lt;p&gt;使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &quot;source ~/catkin_ws/devel/setup.bash&quot; &gt;&gt; ~/.bashrc
source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查环境变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo $ROS_PACKAGE_PATH
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 基础操作例程&lt;/h2&gt;
&lt;h3&gt;5.1 小海龟&lt;/h3&gt;
&lt;p&gt;小海龟例程是ROS非常经典的入门学习包，可以让我们快速熟悉ROS命令行工具的使用&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动ROS Master&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;roscore
&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-bash&quot;&gt;rosrun turtlesim turtlesim_node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241102115548811.DoAKbXoN_Z1Xq9L1.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动海龟控制节点&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun turtlesim turtle_teleop_key
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动上述两个节点之后，我们便可以用键盘来控制海龟的移动。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看系统计算图&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rqt_graph
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241102115935703.D0vh-pac_Z1Jsn5o.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;从图中可知，目前ROS系统中存在两个节点，一个是海龟仿真器节点&lt;code&gt;turtlesim&lt;/code&gt;，其订阅了话题&lt;code&gt;/turtle1/cmd_vel&lt;/code&gt;，还有一个节点是&lt;code&gt;teleop_turtle&lt;/code&gt;，其往&lt;code&gt;/turtle1/cmd_vel&lt;/code&gt;中发布信息，这样两个节点间便完成了通讯。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点相关信息的命令为&lt;code&gt;rosnode&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-bash&quot;&gt;rosnode list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/rosout
/rqt_gui_py_node_83022
/teleop_turtle
/turtlesim
&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-bash&quot;&gt;rosnode info /turtlesim
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Node [/turtlesim]
Publications: 
 * /rosout [rosgraph_msgs/Log]
 * /turtle1/color_sensor [turtlesim/Color]
 * /turtle1/pose [turtlesim/Pose]

Subscriptions: 
 * /turtle1/cmd_vel [geometry_msgs/Twist]

Services: 
 * /clear
 * /kill
 * /reset
 * /spawn
 * /turtle1/set_pen
 * /turtle1/teleport_absolute
 * /turtle1/teleport_relative
 * /turtlesim/get_loggers
 * /turtlesim/set_logger_level


contacting node http://ubuntu:37017/ ...
Pid: 82069
Connections:
 * topic: /rosout
    * to: /rosout
    * direction: outbound (52857 - 127.0.0.1:47492) [26]
    * transport: TCPROS
 * topic: /turtle1/cmd_vel
    * to: /teleop_turtle (http://ubuntu:41229/)
    * direction: inbound (45856 - ubuntu:38849) [32]
    * transport: TCPROS
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;话题相关命令为&lt;code&gt;rostopic&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-bash&quot;&gt;rostopic list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/rosout
/rosout_agg
/statistics
/turtle1/cmd_vel
/turtle1/color_sensor
/turtle1/pose
&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-bash&quot;&gt;rostopic pub /turtle1/cmd_vel geometry_msgs/Twist &quot;linear:
  x: 1.0
  y: 1.0
  z: 0.0
angular:
  x: 1.0
  y: 0.0
  z: 0.0&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;rosmsg&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-bash&quot;&gt;rosmsg show geometry_msgs/Twist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;geometry_msgs/Vector3 linear
  float64 x
  float64 y
  float64 z
geometry_msgs/Vector3 angular
  float64 x
  float64 y
  float64 z
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务相关命令为&lt;code&gt;rosservice&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-bash&quot;&gt;rosservice list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/clear
/kill
/reset
/rosout/get_loggers
/rosout/set_logger_level
/rqt_gui_py_node_84673/get_loggers
/rqt_gui_py_node_84673/set_logger_level
/spawn
/teleop_turtle/get_loggers
/teleop_turtle/set_logger_level
/turtle1/set_pen
/turtle1/teleport_absolute
/turtle1/teleport_relative
/turtlesim/get_loggers
/turtlesim/set_logger_level
&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-bash&quot;&gt;rosservice call /spawn &quot;x: 0.0
y: 0.0
theta: 0.0
name: &apos;turtle2&apos;&quot; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完后会在&lt;code&gt;turtlesim&lt;/code&gt;中产生一个海龟&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;话题记录和复现工具&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;话题记录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosbag record -a -O cmd_record
&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-bash&quot;&gt;rosbag play cmd_record.bag
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.2 发布和订阅&lt;/h3&gt;
&lt;p&gt;发布和订阅是ROS最基础的通信机制，这里演示如何创建功能包，编写一个信息发布节点 &lt;code&gt;Publisher&lt;/code&gt;，和一个信息订阅节点 &lt;code&gt;Pubscriber&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;5.2.1 功能包的构建和编译&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建功能包&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;catkin_create_pkg &amp;#x3C;package_name&gt; [depend1] [depend2] [depend3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;p&gt;执行命令创建功能包&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/catkin_ws/src/
catkin_create_pkg pub_sub_pkg std_msgs rospy roscpp
&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-bash&quot;&gt;cd ~/catkin_ws/
catkin_make
source /devel/setup.bash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5.2.2 Publisher的编写&lt;/h4&gt;
&lt;h5&gt;5.2.2.1 cpp版本的实现&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;pub_sub_pkg/src/&lt;/code&gt;路径下新建文件&lt;code&gt;random_number_publisher.cpp&lt;/code&gt;，添加如下内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;ros/ros.h&gt;
#include &amp;#x3C;std_msgs/Int32.h&gt;
#include &amp;#x3C;cstdlib&gt;  // for rand()
#include &amp;#x3C;ctime&gt;    // for time()

int main(int argc, char** argv) {
    ros::init(argc, argv, &quot;random_number_publisher&quot;);
    ros::NodeHandle nh;

    // 创建一个发布者，发布到 &quot;random_number&quot; 主题
    ros::Publisher random_pub = nh.advertise&amp;#x3C;std_msgs::Int32&gt;(&quot;random_number&quot;, 10);

    // 初始化随机数生成器
    std::srand(static_cast&amp;#x3C;unsigned int&gt;(std::time(0)));

    // 设置循环频率
    ros::Rate loop_rate(1);  // 每秒发布一次

    while (ros::ok()) {
        // 创建一个消息并设置随机数
        std_msgs::Int32 msg;
        msg.data = std::rand() % 100;  // 生成 0 到 99 的随机数

        // 发布消息
        random_pub.publish(msg);
        ROS_INFO(&quot;Published random number: %d&quot;, msg.data);

        // 休眠直到下一个循环
        ros::spinOnce();
        loop_rate.sleep();
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;CMakeLists.txt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_executable(random_number_publisher src/random_number_publisher.cpp)
target_link_libraries(random_number_publisher ${catkin_LIBRARIES})
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编译包&lt;/p&gt;
&lt;p&gt;回到工作空间，输入&lt;code&gt;catkin_make&lt;/code&gt;编译包&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;5.2.2.2 py版本的实现&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;pub_sub_pkg/src&lt;/code&gt; 目录下创建一个名为 &lt;code&gt;random_number_publisher.py&lt;/code&gt; 的文件，并添加如下内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import rospy
from std_msgs.msg import Int32
import random

def random_number_publisher():
    # 初始化节点
    rospy.init_node(&apos;random_number_publisher&apos;, anonymous=True)
    # 创建一个发布者，发布到 &quot;random_number&quot; 主题
    pub = rospy.Publisher(&apos;random_number&apos;, Int32, queue_size=10)
    # 设置循环频率
    rate = rospy.Rate(1)  # 每秒发布一次

    while not rospy.is_shutdown():
        # 生成随机数
        random_number = random.randint(0, 99)  # 生成 0 到 99 的随机数
        rospy.loginfo(&quot;Published random number: %d&quot;, random_number)
        # 创建消息并发布
        pub.publish(random_number)
        # 休眠直到下一个循环
        rate.sleep()

if __name__ == &apos;__main__&apos;:
    try:
        random_number_publisher()
    except rospy.ROSInterruptException:
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;CMakeLists.txt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;catkin_install_python(PROGRAMS
  src/random_number_publisher.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编译包&lt;/p&gt;
&lt;p&gt;回到工作空间，输入&lt;code&gt;catkin_make&lt;/code&gt;编译包&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5.2.3 Subscriber的编写&lt;/h4&gt;
&lt;h5&gt;5.2.3.1 cpp版本的实现&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;pub_sub_pkg/src&lt;/code&gt; 目录下创建一个名为 &lt;code&gt;random_number_subscriber.cpp&lt;/code&gt; 的文件，并添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;ros/ros.h&gt;
#include &amp;#x3C;std_msgs/Int32.h&gt;

void randomNumberCallback(const std_msgs::Int32::ConstPtr&amp;#x26; msg) {
    ROS_INFO(&quot;Received random number: %d&quot;, msg-&gt;data);
}

int main(int argc, char** argv) {
    ros::init(argc, argv, &quot;random_number_subscriber&quot;);
    ros::NodeHandle nh;

    // 创建一个订阅者，订阅 &quot;random_number&quot; 主题
    ros::Subscriber sub = nh.subscribe(&quot;random_number&quot;, 10, randomNumberCallback);

    // 循环等待回调
    ros::spin();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;CMakeLists.txt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_executable(random_number_subscriber src/random_number_subscriber.cpp)
target_link_libraries(random_number_subscriber ${catkin_LIBRARIES})
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编译包&lt;/p&gt;
&lt;p&gt;回到工作空间，输入&lt;code&gt;catkin_make&lt;/code&gt;编译包&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;5.2.3.2 py版本的实现&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;pub_sub_pkg/src&lt;/code&gt; 目录下创建一个名为 &lt;code&gt;random_number_subscriber.py&lt;/code&gt; 的文件，并添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import rospy
from std_msgs.msg import Int32

def random_number_callback(msg):
    rospy.loginfo(&quot;Received random number: %d&quot;, msg.data)

def random_number_subscriber():
    # 初始化节点
    rospy.init_node(&apos;random_number_subscriber&apos;, anonymous=True)
    # 创建一个订阅者，订阅 &quot;random_number&quot; 主题
    rospy.Subscriber(&apos;random_number&apos;, Int32, random_number_callback)

    # 循环等待回调
    rospy.spin()

if __name__ == &apos;__main__&apos;:
    random_number_subscriber()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;CMakeLists.txt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;catkin_install_python(PROGRAMS
  src/random_number_subscriber.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编译包&lt;/p&gt;
&lt;p&gt;回到工作空间，输入&lt;code&gt;catkin_make&lt;/code&gt;编译包&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5.2.4 运行节点&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动 ROS 核心：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;roscore
&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;若运行cpp版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun pub_sub_pkg random_number_publisher
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若运行py版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun pub_sub_pkg random_number_publisher.py
&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;ul&gt;
&lt;li&gt;
&lt;p&gt;若运行cpp版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun pub_sub_pkg random_number_subscriber
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若运行py版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun pub_sub_pkg random_number_subscriber.py
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;运行效果如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241102132155934.BgUe6C1L_2qzTs5.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241102132217586.C3gSQsK1_ZDQrTr.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;h3&gt;5.3 自定义消息类型&lt;/h3&gt;
&lt;p&gt;面向更加复杂的应用，我们需要创建自定义消息类型来满足开发需求。我们创建一个功能包名为&lt;code&gt;custom_message&lt;/code&gt;，并在该功能包中进行以下操作。&lt;/p&gt;
&lt;h4&gt;5.3.1 创建自定义消息类型&lt;/h4&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;custom_message&lt;/code&gt;目录下创建&lt;code&gt;msg&lt;/code&gt;文件夹，并在该文件夹中创建一个&lt;code&gt;RandomNumber.msg&lt;/code&gt;文件，添加如下内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int32 number
string message
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;package.xml&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;package.xml&lt;/code&gt; 文件中添加以下内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;build_depend&gt;message_generation&amp;#x3C;/build_depend&gt;
&amp;#x3C;exec_depend&gt;message_runtime&amp;#x3C;/exec_depend&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;CMakeLists.txt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;可对照下面内容进行修改&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  message_generation
)

add_message_files(
  FILES
  RandomNumber.msg
)

generate_messages(
  DEPENDENCIES
  std_msgs
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以先用&lt;code&gt;catkin_make&lt;/code&gt;编译一次，方便之后的源文件调用&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后在之前的&lt;code&gt;Publisher&lt;/code&gt;和&lt;code&gt;Subscriber&lt;/code&gt;的基础上修改代码&lt;/p&gt;
&lt;h4&gt;5.3.2 修改发布节点&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;random_msg_publisher.cpp&lt;/code&gt;的内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;ros/ros.h&gt;
#include &amp;#x3C;custom_message/RandomNumber.h&gt;
#include &amp;#x3C;cstdlib&gt;  // for rand()
#include &amp;#x3C;ctime&gt;    // for time()

int main(int argc, char** argv) {
    ros::init(argc, argv, &quot;random_number_publisher&quot;);
    ros::NodeHandle nh;

    // 创建一个发布者，发布到 &quot;random_number&quot; 主题
    ros::Publisher random_pub = nh.advertise&amp;#x3C;custom_message::RandomNumber&gt;(&quot;random_number&quot;, 10);

    // 初始化随机数生成器
    std::srand(static_cast&amp;#x3C;unsigned int&gt;(std::time(0)));

    // 设置循环频率
    ros::Rate loop_rate(1);  // 每秒发布一次

    while (ros::ok()) {
        // 创建一个消息并设置随机数
        custom_message::RandomNumber msg;
        msg.number = std::rand() % 100;  // 生成 0 到 99 的随机数
        msg.message = &quot;Random number generated&quot;;
        // 发布消息
        random_pub.publish(msg);
        ROS_INFO(&quot;Published: number = %d, message = %s&quot;, msg.number, msg.message.c_str());

        // 休眠直到下一个循环
        ros::spinOnce();
        loop_rate.sleep();
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;random_msg_publisher.py&lt;/code&gt;的内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import rospy
from custom_message.msg import RandomNumber
import random

def random_number_publisher():
    # 初始化节点
    rospy.init_node(&apos;random_number_publisher&apos;, anonymous=True)
    # 创建一个发布者，发布到 &quot;random_number&quot; 主题
    pub = rospy.Publisher(&apos;random_number&apos;, RandomNumber, queue_size=10)
    # 设置循环频率
    rate = rospy.Rate(1)  # 每秒发布一次

    while not rospy.is_shutdown():
        msg = RandomNumber()
        msg.number = random.randint(0, 99)
        msg.message = &quot;Random number generated&quot;
        
        rospy.loginfo(&quot;Published: number = %d, message = %s&quot;, msg.number, msg.message)
        pub.publish(msg)

        rate.sleep()

if __name__ == &apos;__main__&apos;:
    try:
        random_number_publisher()
    except rospy.ROSInterruptException:
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CMakeLists.txt&lt;/code&gt;添加&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_executable(random_msg_publisher src/random_msg_publisher.cpp)
target_link_libraries(random_msg_publisher ${catkin_LIBRARIES})

catkin_install_python(PROGRAMS
  src/random_msg_publisher.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.3.3 修改订阅节点&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;random_msg_subscriber.cpp&lt;/code&gt;内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;ros/ros.h&gt;
#include &amp;#x3C;custom_message/RandomNumber.h&gt;

void randomNumberCallback(const custom_message::RandomNumber::ConstPtr&amp;#x26; msg) {
    ROS_INFO(&quot;Received: number = %d, message = %s&quot;, msg-&gt;number, msg-&gt;message.c_str());
}

int main(int argc, char** argv) {
    ros::init(argc, argv, &quot;random_number_subscriber&quot;);
    ros::NodeHandle nh;

    ros::Subscriber sub = nh.subscribe(&quot;random_number&quot;, 10, randomNumberCallback);
    ros::spin();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;random_msg_subscriber.py&lt;/code&gt;内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import rospy
from custom_message.msg import RandomNumber

def random_number_callback(msg):
    rospy.loginfo(&quot;Received: number = %d, message = %s&quot;, msg.number, msg.message)

def random_number_subscriber():
    rospy.init_node(&apos;random_number_subscriber&apos;, anonymous=True)
    rospy.Subscriber(&apos;random_number&apos;, RandomNumber, random_number_callback)
    rospy.spin()

if __name__ == &apos;__main__&apos;:
    random_number_subscriber()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CMakeLists.txt&lt;/code&gt;添加：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_executable(random_msg_subscriber src/random_msg_subscriber.cpp)
target_link_libraries(random_msg_subscriber ${catkin_LIBRARIES})

catkin_install_python(PROGRAMS
  src/random_msg_subscriber.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.3.4 运行节点&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动 ROS 核心：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;roscore
&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;若运行cpp版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun custom_message random_msg_publisher
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若运行py版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun custom_message random_msg_publisher.py
&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;ul&gt;
&lt;li&gt;
&lt;p&gt;若运行cpp版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun custom_message random_msg_subscriber
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若运行py版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun custom_message random_msg_subscriber.py
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;运行效果如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241102141836960.thVLxP37_OnjS3.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241102141850785.CQoCoUVA_Z2tAk0k.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;h3&gt;5.4 服务通信&lt;/h3&gt;
&lt;p&gt;服务通信模型中涉及到三个角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ROS master(管理者)&lt;/li&gt;
&lt;li&gt;Server(服务端)&lt;/li&gt;
&lt;li&gt;Client(客户端)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ROS Master 负责保管 Server 和 Client 注册的信息，并匹配话题相同的 Server 与 Client ，帮助 Server 与 Client 建立连接，连接建立后，Client 发送请求信息，Server 返回响应信息。理论模型如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241102142828094.CHJQCtLE_26A5PN.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里实现一个简单的服务通信，客户端发出两个数字请求，服务端返回它们的和。&lt;/p&gt;
&lt;h4&gt;5.4.1 创建功能包&lt;/h4&gt;
&lt;p&gt;我们创建一个名为 &lt;code&gt;add_service&lt;/code&gt; 的包，在&lt;code&gt;src&lt;/code&gt;文件夹下执行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;catkin_create_pkg add_service std_msgs rospy roscpp message_generation
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.4.2 创建自定义服务消息&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在功能包中新建&lt;code&gt;srv&lt;/code&gt;文件夹，在里面添加&lt;code&gt;AddTwoInts.srv&lt;/code&gt;文件，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int64 a
int64 b
---
int64 sum
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;---&lt;/code&gt;上方的是请求数据类型，下方的是响应数据类型。这里定义了一个请求包含两个整数 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt;，响应包含它们的和 &lt;code&gt;sum&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保&lt;code&gt;CMakeLists.txt&lt;/code&gt;被修改成如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;find_package(catkin REQUIRED COMPONENTS
  rospy
  std_msgs
  message_generation
)

add_service_files(
  FILES
  AddTwoInts.srv
)

generate_messages(
  DEPENDENCIES
  std_msgs
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保&lt;code&gt;package.xml&lt;/code&gt;有如下依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;build_depend&gt;message_generation&amp;#x3C;/build_depend&gt;
&amp;#x3C;exec_depend&gt;message_runtime&amp;#x3C;/exec_depend&gt;
&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;h4&gt;5.4.3 创建服务端&lt;/h4&gt;
&lt;h5&gt;5.4.3.1 cpp版本的实现&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;add_service/src&lt;/code&gt; 目录下创建一个名为 &lt;code&gt;add_service_server.cpp&lt;/code&gt; 的文件，并添加以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;ros/ros.h&gt;
#include &amp;#x3C;add_service/AddTwoInts.h&gt; // 引入自定义服务消息头文件

// 处理加法请求的回调函数
bool handle_add_two_ints(add_service::AddTwoInts::Request &amp;#x26;req,
                          add_service::AddTwoInts::Response &amp;#x26;res) {
    // 计算请求的两个整数之和
    res.sum = req.a + req.b;
    ROS_INFO(&quot;Request: a=%ld, b=%ld; Sending back response: sum=%ld&quot;, req.a, req.b, res.sum);
    return true; // 返回成功
}

int main(int argc, char **argv) {
    ros::init(argc, argv, &quot;add_service_server&quot;); // 初始化节点
    ros::NodeHandle n; // 创建节点句柄

    // 创建服务，注册到名为 &quot;add_two_ints&quot; 的服务上
    ros::ServiceServer service = n.advertiseService(&quot;add_two_ints&quot;, handle_add_two_ints);
    ROS_INFO(&quot;Ready to add two ints.&quot;); // 打印日志信息
    ros::spin(); // 进入循环，等待服务请求

    return 0; // 返回成功
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;CMakeLists.txt&lt;/code&gt;，添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_executable(add_service_server src/add_service_server.cpp)
target_link_libraries(add_service_server ${catkin_LIBRARIES})
&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;h5&gt;5.4.3.2 py 版本的实现&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;add_service/src&lt;/code&gt; 目录下创建一个名为 &lt;code&gt;add_service_server.py&lt;/code&gt; 的文件，并添加以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import rospy
from add_service.srv import AddTwoInts, AddTwoIntsResponse # 引入自定义服务消息

# 处理加法请求的回调函数
def handle_add_two_ints(req):
    result = req.a + req.b # 计算请求的两个整数之和
    rospy.loginfo(&quot;Request: a={}, b={}; Sending back response: sum={}&quot;.format(req.a, req.b, result))
    return AddTwoIntsResponse(result) # 返回结果

# 服务端初始化
def add_service_server():
    rospy.init_node(&apos;add_service_server&apos;) # 初始化节点
    s = rospy.Service(&apos;add_two_ints&apos;, AddTwoInts, handle_add_two_ints) # 注册服务
    rospy.loginfo(&quot;Ready to add two ints.&quot;) # 打印日志信息
    rospy.spin() # 进入循环，等待服务请求

if __name__ == &quot;__main__&quot;:
    add_service_server() # 启动服务端
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;CMakeLists.txt&lt;/code&gt;，添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;catkin_install_python(PROGRAMS
src/add_service_server.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
&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;h4&gt;5.4.4 创建客户端&lt;/h4&gt;
&lt;h5&gt;5.4.4.1 cpp版本的实现&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;add_service/src&lt;/code&gt; 目录下创建一个名为 &lt;code&gt;add_service_client.cpp&lt;/code&gt; 的文件，并添加以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;ros/ros.h&gt;
#include &amp;#x3C;add_service/AddTwoInts.h&gt; // 引入自定义服务消息头文件

int main(int argc, char **argv) {
    // 检查命令行参数数量
    if (argc != 3) {
        ROS_INFO(&quot;Usage: add_service_client &amp;#x3C;num1&gt; &amp;#x3C;num2&gt;&quot;);
        return 1; // 返回错误
    }

    ros::init(argc, argv, &quot;add_service_client&quot;); // 初始化节点
    ros::NodeHandle n; // 创建节点句柄

    // 创建服务客户端，连接到 &quot;add_two_ints&quot; 服务
    ros::ServiceClient client = n.serviceClient&amp;#x3C;add_service::AddTwoInts&gt;(&quot;add_two_ints&quot;);
    add_service::AddTwoInts srv; // 创建服务请求对象
    srv.request.a = atoll(argv[1]); // 将第一个参数转为整数并赋值
    srv.request.b = atoll(argv[2]); // 将第二个参数转为整数并赋值

    // 调用服务并检查是否成功
    if (client.call(srv)) {
        ROS_INFO(&quot;Sum: %ld&quot;, srv.response.sum); // 打印返回的和
    } else {
        ROS_ERROR(&quot;Failed to call service add_two_ints&quot;); // 打印错误信息
        return 1; // 返回错误
    }

    return 0; // 返回成功
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;CMakeLists.txt&lt;/code&gt;，添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_executable(add_service_client src/add_service_client.cpp)
target_link_libraries(add_service_client ${catkin_LIBRARIES})
&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;h5&gt;5.4.4.2 py版本的实现&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;add_service/src&lt;/code&gt; 目录下创建一个名为 &lt;code&gt;add_service_client.py&lt;/code&gt; 的文件，并添加以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import sys
import rospy
from add_service.srv import AddTwoInts # 引入自定义服务消息

# 客户端请求加法服务
def add_two_ints_client(x, y):
    rospy.wait_for_service(&apos;add_two_ints&apos;) # 等待服务可用
    try:
        add_two_ints = rospy.ServiceProxy(&apos;add_two_ints&apos;, AddTwoInts) # 创建服务代理
        resp = add_two_ints(x, y) # 调用服务
        return resp.sum # 返回结果
    except rospy.ServiceException as e:
        print(&quot;Service call failed: {}&quot;.format(e)) # 打印错误信息

if __name__ == &quot;__main__&quot;:
    # 检查命令行参数数量
    if len(sys.argv) != 3:
        print(&quot;Usage: add_service_client.py [num1] [num2]&quot;)
        sys.exit(1) # 返回错误

    num1 = int(sys.argv[1]) # 将第一个参数转为整数
    num2 = int(sys.argv[2]) # 将第二个参数转为整数

    rospy.init_node(&apos;add_service_client&apos;) # 初始化节点
    print(&quot;Requesting {} + {}&quot;.format(num1, num2)) # 打印请求信息
    sum = add_two_ints_client(num1, num2) # 请求服务
    print(&quot;Sum: {}&quot;.format(sum)) # 打印结果
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;CMakeLists.txt&lt;/code&gt;，添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;catkin_install_python(PROGRAMS
src/add_service_client.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
&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;h4&gt;5.4.5 运行节点&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动 ROS 核心：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;roscore
&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;若运行cpp版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun add_service add_service_server
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若运行py版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun add_service add_service_server.py
&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;ul&gt;
&lt;li&gt;
&lt;p&gt;若运行cpp版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun add_service add_service_client num1 num2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若运行py版本使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun add_service add_service_client.py num1 num2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;运行效果如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241102151924299.1dMZG9dx_1e13jr.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241102151943338.CzvB5GGg_Zs2avG.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;h3&gt;5.5 参数操作&lt;/h3&gt;
&lt;h4&gt;5.5.1 创建功能包&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;src&lt;/code&gt;文件夹中执行如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;catkin_create_pkg param_example std_msgs rospy roscpp
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.5.2 cpp实现&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;param_example/src&lt;/code&gt; 目录下创建一个名为 &lt;code&gt;param_example_node.cpp&lt;/code&gt; 的文件，并添加以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;ros/ros.h&gt;

int main(int argc, char **argv) {
    ros::init(argc, argv, &quot;param_example_node&quot;); // 初始化节点
    ros::NodeHandle n; // 创建节点句柄

    // 设置参数
    n.setParam(&quot;example_param&quot;, 10); // 设置名为 &quot;example_param&quot; 的参数

    // 获取参数
    int param_value;
    if (n.getParam(&quot;example_param&quot;, param_value)) { // 获取参数值
        ROS_INFO(&quot;Got parameter: example_param=%d&quot;, param_value); // 打印参数值
    } else {
        ROS_WARN(&quot;Could not get parameter: example_param&quot;); // 打印警告
    }

    // 修改参数
    n.setParam(&quot;example_param&quot;, 20); // 修改参数值

    // 重新获取参数
    if (n.getParam(&quot;example_param&quot;, param_value)) {
        ROS_INFO(&quot;Updated parameter: example_param=%d&quot;, param_value); // 打印更新后的参数值
    }

    ros::spinOnce(); // 处理回调
    return 0; // 返回成功
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;CMakeLists.txt&lt;/code&gt; 文件中添加 C++ 可执行文件的配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_executable(param_example_node src/param_example_node.cpp)
target_link_libraries(param_example_node ${catkin_LIBRARIES})
&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;h4&gt;5.5.3 py实现&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;param_example/src&lt;/code&gt; 目录下创建一个名为 &lt;code&gt;param_example_node.py&lt;/code&gt; 的文件，并添加以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import rospy

if __name__ == &quot;__main__&quot;:
    rospy.init_node(&apos;param_example_node&apos;) # 初始化节点

    # 设置参数
    rospy.set_param(&apos;example_param&apos;, 10) # 设置名为 &quot;example_param&quot; 的参数

    # 获取参数
    param_value = rospy.get_param(&apos;example_param&apos;, None) # 获取参数值
    if param_value is not None:
        rospy.loginfo(&quot;Got parameter: example_param={}&quot;.format(param_value)) # 打印参数值
    else:
        rospy.logwarn(&quot;Could not get parameter: example_param&quot;) # 打印警告

    # 修改参数
    rospy.set_param(&apos;example_param&apos;, 20) # 修改参数值

    # 重新获取参数
    param_value = rospy.get_param(&apos;example_param&apos;)
    rospy.loginfo(&quot;Updated parameter: example_param={}&quot;.format(param_value)) # 打印更新后的参数值

    rospy.spin() # 进入循环
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;CMakeLists.txt&lt;/code&gt; 文件中如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;catkin_install_python(PROGRAMS
  src/param_example_node.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
&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;h4&gt;5.5.4 运行节点&lt;/h4&gt;
&lt;p&gt;启动 ROS 核心：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;roscore
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行节点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;cpp版本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun param_example param_example_node
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;py版本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosrun param_example param_example_node.py
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241102154254201.D7vf2vyn_aydmy.webp&quot; alt=&quot;image-20241102154254201.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;6. launch文件的使用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;launch&lt;/code&gt; 文件是一种 XML 格式的文件，用于定义和管理节点的启动过程。通过 &lt;code&gt;launch&lt;/code&gt; 文件，可以一次性启动多个节点，设置参数，定义节点间的连接（如话题、服务等），并配置其他相关选项。这使得管理复杂的机器人系统变得更加简单和高效。&lt;/p&gt;
&lt;h3&gt;6.1 新建launch文件&lt;/h3&gt;
&lt;p&gt;在功能包下新建 &lt;code&gt;launch&lt;/code&gt; 目录, 目录下新建 &lt;code&gt;xxxx.launch&lt;/code&gt; 文件。一个简单示例如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;launch&gt;
    &amp;#x3C;param name=&quot;example_param&quot; value=&quot;10&quot;/&gt; &amp;#x3C;!-- 设置全局参数 --&gt;

    &amp;#x3C;node pkg=&quot;param_example&quot; type=&quot;param_example_node&quot; name=&quot;param_example_node&quot; output=&quot;screen&quot;&gt;
        &amp;#x3C;param name=&quot;node_param&quot; value=&quot;5&quot;/&gt; &amp;#x3C;!-- 设置节点特定参数 --&gt;
    &amp;#x3C;/node&gt;
&amp;#x3C;/launch&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.2 launch文件的标签和属性&lt;/h3&gt;
&lt;p&gt;在 ROS 的 &lt;code&gt;launch&lt;/code&gt; 文件中，有多种属性和标签可用于配置节点的启动、参数设置、依赖管理等，常见的属性和标签如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;launch&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;根元素：每个 &lt;code&gt;launch&lt;/code&gt; 文件都以 &lt;code&gt;&amp;#x3C;launch&gt;&lt;/code&gt; 标签开始，表示该文件的开始。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;node&gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pkg&lt;/code&gt;：节点所属的 ROS 包名称。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;：节点的可执行文件名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：节点的名称（可以与可执行文件名相同，但推荐使用不同的名称以便于识别）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;output&lt;/code&gt;：节点的输出方式，可以是 &lt;code&gt;screen&lt;/code&gt;（在终端显示）或 &lt;code&gt;log&lt;/code&gt;（输出到日志文件）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;args&lt;/code&gt;：传递给节点的命令行参数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;respawn&lt;/code&gt;：如果节点崩溃是否自动重启，取值为 &lt;code&gt;true&lt;/code&gt; 或 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;respawn_delay&lt;/code&gt;：当 &lt;code&gt;respawn&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 时，重启节点的延迟时间（单位为秒）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;required&lt;/code&gt;：如果节点是必需的，设置为 &lt;code&gt;true&lt;/code&gt;，否则设置为 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;node pkg=&quot;my_package&quot; type=&quot;my_node&quot; name=&quot;my_node_name&quot; output=&quot;screen&quot; respawn=&quot;true&quot; respawn_delay=&quot;2&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;子级标签有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;env&lt;/code&gt; 环境变量设置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remap&lt;/code&gt; 重映射节点名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rosparam&lt;/code&gt; 参数设置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;param&lt;/code&gt; 参数设置&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;param&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;设置参数，主要属性如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：参数名称。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value&lt;/code&gt;：参数的值，可以是常量、字符串、布尔值等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;：参数类型，支持 &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;bool&lt;/code&gt; 等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;param name=&quot;param_name&quot; value=&quot;param_value&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;rosparam&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;用于加载参数文件，支持 YAML 格式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;file&lt;/code&gt;：指定要加载的 YAML 文件的路径。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;command&lt;/code&gt;：可以是 &lt;code&gt;load&lt;/code&gt; 或 &lt;code&gt;dump&lt;/code&gt;，分别用于加载和导出参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;rosparam file=&quot;$(find my_package)/config/params.yaml&quot; command=&quot;load&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;include&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;用于从其他 &lt;code&gt;launch&lt;/code&gt; 文件中引用节点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;file&lt;/code&gt;：要包含的 &lt;code&gt;launch&lt;/code&gt; 文件的路径，可以使用 &lt;code&gt;$(find package_name)&lt;/code&gt; 来引用包内的文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;include file=&quot;$(find another_package)/launch/another_launch_file.launch&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;group&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;将多个节点或参数分组，可以设置特定的属性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：分组名称，通常用于命名空间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ns&lt;/code&gt;：定义命名空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;group ns=&quot;my_namespace&quot;&gt;
    &amp;#x3C;node pkg=&quot;my_package&quot; type=&quot;my_node&quot; name=&quot;my_node_name&quot;/&gt;
&amp;#x3C;/group&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;test&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;用于运行测试节点，主要属性如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pkg&lt;/code&gt;：测试节点所属的 ROS 包名称。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;：测试节点的可执行文件名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：测试节点的名称。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;test pkg=&quot;my_package&quot; type=&quot;test_node&quot; name=&quot;test_node_name&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;arg&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;定义命令行参数，主要属性如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：参数名称。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;default&lt;/code&gt;：默认值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;arg name=&quot;my_arg&quot; default=&quot;default_value&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;$(arg my_arg)&lt;/code&gt; 可以引用该参数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;remap&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;用于重映射话题名称。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;from&lt;/code&gt;：原始话题名称。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;to&lt;/code&gt;：新话题名称。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;remap from=&quot;/old_topic&quot; to=&quot;/new_topic&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.3 调用launch文件&lt;/h3&gt;
&lt;p&gt;使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;roslaunch 包名 xxx.launch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;roslaunch&lt;/code&gt; 命令执行launch文件时，首先会判断是否启动了 &lt;code&gt;roscore&lt;/code&gt;，如果启动了，则不再启动，否则，会自动调用 &lt;code&gt;roscore&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;7. 常用的ROS命令&lt;/h2&gt;
&lt;h3&gt;7.1 ROS信息命令&lt;/h3&gt;
&lt;h4&gt;7.1.1 rosnode&lt;/h4&gt;
&lt;p&gt;节点相关命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosnode ping    测试到节点的连接状态
rosnode list    列出活动节点
rosnode info    打印节点信息
rosnode machine    列出指定设备上节点
rosnode kill    杀死某个节点
rosnode cleanup    清除不可连接的节点
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7.1.2 rostopic&lt;/h4&gt;
&lt;p&gt;话题相关命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rostopic bw     显示主题使用的带宽
rostopic delay  显示带有 header 的主题延迟
rostopic echo   打印消息到屏幕
rostopic find   根据类型查找主题
rostopic hz     显示主题的发布频率
rostopic info   显示主题相关信息
rostopic list   显示所有活动状态下的主题
rostopic pub    将数据发布到主题
rostopic type   打印主题类型
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7.1.3 rosmsg&lt;/h4&gt;
&lt;p&gt;消息相关命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosmsg show    显示消息描述
rosmsg info    显示消息信息
rosmsg list    列出所有消息
rosmsg md5    显示 md5 加密后的消息
rosmsg package    显示某个功能包下的所有消息
rosmsg packages    列出包含消息的功能包
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7.1.4 rosservice&lt;/h4&gt;
&lt;p&gt;服务相关命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosservice args 打印服务参数
rosservice call    使用提供的参数调用服务
rosservice find    按照服务类型查找服务
rosservice info    打印有关服务的信息
rosservice list    列出所有活动的服务
rosservice type    打印服务类型
rosservice uri    打印服务的 ROSRPC uri
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7.1.5 rossrv&lt;/h4&gt;
&lt;p&gt;服务信息相关命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rossrv show    显示服务消息详情
rossrv info    显示服务消息相关信息
rossrv list    列出所有服务信息
rossrv md5    显示 md5 加密后的服务消息
rossrv package    显示某个包下所有服务消息
rossrv packages    显示包含服务消息的所有包
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7.1.6 rosparam&lt;/h4&gt;
&lt;p&gt;参数相关命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rosparam set    设置参数
rosparam get    获取参数
rosparam load    从外部文件加载参数
rosparam dump    将参数写出到外部文件
rosparam delete    删除参数
rosparam list    列出所有参数
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7.1.7 rosbag&lt;/h4&gt;
&lt;p&gt;记录和重放相关命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;check        确定 bag 在当前系统中是否可播放，或者是否可以迁移
compress     压缩一个或多个包文件
decompress   解压缩一个或多个包文件
decrypt      解密一个或多个包文件
encrypt      加密一个或多个 bag 文件
filter       过滤bag的内容
fix          修复 bag 文件中的消息，以便它可以在当前系统中播放。
info         总结一个或多个文件包的内容。
play         以时间同步的方式播放一个或多个包文件的内容。
record       录制包含指定 topic 内容的 bag 文件。
reindex      重新索引一个或多个 bag 文件。
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7.1.8 rosversion&lt;/h4&gt;
&lt;p&gt;显示ROS功能包的版本信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-h, --help          显示此帮助消息并退出
-s, --skip-newline  跳过尾随换行符
-d, --distro        输出 ROS 发行版名称
-a, --all           显示所有 ROS 软件包名称及其版本
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.2 ROS shell 命令&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ROS shell&lt;/code&gt;命令又被称为&lt;code&gt;rosbash&lt;/code&gt;。相关命令如下：&lt;/p&gt;
&lt;p&gt;| 命令  | 命令释义                  | 详细说明                  | 参考模板                      |
| ----- | ------------------------- | ------------------------- | ----------------------------- |
| roscd | ros+cd(changes directory) | 移动到指定的ROS功能包目录 | roscd [功能包名称]            |
| rosls | ros+ls(lists files)       | 显示ROS功能包的文件和目录 | rosls [功能包名称]            |
| rosed | ros+ed(editor)            | 编辑ROS功能包的文件       | rosed [功能包名称] [文件名称] |
| roscp | ros+cp(copies files)      | 复制ROS功能包的文件       | —                             |
| rospd | ros+pushd                 | 添加目录至ROS目录索引     | —                             |
| rosd  | ros+directory             | 显示ROS目录索引中的目录   | —                             |&lt;/p&gt;
&lt;h3&gt;7.3 ROS 执行命令&lt;/h3&gt;
&lt;p&gt;ROS 执行命令管理ROS节点的运行。&lt;/p&gt;
&lt;p&gt;| 命令      | 命令释义   | 详细说明                                                     | 参考模板                              |
| --------- | ---------- | ------------------------------------------------------------ | ------------------------------------- |
| roscore   | ros+core   | master（ROS名称服务） + rosout（日志记录） + parameter server（参数管理） | roscore [选项]                        |
| rosrun    | ros+run    | 运行单个节点                                                 | rosrun [功能包名称] [节点名称]        |
| roslaunch | ros+launch | 运行多个节点或设置运行选项                                   | roslaunch [功能包名称] [launch文件名] |
| rosclean  | ros+clean  | 检查或删除ROS日志文件                                        | rosclean [选项]                       |&lt;/p&gt;
&lt;h3&gt;7.4 ROS catkin命令&lt;/h3&gt;
&lt;p&gt;ROS catkin命令ROS的catkin命令用于使用catkin 构建系统来构建功能包。&lt;/p&gt;
&lt;p&gt;| 命令                      | 详细说明                                                     | 参考模板                                                     |
| ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| catkin_create_pkg         | 自动生成功能包                                               | catkin_create_pkg [功能包名称] [依赖性功能包1] [依赖性功能包2] … |
| catkin_make               | 基于catkin 构建系统的构建                                    | catkin_make --[选项]                                         |
| catkin_eclipse            | 把用catkin 构建系统生成的功能包修改，使得可以在Eclipse 环境中使用 | catkin_eclipse                                               |
| catkin_prepare_release    | 发布时用到的日志整理和版本标记                               | catkin_prepare_release                                       |
| catkin_generate_changelog | 发布时生成或更新CHANGELOG.rst文件                            | catkin_generate_changelog                                    |
| catkin_init_workspace     | 初始化catkin 构建系统的工作目录                              | catkin_init_workspace                                        |
| catkin_find               | 搜索catkin                                                   | catkin_find [功能包名称]                                     |&lt;/p&gt;
&lt;h3&gt;7.5 ROS功能包命令&lt;/h3&gt;
&lt;p&gt;ROS 功能包命令用于操作ROS功能包，比如显示功能包信息、安装相关功能包等。&lt;/p&gt;
&lt;p&gt;| 命令          | 命令释义           | 详细说明                              | 参考模板                      |
| ------------- | ------------------ | ------------------------------------- | ----------------------------- |
| rospack       | ros+pack(age)      | 显示与ROS功能包相关的信息             | rospack [选项] [功能包名称]   |
| rosinstall    | ros+install        | 安装ROS附加功能包                     | ——                            |
| rosdep        | ros+dep(endencies) | 安装该功能包的依赖性文件              | rosdep [选项]                 |
| roslocate     | ros+locate         | 与ROS功能包信息有关的命令             | roslocate [选项] [功能包名称] |
| roscreate-pkg | ros+create-pkg     | 自动生成ROS功能包                     | （用于旧的rosbuild系统）      |
| rosmake       | ros+make           | 构建ROS功能包（用于旧的rosbuild系统） | ——                            |&lt;/p&gt;
&lt;h2&gt;8. 参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.ros.org/ROS/Tutorials&quot;&gt;ROS/Tutorials - ROS Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.autolabor.com.cn/book/ROSTutorials/index.html&quot;&gt;Introduction · Autolabor-ROS机器人入门课程《ROS理论与实践》零基础教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_31136513/article/details/106272169&quot;&gt;《学习篇》学会这18个常用ROS命令集合就能入门ROS了_ros常用命令-CSDN博客&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Linux 中使用 Terminator 进行分屏</title><link>https://liangbm3.site/blog/linux-zhong-shi-yong-terminator-jin-xing-fen-ping</link><guid isPermaLink="true">https://liangbm3.site/blog/linux-zhong-shi-yong-terminator-jin-xing-fen-ping</guid><description>在进行复杂的任务时，通常需要开多个终端，有时候非常凌乱，而Terminator则是一个很好的工具，能够很好地帮助我们帮助我们同时管理多个终端</description><pubDate>Sat, 02 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;在进行复杂的任务时，通常需要开多个终端，有时候非常凌乱，而Terminator则是一个很好的工具，能够很好地帮助我们帮助我们同时管理多个终端。Terminator 的核心功能在于其便捷的“分屏”能力，允许用户在单个窗口内垂直或水平分割屏幕，从而同时显示多个终端会话。对于开发者、系统管理员或需要处理多任务的用户来说，Terminator 是一个高效的工具。&lt;/p&gt;
&lt;h3&gt;2.安装&lt;/h3&gt;
&lt;p&gt;在Ubuntu上，可以使用如下命令进行安装&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update
sudo apt install terminator
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 基本使用&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动&lt;/p&gt;
&lt;p&gt;使用快捷键&lt;code&gt;ctrl+Alt+T&lt;/code&gt;或者在终端中输入&lt;code&gt;terminator&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分屏&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;垂直分屏：按下 &lt;code&gt;Ctrl + Shift + E&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;水平分屏：按下 &lt;code&gt;Ctrl + Shift + O&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;可以通过鼠标拖动分隔线来调整各个窗口的大小&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管理分屏&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关闭窗口：右键单击窗口，选择 “关闭” 或使用 &lt;code&gt;Ctrl + Shift + W&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;切换焦点：使用鼠标单击在不同窗口之间切换焦点。&lt;/li&gt;
&lt;li&gt;组合窗口：使用鼠标可以将窗口拖拽到其他窗口旁边，形成组合。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;右键选择 “首选项” 可以进行个性化配置，包括配色方案、字体、背景透明度等。&lt;/li&gt;
&lt;li&gt;Terminator 支持保存多种配置，可以根据不同任务选择对应的终端配置，提高使用效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;p&gt;以上就是terminator的常用命令，非常简单，只是顺手记录一下~&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>华硕笔记本 Linux 系统的充电阈值控制</title><link>https://liangbm3.site/blog/hua-shuo-bi-ji-ben-linux-xi-tong-de-chong-dian-yu-zhi-kong-zhi</link><guid isPermaLink="true">https://liangbm3.site/blog/hua-shuo-bi-ji-ben-linux-xi-tong-de-chong-dian-yu-zhi-kong-zhi</guid><description>解决华硕笔记本安装双系统后，在Linux中的充电阈值控制问题</description><pubDate>Thu, 31 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;如果电脑为华硕笔记本，那么是可以通过在Windows中安装华硕管家，来控制电池的充电阈值，使得电脑的电池保持健康的状态，如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241030041828379.B-IH9eue_23QzUh.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是我们安装双系统后，在Linux系统中，该功能无法使用。目前，华硕官方没有发布在Linux中的驱动程序，但是有一个开源仓库可以解决这个问题。&lt;/p&gt;
&lt;h2&gt;2. 仓库介绍&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/tshakalekholoane/bat&quot;&gt;tshakalekholoane/bat: Battery management utility for Linux laptops.&lt;/a&gt;仓库的目标是在 Windows 上复制适用于华硕笔记本电脑的华硕电池健康充电实用程序的功能，旨在延长电池的使用寿命&lt;/p&gt;
&lt;p&gt;程序的说明如下（已翻译）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;NAME
    bat - Linux 笔记本电脑的电池管理实用程序

SYNOPSIS
    bat [-d | --debug] [-h | --help] [-v | --version]
        &amp;#x3C;command&gt; [&amp;#x3C;arg&gt;]

OPTIONS
    -d, --debug
        显示调试信息。

    -h, --help
        打印此帮助文档。

    -v, --version
        显示版本信息和退出。

COMMANDS
    capacity
        打印当前电池电量。

    health
        打印电池健康状态。

    persist
        在重启之间保持当前阈值。

    reset
        取消重启之间的充电阈值的持久设置。

    status
        打印充电状态。

    threshold num
        打印当前充电阈值限制。

        如果指定了 num（应该是 1 到 100 之间的一个值），则这将设置一个新的充电阈值限制。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 安装流程&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;https://github.com/tshakalekholoane/bat/releases&quot;&gt;发布页面&lt;/a&gt;下载二进制文件，并将他移动到你想保存的位置，假设移动到&lt;code&gt;$HOME/&lt;/code&gt;目录下，执行以下命令，赋予该二进制文件执行权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chmod +x $HOME/bat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置软链接，这样任何用户可以从系统上的任何位置执行程序&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo ln -s $HOME/bat /usr/local/bin/bat
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 使用示例&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查看当前充电阈值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bat threshold
&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-bash&quot;&gt;sudo bat threshold 80
&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-bash&quot;&gt;sudo bat persist
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4. 参考资料&lt;/h2&gt;
&lt;p&gt;https://github.com/tshakalekholoane/bat&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Python 内置库 argparse 的使用</title><link>https://liangbm3.site/blog/python-nei-zhi-ku-argparse-de-shi-yong</link><guid isPermaLink="true">https://liangbm3.site/blog/python-nei-zhi-ku-argparse-de-shi-yong</guid><description>argparse 是 Python 标准库中用于解析命令行参数的模块，提供了一种简单、直观的方式来定义命令行接口，让程序可以直接从命令行获取输入并据此执行不同的操作，在许多深度学习的工程代码中经常用到。</description><pubDate>Wed, 30 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;argparse&lt;/code&gt; 是 Python 标准库中用于解析命令行参数的模块，提供了一种简单、直观的方式来定义命令行接口，让程序可以直接从命令行获取输入并据此执行不同的操作，在许多深度学习的工程代码中经常用到。&lt;/p&gt;
&lt;h2&gt;2. 基本参数类型&lt;/h2&gt;
&lt;p&gt;我们通过命令行运行 Python 文件时，可以向文件传递不同类型的参数。主要有以下几种类型：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;位置参数（Positional arguments）&lt;/strong&gt;：位置参数是指在命令行中按照特定顺序传递给程序的参数，它们不带任何前缀。示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py arg1 arg2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;选项参数（Optional arguments）&lt;/strong&gt;：选项参数是可选的参数，它们通常以短横线（-）或双短横线（--）开头。示例&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py --config ./config/test/aagcn_B_3d.yaml --phase test
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;标志参数（Flag arguments）&lt;/strong&gt;：标志参数是一种特殊的选项参数，它们不带值，只用于表示某个状态或开关是否打开。通常以短横线和单个字符表示，例如 &lt;code&gt;-v&lt;/code&gt; 表示 verbose 模式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py -v
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 基本用法&lt;/h2&gt;
&lt;h3&gt;3.1 创建解析器&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;argparse&lt;/code&gt; 的第一步是创建一个 &lt;code&gt;ArgumentParser&lt;/code&gt; 对象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser = argparse.ArgumentParser(description=&apos;Descriptive information&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;description&lt;/code&gt;是该对象的描述信息，可使用&lt;code&gt;python main.py -h&lt;/code&gt;命令查看。&lt;/p&gt;
&lt;h3&gt;3.2 添加参数&lt;/h3&gt;
&lt;p&gt;可以使用&lt;code&gt;parser.add_argument&lt;/code&gt;方法像解析器添加参数。&lt;/p&gt;
&lt;h4&gt;3.2.1 name属性&lt;/h4&gt;
&lt;p&gt;参数名称（&lt;code&gt;name&lt;/code&gt;）是传递给 &lt;code&gt;add_argument()&lt;/code&gt; 的第一个参数，直接指定了命令行参数的名称。根据参数前缀的不同，可以分为&lt;strong&gt;位置参数&lt;/strong&gt;和&lt;strong&gt;可选参数&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;添加位置参数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;位置参数是一个字符串，位置参数在使用命令行输入的时候位置不能调换，且不用输入参数名，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&apos;num1&apos;, type=int, help=&apos;The first number&apos;)
parser.add_argument(&apos;num2&apos;, type=int, help=&apos;The second number&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;添加可选参数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可选参数通常以短横线（-）或双短横线（--）开头，短横线后面一般是缩写，双短横线后面是参数的完整名称。短横线名称可以选择，双短横线名称必须存在。可选参数在使用命令行输入的时候位置可以调换，且要输入参数名，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&apos;-n1&apos;,&apos;--number1&apos;, type=int, help=&apos;The first number&apos;)
parser.add_argument(&apos;-n2&apos;,&apos;--number2&apos;, type=int, help=&apos;The second number&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py -n1 1314 -n2 520
python main.py --number2 520 --number1 1314
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3.2.2 type属性&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt; 属性用于指定参数的预期数据类型，帮助解析和验证命令行输入的数据格式。如果输入的数据类型不符合 &lt;code&gt;type&lt;/code&gt; 指定的格式，&lt;code&gt;argparse&lt;/code&gt; 会自动抛出错误。&lt;code&gt;type&lt;/code&gt; 属性支持多种常见的数据类型，还可以自定义数据转换函数。常见的数据类型如下&lt;/p&gt;
&lt;ol&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-python&quot;&gt;parser.add_argument(&quot;--name&quot;, type=str, help=&quot;Your name&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int&lt;/code&gt;：将输入解析为整数，适合计数、编号等整数类型的参数；如果输入非整数，会自动报错&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--age&quot;, type=int, help=&quot;Your age&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;float&lt;/code&gt;：将输入解析为浮点数，适合需要精确到小数的参数，比如学习率、权重等&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--learning-rate&quot;, type=float, help=&quot;Learning rate for training&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;除此之外，我们还可以通过传递自定义函数，将输入值解析为自定义格式。例如，可以定义一个将输入转换为复数的函数或仅允许特定格式的字符串输入&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def parse_complex(value):
    return complex(value)

parser.add_argument(&quot;--complex-number&quot;, type=parse_complex, help=&quot;A complex number input&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.2.3 help属性&lt;/h4&gt;
&lt;p&gt;可以通过这个属性为参数添加描述，用户在命令行输入&lt;code&gt;-h&lt;/code&gt;或&lt;code&gt;--help&lt;/code&gt;时会输入相应的帮助信息。&lt;/p&gt;
&lt;h4&gt;3.2.4 default属性&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;default&lt;/code&gt; 属性用于为参数设置默认值。如果命令行中没有提供该参数，解析后的结果会使用 &lt;code&gt;default&lt;/code&gt; 指定的默认值。示例&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--timeout&quot;, type=int, default=30, help=&quot;Set the timeout in seconds (default: 30)&quot;)
args = parser.parse_args()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.2.5 required属性&lt;/h4&gt;
&lt;p&gt;当 &lt;code&gt;required=True&lt;/code&gt; 时，程序运行时必须提供该参数，否则 &lt;code&gt;argparse&lt;/code&gt; 会自动提示错误并输出帮助信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--name&quot;, required=True,type=str, help=&quot;Your name&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.2.6 action属性&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;action&lt;/code&gt; 属性用于控制命令行参数的解析行为。不同的 &lt;code&gt;action&lt;/code&gt; 值会导致参数的处理方式不同，特别适用于控制布尔开关、多值参数以及参数累积。&lt;code&gt;action&lt;/code&gt;的常见可选值如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&quot;store&quot;&lt;/code&gt;：默认值，将用户输入的值存储在变量中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&quot;store_const&quot;&lt;/code&gt;：用于将一个预定义的常量值存储为参数值，而不是从命令行中获取该参数的输入。即不需要输入具体的值，只要参数存在，就赋予一个固定的值。示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--flag&quot;, action=&quot;store_const&quot;, const=42, default=0, help=&quot;Store a constant value of 42 if this flag is present&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当执行命令&lt;code&gt;python main.py --flag&lt;/code&gt;时，参数值为常量42。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&quot;store_true&quot;&lt;/code&gt;：将布尔值 &lt;code&gt;True&lt;/code&gt; 存储为参数值，常用于启用某个特性，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--debug&quot;, action=&quot;store_true&quot;, help=&quot;Enable debug mode&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--debug&lt;/code&gt;会将 &lt;code&gt;args.debug&lt;/code&gt; 设置为 &lt;code&gt;True&lt;/code&gt;，不指定则为 &lt;code&gt;False&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&quot;store_false&quot;&lt;/code&gt;：将布尔值 &lt;code&gt;False&lt;/code&gt; 存储为参数值，常用于禁用某个特性，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--no-cache&quot;, action=&quot;store_false&quot;, help=&quot;Disable caching&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--no-cache&lt;/code&gt; 会将 &lt;code&gt;args.no_cache&lt;/code&gt; 设置为 &lt;code&gt;False&lt;/code&gt;，不指定则为 &lt;code&gt;True&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&quot;append&quot;&lt;/code&gt;：将每次传入的值追加到一个列表中，适用于接受多个输入值的场景，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--tag&quot;, action=&quot;append&quot;, help=&quot;Add a tag to the list of tags&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令行输入 &lt;code&gt;--tag dev --tag test&lt;/code&gt; 后，&lt;code&gt;args.tag&lt;/code&gt; 的值为 &lt;code&gt;[&quot;dev&quot;, &quot;test&quot;]&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3.2.7 dest属性&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;dest&lt;/code&gt; 属性用于指定参数在解析后存储的变量名。通过设置 &lt;code&gt;dest&lt;/code&gt;，可以在代码中访问参数值时使用更直观或自定义的名称，而不依赖于命令行中传入的参数名称。示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import argparse

parser = argparse.ArgumentParser(description=&quot;Example of dest attribute&quot;)

# 使用 dest 定义变量名称为 &apos;input_path&apos;
parser.add_argument(&quot;--input&quot;, dest=&quot;input_path&quot;, type=str, help=&quot;Path to the input file&quot;)
parser.add_argument(&quot;-o&quot;, &quot;--output&quot;, dest=&quot;output_path&quot;, type=str, help=&quot;Path to the output file&quot;)

args = parser.parse_args()

print(&quot;Input path:&quot;, args.input_path)
print(&quot;Output path:&quot;, args.output_path)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.2.8 nargs属性&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;nargs&lt;/code&gt; 属性用于指定命令行参数能够接受的值的数量。允许我们控制参数的输入数量，包括支持单个、多个、可变数量的值，甚至可以将所有剩余的命令行参数捕获到一个列表中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nargs&lt;/code&gt;属性常见的取值如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;整数值：指定参数必须接受确切数量的值，如&lt;code&gt;nargs=2&lt;/code&gt; 表示参数需要接受两个值，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--range&quot;, nargs=2, type=int, help=&quot;Specify a start and end value&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令行输入&lt;code&gt;python main.py --range 1 10&lt;/code&gt;后得到的变量值为&lt;code&gt;args.range = [1, 10]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;?0&lt;/code&gt;：表示参数值是可选的，最多可以接受一个值。若不提供值，则使用默认值（如果设置了默认值）或 &lt;code&gt;None&lt;/code&gt;，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--verbosity&quot;, nargs=&quot;?&quot;, const=&quot;INFO&quot;, help=&quot;Set verbosity level (default: INFO if no value is provided)&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;命令行输入&lt;code&gt;python main.py --verbosity DEBUG&lt;/code&gt;得到的是&lt;code&gt;args.verbosity = &quot;DEBUG&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;命令行输入&lt;code&gt;python main.py --verbosity&lt;/code&gt;得到的是&lt;code&gt;args.verbosity = &quot;INFO&quot;&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--files&quot;, nargs=&quot;*&quot;, help=&quot;List of files to process&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令行输入&lt;code&gt;python main.py --files file1.txt file2.txt file3.txt&lt;/code&gt;得到的是&lt;code&gt;args.files = [&quot;file1.txt&quot;, &quot;file2.txt&quot;, &quot;file3.txt&quot;]&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;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--items&quot;, nargs=&quot;+&quot;, help=&quot;List of items&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令行输入&lt;code&gt;python main.py --items apple banana orange&lt;/code&gt;得到的是&lt;code&gt;args.items = [&quot;apple&quot;, &quot;banana&quot;, &quot;orange&quot;]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;argparse.REMAINDER&lt;/code&gt;：将命令行中所有剩余的参数捕获到一个列表中。通常用于将所有剩余的参数传递给另一个命令或子程序，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;command&quot;, nargs=argparse.REMAINDER, help=&quot;Command to run with all remaining args&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在命令行输入&lt;code&gt;python script.py --command ls -l /home/user&lt;/code&gt;得到的是&lt;code&gt;args.command = [&quot;ls&quot;, &quot;-l&quot;, &quot;/home/user&quot;]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3.2.9 choices属性&lt;/h4&gt;
&lt;p&gt;用于限制参数的值只能在指定的选项范围内，如果传入的值不在范围内，&lt;code&gt;argparse&lt;/code&gt; 会自动报错。示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--mode&quot;, choices=[&quot;train&quot;, &quot;test&quot;, &quot;validate&quot;], help=&quot;Specify the operation mode&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.2.10 metavar属性&lt;/h4&gt;
&lt;p&gt;用于在帮助信息中显示参数的占位符名称，而不影响变量名称。可以让帮助信息更简洁明了，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;parser.add_argument(&quot;--input&quot;, metavar=&quot;INPUT_FILE&quot;, help=&quot;Path to the input file&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 解析参数&lt;/h3&gt;
&lt;p&gt;添加完参数之后，我们使用如下语句进行解析：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;args = parser.parse_args()
#解析命令行参数
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4 使用参数&lt;/h3&gt;
&lt;p&gt;解析完成后，我们使用语句&lt;code&gt;arg.参数名&lt;/code&gt;就可以直接使用了。&lt;/p&gt;
&lt;p&gt;注意：&lt;code&gt;name&lt;/code&gt;属性中的短横线（-）被解析成Python变量时会变成下划线，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import argparse

parser = argparse.ArgumentParser(description=&quot;Example of argparse usage&quot;)
parser.add_argument(&quot;--train-path&quot;, type=str, help=&quot;Path to training data&quot;)
parser.add_argument(&quot;--epochs&quot;, type=int, default=10, help=&quot;Number of training epochs&quot;)

args = parser.parse_args()

# 访问参数
print(args.train_path)
print(args.epochs)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 一个完整的示例&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import argparse

# 创建 ArgumentParser 对象，并设置程序的描述和 epilog
parser = argparse.ArgumentParser(
    prog=&quot;FileProcessor&quot;,
    description=&quot;This program processes an input file and outputs the results based on specified options.&quot;,
    epilog=&quot;For more details, please visit our documentation.&quot;
)

# 添加必需的输入文件参数（位置参数）
parser.add_argument(
    &quot;input_file&quot;,  # 位置参数，不需要 -- 前缀
    type=str,
    help=&quot;Path to the input file that will be processed.&quot;
)

# 添加可选参数：输出文件
parser.add_argument(
    &quot;-o&quot;, &quot;--output&quot;,  # 支持 -o 和 --output 两种形式
    type=str,
    default=&quot;output.txt&quot;,  # 默认值
    metavar=&quot;OUTPUT_FILE&quot;,  # 在帮助信息中显示的参数名称
    help=&quot;Specify the output file path. Default is &apos;output.txt&apos;.&quot;
)

# 添加可选参数：处理模式
parser.add_argument(
    &quot;-m&quot;, &quot;--mode&quot;,
    choices=[&quot;fast&quot;, &quot;slow&quot;, &quot;custom&quot;],  # 限制选项
    default=&quot;fast&quot;,
    help=&quot;Choose the processing mode. Available options are &apos;fast&apos;, &apos;slow&apos;, or &apos;custom&apos;. Default is &apos;fast&apos;.&quot;
)

# 添加可选参数：详细模式（布尔开关）
parser.add_argument(
    &quot;-v&quot;, &quot;--verbose&quot;,
    action=&quot;store_true&quot;,  # 如果提供此参数，则将其值设为 True，否则为 False
    help=&quot;Enable verbose mode to get detailed output.&quot;
)

# 添加可选参数：重复次数（整数类型，默认值为 1）
parser.add_argument(
    &quot;-r&quot;, &quot;--repeat&quot;,
    type=int,
    default=1,
    help=&quot;Specify the number of times to repeat the processing. Default is 1.&quot;
)

# 添加可选参数：设置日志级别（可选的多值参数）
parser.add_argument(
    &quot;--log-level&quot;,
    nargs=&quot;?&quot;,
    const=&quot;INFO&quot;,  # 如果没有提供值，则默认为 INFO
    help=&quot;Set the logging level. If no level is specified, &apos;INFO&apos; is used by default.&quot;
)

# 添加可选参数：标签（允许多个值）
parser.add_argument(
    &quot;--tags&quot;,
    nargs=&quot;*&quot;,
    help=&quot;Specify tags associated with this processing task. You can pass multiple tags.&quot;
)

# 解析命令行参数
args = parser.parse_args()

# 打印解析后的参数（供调试）
print(&quot;Parsed arguments:&quot;)
print(&quot;Input file:&quot;, args.input_file)
print(&quot;Output file:&quot;, args.output)
print(&quot;Mode:&quot;, args.mode)
print(&quot;Verbose:&quot;, args.verbose)
print(&quot;Repeat count:&quot;, args.repeat)
print(&quot;Log level:&quot;, args.log_level)
print(&quot;Tags:&quot;, args.tags)

# 基于解析的参数执行逻辑
if args.verbose:
    print(&quot;[Verbose Mode Enabled] Detailed processing information will be shown.&quot;)
    
for i in range(args.repeat):
    print(f&quot;\nProcessing &apos;{args.input_file}&apos; in &apos;{args.mode}&apos; mode (Run {i+1}/{args.repeat})...&quot;)
    # 模拟处理逻辑
    if args.mode == &quot;fast&quot;:
        print(&quot;Processing quickly...&quot;)
    elif args.mode == &quot;slow&quot;:
        print(&quot;Processing slowly...&quot;)
    elif args.mode == &quot;custom&quot;:
        print(&quot;Processing with custom settings...&quot;)

    # 处理标签
    if args.tags:
        print(&quot;Applying tags:&quot;, &quot;, &quot;.join(args.tags))

    print(f&quot;Output will be saved to &apos;{args.output}&apos;.&quot;)

# 日志信息
if args.log_level:
    print(f&quot;Log level set to: {args.log_level}&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/Q52099999/article/details/137028697&quot;&gt;【Python进阶】argparse库基础用法全总结：高效脚本参数解析 | 参数类型使用代码-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3/library/argparse.html#module-argparse&quot;&gt;argparse --- 用于命令行选项、参数和子命令的解析器 — Python 3.13.0 文档&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Windows 查看和开放特定端口</title><link>https://liangbm3.site/blog/windows-cha-kan-he-kai-fang-te-ding-duan-kou</link><guid isPermaLink="true">https://liangbm3.site/blog/windows-cha-kan-he-kai-fang-te-ding-duan-kou</guid><description>这里记录的是之前比赛时遇到的一个bug，大概场景是在同一个局域网下，在本机（Windows）中开启了一个类似于主机服务，但是其他电脑根据IP和端口无法访问对应的网页，最后发现原因是本机防火墙没有开放对应的端口。</description><pubDate>Tue, 29 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 设置步骤&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;打开控制面板：按下 Win + R 键，然后输入 “control” 并按下 Enter 键，打开控制面板。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入 Windows 防火墙设置：在控制面板中，选择 “系统和安全”，然后选择 “Windows Defender 防火墙”。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241029124825216.C8W0DnD-_KEhnh.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://liangbm3.site/_astro/image-20241029125025487.BSqu12Kn_1QRbGH.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://liangbm3.site/_astro/image-20241029125111173.QpevsZMy_Z11cXpM.webp&quot; alt=&quot;图片&quot;&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;选择端口类型：选择 “特定本地端口”，然后输入要打开的端口号（例如，1883）。选择 “允许连接”，然后点击 “下一步”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择连接的配置：可以选择 “公共”、“专用” 或 “域”，具体根据你的网络设置选择。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;命名规则：给规则起一个名称和可选的描述，然后点击 “下一步”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;完成向导：检查配置，然后点击 “完成”。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. 参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_43577613/article/details/132537593&quot;&gt;【windows】查看和开放特定的端口_windows开放指定端口-CSDN博客&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>使用 tmux 分离终端和会话</title><link>https://liangbm3.site/blog/shi-yong-tmux-fen-chi-zhong-duan-he-hui-hua</link><guid isPermaLink="true">https://liangbm3.site/blog/shi-yong-tmux-fen-chi-zhong-duan-he-hui-hua</guid><description>我们可以使用tmux（terminal multiplexer）（终端复用器）来使终端和会话分离（detach），这样关闭终端时运行的程序不会停止。</description><pubDate>Mon, 28 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;在使用ssh远程连接服务器时，当我们关闭终端或者断开连接，在终端运行的代码也随之停止。特别是使用服务器训练模型时，我们不可能一直保持终端连接，这时我们可以使用tmux（terminal multiplexer）（终端复用器）来使终端和会话分离（detach），这样关闭终端时运行的程序不会停止。这里只对会话分离功能进行记录，因为窗格操作目前基本不会使用。&lt;/p&gt;
&lt;h2&gt;2. 基础命令&lt;/h2&gt;
&lt;h3&gt;2.1 安装&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Ubuntu 或 Debian
sudo apt-get install tmux
 
# CentOS 或 Fedora
sudo yum install tmux
 
# Mac
brew install tmux
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 启动和退出&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动tmux窗口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tmux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以在启动时指定session的名字，这样在解绑后可以快速重新进入该session：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tmux new -s your-session-name
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;退出tmux窗口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;exit
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.3 分离会话&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tmux detach
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行该命令后，就会退出当前 tmux 窗口，但是会话和里面的进程仍然在后台运行。&lt;/p&gt;
&lt;h3&gt;2.4 查找和接入会话&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查看当前所有的 tmux 会话&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tmux ls
&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-bash&quot;&gt;# 使用会话编号
tmux attach -t 0

# 使用会话名称
tmux attach -t &amp;#x3C;session-name&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.5 杀死会话&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 使用会话编号
tmux kill-session -t 0

# 使用会话名称
tmux kill-session -t &amp;#x3C;session-name&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用该命令后，会话和里面的进程会被杀死&lt;/p&gt;
&lt;h3&gt;2.6 切换会话&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 使用会话编号
tmux switch -t 0

# 使用会话名称
tmux switch -t &amp;#x3C;session-name&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令可以从某个会话直接切换到另一个会话&lt;/p&gt;
&lt;h3&gt;2.7 重命名会话&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tmux rename-session -t 0 &amp;#x3C;new-name&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2019/10/tmux.html&quot;&gt;Tmux 使用教程 - 阮一峰的网络日志&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/CSSDCC/article/details/121231906&quot;&gt;Tmux教程 （一文就够）-CSDN博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/hesongzefairy/article/details/112590054&quot;&gt;如何关闭终端后保持ssh远程连接运行程序不终止（tmux基本操作）_退出终端会停止远程服务器工作吗-CSDN博客&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>WSL 的使用</title><link>https://liangbm3.site/blog/wsl-de-shi-yong</link><guid isPermaLink="true">https://liangbm3.site/blog/wsl-de-shi-yong</guid><description>在使用WSL的过程中，由于默认安装到C盘的，导致C盘爆满，所以想着迁移到D盘，顺便总结了一下WSL的命令，固有此文。</description><pubDate>Sun, 27 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. WSL的常用命令&lt;/h2&gt;
&lt;h3&gt;1.1 基础 WSL 命令&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动默认 WSL 发行版&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动特定的 WSL 发行版&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl -d &amp;#x3C;发行版名称&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl -d Ubuntu-20.04
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看所有已安装的 WSL 发行版&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --list --verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者简写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl -l -v
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设置默认 WSL 发行版&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --set-default &amp;#x3C;发行版名称&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --set-default Ubuntu-20.04
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2. WSL 版本管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;检查 WSL 发行版版本&lt;/strong&gt;（1 或 2）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --list --verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设置指定 WSL 发行版为 WSL 2&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --set-version &amp;#x3C;发行版名称&gt; 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --set-version Ubuntu-20.04 2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设置新的默认 WSL 版本&lt;/strong&gt;（将新安装的发行版默认设置为 WSL 2）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --set-default-version 2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3. 启动和停止 WSL&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;停止所有运行中的 WSL 发行版&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --shutdown
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;终止特定 WSL 发行版&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --terminate &amp;#x3C;发行版名称&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --terminate Ubuntu-20.04
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.4. WSL 文件系统操作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;从 WSL 访问 Windows 文件系统&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 WSL 中，可以通过 &lt;code&gt;/mnt/c&lt;/code&gt; 等路径访问 Windows 的 C 盘文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;cd /mnt/&amp;#x3C;windows路径&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;cd /mnt/c/Users/lbm/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;从 Windows 访问 WSL 文件系统&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;在文件资源管理器的地址栏中输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;\\wsl$\&amp;#x3C;发行版名称&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;\\wsl$\Ubuntu-20.04
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.5. 发行版的安装和卸载&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;安装新的 WSL 发行版&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;可以通过 &lt;strong&gt;Microsoft Store&lt;/strong&gt; 下载和安装新的 WSL 发行版（如 Ubuntu、Debian 等）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;卸载 WSL 发行版&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --unregister &amp;#x3C;发行版名称&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --unregister Ubuntu-20.04
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.6. 发行版的导入与导出&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;导出 WSL 发行版&lt;/strong&gt;（备份）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --export &amp;#x3C;发行版名称&gt; &amp;#x3C;文件路径&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --export Ubuntu-20.04 D:\backup\ubuntu_backup.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;导入 WSL 发行版&lt;/strong&gt;（恢复）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --import &amp;#x3C;自定义名称&gt; &amp;#x3C;安装路径&gt; &amp;#x3C;备份文件路径&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --import MyUbuntu D:\WSL\Ubuntu D:\backup\ubuntu_backup.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.7. 设置和调试&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看 WSL 帮助&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --help
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;检查 WSL 状态和配置&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --status
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 更改WSL的安装路径&lt;/h2&gt;
&lt;p&gt;Windows似乎不支持直接更改WSL的默认安装路径，但支持安装发行版的时候自定义安装路径，或者将已经安装好的发行版迁移到其他盘符。&lt;/p&gt;
&lt;h3&gt;2.1 自定义安装路径&lt;/h3&gt;
&lt;p&gt;若想自定义安装路径，不能直接从电脑的&lt;strong&gt;Microsoft Store&lt;/strong&gt;中直接安装，这样的话会直接被安装到默认路径中。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/wsl/install-manual&quot;&gt;https://learn.microsoft.com/zh-cn/windows/wsl/install-manual&lt;/a&gt;中找到发行版的下载链接，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241027141939402.CBGTWPdV_2n3pfb.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择需要安装的发行版，这里以Kali Linux为例，下载完成后解压，得到如图文件夹&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241027142516252.CB00aH8g_ZAG3qx.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择合适的版本，我选择的是&lt;code&gt;x64&lt;/code&gt;，将后缀改为&lt;code&gt;.zip&lt;/code&gt;，解压，得到如图文件夹&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241027142648245.BRvNgVSg_CCgoC.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;install.tar.gz&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-powershell&quot;&gt;wsl --import &amp;#x3C;自定义名称&gt; &amp;#x3C;目标路径&gt; &amp;#x3C;解压目录路径&gt;\install.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置WSL版本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --set-version &amp;#x3C;自定义名称&gt; 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-powershell&quot;&gt;wsl --set-default &amp;#x3C;发行版名称&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建快捷方式：右键点击桌面，选择 &lt;strong&gt;新建 &gt; 快捷方式&lt;/strong&gt;，输入如下命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl -d &amp;#x3C;发行版名称&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;点击 &lt;strong&gt;下一步&lt;/strong&gt;，为快捷方式命名，然后点击 &lt;strong&gt;完成&lt;/strong&gt;。创建完成后，右键点击快捷方式，可以选择 &lt;strong&gt;固定到“开始”屏幕&lt;/strong&gt; 或 &lt;strong&gt;固定到任务栏&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.2 将WSL安装的发行版迁移至其他盘符&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查看安装的发行版&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl -l --all  -v
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;导出分发版为&lt;code&gt;.tar&lt;/code&gt;文件到d盘&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --export Ubuntu-20.04 d:\wsl-ubuntu20.04.tar
&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-powershell&quot;&gt;wsl --unregister Ubuntu-20.04
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重新导入并安装WSL在&lt;code&gt;d:\wsl-ubuntu20.04&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --import Ubuntu-20.04 d:\wsl-ubuntu20.04 d:\wsl-ubuntu20.04.tar --version 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-powershell&quot;&gt;ubuntu2004 config --default-user Username
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除&lt;code&gt;.tar&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;del d:\wsl-ubuntu20.04.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/farer_yyh/article/details/133934904&quot;&gt;Win10/11下安装WSL并修改WSL默认安装目录到其他盘_wsl选择安装目录-CSDN博客&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Markdown 简明教程</title><link>https://liangbm3.site/blog/markdown-jian-ming-jiao-cheng</link><guid isPermaLink="true">https://liangbm3.site/blog/markdown-jian-ming-jiao-cheng</guid><description>Markdown已经渗透到我的方方面面，闲来无事，总结一下。😀😀</description><pubDate>Sat, 26 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Markdown是一种轻量级标记语言，排版语法简洁，让人们更多地关注内容本身而非排版。它使用易读易写的纯文本格式编写文档，可与HTML混编，可导出 HTML、PDF 以及本身的 .md 格式的文件。因简洁、高效、易读、易写，Markdown被大量使用，如Github、Wikipedia、简书等。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 速查表&lt;/h2&gt;
&lt;p&gt;| 元素                        | Markdown 语法                                                |
| --------------------------- | ------------------------------------------------------------ |
| 标题（Heading）             | # H1                                                         |
| 粗体（Bold）                | **bold text**                                            |
| 斜体（Italic）              | *italicized text*                                          |
| 引用块（Blockquote）        | &gt; blockquote                                                 |
| 有序列表（Ordered List）    | 1. First item                                                |
| 无序列表（Unordered List）  | - First item                                                 |
| 代码（Code）                | `code`                                                     |
| 分隔线（Horizontal Rule）   | ---                                                          |
| 链接（Link）                | [title](https://www.example.com)                         |
| 图片（Image）               | ![alt text](image.jpg)                                   |
| 表格（Table）               | |标题|标题||---|---||内容|内容|             |
| 代码块（Fenced Code Block） | ```{&quot;firstName&quot;: &quot;John&quot;&quot;lastName&quot;: &quot;Smith&quot;,&quot;age&quot;: 25}``` |
| 删除线（Strikethrough）     | ~~The world is flat.~~                                     |
| 任务列表（Task List）       | - [x] Write the press release- [ ] Update the website- [ ] Contact the media |&lt;/p&gt;
&lt;h2&gt;2. 具体语法&lt;/h2&gt;
&lt;h3&gt;2.1. 标题语法&lt;/h3&gt;
&lt;p&gt;在单词或短语前面添加井号 (#) 。# 的数量代表了标题的级别。例如，添加三个 # 表示创建一个三级标题 (&amp;#x3C;h3&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;### My Header
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;考虑到兼容问题，#和标题直接要有空格&lt;/p&gt;
&lt;h3&gt;2.2. 段落语法&lt;/h3&gt;
&lt;p&gt;使用空白行将一行或多行文本进行分隔。不要用空格（spaces）或制表符（ tabs）缩进段落。&lt;/p&gt;
&lt;h2&gt;2.3. 换行语法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可以在一行的末尾添加两个或多个空格，然后按回车键，即可创建一个换行(&amp;#x3C;br&gt;)。  几乎每个 Markdown 应用程序都支持两个或多个空格进行换行，称为结尾空格(trailing whitespace) 的方式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;几乎每个 Markdown 应用程序都支持另一种换行方式：HTML 的 &amp;#x3C;br&gt; 标签。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;hello world&amp;#x3C;br&gt;
hello world
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4. 强调语法&lt;/h3&gt;
&lt;h4&gt;2.4.1 粗体&lt;/h4&gt;
&lt;p&gt;在单词或短语的前后各添加两个星号（asterisks）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;**加粗**
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.4.2 斜体&lt;/h4&gt;
&lt;p&gt;在单词或短语前后添加一个星号（asterisk）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;*斜体*
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.4.3 粗体和斜体&lt;/h4&gt;
&lt;p&gt;要同时使用用粗体和斜体时，在单词或短语的前后各添加三个星号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;***加粗和斜体***
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.5. 引用语法&lt;/h3&gt;
&lt;p&gt;要创建块引用，请在段落前添加一个&lt;code&gt;&gt;&lt;/code&gt;符号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&gt; 引用块
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果引用想包含多个段落，要在段落之间的空白行添加一个&lt;code&gt;&gt;&lt;/code&gt;符号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&gt; 第一段
&gt;
&gt; 第二段
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;块引用可以嵌套。在要嵌套的段落前添加一个&lt;code&gt;&gt;&gt;&lt;/code&gt;符号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&gt; 引用块
&gt;&gt; 引用块里面的引用块
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.6. 列表语法&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;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;h3&gt;2.7. 代码语法&lt;/h3&gt;
&lt;p&gt;行内代码，将将目标包裹在反引号(`)中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;你好，`code`行内代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果单词或短语中包含一个或多个反引号，则可以通过将单词或短语包裹在双反引号(``)中.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;你好，``code``行内代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码块使用三个反引号(```)，要添加语法突出显示，可以代码块之前的反引号旁边指定一种语言。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;```python
print(&quot;hello world&quot;)
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.8. 分割线语法&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;h3&gt;2.9. 链接语法&lt;/h3&gt;
&lt;p&gt;超链接Markdown语法代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;[超链接显示名](超链接地址 &quot;超链接title&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用尖括号可以很方便地把URL或者email地址变成可点击的链接。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;https://markdown.com.cn&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.10. 图片语法&lt;/h3&gt;
&lt;p&gt;图片语法比链接语法多一个感叹号 (!)，然后在方括号增加替代文本，图片链接放在圆括号里，括号里的链接后可以增加一个可选的图片标题文本。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;![图片alt](图片链接 &quot;图片title&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.11. 表格&lt;/h3&gt;
&lt;p&gt;使用三个或多个连字符（---）创建每列的标题，并使用管道（|）分隔每列。您可以选择在表的任一端添加管道。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;| Syntax      | Description |
| ----------- | ----------- |
| Header      | Title       |
| Paragraph   | Text        |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;您可以通过在标题行中的连字符的左侧，右侧或两侧添加冒号（:），将列中的文本对齐到左侧，右侧或中心。中间对齐示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| Syntax      | Description |
| :---------: | :---------: |
| Header      | Title       |
| Paragraph   | Text        |
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.12. 删除线&lt;/h3&gt;
&lt;p&gt;在单词前后使用两个波浪号&lt;code&gt;~~&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~~删除~~
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.13. 使用 Emoji 表情&lt;/h3&gt;
&lt;p&gt;有两种方法可以将表情符号添加到Markdown文件中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将表情符号复制并粘贴到Markdown格式的文本中&lt;/li&gt;
&lt;li&gt;键入emoji shortcodes。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在大多数情况下，可以简单地从&lt;a href=&quot;https://emojipedia.org/&quot;&gt;Emojipedia&lt;/a&gt;等来源复制表情符号并将其粘贴到文档中。 还可以通过键入表情符号短代码来插入表情符号。这些以冒号开头和结尾，并包含表情符号的名称。
&lt;a href=&quot;https://gist.github.com/liangbm3/cefcf740d30f484c8f99b9581b5d213a&quot;&gt;表情简码表&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;2.14 任务列表&lt;/h3&gt;
&lt;p&gt;要创建任务列表，需要在任务列表项之前添加破折号&lt;code&gt;-&lt;/code&gt;和方括号&lt;code&gt;[ ]&lt;/code&gt;，并在&lt;code&gt;[ ]&lt;/code&gt;前面加上空格。要选择一个复选框，请在方括号&lt;code&gt;[]&lt;/code&gt;里面添加 &lt;code&gt;x&lt;/code&gt;即可 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- [x] Write the press release
- [ ] Update the website
- [ ] Contact the media
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://markdown.com.cn/&quot;&gt;Markdown 教程&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Git 的使用总结</title><link>https://liangbm3.site/blog/git-de-shi-yong-zong-jie</link><guid isPermaLink="true">https://liangbm3.site/blog/git-de-shi-yong-zong-jie</guid><description>自己在平时的学习中，Git是使用比较多的工具，但是有一些命令常常忘记，或是对一些命令理解不透彻，这里做一个总结和记录。</description><pubDate>Sat, 19 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;自己在平时的学习中，Git是使用比较多的工具，但是有一些命令常常忘记，或是对一些命令理解不透彻，这里做一个总结和记录。&lt;/p&gt;
&lt;h2&gt;2. Git简介&lt;/h2&gt;
&lt;p&gt;这里长话短说，Git是目前世界上最先进的分布式版本控制系统，Git 采用分布式架构，在 Git 中每个开发人员的代码工作副本也是一个可以包含所有变更完整历史记录的存储库。&lt;/p&gt;
&lt;p&gt;每当你在Git中提交更新或保存项目状态时，它基本上就会对当时的全部文件创建一个快照并保存这个快照的索引。 为了效率，如果文件没有修改，Git 不再重新存储该文件，而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个 &lt;strong&gt;快照流&lt;/strong&gt;。如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241014234011540.D8joTOtj_Z1NTlwz.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;你执行的 Git 操作，几乎只往 Git 数据库中 &lt;strong&gt;添加&lt;/strong&gt; 数据。 你很难使用 Git 从数据库中删除数据，也就是说 Git 几乎不会执行任何可能导致文件不可恢复的操作。&lt;/p&gt;
&lt;h2&gt;3. 基本知识&lt;/h2&gt;
&lt;h3&gt;3.1 版本管理&lt;/h3&gt;
&lt;p&gt;Git 有三种状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;已修改(modified)：&lt;/strong&gt; 表示修改了文件，但还没保存到数据库中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已暂存(staged)：&lt;/strong&gt; 表示对一个已修改文件的当前版本做了标记，使之包含在下次提交的快照中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已提交(committed)：&lt;/strong&gt; 表示数据已经安全地保存在本地数据库中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Git 项目拥有三个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作区&lt;/strong&gt;：工作区是对项目的某个版本独立提取出来的内容。可以理解为你正在编辑的那些文件就在工作区之中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;暂存区&lt;/strong&gt;：是一个文件，保存了下次将要提交的文件列表信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Git 仓库&lt;/strong&gt;： Git 用来保存项目的元数据和对象数据库的地方。 这是 Git 中最重要的部分，从其它计算机克隆仓库时，复制的就是这里的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基本的 Git 工作流程如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241014235221867.CJ-dD8Ho_Zz52Fp.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;即：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在工作区修改文件&lt;/li&gt;
&lt;li&gt;将你想要下次提交的更改选择性地暂存，这样只会将更改的部分添加到暂存区。&lt;/li&gt;
&lt;li&gt;提交更新，找到暂存区的文件，将快照永久性存储到 Git 目录。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.2 分支&lt;/h3&gt;
&lt;p&gt;分支模型是Git的一个重要特性。其实在每次提交时，Git都会包含一个指向父对象的指针（首次提交对象没有父对象），Git的分支，本质上是指向提交对象的可变指针，Git的默认分支名字是master，多次提交后，我们其实已经有一个指向最后那个提交对象的master分支。&lt;/p&gt;
&lt;p&gt;HEAD严格来说不是指向提交，而是指向master，master才是指向提交的，所以，HEAD指向的就是当前分支。如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241015123909809.DNrJMq4I_xmXWi.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;当我们创建分支时，相当于创建了一个类似于master的指针，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241015124125630.Cd19rbgK_1cQ4J7.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果HEAD指向dev，那么所作出的修改都会提交到dev分支，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241015124323636.BaMh_aiP_2x4YBB.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;合并分支相当于修改master的指针指向dev。&lt;/p&gt;
&lt;h2&gt;4. Git的安装&lt;/h2&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://zhuanlan.zhihu.com/p/242540359&quot;&gt;Windows系统Git安装教程（详解Git安装过程） - 知乎 (zhihu.com)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;每次安装完Git时，记得配置用户名和邮件地址。每一个 Git 提交都会使用这些信息，并且会写入到每一次提交中，不可更改。配置命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global user.name &quot;John Doe&quot;
git config --global user.email johndoe@example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 基础的工作流程&lt;/h2&gt;
&lt;h3&gt;5.1 使用Git进行版本管理&lt;/h3&gt;
&lt;h4&gt;5.1.1 获取 Git 仓库&lt;/h4&gt;
&lt;p&gt;有两种方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将尚未进行版本控制的本地目录转换为 Git 仓库。&lt;/p&gt;
&lt;p&gt;执行命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将创建一个名为 &lt;code&gt;.git&lt;/code&gt; 的子目录，这个子目录含有初始化的 Git 仓库中所有的必须文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从其它服务器 &lt;strong&gt;克隆&lt;/strong&gt; 一个已存在的 Git 仓库。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/libgit2/libgit2 mylibgit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以在克隆的时候指定本地仓库名&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/libgit2/libgit2 mylibgit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以指定分支&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone -b master https://github.com/libgit2/libgit2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5.1.2 查看文件的状态&lt;/h4&gt;
&lt;p&gt;文件会存在两种状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已跟踪：已经被纳入版本控制的文件&lt;/li&gt;
&lt;li&gt;未跟踪：没有被纳入版本管理的文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以使用命令来检查文件处于什么状态&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要查看更加详细的信息可以使用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git diff
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此命令比较的是工作目录中当前文件和暂存区域快照之间的差异。 也就是修改之后还没有暂存起来的变化内容。&lt;/p&gt;
&lt;h4&gt;5.1.3 添加文件到暂存区&lt;/h4&gt;
&lt;p&gt;跟踪新文件和或把已跟踪的文件放到暂存区&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git add 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数列表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git add -A&lt;/code&gt;：提交所有变化（ -A  ==  --all ）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git add -u&lt;/code&gt;：提交被修改（modified）和被删除（deleted）的文件，不包括新文件（new）。（ -u  ==  --update）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git add .&lt;/code&gt;：提交新文件（new）和被修改（modified）文件，不包括被删除（deleted）文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git add &amp;#x3C;path&gt; &lt;/code&gt;：等同于 &lt;code&gt;git add .&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5.1.4 将暂存区内容提交到仓库&lt;/h4&gt;
&lt;p&gt;提交更新&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git commit -m &quot;提交说明&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以使用以下命令跳过使用暂存区，即会自动把所有已经跟踪过的文件暂存起来一并提交&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git commit -a -m &quot;提交说明&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.1.5 查看提交历史&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显示每次提交的差异：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只显示最近两次的提交：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log -2
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.1.6 撤销操作&lt;/h4&gt;
&lt;p&gt;如果发现漏了文件忘了提交，可以使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git commit --amend
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git commit -m &apos;initial commit&apos;
git add forgotten_file
git commit --amend
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;取消暂存的文件，可以使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset &amp;#x3C;file&gt;...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;撤销对文件的修改&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout --&amp;#x3C;file&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;`git checkout — &amp;#x3C;file&gt;` 是一个危险的命令。 你对那个文件在本地的任何修改都会消失——Git 会用最近提交的版本覆盖掉它。
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h4&gt;5.1.7 版本回退&lt;/h4&gt;
&lt;p&gt;命令为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回退到上一版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset --hard HEAD^
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--hard&lt;/code&gt;：回退到上一个版本已提交状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--soft&lt;/code&gt;：回退到是一个版本未提交的状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--mixed&lt;/code&gt;：回退到上一个版本已添加但为提交的状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;回退到某个版本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset --hard 1094a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面的字符串是版本号，写前几个即可，Git会自动查找。&lt;/p&gt;
&lt;p&gt;如果想恢复到新版本，却找不到新版本的commit id时，可以使用&lt;code&gt;git reflog&lt;/code&gt;命令，通过查看命令历史来找到新版本提交的commit id。&lt;/p&gt;
&lt;h3&gt;5.2 Git 分支的使用&lt;/h3&gt;
&lt;h4&gt;5.2.1 查看分支&lt;/h4&gt;
&lt;p&gt;查看所有分支&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git branch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git branch&lt;/code&gt;命令会列出所有分支，当前分支前面会标一个&lt;code&gt;*&lt;/code&gt;号。&lt;/p&gt;
&lt;p&gt;查看远程分支&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git branch -r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看远程和本地分支&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git branch -a
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.2.2 创建分支&lt;/h4&gt;
&lt;p&gt;创建dev分支，然后切换到dev分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout -b dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git checkout&lt;/code&gt;命令加上&lt;code&gt;-b&lt;/code&gt;参数表示创建并切换，相当于以下两条命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git branch dev
git checkout dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新版本可以使用如下命令来创建和切换分支:&lt;/p&gt;
&lt;p&gt;创建并切换到新的dev分支，可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git switch -c dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接切换到已有的master分支，可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git switch master
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.2.3 合并分支&lt;/h4&gt;
&lt;p&gt;一种比较简单的情况是，我们在master分支中，创建并切换到dev分支上进行了修改后，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241015154405605.BNBpLF9v_ygldv.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后我们将dev分支合并到master分支上，这时我们需要切换到master分支，执行命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git merge dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果两者没有冲突，很快地就会合并成功了。但如果我们有一个新的feature1分支，master和feature1分支都有了新的提交，并且两者修改了同一地方，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241015154526015.B9ix59Zq_Z2ppY39.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;这样会产生冲突，必须手动解决冲突后才能合并。Git检测到这种冲突时，它会在冲突的文件中插入冲突标记。&lt;/p&gt;
&lt;p&gt;冲突标记通常以&lt;code&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&lt;/code&gt;，&lt;code&gt;=======&lt;/code&gt;和&lt;code&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&lt;/code&gt;开头，并在两个冲突部分的之间有额外的信息。一个示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C; HEAD
这是版本A的修改内容
=======
这是版本B的修改内容
&gt;&gt;&gt;&gt;&gt;&gt;&gt; branchB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们对其修改后，再进行提交即可，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241015155351642.BPfbsh8U_sCy2Y.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;h4&gt;5.2.4 删除分支&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git branch -d feature1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.4 远程仓库的使用&lt;/h3&gt;
&lt;p&gt;查看已经连接的远程仓库&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git remote -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加远程仓库&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git remote add &amp;#x3C;shortname&gt; &amp;#x3C;url&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git remote add origin https://github.com/paulboone/ticgit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后我们可以在命令行中使用pd代替后面整个URL。&lt;/p&gt;
&lt;p&gt;此时我们可以运行&lt;code&gt;git fetch origin&lt;/code&gt;命令来获得远程仓库的副本，但是不会自动修改或合并当前的工作，需要将当前分支合并到远程分支。我们可以设置当前分支跟踪远程分支，然后就可以使用&lt;code&gt;git pull&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-bash&quot;&gt;git checkout -b &amp;#x3C;branch-name&gt; origin/&amp;#x3C;branch-name&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-bash&quot;&gt;git branch --set-upstream-to=origin/&amp;#x3C;branch-name&gt; &amp;#x3C;branch-name&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果当前处于该分支，可以省略本地分支名&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git branch --set-upstream-to=origin/&amp;#x3C;branch-name&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果仓库是使用&lt;code&gt;git clone&lt;/code&gt;得到的，该命令会自动设置本地master分支跟踪克隆远程仓库的master分支。&lt;/p&gt;
&lt;h2&gt;6 .gitignore文件的编写&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.gitignore&lt;/code&gt;文件用来告诉 Git 哪些文件或目录不应该被跟踪或提交到版本库中。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;忽略文件或目录的基本规则&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;忽略某个文件，直接写文件名称&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myfile.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;忽略某个目录，在目录名称后面加&lt;code&gt;/&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logs/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;忽略特定类型的文件，使用通配符&lt;code&gt;*&lt;/code&gt;来忽略某种类型的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*.log
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;忽略某个文件夹下的所有文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;folder_name/*
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例外规则&lt;/p&gt;
&lt;p&gt;如果你想忽略大多数文件，但保留其中某些文件，可以使用 &lt;code&gt;!&lt;/code&gt; 进行例外。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*.log      # 忽略所有 .log 文件
!important.log  # 但不忽略 important.log
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通配符的使用&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;*&lt;/code&gt;：匹配任意数量的字符。例如，忽略所有 &lt;code&gt;.tmp&lt;/code&gt; 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*.tmp
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;?&lt;/code&gt;：匹配任意单个字符。例如，忽略 &lt;code&gt;file1.txt&lt;/code&gt;, &lt;code&gt;file2.txt&lt;/code&gt;, 但不忽略 &lt;code&gt;file11.txt&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;file?.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;**&lt;/code&gt;：匹配任意层级的目录。例如，忽略所有 &lt;code&gt;tmp&lt;/code&gt; 目录下的文件，不论层级&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tmp/**/*
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;忽略指定目录中特定文件类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logs/*.log
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：即使文件被添加到 &lt;code&gt;.gitignore&lt;/code&gt; 中，但它们之前已经被 Git 跟踪，这时需要先停止追踪&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git rm --cached &amp;#x3C;file&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;7. 配置git的ssh key&lt;/h2&gt;
&lt;p&gt;确保用户名和邮箱已经配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global  --list 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如未配置，则执行以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global  user.name &quot;这里换上你的用户名&quot;
git config --global user.email &quot;这里换上你的邮箱&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成公钥&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen -t rsa -C &quot;这里换上你的邮箱&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行命令后需要进行3次或4次确认：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确认秘钥的保存路径（如果不需要改路径则直接回车）；&lt;/li&gt;
&lt;li&gt;如果上一步置顶的保存路径下已经有秘钥文件，则需要确认是否覆盖（如果之前的秘钥不再需要则直接回车覆盖，如需要则手动拷贝到其他目录后再覆盖）；&lt;/li&gt;
&lt;li&gt;创建密码（如果不需要密码则直接回车）；&lt;/li&gt;
&lt;li&gt;确认密码；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;找到生成的公钥文件，windows下一般在目录&lt;code&gt;C:\Users\用户名\.ssh\id_rsa.pub&lt;/code&gt;，Linux一般在&lt;code&gt;~/.ssh/id_rsa.pub&lt;/code&gt;，打开复制内容&lt;/p&gt;
&lt;p&gt;然后打开GitHub，进入设置页面，点击“SSH and GPG keys”，粘贴刚才复制的内容，生成私钥，大功告成。&lt;/p&gt;
&lt;h2&gt;8. 参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://geek-docs.com/git/git-questions/518_git_git_conflict_markers.html&quot;&gt;Git冲突标记|极客教程 (geek-docs.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.runoob.com/git/git-tutorial.html&quot;&gt;Git 教程 | 菜鸟教程 (runoob.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/418947255&quot;&gt;Git 系列四：Git远程仓库操作 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git-scm.com/book/zh/v2&quot;&gt;Git&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://liaoxuefeng.com/books/git/introduction/index.html&quot;&gt;简介 - Git教程 - 廖雪峰的官方网站&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;9. 结语&lt;/h2&gt;
&lt;p&gt;若有其它新内容，后续再补充。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>通过 Cloudflare 免费加速个人网站</title><link>https://liangbm3.site/blog/tong-guo-cloudflare-mian-fei-jia-su-ge-ren-wang-zhan</link><guid isPermaLink="true">https://liangbm3.site/blog/tong-guo-cloudflare-mian-fei-jia-su-ge-ren-wang-zhan</guid><description>通过Cloudflare的CDN服务，通过其免费的free计划，来加速博客网站的访问速度。</description><pubDate>Sat, 12 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;在搭建博客的过程中，发现网站访问的速度有些慢，便参考了网上的许多教程和博客，并成功使用了Cloudflare 来加速我的博客网站，因此特地写了一篇blog来总结一下。&lt;/p&gt;
&lt;h2&gt;2. 前置知识&lt;/h2&gt;
&lt;h3&gt;2.1 Cloudflare&lt;/h3&gt;
&lt;p&gt;Cloudflare（Cloudflare, Inc.）是一家总部位于旧金山的美国跨国科技企业，以向客户提供基于反向代理的内容分发网络（Content Delivery Network, CDN）及分布式域名解析服务（Distributed Domain Name Server）为主要业务。Cloudflare 已经成为互联网上运行的最大网络之一。&lt;/p&gt;
&lt;p&gt;Cloudflare的优势有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DDoS保护：过滤恶意流量，保护网站免受分布式拒绝服务的攻击。&lt;/li&gt;
&lt;li&gt;内容分发网络（CDN）：帮助加速网站内容在全球范围内的传输。&lt;/li&gt;
&lt;li&gt;SSL/TLS加密安全保护：帮助保护网站和访问者的安全。&lt;/li&gt;
&lt;li&gt;提升网页访问速度：通过压缩代码、压缩资源和缓存内容来优化网站性能。&lt;/li&gt;
&lt;li&gt;详细流量分析和报告：包括请求、带宽使用和安全事件等信息。&lt;/li&gt;
&lt;li&gt;简单直观的界面：易于使用的管理网站设置和配置。&lt;/li&gt;
&lt;li&gt;免费和收费计划：可以是获得CDN、DDoS保护和安全等优势的经济实惠方式。&lt;/li&gt;
&lt;li&gt;更重要的是有中文的控制面板🫣&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 CDN是什么&lt;/h3&gt;
&lt;p&gt;我们主要是使用Cloudflare的CDN服务进行加速，那么CDN到底是什么呢？&lt;/p&gt;
&lt;p&gt;CDN (Content Delivery Network或Content Distribution Network)是一个互连服务器网络，可加快数据密集型应用程序的网页加载速度。CDN 可以表示内容分发网络或内容分配网络。当用户访问某个网站时，来自该网站服务器的数据必须通过互联网传输到用户的计算机。如果用户距离该服务器较远，则加载大文件（例如视频或网站图像）将需要很长时间。相反，如果网站内容存储在距离用户较近的 CDN 服务器上，就可以更快到达他们的计算机。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012170948597.DL5ZPOz0_VoYJ7.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3. 操作流程&lt;/h2&gt;
&lt;h3&gt;3.1 配置服务&lt;/h3&gt;
&lt;p&gt;首先你需要一个私有域名，没有可先去注册：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://wanwang.aliyun.com/domain/&quot;&gt;阿里云&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://buy.cloud.tencent.com/domain&quot;&gt;腾讯云&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.huaweicloud.com/product/domain.html&quot;&gt;华为云&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后登录&lt;a href=&quot;https://dash.cloudflare.com/sign-up&quot;&gt;Cloudflare&lt;/a&gt;，没有账号先注册账号。进入的面板如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012171108680.D_1WXxqM_1oTCdX.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们输入我们的私有域名，点击继续，然后选择免费的计划&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：不要填带有www.域名&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012171120482.BWbzGcUC_ZCHI4v.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;点击继续，我们可以看到如下界面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012171133590.DuVXWDUS_Z1lLssQ.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;h3&gt;3.2 按照下方的快速入门指南进行配置&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012172732085.BwOxvhvJ_ZK7vuQ.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;一律保存即可。&lt;/p&gt;
&lt;h3&gt;3.3 更改DNS服务器&lt;/h3&gt;
&lt;p&gt;先在Cloudflare中找到给我们分配的DNS服务器，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012171651888.C11VC4QU_ZmMs7P.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来需要到我们的服务器商那里更换我们域名的DNS服务器，以阿里云为例，进入&lt;a href=&quot;https://dc.console.aliyun.com/#/domain-list/all&quot;&gt;域名控制台 (aliyun.com)&lt;/a&gt;，找到我们需要加速的域名&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012171932162.BxpXEYTH_lmKsE.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;点击管理&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012172054672.CATA5YPv_ES8YA.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;在DNS修改中，输入刚才复制的域名&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012172444890.DdiiK_XJ_ZG2R0X.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;等待一段时间后，会收到Cloudflare发来的邮件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012172903024.CyDsOl0r_OdADV.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里每个人的等待时间不一样，请耐心等待&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;成功后，界面变成如图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012173124328.0EeD_hbC_qGRr8.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;到此我们已经可以使用Cloudflare进行CDN加速了，我们还可以进行其他配置。&lt;/p&gt;
&lt;h3&gt;3.4 其他配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将SSL/TLS设置成完全（严格），增加网页安全性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012173653312.01NezV3w_Z1djKPu.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启用 Always Online 后，即使你源服务器一时瘫痪，也会显示 Cloudflare 创建好的网站副本。这样就可以始终向访问者提供信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://liangbm3.site/_astro/image-20241012173850174.CaHZTqDa_7KsyG.webp&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4. 参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://for-tiger.com/cloudflare-setup-tutorial-for-cdn-acceleration&quot;&gt;Cloudflare设置流程 免费CDN加速你的网站【2024年最新】&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>我的第一篇博客</title><link>https://liangbm3.site/blog/wo-de-di-yi-pian-bo-ke</link><guid isPermaLink="true">https://liangbm3.site/blog/wo-de-di-yi-pian-bo-ke</guid><description>建站的第一篇博客</description><pubDate>Thu, 10 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;你好，欢迎来到我的博客！&lt;/p&gt;
&lt;p&gt;拥有一个属于自己的博客一直是我小小的愿望，尽管之前由于时间问题我多次推迟了动笔的计划，现在终于迈出了这一步了。希望通过这个博客，不仅能积累和分享技术知识，还能记录自己的学习和成长轨迹，遇见更多志同道合的你们。&lt;/p&gt;
&lt;h2&gt;关于我&lt;/h2&gt;
&lt;p&gt;目前就读于中山大学通信工程专业，对编程和技术有浓厚的兴趣，喜欢探索新领域。无论是前端还是后端，硬件还是软件，EE还是CS，都有一定的了解，目前正在探索自己感兴趣的方向。&lt;/p&gt;
&lt;h2&gt;为什么选择写博客&lt;/h2&gt;
&lt;p&gt;这个想法由来已久，加上受到一位朋友的启发，意识到写博客不仅是整理思路、总结经验的好方法，还能帮助那些有类似经历的人。因此，我决定搭建属于自己的博客，既是实现自己的目标，也是记录成长的过程。在这里，我会分享技术文章、学习经验以及一些生活中的思考。&lt;/p&gt;
&lt;h2&gt;希望分享的内容&lt;/h2&gt;
&lt;p&gt;未来，我会分享学习中的心得体会、问题解决的技巧，甚至是失败的教训。同时，也会涉及一些生活和个人成长的内容，让博客更加丰富。无论你是对技术感兴趣，还是寻找学习启发的伙伴，我都期待与你在这里交流。&lt;/p&gt;
&lt;h2&gt;一起前行&lt;/h2&gt;
&lt;p&gt;写博客对我来说都是全新的挑战。如果你有任何建议或想法，欢迎随时留言，我们可以一起讨论、共同进步。&lt;/p&gt;
&lt;p&gt;感谢你的到访，希望你能在这里找到感兴趣的内容！&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>