课堂

代码书写细节备忘

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

根因:首向量为零时没有更新状态。

正确的调试流程:

  1. 构造能够稳定复现 Bug 的最小输入。
  2. 记录预期输出与实际输出。
  3. 逐行追踪关键变量。
  4. 确定第一个偏离预期的步骤。
  5. 解释错误原因,再修改代码。
  6. 重新运行原用例与边界用例,防止引入新错误。

四、循环不变量:用数学思维证明程序正确

循环不变量(Loop Invariant) 在每次循环开始或结束时,始终成立的性质。

以 Gram-Schmidt 为例:

不变量: 在开始处理第 i 个向量之前,q[0] 到 q[i-1] 构成前面输入向量张成空间的一组标准正交基(前提是此前未发现线性相关)。

证明思路:

  1. 初始化:处理第一个向量之前,空向量组满足要求。
  2. 保持:从当前向量减去之前各基向量上的投影;若结果非零,归一化后仍满足正交归一性质。
  3. 终止:若正交化后为零,则当前向量属于之前向量张成的空间;否则处理完所有向量,得到标准正交组。

循环不变量帮助我们从「观察运行结果」进步到「说明算法为什么正确」。

五、日常练习流程

写代码前

  • 描述算法思路
  • 写出输入输出约束
  • 设计至少 3 个边界用例

写代码时

  • 使用有意义的变量名
  • 减少不必要的状态变量
  • 一个循环尽量承担单一职责
  • 危险操作之前检查前提

写代码后

  • 手动模拟正常与异常用例
  • 检查下标、循环终止和返回分支
  • 开启警告并测试

出现 Bug 后

  • 先定位根因,再修改
  • 修复后进行回归测试
  • 记录错误模式,防止下次重犯

六、核心原则

  1. 正确性优先于代码简洁。
  2. 减少状态,比维护复杂状态更可靠。
  3. 先验证操作前提,再执行操作。
  4. 测试负责发现错误,推理负责解释正确性。
  5. 每次 Debug 都应总结可复用的错误模式。

一句话总结 好的程序不是写出来以后碰巧运行正确,而是设计时就有理由相信每一步都正确。