与 08 总纲 §2 完全一致:C/C++ 是根基,不是交付主线。先把边界划清楚,免得它反向吞掉你宝贵的主线时间。
| 维度 | Go(主线交付) | C/C++(根基打底) |
|---|---|---|
| 内存谁管 | GC 自动管;你只需关心逃逸分析与指针语义 | 你自己管 malloc/free / RAII;段错误是家常便饭 |
| 你能学到什么 | 怎么把系统快速、正确地交付出去 | 内存到底是怎么回事——GC 替你藏起来的那层 |
| 读 runtime 时 | 你在"用"调度器与 GC | 你能"看懂"调度器与 GC 怎么写出来的 |
| 时间预算 | 每日 4 小时主线 | 每周 1 小时时间盒,硬上限 |
| 典型产物 | workbench 服务、CLI 工具 | 链表/内存池练习、QEMU 里的 toy kernel |
一句话定位:Go 让你把东西卖出去、跑起来;C/C++ 让你知道机器底层发生了什么。后者每周只花 1 小时,但它决定你四阶段之后能不能再往上走一层。
安装本体只在 09 模块讲(§1.7),这里只做"装好之后怎么起一个最小工程"。版本纪律:照抄下表,不要自己选版本。
| 组件 | 锁定版本 | 说明 |
|---|---|---|
| Visual Studio 2022 | MSVC v143 工具集 | 安装时勾选工作负载"使用 C++ 的桌面开发";含 MSVC、Windows SDK、CMake 集成 |
| CMake | 4.4.3 | Windows 装 x86_64 installer;命令行可用即可(VS 自带的 CMake 版本可能旧,本项目统一用 4.4.3) |
| MinGW-w64(备选) | 不锁具体号 | 仅当你不想装 VS 时的轻量替代(MSYS2/WinLibs 分发);本模块默认路径是 MSVC,遇到问题别混用两套工具链(见 §8 报错表) |
新建目录 cpp-root/,里面放三个文件:
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 —— 验证工具链是否通:打印一句 + 算个斐波那契
#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;
}
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(...) 即可。
不教指针是什么。下面每个小节只给"一句话本质 + 一个完整练习"。五个练习全是阶段四读 Go runtime 时天天见面的东西:链表、内存池、字符串、位操作、环形缓冲。
本质一句话:数组名是"首元素地址"的语法糖,指针是"一个保存地址的变量"。arr[i] 编译后永远是 *(arr + i)——偏移量按元素大小缩放。你当年写 Delphi/Qt 的手感在这里,提醒自己别在边界上偷懒。
本质一句话:struct 成员按声明顺序连续排列,编译器可能插入填充字节对齐。这直接决定 sizeof、决定缓存行利用率、决定你后面 §5 的一切。
本质一句话:局部变量在栈上(函数返回即失效),malloc 在堆上(你必须 free)。返回局部变量地址是 C 里最经典的自毁。
本质一句话:函数名 decays 成函数指针;回调 = 把"要做什么"当参数传进去。你写 Qt 信号槽、Go 里传函数值,底层都是这个。
// 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;
}
// 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;
}
// 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 状态标志。现在手写一遍,将来读源码就是"老朋友见面"。
你是 C++98 时代过来的。这十几年标准加了很多东西,下面只挑"你现在写现代代码绕不开"的,配一个完整示例。
| 特性 | 一句话要点 | 什么时候用 |
|---|---|---|
| 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 就够打根基。
| 你当年的写法 | 现在的写法 | 为什么变了 |
|---|---|---|
| 手动 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 | 少写样板 |
// 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++ 最常见的过度设计。
这一节是全模块的"理论核心"。Go 把 GC 和虚拟内存替你藏起来了,但阶段四你要读 runtime、要写 toy kernel,这层必须自己看得见。
// 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 里的页表,全是在这个图上做文章。
这是 07 报告"操作系统梦在 QEMU 里继续"的执行版,与 08 阶段四"Go runtime 源码深潜 + QEMU toy kernel"完全一致。每周时间盒推一站,不求快,求每站亲手跑通。参考资源只写书名,照读即可,不用跳出教程去找。
准备两样东西:①装 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 是梦想基金,不是交付压力——卡住一周就停在那一周,不影响主线。
与 08 §3 周末盘一致:周六下午的"C/C++/QEMU 时间盒"。1 小时硬上限,到点就停,绝不延期到主线时间。
| 时段 | 动作 | 做什么 |
|---|---|---|
| 0:00–0:30 | 理论精读 | 读本模块当前一节(如 §5 内存模型、或 §6 当前站的关键概念)。只读不写,把"一句话本质"抄进 playbook。 |
| 0:30–1:00 | 照做动手 | 把当周对应的完整代码亲手敲一遍跑通:轮换 §3 的五个练习 → §4 现代 C++ 示例 → §5 布局演示 → §6 QEMU 各站。 |
| 到点 | 立刻停 | 没做完也停。把卡壳点写进 playbook,下周第一个 30 分钟先解决它。 |
至少覆盖你在本模块会真实踩到的坑。按"报错原文 / 原因 / 解决"三列对照。
| 报错原文 / 现象 | 原因 | 解决 |
|---|---|---|
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 function | unique_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 |
复制到日程本,三项全部打勾才算本模块闭环。它不要求你成为 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 小时,卡住就记录、下周再来。
本模块所有版本号均来自全系列唯一事实来源 versions.md(见 09 模块 §4 总表)。这里列出与 C/C++ 直接相关的条目。
| 组件 | 锁定版本(2026-10-14 核验) | 来源 |
|---|---|---|
| CMake | 4.4.3 | https://cmake.org/download/ |
| Visual Studio 2022 | MSVC v143 + "使用 C++ 的桌面开发"工作负载 | https://visualstudio.microsoft.com/ |
| MinGW-w64 | 备选(MSYS2/WinLibs 分发,不锁具体号) | https://www.mingw-w64.org/ |
| 本系列"根基/梦想基金/时间盒"定位、阶段四 QEMU toy kernel 路线:07 编程主线决策报告 与 08 闭关总纲 §2/§3 | file:///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 结论的执行化设计,无外部独立来源。)