常见陷阱与最佳实践
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.ccppcheck:常见错误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 prog14. 头文件设计原则
- 使用
#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 代码也能相当稳健。