Skip to content

Commit e152323

Browse files
committed
docs: 补全主干章节并补充标准前提说明
1 parent 92951e8 commit e152323

32 files changed

Lines changed: 1284 additions & 9 deletions
Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,44 @@
11
# 有条件编译
22

3-
1. `defined` 结构
3+
有条件编译用于在预处理阶段按条件选择代码片段,常见场景是平台差异、特性开关和头文件包含保护。
44

5-
`defined 标识符`:如果定义了 `标识符`,求值为 **1**,否则为 **0**
5+
## 1. `defined` 运算符
66

7-
2. `#if``#elif``#else``#endif`
8-
3. `#elifdef``#elifndef`
7+
`defined 标识符``defined(标识符)` 会在预处理表达式中产生 `1``0`,用于判断宏是否已定义。
8+
9+
```c
10+
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 202311L)
11+
#define USE_C23 1
12+
#else
13+
#define USE_C23 0
14+
#endif
15+
```
16+
17+
## 2. 条件指令族
18+
19+
`#if``#elif``#else``#endif` 构成通用分支;`#ifdef``#ifndef` 是“只检查是否定义”的简写;C23 增加了 `#elifdef``#elifndef`,可让多分支条件更紧凑。若目标仍是 C11/C17,可写成 `#elif defined(...)` 以保持兼容。
20+
21+
```c
22+
#ifdef _WIN32
23+
#define PATH_SEP "\\"
24+
#elifdef __unix__
25+
#define PATH_SEP "/"
26+
#else
27+
#define PATH_SEP "/"
28+
#endif
29+
```
30+
31+
## 3. 包含保护
32+
33+
头文件通常使用 `#ifndef` 保护,防止同一翻译单元重复包含:
34+
35+
```c
36+
#ifndef MY_LIB_H
37+
#define MY_LIB_H
38+
39+
/* 声明 */
40+
41+
#endif
42+
```
43+
44+
包含保护不是可选装饰,而是头文件最基础的正确性要求。
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,29 @@
11
# `#line`
2+
3+
`#line` 指令用于修改后续代码中 `__LINE__``__FILE__` 的逻辑值。它主要服务于“代码生成器”场景:当源代码由工具生成时,编译诊断可以映射回原始输入文件。
4+
5+
## 1. 基本形式
6+
7+
```c
8+
#line 120 "schema.dsl"
9+
```
10+
11+
从这一行之后,预处理器会把行号视为 `120`,文件名视为 `schema.dsl`。编译器报错、`assert` 信息、调试输出中的位置元数据也会随之变化。
12+
13+
## 2. 示例
14+
15+
```c
16+
#include <stdio.h>
17+
18+
int main(void) {
19+
#line 42 "generated_from_template.c"
20+
printf("%s:%d\n", __FILE__, __LINE__);
21+
return 0;
22+
}
23+
```
24+
25+
输出会显示为 `generated_from_template.c:42`。这能让用户在阅读诊断时定位到“真正编辑的源”,而不是中间产物。
26+
27+
## 3. 使用边界
28+
29+
手写业务代码通常不需要 `#line`。只有在“源到源转换”链路中,需要保持诊断可追踪时,它才值得使用。滥用会让调试信息失真,反而增加排错难度。
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,47 @@
11
# 动态内存管理
2+
3+
动态内存管理让对象的创建时机不再受限于代码块作用域。它是构建容器、缓存和运行期可变数据结构的基础能力,同时也是 C 程序最常见的错误来源之一。
4+
5+
## 1. 四个核心接口
6+
7+
`malloc` 申请一段未初始化存储,`calloc` 申请并清零,`realloc` 调整已分配存储大小,`free` 释放已分配存储。它们都定义在 `<stdlib.h>` 中,返回值与失败语义必须显式检查。
8+
9+
```c
10+
#include <stdlib.h>
11+
12+
int *buf = malloc(16 * sizeof *buf);
13+
if (buf == NULL) {
14+
return;
15+
}
16+
17+
free(buf);
18+
buf = NULL;
19+
```
20+
21+
把指针置为 `NULL` 不是语义必须,但它可以减少重复释放和悬垂访问风险。
22+
23+
## 2. `realloc` 的安全模式
24+
25+
`realloc` 失败时会返回 `NULL`,同时原指针仍然有效。因此不能直接覆盖原指针,应该先用临时指针接收结果。
26+
27+
```c
28+
#include <stdlib.h>
29+
30+
int *grow(int *old_buf, size_t new_count) {
31+
int *new_buf = realloc(old_buf, new_count * sizeof *new_buf);
32+
if (new_buf == NULL) {
33+
return old_buf;
34+
}
35+
return new_buf;
36+
}
37+
```
38+
39+
这种写法可以确保失败时不丢失原有存储地址。
40+
41+
## 3. 常见错误与规避
42+
43+
最常见问题包括:忘记释放导致泄漏,释放后继续访问导致悬垂引用,重复释放导致运行时崩溃,越界写入破坏堆元数据。规避思路很直接:统一所有权、固定释放路径、在接口层写清责任边界,并使用工具链做持续检测(如 ASan、Valgrind)。
44+
45+
## 4. 工程建议
46+
47+
把“申请成功检查”和“失败回滚路径”当成模板代码,而不是临场补丁。动态内存本质是资源管理问题,不是语法问题;资源模型一旦清楚,代码复杂度会明显下降。
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,34 @@
11
# 字符和字符串库
2+
3+
C 标准库把“字符分类与大小写转换”和“字节字符串处理”分在两个头文件:`<ctype.h>``<string.h>`。前者解决“这个字符是什么”,后者解决“这段字节序列怎么处理”。
4+
5+
## 1. 学习重点
6+
7+
这一章的重点不是函数数量,而是边界意识:字符分类函数的输入约束、字符串以空终止字符结尾这一前提、缓冲区容量与复制长度之间的关系。只要边界模型正确,绝大多数字符串 bug 都能在编码阶段消失。
8+
9+
## 2. 两个子章节
10+
11+
字符分类与大小写转换见 [16.1 `<ctype.h>`](/教程/正文/语法和标准库/16_字符字符串库/16_1_ctype),字符串与内存字节处理见 [16.2 `<string.h>`](/教程/正文/语法和标准库/16_字符字符串库/16_2_string)。建议先读 16.1,再读 16.2,因为很多解析函数都依赖前者的分类规则。
12+
13+
## 3. 一个简短示例
14+
15+
```c
16+
#include <ctype.h>
17+
#include <stdio.h>
18+
#include <string.h>
19+
20+
int main(void) {
21+
char s[] = "C23!";
22+
23+
for (size_t i = 0; i < strlen(s); ++i) {
24+
if (isalpha((unsigned char)s[i])) {
25+
s[i] = (char)tolower((unsigned char)s[i]);
26+
}
27+
}
28+
29+
puts(s);
30+
return 0;
31+
}
32+
```
33+
34+
这段代码同时展示了字符分类和字符串遍历。注意把 `char` 转为 `unsigned char` 再传给 `isalpha`/`tolower`,这是标准要求下更稳妥的写法。
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,39 @@
11
# 本地化库
2+
3+
本地化库 (Localization) 让程序根据地区规则处理数字、货币、排序和消息文本。它的核心头文件是 `<locale.h>`,并与 `<ctype.h>``<string.h>``<wchar.h>` 等库能力联动。
4+
5+
这部分能力主要面向宿主实现;若目标是独立实现环境,需要先确认运行时是否提供对应支持。
6+
7+
## 1. 核心接口 `setlocale`
8+
9+
`setlocale` 用于查询或设置当前本地化类别。若设置失败会返回空指针,因此任何切换动作都应检查返回值。
10+
11+
```c
12+
#include <locale.h>
13+
#include <stdio.h>
14+
15+
int main(void) {
16+
const char *result = setlocale(LC_ALL, "");
17+
if (result == NULL) {
18+
puts("setlocale failed");
19+
return 1;
20+
}
21+
22+
puts(result);
23+
return 0;
24+
}
25+
```
26+
27+
传入空字符串 `""` 的常见语义是“按运行环境默认地区设置初始化”。
28+
29+
## 2. 数字与货币格式
30+
31+
`localeconv` 返回当前地区的格式描述,例如小数点符号和货币分隔。它适合在“展示层”做格式化,而不应拿来改变核心计算逻辑。计算逻辑应保持地区无关,输入输出再做本地化适配。
32+
33+
## 3. 排序与比较
34+
35+
字符串排序若需要遵循地区规则,应优先使用 `strcoll` 和 `strxfrm`,而不是直接按字节比较。字节序比较只适合协议字段和机器可读数据,不适合人类语言文本。
36+
37+
## 4. 实践建议
38+
39+
本地化配置应在程序启动早期一次性完成,避免在业务流程中频繁切换全局地区状态。若项目规模较大,建议把“格式化输出”集中在独立模块中管理,避免全局状态影响扩散。
Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,51 @@
11
# 环境访问
22

3-
## 环境变量
3+
程序运行时会与宿主环境交换信息,例如命令行参数、环境表项和外部命令执行能力。标准库在 `<stdlib.h>` 中提供了最小且可移植的访问接口。
44

5-
## `system()` 函数
5+
## 1. 环境表项读取:`getenv`
66

7-
## 命令行参数
7+
`getenv` 通过键名读取环境表中的字符串值。返回指针可能为 `NULL`,也可能指向由实现管理的存储,因此调用方不应修改其内容。
88

9-
## 系统调用
9+
```c
10+
#include <stdlib.h>
11+
#include <stdio.h>
12+
13+
int main(void) {
14+
const char *home = getenv("HOME");
15+
if (home != NULL) {
16+
printf("HOME=%s\n", home);
17+
}
18+
return 0;
19+
}
20+
```
21+
22+
## 2. 命令行参数
23+
24+
宿主实现中,`main` 常见原型为 `int main(int argc, char *argv[])`。`argc` 表示参数个数,`argv` 保存参数字符串数组,`argv[0]` 通常是程序名。读取参数时应先检查下标边界,再做解析。
25+
26+
```c
27+
#include <stdio.h>
28+
29+
int main(int argc, char *argv[]) {
30+
for (int i = 0; i < argc; ++i) {
31+
printf("argv[%d] = %s\n", i, argv[i]);
32+
}
33+
return 0;
34+
}
35+
```
36+
37+
## 3. 外部命令执行:`system`
38+
39+
`system` 会把命令字符串交给宿主命令处理器执行。它适合教学示例和简单脚本桥接,但在安全敏感场景应谨慎使用,尤其是命令字符串来自不可信输入时。
40+
41+
```c
42+
#include <stdlib.h>
43+
44+
int main(void) {
45+
return system("echo hello");
46+
}
47+
```
48+
49+
## 4. 边界与建议
50+
51+
环境访问属于“宿主能力”,并非所有目标平台都完整支持。写跨平台代码时,建议把这类能力封装在适配层,不要让业务逻辑直接依赖命令处理器语义。
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# 跳转
2+
3+
除了 `if`、循环和 `goto` 这类结构化控制流,C 标准库还提供了非局部跳转能力:`setjmp``longjmp`。它们定义在 `<setjmp.h>`,可在深层调用栈中快速回到某个恢复点。
4+
5+
## 1. 基本语义
6+
7+
`setjmp(env)` 会保存当前执行上下文,并返回 `0`;后续若调用 `longjmp(env, value)`,程序会跳回该 `setjmp` 位置,并让 `setjmp` 的返回值变为 `value`(若 `value``0`,则返回 `1`)。
8+
9+
```c
10+
#include <setjmp.h>
11+
#include <stdio.h>
12+
13+
static jmp_buf env;
14+
15+
void deep_call(void) {
16+
longjmp(env, 42);
17+
}
18+
19+
int main(void) {
20+
int code = setjmp(env);
21+
if (code == 0) {
22+
deep_call();
23+
} else {
24+
printf("recovered: %d\n", code);
25+
}
26+
return 0;
27+
}
28+
```
29+
30+
## 2. 适用场景
31+
32+
它适合表达“统一失败回收点”这类机制,例如解释器、解析器或受限环境中的错误回退。若普通 `return` 就能清晰表达控制流,应优先使用普通 `return`,因为可读性更高。
33+
34+
## 3. 关键约束
35+
36+
`longjmp` 只能跳回仍然有效的调用栈上下文;若目标函数已经返回,再跳回去就是未定义行为。另外,`setjmp` 与自动存储期对象的可见值之间存在细节约束,涉及优化与寄存器分配时尤其要谨慎。
37+
38+
## 4. 工程建议
39+
40+
把 `setjmp`/`longjmp` 限定在极小范围,并把资源释放策略写成可复核模板。非局部跳转不是异常系统的完全替代,它是底层工具,适合精确控制,不适合泛化滥用。
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,23 @@
11
# 并发支持库
2+
3+
C11 开始,标准库提供了跨平台并发接口,核心头文件是 `<threads.h>``<stdatomic.h>`。它们分别解决“线程与同步原语”以及“原子读写与内存序”两个层面的问题。
4+
5+
其中 `<stdatomic.h>` 在现代实现里通常可用,而 `<threads.h>` 在不同编译器上的支持度不完全一致;需要时可在工程层做一层平台适配。
6+
7+
## 1. 本章结构
8+
9+
本章先介绍线程基础与原子操作,再进入互斥、条件等待与线程局部存储。阅读顺序建议保持与目录一致,因为后续机制都建立在线程和内存可见性的基础上。
10+
11+
1. [22.1 线程](/教程/正文/语法和标准库/22_并发支持/22_1_线程)
12+
2. [22.2 原子操作](/教程/正文/语法和标准库/22_并发支持/22_2_原子操作)
13+
3. [22.3 互斥](/教程/正文/语法和标准库/22_并发支持/22_3_互斥)
14+
4. [22.4 条件等待](/教程/正文/语法和标准库/22_并发支持/22_4_条件变量)
15+
5. [22.5 线程局部存储](/教程/正文/语法和标准库/22_并发支持/22_5_线程局部存储)
16+
17+
## 2. 并发代码的底线
18+
19+
只要多个线程可能同时访问同一对象,就必须明确同步策略:要么使用原子操作,要么使用互斥保护并保证访问路径一致。未同步的并发读写会形成数据竞争,标准层面属于未定义行为。
20+
21+
## 3. 设计建议
22+
23+
并发设计优先追求“正确性可证明”,再考虑性能微调。先写出简单、清晰、可复核的同步模型,再根据压力点做局部优化,通常比一开始追求复杂无锁结构更稳妥。
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# 互斥
2+
3+
互斥量 (Mutex) 用于保证同一时刻只有一个线程进入临界区。它是最常见、最直接的同步原语,适合保护共享对象的一致性。
4+
5+
下文示例基于 `<threads.h>`;若目标工具链未提供该头文件,请把同一思路迁移到平台线程接口。
6+
7+
## 1. 生命周期
8+
9+
互斥量的典型流程是:`mtx_init` 初始化、`mtx_lock` 加锁、`mtx_unlock` 解锁、`mtx_destroy` 销毁。任何一条执行路径只要拿到锁,就必须保证最终释放。
10+
11+
```c
12+
#include <stdio.h>
13+
#include <threads.h>
14+
15+
static mtx_t lock;
16+
static int counter = 0;
17+
18+
int worker(void *arg) {
19+
(void)arg;
20+
for (int i = 0; i < 100000; ++i) {
21+
mtx_lock(&lock);
22+
counter++;
23+
mtx_unlock(&lock);
24+
}
25+
return 0;
26+
}
27+
28+
int main(void) {
29+
thrd_t t1, t2;
30+
31+
if (mtx_init(&lock, mtx_plain) != thrd_success) {
32+
return 1;
33+
}
34+
35+
thrd_create(&t1, worker, NULL);
36+
thrd_create(&t2, worker, NULL);
37+
thrd_join(t1, NULL);
38+
thrd_join(t2, NULL);
39+
40+
printf("counter = %d\n", counter);
41+
mtx_destroy(&lock);
42+
return 0;
43+
}
44+
```
45+
46+
## 2. 易错点
47+
48+
忘记解锁会导致死锁;不同路径上以不一致顺序获取多个锁,会导致循环等待。并发代码出现“偶发卡死”时,首要检查点通常就是锁顺序与异常路径释放策略。
49+
50+
## 3. 实践建议
51+
52+
临界区应尽量短,只放必须受保护的读写;耗时操作放在锁外。这样既减少锁竞争,也降低把系统拖入阻塞链的概率。

0 commit comments

Comments
 (0)