编程闭关教程 · 系列 08–20 · 编制日期 2026-10-14

C 与 C++ 根基专题:保持锋利 + 为 OS 梦打底

你从 C/C++ 起家,做过 Delphi、Qt——所以本模块不教"什么是指针",只做三件事:①用每周 1 小时时间盒把手感焊住;②补上 C++17/20 这十几年的现代语法差;③把操作系统梦放进 QEMU。C/C++ 在本系列的定位是根基与梦想基金:不是商业交付主线,而是阶段四深潜 Go runtime 与 toy kernel 的底子。
本模块承诺每周固定 1 小时时间盒(30 分钟理论 + 30 分钟动手),不抢主线;一年后你能在 QEMU 里跑通自己的 toy kernel——从引导扇区打印字符,走到页表与用户态切换。它同时让你读 Go runtime 源码时"不发抖"。
根基
C/C++ 在本系列的定位
1h/周
时间盒,不占用交付主线
VS2022
MSVC v143 + CMake 4.4.3
6 站
QEMU toy kernel 路线
12 月
引导扇区 → 页表入门
与 08 总纲一致:主线=Go;根基=C/C++;阶段四=Go runtime 源码深潜 + QEMU toy kernel(08 §2 时间线第 4 月起持续段)。工具链版本全部来自 09 与 versions.md(CMake 4.4.3、VS2022 MSVC v143)。
08 总纲 09 环境·版本 10 Go 内核 11 工作台 12 Vue3 13 Flutter 14 Python 15 C/C++ 16 MySQL 17 PostgreSQL 18 Redis 19 SQLite3 20 部署
本页目录
  1. 定位与边界:什么时候用、什么时候不用
  2. 2026 工具链最小工程(VS2022 + CMake 4.4.3)
  3. C 快速回顾:保持向的五个练习
  4. 现代 C++ 要点(C++17/20):与你当年的差异
  5. 内存模型基础:为 Go runtime 与 OS 打底
  6. QEMU toy kernel 六站路线图(梦想基金核心)
  7. 每周 1 小时时间盒怎么用
  8. 常见报错表
  9. 本模块验收
  10. 版本与来源
01

定位与边界:什么时候用、什么时候不用

与 08 总纲 §2 完全一致:C/C++ 是根基,不是交付主线。先把边界划清楚,免得它反向吞掉你宝贵的主线时间。

1.1 什么时候必须用 C/C++

OS 学习与裸机梦

写 toy kernel、读操作系统源码,没有第二语言可选。这是本模块 §6 路线的唯一载体,也是 07 报告"操作系统梦在 QEMU 里继续"的落点。

性能热点与运行时级调试

Go 里 pprof 定位到的热点、cgo 边界、内存分配器行为——要读懂它、要能下手改,前提是你懂指针、布局、缓存局部性。

读别人的源码

Go runtime、SQLite、Redis、许多库的核心都是 C/C++。阶段四读 runtime 源码时,这份底子直接决定你是"读得懂"还是"只能背结论"。

嵌入式/客户存量需求

客户或硬件场景明确要求 C/C++(单片机、工控、桌面原生插件)时才出手;纯 Web/后端交付不主动选它。

1.2 什么时候坚决不用

1.3 与 Go 的互补关系(记住这张表)

维度Go(主线交付)C/C++(根基打底)
内存谁管GC 自动管;你只需关心逃逸分析与指针语义你自己管 malloc/free / RAII;段错误是家常便饭
你能学到什么怎么把系统快速、正确地交付出去内存到底是怎么回事——GC 替你藏起来的那层
读 runtime 时你在"用"调度器与 GC你能"看懂"调度器与 GC 怎么写出来的
时间预算每日 4 小时主线每周 1 小时时间盒,硬上限
典型产物workbench 服务、CLI 工具链表/内存池练习、QEMU 里的 toy kernel

一句话定位:Go 让你把东西卖出去、跑起来;C/C++ 让你知道机器底层发生了什么。后者每周只花 1 小时,但它决定你四阶段之后能不能再往上走一层。

02

2026 工具链最小工程(VS2022 + CMake 4.4.3)

安装本体只在 09 模块讲(§1.7),这里只做"装好之后怎么起一个最小工程"。版本纪律:照抄下表,不要自己选版本。

2.1 你需要装的东西(与 09 一致)

组件锁定版本说明
Visual Studio 2022MSVC v143 工具集安装时勾选工作负载"使用 C++ 的桌面开发";含 MSVC、Windows SDK、CMake 集成
CMake4.4.3Windows 装 x86_64 installer;命令行可用即可(VS 自带的 CMake 版本可能旧,本项目统一用 4.4.3)
MinGW-w64(备选)不锁具体号仅当你不想装 VS 时的轻量替代(MSYS2/WinLibs 分发);本模块默认路径是 MSVC,遇到问题别混用两套工具链(见 §8 报错表)

2.2 最小 CMake 工程(三个文件,完整可跑)

新建目录 cpp-root/,里面放三个文件:

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(root_demo C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 常用开关:把警告打开,错误尽量早暴露
if(MSVC)
    add_compile_options(/W4 /permissive-)
else()
    add_compile_options(-Wall -Wextra)
endif()

add_executable(root_demo main.cpp)

main.cpp

// main.cpp —— 验证工具链是否通:打印一句 + 算个斐波那契
#include <cstdio>

int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}

int main() {
    std::printf("toolchain ok: fib(10) = %d\n", fib(10));
    return 0;
}

构建与运行命令(PowerShell,在 cpp-root/ 目录里执行)

cmake -S . -B build
cmake --build build --config Release
.\build\Release\root_demo.exe

看到输出 toolchain ok: fib(10) = 55 即工具链通了。之后本模块所有练习都往这个工程里加新 .cpp / .c 文件、在 CMakeLists.txt 里加一行 add_executable(...) 即可。

为什么锁 CMake 4.4.3:它是 2026-10 官网稳定版(见 versions.md A 表);锁它是为了让 08/09 与本模块的命令逐字可复现,不是让你每次重新决策。AI 若建议"升最新",默认拒绝(08 §7 规则 5)。
03

C 快速回顾:保持锋利向的五个练习

不教指针是什么。下面每个小节只给"一句话本质 + 一个完整练习"。五个练习全是阶段四读 Go runtime 时天天见面的东西:链表、内存池、字符串、位操作、环形缓冲。

3.1 指针与数组的本质

本质一句话:数组名是"首元素地址"的语法糖,指针是"一个保存地址的变量"。arr[i] 编译后永远是 *(arr + i)——偏移量按元素大小缩放。你当年写 Delphi/Qt 的手感在这里,提醒自己别在边界上偷懒。

3.2 结构体与内存布局

本质一句话:struct 成员按声明顺序连续排列,编译器可能插入填充字节对齐。这直接决定 sizeof、决定缓存行利用率、决定你后面 §5 的一切。

3.3 内存管理:malloc/free、栈与堆

本质一句话:局部变量在栈上(函数返回即失效),malloc 在堆上(你必须 free)。返回局部变量地址是 C 里最经典的自毁。

3.4 函数指针与回调

本质一句话:函数名 decays 成函数指针;回调 = 把"要做什么"当参数传进去。你写 Qt 信号槽、Go 里传函数值,底层都是这个。

3.5 五个练习(每个都是完整可编译文件)

练习 ① 链表(插入/遍历/销毁)

// list.c —— 单向链表:尾插、遍历、整体销毁
#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int value;
    struct Node* next;
} Node;

Node* list_push(Node* head, int v) {
    Node* n = (Node*)malloc(sizeof(Node));
    n->value = v;
    n->next = NULL;
    if (head == NULL) return n;
    Node* cur = head;
    while (cur->next) cur = cur->next;
    cur->next = n;
    return head;
}

void list_print(const Node* head) {
    for (const Node* p = head; p; p = p->next)
        printf("%d -> ", p->value);
    printf("NULL\n");
}

void list_free(Node* head) {
    while (head) {
        Node* next = head->next;
        free(head);
        head = next;
    }
}

int main(void) {
    Node* head = NULL;
    for (int i = 1; i <= 5; ++i) head = list_push(head, i * 10);
    list_print(head);      /* 10 -> 20 -> 30 -> 40 -> 50 -> NULL */
    list_free(head);
    return 0;
}

练习 ② 内存池(定长块,替代频繁 malloc/free)

// pool.c —— 固定大小内存块池:预先分配一大块,用空闲链表串起来
#include <stdio.h>
#include <stdlib>

#define BLOCK_SIZE  64
#define POOL_BLOCKS 16

typedef struct FreeNode {
    struct FreeNode* next;
} FreeNode;

typedef struct Pool {
    char storage[POOL_BLOCKS * BLOCK_SIZE];
    FreeNode* free_list;
} Pool;

void pool_init(Pool* p) {
    p->free_list = NULL;
    for (int i = POOL_BLOCKS - 1; i >= 0; --i) {
        FreeNode* n = (FreeNode*)(p->storage + i * BLOCK_SIZE);
        n->next = p->free_list;
        p->free_list = n;
    }
}

void* pool_alloc(Pool* p) {
    if (p->free_list == NULL) return NULL;
    FreeNode* n = p->free_list;
    p->free_list = n->next;
    return (void*)n;
}

void pool_free(Pool* p, void* block) {
    FreeNode* n = (FreeNode*)block;
    n->next = p->free_list;
    p->free_list = n;
}

int main(void) {
    Pool p;
    pool_init(&p);
    void* a = pool_alloc(&p);
    void* b = pool_alloc(&p);
    printf("a=%p b=%p\n", a, b);
    pool_free(&p, a);
    pool_free(&p, b);
    printf("pool ok\n");
    return 0;
}

练习 ③ 字符串处理(不依赖库的 strcpy/strlen 手写字)

// str.c —— 手写 strlen / strcpy / 统计单词数
#include <stdio.h>

size_t my_strlen(const char* s) {
    const char* p = s;
    while (*p) ++p;
    return (size_t)(p - s);
}

char* my_strcpy(char* dst, const char* src) {
    char* head = dst;
    while ((*dst++ = *src++)) ;   /* 连 '\0' 一起拷 */
    return head;
}

int count_words(const char* s) {
    int n = 0, in = 0;
    for (; *s; ++s) {
        if (*s == ' ' || *s == '\t' || *s == '\n') {
            in = 0;
        } else if (!in) {
            in = 1;
            ++n;
        }
    }
    return n;
}

int main(void) {
    char buf[64];
    my_strcpy(buf, "hello rooted c");
    printf("len=%zu words=%d buf=%s\n",
           my_strlen(buf), count_words(buf), buf);
    return 0;   /* len=14 words=3 */
}

练习 ④ 位操作(标志位与位域)

// bits.c —— 置位/清位/取位;位操作是驱动与协议层的家常便饭
#include <stdio.h>

#define FLAG_READ   (1u << 0)
#define FLAG_WRITE  (1u << 1)
#define FLAG_EXEC   (1u << 2)
#define FLAG_ADMIN  (1u << 3)

static void set(unsigned* flags, unsigned f)  { *flags |= f; }
static void clear(unsigned* flags, unsigned f){ *flags &= ~f; }
static int  has(unsigned flags, unsigned f)   { return (flags & f) != 0; }

int main(void) {
    unsigned flags = 0;
    set(&flags, FLAG_READ | FLAG_WRITE);
    set(&flags, FLAG_ADMIN);
    printf("read=%d exec=%d admin=%d\n",
           has(flags, FLAG_READ),
           has(flags, FLAG_EXEC),
           has(flags, FLAG_ADMIN));   /* 1 0 1 */
    clear(&flags, FLAG_ADMIN);
    printf("after clear admin=%d\n", has(flags, FLAG_ADMIN)); /* 0 */
    return 0;
}

练习 ⑤ 环形缓冲(单读单写,无锁思路)

// ring.c —— 定长环形缓冲:这就是 Go channel 底层的兄弟结构
#include <stdio.h>

#define CAP 8

typedef struct {
    char buf[CAP];
    int head;   /* 读位置 */
    int tail;   /* 写位置 */
    int count;  /* 当前元素数 */
} Ring;

int ring_push(Ring* r, char c) {
    if (r->count == CAP) return -1;      /* 满 */
    r->buf[r->tail] = c;
    r->tail = (r->tail + 1) % CAP;
    ++r->count;
    return 0;
}

int ring_pop(Ring* r, char* out) {
    if (r->count == 0) return -1;        /* 空 */
    *out = r->buf[r->head];
    r->head = (r->head + 1) % CAP;
    --r->count;
    return 0;
}

int main(void) {
    Ring r = {0};
    for (char c = 'a'; c < 'a' + 5; ++c) ring_push(&r, c);
    char out;
    while (ring_pop(&r, &out) == 0) putchar(out);
    putchar('\n');   /* abcde */
    return 0;
}

五个练习的共同用途:它们就是你在阶段四读 Go runtime 时会反复遇到的原语。链表=调度器运行队列;内存池=mcache/mheap;环形缓冲=channel;位操作=goroutine 状态标志。现在手写一遍,将来读源码就是"老朋友见面"。

04

现代 C++ 要点(C++17/20):与你当年的差异

你是 C++98 时代过来的。这十几年标准加了很多东西,下面只挑"你现在写现代代码绕不开"的,配一个完整示例。

4.1 必须知道的七件事

特性一句话要点什么时候用
RAII对象构造即获取资源,析构即释放;函数 return 多少次都安全一切"加锁/开文件/分配"的场景,替代手写 try/finally
unique_ptr独占所有权,零开销,不能拷贝只能 move默认首选:谁拥有这块内存就用它
shared_ptr引用计数共享所有权,有控制块开销仅当"真的多主共享"且无法用所有权图简化时
移动语义转移内部指针而非深拷贝;函数返回大对象时编译器自动用容器元素入出栈、返回 vector;你只需知道 std::move 是"承诺不再用它"
auto + lambda类型能让编译器推就推;小匿名函数就地写STL 算法回调、std::thread 参数、局部一次性逻辑
STL 容器与算法vector/map/unordered_map + <algorithm> 排序/查找/计数代替你手写的容器与循环;性能不够再换
constexpr编译期可求值的函数/常量数组大小、配置常量、查表——把计算挪到编译期

C++20 的模块(module)与概念(concepts)知道有这回事即可:模块替代 #include 加速编译,concepts 给模板参数加约束。本模块不展开,你写 C++17 就够打根基。

4.2 与"C++98 时代"差异速览表

你当年的写法现在的写法为什么变了
手动 new / delete,到处查谁负责释放unique_ptr / shared_ptr 自动释放异常安全 + 杜绝忘记 delete
手动深拷贝构造函数= default / = delete;需要时写移动构造编译器规则更明确,少手写 bug
vector<int>::iterator 写满屏auto it = v.begin(); 范围 for类型推导,眼不见为净
函数对象类重载 operator()lambda 就地写局部逻辑不必另起一个类
宏定义常量constexpr 常量/函数有类型、可调试、遵守作用域
.h/.cpp 手写头文件守卫#pragma once 或 module少写样板

4.3 一个完整现代 C++ 示例(把上面特性串起来)

// modern.cpp —— RAII + unique_ptr + lambda + STL + constexpr
#include <memory>
#include <vector>
#include <algorithm>
#include <iostream>

constexpr int kMaxItems = 8;   // 编译期常量

int main() {
    // 1) unique_ptr 独占数组,离开作用域自动 delete[]
    auto buf = std::make_unique<int[]>(kMaxItems);
    for (int i = 0; i < kMaxItems; ++i) buf[i] = (i * i) % 7;

    // 2) vector + lambda + STL 算法
    std::vector<int> v(buf.get(), buf.get() + kMaxItems);
    std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

    // 3) 范围 for 打印
    for (int x : v) std::cout << x << ' ';
    std::cout << '\n';

    // buf 在这里自动释放,无需 delete[]
    return 0;
}

选型纪律:默认 unique_ptr;只有共享所有权绕不开时才用 shared_ptr。滥用 shared_ptr 会引入循环引用与难以追踪的生命周期——这是现代 C++ 最常见的过度设计。

05

内存模型基础:为 Go runtime 与 OS 打底

这一节是全模块的"理论核心"。Go 把 GC 和虚拟内存替你藏起来了,但阶段四你要读 runtime、要写 toy kernel,这层必须自己看得见。

5.1 最小必要理论(四条)

5.2 演示程序:亲手打印你的内存布局

// layout.cpp —— 打印代码/全局/堆/栈四类地址,建立"地址空间直觉"
#include <cstdio>
#include <cstdlib>

int g_global = 42;            // 数据段
static int g_static = 7;      // 数据段(本文件可见)

void some_function() {        // 代码段
    int local = 1;            // 栈
    std::printf("  函数 some_function 地址 : %p\n", (void*)&some_function);
    std::printf("  栈上局部变量 local 地址 : %p\n", (void*)&local);
}

int main() {
    int* heap = (int*)malloc(sizeof(int) * 4);   // 堆

    std::printf("代码段  &main            : %p\n", (void*)&main);
    std::printf("数据段  g_global        : %p\n", (void*)&g_global);
    std::printf("数据段  g_static        : %p\n", (void*)&g_static);
    std::printf("堆     heap[0]         : %p\n", (void*)&heap[0]);
    some_function();

    free(heap);
    return 0;
}

跑一遍你会看到:代码段地址最低,数据段在其后,堆在中间向上长,栈地址最高(向低地址方向)。把这张图记住——后面 Go runtime 的 mheap/goroutine 栈、QEMU 里的页表,全是在这个图上做文章。

为什么这节配 Go:Go 的 goroutine 栈是"可增长的栈"(初始几 KB,不够了拷贝扩容)——它本质就是 §5.1 里"栈"这条概念在运行时的再实现。你现在把布局看透,将来读 stack growth 源码不会觉得玄。
06

QEMU toy kernel 六站路线图(梦想基金核心)

这是 07 报告"操作系统梦在 QEMU 里继续"的执行版,与 08 阶段四"Go runtime 源码深潜 + QEMU toy kernel"完全一致。每周时间盒推一站,不求快,求每站亲手跑通。参考资源只写书名,照读即可,不用跳出教程去找。

第 1 站
搭建 QEMU + 裸机工具链,引导扇区打印字符
学习目标:理解 x86 开机后 BIOS 把 512 字节引导扇区加载到 0x7C00 并跳转;用 NASM 写出第一个能在 QEMU 里打印字符的扇区。
关键概念:实模式 16 位、BIOS 中断 int 0x10 打印、魔数 0xAA55。
参考资源:osdev wiki 的 Boot Sequence 条目;《操作系统真象还原》第 1 章。
照做代码:见 §6.1,本站是本模块验收项之一。
第 2 站
GDT 与保护模式
学习目标:从 16 位实模式切到 32 位保护模式;建立全局描述符表(GDT),理解段寄存器从"段偏移"变成"段选择子"。
关键概念:描述符、权限环、CR0 PE 位。
参考资源:osdev wiki GDT 条目;《操作系统真象还原》第 3 章。
第 3 站
中断与 IDT
学习目标:建立中断描述符表(IDT),让 CPU 遇到中断/异常时跳进你的 C 函数;实现时钟中断与键盘中断。
关键概念:中断门、PIC 8259 重映射、中断处理栈切换。
参考资源:osdev wiki Interrupts 条目;《操作系统真象还原》第 6 章。
第 4 站
显存与简单输出
学习目标:直接写文本显存 0xB8000,实现自己的 printf;不再依赖 BIOS 打印。
关键概念:字符单元(ASCII + 属性字节)、光标寄存器、滚屏。
参考资源:osdev wiki Text Mode 条目;《操作系统真象还原》第 5 章。
第 5 站
内存管理 / 页表入门
学习目标:亲手建一级/二级页表,开启分页;把 §5 的虚拟内存理论变成能跑的代码;写一个最简物理页分配器。
关键概念:页目录/页表项、权限位、虚拟→物理翻译、伙伴分配雏形。
参考资源:osdev wiki Paging 条目;《操作系统真象还原》第 10–12 章。
这一站直接反哺 Go:你亲手建过页表,再回头看 Go runtime 的 mheap/mheapArena 就不抽象了。
第 6 站
最小内核 + 用户态切换预告
学习目标:把前五站串成一个能常驻运行的小内核(中断 + 输出 + 内存分配);预告 ring0→ring3 用户态切换与系统调用,作为下一年的入口。
关键概念:特权级切换、系统门、TSS。
参考资源:osdev wiki Introduction to保护模式与用户态相关条目;《操作系统真象还原》第 14 章。

6.1 第 1 站完整代码(引导扇区打印,照做即通)

准备两样东西:①装 QEMU(Windows 下装 qemU 模拟器,命令行 qemu-system-x86_64 可用);②装 NASM 汇编器。然后新建 boot.asm:

; boot.asm —— 第一个引导扇区:QEMU 启动后屏幕上打印一行字
org 0x7c00            ; BIOS 把本扇区加载到物理地址 0x7C00
bits 16               ; 开机默认 16 位实模式

start:
    mov si, msg       ; DS:SI 指向字符串
.print:
    lodsb             ; 取一个字符到 AL,SI++
    or al, al         ; 碰到字符串结束符 0?
    jz .halt          ; 是 → 停住
    mov ah, 0x0e      ; BIOS teletype 功能号
    mov bh, 0         ; 页码
    int 0x10          ; 调用 BIOS 视频中断打印 AL
    jmp .print
.halt:
    hlt               ; 停机,等中断
    jmp .halt

msg db "Hello, toy kernel!", 0

times 510 - ($ - $$) db 0   ; 填零到第 510 字节
dw 0xaa55                   ; 最后两字节是引导魔数

构建与运行(PowerShell,在 boot.asm 所在目录):

nasm -f bin boot.asm -o boot.bin
qemu-system-x86_64 boot.bin

QEMU 窗口弹出、屏幕上出现 Hello, toy kernel! 即为第 1 站通过。看到的原理:BIOS 读磁盘第一个扇区到 0x7C00,校验最后两字节是 0xAA55 后跳转执行——你刚写的 512 字节就是"操作系统的第一步"。

与 08 的一致性:08 §2 阶段四(第 4 月起持续)明确写"15 C/C++ 根基专题配合 QEMU toy kernel(osdev 路线)每周时间盒推进";本节六站就是该条目的展开。QEMU 里跑 toy kernel 是梦想基金,不是交付压力——卡住一周就停在那一周,不影响主线。

07

每周 1 小时时间盒怎么用

与 08 §3 周末盘一致:周六下午的"C/C++/QEMU 时间盒"。1 小时硬上限,到点就停,绝不延期到主线时间。

7.1 标准模板(30 分钟理论 + 30 分钟动手)

时段动作做什么
0:00–0:30理论精读读本模块当前一节(如 §5 内存模型、或 §6 当前站的关键概念)。只读不写,把"一句话本质"抄进 playbook。
0:30–1:00照做动手把当周对应的完整代码亲手敲一遍跑通:轮换 §3 的五个练习 → §4 现代 C++ 示例 → §5 布局演示 → §6 QEMU 各站。
到点立刻停没做完也停。把卡壳点写进 playbook,下周第一个 30 分钟先解决它。

7.2 全年节奏建议(12 个月,每周 1h ≈ 50h)

弹性纪律:主线忙的月份,本时间盒可以缩到 30 分钟甚至跳过——但不要连续跳过 3 周。根基的价值在连续性,不在某周学了多少。
08

常见报错表

至少覆盖你在本模块会真实踩到的坑。按"报错原文 / 原因 / 解决"三列对照。

报错原文 / 现象原因解决
LNK2019 unresolved external symbol ...MSVC 链接期找不到符号:函数只声明没定义,或 .cpp 没加进 add_executable检查该函数是否有实现;在 CMakeLists.txt 里把对应 .cpp 加进目标文件列表;重新 cmake --build
CMake Error: Could not find any instance of Visual Studio / 找不到编译器没装"使用 C++ 的桌面开发"工作负载,或在没配好环境的 PowerShell 里直接跑 cmake用 VS Installer 确认勾选该工作负载;或在"开始菜单 → VS 2022 → x64 Native Tools Command Prompt"里跑命令
源码里中文输出乱码(控制台打印变问号/方块)源文件保存为 GBK 而 MSVC 按 UTF-8 解释,或反之;控制台代码页不匹配源文件统一存 UTF-8;MSVC 编译加 /utf-8 选项;PowerShell 先 chcp 65001 再运行
运行即崩溃 / 段错误(Windows 下"已停止工作")内存越界:数组写过头、free 了野指针、返回局部变量地址、栈溢出用 VS 调试器断点盯指针;打开 AddressSanitizer(MSVC 加 /fsanitize=address);先查最近改过的下标与 malloc/free 配对
智能指针误用:double free / 重复释放 / 编译报 use of deleted functionunique_ptr 被拷贝(它禁止拷贝);或两个 shared_ptr 从同一裸指针分别构造独占就用 std::move 转移所有权;共享同一对象时用 std::make_shared 一处构造再拷贝 shared_ptr,绝不从裸指针再造第二个
MinGW 编出的库被 MSVC 链接报一堆符号错MSVC 与 MinGW-w64 的 C++ ABI 不兼容,混链必错一个工程只用一套工具链。本模块默认 MSVC;若用 MinGW,全工程(含第三方库)都用 MinGW 编
undefined reference to `xxx'(MinGW/g++ 风格)与 LNK2019 同类:链接期缺符号,常见于漏加 .cpp 或库文件顺序补全源文件;注意 g++ 链接顺序:被依赖的库放后面;确认编译单元都进了 target
QEMU 启动后窗口黑屏无任何输出引导扇区最后两字节不是 0xAA55,或 times 填充算错导致魔数位置偏了;或 int 0x10 打印参数没设对用 nasm -f bin 重新生成;检查 times 510 - ($ - $$) db 0 与 dw 0xaa55 是否在文件末尾;确认 mov ah,0x0e 后再 int 0x10
改了 CMakeLists.txt 后构建行为不更新CMake 只在首次 configure 读 CMakeLists.txt;直接 build 不会重配改完后重跑 cmake -S . -B build(重新 configure)再 cmake --build build
09

本模块验收

复制到日程本,三项全部打勾才算本模块闭环。它不要求你成为 C++ 专家,只要求"手感在、工具链通、OS 梦迈出第一步"。

序号验收项对应小节
①练习 ① 链表:亲手敲通 list.c,输出 10 -> 20 -> 30 -> 40 -> 50 -> NULL,并确认 list_free 不内存泄漏§3.5 练习 ①
②练习 ② 内存池:亲手敲通 pool.c,能从池里 alloc 两块、free 回池,理解"空闲链表"原理§3.5 练习 ②
③QEMU 第 1 站:nasm 编出 boot.bin,qemu-system-x86_64 boot.bin 窗口里打印出 Hello, toy kernel!§6.1

验收不过怎么办:回到 §8 报错表对照;还不行就把报错原文 + 复现步骤喂给 AI(按 08 §6 双会话评审流程),不要在一个问题上消耗主线时间——本模块每周只有 1 小时,卡住就记录、下周再来。

10

版本与来源

本模块所有版本号均来自全系列唯一事实来源 versions.md(见 09 模块 §4 总表)。这里列出与 C/C++ 直接相关的条目。

组件锁定版本(2026-10-14 核验)来源
CMake4.4.3https://cmake.org/download/
Visual Studio 2022MSVC v143 + "使用 C++ 的桌面开发"工作负载https://visualstudio.microsoft.com/
MinGW-w64备选(MSYS2/WinLibs 分发,不锁具体号)https://www.mingw-w64.org/

来源表

本系列"根基/梦想基金/时间盒"定位、阶段四 QEMU toy kernel 路线:07 编程主线决策报告 与 08 闭关总纲 §2/§3file:///D:/02-Docs/金融知识/08-闭关总纲.html
CMake 4.4.3 与 VS2022 MSVC v143 工作负载(本系列版本唯一事实来源)https://cmake.org/download/ · https://visualstudio.microsoft.com/
MinGW-w64 作为 MSVC 轻量备选https://www.mingw-w64.org/
x86 引导扇区/实模式/GDT/IDT/分页等概念路线参考:osdev wiki(Boot Sequence / GDT / Interrupts / Paging 等条目)osdev.org(站内条目,本模块只写书名与站名,不引导跳出)
toy kernel 中文配套参考书《操作系统真象还原》(郑钢)

(编者整理:本模块的五个 C 练习、现代 C++ 示例、内存布局演示、QEMU 六站路线与每周时间盒模板为基于 07/08 结论的执行化设计,无外部独立来源。)