什么是运行时错误?
就是你的代码语法上没错,计算机也能成功生成可执行文件,但在程序运行时,因为某些操作不符合逻辑或系统限制,导致程序无法继续执行而崩溃。

当程序发生运行时错误时,操作系统通常会终止它,并可能弹出一个错误窗口,或者在控制台输出一条错误信息,在Linux/macOS下,最常见的信号是 Segmentation Fault (核心已转储)。
常见的运行时错误及解决方法
以下是导致运行时错误的最常见原因,以及如何识别和修复它们。
内存访问错误 (最常见)
这是C语言中最多也最难排查的错误类型,主要与指针和内存管理有关。
a) 空指针解引用
-
错误描述:你试图访问一个值为
NULL或0的指针所指向的内存。
(图片来源网络,侵删) -
示例代码:
#include <stdio.h> #include <stdlib.h> int main() { int *p = NULL; // p 指向空地址 *p = 10; // 尝试向空地址写入数据,导致崩溃 printf("This line will not be printed.\n"); return 0; } -
如何定位:
- 使用调试器(如 GDB)运行程序,当程序崩溃时,调试器会停在出错的那一行,并告诉你变量
p的值是0x0。 - 在错误发生前,检查指针是否为
NULL。
- 使用调试器(如 GDB)运行程序,当程序崩溃时,调试器会停在出错的那一行,并告诉你变量
-
如何修复:
- 防御性编程:在解引用指针前,务必检查它是否为
NULL。if (p != NULL) { *p = 10; } else { printf("Error: Pointer is NULL!\n"); } - 确保指针在赋值前已经被正确初始化。
- 防御性编程:在解引用指针前,务必检查它是否为
b) 野指针
-
错误描述:指针指向的内存空间已经被释放,或者该内存空间从未被分配过,它像一个“野”地址,指向任何地方,访问它是未定义行为,几乎肯定会崩溃。
(图片来源网络,侵删) -
示例代码:
#include <stdio.h> #include <stdlib.h> int main() { int *p = (int*)malloc(sizeof(int)); *p = 100; free(p); // p 指向的内存被释放了 // p 就是一个野指针 *p = 200; // 尝试访问已释放的内存,导致崩溃 printf("This line will not be printed.\n"); return 0; } -
如何定位:与空指针解引用类似,调试器会指出你访问了一个无效的内存地址,这个地址可能不是
0x0,而是一个看起来随机的值。 -
如何修复:
- 在
free()指针后,立即将指针设置为NULL,这是一种好习惯,可以防止后续的误用。free(p); p = NULL; // 将指针置为空
- 在
c) 数组越界访问
-
错误描述:访问数组元素时,使用了超出其合法范围的索引(小于0或大于等于数组大小)。
-
示例代码:
#include <stdio.h> int main() { int arr[5] = {0, 1, 2, 3, 4}; // 数组索引范围是 0 到 4 int value = arr[5]; // 访问 arr[5],越界了! printf("Value: %d\n", value); // 程序可能崩溃,或者打印一个垃圾值 return 0; } -
如何定位:
- 调试器可以帮你定位到出错的数组访问行。
- 仔细检查你的循环条件,确保
i < size而不是i <= size。
-
如何修复:
- 严格检查数组索引的范围,确保它在
[0, size-1]之间。 - 使用
sizeof运算符计算数组大小时要小心,对于函数参数会退化为指针。
- 严格检查数组索引的范围,确保它在
d) 未初始化的内存访问
-
错误描述:你试图读取一块内存(通常是堆或栈上分配的)中的数据,但没有先给它赋值,这块内存里是随机的“垃圾数据”。
-
示例代码:
#include <stdio.h> #include <stdlib.h> int main() { int *p = (int*)malloc(sizeof(int)); // 分配了内存,但未初始化 printf("Value: %d\n", *p); // 打印一个随机的垃圾值,如果运气不好,这个值可能被当作地址,导致后续崩溃 return 0; } -
如何定位:程序行为不确定,可能打印奇怪的数字,或者在某些情况下崩溃,调试器可以显示变量的值是无效的。
-
如何修复:
- 在使用变量前,始终对其进行初始化。
int *p = (int*)malloc(sizeof(int)); *p = 0; // 或者你想要的任何初始值
- 在使用变量前,始终对其进行初始化。
栈溢出
-
错误描述:函数调用会在栈上分配空间,如果一个函数递归调用自己太深,或者定义了过大的局部变量(如巨大的数组),就会耗尽栈的内存空间,导致栈溢出。
-
示例代码:
#include <stdio.h> void recursive_function() { int large_array[100000]; // 每次调用都分配一个巨大的数组 recursive_function(); // 无限递归 } int main() { recursive_function(); return 0; } -
如何定位:程序崩溃,错误信息通常是
Segmentation Fault,在Linux下,可以用ulimit -s查看栈大小限制。 -
如何修复:
- 避免无限递归,确保递归有明确的终止条件。
- 对于需要大量内存的局部变量,考虑使用
malloc在堆上分配。
数学错误
-
错误描述:进行非法的数学运算,如除以零。
-
示例代码:
#include <stdio.h> int main() { int a = 10; int b = 0; int result = a / b; // 除以零! printf("Result: %d\n", result); return 0; } -
如何定位:程序会收到一个信号(如
SIGFPE),导致崩溃。 -
如何修复:
- 在进行除法运算前,检查除数是否为零。
if (b != 0) { result = a / b; } else { printf("Error: Division by zero!\n"); }
- 在进行除法运算前,检查除数是否为零。
其他库函数错误
-
错误描述:标准库函数在遇到错误时,可能会通过返回值或设置全局变量
errno来指示错误,如果你忽略了这些错误检查,可能会导致后续操作失败。 -
示例代码:
#include <stdio.h> #include <stdlib.h> int main() { // 假设系统内存不足,malloc 失败 int *p = (int*)malloc(1000000000000000000); // 尝试分配一个巨大的内存块 if (p == NULL) { // 必须检查 malloc 的返回值! perror("malloc failed"); // 打印错误信息 return 1; // 返回非零表示错误 } *p = 10; free(p); return 0; } -
如何定位:库函数文档会说明在什么情况下会返回错误。
-
如何修复:
- 始终检查可能失败的库函数的返回值(如
malloc,fopen,scanf等)。 - 使用
perror或strerror(errno)来打印有意义的错误信息。
- 始终检查可能失败的库函数的返回值(如
调试工具:你的最佳朋友
遇到运行时错误,不要慌张,善用调试工具可以让你事半功倍。
GDB (GNU Debugger) - Linux/macOS
GDB是Linux和macOS下最强大的调试工具。
- 编译时加
-g:gcc -g your_program.c -o your_program,这会生成包含调试信息的可执行文件。 - 启动GDB:
gdb ./your_program - 常用命令:
run: 运行程序。break main或b your_file.c:15: 在main函数或指定行设置断点。next或n: 逐过程执行(不进入函数)。step或s: 逐语句执行(进入函数)。print var_name或p var_name: 打印变量的值。backtrace或bt: 查看函数调用栈,帮你了解程序是如何走到当前这一步的。info locals: 查看当前作用域内的所有局部变量。quit: 退出GDB。
当程序在GDB中崩溃时,它会告诉你是在哪一行代码崩溃的,以及当时的变量状态,这对于定位问题至关重要。
IDE内置调试器 (Visual Studio, CLion, Xcode)
现代集成开发环境(IDE)通常提供了图形化的调试器,它们封装了GDB等底层工具,使用起来更直观。
- 设置断点:点击代码行号旁边即可。
- 调试控制:通常有“继续”、“单步跳过”、“单步进入”等按钮。
- 查看变量:将鼠标悬停在变量上,或在“监视”窗口中添加变量名来实时查看其值。
AddressSanitizer (ASan) - 查找内存错误
AddressSanitizer 是一个极其强大的内存错误检测工具,它集成在GCC和Clang中,它能帮你快速发现数组越界、内存泄漏、使用已释放内存等问题。
- 编译时加选项:
gcc -fsanitize=address -g your_program.c -o your_program
- 运行:
./your_program - 效果:如果程序有内存错误,ASan会打印出非常详细的报告,包括错误类型、出错位置(文件名、行号)、以及导致错误的内存分配点,这比普通调试器给出的信息要精确得多。
总结与最佳实践
-
预防胜于治疗:编写代码时就要考虑健壮性。
- 检查指针:永远不要解引用未检查是否为
NULL的指针。 - 检查返回值:始终检查可能失败的函数(如
malloc,fopen,scanf)的返回值。 - 注意边界:处理数组、字符串时,时刻注意边界条件。
- 初始化变量:在使用任何变量前,确保它已经被正确初始化。
- 检查指针:永远不要解引用未检查是否为
-
善用工具:
- 编译器警告:使用
-Wall -Wextra编译选项,让编译器帮你发现潜在问题。 - 调试器:GDB或IDE调试器是定位运行时错误的利器。
- 静态分析工具:如
clang-tidy,可以在不运行代码的情况下分析代码缺陷。 - 动态分析工具:如
AddressSanitizer,是发现内存错误的“神器”。
- 编译器警告:使用
遇到运行时错误是学习C语言过程中必不可少的一环,通过理解这些常见错误的原因,并掌握调试工具的使用,你将能更快、更准地定位和解决问题,从而写出更可靠的代码。
