C Programming

Courses

C 程序常见运行错误与排查方法

Resources

C 程序常见运行错误与排查方法

程序成功编译,并不意味着程序一定正确。

运行阶段常见问题包括:

  • 输出结果错误;
  • 输入无法正确读取;
  • 程序没有响应;
  • 程序异常退出;
  • 不同运行得到不同结果;
  • 某些输入正常,某些输入出错。

以下列出 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. 结果依赖未定义行为

下面这些问题都可能产生未定义行为:

  • 数组越界;
  • 空指针解引用;
  • 使用失效指针;
  • 重复释放动态内存;
  • 有符号整数溢出;
  • 对无效地址进行读写。

未定义行为的特点是:

程序可能崩溃,也可能输出错误结果,也可能暂时表现正常。

因此,“运行没有报错”不能证明这些代码是正确的。

更多属于语法、声明、类型检查和链接阶段的问题,参见常见编译错误 。