C 程序常见运行错误与排查方法
ResourcesC 程序常见运行错误与排查方法
程序成功编译,并不意味着程序一定正确。
运行阶段常见问题包括:
- 输出结果错误;
- 输入无法正确读取;
- 程序没有响应;
- 程序异常退出;
- 不同运行得到不同结果;
- 某些输入正常,某些输入出错。
以下列出 C 程序中常见的运行错误和逻辑错误。
1. 中文字符显示乱码
现象: 中文输出变成乱码、问号或方框。
printf("你好,世界!\n");
常见原因:
- 源文件编码与编译环境不一致;
- 程序输出编码与终端编码不一致;
- 终端字体不支持中文。
建议源文件统一使用 UTF-8。
Windows PowerShell 中可检查:
chcp 65001
如果只是中文显示异常,而数字和英文正常,通常不是程序计算逻辑的问题。
2. 使用未初始化的变量
错误:
int x;
printf("%d\n", x);
x 尚未赋值,其值是不确定的。
正确:
int x = 0;
printf("%d\n", x);
未初始化变量可能导致:
- 每次运行结果不同;
- 出现很大的随机数;
- 后续计算结果异常。
3. scanf 读取失败后继续使用变量
错误:
int x;
scanf("%d", &x);
printf("%d\n", x);
如果用户输入:
abc
scanf 无法读取整数,x 没有获得有效输入值。
建议:
int x;
if (scanf("%d", &x) == 1) {
printf("%d\n", x);
}
scanf 的返回值表示成功完成了多少次转换。
4. scanf 输入格式与实际输入不一致
代码:
int a, b;
scanf("%d%d", &a, &b);
正确输入可以是:
10 20
但如果输入:
10,20
第二个 %d 会在逗号处读取失败。
如果要求输入逗号:
scanf("%d,%d", &a, &b);
对应输入:
10,20
5. %s 遇到空格停止读取
代码:
char text[20];
scanf("%19s", text);
输入:
hello world
实际只读入:
hello
因为 %s 遇到空白字符就停止。
需要读取整行时可使用:
char text[100];
fgets(text, sizeof text, stdin);
6. scanf 后的换行被 fgets 读走
示例:
int age;
char name[100];
scanf("%d", &age);
fgets(name, sizeof name, stdin);
输入:
18
Alice
fgets 可能立即结束,因为前面的 scanf 留下了换行符。
这是混合使用 scanf 和 fgets 时非常常见的问题。
7. %c 读到了换行或空格
代码:
char ch;
scanf("%c", &ch);
%c 不会自动跳过空白字符,因此可能读到之前留下的:
\n
如果希望跳过前面的空白字符:
scanf(" %c", &ch);
注意 %c 前面的空格。
8. 输入的数据超出变量类型范围
例如:
short x;
scanf("%hd", &x);
如果输入的数值超过 short 能表示的范围,结果可能不是预期值。
类似问题也可能出现在:
char
short
int
long
等整数类型中。
9. 整数除法没有得到小数
错误:
double x = 1 / 2;
结果是:
0.0
因为:
1 / 2
首先进行整数除法,结果为 0。
正确:
double x = 1.0 / 2;
结果:
0.5
10. 除数为 0
错误:
int a = 10;
int b = 0;
printf("%d\n", a / b);
整数除以 0 属于未定义行为,程序可能异常退出。
应在除法前检查:
if (b != 0) {
printf("%d\n", a / b);
}
11. 求余时除数为 0
同样:
int x = 10 % 0;
是错误的。
% 运算的右操作数不能为 0。
12. 整数溢出
例如:
int x = 2000000000;
int y = x + x;
printf("%d\n", y);
结果可能与数学计算明显不同。
有符号整数超过其表示范围属于未定义行为。
如果数据范围较大,应考虑:
long long
例如:
long long x = 2000000000LL;
long long y = x + x;
13. 中间计算已经溢出
即使最终变量使用 long long,中间计算也可能先发生溢出。
错误:
int a = 100000;
int b = 100000;
long long x = a * b;
a * b 首先按 int 计算。
正确:
long long x = 1LL * a * b;
或:
long long x = (long long)a * b;
14. 浮点数不能精确表示某些十进制小数
例如:
double x = 0.1 + 0.2;
printf("%.17f\n", x);
结果可能类似:
0.30000000000000004
这是浮点数表示方式造成的正常现象,不是 printf 出错。
15. 直接使用 == 比较浮点数
容易出错:
double x = 0.1 + 0.2;
if (x == 0.3) {
printf("equal\n");
}
由于浮点误差,条件可能不成立。
通常应比较误差:
#include <math.h>
if (fabs(x - 0.3) < 1e-9) {
printf("equal\n");
}
16. 运算符优先级理解错误
示例:
int x = 2 + 3 * 4;
结果是:
14
而不是:
20
因为乘法优先于加法。
如果需要先加:
int x = (2 + 3) * 4;
17. 整数平均值计算错误
错误:
int a = 1;
int b = 2;
double avg = (a + b) / 2;
结果为:
1.0
因为右侧仍然进行整数除法。
正确:
double avg = (a + b) / 2.0;
18. 把赋值 = 写成比较 ==
错误:
if (x = 10) {
printf("yes\n");
}
这里执行的是:
x = 10;
赋值表达式的结果为 10,因此条件为真。
正确:
if (x == 10) {
printf("yes\n");
}
19. if 后多写分号
错误:
if (x > 0);
{
printf("positive\n");
}
这里:
if (x > 0);
已经是一条完整的 if 语句。
因此后面的代码块无论条件是否成立都会执行。
正确:
if (x > 0) {
printf("positive\n");
}
20. for 后多写分号
错误:
for (int i = 0; i < 10; i++);
{
printf("hello\n");
}
循环执行的是空语句:
;
后面的代码块只执行一次。
正确:
for (int i = 0; i < 10; i++) {
printf("hello\n");
}
21. while 后多写分号
错误:
while (x < 10);
{
x++;
}
如果:
x < 10
一直成立,程序可能进入死循环。
正确:
while (x < 10) {
x++;
}
注意:
do {
...
} while (condition);
do ... while 最后的分号是必须的。
22. 循环变量没有更新
错误:
int i = 0;
while (i < 10) {
printf("%d\n", i);
}
i 始终为 0,循环不会结束。
正确:
while (i < 10) {
printf("%d\n", i);
i++;
}
23. 循环变量更新方向错误
错误:
for (int i = 0; i < 10; i--) {
printf("%d\n", i);
}
i 越来越小,因此:
i < 10
长期成立。
正确:
for (int i = 0; i < 10; i++) {
printf("%d\n", i);
}
24. 循环边界多执行一次
错误:
for (int i = 0; i <= 10; i++) {
...
}
如果原本只希望执行:
0 ~ 9
就应该写:
for (int i = 0; i < 10; i++) {
...
}
这是典型的 off-by-one error。
25. 数组下标越界
长度为 3 的数组:
int a[3] = {10, 20, 30};
有效下标只有:
0
1
2
错误:
printf("%d\n", a[3]);
数组越界属于未定义行为,可能:
- 输出异常值;
- 修改其他变量;
- 程序崩溃;
- 看起来完全正常。
“没有崩溃”并不表示代码正确。
26. 数组循环边界错误
错误:
int a[10];
for (int i = 0; i <= 10; i++) {
a[i] = 0;
}
当:
i == 10
时访问:
a[10]
已经越界。
正确:
for (int i = 0; i < 10; i++) {
a[i] = 0;
}
27. 使用负数作为数组下标
错误:
int a[10];
int i = -1;
printf("%d\n", a[i]);
数组下标不能超出有效范围。
应保证:
0 <= i < 数组长度
28. 二维数组行列下标写反
定义:
int a[3][4];
合法范围是:
行:0 ~ 2
列:0 ~ 3
如果误认为是:
a[4][3]
可能造成越界。
29. 字符数组空间不足
错误:
char text[5] = "hello";
字符串:
hello
需要 5 个字符,还需要一个:
'\0'
因此至少需要:
char text[6] = "hello";
也可以让编译器自动计算:
char text[] = "hello";
30. 字符串缺少 '\0'
例如:
char text[5] = {'h', 'e', 'l', 'l', 'o'};
printf("%s\n", text);
数组中没有字符串结束符:
'\0'
printf("%s") 会继续读取后面的内存,直到偶然遇到 '\0'。
正确:
char text[6] = {'h', 'e', 'l', 'l', 'o', '\0'};
或者:
char text[] = "hello";
31. scanf("%s") 导致字符数组越界
危险写法:
char name[10];
scanf("%s", name);
如果输入超过数组容量,可能覆盖其他内存。
限制读取长度:
scanf("%9s", name);
长度为 10 的字符数组最多读取 9 个字符,最后保留一个位置存放:
'\0'
32. fgets 读取的字符串包含换行符
代码:
char text[100];
fgets(text, sizeof text, stdin);
输入:
hello
通常实际读入的是:
"h e l l o \n \0"
因此:
strlen(text)
通常为 6,而不是 5。
如果不需要末尾换行,可以删除:
#include <string.h>
text[strcspn(text, "\n")] = '\0';
33. 使用 == 比较字符串
错误:
char a[] = "hello";
char b[] = "hello";
if (a == b) {
...
}
这里比较的是地址,而不是字符串内容。
应使用:
#include <string.h>
if (strcmp(a, b) == 0) {
...
}
34. 使用 = 给字符数组整体赋值
定义以后:
char text[20];
不能通过:
text = "hello";
改变数组内容。
如果需要复制字符串:
#include <string.h>
strcpy(text, "hello");
同时必须保证目标数组容量足够。
35. 字符串复制超过数组容量
错误:
char text[5];
strcpy(text, "hello");
"hello" 实际需要 6 个字节:
h e l l o \0
这会发生缓冲区越界。
36. 空指针解引用
错误:
int *p = NULL;
printf("%d\n", *p);
p 没有指向有效对象。
对空指针使用:
*p
属于未定义行为,程序通常会异常退出。
37. 未初始化的指针
错误:
int *p;
*p = 10;
p 没有指向确定的位置。
正确示例:
int x;
int *p = &x;
*p = 10;
38. 指针指向已经失效的局部变量
错误:
int *get_value(void)
{
int x = 10;
return &x;
}
函数结束后,局部变量:
x
的生命周期已经结束。
返回的指针成为无效指针。
39. 动态内存申请失败后继续使用
错误:
int *p = malloc(100 * sizeof(int));
p[0] = 10;
malloc 可能返回:
NULL
应先检查:
int *p = malloc(100 * sizeof(int));
if (p != NULL) {
p[0] = 10;
}
需要:
#include <stdlib.h>
40. 动态内存大小计算错误
错误:
int *p = malloc(10);
如果目的是存放 10 个 int,这通常不够。
正确:
int *p = malloc(10 * sizeof(int));
更常用:
int *p = malloc(10 * sizeof *p);
41. 释放内存后继续访问
错误:
int *p = malloc(sizeof *p);
*p = 10;
free(p);
printf("%d\n", *p);
free(p) 以后,这块内存已经不再属于程序。
继续访问属于 use-after-free。
42. 同一块内存释放两次
错误:
free(p);
free(p);
重复释放同一个动态内存块属于未定义行为。
通常释放后可以:
free(p);
p = NULL;
43. 忘记释放动态内存
例如:
int *p = malloc(1000 * sizeof(int));
使用完后一直没有:
free(p);
会产生 内存泄漏(memory leak)。
短程序可能不明显,长期运行或反复申请内存时会逐渐消耗内存。
44. malloc 分配数量和元素大小写反或漏乘
例如希望创建 n 个 double:
double *p = malloc(n);
通常是错误的。
应该:
double *p = malloc(n * sizeof *p);
45. 使用错误的 sizeof
例如:
int *p = malloc(100 * sizeof(p));
这里:
sizeof(p)
得到的是指针本身的大小,而不是 int 的大小。
推荐:
int *p = malloc(100 * sizeof *p);
46. 递归没有正确结束条件
错误:
void f(int n)
{
printf("%d\n", n);
f(n + 1);
}
递归会不断进行,最终可能导致栈空间耗尽。
递归函数必须有能够实际到达的终止条件。
47. 递归终止条件方向错误
例如:
void f(int n)
{
if (n == 0)
return;
f(n + 1);
}
如果最初:
n > 0
那么 n 会越来越大,永远不会到达 0。
48. switch 忘记写 break
代码:
switch (x) {
case 1:
printf("one\n");
case 2:
printf("two\n");
break;
}
当:
x == 1
时,会继续执行 case 2。
如果不希望发生 fall-through:
case 1:
printf("one\n");
break;
49. switch 中错误理解 default
default 并不是程序最后一定执行的代码。
它只在没有任何 case 匹配时执行。
例如:
switch (x) {
case 1:
printf("one\n");
break;
default:
printf("other\n");
}
50. break 只退出一层循环
例如:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
if (j == 5)
break;
}
}
这里的:
break;
只退出内层 for,不会同时退出外层循环。
51. continue 跳过了必要的操作
例如:
int i = 0;
while (i < 10) {
if (i == 5)
continue;
i++;
}
当:
i == 5
时,程序不断执行 continue,而:
i++;
永远无法执行。
结果是死循环。
52. 逻辑与 && 和逻辑或 || 使用错误
例如本来希望:
x 在 0 到 100 之间
正确:
if (x >= 0 && x <= 100) {
...
}
误写为:
if (x >= 0 || x <= 100) {
...
}
这个条件对几乎所有整数都成立。
53. 把区间比较写成数学形式
错误:
if (0 <= x <= 100) {
...
}
C 会先计算:
0 <= x
结果是 0 或 1,然后再比较:
0 <= 100
或:
1 <= 100
因此条件几乎总为真。
正确:
if (x >= 0 && x <= 100) {
...
}
54. ! 的作用范围理解错误
例如:
if (!x > 0)
实际先计算:
!x
再与 0 比较。
如果希望表示:
x 不大于 0
直接写:
if (x <= 0)
更加清楚。
55. && 与 || 优先级理解错误
例如:
if (a || b && c)
实际按照:
if (a || (b && c))
计算。
如果逻辑较复杂,建议显式使用括号:
if ((a || b) && c)
56. 条件表达式写成永远为真的形式
错误:
if (x == 1 || 2) {
...
}
这里的:
2
是非零值,因此条件始终为真。
正确:
if (x == 1 || x == 2) {
...
}
同样不能写:
if (x == 1 || 2 || 3)
57. 错误理解前置和后置自增
例如:
int i = 1;
printf("%d\n", i++);
先输出:
1
然后 i 变为 2。
而:
printf("%d\n", ++i);
先增加,再使用新值。
如果不需要依赖表达式求值结果,通常单独写:
i++;
更容易理解。
58. 在复杂表达式中多次修改同一变量
例如:
i = i++ + ++i;
这类表达式可能涉及未定义行为或难以预测的求值关系。
不要依赖这种写法。
将操作拆开:
i++;
...
通常更清晰、更安全。
59. 忘记更新最大值或最小值
例如求最大值时:
if (x > max) {
printf("%d\n", x);
}
这里只输出了 x,却没有:
max = x;
正确:
if (x > max) {
max = x;
}
60. 最大值、最小值初始值不合理
例如:
int max = 0;
如果所有数据都是负数:
-5 -10 -3
最终可能仍得到:
0
更合理的方式通常是用第一个有效数据初始化:
int max = a[0];
61. 累加变量没有初始化
错误:
int sum;
for (int i = 0; i < 10; i++) {
sum += a[i];
}
sum 初始值不确定。
正确:
int sum = 0;
62. 计数变量没有初始化
错误:
int count;
if (x > 0)
count++;
应该先初始化:
int count = 0;
63. 循环中错误地重新初始化累计变量
错误:
for (int i = 0; i < 10; i++) {
int sum = 0;
sum += a[i];
}
每次循环:
sum
都会重新变成 0。
通常应该:
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += a[i];
}
64. 文件打开失败后继续使用文件指针
错误:
FILE *fp = fopen("data.txt", "r");
fscanf(fp, "%d", &x);
如果文件不存在:
fopen
可能返回:
NULL
继续使用会导致错误。
应该:
FILE *fp = fopen("data.txt", "r");
if (fp != NULL) {
fscanf(fp, "%d", &x);
}
65. 文件路径错误
例如:
fopen("data.txt", "r");
相对路径是相对于程序运行时的当前工作目录,并不一定是源代码所在目录。
因此文件明明存在,程序仍可能打不开。
66. Windows 路径中的反斜杠被当作转义字符
错误:
fopen("C:\temp\data.txt", "r");
例如:
\t
会被解释为制表符。
应写:
fopen("C:\\temp\\data.txt", "r");
或:
fopen("C:/temp/data.txt", "r");
67. 文件读到末尾后继续按成功结果处理
文件读取函数可能返回:
EOF
例如:
while (fscanf(fp, "%d", &x) == 1) {
...
}
比:
while (!feof(fp)) {
fscanf(fp, "%d", &x);
...
}
通常更加可靠。
68. 读取文件后忘记关闭
程序打开文件:
FILE *fp = fopen(...);
使用完后应:
fclose(fp);
特别是写文件时,正常关闭文件也有助于确保缓冲数据被写出。
69. 输出没有立即出现
例如:
printf("Input: ");
scanf("%d", &x);
在某些环境中:
Input:
可能没有立即显示,因为标准输出存在缓冲。
可以:
printf("Input: ");
fflush(stdout);
70. printf 没有换行导致显示结果不符合预期
例如:
printf("%d", x);
printf("%d", y);
如果:
x = 10
y = 20
输出为:
1020
如果希望分开:
printf("%d %d\n", x, y);
71. 输出格式精度错误
例如:
double x = 3.1415926;
printf("%.2f\n", x);
输出:
3.14
这只是显示为两位小数,并不意味着变量 x 本身被修改为 3.14。
72. 程序看起来“卡住”,实际上正在等待输入
例如:
int x;
scanf("%d", &x);
如果程序没有提示信息,运行后只有光标闪烁,学生很容易误以为程序死循环。
实际上程序可能只是在等待键盘输入。
73. 程序窗口一闪而过
在某些 Windows 运行方式中,程序执行完毕后控制台窗口会立即关闭。
这并不一定表示程序崩溃。
在 VS Code 终端中运行程序,可以保留输出结果。
74. 修改代码后运行的仍是旧程序
如果上一次编译成功生成:
main.exe
随后修改代码,但新代码编译失败,旧的:
main.exe
仍可能存在。
再次运行它时,看到的是上一次成功编译的程序,而不是当前源代码。
75. 结果依赖未定义行为
下面这些问题都可能产生未定义行为:
- 数组越界;
- 空指针解引用;
- 使用失效指针;
- 重复释放动态内存;
- 有符号整数溢出;
- 对无效地址进行读写。
未定义行为的特点是:
程序可能崩溃,也可能输出错误结果,也可能暂时表现正常。
因此,“运行没有报错”不能证明这些代码是正确的。
更多属于语法、声明、类型检查和链接阶段的问题,参见常见编译错误 。