打印大法好,别小看printf
写C语言程序时,最怕的就是运行后直接“闪退”或者输出一堆乱码。很多人一上来就想用GDB,其实先用printf打点日志更直接。比如你在循环里怀疑某个变量变了,别猜,直接打出来:
#include <stdio.h>
int main() {
int i;
for (i = 0; i < 10; i++) {
printf("[DEBUG] i 的值是: %d\n", i); // 加个标记,一眼看出执行到哪了
if (i == 5) {
// 模拟出问题
int *p = NULL;
*p = 100; // 崩溃在这里
}
}
return 0;
}
虽然粗暴,但能快速定位执行流程卡在哪。上线前记得把DEBUG行删掉或用宏控制。
用断言assert避免低级错误
空指针、数组越界这类问题,靠肉眼很难查。assert是个好帮手,它在调试时帮你拦住明显错误。比如你写了个处理字符串的函数:
#include <assert.h>
#include <string.h>
void safe_copy(char *dst, const char *src, int len) {
assert(dst != NULL);
assert(src != NULL);
assert(len > 0);
strncpy(dst, src, len);
}
一旦传了NULL进去,程序会直接报错并提示文件行号,比段错误后去翻堆栈强多了。
编译器警告要当回事
很多人编译时看到warning就忽略,其实gcc的-Wall已经能发现不少隐患。比如下面这段代码:
int func() {
int x;
return x; // 使用未初始化变量
}
加上 -Wall 编译,就会提示 warning: ‘x’ is used uninitialized。这种问题在复杂函数里极难发现,但编译器一句话就点出来了。
善用GDB,不止会run和break
GDB不是非得等到崩溃才用。你可以编译时加-g,然后在可疑位置下断点。比如程序在第10次循环出错,可以这样:
gdb ./a.out
(gdb) break main.c:15 if i==10
(gdb) run
条件断点只在i等于10时停下来,省得一遍遍c(continue)。停住之后,用print i查看变量,用bt看调用栈,都很实用。
内存问题交给Valgrind
Linux下调试内存泄漏、非法访问,Valgrind比你自己猜强十倍。比如你malloc了忘了free:
int *p = malloc(4);
*p = 10;
// 忘了 free(p)
用 valgrind --leak-check=full ./a.out 跑一遍,它会明确告诉你哪一行漏了free,多少字节没释放。对野指针访问也能精准捕获。
写点测试用例,别总靠手动输
与其每次运行都手动敲数据,不如准备几个输入文件,比如input1.txt,内容固定。然后程序从stdin读,测试时直接 ./a.out < input1.txt。改完代码一跑,结果不对立马能看出来。配合diff还能自动化比对输出。
宏定义辅助调试
调试信息没必要一直开着。可以用宏控制,在调试时输出,发布时自动去掉:
#ifdef DEBUG
#define LOG(msg, ...) printf("[LOG] " msg "\n", ##__VA_ARGS__)
#else
#define LOG(msg, ...)
#endif
// 使用
LOG("当前索引: %d, 值: %d", i, arr[i]);
编译时加-DDEBUG就开日志,不加就完全不输出,干净又灵活。