<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Galileo</title><description>编程、工具、系统环境与工程实践记录</description><link>https://fuwari.vercel.app/</link><language>en</language><item><title>C++ 指针入门：地址、解引用与内存访问</title><link>https://fuwari.vercel.app/posts/cpp-pointer-intro/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/cpp-pointer-intro/</guid><description>本文从变量地址开始，整理 C++ 指针的基本模型：指针变量保存地址，解引用访问对象，指针类型影响读取方式和指针运算，同时说明空指针、悬空指针等常见错误。</description><pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是 &lt;a href=&quot;/posts/cpp-series/&quot;&gt;C++ 系列&lt;/a&gt; 的第二篇。&lt;/p&gt;
&lt;p&gt;上一篇：&lt;a href=&quot;/posts/cpp-series-preface/&quot;&gt;C++ 系列前言：从语法到工程能力&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;指针是 C++ 里最容易被低估的基础概念之一。&lt;/p&gt;
&lt;p&gt;表面上看，指针只是“保存地址的变量”。但一旦把它放进实际代码，就会继续牵出对象、类型、解引用、数组、函数参数、生命周期和未定义行为。&lt;/p&gt;
&lt;p&gt;本文先不讨论动态内存和智能指针，只关注最小模型：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;变量有值，也有地址&lt;/li&gt;
&lt;li&gt;指针变量保存某个对象的地址&lt;/li&gt;
&lt;li&gt;解引用可以通过地址访问对象&lt;/li&gt;
&lt;li&gt;指针类型决定如何解释那块内存&lt;/li&gt;
&lt;li&gt;只有指向有效对象的指针才能被解引用&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;1. 变量、对象和地址&lt;/h2&gt;
&lt;p&gt;在 C++ 里，一个普通变量至少包含几层信息：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;变量名：代码中访问它的名字&lt;/li&gt;
&lt;li&gt;类型：决定这块数据应该按什么格式解释&lt;/li&gt;
&lt;li&gt;值：当前保存的数据&lt;/li&gt;
&lt;li&gt;地址：对象在内存中的位置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;看一个最小例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main() {
    int a = 10;

    std::cout &amp;lt;&amp;lt; &quot;a  = &quot; &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;a = &quot; &amp;lt;&amp;lt; &amp;amp;a &amp;lt;&amp;lt; &apos;\n&apos;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一次可能的运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a  = 10
&amp;amp;a = 0x16b7fad4c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;a&lt;/code&gt; 表示变量的值，也就是 &lt;code&gt;10&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;a&lt;/code&gt; 表示对变量 &lt;code&gt;a&lt;/code&gt; 取地址&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;地址每次运行都可能不同，这和栈布局、地址空间随机化等因素有关。理解指针时不需要记住某个具体地址，只需要记住：&lt;strong&gt;变量除了有值，也有对应的存储位置&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;从语言层面看，&lt;code&gt;&amp;amp;a&lt;/code&gt; 的结果类型是 &lt;code&gt;int*&lt;/code&gt;，意思是“指向 &lt;code&gt;int&lt;/code&gt; 对象的指针”。&lt;/p&gt;
&lt;h2&gt;2. 指针是保存地址的变量&lt;/h2&gt;
&lt;p&gt;指针本身也是变量。&lt;/p&gt;
&lt;p&gt;普通 &lt;code&gt;int&lt;/code&gt; 变量保存整数，&lt;code&gt;double&lt;/code&gt; 变量保存浮点数，而指针变量保存的是地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

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

    std::cout &amp;lt;&amp;lt; &quot;a  = &quot; &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;a = &quot; &amp;lt;&amp;lt; &amp;amp;a &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt; &quot;p  = &quot; &amp;lt;&amp;lt; p &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt; &quot;*p = &quot; &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; &apos;\n&apos;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一次可能的运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a  = 10
&amp;amp;a = 0x16d7dad4c
p  = 0x16d7dad4c
*p = 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码里最关键的是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* p = &amp;amp;a;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它可以拆成两部分理解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;int*&lt;/code&gt;：&lt;code&gt;p&lt;/code&gt; 是一个指针，指向的对象类型是 &lt;code&gt;int&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;a&lt;/code&gt;：取出 &lt;code&gt;a&lt;/code&gt; 的地址，并把这个地址保存到 &lt;code&gt;p&lt;/code&gt; 里&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;p&lt;/code&gt; 的值等于 &lt;code&gt;&amp;amp;a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*p&lt;/code&gt; 表示沿着 &lt;code&gt;p&lt;/code&gt; 保存的地址访问对应对象&lt;/li&gt;
&lt;li&gt;因为 &lt;code&gt;p&lt;/code&gt; 指向 &lt;code&gt;a&lt;/code&gt;，所以 &lt;code&gt;*p&lt;/code&gt; 读到的就是 &lt;code&gt;a&lt;/code&gt; 的值&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;需要注意声明写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* p1, p2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行里只有 &lt;code&gt;p1&lt;/code&gt; 是 &lt;code&gt;int*&lt;/code&gt;，&lt;code&gt;p2&lt;/code&gt; 是普通 &lt;code&gt;int&lt;/code&gt;。如果要声明两个指针，应该写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* p1;
int* p2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int *p1, *p2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更推荐每行只声明一个变量，避免在指针声明上制造歧义。&lt;/p&gt;
&lt;h2&gt;3. 解引用：通过地址访问对象&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;*p&lt;/code&gt; 叫解引用。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;p&lt;/code&gt; 保存了某个有效对象的地址，那么 &lt;code&gt;*p&lt;/code&gt; 就表示那个对象本身。&lt;/p&gt;
&lt;p&gt;这意味着我们不仅可以通过指针读取变量，也可以通过指针修改变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

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

    *p = 20;

    std::cout &amp;lt;&amp;lt; &quot;a  = &quot; &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt; &quot;*p = &quot; &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; &apos;\n&apos;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a  = 20
*p = 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键点在这里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*p = 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这不是修改 &lt;code&gt;p&lt;/code&gt; 这个指针变量本身，而是修改 &lt;code&gt;p&lt;/code&gt; 指向的对象。&lt;/p&gt;
&lt;p&gt;可以把两种赋值分开看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p = &amp;amp;a;   // 修改指针变量 p 保存的地址
*p = 20;  // 修改 p 指向的对象
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是理解指针时最重要的分界线：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;p&lt;/code&gt; 是指针变量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*p&lt;/code&gt; 是指针指向的对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;p&lt;/code&gt; 是指针变量自己的地址&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4. 指针自己也有地址&lt;/h2&gt;
&lt;p&gt;因为指针也是变量，所以指针自己也需要存储空间，也有自己的地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

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

    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;a = &quot; &amp;lt;&amp;lt; &amp;amp;a &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt; &quot;p  = &quot; &amp;lt;&amp;lt; p &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;p = &quot; &amp;lt;&amp;lt; &amp;amp;p &amp;lt;&amp;lt; &apos;\n&apos;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一次可能的运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;a = 0x16b5dad4c
p  = 0x16b5dad4c
&amp;amp;p = 0x16b5dad40
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有两层地址：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;p&lt;/code&gt; 的值是 &lt;code&gt;a&lt;/code&gt; 的地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;p&lt;/code&gt; 是指针变量 &lt;code&gt;p&lt;/code&gt; 自己的地址&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以 &lt;code&gt;p&lt;/code&gt; 和 &lt;code&gt;&amp;amp;p&lt;/code&gt; 不是一回事。&lt;/p&gt;
&lt;p&gt;如果继续取地址，&lt;code&gt;&amp;amp;p&lt;/code&gt; 的类型是 &lt;code&gt;int**&lt;/code&gt;，也就是“指向 &lt;code&gt;int*&lt;/code&gt; 的指针”。这就是二级指针的来源，后面遇到函数修改指针本身、二维数组、命令行参数时还会继续用到。&lt;/p&gt;
&lt;h2&gt;5. 指针类型为什么重要&lt;/h2&gt;
&lt;p&gt;指针保存的是地址，但 C++ 里的指针不只是一个裸地址。&lt;/p&gt;
&lt;p&gt;指针类型至少影响两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解引用时，编译器应该按什么类型解释这块内存&lt;/li&gt;
&lt;li&gt;指针运算时，地址应该按多大步长移动&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;先看数组和指针加法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main() {
    int arr[3] = {10, 20, 30};
    int* p = arr;

    std::cout &amp;lt;&amp;lt; &quot;p           = &quot; &amp;lt;&amp;lt; p &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt; &quot;p + 1       = &quot; &amp;lt;&amp;lt; p + 1 &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt; &quot;*p          = &quot; &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt; &quot;*(p + 1)    = &quot; &amp;lt;&amp;lt; *(p + 1) &amp;lt;&amp;lt; &apos;\n&apos;;
    std::cout &amp;lt;&amp;lt; &quot;sizeof(int) = &quot; &amp;lt;&amp;lt; sizeof(int) &amp;lt;&amp;lt; &apos;\n&apos;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一次可能的运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p           = 0x16f9dad40
p + 1       = 0x16f9dad44
*p          = 10
*(p + 1)    = 20
sizeof(int) = 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;int&lt;/code&gt; 占 4 个字节，那么 &lt;code&gt;p + 1&lt;/code&gt; 不是让地址数值加 1，而是移动到下一个 &lt;code&gt;int&lt;/code&gt; 的位置，也就是向后移动 4 个字节。&lt;/p&gt;
&lt;p&gt;这就是指针类型的作用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* p;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示 &lt;code&gt;p&lt;/code&gt; 指向的是 &lt;code&gt;int&lt;/code&gt;，所以：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;*p&lt;/code&gt; 会按 &lt;code&gt;int&lt;/code&gt; 读取或写入&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p + 1&lt;/code&gt; 会移动 &lt;code&gt;sizeof(int)&lt;/code&gt; 个字节&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double* q;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么 &lt;code&gt;q + 1&lt;/code&gt; 会移动 &lt;code&gt;sizeof(double)&lt;/code&gt; 个字节。&lt;/p&gt;
&lt;p&gt;顺便注意一件事：在大多数表达式里，数组名 &lt;code&gt;arr&lt;/code&gt; 会转换成指向首元素的指针，也就是 &lt;code&gt;&amp;amp;arr[0]&lt;/code&gt;。所以下面两行在这个例子里效果相同：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* p1 = arr;
int* p2 = &amp;amp;arr[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但数组本身和指针不是同一种类型，后面讲数组、函数参数和 &lt;code&gt;sizeof&lt;/code&gt; 时需要单独区分。&lt;/p&gt;
&lt;h2&gt;6. &lt;code&gt;nullptr&lt;/code&gt;：明确表示“不指向对象”&lt;/h2&gt;
&lt;p&gt;如果一个指针暂时不指向任何有效对象，应该初始化为 &lt;code&gt;nullptr&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main() {
    int* p = nullptr;

    if (p == nullptr) {
        std::cout &amp;lt;&amp;lt; &quot;p does not point to anything now\n&quot;;
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;nullptr&lt;/code&gt; 表示空指针，它不指向任何有效对象。&lt;/p&gt;
&lt;p&gt;所以不能对空指针解引用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* p = nullptr;

// 错误示范：未定义行为
std::cout &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; &apos;\n&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对空指针解引用是未定义行为。程序可能崩溃，也可能表现出别的异常现象，不能依赖任何结果。&lt;/p&gt;
&lt;p&gt;在现代 C++ 里，优先使用 &lt;code&gt;nullptr&lt;/code&gt;，不要继续使用 &lt;code&gt;NULL&lt;/code&gt; 或 &lt;code&gt;0&lt;/code&gt; 表示空指针。&lt;code&gt;nullptr&lt;/code&gt; 有独立类型 &lt;code&gt;std::nullptr_t&lt;/code&gt;，在重载解析等场景下更清晰。&lt;/p&gt;
&lt;h2&gt;7. 指针作为函数参数&lt;/h2&gt;
&lt;p&gt;指针常见用途之一，是把对象地址传给函数。&lt;/p&gt;
&lt;p&gt;如果函数拿到的是值，它修改的是副本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

void changeByValue(int x) {
    x = 100;
}

int main() {
    int a = 10;

    changeByValue(a);

    std::cout &amp;lt;&amp;lt; &quot;a = &quot; &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &apos;\n&apos;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果函数拿到的是指针，它就可以通过地址修改调用者传入的对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

void changeByPointer(int* p) {
    if (p == nullptr) {
        return;
    }

    *p = 100;
}

int main() {
    int a = 10;

    changeByPointer(&amp;amp;a);

    std::cout &amp;lt;&amp;lt; &quot;a = &quot; &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &apos;\n&apos;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里传进去的是 &lt;code&gt;a&lt;/code&gt; 的地址。函数内部执行 &lt;code&gt;*p = 100;&lt;/code&gt; 时，修改的是外面的 &lt;code&gt;a&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;从接口设计角度看，指针参数通常表达两层含义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;函数可能通过这个参数访问或修改外部对象&lt;/li&gt;
&lt;li&gt;这个参数可能为空，因此函数需要考虑 &lt;code&gt;nullptr&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果函数要求参数一定存在，而且只是想直接操作对象，很多时候引用会更合适：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void changeByReference(int&amp;amp; x) {
    x = 100;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单规则是：&lt;strong&gt;可以为空时用指针，必须存在时优先考虑引用&lt;/strong&gt;。当然，具体选择还要结合接口语义和已有代码风格。&lt;/p&gt;
&lt;h2&gt;8. 常见错误&lt;/h2&gt;
&lt;h3&gt;8.1 未初始化指针&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int* p;

// 错误示范：p 没有被初始化
*p = 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;局部指针变量如果没有初始化，它的值是不确定的。此时直接解引用，程序会访问一个不可预期的地址，属于未定义行为。&lt;/p&gt;
&lt;p&gt;如果暂时不知道它应该指向谁，先写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* p = nullptr;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.2 空指针解引用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int* p = nullptr;

// 错误示范：nullptr 不指向有效对象
std::cout &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; &apos;\n&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;nullptr&lt;/code&gt; 的作用是表达“当前没有指向对象”，不是提供一个可以访问的默认对象。&lt;/p&gt;
&lt;p&gt;使用可能为空的指针前，先判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (p != nullptr) {
    std::cout &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; &apos;\n&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.3 悬空指针&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main() {
    int* p = nullptr;

    {
        int a = 10;
        p = &amp;amp;a;
    }

    // 错误示范：a 的生命周期已经结束
    std::cout &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; &apos;\n&apos;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;a&lt;/code&gt; 是代码块里的局部变量。代码块结束后，&lt;code&gt;a&lt;/code&gt; 的生命周期结束，&lt;code&gt;p&lt;/code&gt; 仍然保存原来的地址，但那个地址上已经没有一个有效的 &lt;code&gt;int a&lt;/code&gt; 对象。&lt;/p&gt;
&lt;p&gt;这种指针叫悬空指针。&lt;/p&gt;
&lt;p&gt;悬空指针最危险的地方在于：它的值看起来仍然像一个地址，但这个地址已经不能再按原来的对象使用。&lt;/p&gt;
&lt;h3&gt;8.4 越界指针访问&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int arr[3] = {10, 20, 30};
int* p = arr;

// 错误示范：越过数组有效元素
std::cout &amp;lt;&amp;lt; *(p + 3) &amp;lt;&amp;lt; &apos;\n&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于长度为 3 的数组，合法元素是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;arr[0], arr[1], arr[2]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;p + 3&lt;/code&gt; 可以作为“尾后位置”参与比较，但不能解引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int* begin = arr;
int* end = arr + 3;

for (int* it = begin; it != end; ++it) {
    std::cout &amp;lt;&amp;lt; *it &amp;lt;&amp;lt; &apos;\n&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 &lt;code&gt;end&lt;/code&gt; 表示范围结束位置，只作为边界，不作为可访问元素。&lt;/p&gt;
&lt;h2&gt;9. 小结&lt;/h2&gt;
&lt;p&gt;这一篇先记住这些规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;a&lt;/code&gt; 表示取出变量 &lt;code&gt;a&lt;/code&gt; 的地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int* p = &amp;amp;a;&lt;/code&gt; 表示 &lt;code&gt;p&lt;/code&gt; 保存一个 &lt;code&gt;int&lt;/code&gt; 对象的地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p&lt;/code&gt; 是指针变量本身，&lt;code&gt;*p&lt;/code&gt; 是指针指向的对象，&lt;code&gt;&amp;amp;p&lt;/code&gt; 是指针变量自己的地址&lt;/li&gt;
&lt;li&gt;指针类型决定解引用方式，也决定 &lt;code&gt;p + 1&lt;/code&gt; 移动多少字节&lt;/li&gt;
&lt;li&gt;未初始化指针、空指针、悬空指针都不能解引用&lt;/li&gt;
&lt;li&gt;指针参数常用来表达“函数可能访问或修改外部对象”，也可能表达“这个参数允许为空”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;理解指针的关键不是背概念，而是把每个符号对应到明确含义上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = 10;
int* p = &amp;amp;a;

p;   // 保存的地址
*p;  // 地址指向的 int 对象
&amp;amp;p;  // 指针变量 p 自己的地址
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面再看引用、数组、动态内存、对象生命周期、智能指针和迭代器失效，都会不断回到这几个基本问题：这个对象在哪里，谁保存了它的地址，它现在是否还有效。&lt;/p&gt;
</content:encoded></item><item><title>C++ 系列前言：从语法到工程能力</title><link>https://fuwari.vercel.app/posts/cpp-series-preface/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/cpp-series-preface/</guid><description>这个系列会从 C++ 基础概念开始，围绕对象、内存、生命周期、STL、编译调试工具和小项目实践，逐步建立更完整的 C++ 工程理解。</description><pubDate>Fri, 15 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++ 系列前言：从语法到工程能力&lt;/h1&gt;
&lt;h2&gt;1. 为什么重新整理 C++&lt;/h2&gt;
&lt;p&gt;C++ 不是一门只靠记语法就能写好的语言。&lt;/p&gt;
&lt;p&gt;写出 &lt;code&gt;for&lt;/code&gt;、&lt;code&gt;class&lt;/code&gt;、&lt;code&gt;vector&lt;/code&gt; 并不难，真正容易出问题的是这些代码背后的运行机制：对象什么时候创建和销毁，指针保存的地址是否仍然有效，拷贝发生在什么地方，容器扩容会不会让迭代器失效，编译器为什么能通过声明找到定义。&lt;/p&gt;
&lt;p&gt;这些问题平时看起来分散，但在实际工程里经常同时出现。一个程序崩溃，可能不是算法写错了，而是访问了已经失效的对象；一个链接错误，可能不是代码逻辑问题，而是声明、定义和编译单元之间的关系没有处理好。&lt;/p&gt;
&lt;p&gt;所以这个系列的重点不是把 C++ 语法列表重新讲一遍，而是把常见概念放回程序运行和工程开发的上下文中理解。&lt;/p&gt;
&lt;h2&gt;2. 这个系列想解决什么问题&lt;/h2&gt;
&lt;p&gt;这个系列的目标，是把“能写一点 C++ 代码”逐步推进到“能解释代码行为，并写出更可靠的 C++ 代码”。&lt;/p&gt;
&lt;p&gt;我会重点关注几个方向：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;类型与对象&lt;/strong&gt;：变量、对象、地址、生命周期、值类别等基础概念&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指针与引用&lt;/strong&gt;：内存访问、解引用、空指针、悬空指针、函数参数传递&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源管理&lt;/strong&gt;：构造析构、拷贝控制、移动语义、RAII、智能指针&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;STL 使用&lt;/strong&gt;：容器、迭代器、算法、扩容、失效规则和性能取舍&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程工具&lt;/strong&gt;：头文件、源文件、编译链接、CMake、gdb、库文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小项目实践&lt;/strong&gt;：把零散概念放到一个可运行、可调试、可维护的项目里验证&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这不是一套“高手教程”，更像是一份逐步补全 C++ 基本功的技术笔记。&lt;/p&gt;
&lt;h2&gt;3. 写作方式&lt;/h2&gt;
&lt;p&gt;每篇文章会尽量采用同一套结构：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先说明要解决的具体问题&lt;/li&gt;
&lt;li&gt;给出一段足够小的 C++ 示例&lt;/li&gt;
&lt;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;这样写的原因很简单：C++ 里的很多概念只看定义很容易产生错觉，必须通过代码、输出和错误信息反复验证。&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;指针加法不是普通整数加法&lt;/li&gt;
&lt;li&gt;空指针和悬空指针都不能解引用&lt;/li&gt;
&lt;li&gt;函数拿到指针后可能修改调用者的对象&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;把这些细节拆开验证，理解会更稳。&lt;/p&gt;
&lt;h2&gt;4. 暂定目录&lt;/h2&gt;
&lt;p&gt;目录会随着学习进度调整，但整体路线先按“基础概念 -&amp;gt; 对象与内存 -&amp;gt; STL -&amp;gt; 工程工具 -&amp;gt; 小项目实践”推进。&lt;/p&gt;
&lt;h3&gt;第一部分：C++ 基础概念&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;变量、地址和指针&lt;/li&gt;
&lt;li&gt;指针和引用的差异&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt; 到底修饰谁&lt;/li&gt;
&lt;li&gt;栈、堆和对象生命周期&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new/delete&lt;/code&gt; 和 &lt;code&gt;malloc/free&lt;/code&gt; 的区别&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;第二部分：对象与内存管理&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;构造函数和析构函数什么时候执行&lt;/li&gt;
&lt;li&gt;浅拷贝和深拷贝为什么危险&lt;/li&gt;
&lt;li&gt;拷贝构造函数和赋值运算符有什么区别&lt;/li&gt;
&lt;li&gt;为什么基类析构函数有时要写成 &lt;code&gt;virtual&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this&lt;/code&gt; 指针到底是什么&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;第三部分：STL 与常用容器&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;vector&lt;/code&gt; 为什么会自动扩容&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vector&lt;/code&gt; 和 &lt;code&gt;list&lt;/code&gt; 应该怎么选&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt; 和 &lt;code&gt;unordered_map&lt;/code&gt; 的区别&lt;/li&gt;
&lt;li&gt;迭代器失效是什么问题&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push_back&lt;/code&gt; 和 &lt;code&gt;emplace_back&lt;/code&gt; 的区别&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;第四部分：编译、调试与工程工具&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;C++ 项目为什么要分 &lt;code&gt;.h&lt;/code&gt; 和 &lt;code&gt;.cpp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一个 C++ 程序从 &lt;code&gt;.cpp&lt;/code&gt; 到可执行文件经历了什么&lt;/li&gt;
&lt;li&gt;使用 CMake 管理一个最小 C++ 项目&lt;/li&gt;
&lt;li&gt;用 gdb 定位一次段错误&lt;/li&gt;
&lt;li&gt;静态库和动态库的区别&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;第五部分：小项目实践&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;用 C++ 写一个命令行小工具&lt;/li&gt;
&lt;li&gt;从零写一个简单日志系统&lt;/li&gt;
&lt;li&gt;线程池解决了什么问题&lt;/li&gt;
&lt;li&gt;从零写一个最小线程池&lt;/li&gt;
&lt;li&gt;一个 C++ 小项目的完整复盘&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;5. 需要先建立的几个判断&lt;/h2&gt;
&lt;p&gt;后面的文章会反复围绕几个判断展开。&lt;/p&gt;
&lt;p&gt;第一，C++ 代码是否正确，不能只看“能不能编译过”。编译通过只能说明语法和类型检查没有发现问题，不代表运行时访问的内存一定有效。&lt;/p&gt;
&lt;p&gt;第二，资源的所有权必须说清楚。谁创建对象，谁负责释放资源，谁只是临时观察对象，这些关系如果不明确，后面就很容易出现泄漏、重复释放或悬空访问。&lt;/p&gt;
&lt;p&gt;第三，工具链也是 C++ 学习的一部分。编译错误、链接错误、运行时崩溃和调试信息，都是理解程序行为的重要入口。&lt;/p&gt;
&lt;p&gt;第四，C++ 的复杂性需要用约束来管理。能用标准库就不要手写底层结构；能用 RAII 就不要手动管理释放；能让生命周期清晰，就不要把裸指针到处传。&lt;/p&gt;
&lt;h2&gt;6. 从哪里开始&lt;/h2&gt;
&lt;p&gt;这个系列从指针开始。&lt;/p&gt;
&lt;p&gt;指针是 C++ 里最基础也最容易误解的概念之一。它连接了变量、地址、内存访问、数组、动态分配、函数参数、对象生命周期等很多内容。&lt;/p&gt;
&lt;p&gt;理解指针不等于以后处处使用裸指针，但如果完全绕开指针，就很难真正理解 C++ 为什么会有空指针、悬空指针、迭代器失效、智能指针和 RAII。&lt;/p&gt;
&lt;p&gt;后面的文章会从最小示例开始，一步一步把这些概念补起来。&lt;/p&gt;
</content:encoded></item><item><title>C++ 系列</title><link>https://fuwari.vercel.app/posts/cpp-series/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/cpp-series/</guid><description>这个系列从 C++ 基础语法出发，围绕对象、内存、生命周期、STL、编译调试工具和小项目实践，逐步整理 C++ 工程开发中的核心概念。</description><pubDate>Fri, 15 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这个系列整理 C++ 相关内容，重点不只是“语法怎么写”，而是理解代码背后的运行机制：对象如何存储，指针和引用如何访问内存，资源生命周期如何管理，程序如何经过编译链接变成可执行文件。&lt;/p&gt;
&lt;p&gt;文章会尽量采用固定结构：先给出问题，再用最小代码复现现象，观察运行结果，最后解释原因和常见错误。&lt;/p&gt;
&lt;h3&gt;系列目录&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cpp-series-preface/&quot;&gt;C++ 系列前言：从语法到工程能力&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cpp-pointer-intro/&quot;&gt;C++ 指针入门：地址、解引用与内存访问&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Linux系统</title><link>https://fuwari.vercel.app/posts/linux-system/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/linux-system/</guid><description>这个系列记录 Linux 系统使用、命令行操作和环境配置相关内容。</description><pubDate>Mon, 11 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这个系列记录 Linux 系统使用、命令行操作和环境配置相关内容。&lt;/p&gt;
&lt;h3&gt;系列目录&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/linux-basic-commands/&quot;&gt;Linux常用指令入门&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/windows-and-ubuntu-dual-boot-installation/&quot;&gt;Windows与ubuntu双系统的安装&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Linux常用指令入门</title><link>https://fuwari.vercel.app/posts/linux-basic-commands/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/linux-basic-commands/</guid><description>最近在和我一个学长做项目。因为十分贫穷，加上校内服务器十分堵塞，压根排不到我们，最后我们决定上网租服务器（虽然发现这个决策使得我们的钱包更加不饱满）。这个时候发现学长并不熟悉 Linux 的一些常用指令，这也启发我给自己...</description><pubDate>Mon, 11 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近在和我一个学长做项目。因为十分贫穷，加上校内服务器十分堵塞，压根排不到我们，最后我们决定上网租服务器（虽然发现这个决策使得我们的钱包更加不饱满）。这个时候发现学长并不熟悉 Linux 的一些常用指令，这也启发我给自己的 blog 加上一篇关于常用指令的推文。&lt;/p&gt;
&lt;p&gt;Linux 的很多操作都可以通过命令行完成。刚开始接触时，不需要一次性记住所有命令，先掌握目录切换、文件操作、文件内容查看、权限管理和进程查看这几类常用指令，就可以完成大部分日常工作。&lt;/p&gt;
&lt;p&gt;本文记录一些入门阶段最常用的 Linux 指令，方便之后查阅。&lt;/p&gt;
&lt;h2&gt;1. 查看当前所在位置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pwd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pwd&lt;/code&gt; 会显示当前终端所在的目录路径。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/home/user/project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你不知道自己现在在哪个文件夹时，可以先运行这个命令。&lt;/p&gt;
&lt;h2&gt;2. 查看目录内容&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;ls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls          # 查看当前目录下的文件
ls -l       # 以详细列表形式显示
ls -a       # 显示隐藏文件
ls -lh      # 以更易读的大小单位显示
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Linux 中，以 &lt;code&gt;.&lt;/code&gt; 开头的文件或目录是隐藏文件，例如 &lt;code&gt;.git&lt;/code&gt;、&lt;code&gt;.bashrc&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;3. 切换目录&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;cd 目录名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /home/user       # 切换到指定绝对路径
cd project          # 进入当前目录下的 project 文件夹
cd ..               # 返回上一级目录
cd ~                # 回到当前用户的家目录
cd -                # 回到上一次所在目录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绝对路径从 &lt;code&gt;/&lt;/code&gt; 开始，相对路径从当前目录开始。&lt;/p&gt;
&lt;h2&gt;4. 创建文件和目录&lt;/h2&gt;
&lt;p&gt;创建目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir notes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;递归创建多级目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p projects/linux/test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建空文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch hello.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;touch&lt;/code&gt; 如果遇到不存在的文件，会创建它；如果文件已经存在，会更新它的修改时间。&lt;/p&gt;
&lt;h2&gt;5. 复制、移动和重命名&lt;/h2&gt;
&lt;p&gt;复制文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp source.txt target.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复制目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp -r old_dir new_dir
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;移动文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mv file.txt /tmp/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重命名文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mv old_name.txt new_name.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mv&lt;/code&gt; 既可以移动文件，也可以重命名文件，具体取决于目标路径。&lt;/p&gt;
&lt;h2&gt;6. 删除文件和目录&lt;/h2&gt;
&lt;p&gt;删除文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm file.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -r folder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;强制删除：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -rf folder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rm -rf&lt;/code&gt; 很危险，它不会进入回收站，执行前一定要确认路径是否正确。&lt;/p&gt;
&lt;h2&gt;7. 查看文件内容&lt;/h2&gt;
&lt;p&gt;一次性输出文件内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat file.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分页查看长文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;less file.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看文件开头：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;head file.txt
head -n 20 file.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看文件末尾：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tail file.txt
tail -n 20 file.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实时查看日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tail -f app.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看程序日志时，&lt;code&gt;tail -f&lt;/code&gt; 非常常用。&lt;/p&gt;
&lt;h2&gt;8. 搜索文件内容&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;grep &quot;keyword&quot; file.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep &quot;error&quot; app.log
grep -n &quot;error&quot; app.log        # 显示行号
grep -i &quot;error&quot; app.log        # 忽略大小写
grep -r &quot;TODO&quot; .               # 在当前目录递归搜索
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果只记一个搜索命令，先记住 &lt;code&gt;grep -r &quot;关键词&quot; .&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;9. 查找文件&lt;/h2&gt;
&lt;p&gt;按文件名查找：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -name &quot;*.md&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查找当前目录下所有 Markdown 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -type f -name &quot;*.md&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查找目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -type d -name &quot;build&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;find&lt;/code&gt; 的第一个参数表示从哪里开始找，&lt;code&gt;.&lt;/code&gt; 表示当前目录。&lt;/p&gt;
&lt;h2&gt;10. 查看磁盘和文件大小&lt;/h2&gt;
&lt;p&gt;查看磁盘空间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df -h
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看当前目录下各文件和目录大小：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;du -sh *
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看某个目录总大小：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;du -sh folder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;-h&lt;/code&gt; 表示 human-readable，会用 KB、MB、GB 这种更容易理解的单位显示。&lt;/p&gt;
&lt;h2&gt;11. 查看和结束进程&lt;/h2&gt;
&lt;p&gt;查看当前用户的进程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看所有进程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ps aux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动态查看系统资源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结束某个进程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kill PID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果进程无法正常结束，可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kill -9 PID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PID&lt;/code&gt; 是进程编号，可以通过 &lt;code&gt;ps aux&lt;/code&gt; 或 &lt;code&gt;top&lt;/code&gt; 找到。&lt;/p&gt;
&lt;h2&gt;12. 权限相关命令&lt;/h2&gt;
&lt;p&gt;查看权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给脚本添加可执行权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x script.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改文件拥有者：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo chown user:user file.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sudo&lt;/code&gt; 表示用管理员权限执行命令。使用 &lt;code&gt;sudo&lt;/code&gt; 前要确认命令确实需要更高权限。&lt;/p&gt;
&lt;h2&gt;13. 压缩和解压&lt;/h2&gt;
&lt;p&gt;打包并压缩：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tar -czvf archive.tar.gz folder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tar -xzvf archive.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见参数含义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c: create，创建压缩包
x: extract，解压
z: 使用 gzip
v: 显示过程
f: 指定文件名
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;14. 网络相关命令&lt;/h2&gt;
&lt;p&gt;测试网络连通：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ping github.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -O https://example.com/file.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看当前机器 IP：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip addr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 macOS 上，类似命令是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ifconfig
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;15. 常用组合&lt;/h2&gt;
&lt;p&gt;进入目录并查看内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd project
ls -lh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查找日志中的错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep -n &quot;error&quot; app.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看某个目录大小：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;du -sh .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看最近的日志输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tail -f app.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查找当前目录下所有 Python 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -type f -name &quot;*.py&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Linux 命令入门可以先按使用场景记忆：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;位置和目录：&lt;code&gt;pwd&lt;/code&gt;、&lt;code&gt;ls&lt;/code&gt;、&lt;code&gt;cd&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;文件操作：&lt;code&gt;mkdir&lt;/code&gt;、&lt;code&gt;touch&lt;/code&gt;、&lt;code&gt;cp&lt;/code&gt;、&lt;code&gt;mv&lt;/code&gt;、&lt;code&gt;rm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;内容查看：&lt;code&gt;cat&lt;/code&gt;、&lt;code&gt;less&lt;/code&gt;、&lt;code&gt;head&lt;/code&gt;、&lt;code&gt;tail&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;搜索查找：&lt;code&gt;grep&lt;/code&gt;、&lt;code&gt;find&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;系统状态：&lt;code&gt;df&lt;/code&gt;、&lt;code&gt;du&lt;/code&gt;、&lt;code&gt;ps&lt;/code&gt;、&lt;code&gt;top&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;权限管理：&lt;code&gt;chmod&lt;/code&gt;、&lt;code&gt;chown&lt;/code&gt;、&lt;code&gt;sudo&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些命令不需要死记硬背，最好的方式是在真实项目和日常操作里反复使用。&lt;/p&gt;
</content:encoded></item><item><title>EKF 算法详解：(一) 简介与基础</title><link>https://fuwari.vercel.app/posts/ekf-introduction/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/ekf-introduction/</guid><description>在机器人、自动驾驶、无人机等领域中，“状态估计”是一个绕不开的核心问题。无论是云台的姿态解算，还是车辆的轨迹跟踪，我们都希望知道系统在当前时刻的准确状态。而在众多状态估计算法中，扩展卡尔曼滤波（Extended Kalm...</description><pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是 &lt;a href=&quot;/posts/ekf-series/&quot;&gt;EKF 算法详解系列&lt;/a&gt; 的第一篇。&lt;/p&gt;
&lt;p&gt;下一篇：&lt;a href=&quot;/posts/ekf-math/&quot;&gt;EKF 算法详解：(二) 姿态解算与数学推导&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在机器人、自动驾驶、无人机等领域中，“状态估计”是一个绕不开的核心问题。无论是云台的姿态解算，还是车辆的轨迹跟踪，我们都希望知道系统在当前时刻的准确状态。而在众多状态估计算法中，&lt;strong&gt;扩展卡尔曼滤波（Extended Kalman Filter, 简称 EKF）&lt;/strong&gt; 无疑是最经典、应用最广泛的算法之一。&lt;/p&gt;
&lt;p&gt;本系列文章将带你从零开始，理解 EKF 算法。本文为第一篇，主要介绍 EKF 的基本概念、它与标准卡尔曼滤波的区别，以及它的核心思想。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 什么是卡尔曼滤波 (Kalman Filter)?&lt;/h2&gt;
&lt;p&gt;在介绍 EKF 之前，我们需要先简单回顾一下&lt;strong&gt;标准卡尔曼滤波 (KF)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在现实世界中，任何传感器都有噪声（误差），任何物理模型也都无法做到 100% 完美预测。卡尔曼滤波的本质，就是&lt;strong&gt;融合“系统模型的预测”与“传感器的实际测量”&lt;/strong&gt;，通过数学推导，找出一个在统计意义上“最靠谱”的最优估计值。&lt;/p&gt;
&lt;p&gt;卡尔曼滤波主要分为两个不断循环的步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;预测 (Predict)&lt;/strong&gt;：根据上一时刻的状态和物理模型（如运动学方程：位移 = 速度 × 时间），推算出当前时刻的“先验状态”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新 (Update)&lt;/strong&gt;：读取传感器的测量值，计算测量值与预测值之间的差异（残差），然后通过“卡尔曼增益”来动态决定是更相信模型预测，还是更相信传感器测量，最终得到当前时刻的“后验状态”。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;标准 KF 的局限性&lt;/h3&gt;
&lt;p&gt;标准卡尔曼滤波有一个致命的假设前提：&lt;strong&gt;系统必须是线性的&lt;/strong&gt;。也就是说，状态的转移和观测过程都必须能用线性矩阵相乘来表示。
但在真实世界里，比如角度的旋转涉及正弦/余弦运算（如欧拉角、四元数），或者是复杂的阻力模型，这些都是&lt;strong&gt;非线性&lt;/strong&gt;的。这时候，标准 KF 就会失效。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 为什么需要扩展卡尔曼滤波 (EKF)?&lt;/h2&gt;
&lt;p&gt;既然现实系统大多是非线性的，我们就需要对标准卡尔曼滤波进行“扩展”——这就是 &lt;strong&gt;EKF&lt;/strong&gt; 诞生的原因。&lt;/p&gt;
&lt;p&gt;EKF 的核心思想非常简单粗暴但又极其有效：&lt;strong&gt;既然系统是非线性的，那就在当前工作点附近把它“局部线性化”&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;具体来说，EKF 利用了高等数学中的&lt;strong&gt;泰勒展开（Taylor Expansion）&lt;/strong&gt;。它将非线性的状态转移函数和观测函数在当前的最优估计值处进行一阶泰勒展开，忽略掉高阶项。这样一来，复杂的非线性函数就被近似成了一个线性模型（即求导数，得到&lt;strong&gt;雅可比矩阵 Jacobian&lt;/strong&gt;）。&lt;/p&gt;
&lt;h3&gt;EKF 的优势：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;适用性广&lt;/strong&gt;：完美解决了非线性系统的状态估计问题（例如机器人的视觉位姿追踪）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算效率高&lt;/strong&gt;：相比于粒子滤波（PF）或无迹卡尔曼滤波（UKF），EKF 只需要计算一阶雅可比矩阵，计算量较小，非常适合部署在算力受限的嵌入式设备（如各类单片机、STM32 等）上。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. EKF 算法的通用五步公式&lt;/h2&gt;
&lt;p&gt;一个标准的 EKF 同样遵循“预测”和“更新”两大阶段，具体的数学公式通常被归纳为以下经典的五步：&lt;/p&gt;
&lt;h3&gt;预测阶段 (Predict)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;状态预测&lt;/strong&gt;：根据非线性运动方程推算先验状态。
$$ \hat{x}&lt;em&gt;k^- = f(\hat{x}&lt;/em&gt;{k-1}, u_{k-1}) $$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协方差预测&lt;/strong&gt;：通过状态转移的雅可比矩阵 ( F )，更新预测误差协方差矩阵。
$$ P_k^- = F P_{k-1} F^T + Q $$&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;更新阶段 (Update)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;计算卡尔曼增益&lt;/strong&gt;：利用观测模型的雅可比矩阵 ( H ) 计算增益 ( K )。
$$ K_k = P_k^- H^T (H P_k^- H^T + R)^{-1} $$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态更新&lt;/strong&gt;：根据传感器实际测量值 ( z_k ) 与预测值的残差，修正状态，得到后验最优估计。
$$ \hat{x}_k = \hat{x}_k^- + K_k (z_k - h(\hat{x}_k^-)) $$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协方差更新&lt;/strong&gt;：降低不确定度，更新协方差矩阵供下一轮使用。
$$ P_k = (I - K_k H) P_k^- $$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;(注：这里的 ( Q ) 是过程噪声协方差，( R ) 是测量噪声协方差，它们通常在实际工程中作为“超参数”根据经验进行调参。)&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 结语与预告&lt;/h2&gt;
&lt;p&gt;EKF 虽然在数学表达上看起来有些劝退，但只要理解了它**“线性化”&lt;strong&gt;与&lt;/strong&gt;“加权融合”**的核心思想，就能在工程中游刃有余。&lt;/p&gt;
&lt;p&gt;在许多实际开源项目（如 RoboMaster 的视觉算法）中，EKF 常被用于追踪装甲板的运动或云台的姿态。在处理三维空间的旋转时，我们为了避免“万向节死锁”和保持约束，通常会引入&lt;strong&gt;四元数&lt;/strong&gt;与&lt;strong&gt;误差状态 (Error-State)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在下一篇文章中，我们将直接进入硬核部分：结合机器人的视觉姿态解算场景，详细推导&lt;strong&gt;基于四元数和误差状态的 EKF 数学模型&lt;/strong&gt;！&lt;/p&gt;
</content:encoded></item><item><title>EKF 算法详解：(二) 姿态解算与数学推导</title><link>https://fuwari.vercel.app/posts/ekf-math/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/ekf-math/</guid><description>在实际的机器人（如 RoboMaster）视觉和云台控制中，扩展卡尔曼滤波（EKF）经常被用于姿态解算（Attitude Estimation）。在本篇文章中，我们将结合实际项目中的四元数（Quaternion）状态，深...</description><pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是 &lt;a href=&quot;/posts/ekf-series/&quot;&gt;EKF 算法详解系列&lt;/a&gt; 的第二篇。&lt;/p&gt;
&lt;p&gt;上一篇：&lt;a href=&quot;/posts/ekf-introduction/&quot;&gt;EKF 算法详解：(一) 简介与基础&lt;/a&gt; | 下一篇：&lt;a href=&quot;/posts/ekf-code/&quot;&gt;EKF 算法详解：(三) 源码解析实现&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在实际的机器人（如 RoboMaster）视觉和云台控制中，扩展卡尔曼滤波（EKF）经常被用于&lt;strong&gt;姿态解算&lt;/strong&gt;（Attitude Estimation）。在本篇文章中，我们将结合实际项目中的四元数（Quaternion）状态，深入推导 EKF 的核心数学模型。&lt;/p&gt;
&lt;h2&gt;1. 为什么使用四元数与误差状态？&lt;/h2&gt;
&lt;p&gt;在三维空间中表示旋转，欧拉角容易遇到“万向节死锁”问题，旋转矩阵则需要维护 9 个元素且保持正交约束。&lt;strong&gt;四元数 (Quaternion)&lt;/strong&gt; 只需要 4 个元素，且计算效率高，是姿态表示的最佳选择。&lt;/p&gt;
&lt;p&gt;然而，传统的 EKF 难以直接应用于四元数，因为四元数必须满足&lt;strong&gt;单位长度约束&lt;/strong&gt;（( ||q|| = 1 )）。如果直接对四元数进行线性加减（如 ( q = q + \Delta q )），会破坏其模长。&lt;/p&gt;
&lt;p&gt;因此，在实际工程中，我们通常采用 &lt;strong&gt;误差状态卡尔曼滤波（Error-State Kalman Filter, ESKF）&lt;/strong&gt; 或称乘性 EKF（MEKF）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;名义状态 (Nominal State)&lt;/strong&gt;：用四元数 ( q ) 表示，通过乘法进行更新：( q_{new} = q \otimes \Delta q )&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;误差状态 (Error State)&lt;/strong&gt;：用一个三维向量（误差角 ( \delta \theta )）表示，其协方差矩阵 ( P ) 为 3x3 的矩阵。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 状态预测方程 (Prediction)&lt;/h2&gt;
&lt;p&gt;假设我们已知当前的角速度 ( \omega ) 和角加速度 ( \alpha )，在时间间隔 ( \Delta t ) 内，角度的变化量 ( \Delta \theta ) 可以近似表示为：
$$ \Delta \theta \approx (\omega + \alpha \cdot \Delta t) \Delta t $$&lt;/p&gt;
&lt;p&gt;根据角度变化量 ( \Delta \theta )，我们可以构造出一个微小旋转的四元数更新量 ( q_{update} )。
设 ( \theta = ||\Delta \theta|| ) 为旋转角，旋转轴 ( u = \frac{\Delta \theta}{\theta} )：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果旋转角极小（如 ( \theta &amp;lt; 10^{-6} )），根据泰勒展开，四元数更新量可以近似为：
$$ q_{update} \approx \begin{bmatrix} 1 \\ 0.5 \Delta \theta_x \\ 0.5 \Delta \theta_y \\ 0.5 \Delta \theta_z \end{bmatrix} $$&lt;/li&gt;
&lt;li&gt;如果角度较大，则严格按照四元数轴角公式：
$$ q_{update} = \begin{bmatrix} \cos(\frac{\theta}{2}) \\ u \sin(\frac{\theta}{2}) \end{bmatrix} $$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终预测的四元数状态为：
$$ \hat{q}&lt;em&gt;{k} = q&lt;/em&gt;{k-1} \otimes q_{update} $$&lt;/p&gt;
&lt;h2&gt;3. 雅可比矩阵的计算 (Jacobian)&lt;/h2&gt;
&lt;p&gt;在误差状态模型中，我们需要计算误差状态相对于角速度和旋转的雅可比矩阵 ( J )（部分实现中作为观测矩阵或状态转移矩阵的近似）。&lt;/p&gt;
&lt;p&gt;定义角速度 ( \omega ) 的反对称矩阵（Skew-symmetric matrix）为 ( [\omega]&lt;em&gt;\times )：
$$ [\omega]&lt;/em&gt;\times = \begin{bmatrix} 0 &amp;amp; -\omega_z &amp;amp; \omega_y \\ \omega_z &amp;amp; 0 &amp;amp; -\omega_x \\ -\omega_y &amp;amp; \omega_x &amp;amp; 0 \end{bmatrix} $$&lt;/p&gt;
&lt;p&gt;一阶近似下，误差状态转移的雅可比矩阵可以表示为：
$$ J = I_{3\times3} + \frac{1}{2} \Delta t [\omega]_\times $$&lt;/p&gt;
&lt;h2&gt;4. 状态更新方程 (Update)&lt;/h2&gt;
&lt;p&gt;当新的测量值（例如视觉 PnP 解算得到的四元数测量值 ( q_{meas} )）到来时，我们需要计算预测值 ( \hat{q}&lt;em&gt;k ) 与测量值的&lt;strong&gt;残差（Residual）&lt;/strong&gt;：
$$ q&lt;/em&gt;{res} = q_{meas} \otimes \hat{q}&lt;em&gt;k^{-1} $$
我们提取这个残差四元数的虚部向量部分 ( z&lt;/em&gt;{res} = [q_{res}.x, q_{res}.y, q_{res}.z]^T ) 作为测量残差向量。&lt;/p&gt;
&lt;p&gt;接着，计算卡尔曼增益 ( K )。假设我们提取的雅可比矩阵为 ( J )，系统误差协方差为 ( P )，测量噪声协方差为 ( R )：
$$ K = P J^T (J P J^T + R)^{-1} $$&lt;/p&gt;
&lt;p&gt;随后将增益乘上残差，得到误差角度修正量：
$$ K_z = K \cdot z_{res} $$&lt;/p&gt;
&lt;p&gt;最后，利用这个三维修正量构造误差四元数，乘到预测状态上，并更新协方差矩阵 ( P )：
$$ q_{new} = \hat{q}&lt;em&gt;k \otimes \begin{bmatrix} 1 \\ K_z.x \\ K_z.y \\ K_z.z \end{bmatrix} $$
$$ P&lt;/em&gt;{new} = (I_{3\times3} - K J) P $$&lt;/p&gt;
&lt;p&gt;以上就是一套完整用于姿态跟踪的轻量级 EKF/ESKF 数学推导模型。在下一篇文章中，我们将直接拆解 C++ 落地代码，看看这些公式是如何变成实际运行的程序的！&lt;/p&gt;
</content:encoded></item><item><title>EKF算法概述</title><link>https://fuwari.vercel.app/posts/ekf-series/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/ekf-series/</guid><description>欢迎来到 EKF (扩展卡尔曼滤波) 算法详解系列。本系列将从基础概念出发，逐步深入到复杂的数学推导，并最终通过代码实现一个完整的 EKF 算法。</description><pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;欢迎来到 EKF (扩展卡尔曼滤波) 算法详解系列。本系列将从基础概念出发，逐步深入到复杂的数学推导，并最终通过代码实现一个完整的 EKF 算法。&lt;/p&gt;
&lt;h3&gt;系列目录&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/ekf-introduction/&quot;&gt;EKF 算法详解：(一) 简介与基础&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/ekf-math/&quot;&gt;EKF 算法详解：(二) 数学推导&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/ekf-code/&quot;&gt;EKF 算法详解：(三) 代码实现&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>EKF 算法详解：(三) 源码解析实现</title><link>https://fuwari.vercel.app/posts/ekf-code/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/ekf-code/</guid><description>在上一篇文章中，我们详细推导了基于四元数和误差状态的 EKF 数学模型。理论推导无论多么完美，最终都需要落地成代码。本文我们将结合一个实际项目的源码（基于 C++ 和 Eigen 库），带你一行行看懂 EKF 的实现细节。</description><pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是 &lt;a href=&quot;/posts/ekf-series/&quot;&gt;EKF 算法详解系列&lt;/a&gt; 的第三篇。&lt;/p&gt;
&lt;p&gt;上一篇：&lt;a href=&quot;/posts/ekf-math/&quot;&gt;EKF 算法详解：(二) 姿态解算与数学推导&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在上一篇文章中，我们详细推导了基于四元数和误差状态的 EKF 数学模型。理论推导无论多么完美，最终都需要落地成代码。本文我们将结合一个实际项目的源码（基于 C++ 和 Eigen 库），带你一行行看懂 EKF 的实现细节。&lt;/p&gt;
&lt;h2&gt;1. 类的定义与初始化 (&lt;code&gt;ekf.hpp&lt;/code&gt;)&lt;/h2&gt;
&lt;p&gt;在 C++ 中，我们通常会定义一个 &lt;code&gt;ekf&lt;/code&gt; 类来维护卡尔曼滤波器的状态和协方差矩阵。这里我们使用开源且极其强大的矩阵运算库 &lt;strong&gt;Eigen&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifndef TOOLS__EKF_HPP
#define TOOLS__EKF_HPP
#include &amp;lt;Eigen/Geometry&amp;gt;
#include &amp;lt;eigen3/Eigen/src/Geometry/Quaternion.h&amp;gt;
#include &amp;lt;iostream&amp;gt;

namespace tools
{
class ekf
{
public: 
    ekf();
    ~ekf();
    // 预测步骤：根据角速度和角加速度预测下一个四元数状态
    Eigen::Quaterniond predict(const Eigen::Quaterniond &amp;amp;q, const Eigen::Vector3d &amp;amp;omega, const Eigen::Vector3d &amp;amp;alpha, float dt);
  
    // 计算雅可比矩阵
    Eigen::Matrix3d calculate_jacobian(const Eigen::Quaterniond &amp;amp;q, const Eigen::Vector3d &amp;amp;omega, float dt);

    // 核心更新步骤
    void ekf_update(Eigen::Quaterniond &amp;amp;q, const Eigen::Vector3d &amp;amp;omega, Eigen::Vector3d &amp;amp;alpha, float dt);
 
private:
    // 测量噪声协方差 R 和 状态误差协方差 P
    // 初始化为单位矩阵乘以一个相对较小的置信系数 0.1
    Eigen::Matrix3d R = Eigen::Matrix3d::Identity() * 0.1;
    Eigen::Matrix3d P = Eigen::Matrix3d::Identity() * 0.1;  
};
}
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，值得注意的是协方差矩阵 &lt;code&gt;P&lt;/code&gt; 和 &lt;code&gt;R&lt;/code&gt; 都是 $3 \times 3$ 的矩阵。这就是我们在&lt;a href=&quot;/posts/ekf-math/&quot;&gt;上一篇&lt;/a&gt;中提到的：状态虽为四元数（4维），但为了满足归一化约束，我们对**误差角（3维）**进行协方差维护。&lt;/p&gt;
&lt;h2&gt;2. 状态预测函数：&lt;code&gt;predict&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;预测函数的任务是通过当前的角速度 $\omega$ 和角加速度 $\alpha$ 积分，求出这 $\Delta t$ 时间内的微小旋转，然后更新四元数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Eigen::Quaterniond ekf::predict(const Eigen::Quaterniond &amp;amp;q, const Eigen::Vector3d &amp;amp;omega, const Eigen::Vector3d &amp;amp;alpha, float dt)
{
    // 计算在这 dt 时间内的角度变化量 (delta_theta)
    Eigen::Vector3d delta_theta = (omega + alpha * dt) * dt;
    float theta = delta_theta.norm(); // 求出旋转角的模长
    
    Eigen::Quaterniond q_update;
    
    // 为了防止浮点数精度问题以及计算效率，当角度极小时采用一阶泰勒近似
    if(theta &amp;lt; 1e-6)
    {
        q_update = Eigen::Quaterniond(1, 0.5 * delta_theta.x(), 0.5 * delta_theta.y(), 0.5 * delta_theta.z());
    }
    else 
    {
        // 角度较大时，严格按照轴角转换为四元数
        q_update = Eigen::Quaterniond(Eigen::AngleAxisd(theta, delta_theta.normalized()));
    }
    
    // 四元数乘法完成姿态的预测叠加
    return q * q_update;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 雅可比矩阵：&lt;code&gt;calculate_jacobian&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;这里构建了反对称矩阵（Skew-symmetric matrix），用于计算雅可比矩阵 $J$：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Eigen::Matrix3d ekf::calculate_jacobian(const Eigen::Quaterniond &amp;amp;q, const Eigen::Vector3d &amp;amp;omega, float dt)
{
    Eigen::Matrix3d j;
    Eigen::Matrix3d omega_skew;

    // 构造角速度的反对称矩阵
    omega_skew &amp;lt;&amp;lt; 0,         -omega.z(),  omega.y(),
                  omega.z(),  0,         -omega.x(),
                 -omega.y(),  omega.x(),  0;

    // 根据一阶近似，计算状态转移/观测的雅可比矩阵
    j = Eigen::Matrix3d::Identity() + 0.5 * dt * omega_skew;
    return j;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. EKF 核心更新逻辑：&lt;code&gt;ekf_update&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;这个函数是整个滤波器的灵魂所在，它结合了前两个函数，完成“预测-计算增益-修正-更新协方差”的闭环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void ekf::ekf_update(Eigen::Quaterniond &amp;amp;q, const Eigen::Vector3d &amp;amp;omega, Eigen::Vector3d &amp;amp;alpha, float dt)
{
    // 1. 预测步：得到先验状态 q_u
    Eigen::Quaterniond q_u = predict(q, omega, alpha, dt);
    
    // 2. 计算雅可比矩阵 J
    Eigen::Matrix3d J = calculate_jacobian(q, omega, dt);
    
    // 3. 计算残差：用实际测量的状态 q 乘以预测状态的逆 q_u.inverse()
    // 注意：在这里的实现中，传入的 q 被视为带有测量信息的最新状态
    Eigen::Quaterniond q_r = q * q_u.inverse();
    // 提取残差四元数的虚部（向量部分）作为测量残差向量
    Eigen::Vector3d z_r(q_r.x(), q_r.y(), q_r.z());

    // 4. 计算卡尔曼增益 K
    // 公式: K = P * H^T * (H * P * H^T + R)^-1
    // (代码将雅可比矩阵 J 作为 H 矩阵直接代入观测模型中计算)
    Eigen::Matrix3d K = P * J.transpose() * (J * P * J.transpose() + R).inverse();
    
    // 5. 计算误差修正量
    Eigen::Vector3d Kz = K * z_r;
    
    // 6. 后验状态更新：将算出的误差转换为四元数，乘上之前的预测值
    q = q_u * Eigen::Quaterniond(1, Kz[0], Kz[1], Kz[2]);
    
    // 7. 更新协方差矩阵 P
    P = (Eigen::Matrix3d::Identity() - K * J) * P;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这段代码是一个非常轻量且实用的工程化 EKF 实现，它舍弃了标准 EKF 中过于繁琐的矩阵运算（例如舍弃了复杂的高维状态转移阵，精简了观测矩阵提取的步骤），直接针对“四元数位姿跟踪”这一特定场景进行了极致优化。通过&lt;strong&gt;协方差降维&lt;/strong&gt;和&lt;strong&gt;一阶近似泰勒展开&lt;/strong&gt;，使得这套算法可以在算力有限的嵌入式设备（如各类机器人的下位机/视觉板）上高速运行。&lt;/p&gt;
&lt;p&gt;至此，我们的 EKF 算法详解系列就全部结束了！希望这三篇文章能帮你从概念到数学，再到代码落地，彻底打通 EKF 的任督二脉。&lt;/p&gt;
</content:encoded></item><item><title>关于github的一些使用方法</title><link>https://fuwari.vercel.app/posts/about-github/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/about-github/</guid><description>GitHub 是一个面向开源及私有软件项目的托管平台，因为只支持 Git 作为唯一的版本库格式进行托管，故名 GitHub。它不仅是代码仓库，还是一个强大的协作工具。本文将介绍一些 GitHub 的基本使用方法。</description><pubDate>Wed, 12 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;GitHub 是一个面向开源及私有软件项目的托管平台，因为只支持 Git 作为唯一的版本库格式进行托管，故名 GitHub。它不仅是代码仓库，还是一个强大的协作工具。本文将介绍一些 GitHub 的基本使用方法。&lt;/p&gt;
&lt;h2&gt;准备工作：安装 Git&lt;/h2&gt;
&lt;p&gt;在开始使用 GitHub 之前，你的电脑上需要安装 Git。Git 是一个免费、开源的分布式版本控制系统，是与 GitHub 交互的基础。&lt;/p&gt;
&lt;p&gt;你可以从 &lt;a href=&quot;https://git-scm.com/downloads&quot;&gt;Git 官网&lt;/a&gt; 下载适合你操作系统的安装包。安装过程通常很简单，保持默认设置即可。&lt;/p&gt;
&lt;p&gt;安装完成后，你可以在终端（Windows 上的 PowerShell 或 CMD，macOS/Linux 上的 Terminal）中输入以下命令来验证 Git 是否安装成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果能看到版本号，说明 Git 已经成功安装。&lt;/p&gt;
&lt;h2&gt;核心概念&lt;/h2&gt;
&lt;p&gt;在开始使用 GitHub 之前，了解一些核心概念非常重要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;仓库 (Repository)&lt;/strong&gt;: 你的项目存放的地方。可以想象成一个项目的文件夹，里面包含了项目的所有文件和修订历史。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;克隆 (Clone)&lt;/strong&gt;: 将远程仓库复制到你的本地计算机上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提交 (Commit)&lt;/strong&gt;: 将你的代码更改保存到本地仓库。每次提交都有一个唯一的 ID 和一条描述信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推送 (Push)&lt;/strong&gt;: 将本地仓库的提交上传到远程仓库（例如 GitHub）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拉取 (Pull)&lt;/strong&gt;: 从远程仓库获取最新的更改并合并到你的本地仓库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分支 (Branch)&lt;/strong&gt;: 为了在不影响主线（通常是 &lt;code&gt;main&lt;/code&gt; 或 &lt;code&gt;master&lt;/code&gt; 分支：这几年估计都改成了main，说是master有奇怪的隐喻😅）开发的情况下进行功能开发或修复 bug，你可以创建一个分支。开发完成后，再将分支合并到主线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拉取请求 (Pull Request / PR)&lt;/strong&gt;: 当你希望将你的分支合并到另一个分支时，你可以创建一个拉取请求。这是一个请求审查和讨论你的代码更改的地方。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合并 (Merge)&lt;/strong&gt;: 将一个分支的更改合并到另一个分支。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;基本工作流程&lt;/h2&gt;
&lt;h3&gt;1. 创建一个新的仓库&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;登录你的 GitHub 账户。&lt;/li&gt;
&lt;li&gt;点击右上角的 &quot;+&quot; 图标，然后选择 &quot;New repository&quot;。&lt;/li&gt;
&lt;li&gt;为你的仓库命名，添加一个可选的描述。&lt;/li&gt;
&lt;li&gt;选择 &quot;Public&quot;（公开）或 &quot;Private&quot;（私有）。&lt;/li&gt;
&lt;li&gt;你可以选择使用 &lt;code&gt;README&lt;/code&gt; 文件、&lt;code&gt;.gitignore&lt;/code&gt; 文件和许可证来初始化仓库。&lt;/li&gt;
&lt;li&gt;点击 &quot;Create repository&quot;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. 克隆仓库到本地&lt;/h3&gt;
&lt;p&gt;要对仓库进行更改，你需要先把它克隆到你的电脑上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/your-username/your-repository.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 &lt;code&gt;your-username&lt;/code&gt; 和 &lt;code&gt;your-repository&lt;/code&gt; 替换为你的 GitHub 用户名和仓库名。&lt;/p&gt;
&lt;h3&gt;3. 添加和提交更改&lt;/h3&gt;
&lt;p&gt;在本地对项目文件进行修改后，你需要将这些更改提交到本地仓库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 进入仓库目录
cd your-repository

# 查看文件状态
git status

# 添加所有更改的文件到暂存区
git add .

# 提交更改，并附上描述信息
git commit -m &quot;你的提交信息，例如：添加了新功能&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 推送更改到 GitHub&lt;/h3&gt;
&lt;p&gt;提交到本地仓库后，你需要将这些更改推送到 GitHub 上的远程仓库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push origin main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;main&lt;/code&gt; 可能是你的主分支名，也可能是 &lt;code&gt;master&lt;/code&gt;)&lt;/p&gt;
&lt;h2&gt;分支与协作&lt;/h2&gt;
&lt;p&gt;分支是 GitHub 协作的核心。&lt;/p&gt;
&lt;h3&gt;1. 创建并切换到新分支&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建一个名为 feature-x 的新分支并切换过去
git checkout -b feature-x
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 推送新分支到 GitHub&lt;/h3&gt;
&lt;p&gt;在你的新分支上进行提交后，将该分支推送到远程仓库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push origin feature-x
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 创建拉取请求 (Pull Request)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在 GitHub 上你的仓库页面，你会看到一个提示，可以为 &lt;code&gt;feature-x&lt;/code&gt; 分支创建一个拉取请求。&lt;/li&gt;
&lt;li&gt;点击 &quot;Compare &amp;amp; pull request&quot;。&lt;/li&gt;
&lt;li&gt;填写标题和描述，说明你的更改内容。&lt;/li&gt;
&lt;li&gt;点击 &quot;Create pull request&quot;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;之后，项目维护者可以审查你的代码，提出修改意见，最后将其合并到主分支中。&lt;/p&gt;
&lt;h2&gt;GitHub Pages&lt;/h2&gt;
&lt;p&gt;GitHub Pages 是 GitHub 提供的一个静态网站托管服务。你可以用它来托管你的个人博客、项目文档等。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确保你的仓库中有一个 &lt;code&gt;index.html&lt;/code&gt; 文件。&lt;/li&gt;
&lt;li&gt;进入仓库的 &quot;Settings&quot; 页面。&lt;/li&gt;
&lt;li&gt;在左侧菜单中选择 &quot;Pages&quot;。&lt;/li&gt;
&lt;li&gt;在 &quot;Source&quot; 部分，选择你想要部署的分支（通常是 &lt;code&gt;main&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;保存后，GitHub 会为你的网站生成一个 URL，格式通常是 &lt;code&gt;https://your-username.github.io/your-repository/&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;希望这篇指南能帮助你更好地开始使用 GitHub！&lt;/p&gt;
</content:encoded></item><item><title>Windows与ubuntu双系统的安装</title><link>https://fuwari.vercel.app/posts/windows-and-ubuntu-dual-boot-installation/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/windows-and-ubuntu-dual-boot-installation/</guid><description>我们IST实验室选择了Ubuntu作为视觉工作的系统，通常使用22.04版本。详细安装流程见下文。</description><pubDate>Wed, 01 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Windows下ubuntu双系统的安装&lt;/h1&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们IST实验室选择了Ubuntu作为视觉工作的系统，通常使用22.04版本。详细安装流程见下文。&lt;/p&gt;
&lt;p&gt;如果你想生动地了解如何下载，可以去这个网址 &lt;a href=&quot;https://www.bilibili.com/video/BV1554y1n7zv/?share_source=copy_web&amp;amp;vd_source=48d5cb51bbb656914376a7aa87b5e244&quot;&gt;B站教学&lt;/a&gt; 看视频学习。&lt;/p&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;一台已安装 Windows 的电脑&lt;/li&gt;
&lt;li&gt;至少 100GB 的可用磁盘空间&lt;/li&gt;
&lt;li&gt;一个 8GB 及以上的 U盘（用于制作 Ubuntu 启动盘）&lt;/li&gt;
&lt;li&gt;Ubuntu 镜像文件（可从官网 https://ubuntu.com/download 获取）&lt;/li&gt;
&lt;li&gt;备份重要数据，防止操作失误导致数据丢失&lt;/li&gt;
&lt;li&gt;有问题时不用慌，先去看一下后文的常见问题有没有解决方案，也可以询问AI&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Windows 分区调整&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;打开“磁盘管理”工具（Win+X → 磁盘管理）。&lt;/li&gt;
&lt;li&gt;右键 C 盘或其他分区，选择“压缩卷”，为 Ubuntu 预留空间（建议 20GB 以上）。&lt;/li&gt;
&lt;li&gt;压缩后会出现“未分配空间”，无需新建分区，安装时由 Ubuntu 自动分配。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;制作 Ubuntu 启动盘&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;下载并安装 Rufus 工具（&lt;a href=&quot;https://rufus.ie/&quot;&gt;官网地址&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;插入 U盘，打开 Rufus，选择下载好的 Ubuntu 镜像。&lt;/li&gt;
&lt;li&gt;保持默认设置，点击“开始”制作启动盘。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;BIOS/UEFI 设置&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;重启电脑，进入 BIOS 设置（根据不同电脑品牌自行查询）。&lt;/li&gt;
&lt;li&gt;将 U盘设置为第一启动项。&lt;/li&gt;
&lt;li&gt;若为 UEFI 模式，建议关闭安全启动（Secure Boot）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;安装 Ubuntu&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;插入启动盘，重启电脑进入 Ubuntu 安装界面。&lt;/li&gt;
&lt;li&gt;选择“安装 Ubuntu”。&lt;/li&gt;
&lt;li&gt;安装类型选择“与 Windows 共存”或“其他选项”，手动选择刚才压缩出的未分配空间。（个人推荐最好选择“与 Windows 共存”，操作简单）&lt;/li&gt;
&lt;li&gt;设置分区（推荐：/ 根分区 20GB+，swap 2GB+，/home 可选）。&lt;/li&gt;
&lt;li&gt;按提示完成安装，期间设置用户名、密码等。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;引导修复与启动选择&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;安装完成后，重启电脑会进入 GRUB 引导界面，可选择 Windows 或 Ubuntu。&lt;/li&gt;
&lt;li&gt;若未出现 GRUB，可用启动盘进入 Ubuntu，安装并运行 boot-repair 工具修复引导。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题与解决&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分区丢失/数据丢失&lt;/strong&gt;：安装前务必备份重要数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无法进入 Ubuntu/Windows&lt;/strong&gt;：检查 BIOS 启动项及分区设置，必要时使用 boot-repair。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GRUB 未显示&lt;/strong&gt;：尝试修复引导或检查分区格式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;英伟达显卡安装黑屏或安全模式问题&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;如果在安装 Ubuntu 时出现黑屏或只能进入安全模式（可视范围很小），且 Windows 进入了独显模式，有两种解决方案：
&lt;ol&gt;
&lt;li&gt;进入 Windows，关闭独显模式，重新进入安装界面。安装完成后，进入 Ubuntu，打开“附加驱动”，安装英伟达驱动，优先选择带有“test”或“专用”字样的版本。&lt;/li&gt;
&lt;li&gt;在 GRUB 引导界面时，将光标移动到 Ubuntu 安装选项，不要回车。按“E”进入编辑模式，使用方向键向下滚动，找到以 linux 开头的一行（结尾通常是 $vt_handoff 或 quiet splash），在 splash 后面加上 nomodeset，然后按 Ctrl+X 启动。进入系统后，按方案1安装驱动。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;通过以上步骤，你可以在一台电脑上顺利安装并使用 Windows 与 Ubuntu 双系统。遇到问题时，可以发到群里让你亲爱的学长帮助你，如果学长也无法帮助你，可以查阅官方文档或相关社区获取帮助，如果還是無法解決，去電腦店刷機。&lt;/p&gt;
</content:encoded></item><item><title>Anaconda及pycharm的安装</title><link>https://fuwari.vercel.app/posts/anaconda-and-pycharm-installation/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/anaconda-and-pycharm-installation/</guid><description>本文是为Python新手准备的开发环境配置指南。对于初学者，我们强烈推荐使用Anaconda和PyCharm的组合——Anaconda提供了完整的Python环境和科学计算库，而PyCharm则是专业的Python开发工...</description><pubDate>Sat, 16 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Python开发环境搭建指南：Anaconda与PyCharm安装教程&lt;/h1&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文是为Python新手准备的开发环境配置指南。对于初学者，我们强烈推荐使用Anaconda和PyCharm的组合——Anaconda提供了完整的Python环境和科学计算库，而PyCharm则是专业的Python开发工具，二者搭配能显著降低学习门槛。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、Anaconda安装指南&lt;/h2&gt;
&lt;h3&gt;1. Anaconda是什么？&lt;/h3&gt;
&lt;p&gt;Anaconda是Python的一个发行版，它包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python解释器&lt;/li&gt;
&lt;li&gt;Conda包管理器（比pip更强大的依赖管理）&lt;/li&gt;
&lt;li&gt;180+预装科学计算库（如NumPy, Pandas, Matplotlib等）&lt;/li&gt;
&lt;li&gt;Jupyter Notebook等实用工具&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;安装Anaconda后就无需单独安装Python和其他基础库了。&lt;/p&gt;
&lt;h3&gt;2. 下载安装包&lt;/h3&gt;
&lt;h4&gt;2.1 官方渠道（不推荐新手使用）&lt;/h4&gt;
&lt;p&gt;官网地址：&lt;a href=&quot;https://www.anaconda.com&quot;&gt;https://www.anaconda.com&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;2.2 国内镜像（推荐）&lt;/h4&gt;
&lt;p&gt;清华大学开源镜像站：
&lt;a href=&quot;https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/&quot;&gt;https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;选择对应操作系统（由于清华源内容很多，找的时候一定要看清）：
&lt;img src=&quot;/images/python/anaconda%E4%B8%8B%E8%BD%BD.png&quot; alt=&quot;Anaconda下载界面&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. 详细安装步骤（Windows示例）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;运行安装程序&lt;/strong&gt;：双击下载的.exe文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户选择&lt;/strong&gt;：建议选择&quot;Just Me&quot;（仅当前用户）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安装路径&lt;/strong&gt;：建议修改为&lt;code&gt;D:\Anaconda3&lt;/code&gt;等非系统盘路径，别安装到C盘就行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高级选项&lt;/strong&gt;（关键步骤）：
&lt;ul&gt;
&lt;li&gt;✅ Add Anaconda to my PATH environment variable(如果你想要自己手动添加到环境变量也可以不勾选)&lt;/li&gt;
&lt;li&gt;✅ Register Anaconda as my default Python 3.x&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;等待安装完成（约10-20分钟）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4. 验证安装&lt;/h3&gt;
&lt;p&gt;打开命令提示符（Win+R → 输入cmd），执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda --version
# 应显示类似：conda 23.7.4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/anaconda%E4%B8%8B%E8%BD%BD%E6%88%90%E5%8A%9F.png&quot; alt=&quot;Anaconda&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;二、pycharm安装指南&lt;/h2&gt;
&lt;h3&gt;1. PyCharm 是什么？&lt;/h3&gt;
&lt;p&gt;PyCharm 是 JetBrains 开发的一款专业 Python IDE，具备：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;智能代码补全与语法高亮&lt;/li&gt;
&lt;li&gt;图形化调试器 &amp;amp; 测试运行器&lt;/li&gt;
&lt;li&gt;Git / SVN / Docker 等一键集成&lt;/li&gt;
&lt;li&gt;内置终端、虚拟环境管理与 Conda 支持&lt;/li&gt;
&lt;li&gt;科学模式（Scientific Mode），可无缝调用 Anaconda 中的 NumPy、Matplotlib 等库&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 下载安装包&lt;/h3&gt;
&lt;h4&gt;1 官方渠道（推荐）&lt;/h4&gt;
&lt;p&gt;官网地址：https://www.jetbrains.com/pycharm/download&lt;br /&gt;
之前本来还分社区版（白嫖）和专业版（氪佬），但是好像这段时间统一成一个程序里了，可选择pro用户。&lt;/p&gt;
&lt;h3&gt;3. 详细安装步骤（Windows 示例）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行安装程序&lt;/strong&gt;&lt;br /&gt;
双击下载的 &lt;code&gt;pycharm-2024.x.x.exe&lt;/code&gt; → 点击 “Next”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;安装路径&lt;/strong&gt;&lt;br /&gt;
建议改为 &lt;code&gt;D:\JetBrains\PyCharm 2024.x&lt;/code&gt;，避免 C 盘空间不足。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;安装选项&lt;/strong&gt;（建议全部勾选）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Create Desktop Shortcut（64-bit launcher）&lt;/li&gt;
&lt;li&gt;✅ Add &quot;Open Folder as Project&quot;&lt;/li&gt;
&lt;li&gt;✅ .py 关联&lt;/li&gt;
&lt;li&gt;✅ Add launchers dir to the PATH（后续命令行可直接 &lt;code&gt;pycharm&lt;/code&gt; 打开，没错，这也是为什么anaconda也让你放环境中的理由）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4. 配置conda环境&lt;/h3&gt;
&lt;p&gt;1.在你的任务栏中搜索&lt;code&gt;anaconda prompt&lt;/code&gt;，打开之后输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda create -n myenv python=3.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里myenv是你创建的虚拟环境的名字，3.11是这个环境中python的版本号
2.稍等片刻后输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda activate myenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.像这样你就创建成功了
&lt;img src=&quot;/images/python/%E7%8E%AF%E5%A2%83.png&quot; alt=&quot;Anaconda&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;5. 配置 Anaconda 解释器（关键步骤）&lt;/h3&gt;
&lt;p&gt;可以先去网上搜索中文插件&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;新建项目&lt;/strong&gt;&lt;br /&gt;
打开 PyCharm → New Project → Location 填写 &lt;code&gt;D:\Code\PyDemo&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;选择 Conda 环境&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接打开右下角的地方，如图，选择添加新的解释器
&lt;img src=&quot;/images/python/%E9%85%8D%E7%BD%AE.png&quot; alt=&quot;Anaconda&quot; /&gt;&lt;/li&gt;
&lt;li&gt;选择现有，类型选择conda类型，conda路径在anaconda的下载地址中找类似于我这个
E:\anaconda\Scripts\conda.exe（我安装到了E盘）
&lt;img src=&quot;/images/python/%E5%8A%A0%E8%BD%BD%E7%8E%AF%E5%A2%83.png&quot; alt=&quot;Anaconda&quot; /&gt;&lt;/li&gt;
&lt;li&gt;环境选择我们刚才创建好的&lt;code&gt;myenv&lt;/code&gt;，点击确认&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;验证&lt;/strong&gt;&lt;br /&gt;
新建 &lt;code&gt;hello.py&lt;/code&gt; 输入以下代码并运行 &lt;code&gt;Shift+F10&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&quot;hello world&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果可以正常输出&lt;code&gt;hello world&lt;/code&gt;那么就成功安装好了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item></channel></rss>