<?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/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Chitori.blog</title><description>A star.</description><link>https://chitori-blog.vercel.app</link><item><title>C++工程实践</title><link>https://chitori-blog.vercel.app/blog/cpp-practice</link><guid isPermaLink="true">https://chitori-blog.vercel.app/blog/cpp-practice</guid><description>在掌握了c++基本语法的情况下，掌握一些进阶的工程实践</description><pubDate>Sun, 13 Sep 2026 21:33:44 GMT</pubDate><content:encoded>&lt;p&gt;经历了c++一些基础语法的学习（比如变量、逻辑语句、指针、函数、结构体等电类大一程设课就会学到的知识）之后，我们需要掌握一些常用的小技巧,来让我们在较为大型的项目编写中效率更高。&lt;/p&gt;
&lt;h2&gt;面向对象&lt;/h2&gt;
&lt;p&gt;在只会写小作业的时候，我们可以把程序组织成「结构体装数据 + 一堆函数处理数据」的样子，这在代码只有几百行时没什么问题。但当项目变大——比如一个视觉程序要同时处理自瞄、能量机关和导航——这种写法的弊端就会暴露：数据是公开的，函数是散落的，你想改一个字段的含义，得先把操作它的所有函数翻出来改一遍。&lt;/p&gt;
&lt;p&gt;面向对象（Object-Oriented Programming）的核心想法只有一句话：&lt;strong&gt;把数据和操作数据的方法打包成一个整体&lt;/strong&gt;，这个整体就是类（class）。这一节按「封装 → 继承 → 多态」的顺序，把它拆开来看。&lt;/p&gt;
&lt;h3&gt;从结构体到类&lt;/h3&gt;
&lt;p&gt;先看一个反例。结构体 &lt;code&gt;Circle&lt;/code&gt; 保存半径，函数 &lt;code&gt;area&lt;/code&gt; 根据半径算面积：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

struct Circle {
    double radius;
};

double area(const Circle&amp;#x26; c) { return 3.14159 * c.radius * c.radius; }

int main() {
    Circle c;
    c.radius = -5;                 // 编译通过，但圆的半径不该是负数
    std::cout &amp;#x3C;&amp;#x3C; area(c) &amp;#x3C;&amp;#x3C; &apos;\n&apos;;  // 输出 78.5397，一个不存在的圆
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;半径是圆的内部状态，&lt;code&gt;area&lt;/code&gt; 是依赖这个状态的行为，但两者的联系完全靠命名维持：任何人都能往 &lt;code&gt;radius&lt;/code&gt; 里塞进任意数字，&lt;code&gt;area&lt;/code&gt; 没有任何机会阻止。&lt;/p&gt;
&lt;p&gt;我们希望的写法是：数据自己保管状态，操作跟着数据走，只有类允许的操作才能改它——这正是 &lt;code&gt;class&lt;/code&gt; 要做的事。&lt;/p&gt;
&lt;h3&gt;封装：把状态藏起来&lt;/h3&gt;
&lt;p&gt;把上面的例子改写成类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

class Circle {
public:
    double area() const { return 3.14159 * radius_ * radius_; }

    double radius() const { return radius_; }

    void setRadius(double r) {
        if (r &amp;#x3C;= 0) {
            std::cout &amp;#x3C;&amp;#x3C; &quot;半径必须为正数\n&quot;;
            return;
        }
        radius_ = r;
    }

private:
    double radius_ = 1.0;
};

int main() {
    Circle c;
    c.setRadius(-5);
    std::cout &amp;#x3C;&amp;#x3C; c.radius() &amp;#x3C;&amp;#x3C; &apos; &apos; &amp;#x3C;&amp;#x3C; c.area() &amp;#x3C;&amp;#x3C; &apos;\n&apos;;
    c.setRadius(2);
    std::cout &amp;#x3C;&amp;#x3C; c.radius() &amp;#x3C;&amp;#x3C; &apos; &apos; &amp;#x3C;&amp;#x3C; c.area() &amp;#x3C;&amp;#x3C; &apos;\n&apos;;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;public&lt;/code&gt; 里的成员组成类的对外接口，&lt;code&gt;private&lt;/code&gt; 里的成员只有类自己能看到。现在外界没法直接碰 &lt;code&gt;radius_&lt;/code&gt; 了，只能通过 &lt;code&gt;setRadius&lt;/code&gt; 函数修改，而 &lt;code&gt;setRadius&lt;/code&gt; 顺手就挡掉了非正数；想读半径或者算面积，分别调用 &lt;code&gt;radius()&lt;/code&gt; 和 &lt;code&gt;area()&lt;/code&gt; 即可。函数签名末尾的 &lt;code&gt;const&lt;/code&gt; 表示这个成员函数不会修改对象。&lt;/p&gt;
&lt;p&gt;通过封装，在&lt;code&gt;main&lt;/code&gt;函数里就可以直接调用对象内部函数的接口，极大简化了&lt;code&gt;main&lt;/code&gt;函数内部的操作，也增加了安全性。这样的话，调用者只需要记住 &lt;code&gt;setRadius&lt;/code&gt; 会拒绝非正数这个承诺，不需要关心内部是用 &lt;code&gt;if&lt;/code&gt; 判断还是抛异常。&lt;/p&gt;
&lt;h3&gt;构造函数、析构函数与初始化列表&lt;/h3&gt;
&lt;p&gt;上面的 &lt;code&gt;radius_&lt;/code&gt; 靠 &lt;code&gt;= 1.0&lt;/code&gt; 就地初始化，但如果初始值要由创建者决定，就得用构造函数。构造函数在对象创建时自动调用，名字与类相同，没有返回值；析构函数反过来，在对象销毁时自动调用，名字是 &lt;code&gt;~&lt;/code&gt; 加类名，负责收尾。C++ 最经典的资源管理手法就建立在这两者之上：构造时申请，析构时释放，也就是常说的 RAII。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

class IntArray {
public:
    IntArray(int size) : size_(size), data_(new int[size]) {}

    ~IntArray() { delete[] data_; }

    void set(int index, int value) { data_[index] = value; }

    int get(int index) const { return data_[index]; }

private:
    int size_;
    int* data_;
};

int main() {
    IntArray arr(3);
    arr.set(0, 1);
    arr.set(1, 2);
    arr.set(2, 3);
    std::cout &amp;#x3C;&amp;#x3C; arr.get(0) + arr.get(1) + arr.get(2) &amp;#x3C;&amp;#x3C; &apos;\n&apos;;  // 输出 6
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;冒号后面的部分叫初始化列表，它在构造函数体执行之前完成成员的初始化。对 &lt;code&gt;const&lt;/code&gt; 成员、引用成员，或者没有默认构造函数的成员来说，初始化列表是唯一的写法——因为进入函数体时，所有成员都已经构造完毕了。&lt;/p&gt;
&lt;p&gt;初始化列表还有一个注意点：&lt;strong&gt;成员的初始化顺序由声明顺序决定，与你写在列表里的顺序无关&lt;/strong&gt;。所以列表最好按声明顺序写，否则很容易踩到「拿一个还没初始化的成员去初始化另一个成员」的问题。&lt;/p&gt;
&lt;p&gt;另外要注意：一旦为类写了构造函数，编译器就不会再自动生成默认构造函数；而本例为了简单，没有处理拷贝的情况（正式代码里应当禁用拷贝或实现深拷贝），这里只需要记住「析构函数会自动收尾」这件事。&lt;/p&gt;
&lt;h3&gt;this 指针&lt;/h3&gt;
&lt;p&gt;构造函数里有个常见歧义：如果参数名和成员名一样，函数里的名字到底指谁？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

class Point {
public:
    Point(int x, int y) {
        this-&gt;x = x;  // this-&gt;x 是成员，右边的 x 是参数
        this-&gt;y = y;
    }

    int getX() const { return x; }
    int getY() const { return y; }

    void setX(int x) { this-&gt;x = x; }

private:
    int x;
    int y;
};

int main() {
    Point p(3, 4);
    p.setX(10);
    std::cout &amp;#x3C;&amp;#x3C; p.getX() &amp;#x3C;&amp;#x3C; &apos; &apos; &amp;#x3C;&amp;#x3C; p.getY() &amp;#x3C;&amp;#x3C; &apos;\n&apos;;  // 输出 10 4
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;this&lt;/code&gt; 是指向「当前对象」的指针，&lt;code&gt;this-&gt;x&lt;/code&gt; 明确表示成员变量，而不带前缀的 &lt;code&gt;x&lt;/code&gt; 会优先匹配参数。平时在成员函数里访问成员，编译器会自动补上 &lt;code&gt;this-&gt;&lt;/code&gt;，所以几乎感觉不到它的存在；出现重名歧义时，它才是唯一说得清楚的写法。此外 &lt;code&gt;return *this;&lt;/code&gt; 可以返回对象自身（用于支持链式调用），这里不展开。&lt;/p&gt;
&lt;h3&gt;继承&lt;/h3&gt;
&lt;p&gt;假设程序里除了“猫”还会有“狗”两个对象，它们都共享「名字」「呼吸」这些成员，从头写两遍很快就会变成维护负担。继承把公共部分提取到一个父类（基类）里，子类（派生类）自动拥有父类的成员，只需要补上自己的部分：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;

class Animal {
public:
    Animal(const std::string&amp;#x26; name) : name_(name) {}

    void breathe() const { std::cout &amp;#x3C;&amp;#x3C; name_ &amp;#x3C;&amp;#x3C; &quot; 在呼吸\n&quot;; }

protected:
    std::string name_;  // 子类可以访问，外部不行
};

class Cat : public Animal {
public:
    Cat(const std::string&amp;#x26; name) : Animal(name) {}  // 先构造父类
    void meow() const { std::cout &amp;#x3C;&amp;#x3C; name_ &amp;#x3C;&amp;#x3C; &quot; 喵\n&quot;; }
};

int main() {
    Cat cat(&quot;咪咪&quot;);
    cat.breathe();
    cat.meow();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;class Cat : public Animal&lt;/code&gt; 读作「Cat 是一种 Animal」。创建 &lt;code&gt;Cat&lt;/code&gt; 时先调用父类构造函数、再执行子类构造函数，析构顺序相反；如果父类没有默认构造函数，子类就必须在初始化列表里显式调用它。&lt;code&gt;protected&lt;/code&gt; 介于 &lt;code&gt;public&lt;/code&gt; 和 &lt;code&gt;private&lt;/code&gt; 之间：外部访问不了，但子类可以，适合放这种「留给子类用」的内部状态。&lt;/p&gt;
&lt;p&gt;需要留意，&lt;code&gt;public&lt;/code&gt; 在这里不能省略：&lt;code&gt;class&lt;/code&gt; 的继承默认是 &lt;code&gt;private&lt;/code&gt; 继承，会把父类成员全部对外藏起来，和 &lt;code&gt;struct&lt;/code&gt; 默认的 &lt;code&gt;public&lt;/code&gt; 继承刚好相反。&lt;/p&gt;
&lt;h3&gt;多态与虚函数&lt;/h3&gt;
&lt;p&gt;继承更重要的能力体现在下面这种场景：例如写一个函数，接收任意一种动物并让它发出叫声。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;

class Animal {
public:
    Animal(const std::string&amp;#x26; name) : name_(name) {}
    void speak() const { std::cout &amp;#x3C;&amp;#x3C; name_ &amp;#x3C;&amp;#x3C; &quot; 发出声音\n&quot;; }
protected:
    std::string name_;
};

class Cat : public Animal {
public:
    Cat(const std::string&amp;#x26; name) : Animal(name) {}
    void speak() const { std::cout &amp;#x3C;&amp;#x3C; name_ &amp;#x3C;&amp;#x3C; &quot; 喵\n&quot;; }  // 看起来覆盖了父类版本
};

void greet(const Animal&amp;#x26; animal) { animal.speak(); }

int main() {
    Cat cat(&quot;咪咪&quot;);
    greet(cat);  // 输出：咪咪 发出声音
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;奇怪的是，&lt;code&gt;cat&lt;/code&gt; 明明是 &lt;code&gt;Cat&lt;/code&gt;，&lt;code&gt;greet&lt;/code&gt; 却调用了父类的 &lt;code&gt;speak&lt;/code&gt;。原因是 &lt;code&gt;Animal&amp;#x26;&lt;/code&gt; 这个类型在编译期就决定了调用哪个函数，这叫静态绑定；而我们想要的是运行期根据对象的实际类型选择函数，也就是多态。让这件事发生的关键字是 &lt;code&gt;virtual&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;string&gt;

class Animal {
public:
    Animal(const std::string&amp;#x26; name) : name_(name) {}

    virtual void speak() const { std::cout &amp;#x3C;&amp;#x3C; name_ &amp;#x3C;&amp;#x3C; &quot; 发出声音\n&quot;; }

    virtual ~Animal() = default;

protected:
    std::string name_;
};

class Cat : public Animal {
public:
    Cat(const std::string&amp;#x26; name) : Animal(name) {}

    void speak() const override { std::cout &amp;#x3C;&amp;#x3C; name_ &amp;#x3C;&amp;#x3C; &quot; 喵\n&quot;; }
};

void greet(const Animal&amp;#x26; animal) { animal.speak(); }

int main() {
    Cat cat(&quot;咪咪&quot;);
    greet(cat);  // 输出：咪咪 喵
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;父类函数加上 &lt;code&gt;virtual&lt;/code&gt; 后变得「可以被覆盖」，运行时会根据对象的真实类型选择版本。子类这边推荐写上 &lt;code&gt;override&lt;/code&gt;：它对运行没有任何开销，但能让编译器检查「你以为覆盖了、其实没覆盖」的情况——参数类型或 &lt;code&gt;const&lt;/code&gt; 不一致时，没有 &lt;code&gt;override&lt;/code&gt; 的写法会悄悄变成新增一个重载，加上 &lt;code&gt;override&lt;/code&gt; 就会直接编译报错。&lt;/p&gt;
&lt;p&gt;还需要注意父类的 &lt;code&gt;virtual ~Animal() = default;&lt;/code&gt;。如果一个类会被继承，并且可能通过基类指针删除子类对象，基类的析构函数就必须是虚函数，否则子类的析构不会执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

class Base {
public:
    ~Base() { std::cout &amp;#x3C;&amp;#x3C; &quot;~Base\n&quot;; }  // 不是虚函数！
};

class Derived : public Base {
public:
    ~Derived() { std::cout &amp;#x3C;&amp;#x3C; &quot;~Derived\n&quot;; }
};

int main() {
    Base* p = new Derived;
    delete p;  // 在绝大多数实现上只输出 ~Base，~Derived 被跳过
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把 &lt;code&gt;~Base&lt;/code&gt; 改成 &lt;code&gt;virtual ~Base() = default;&lt;/code&gt;，输出就会变成先 &lt;code&gt;~Derived&lt;/code&gt; 再 &lt;code&gt;~Base&lt;/code&gt;。规则很好记：&lt;strong&gt;只要一个类可能被继承，就给它一个虚析构函数。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;纯虚函数与抽象类&lt;/h3&gt;
&lt;p&gt;回头再看 &lt;code&gt;Animal&lt;/code&gt;，它的 &lt;code&gt;speak&lt;/code&gt; 实现其实很鸡肋：现实里没有一种叫「动物」的东西会发出声音，父类在这里只负责给子类定标准。既然父类不需要实现，可以干脆写成纯虚函数；包含纯虚函数的类叫抽象类，不能被实例化：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

class Animal {
public:
    virtual void speak() const = 0;  // 纯虚函数
    virtual ~Animal() = default;
};

class Cat : public Animal {
public:
    void speak() const override { std::cout &amp;#x3C;&amp;#x3C; &quot;喵\n&quot;; }
};

int main() {
    // Animal animal;         // 错误：抽象类不能被实例化
    Animal* pet = new Cat();  // 可以：基类指针指向具体子类
    pet-&gt;speak();             // 输出：喵
    delete pet;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;抽象类像一份合同：凡是宣称自己是一种 &lt;code&gt;Animal&lt;/code&gt; 的类，都必须把 &lt;code&gt;speak&lt;/code&gt; 实现出来，否则它也是抽象类，同样不能实例化。工程里常用它来表达接口——比如视觉程序可以把自瞄、能量机关等任务都抽象成同一个「检测器」接口，各自实现识别逻辑，上层调度代码只面对接口，不必知道此刻在跑的是哪一个检测器。&lt;/p&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;p&gt;封装让数据只通过接口被修改，继承让公共代码只需要写一遍，多态让调用者与具体实现解耦——这三者是面向对象最基本的骨架。到这里，我们已经能把程序拆成多个互相配合的类了；但一个正经项目会把它们分散在几十个 &lt;code&gt;.hpp&lt;/code&gt; 和 &lt;code&gt;.cpp&lt;/code&gt; 文件里，怎么让这些文件一起变成一个可执行文件？接下来的 CMake 解决的就是这件事。&lt;/p&gt;
&lt;h2&gt;CMake&lt;/h2&gt;
&lt;h3&gt;编译与链接&lt;/h3&gt;
&lt;p&gt;问题先从「多文件怎么编译」说起。假设我们把上一节的工具函数整理成一个真实工程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;cpp-practice/
├── include/
│   └── math_utils.hpp
└── src/
    ├── main.cpp
    └── math_utils.cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.hpp&lt;/code&gt; 放函数声明，&lt;code&gt;.cpp&lt;/code&gt; 放函数实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#pragma once

int add(int a, int b);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &quot;math_utils.hpp&quot;

int add(int a, int b) { return a + b; }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

#include &quot;math_utils.hpp&quot;

int main() {
    std::cout &amp;#x3C;&amp;#x3C; &quot;1 + 2 = &quot; &amp;#x3C;&amp;#x3C; add(1, 2) &amp;#x3C;&amp;#x3C; &apos;\n&apos;;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;#pragma once&lt;/code&gt; 用来防止同一个头文件被重复展开；&lt;code&gt;#include &quot;...&quot;&lt;/code&gt; 在预处理阶段把头文件内容原样粘贴进来。真正的编译以每个 &lt;code&gt;.cpp&lt;/code&gt; 为单位（称为一个翻译单元），各自生成 &lt;code&gt;.o&lt;/code&gt; 目标文件，最后链接器把目标文件和用到的库拼成可执行文件。头文件里只有声明，所以任何包含 &lt;code&gt;math_utils.hpp&lt;/code&gt; 的源文件都能通过编译；如果忘了写 &lt;code&gt;math_utils.cpp&lt;/code&gt;，就会在链接阶段报 &lt;code&gt;undefined reference&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;不借助任何工具，编译这个工程只需要：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;g++ -std=c++17 src/main.cpp src/math_utils.cpp -Iinclude -o main
./main    # 输出：1 + 2 = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文件一多，这条命令就会越写越长；换台机器、换个库，路径和参数又要重来；更麻烦的是每次都全量重新编译。构建工具要解决的正是「怎么描述并执行这套流程」。&lt;/p&gt;
&lt;h3&gt;为什么是 CMake&lt;/h3&gt;
&lt;p&gt;在 CMake 之前流行的是 Makefile，它也能做到增量编译，但语法古老（规则行必须以 Tab 开头）、且与平台绑定。CMake 换了个思路：我们只写一份平台无关的 &lt;code&gt;CMakeLists.txt&lt;/code&gt; 描述工程，CMake 再据此生成对应平台的构建文件（Linux 下是 Makefile，Windows 下是 Visual Studio 工程等）。整个构建分成两步：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cmake -S . -B build     # 配置：读取 CMakeLists.txt，在 build/ 下生成构建文件
# 其中-S . 为Source directory（源码目录）是当前目录
# -B build 为Build directory（构建目录）是 build/
cmake --build build     # 构建：调用底层的 make 等工具完成编译链接
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外有一种写法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir build   # 在当前目录建立build文件夹，mkdir为“make directory”
cd build    # 进入build文件夹
cmake ..    # ..表示源码目录是当前目录的上一级目录
make -j8    # 使用 Make 进行编译，并同时开启最多 8 个并行编译任务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意所有生成物都在 &lt;code&gt;build/&lt;/code&gt; 里，源码目录保持干净——这就是所谓的 out-of-source 构建，想从头再来直接删掉 &lt;code&gt;build/&lt;/code&gt; 即可。记得把 &lt;code&gt;build/&lt;/code&gt; 写进 &lt;code&gt;.gitignore&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;第一个 CMakeLists.txt&lt;/h3&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;cmake_minimum_required(VERSION 3.22)
project(cpp_practice LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(main src/main.cpp src/math_utils.cpp)
target_include_directories(main PRIVATE include)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逐行看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cmake_minimum_required&lt;/code&gt; 声明所需的 CMake 最低版本。这里取 3.22，也就是 Ubuntu 22.04 自带的版本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;project&lt;/code&gt; 声明工程名与使用的语言。&lt;/li&gt;
&lt;li&gt;两行 &lt;code&gt;set&lt;/code&gt; 让所有目标统一使用 C++17；&lt;code&gt;CMAKE_CXX_STANDARD_REQUIRED ON&lt;/code&gt; 表示编译器不支持就报错，而不是悄悄退回旧标准。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add_executable&lt;/code&gt; 生成名为 &lt;code&gt;main&lt;/code&gt; 的可执行文件，源文件需要一一列出。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;target_include_directories&lt;/code&gt; 告诉编译器，&lt;code&gt;#include &quot;math_utils.hpp&quot;&lt;/code&gt; 时去 &lt;code&gt;include/&lt;/code&gt; 目录里找。这里的 &lt;code&gt;PRIVATE&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;cmake -S . -B build     # 配置
cmake --build build     # 构建
./build/main            # 输出：1 + 2 = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;目标、库与传播&lt;/h3&gt;
&lt;p&gt;CMake 里最重要的概念是&lt;strong&gt;目标（target）&lt;/strong&gt;：每个 &lt;code&gt;add_executable&lt;/code&gt;、&lt;code&gt;add_library&lt;/code&gt; 生成的都是一个目标，目标的属性包括源文件、头文件路径、编译选项、依赖的库等。现代 CMake 的写法就是围绕目标来配置，而不是设置一堆全局变量。&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;math_utils&lt;/code&gt; 这类工具代码被多个可执行文件共用时，应该把它编译成库：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;cmake_minimum_required(VERSION 3.22)
project(cpp_practice LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(math STATIC src/math_utils.cpp)
target_include_directories(math PUBLIC include)

add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE math)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;add_library&lt;/code&gt; 把 &lt;code&gt;math_utils.cpp&lt;/code&gt; 编译成静态库 &lt;code&gt;libmath.a&lt;/code&gt;；&lt;code&gt;main&lt;/code&gt; 只需要 &lt;code&gt;target_link_libraries(main PRIVATE math)&lt;/code&gt;，既完成了链接，也自动获得了库的头文件路径——因为 &lt;code&gt;include/&lt;/code&gt; 是用 &lt;code&gt;PUBLIC&lt;/code&gt; 挂到 &lt;code&gt;math&lt;/code&gt; 上的。三个关键字的区别可以总结成一句话：&lt;/p&gt;
&lt;p&gt;| 关键字 | 我自己编译时需要 | 使用我的人编译时需要 | 典型场景 |
| --- | --- | --- | --- |
| &lt;code&gt;PRIVATE&lt;/code&gt; | 是 | 否 | 只在自己源文件里用到的依赖 |
| &lt;code&gt;PUBLIC&lt;/code&gt; | 是 | 是 | 头文件里就 &lt;code&gt;#include&lt;/code&gt; 的依赖（如 &lt;code&gt;include/&lt;/code&gt;） |
| &lt;code&gt;INTERFACE&lt;/code&gt; | 否 | 是 | 纯头文件库 |&lt;/p&gt;
&lt;p&gt;也就是：&lt;code&gt;PRIVATE&lt;/code&gt; 是「我用但你别管」，&lt;code&gt;PUBLIC&lt;/code&gt; 是「我用你也得用」，&lt;code&gt;INTERFACE&lt;/code&gt; 是「我不用但你得用」。属性会沿着链接关系自动传播给使用者，这正是它比全局的 &lt;code&gt;include_directories()&lt;/code&gt; 更可靠的地方——全局设置会污染每一个目标，而目标属性只影响该影响的人。&lt;/p&gt;
&lt;h3&gt;引入第三方库：OpenCV&lt;/h3&gt;
&lt;p&gt;RM 视觉离不开 OpenCV，引入方式与上面的 &lt;code&gt;math&lt;/code&gt; 库没有本质区别，只是库的位置交给 &lt;code&gt;find_package&lt;/code&gt; 去找：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;cmake_minimum_required(VERSION 3.22)
project(rm_vision LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(OpenCV REQUIRED)

add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE opencv_core opencv_imgproc)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;

#include &amp;#x3C;opencv2/core.hpp&gt;
#include &amp;#x3C;opencv2/imgproc.hpp&gt;

int main() {
    cv::Mat gray(480, 640, CV_8UC1, cv::Scalar(128));
    cv::Mat color;
    cv::cvtColor(gray, color, cv::COLOR_GRAY2BGR);
    std::cout &amp;#x3C;&amp;#x3C; color.cols &amp;#x3C;&amp;#x3C; &quot;x&quot; &amp;#x3C;&amp;#x3C; color.rows
              &amp;#x3C;&amp;#x3C; &quot;, channels = &quot; &amp;#x3C;&amp;#x3C; color.channels() &amp;#x3C;&amp;#x3C; &apos;\n&apos;;  // 输出：640x480, channels = 3
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;find_package(OpenCV REQUIRED)&lt;/code&gt; 会在系统约定路径里寻找 OpenCV 的 CMake 配置，找到后把各个模块注册成可以链接的目标；&lt;code&gt;opencv_core&lt;/code&gt;、&lt;code&gt;opencv_imgproc&lt;/code&gt; 就是导入目标，它们的头文件路径等属性由 OpenCV 自己带过来，所以不需要再手写 &lt;code&gt;target_include_directories&lt;/code&gt;。Eigen 这种纯头文件库同理：&lt;code&gt;find_package(Eigen3 REQUIRED)&lt;/code&gt; 之后链接 &lt;code&gt;Eigen3::Eigen&lt;/code&gt; 即可。&lt;/p&gt;
&lt;h3&gt;几条工程习惯&lt;/h3&gt;
&lt;p&gt;最后是几条实践建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;永远在源码目录之外构建，不要用 &lt;code&gt;cmake .&lt;/code&gt;，否则生成物散落在源码里，清理起来非常麻烦。&lt;/li&gt;
&lt;li&gt;加上 &lt;code&gt;-DCMAKE_EXPORT_COMPILE_COMMANDS=ON&lt;/code&gt;，CMake 会生成 &lt;code&gt;build/compile_commands.json&lt;/code&gt;，VS Code 的 clangd 插件靠它提供补全和跳转。&lt;/li&gt;
&lt;li&gt;构建行为诡异时，最省事的做法通常是 &lt;code&gt;rm -rf build&lt;/code&gt; 重来，CMake 缓存被旧配置污染后，排查成本远高于重建。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;p&gt;CMake 自己不编译代码，它只是把我们关于工程的描述翻译成构建工具能执行的指令。掌握「一份 &lt;code&gt;CMakeLists.txt&lt;/code&gt; + 若干 target + 用 &lt;code&gt;target_link_libraries&lt;/code&gt; 连接依赖」这套骨架，就足以应付绝大多数工程；以后面对陌生的 CMake 项目，从 &lt;code&gt;add_executable&lt;/code&gt; / &lt;code&gt;add_library&lt;/code&gt; 两行读起，基本不会迷路。RM 视觉工程的结构也大多如此：&lt;code&gt;include/&lt;/code&gt; 放头文件，&lt;code&gt;src/&lt;/code&gt; 放实现，再用 &lt;code&gt;find_package&lt;/code&gt; 引入 OpenCV、Eigen 这些依赖。&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.CULmzxB-.jpg"/><enclosure url="/_astro/cover.CULmzxB-.jpg"/></item><item><title>Tips to improve concentration</title><link>https://chitori-blog.vercel.app/blog/improve-concentration</link><guid isPermaLink="true">https://chitori-blog.vercel.app/blog/improve-concentration</guid><description>Mindfulness, cognitive training, and a healthy lifestyle may help sharpen your focus.</description><pubDate>Sat, 10 May 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;You&apos;re trying to concentrate, but your mind is wandering or you&apos;re easily distracted. What happened to the laser-sharp focus you once enjoyed? As we age, we tend to have more difficulty filtering out stimuli that are not relevant to the task at hand.&lt;/p&gt;
&lt;h2&gt;What&apos;s fogging up focus?&lt;/h2&gt;
&lt;p&gt;Like a computer that slows with use, the brain accumulates wear and tear that affects processing. This can be caused by a number of physiological stressors such as inflammation, injury to blood vessels (especially if you have high blood pressure), the buildup of abnormal proteins, and naturally occurring brain shrinkage.&lt;/p&gt;
&lt;p&gt;The following factors can also affect your concentration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Underlying conditions.&lt;/strong&gt; Depression or sleep disorders (such as sleep apnea) can undermine your ability to concentrate. So can the effects of vision or hearing loss. You waste precious cognitive resources when you spend too much time trying to make out what&apos;s written on a page or just hear what someone is saying.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Medication side effects.&lt;/strong&gt; Some drugs, especially anticholinergics (such as treatments for incontinence, depression, or allergies), can slow processing speed and your ability to think clearly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Excessive drinking.&lt;/strong&gt; Having too much alcohol impairs thinking and causes interrupted sleep, which affects concentration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Information overload.&lt;/strong&gt; We are bombarded with information from TVs, computers, and messages such as texts or emails. When there&apos;s too much material, it burdens our filtering system and it&apos;s easy to get distracted.&lt;/p&gt;
&lt;h2&gt;Strategies to stay focused&lt;/h2&gt;
&lt;p&gt;To improve attention, consider the following strategies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mindfulness.&lt;/strong&gt; &quot;Mindfulness is about focusing attention on the present moment, and practicing mindfulness has been shown to rewire the brain so that attention is stronger in everyday life,&quot; says Kim Willment, a neuropsychologist with Brigham and Women&apos;s Hospital. She recommends sitting still for a few minutes each day, closing your eyes, and focusing on your breathing as well as the sounds and sensations around you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cognitive training.&lt;/strong&gt; Computerized cognitive training games aim to improve your response times and attention. Evidence that this works has been mixed. &quot;The goal of playing these games is not to get better at them, but to get better in the cognitive activities of everyday life,&quot; Willment says. &quot;But there is evidence that a person&apos;s ability to pay attention can be improved by progressively pushing the person to higher levels of performance. So if you reach a certain level of sustained attention, pushing it to the next level can help improve it, and this may translate to everyday life.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A healthier lifestyle.&lt;/strong&gt; Many aspects of a healthy lifestyle can help attention, starting with sleep and exercise. There is a direct link between exercise and cognitive ability, especially attention. When you exercise, you increase the availability of brain chemicals that promote new brain connections, reduce stress, and improve sleep. And when we sleep, we reduce stress hormones that can be harmful to the brain, and we clear out proteins that injure it.&lt;/p&gt;
&lt;p&gt;Aim for seven to eight hours of sleep each night, and 150 minutes per week of aerobic exercise, such as brisk walking.&lt;/p&gt;
&lt;p&gt;Other healthy steps to improve focus: eat a Mediterranean-style diet, which has been shown to support brain health; treat underlying conditions; and change medications that may be affecting your ability to focus.&lt;/p&gt;
&lt;p&gt;Getting older is out of your control, but healthier living is something you determine, and it may improve concentration.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Article from: &lt;a href=&quot;https://www.health.harvard.edu/mind-and-mood/tips-to-improve-concentration&quot;&gt;Harvard Health Publishing&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="/_astro/thumbnail.1GZ294Dz.jpg"/><enclosure url="/_astro/thumbnail.1GZ294Dz.jpg"/><atom:updated>2025-06-12T00:00:00.000Z</atom:updated></item><item><title>Using MDX</title><link>https://chitori-blog.vercel.app/blog/using-mdx</link><guid isPermaLink="true">https://chitori-blog.vercel.app/blog/using-mdx</guid><description>Learning how to use MDX in Astro</description><pubDate>Sun, 01 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This theme comes with the &lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/mdx/&quot;&gt;@astrojs/mdx&lt;/a&gt; integration installed and configured in your &lt;code&gt;astro.config.ts&lt;/code&gt; config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file.&lt;/p&gt;
&lt;h2&gt;Why MDX?&lt;/h2&gt;
&lt;p&gt;MDX is a special flavor of Markdown that supports embedded JavaScript &amp;#x26; JSX syntax. This unlocks the ability to &lt;a href=&quot;https://docs.astro.build/en/guides/markdown-content/#mdx-features&quot;&gt;mix JavaScript and UI Components into your Markdown content&lt;/a&gt; for things like interactive charts or alerts.&lt;/p&gt;
&lt;p&gt;If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze.&lt;/p&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;p&gt;Here is how you import and use a UI component inside of MDX.&lt;br&gt;
When you open this page in the browser, you should see the clickable button below.&lt;/p&gt;
&lt;p&gt;import { Button } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;Click Me&lt;/p&gt;
&lt;h2&gt;More Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mdxjs.com/docs/what-is-mdx&quot;&gt;MDX Syntax Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/markdown-content/#markdown-and-mdx-pages&quot;&gt;Astro Usage Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;a href=&quot;https://docs.astro.build/en/reference/directives-reference/#client-directives&quot;&gt;Client Directives&lt;/a&gt; are still required to create interactive components. Otherwise, all components in your MDX will render as static HTML (no JavaScript) by default.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Markdown 语法支持</title><link>https://chitori-blog.vercel.app/blog/markdown-zh</link><guid isPermaLink="true">https://chitori-blog.vercel.app/blog/markdown-zh</guid><description>Markdown 是一种轻量级的「标记语言」。</description><pubDate>Wed, 26 Jul 2023 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;基本语法&lt;/h2&gt;
&lt;p&gt;Markdown 是一种轻量级且易于使用的语法，用于为您的写作设计风格。&lt;/p&gt;
&lt;h3&gt;标题&lt;/h3&gt;
&lt;p&gt;文章内容较多时，可以用标题分段：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# 标题 1

## 标题 2

## 大标题

### 小标题
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;标题预览会打乱文章的结构，所以在此不展示。&lt;/p&gt;
&lt;h3&gt;粗斜体&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;_斜体文本_

**粗体文本**

**_粗斜体文本_**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;斜体文本&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;粗体文本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;粗斜体文本&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;链接&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;文字链接 [链接名称](http://链接网址)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;文字链接 &lt;a href=&quot;http://%E9%93%BE%E6%8E%A5%E7%BD%91%E5%9D%80&quot;&gt;链接名称&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;行内代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;这是一条 `单行代码`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;这是一条 &lt;code&gt;行内代码&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;```js
// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  return fibonacci(n - 1) + fibonacci(n - 2)
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  return fibonacci(n - 1) + fibonacci(n - 2)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当前使用 shiki 作为代码高亮插件，支持的语言请参考 &lt;a href=&quot;https://shiki.matsu.io/languages.html&quot;&gt;shiki / languages&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;行内公式&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;这是一条行内公式 $e^{i\pi} + 1 = 0$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;这是一条行内公式 $e^{i\pi} + 1 = 0$&lt;/p&gt;
&lt;h3&gt;公式块&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} \, dx
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} , dx
$$&lt;/p&gt;
&lt;p&gt;当前使用 KaTeX 作为数学公式插件，支持的语法请参考 &lt;a href=&quot;https://katex.org/docs/supported.html&quot;&gt;KaTeX Supported Functions&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;图片&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;![CWorld](https://gravatar.loli.net/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gravatar.loli.net/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200&quot; alt=&quot;CWorld&quot;&gt;&lt;/p&gt;
&lt;h4&gt;删除线&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;~~删除线~~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;~~删除线~~&lt;/p&gt;
&lt;h3&gt;列表&lt;/h3&gt;
&lt;p&gt;普通无序列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- 1
- 2
- 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1&lt;/li&gt;
&lt;li&gt;2&lt;/li&gt;
&lt;li&gt;3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;普通有序列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;1. GPT-4
2. Claude Opus
3. LLaMa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GPT-4&lt;/li&gt;
&lt;li&gt;Claude Opus&lt;/li&gt;
&lt;li&gt;LLaMa&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;列表里可以继续嵌套语法&lt;/p&gt;
&lt;h3&gt;引用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&gt; 枪响，雷鸣，剑起。繁花血景。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;枪响，雷鸣，剑起。繁花血景。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;引用里也可以继续嵌套语法。&lt;/p&gt;
&lt;h3&gt;换行&lt;/h3&gt;
&lt;p&gt;markdown 分段落是需要空一行的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;如果不空行
就会在一段

第一段

第二段
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;如果不空行
就会在一段&lt;/p&gt;
&lt;p&gt;第一段&lt;/p&gt;
&lt;p&gt;第二段&lt;/p&gt;
&lt;h3&gt;分隔符&lt;/h3&gt;
&lt;p&gt;如果你有写分割线的习惯，可以新起一行输入三个减号&lt;code&gt;---&lt;/code&gt; 或者星号 &lt;code&gt;***&lt;/code&gt;。当前后都有段落时，请空出一行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;高级技巧&lt;/h2&gt;
&lt;h3&gt;行内 HTML 元素&lt;/h3&gt;
&lt;p&gt;目前只支持部分段内 HTML 元素效果，包括 &lt;code&gt;&amp;#x3C;kdb&gt; &amp;#x3C;b&gt; &amp;#x3C;i&gt; &amp;#x3C;em&gt; &amp;#x3C;sup&gt; &amp;#x3C;sub&gt; &amp;#x3C;br&gt;&lt;/code&gt; ，如&lt;/p&gt;
&lt;h4&gt;键位显示&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;使用 &amp;#x3C;kbd&gt;Ctrl&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Alt&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Del&amp;#x3C;/kbd&gt; 重启电脑
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;使用 Ctrl + Alt + Del 重启电脑&lt;/p&gt;
&lt;h4&gt;粗斜体&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;b&gt; Markdown 在此处同样适用，如 _加粗_ &amp;#x3C;/b&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt; Markdown 在此处同样适用，如 &lt;em&gt;加粗&lt;/em&gt; &lt;/p&gt;
&lt;h3&gt;其他 HTML 写法&lt;/h3&gt;
&lt;h4&gt;折叠块&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;details&gt;&amp;#x3C;summary&gt;点击展开&amp;#x3C;/summary&gt;它被隐藏了&amp;#x3C;/details&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;h3&gt;表格&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;| 表头1 | 表头2 |
| ----- | ----- |
| 内容1 | 内容2 |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;| 表头1 | 表头2 |
| ----- | ----- |
| 内容1 | 内容2 |&lt;/p&gt;
&lt;h3&gt;注释&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;在引用的地方使用 [^注释] 来添加注释。

然后在文档的结尾，添加注释的内容（会默认于文章结尾渲染之）。

[^注释]: 这里是注释的内容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;在引用的地方使用 &lt;a href=&quot;%E8%BF%99%E9%87%8C%E6%98%AF%E6%B3%A8%E9%87%8A%E7%9A%84%E5%86%85%E5%AE%B9&quot;&gt;^注释&lt;/a&gt; 来添加注释。&lt;/p&gt;
&lt;p&gt;然后在文档的结尾，添加注释的内容（会默认于文章结尾渲染之）。&lt;/p&gt;
&lt;h3&gt;To-Do 列表&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- [ ] 未完成的任务
- [x] 已完成的任务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 未完成的任务&lt;/li&gt;
&lt;li&gt;[x] 已完成的任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;符号转义&lt;/h3&gt;
&lt;p&gt;如果你的描述中需要用到 markdown 的符号，比如 _ # * 等，但又不想它被转义，这时候可以在这些符号前加反斜杠，如 &lt;code&gt;\_&lt;/code&gt; &lt;code&gt;\#&lt;/code&gt; &lt;code&gt;\*&lt;/code&gt; 进行避免。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;\_不想这里的文本变斜体\_

\*\*不想这里的文本被加粗\*\*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;_不想这里的文本变斜体_&lt;/p&gt;
&lt;p&gt;**不想这里的文本被加粗**&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;内嵌 Astro 组件&lt;/h2&gt;
&lt;p&gt;See &lt;a href=&quot;/docs/integrations/components&quot;&gt;User Components&lt;/a&gt; and &lt;a href=&quot;/docs/integrations/advanced&quot;&gt;Advanced Components&lt;/a&gt; for details.&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.HAXFr_hw.jpg"/><enclosure url="/_astro/thumbnail.HAXFr_hw.jpg"/></item></channel></rss>