[{"content":"女神拒绝我后，C++ 成了我的命中劫 1. 故事开始之前 遥想当年，高考结束以后，很多人都在忙着和自己的女神或者男神表白，试图给青春画上一个浪漫的句号。\n而我，在那个本该热血、暧昧、充满青春滤镜的夏天，也展示出了自己非常独特的一面——\n我压根没去理那个陪伴了我整个青春时期的女神。\n当然，听起来好像挺潇洒。\n实际情况是：我在高考前就已经表白过了，人家压根没搭理我。\n所以严格来说，并不是我没有选择爱情，而是爱情没有选择我。\n于是，高考结束后的我，带着一种“反正也没人理我”的洒脱，开始疯狂游玩。那段时间没有作业，没有考试，也没有早八，人生看起来突然自由得有些不真实。\n但这种自由没有持续太久。\n疯玩了一阵之后，我慢慢感受到了一种很奇怪的空虚。每天好像都很轻松，但又总觉得少了点什么。以前被考试推着走，至少知道自己每天为什么起床；可当那个目标突然消失以后，我反而不知道自己该往哪里走了。\n就在这个时候，我看到了大学实验室开始招新。\n那一刻，我和 C++ 的缘分，算是正式开始了。\n2. 为什么偏偏是 C++ 说实话，我一开始并没有什么特别宏大的目标。\n不是因为我突然立志要成为系统架构师，也不是因为我那时候已经理解了什么叫高性能计算、底层开发、内存模型、工程能力。\n更真实的情况是：我只是觉得，实验室里的东西看起来很酷。\n写代码、跑程序、做项目、调设备，这些事情听起来比单纯上课更接近我想象中的“真正的技术”。而 C++，正好就是那个我绕不开的入口。\n后来接触得多了，我慢慢意识到，C++ 这门语言很特别。\n它不像 Python 那样一上来就很舒服，也不像很多脚本语言那样可以快速看到结果。C++ 经常让人痛苦：指针、引用、内存、对象生命周期、编译链接、模板、构造析构……每一个概念都像是在提醒你：\n你以为你懂了？其实你还差得远。\n但也正是因为这样，C++ 很适合用来训练一个人的工程基本功。\n它会逼着你去理解程序到底是怎么运行的，变量到底放在哪里，内存为什么会泄漏，对象什么时候创建、什么时候销毁，为什么有些代码看起来没问题但运行起来就是崩。\n这种感觉一开始很折磨，但后来我发现，这可能正是我需要补上的东西。\n3. 我现在并不熟练 必须承认，我现在并不是一个很熟练的 C++ 使用者。\n很多东西我还在补。\n比如指针和引用，我以前知道怎么写，但不一定真的知道它们在内存里意味着什么。\n比如 const，我知道它表示“常量”，但遇到 const int*、int* const、const int\u0026amp; 的时候，还是会忍不住停下来想一想。\n比如构造函数、析构函数、拷贝构造、移动语义，这些词看起来都认识，但真要在工程里写对，其实没那么简单。\n再比如 CMake、gdb、Linux 编译链接这些东西，以前总觉得它们只是工具，后来才发现，工程开发里很多问题根本不是算法问题，而是你连程序怎么编译、怎么链接、怎么调试都没搞明白。\n所以这个系列不是“C++ 高手教程”。\n更准确地说，它是一个学习记录。\n我想把自己从“会写一点 C++ 语法”，慢慢推进到“能写比较可靠的 C++ 工程代码”的过程记录下来。\n4. 目录 这个系列会从 C++ 最基础、最容易混乱的概念开始，一步一步往工程化方向推进。\n目前暂定目录如下，后面会根据学习进度不断补充和调整。\n第一部分：C++ 基础概念 变量、地址和指针：我终于理解了指针是什么 引用和指针到底有什么区别 const 到底修饰的是谁 栈和堆到底有什么区别 new/delete 和 malloc/free 有什么区别 第二部分：对象与内存管理 构造函数和析构函数什么时候执行 浅拷贝和深拷贝为什么危险 拷贝构造函数和赋值运算符有什么区别 为什么 C++ 析构函数有时要写 virtual this 指针到底是什么 第三部分：STL 与常用容器 vector 为什么会自动扩容 vector 和 list 到底应该怎么选 map 和 unordered_map 有什么区别 迭代器失效是什么问题 push_back 和 emplace_back 有什么区别 第四部分：编译、调试与工程工具 C++ 项目为什么要分 .h 和 .cpp 一个 C++ 程序从 .cpp 到可执行文件经历了什么 第一次使用 CMake 管理 C++ 项目 第一次用 gdb 调试段错误 静态库和动态库有什么区别 第五部分：小项目实践 用 C++ 写一个命令行小工具 从零写一个简单日志系统 线程池到底解决了什么问题 从零写一个最简单的线程池 一个 C++ 小项目的完整复盘 这些标题不一定一次性全部写完。\n它们更像是一张路线图：我会先从最基础的问题开始，慢慢把 C++ 这条路走清楚。\n5. 我想用什么方式学 C++ 我以前也看过不少教程，但看教程有一个问题：当时好像懂了，过几天又忘了。\n后来我发现，真正能留下来的东西，往往不是“我看懂了什么”，而是“我亲手验证过什么”。\n所以这个系列里，我会尽量按照这样的方式写：\n我为什么学这个问题 我一开始哪里不理解 用一段最小代码复现现象 观察运行结果 解释背后的原因 最后写下我目前的理解 如果还有没搞懂的地方，我也会直接写出来。\n因为学习本来就不是一次性完成的。\n有些概念第一次只能理解 50%，过一段时间写项目遇到了，再回头看，可能才能理解到 80%。再等真正踩坑了，才会突然意识到：原来书上那句话是这个意思。\n6. 为什么要把这些写在 blog 上 一方面，是为了逼自己输出。\n如果只是看视频、看书、写一点零散代码，很容易产生一种“我学过了”的错觉。但真正写成文章时，你会发现很多东西其实没有想清楚。\n你能不能把一个概念讲明白，往往能暴露你到底懂没懂。\n另一方面，我也希望这个 blog 不是一个空荡荡的简历页面，而是一个能看见成长轨迹的地方。\n我不想把自己包装成一个已经很厉害的人。\n我更希望别人在阅读我博客的时候感受到的是：\n这个人选择的方向很有趣诶，看看他都踩过哪些坑，他在前面都有什么经验总结吧\n这对我来说就够了。\n7. 从这里开始 所以，这个 C++ 系列就从这里开始。\n它的起点不是某个宏大的项目，也不是某个听起来很厉害的技术名词。\n它的起点只是一个很普通的问题：\n我想真正把 C++ 学明白。\n从指针、引用、const、栈和堆这些基础概念开始，一点点往前走。\n也许这个过程会很慢，也许中间会反复踩坑，也许有些文章以后回头看会觉得幼稚。\n但没关系。\n至少从这里开始，我不是只是在“学过 C++”。\n而是在认真尝试理解它。\n","date":"2026-05-15T00:00:00+08:00","permalink":"https://Galileo927.github.io/p/cpp-series-preface/","title":"女神拒绝我后，C++ 成了我的命中劫"},{"content":" 本文是 EKF 算法详解系列 的第一篇。\n下一篇：EKF 算法详解：(二) 姿态解算与数学推导\n在机器人、自动驾驶、无人机等领域中，“状态估计”是一个绕不开的核心问题。无论是云台的姿态解算，还是车辆的轨迹跟踪，我们都希望知道系统在当前时刻的准确状态。而在众多状态估计算法中，扩展卡尔曼滤波（Extended Kalman Filter, 简称 EKF） 无疑是最经典、应用最广泛的算法之一。\n本系列文章将带你从零开始，理解 EKF 算法。本文为第一篇，主要介绍 EKF 的基本概念、它与标准卡尔曼滤波的区别，以及它的核心思想。\n1. 什么是卡尔曼滤波 (Kalman Filter)? 在介绍 EKF 之前，我们需要先简单回顾一下标准卡尔曼滤波 (KF)。\n在现实世界中，任何传感器都有噪声（误差），任何物理模型也都无法做到 100% 完美预测。卡尔曼滤波的本质，就是融合“系统模型的预测”与“传感器的实际测量”，通过数学推导，找出一个在统计意义上“最靠谱”的最优估计值。\n卡尔曼滤波主要分为两个不断循环的步骤：\n预测 (Predict)：根据上一时刻的状态和物理模型（如运动学方程：位移 = 速度 × 时间），推算出当前时刻的“先验状态”。 更新 (Update)：读取传感器的测量值，计算测量值与预测值之间的差异（残差），然后通过“卡尔曼增益”来动态决定是更相信模型预测，还是更相信传感器测量，最终得到当前时刻的“后验状态”。 标准 KF 的局限性 标准卡尔曼滤波有一个致命的假设前提：系统必须是线性的。也就是说，状态的转移和观测过程都必须能用线性矩阵相乘来表示。 但在真实世界里，比如角度的旋转涉及正弦/余弦运算（如欧拉角、四元数），或者是复杂的阻力模型，这些都是非线性的。这时候，标准 KF 就会失效。\n2. 为什么需要扩展卡尔曼滤波 (EKF)? 既然现实系统大多是非线性的，我们就需要对标准卡尔曼滤波进行“扩展”——这就是 EKF 诞生的原因。\nEKF 的核心思想非常简单粗暴但又极其有效：既然系统是非线性的，那就在当前工作点附近把它“局部线性化”。\n具体来说，EKF 利用了高等数学中的泰勒展开（Taylor Expansion）。它将非线性的状态转移函数和观测函数在当前的最优估计值处进行一阶泰勒展开，忽略掉高阶项。这样一来，复杂的非线性函数就被近似成了一个线性模型（即求导数，得到雅可比矩阵 Jacobian）。\nEKF 的优势： 适用性广：完美解决了非线性系统的状态估计问题（例如机器人的视觉位姿追踪）。 计算效率高：相比于粒子滤波（PF）或无迹卡尔曼滤波（UKF），EKF 只需要计算一阶雅可比矩阵，计算量较小，非常适合部署在算力受限的嵌入式设备（如各类单片机、STM32 等）上。 3. EKF 算法的通用五步公式 一个标准的 EKF 同样遵循“预测”和“更新”两大阶段，具体的数学公式通常被归纳为以下经典的五步：\n预测阶段 (Predict) 状态预测：根据非线性运动方程推算先验状态。 $$ \\hat{x}_k^- = f(\\hat{x}_{k-1}, u_{k-1}) $$ 协方差预测：通过状态转移的雅可比矩阵 \\( F \\)，更新预测误差协方差矩阵。 $$ P_k^- = F P_{k-1} F^T + Q $$ 更新阶段 (Update) 计算卡尔曼增益：利用观测模型的雅可比矩阵 \\( H \\) 计算增益 \\( K \\)。 $$ K_k = P_k^- H^T (H P_k^- H^T + R)^{-1} $$ 状态更新：根据传感器实际测量值 \\( z_k \\) 与预测值的残差，修正状态，得到后验最优估计。 $$ \\hat{x}_k = \\hat{x}_k^- + K_k (z_k - h(\\hat{x}_k^-)) $$ 协方差更新：降低不确定度，更新协方差矩阵供下一轮使用。 $$ P_k = (I - K_k H) P_k^- $$ (注：这里的 \\( Q \\) 是过程噪声协方差，\\( R \\) 是测量噪声协方差，它们通常在实际工程中作为“超参数”根据经验进行调参。)\n4. 结语与预告 EKF 虽然在数学表达上看起来有些劝退，但只要理解了它**“线性化”与“加权融合”**的核心思想，就能在工程中游刃有余。\n在许多实际开源项目（如 RoboMaster 的视觉算法）中，EKF 常被用于追踪装甲板的运动或云台的姿态。在处理三维空间的旋转时，我们为了避免“万向节死锁”和保持约束，通常会引入四元数与误差状态 (Error-State)。\n在下一篇文章中，我们将直接进入硬核部分：结合机器人的视觉姿态解算场景，详细推导基于四元数和误差状态的 EKF 数学模型！\n","date":"2026-03-12T00:00:00+08:00","permalink":"https://Galileo927.github.io/p/ekf-introduction/","title":"EKF 算法详解：(一) 简介与基础"},{"content":" 本文是 C++ 学习记录系列 的第二篇。\n上一篇：女神拒绝我后，C++ 成了我的命中劫\n这个事情还得从疫情期间说起，当时我正上高二，因为在学校旁边租了房子所以不怎么去上晚自习。母上大人作为家委会成员和班主任的关系还很不错（这个家委会的职位纯粹是想和老师打好关系在学校监视我才做的）。有一天晚上班主任给我妈打电话了说我们班有一个密切和患者接触过的人，于是我妈让我从家出发去班里通知————开（放）始（假）网（回）课（家）。正如我所说的，租的房子距离学校很近，所以我五分钟就到了给他们宣布了这好消息。但是我依旧等了很长时间————当时不只我一个人走读，还有同学正在回来取书的路上。\n我当时就在教室里等待着我的同桌————看过前言的朋友应该都知道这是我对我同桌是什么意思了————总而言之，等到她来了以后，我就做了青春期小男生都会做的事情，我开始尽可能找话题聊天。\n走运的是我确实要到了她的联系方式，但是我真不知道人家的地址————友情提醒，在任何情况下贸然了解非熟人的地址都是一件涉及隐私的行为，慎行。但是请不要看到这就说我 Galileo 标题党，恰恰相反，虽然有了联系方式，但是我在最后还不如没有呢！想知道后面发生了什么，请你耐心地读到到后面的 C++ 系列吧。:）\n不过回到 C++，情况就简单多了。\n现实里不能随便问人家的地址，但程序里的变量很大方：只要你写一个 \u0026amp;，它就会把自己的内存地址交出来。\n这篇文章要讲的指针，本质上就是在回答一个问题：如果我知道了一个变量的地址，我到底能做什么？\n1. 先从变量开始 在理解指针之前，先要理解一个普通变量在程序里至少包含几层信息：\n变量名：代码里用来访问它的名字 变量类型：决定这块数据应该如何解释 变量值：变量当前保存的数据 变量地址：变量在内存中的位置 比如下面这段代码：\n#include \u0026lt;iostream\u0026gt; int main() { int a = 10; std::cout \u0026lt;\u0026lt; \u0026#34;a = \u0026#34; \u0026lt;\u0026lt; a \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;\u0026amp;a = \u0026#34; \u0026lt;\u0026lt; \u0026amp;a \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return 0; } 一次可能的运行结果：\na = 10 \u0026amp;a = 0x16b7fad4c 这里的 a 表示变量的值，也就是 10。\n而 \u0026amp;a 表示变量 a 的地址，也就是它在内存中的位置。\n需要注意的是，地址每次运行都可能不一样。我们不需要记住某个具体地址，只需要理解：变量除了有值之外，也有自己的内存地址。\n如果继续用前面的故事类比，变量名有点像“我在代码里怎么称呼它”，变量值是它当前保存的内容，而变量地址才是它在内存里的具体位置。\n联系方式和地址不是一回事，变量名和变量地址也不是一回事。\n2. 指针到底是什么 指针本质上也是一个变量。\n只不过普通变量保存的是普通数据，而指针变量保存的是地址。\n也就是说，指针不是变量本身，而是一个“记着变量地址的小纸条”。\n（啊这让我想到了我的好homie，当时他也在追一个姑娘，可以说死缠烂打，终于又一次在晚自习时写在纸条上的表白被答应了，但是刚答应，纸条就被班主任收走看见了，从严格意义上来讲，班主任是第一个知道女生小心思的人，纸条返回的地址输出给班主任了，他没看到）\n纸条上写着地址，你顺着地址才能找到真正的数据。\n#include \u0026lt;iostream\u0026gt; int main() { int a = 10; int* p = \u0026amp;a; std::cout \u0026lt;\u0026lt; \u0026#34;a = \u0026#34; \u0026lt;\u0026lt; a \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;\u0026amp;a = \u0026#34; \u0026lt;\u0026lt; \u0026amp;a \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;p = \u0026#34; \u0026lt;\u0026lt; p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;*p = \u0026#34; \u0026lt;\u0026lt; *p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return 0; } 一次可能的运行结果：\na = 10 \u0026amp;a = 0x16d7dad4c p = 0x16d7dad4c *p = 10 这段代码里有三个符号特别关键：\n\u0026amp;a：取出变量 a 的地址 p：保存变量 a 的地址 *p：根据 p 里保存的地址，找到对应变量并访问它的值 所以这行代码：\nint* p = \u0026amp;a; 可以理解为：\n创建一个 int* 类型的指针变量 p，让它保存 a 的地址。\n这里的 int* 表示：p 是一个指针，它指向的对象类型是 int。\n所以标题里说“变量给了我地址”，放到代码里就是这句：\nint* p = \u0026amp;a; a 把地址交出来，p 负责把这个地址保存好。\n3. 通过指针修改变量 因为指针保存的是变量地址，所以我们可以通过指针找到原来的变量，并修改它。\n#include \u0026lt;iostream\u0026gt; int main() { int a = 10; int* p = \u0026amp;a; *p = 20; std::cout \u0026lt;\u0026lt; \u0026#34;a = \u0026#34; \u0026lt;\u0026lt; a \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;*p = \u0026#34; \u0026lt;\u0026lt; *p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return 0; } 运行结果：\na = 20 *p = 20 关键点在这一句：\n*p = 20; 它不是在修改指针变量 p 本身，而是在修改 p 指向的那块内存。\n也就是说：\np = \u0026amp;a; 表示让 p 保存 a 的地址。\n而：\n*p = 20; 表示通过 p 找到 a，然后把 a 改成 20。\n这里最容易误会的是：p 只是保存地址的变量，真正被改的是地址指向的对象。\n打个不太浪漫但是很准确的比方：你改的不是纸条上的地址，而是按地址找到房间以后，改了房间里的东西。\n4. 指针自己也有地址 指针是变量，所以指针自己也会占内存，也有自己的地址。\n#include \u0026lt;iostream\u0026gt; int main() { int a = 10; int* p = \u0026amp;a; std::cout \u0026lt;\u0026lt; \u0026#34;\u0026amp;a = \u0026#34; \u0026lt;\u0026lt; \u0026amp;a \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;p = \u0026#34; \u0026lt;\u0026lt; p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;\u0026amp;p = \u0026#34; \u0026lt;\u0026lt; \u0026amp;p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return 0; } 一次可能的运行结果：\n\u0026amp;a = 0x16b5dad4c p = 0x16b5dad4c \u0026amp;p = 0x16b5dad40 这里很容易混：\np 是指针变量的值，它保存的是 a 的地址 \u0026amp;p 是指针变量 p 自己的地址 所以可以把它拆成两层看：\np 这个变量自己住在一个地址里，而它里面保存的值又是另一个变量的地址。\n换成刚才的小纸条类比就是：纸条上写着 a 的地址，但纸条本身也得放在某个地方。\np 是纸条上写的内容，\u0026amp;p 是这张纸条自己的位置。\n这可不要弄混，不然就会像我的学长一样，本来是想加p学姐和人家认识认识聊聊人生，但是加成了\u0026amp;p学姐，这下好了，他用了一个月发现了error，但是你的报错可是会瞬间给你一巴掌。\n5. 指针类型为什么重要 指针保存的是地址，但 C++ 里的指针不只是一个地址。\n它还带着类型信息。\n比如 int* 表示这个指针指向的是 int，double* 表示这个指针指向的是 double。\n类型至少影响两件事：\n解引用时，编译器应该按什么类型读取数据 指针做加法时，地址应该移动多少字节 看下面这个例子：\n#include \u0026lt;iostream\u0026gt; int main() { int arr[3] = {10, 20, 30}; int* p = arr; std::cout \u0026lt;\u0026lt; \u0026#34;p = \u0026#34; \u0026lt;\u0026lt; p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;p + 1 = \u0026#34; \u0026lt;\u0026lt; p + 1 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;*p = \u0026#34; \u0026lt;\u0026lt; *p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;*(p+1) = \u0026#34; \u0026lt;\u0026lt; *(p + 1) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;sizeof(int) = \u0026#34; \u0026lt;\u0026lt; sizeof(int) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return 0; } 一次可能的运行结果：\np = 0x16f9dad40 p + 1 = 0x16f9dad44 *p = 10 *(p+1) = 20 sizeof(int) = 4 如果 int 占 4 个字节，那么 p + 1 不是让地址加 1，而是让地址向后移动 1 个 int 的大小，也就是 4 个字节。\n所以指针加法不是简单的数字加法，而是和指针指向的类型有关。\n这就像只知道“地址”还不够，你还得知道那里住的到底是什么类型的对象。\n如果 p 指向的是 int，那往后走一步就是下一个 int；如果指向的是 double，往后走一步就要按 double 的大小来走。\nC++ 不会只问“你要去哪”，它还会问“你要按什么规格走”。\n6. 空指针 nullptr 如果一个指针暂时不指向任何有效对象，应该让它等于 nullptr。\n如果暂时没有地址，就老老实实说没有地址。\n最怕的是明明不知道指向哪里，还装作自己知道。\n#include \u0026lt;iostream\u0026gt; int main() { int* p = nullptr; if (p == nullptr) { std::cout \u0026lt;\u0026lt; \u0026#34;p does not point to anything now\\n\u0026#34;; } return 0; } nullptr 表示空指针。\n它不是一个有效对象的地址，所以不能对空指针解引用：\nint* p = nullptr; // 错误示范：不要这样写 std::cout \u0026lt;\u0026lt; *p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; 对空指针解引用会导致未定义行为，程序可能直接崩溃。\n这件事也很好理解：你手里没有地址，却非要按地址去找东西，那程序只能当场迷路。\nnullptr 的意义不是让你去访问它，而是提醒你：这个指针现在还没有目标。\n7. 指针作为函数参数 指针常见的用途之一，是把某个变量的地址传给函数，让函数可以修改外面的变量。\n这就开始有点像“把地址告诉别人，让别人能找到原来的东西”了。\n普通传值只是给别人一份复印件；传指针则是把原对象的位置告诉别人。\n先看普通传值：\n#include \u0026lt;iostream\u0026gt; void changeByValue(int x) { x = 100; } int main() { int a = 10; changeByValue(a); std::cout \u0026lt;\u0026lt; \u0026#34;a = \u0026#34; \u0026lt;\u0026lt; a \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return 0; } 运行结果：\na = 10 这里 changeByValue 拿到的是 a 的一份拷贝，函数内部修改的是拷贝，不会影响外面的 a。\n再看传指针：\n#include \u0026lt;iostream\u0026gt; void changeByPointer(int* p) { if (p == nullptr) { return; } *p = 100; } int main() { int a = 10; changeByPointer(\u0026amp;a); std::cout \u0026lt;\u0026lt; \u0026#34;a = \u0026#34; \u0026lt;\u0026lt; a \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return 0; } 运行结果：\na = 100 这里传进去的是 a 的地址。\n函数内部通过 *p = 100; 找到外面的 a，并修改它。\n这也是很多 C 风格接口喜欢传指针的原因：函数可以通过地址访问调用者传进来的对象。\n所以传指针的威力更大，责任也更大。\n你把地址交出去了，函数就不只是看看，它是真的可能改到原来的对象。\n8. 几个我踩过的坑 8.1 未初始化指针 int* p; // 错误示范：p 没有被初始化，不知道指向哪里 *p = 10; 未初始化的指针里可能是任意地址。\n直接解引用它非常危险。\n这就像手里拿了一张没写清楚的纸条，却硬说它是情书，上面记了人家要和你表白的地址。\n你顺着它走，走到哪里完全看运气，而写程序最不应该靠运气，感情也是，万一那是年级主任钓鱼用的呢。\n如果暂时不知道指向谁，先写成：\nint* p = nullptr; 你要是写情书的话另说，这东西不兴有模版或者定义之类的。\n8.2 空指针解引用 int* p = nullptr; // 错误示范 std::cout \u0026lt;\u0026lt; *p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; nullptr 不指向任何有效对象，不能通过 *p 访问。\n它至少比未初始化指针诚实：未初始化指针是不知道自己去哪还乱跑，nullptr 是明确告诉你“我现在哪也不去”。\n8.3 悬空指针 int* p = nullptr; { int a = 10; p = \u0026amp;a; } // 错误示范：a 的生命周期已经结束 std::cout \u0026lt;\u0026lt; *p \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; a 是代码块里的局部变量。\n当代码块结束后，a 的生命周期结束，p 还保存着原来的地址，但那个地址对应的对象已经不存在了。\n这种指针叫悬空指针。\n这个坑很像你保存了一个旧地址，但那个人早就搬走了。\n地址看起来还像个地址，但你再按它去找，找到的已经不是原来的对象。\n9. 总结 这一篇先抓住几句话就够了：\n变量有值，也有地址 \u0026amp;a 表示取出变量 a 的地址 指针变量保存的是地址 *p 表示根据指针保存的地址访问对象 指针类型会影响解引用和指针运算 不确定指向谁的指针，先初始化为 nullptr 类比一下就是：\n变量名像称呼，变量值像当前内容，变量地址像它在内存里的位置，指针像一张专门保存地址的纸条。\n但 C++ 比现实世界更严格：你可以拿地址，也可以顺着地址改东西，可一旦地址是假的、空的、过期的，程序就会用崩溃或者未定义行为提醒你，这事不能乱来。尤其是当女生给你了假的联系方式的时候（当然人家肯定没给我假的，不然也没后面的事了）\n如果把这几个点想清楚，后面再看引用、数组、动态内存、对象生命周期，都会顺很多。\n","date":"2026-05-17T00:00:00+08:00","permalink":"https://Galileo927.github.io/p/cpp-pointer-intro/","title":"她没给我联系方式，但变量给了我地址：C++ 指针入门"},{"content":" 本文是 EKF 算法详解系列 的第二篇。\n上一篇：EKF 算法详解：(一) 简介与基础 | 下一篇：EKF 算法详解：(三) 源码解析实现\n在实际的机器人（如 RoboMaster）视觉和云台控制中，扩展卡尔曼滤波（EKF）经常被用于姿态解算（Attitude Estimation）。在本篇文章中，我们将结合实际项目中的四元数（Quaternion）状态，深入推导 EKF 的核心数学模型。\n1. 为什么使用四元数与误差状态？ 在三维空间中表示旋转，欧拉角容易遇到“万向节死锁”问题，旋转矩阵则需要维护 9 个元素且保持正交约束。四元数 (Quaternion) 只需要 4 个元素，且计算效率高，是姿态表示的最佳选择。\n然而，传统的 EKF 难以直接应用于四元数，因为四元数必须满足单位长度约束（\\( ||q|| = 1 \\)）。如果直接对四元数进行线性加减（如 \\( q = q + \\Delta q \\)），会破坏其模长。\n因此，在实际工程中，我们通常采用 误差状态卡尔曼滤波（Error-State Kalman Filter, ESKF） 或称乘性 EKF（MEKF）：\n名义状态 (Nominal State)：用四元数 \\( q \\) 表示，通过乘法进行更新：\\( q_{new} = q \\otimes \\Delta q \\) 误差状态 (Error State)：用一个三维向量（误差角 \\( \\delta \\theta \\)）表示，其协方差矩阵 \\( P \\) 为 3x3 的矩阵。 2. 状态预测方程 (Prediction) 假设我们已知当前的角速度 \\( \\omega \\) 和角加速度 \\( \\alpha \\)，在时间间隔 \\( \\Delta t \\) 内，角度的变化量 \\( \\Delta \\theta \\) 可以近似表示为： $$ \\Delta \\theta \\approx (\\omega + \\alpha \\cdot \\Delta t) \\Delta t $$根据角度变化量 \\( \\Delta \\theta \\)，我们可以构造出一个微小旋转的四元数更新量 \\( q_{update} \\)。 设 \\( \\theta = ||\\Delta \\theta|| \\) 为旋转角，旋转轴 \\( u = \\frac{\\Delta \\theta}{\\theta} \\)：\n如果旋转角极小（如 \\( \\theta \u003c 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} $$ 如果角度较大，则严格按照四元数轴角公式： $$ q_{update} = \\begin{bmatrix} \\cos(\\frac{\\theta}{2}) \\\\\\\\ u \\sin(\\frac{\\theta}{2}) \\end{bmatrix} $$ 最终预测的四元数状态为： $$ \\hat{q}_{k} = q_{k-1} \\otimes q_{update} $$3. 雅可比矩阵的计算 (Jacobian) 在误差状态模型中，我们需要计算误差状态相对于角速度和旋转的雅可比矩阵 \\( J \\)（部分实现中作为观测矩阵或状态转移矩阵的近似）。\n定义角速度 \\( \\omega \\) 的反对称矩阵（Skew-symmetric matrix）为 \\( [\\omega]_\\times \\)： $$ [\\omega]_\\times = \\begin{bmatrix} 0 \u0026 -\\omega_z \u0026 \\omega_y \\\\\\\\ \\omega_z \u0026 0 \u0026 -\\omega_x \\\\\\\\ -\\omega_y \u0026 \\omega_x \u0026 0 \\end{bmatrix} $$一阶近似下，误差状态转移的雅可比矩阵可以表示为： $$ J = I_{3\\times3} + \\frac{1}{2} \\Delta t [\\omega]_\\times $$4. 状态更新方程 (Update) 当新的测量值（例如视觉 PnP 解算得到的四元数测量值 \\( q_{meas} \\)）到来时，我们需要计算预测值 \\( \\hat{q}_k \\) 与测量值的残差（Residual）： $$ q_{res} = q_{meas} \\otimes \\hat{q}_k^{-1} $$ 我们提取这个残差四元数的虚部向量部分 \\( z_{res} = [q_{res}.x, q_{res}.y, q_{res}.z]^T \\) 作为测量残差向量。\n接着，计算卡尔曼增益 \\( K \\)。假设我们提取的雅可比矩阵为 \\( J \\)，系统误差协方差为 \\( P \\)，测量噪声协方差为 \\( R \\)： $$ K = P J^T (J P J^T + R)^{-1} $$随后将增益乘上残差，得到误差角度修正量： $$ K_z = K \\cdot z_{res} $$最后，利用这个三维修正量构造误差四元数，乘到预测状态上，并更新协方差矩阵 \\( P \\)： $$ q_{new} = \\hat{q}_k \\otimes \\begin{bmatrix} 1 \\\\\\\\ K_z.x \\\\\\\\ K_z.y \\\\\\\\ K_z.z \\end{bmatrix} $$ $$ P_{new} = (I_{3\\times3} - K J) P $$以上就是一套完整用于姿态跟踪的轻量级 EKF/ESKF 数学推导模型。在下一篇文章中，我们将直接拆解 C++ 落地代码，看看这些公式是如何变成实际运行的程序的！\n","date":"2026-03-12T00:00:00+08:00","permalink":"https://Galileo927.github.io/p/ekf-math/","title":"EKF 算法详解：(二) 姿态解算与数学推导"},{"content":" 本文是 EKF 算法详解系列 的第三篇。\n上一篇：EKF 算法详解：(二) 姿态解算与数学推导\n在上一篇文章中，我们详细推导了基于四元数和误差状态的 EKF 数学模型。理论推导无论多么完美，最终都需要落地成代码。本文我们将结合一个实际项目的源码（基于 C++ 和 Eigen 库），带你一行行看懂 EKF 的实现细节。\n1. 类的定义与初始化 (ekf.hpp) 在 C++ 中，我们通常会定义一个 ekf 类来维护卡尔曼滤波器的状态和协方差矩阵。这里我们使用开源且极其强大的矩阵运算库 Eigen。\n#ifndef TOOLS__EKF_HPP #define TOOLS__EKF_HPP #include \u0026lt;Eigen/Geometry\u0026gt; #include \u0026lt;eigen3/Eigen/src/Geometry/Quaternion.h\u0026gt; #include \u0026lt;iostream\u0026gt; namespace tools { class ekf { public: ekf(); ~ekf(); // 预测步骤：根据角速度和角加速度预测下一个四元数状态 Eigen::Quaterniond predict(const Eigen::Quaterniond \u0026amp;q, const Eigen::Vector3d \u0026amp;omega, const Eigen::Vector3d \u0026amp;alpha, float dt); // 计算雅可比矩阵 Eigen::Matrix3d calculate_jacobian(const Eigen::Quaterniond \u0026amp;q, const Eigen::Vector3d \u0026amp;omega, float dt); // 核心更新步骤 void ekf_update(Eigen::Quaterniond \u0026amp;q, const Eigen::Vector3d \u0026amp;omega, Eigen::Vector3d \u0026amp;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 在这里，值得注意的是协方差矩阵 P 和 R 都是 $3 \\times 3$ 的矩阵。这就是我们在上一篇中提到的：状态虽为四元数（4维），但为了满足归一化约束，我们对**误差角（3维）**进行协方差维护。\n2. 状态预测函数：predict 预测函数的任务是通过当前的角速度 $\\omega$ 和角加速度 $\\alpha$ 积分，求出这 $\\Delta t$ 时间内的微小旋转，然后更新四元数。\nEigen::Quaterniond ekf::predict(const Eigen::Quaterniond \u0026amp;q, const Eigen::Vector3d \u0026amp;omega, const Eigen::Vector3d \u0026amp;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 \u0026lt; 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; } 3. 雅可比矩阵：calculate_jacobian 这里构建了反对称矩阵（Skew-symmetric matrix），用于计算雅可比矩阵 $J$：\nEigen::Matrix3d ekf::calculate_jacobian(const Eigen::Quaterniond \u0026amp;q, const Eigen::Vector3d \u0026amp;omega, float dt) { Eigen::Matrix3d j; Eigen::Matrix3d omega_skew; // 构造角速度的反对称矩阵 omega_skew \u0026lt;\u0026lt; 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; } 4. EKF 核心更新逻辑：ekf_update 这个函数是整个滤波器的灵魂所在，它结合了前两个函数，完成“预测-计算增益-修正-更新协方差”的闭环。\nvoid ekf::ekf_update(Eigen::Quaterniond \u0026amp;q, const Eigen::Vector3d \u0026amp;omega, Eigen::Vector3d \u0026amp;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; } 总结 这段代码是一个非常轻量且实用的工程化 EKF 实现，它舍弃了标准 EKF 中过于繁琐的矩阵运算（例如舍弃了复杂的高维状态转移阵，精简了观测矩阵提取的步骤），直接针对“四元数位姿跟踪”这一特定场景进行了极致优化。通过协方差降维和一阶近似泰勒展开，使得这套算法可以在算力有限的嵌入式设备（如各类机器人的下位机/视觉板）上高速运行。\n至此，我们的 EKF 算法详解系列就全部结束了！希望这三篇文章能帮你从概念到数学，再到代码落地，彻底打通 EKF 的任督二脉。\n","date":"2026-03-12T00:00:00+08:00","permalink":"https://Galileo927.github.io/p/ekf-code/","title":"EKF 算法详解：(三) 源码解析实现"},{"content":"最近在和我一个学长做项目。因为十分贫穷，加上校内服务器十分堵塞，压根排不到我们，最后我们决定上网租服务器（虽然发现这个决策使得我们的钱包更加不饱满）。这个时候发现学长并不熟悉 Linux 的一些常用指令，这也启发我给自己的 blog 加上一篇关于常用指令的推文。\nLinux 的很多操作都可以通过命令行完成。刚开始接触时，不需要一次性记住所有命令，先掌握目录切换、文件操作、文件内容查看、权限管理和进程查看这几类常用指令，就可以完成大部分日常工作。\n本文记录一些入门阶段最常用的 Linux 指令，方便之后查阅。\n1. 查看当前所在位置 pwd pwd 会显示当前终端所在的目录路径。例如：\n/home/user/project 当你不知道自己现在在哪个文件夹时，可以先运行这个命令。\n2. 查看目录内容 ls 常见用法：\nls # 查看当前目录下的文件 ls -l # 以详细列表形式显示 ls -a # 显示隐藏文件 ls -lh # 以更易读的大小单位显示 在 Linux 中，以 . 开头的文件或目录是隐藏文件，例如 .git、.bashrc。\n3. 切换目录 cd 目录名 常见用法：\ncd /home/user # 切换到指定绝对路径 cd project # 进入当前目录下的 project 文件夹 cd .. # 返回上一级目录 cd ~ # 回到当前用户的家目录 cd - # 回到上一次所在目录 绝对路径从 / 开始，相对路径从当前目录开始。\n4. 创建文件和目录 创建目录：\nmkdir notes 递归创建多级目录：\nmkdir -p projects/linux/test 创建空文件：\ntouch hello.txt touch 如果遇到不存在的文件，会创建它；如果文件已经存在，会更新它的修改时间。\n5. 复制、移动和重命名 复制文件：\ncp source.txt target.txt 复制目录：\ncp -r old_dir new_dir 移动文件：\nmv file.txt /tmp/ 重命名文件：\nmv old_name.txt new_name.txt mv 既可以移动文件，也可以重命名文件，具体取决于目标路径。\n6. 删除文件和目录 删除文件：\nrm file.txt 删除目录：\nrm -r folder 强制删除：\nrm -rf folder rm -rf 很危险，它不会进入回收站，执行前一定要确认路径是否正确。\n7. 查看文件内容 一次性输出文件内容：\ncat file.txt 分页查看长文件：\nless file.txt 查看文件开头：\nhead file.txt head -n 20 file.txt 查看文件末尾：\ntail file.txt tail -n 20 file.txt 实时查看日志：\ntail -f app.log 查看程序日志时，tail -f 非常常用。\n8. 搜索文件内容 grep \u0026#34;keyword\u0026#34; file.txt 常见用法：\ngrep \u0026#34;error\u0026#34; app.log grep -n \u0026#34;error\u0026#34; app.log # 显示行号 grep -i \u0026#34;error\u0026#34; app.log # 忽略大小写 grep -r \u0026#34;TODO\u0026#34; . # 在当前目录递归搜索 如果只记一个搜索命令，先记住 grep -r \u0026quot;关键词\u0026quot; .。\n9. 查找文件 按文件名查找：\nfind . -name \u0026#34;*.md\u0026#34; 查找当前目录下所有 Markdown 文件：\nfind . -type f -name \u0026#34;*.md\u0026#34; 查找目录：\nfind . -type d -name \u0026#34;build\u0026#34; find 的第一个参数表示从哪里开始找，. 表示当前目录。\n10. 查看磁盘和文件大小 查看磁盘空间：\ndf -h 查看当前目录下各文件和目录大小：\ndu -sh * 查看某个目录总大小：\ndu -sh folder 其中 -h 表示 human-readable，会用 KB、MB、GB 这种更容易理解的单位显示。\n11. 查看和结束进程 查看当前用户的进程：\nps 查看所有进程：\nps aux 动态查看系统资源：\ntop 结束某个进程：\nkill PID 如果进程无法正常结束，可以使用：\nkill -9 PID PID 是进程编号，可以通过 ps aux 或 top 找到。\n12. 权限相关命令 查看权限：\nls -l 给脚本添加可执行权限：\nchmod +x script.sh 修改文件拥有者：\nsudo chown user:user file.txt sudo 表示用管理员权限执行命令。使用 sudo 前要确认命令确实需要更高权限。\n13. 压缩和解压 打包并压缩：\ntar -czvf archive.tar.gz folder 解压：\ntar -xzvf archive.tar.gz 常见参数含义：\nc: create，创建压缩包 x: extract，解压 z: 使用 gzip v: 显示过程 f: 指定文件名 14. 网络相关命令 测试网络连通：\nping github.com 下载文件：\ncurl -O https://example.com/file.zip 查看当前机器 IP：\nip addr 在 macOS 上，类似命令是：\nifconfig 15. 常用组合 进入目录并查看内容：\ncd project ls -lh 查找日志中的错误：\ngrep -n \u0026#34;error\u0026#34; app.log 查看某个目录大小：\ndu -sh . 查看最近的日志输出：\ntail -f app.log 查找当前目录下所有 Python 文件：\nfind . -type f -name \u0026#34;*.py\u0026#34; 总结 Linux 命令入门可以先按使用场景记忆：\n位置和目录：pwd、ls、cd 文件操作：mkdir、touch、cp、mv、rm 内容查看：cat、less、head、tail 搜索查找：grep、find 系统状态：df、du、ps、top 权限管理：chmod、chown、sudo 这些命令不需要死记硬背，最好的方式是在真实项目和日常操作里反复使用。\n","date":"2026-05-11T17:46:51+08:00","permalink":"https://Galileo927.github.io/p/linux-basic-commands/","title":"Linux常用指令入门"},{"content":"GitHub 是一个面向开源及私有软件项目的托管平台，因为只支持 Git 作为唯一的版本库格式进行托管，故名 GitHub。它不仅是代码仓库，还是一个强大的协作工具。本文将介绍一些 GitHub 的基本使用方法。\n准备工作：安装 Git 在开始使用 GitHub 之前，你的电脑上需要安装 Git。Git 是一个免费、开源的分布式版本控制系统，是与 GitHub 交互的基础。\n你可以从 Git 官网 下载适合你操作系统的安装包。安装过程通常很简单，保持默认设置即可。\n安装完成后，你可以在终端（Windows 上的 PowerShell 或 CMD，macOS/Linux 上的 Terminal）中输入以下命令来验证 Git 是否安装成功：\ngit --version 如果能看到版本号，说明 Git 已经成功安装。\n核心概念 在开始使用 GitHub 之前，了解一些核心概念非常重要：\n仓库 (Repository): 你的项目存放的地方。可以想象成一个项目的文件夹，里面包含了项目的所有文件和修订历史。 克隆 (Clone): 将远程仓库复制到你的本地计算机上。 提交 (Commit): 将你的代码更改保存到本地仓库。每次提交都有一个唯一的 ID 和一条描述信息。 推送 (Push): 将本地仓库的提交上传到远程仓库（例如 GitHub）。 拉取 (Pull): 从远程仓库获取最新的更改并合并到你的本地仓库。 分支 (Branch): 为了在不影响主线（通常是 main 或 master 分支：这几年估计都改成了main，说是master有奇怪的隐喻😅）开发的情况下进行功能开发或修复 bug，你可以创建一个分支。开发完成后，再将分支合并到主线。 拉取请求 (Pull Request / PR): 当你希望将你的分支合并到另一个分支时，你可以创建一个拉取请求。这是一个请求审查和讨论你的代码更改的地方。 合并 (Merge): 将一个分支的更改合并到另一个分支。 基本工作流程 1. 创建一个新的仓库 登录你的 GitHub 账户。 点击右上角的 \u0026ldquo;+\u0026rdquo; 图标，然后选择 \u0026ldquo;New repository\u0026rdquo;。 为你的仓库命名，添加一个可选的描述。 选择 \u0026ldquo;Public\u0026rdquo;（公开）或 \u0026ldquo;Private\u0026rdquo;（私有）。 你可以选择使用 README 文件、.gitignore 文件和许可证来初始化仓库。 点击 \u0026ldquo;Create repository\u0026rdquo;。 2. 克隆仓库到本地 要对仓库进行更改，你需要先把它克隆到你的电脑上。\ngit clone https://github.com/your-username/your-repository.git 将 your-username 和 your-repository 替换为你的 GitHub 用户名和仓库名。\n3. 添加和提交更改 在本地对项目文件进行修改后，你需要将这些更改提交到本地仓库。\n# 进入仓库目录 cd your-repository # 查看文件状态 git status # 添加所有更改的文件到暂存区 git add . # 提交更改，并附上描述信息 git commit -m \u0026#34;你的提交信息，例如：添加了新功能\u0026#34; 4. 推送更改到 GitHub 提交到本地仓库后，你需要将这些更改推送到 GitHub 上的远程仓库。\ngit push origin main (main 可能是你的主分支名，也可能是 master)\n分支与协作 分支是 GitHub 协作的核心。\n1. 创建并切换到新分支 # 创建一个名为 feature-x 的新分支并切换过去 git checkout -b feature-x 2. 推送新分支到 GitHub 在你的新分支上进行提交后，将该分支推送到远程仓库。\ngit push origin feature-x 3. 创建拉取请求 (Pull Request) 在 GitHub 上你的仓库页面，你会看到一个提示，可以为 feature-x 分支创建一个拉取请求。 点击 \u0026ldquo;Compare \u0026amp; pull request\u0026rdquo;。 填写标题和描述，说明你的更改内容。 点击 \u0026ldquo;Create pull request\u0026rdquo;。 之后，项目维护者可以审查你的代码，提出修改意见，最后将其合并到主分支中。\nGitHub Pages GitHub Pages 是 GitHub 提供的一个静态网站托管服务。你可以用它来托管你的个人博客、项目文档等。\n确保你的仓库中有一个 index.html 文件。 进入仓库的 \u0026ldquo;Settings\u0026rdquo; 页面。 在左侧菜单中选择 \u0026ldquo;Pages\u0026rdquo;。 在 \u0026ldquo;Source\u0026rdquo; 部分，选择你想要部署的分支（通常是 main）。 保存后，GitHub 会为你的网站生成一个 URL，格式通常是 https://your-username.github.io/your-repository/。 希望这篇指南能帮助你更好地开始使用 GitHub！\n","date":"2025-11-12T12:16:19+08:00","permalink":"https://Galileo927.github.io/p/about-github/","title":"关于github的一些使用方法"},{"content":"Windows下ubuntu双系统的安装 前言 我们IST实验室选择了Ubuntu作为视觉工作的系统，通常使用22.04版本。详细安装流程见下文。\n如果你想生动地了解如何下载，可以去这个网址 B站教学 看视频学习。\n准备工作 一台已安装 Windows 的电脑 至少 100GB 的可用磁盘空间 一个 8GB 及以上的 U盘（用于制作 Ubuntu 启动盘） Ubuntu 镜像文件（可从官网 https://ubuntu.com/download 获取） 备份重要数据，防止操作失误导致数据丢失 有问题时不用慌，先去看一下后文的常见问题有没有解决方案，也可以询问AI Windows 分区调整 打开“磁盘管理”工具（Win+X → 磁盘管理）。 右键 C 盘或其他分区，选择“压缩卷”，为 Ubuntu 预留空间（建议 20GB 以上）。 压缩后会出现“未分配空间”，无需新建分区，安装时由 Ubuntu 自动分配。 制作 Ubuntu 启动盘 下载并安装 Rufus 工具（官网地址）。 插入 U盘，打开 Rufus，选择下载好的 Ubuntu 镜像。 保持默认设置，点击“开始”制作启动盘。 BIOS/UEFI 设置 重启电脑，进入 BIOS 设置（根据不同电脑品牌自行查询）。 将 U盘设置为第一启动项。 若为 UEFI 模式，建议关闭安全启动（Secure Boot）。 安装 Ubuntu 插入启动盘，重启电脑进入 Ubuntu 安装界面。 选择“安装 Ubuntu”。 安装类型选择“与 Windows 共存”或“其他选项”，手动选择刚才压缩出的未分配空间。（个人推荐最好选择“与 Windows 共存”，操作简单） 设置分区（推荐：/ 根分区 20GB+，swap 2GB+，/home 可选）。 按提示完成安装，期间设置用户名、密码等。 引导修复与启动选择 安装完成后，重启电脑会进入 GRUB 引导界面，可选择 Windows 或 Ubuntu。 若未出现 GRUB，可用启动盘进入 Ubuntu，安装并运行 boot-repair 工具修复引导。 常见问题与解决 分区丢失/数据丢失：安装前务必备份重要数据。 无法进入 Ubuntu/Windows：检查 BIOS 启动项及分区设置，必要时使用 boot-repair。 GRUB 未显示：尝试修复引导或检查分区格式。 英伟达显卡安装黑屏或安全模式问题： 如果在安装 Ubuntu 时出现黑屏或只能进入安全模式（可视范围很小），且 Windows 进入了独显模式，有两种解决方案： 进入 Windows，关闭独显模式，重新进入安装界面。安装完成后，进入 Ubuntu，打开“附加驱动”，安装英伟达驱动，优先选择带有“test”或“专用”字样的版本。 在 GRUB 引导界面时，将光标移动到 Ubuntu 安装选项，不要回车。按“E”进入编辑模式，使用方向键向下滚动，找到以 linux 开头的一行（结尾通常是 $vt_handoff 或 quiet splash），在 splash 后面加上 nomodeset，然后按 Ctrl+X 启动。进入系统后，按方案1安装驱动。 结语 通过以上步骤，你可以在一台电脑上顺利安装并使用 Windows 与 Ubuntu 双系统。遇到问题时，可以发到群里让你亲爱的学长帮助你，如果学长也无法帮助你，可以查阅官方文档或相关社区获取帮助，如果還是無法解決，去電腦店刷機。\n","date":"2025-10-01T22:30:41+08:00","permalink":"https://Galileo927.github.io/p/windows-and-ubuntu-dual-boot-installation/","title":"Windows与ubuntu双系统的安装"},{"content":"Python开发环境搭建指南：Anaconda与PyCharm安装教程 前言 本文是为Python新手准备的开发环境配置指南。对于初学者，我们强烈推荐使用Anaconda和PyCharm的组合——Anaconda提供了完整的Python环境和科学计算库，而PyCharm则是专业的Python开发工具，二者搭配能显著降低学习门槛。\n一、Anaconda安装指南 1. Anaconda是什么？ Anaconda是Python的一个发行版，它包含：\nPython解释器 Conda包管理器（比pip更强大的依赖管理） 180+预装科学计算库（如NumPy, Pandas, Matplotlib等） Jupyter Notebook等实用工具 安装Anaconda后就无需单独安装Python和其他基础库了。\n2. 下载安装包 2.1 官方渠道（不推荐新手使用） 官网地址：https://www.anaconda.com\n2.2 国内镜像（推荐） 清华大学开源镜像站： https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/\n选择对应操作系统（由于清华源内容很多，找的时候一定要看清）： 3. 详细安装步骤（Windows示例） 运行安装程序：双击下载的.exe文件 用户选择：建议选择\u0026quot;Just Me\u0026quot;（仅当前用户） 安装路径：建议修改为D:\\Anaconda3等非系统盘路径，别安装到C盘就行 高级选项（关键步骤）： ✅ Add Anaconda to my PATH environment variable(如果你想要自己手动添加到环境变量也可以不勾选) ✅ Register Anaconda as my default Python 3.x 等待安装完成（约10-20分钟） 4. 验证安装 打开命令提示符（Win+R → 输入cmd），执行：\nconda --version # 应显示类似：conda 23.7.4 二、pycharm安装指南 1. PyCharm 是什么？ PyCharm 是 JetBrains 开发的一款专业 Python IDE，具备：\n智能代码补全与语法高亮 图形化调试器 \u0026amp; 测试运行器 Git / SVN / Docker 等一键集成 内置终端、虚拟环境管理与 Conda 支持 科学模式（Scientific Mode），可无缝调用 Anaconda 中的 NumPy、Matplotlib 等库 2. 下载安装包 1 官方渠道（推荐） 官网地址：https://www.jetbrains.com/pycharm/download\n之前本来还分社区版（白嫖）和专业版（氪佬），但是好像这段时间统一成一个程序里了，可选择pro用户。\n3. 详细安装步骤（Windows 示例） 运行安装程序\n双击下载的 pycharm-2024.x.x.exe → 点击 “Next”。\n安装路径\n建议改为 D:\\JetBrains\\PyCharm 2024.x，避免 C 盘空间不足。\n安装选项（建议全部勾选）\n✅ Create Desktop Shortcut（64-bit launcher） ✅ Add \u0026ldquo;Open Folder as Project\u0026rdquo; ✅ .py 关联 ✅ Add launchers dir to the PATH（后续命令行可直接 pycharm 打开，没错，这也是为什么anaconda也让你放环境中的理由） 4. 配置conda环境 1.在你的任务栏中搜索anaconda prompt，打开之后输入\nconda create -n myenv python=3.11 在这里myenv是你创建的虚拟环境的名字，3.11是这个环境中python的版本号 2.稍等片刻后输入\nconda activate myenv 3.像这样你就创建成功了 5. 配置 Anaconda 解释器（关键步骤） 可以先去网上搜索中文插件\n新建项目\n打开 PyCharm → New Project → Location 填写 D:\\Code\\PyDemo。\n选择 Conda 环境\n直接打开右下角的地方，如图，选择添加新的解释器 选择现有，类型选择conda类型，conda路径在anaconda的下载地址中找类似于我这个 E:\\anaconda\\Scripts\\conda.exe（我安装到了E盘） 环境选择我们刚才创建好的myenv，点击确认 验证\n新建 hello.py 输入以下代码并运行 Shift+F10：\nprint(\u0026#34;hello world\u0026#34;) 如果可以正常输出hello world那么就成功安装好了。\n","date":"2025-08-16T13:14:47+08:00","permalink":"https://Galileo927.github.io/p/anaconda-and-pycharm-installation/","title":"Anaconda及pycharm的安装"},{"content":"Hello Blog 这是我的第一篇blog，感谢b站的大佬以及背靠微软的GitHub，让我一个学生党也能免费部署。 之后会发布一些我自己的学习总结以及经历，具体学习的方向应该是数学、cpp、python、计算机视觉、数据分析以及深度学习的内容。有点偏向技术文档的日记。\n","date":"2025-08-15T14:02:05+08:00","permalink":"https://Galileo927.github.io/p/myfirstblog/","title":"MyFirstBlog"}]