常见陷阱与最佳实践

翻译整理自 cppreference - 未定义行为 与 CERT C Coding Standard。

1. 未定义行为(UB)

UB 是 C 语言最大的坑,编译器可能任意处理:

int i = 0;
i = i++ + 1;             // UB
arr[i] = i++;             // UB
int x = 1 / 0;            // UB
int y = INT_MAX + 1;      // 有符号溢出 UB
char buf[3] = "abc";      // 没有 \0 结尾

2. 缓冲区溢出

经典漏洞:

char buf[16];
strcpy(buf, argv[1]);          // ❌ 用户输入可能 > 16

// ✅
strncpy(buf, argv[1], sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';

// ✅ 推荐
snprintf(buf, sizeof(buf), "%s", argv[1]);

避免 gets、strcpy、sprintf,使用 fgets、strncpy、snprintf。

3. 整数溢出

int n = ...;
if (n + 1 < n) /* 检查上溢 */   // ❌ 编译器可假设不溢出

// ✅ 用 unsigned 或 pre-check
if (n > INT_MAX - 1) abort();

size_t 是无符号,小心负数比较:

int n = -1;
if (n < sizeof(arr)) ...   // sizeof 返回 size_t,n 转无符号变巨大!

4. 数组与指针混淆

void f(int arr[]) {
    int n = sizeof(arr) / sizeof(arr[0]);  // ❌ arr 退化为指针
}

修复:显式传长度参数。

5. 字符串字面量修改

char *s = "hello";
s[0] = 'H';               // UB(只读段)

用数组:

char s[] = "hello";
s[0] = 'H';               // OK

6. 悬空指针 / 二次释放

int *p = malloc(4);
free(p);
*p = 10;                   // UB
free(p);                   // UB

7. 返回局部变量地址

int *bad(void) {
    int x = 1;
    return &x;             // ❌ 栈失效
}

// ✅ 用 static、堆、或参数传出

8. 类型混淆

int arr[5];
int *p = arr;
double *q = (double *)p;
*q = 1.0;                  // 内存别名对齐问题,UB

9. 未初始化变量

int x;
printf("%d\n", x);        // UB,值不确定

启用 -Wuninitialized 警告。

10. 浮点比较

if (a == 0.1) ...          // 浮点不精确
if (fabs(a - 0.1) < 1e-9) ...

11. 移位运算

1 << 32                   // UB(int 32 位时)

12. 编译警告与静态检查

gcc -std=c11 -Wall -Wextra -Wpedantic -Wconversion -O2 -g prog.c
  • cppcheck:常见错误
  • clang --analyze:静态分析
  • Valgrind:运行时内存
  • AddressSanitizer (-fsanitize=address):内存错误
  • UndefinedBehaviorSanitizer (-fsanitize=undefined):UB 检测
  • MemorySanitizer:未初始化内存

13. 现代编译推荐参数

gcc -std=c11 -Wall -Wextra -Wpedantic \
    -Wconversion -Wshadow -Wformat=2 \
    -Wno-unused-parameter \
    -O2 -g \
    -fsanitize=address,undefined \
    -fno-strict-aliasing \
    prog.c -o prog

14. 头文件设计原则

  • 使用 #ifndef/#pragma once 防重复
  • 不在头文件分配内存
  • 不在头文件写实现(除非 inline)
  • 用 extern 声明全局变量

15. 工程化

  • 一个文件一个清晰的模块
  • 接口放在 *.h,实现在 *.c
  • 用 static 隐藏内部函数
  • 命名:模块前缀,如 vector_push
  • 错误码 + goto cleanup 跳转
  • 用 make 或 CMake 管理
CC=gcc
CFLAGS=-std=c11 -Wall -Wextra -O2

prog: main.c util.c
	$(CC) $(CFLAGS) -o $@ $^

小结

C 语言没有安全网,纪律与工具链就是你的安全网。坚持用警告、 sanitizer、静态分析,写出的 C 代码也能相当稳健。