C 语言编程习惯与 Debug 方法
核心思想 从「写完代码后检查结果」转向「写代码时思考为什么每一步一定正确」。
算法主体正确不代表程序正确。边界条件、状态控制和执行顺序同样重要。
一、五个核心编程习惯
1. 先考虑边界条件
写代码前至少设计 3 个特殊测试用例。
以 Gram-Schmidt 正交化为例:
| 测试情况 | 预期结果 | 检查重点 |
|---|---|---|
| 只有一个非零向量 | NO | 循环边界 |
| 第一个向量为零 | YES 1 | 异常状态初始化 |
| 第二个向量与第一个相同 | YES 2 | 归一化前判零 |
| 最后一个向量线性相关 | YES m | 最后一次判断 |
| 全部向量线性无关 | NO | 正常退出 |
| 向量数大于维数 | 必定线性相关 | 数学约束 |
2. 明确变量含义,减少状态变量
不够清晰:
int i = 0, flag = 1;
更清晰:
int independent = 1;
更推荐在发现异常时直接返回:
if (is_0vector(q[i], n)) {
printf("YES %d\n", i + 1);
return 0;
}
原则:每增加一个状态变量,就增加一个需要维护的条件。
3. 一个循环尽量只负责一件事
避免将遍历、判零、退出逻辑全部塞进循环条件。
推荐:
for (int i = 0; i < m; i++) {
// 1. 正交化
// 2. 判断是否为零向量
// 3. 归一化
}
遍历由循环负责,异常情况由循环体处理。
4. 危险操作之前检查前提
| 操作 | 需要满足的前提 |
|---|---|
a[i] |
0 <= i && i < n |
a / b |
b != 0 |
normalize(v) |
向量范数不能过小 |
malloc() |
检查是否为 NULL |
scanf() |
检查输入是否成功 |
| 指针解引用 | 指针有效 |
例如:
if (is_0vector(q[i], n)) {
printf("YES %d\n", i + 1);
return 0;
}
normalize(q[i], n);
检查前提 → 执行操作,不能颠倒顺序。
5. 使用编译器警告与调试工具
gcc main.c -o main -std=c11 -Wall -Wextra -Wpedantic -Wshadow -g -fsanitize=address,undefined -lm
-Wall -Wextra:常见编译警告-Wpedantic:严格标准相关警告-Wshadow:变量名遮蔽警告-g:调试信息-fsanitize=address,undefined:帮助检测部分内存错误与未定义行为-lm:链接数学库(适用时)
注意:编译器无法自动发现所有逻辑错误。
二、提交前检查清单
- 边界条件:考虑
n=1、空数据、首尾元素、全部相同等情况 - 数组访问:每次访问
a[i]时下标是否合法 - 变量初始化:所有参与计算的变量是否已初始化
- 状态变化:标志位、计数器和下标是否在所有分支中正确更新
- 数学前提:除法、开方、归一化是否满足条件
- 执行顺序:先检查再操作,循环先后顺序是否正确
- 极端测试:至少手动构造 3 个特殊测试用例
- 编译检查:查看警告,必要时使用调试器
三、Debug:先定位,再修改
Dry Run(手动模拟执行)
例:Gram-Schmidt 输入
2 2
0 0
1 1
| 步骤 | i | flag | 发生的事情 |
|---|---|---|---|
| 初始化 | 0 | 1 | 读取向量 |
| 检查 q[0] | 0 | 1 | 发现零向量 |
| 跳过 if | 0 | 1 | 未修改 flag |
| 最终输出 | 0 | 1 | 错误输出 NO |
根因:首向量为零时没有更新状态。
正确的调试流程:
- 构造能够稳定复现 Bug 的最小输入。
- 记录预期输出与实际输出。
- 逐行追踪关键变量。
- 确定第一个偏离预期的步骤。
- 解释错误原因,再修改代码。
- 重新运行原用例与边界用例,防止引入新错误。
四、循环不变量:用数学思维证明程序正确
循环不变量(Loop Invariant) 在每次循环开始或结束时,始终成立的性质。
以 Gram-Schmidt 为例:
不变量: 在开始处理第 i 个向量之前,q[0] 到 q[i-1] 构成前面输入向量张成空间的一组标准正交基(前提是此前未发现线性相关)。
证明思路:
- 初始化:处理第一个向量之前,空向量组满足要求。
- 保持:从当前向量减去之前各基向量上的投影;若结果非零,归一化后仍满足正交归一性质。
- 终止:若正交化后为零,则当前向量属于之前向量张成的空间;否则处理完所有向量,得到标准正交组。
循环不变量帮助我们从「观察运行结果」进步到「说明算法为什么正确」。
五、日常练习流程
写代码前
- 描述算法思路
- 写出输入输出约束
- 设计至少 3 个边界用例
写代码时
- 使用有意义的变量名
- 减少不必要的状态变量
- 一个循环尽量承担单一职责
- 危险操作之前检查前提
写代码后
- 手动模拟正常与异常用例
- 检查下标、循环终止和返回分支
- 开启警告并测试
出现 Bug 后
- 先定位根因,再修改
- 修复后进行回归测试
- 记录错误模式,防止下次重犯
六、核心原则
- 正确性优先于代码简洁。
- 减少状态,比维护复杂状态更可靠。
- 先验证操作前提,再执行操作。
- 测试负责发现错误,推理负责解释正确性。
- 每次 Debug 都应总结可复用的错误模式。
一句话总结 好的程序不是写出来以后碰巧运行正确,而是设计时就有理由相信每一步都正确。