C Programming Language

Practice #01: Summary of Common Mistakes for Beginners

# Basic Tutorial,  # Programming Language,  # C Programming Language

Last Updated: 2020-01-01


前置知识

在阅读本文档前,请先阅读本教程的整体介绍,详见 Introduction


前言

因为我曾帮助过相当多的 C 语言初学者,在大量的答疑和帮调试之后,我对大多数的初学者常见错误都了如指掌,对于初学者经常会做哪些“蠢事”比较清楚,所以我相信这篇文章能够在一定程度上帮到读者。

但是一个人的记忆力终究是有极限的,恐怕难免会有所疏漏,还需要其他人为我提供思路,帮我补全常见错误。感谢 Lingzhi Pan 和 Zicheng Zhang 两位同学的帮助。

本文中记载的主要是最简单的那些常见错误,对于那些可能需要经过 DEBUG 才能发现的错误,我将其放在另一篇文章中介绍,详见 Practice #02: Summary of Basic Debugging Skills


代码无关错误汇总

修改代码后未重新编译

有时因为粗心或是 DEBUG 时着急,初学者会在修改程序后,在 IDE 中直接点击“运行”而不是“编译运行”,此时运行的是你修改前的程序,因为新的程序没有被编译,而旧的程序编译出来的可执行文件还存在。因此,请确保自己总是在运行最新的程序。

另外一种情况是,修改后的程序编译失败,而如果初学者运行程序的方式是分别按下 IDE 中的“编译”按钮和“运行”按钮,那么因为编译失败,新的程序没有被编译,运行的仍然是旧的程序,且还误以为自己已经编译了最新的程序。因此,我建议总是使用“编译运行”按钮一键完成编译和运行,因为几乎所有 IDE 的一键编译运行都是一旦编译失败就会阻止程序运行的,可以完全避免此类粗心问题。

还有一种极端情况是,虽然使用的是“编译运行”按钮,但是初学者可能无意间在 IDE 中启用了“如果编译不通过就运行旧的可运行的程序”的设置(部分 IDE 有这个功能),那就糟糕了,建议将其关闭以免不小心被坑。


粗心错误汇总

main 拼写错误

把 main 写成 mian ,例如:

int mian() {
    return 0;
}

程序会报告编译错误,但是有些初学者读不懂这个报错。上述代码在我本机的编译错误如下,不同运行环境中的结果可能有所不同,例如有时可能会是 Exit 1 status 之类的,但都是表示 main 函数未定义。

| | undefined reference to `WinMain@16'

漏打表达式末尾的分号

漏打表达式末尾的分号,例如:

int main(void) {
    int x = 1
    short y = 2;
    return 0;
}

上述代码的编译错误如下:

| | In function 'main':
|3| error: expected ',' or ';' before 'short'

取决于出错语句的下文,通常会在出错语句的下一个词法单元处报错。

scanf 取地址错误

忘记对变量取地址,例如:

int x;
scanf("%d", x);
printf("x = %d\n", x);

这将触发未定义行为(非法内存访问), scanf 会以 xx 中此时的值作为地址,并试图向该地址传输数据。此时程序很有可能会崩溃,也可能不会,并将数值读入到一个随机的地方。

scanf 类型错误

没有使用和变量类型正确对应的格式化字符,例如:

char ch;
scanf("%d", &ch);

这将触发未定义行为,不同运行环境会出现不同的结果,可能会导致程序直接崩溃,可能会正确读入该变量、但是下一个变量的读入出现古怪的错误,可能会读入一个奇怪的值(零或随机乱数),可能会维持 xx 被读入之前的值不变。

scanf 格式错误

没有按照自己 scanf 中所写的格式进行输入。在默认情况下 scanf 使用空格和换行作为分隔符,但如果希望使用逗号或其它符号作为分隔符,则需要特殊指定。

对于如下代码:

int a, b, c;
scanf("%d,%d,%d", &a, &b, &c);

正确的输入格式为:

1,2,3

对于如下代码:

int a, b, c;
scanf("%d %d %d", &a, &b, &c);

正确的输入格式为:

1 2 3

或:

1
2
3

或:

1 2
3

对于如下代码:

int a, b, c;
scanf("%d%d%d", &a, &b, &c);

正确的输入格式同第二份代码。

使用变量前未初始化

使用变量前未初始化,例如:

int x;
int y = x + 1;

这将触发未定义行为,局部变量 xx 未经初始化,不同运行环境会出现不同的结果,可能其初始值为 00 ,可能其初始值是一个随机乱数,甚至可能会导致程序直接崩溃。

有时在本地编译无法暴露出此问题,例如:

int x = 1, y = 2, z = 3;
int sum;
sum += x;
sum += y;
sum += z;

局部变量 sumsum 的初始值有时会是 00 ,这导致本地运行结果显得十分正常,但是在其它人的电脑上或是在线上评测机就会产生错误,对初学者来说是最常见的难排查的问题之一。

但是下面的代码是没有问题的:

int x;
 
int main(void) {
    int y = x + 1;
    return 0;
}

因为此时 xx 是全局变量,它将被默认初始化为 00 ,因此 yy 的值为 11

相等比较运算符少打一个等号

在使用相等比较运算符时少打一个等号,例如:

int a = 1, b = 0;
int c = 0;
if (a = 0 && b == 0) {
    printf("true1\n");
}
if (c = 1) {
    printf("true2\n");
}
printf("a = %d, b = %d, c = %d\n", a, b, c);

此时赋值运算符会先对变量进行赋值,然后根据变量值是否为 00 作为布尔表达式真假的判定标准。

上述代码的运行结果如下:

true2
a = 0, b = 0, c = 1

在条件或循环语句末尾误加分号

在 if/for/while 等语句末尾不小心多加一个分号,例如:

if (a == 1);
{
    printf("a = 1\n");
}

此时大括号中的内容与 if 语句无关,由于分号的存在,使得第 11 行代码自成一体。

有时这种错误还会导致死循环,因此需要格外小心,例如:

int i = 1;
while (i < 5);
{
    i ++;
}

通常只有习惯于大括号换行的同学才会犯下该错误,因为如果大括号不换行,该错误是比较容易发现的:

if (a == 1); {
    printf("a = 1\n");
}

把多层 if/else 结构看错

由于未加大括号且缩进混乱,导致把多层 if/else 结构看错,例如:

int a, b;
scanf("%d %d", &a, &b);
if (a == 1)
    if (b == 2)
        printf("a = 1 and b = 2\n");
else
    printf("a != 1\n");

如果输入 1 2 ,则输出结果为:

a = 1 and b = 2

如果输入 1 3 ,则输出结果为:

a != 1

如果输入 2 2 ,则程序不会输出任何内容。

在不使用大括号的情况下, else 默认与最近的 if 结合,与你如何缩进没有任何关系。要想达成我们所需的效果,至少应当写成:

if (a == 1) {
    if (b == 2)
        printf("a = 1 and b = 2\n");
} else {
    printf("a != 1\n");
}

为避免引起混淆,建议初学者在任何情况下都为 if/for/while 等语句加上大括号:

if (a == 1) {
    if (b == 2) {
        printf("a = 1 and b = 2\n");
    }
} else {
    printf("a != 1\n");
}

在 for 循环中误用逗号

在 for 循环中误用逗号而不是分号,例如:

int a[5];
int n = 5;
int i;
for (i = 0, i < n, i ++) {
    printf("%d\n", a[i]);
}

上述代码的编译错误如下:

| | In function 'main':
|4| error: expected ';' before ')' token
|4| error: expected expression before ')' token

如果是在 C99 标准下,可以在 for 循环内部定义局部变量 ii ,例如:

int a[5];
int n = 5;
for (int i = 0, i < n, i ++) {
    printf("%d\n", a[i]);
}

上述代码的编译错误如下:

| | In function 'main':
|3| error: expected '=', ',', ';', 'asm' or '__attribute__' before '<' token
|6| error: expected expression before '}' token
|6| error: expected expression before '}' token

在 switch 中忘记使用 break 语句

在 switch 中忘记使用 break 语句,例如:

char grade;
scanf("%c", &grade);
switch (grade) {
case 'A':
    printf("Excellent!\n");
case 'B':
    printf("Good!\n");
case 'C':
    printf("Bad!\n");
default:
    printf("Illegal input!\n");
}

当你输入 B 时,上述代码的运行结果如下:

Good!
Bad!
Illegal input!

如果不使用 break 语句,结果并不是你臆想中的只执行第一个满足条件的 case 分支中的内容。事实上,从第一个满足条件的 case 分支开始,后面的代码会一直继续执行,直到全部执行完毕或是遇到一个 break 语句为止。

使用全角字符

在程序中使用全角字符,例如:

#include <stdio.h>
 
int main(void) {
    int x, y;
    scanf("%d %d", &x, &y);
    if (x == y) {
        printf("x = y\n");
    }
    return 0;
}

上述代码在我本机的编译错误如下,不同运行环境中的结果可能有所不同:

| | In function 'main':
|6| error: stray '\243' in program
|6| error: stray '\250' in program
|6| error: expected '(' before 'x'
|6| error: stray '\243' in program
|6| error: stray '\251' in program

一般看到类似这种莫名其妙的报错,多半是因为不小心使用了全角字符,下面给出易混淆的几个字符的对照表,供初学者加以区分:

半角字符全角字符
(
)
;
:
!
,

除了这些以外,最麻烦的就是全角空格,因为从外观上完全看不出来差异。一般使用 tab 缩进的人不太会被这个坑到,但如果习惯用空格缩进则有可能会踩坑。请看下面这段代码:

int main(void) {
    int x;
    short y;
  char ch;
  x = 1;
   y = 2;
    return 0;
}

上述代码在我本机的编译错误如下,不同运行环境中的结果可能有所不同:

| | In function 'main':
|4| error: stray '\241' in program
|4| error: stray '\241' in program
|4| error: stray '\241' in program
|4| error: stray '\241' in program
|5| error: stray '\241' in program
|5| error: stray '\241' in program
|5| error: stray '\241' in program
|5| error: stray '\241' in program
|6| error: stray '\241' in program
|6| error: stray '\241' in program

请读者试着从上述代码中找到所有的全角空格,共有 55 个。

处理这种令人头疼的问题有两个比较好的办法,一是全文查找全角空格字符“ ”,二是利用 IDE 的解缩进快捷键(例如 CodeBlocks 是 Shift + Tab 键)对全文解缩进,看哪些行无法解除,则说明存在全角空格。

另外有时也会因为在输入时误用了全角字符,导致输入错误,例如:

int a, b;
scanf("%d,%d", &a, &b);

如果输入:

1,2

则会触发未定义行为,输入的结果将是不确定的错误结果。

误用全角字符多半是因为在编程时不经意间切换到了中文输入法,因此请读者小心注意自己的输入法。当然,一劳永逸的方法还是记住半角字符和全角字符在外表上的区别。

字符串未初始化

字符串未初始化,例如:

char s[10];
s[0] = 'a';
s[1] = 'b';
s[2] = 'c';
printf("%s\n", s);

上述代码是非常危险的,这将触发未定义行为,在一些编译环境中,你或许就能看到臭名昭著的“烫烫烫的锟斤拷”了。

因为我们没有初始化 ss ,即便你手动设置了 s[0], s[1], s[2]s[0],\ s[1],\ s[2] 的值,由于 s[3]s[3] 未初始化,它不一定是 \0 字符,此时 printf 函数就不能准确识别字符串的末尾,它很可能会输出大量的随机混乱结果,也可能会导致程序崩溃。

但如果 ss 是由输入得到的,因为 scanf 函数在输入字符串时会自动在结尾追加 \0 字符,只要输入的字符串长度不超过范围就不会出问题。

函数未设置返回值

在返回类型不是 void 的函数中,未给函数设置返回值,或是没有保证任何情况下函数总是有返回值,例如:

int example1() {
    int x = 1;
    int y;
    y = 2 * x + 1;
}
int example2(int x) {
    int y = x * x + 2 * x + 1;
    if (y != 1) {
        return 5;
    }
}

这属于未定义行为,可能会导致不可预料的结果。可能会返回一个 00 ,但也可能返回一个乱数,或是程序崩溃,或是直接编译错误。


理解错误汇总

混淆 C/C++ 语言

注意,C 和 C++ 是完全不同的语言,即便它们的语法极其相似,甚至它们的大部分语法特性都是相同的,但并非总是如此。

萌新在写代码的时候注意自己新建的文件的后缀是 .c 还是 .cpp ,你用的编程软件一般会根据你的后缀决定把你的代码视为 C 还是 C++ 。

如果你是在学习 C 语言,就不要在后缀为 .cpp 的文件中写代码,那样会导致一些本来在 C 语言中不正确的语法被编译通过,也会导致一些在 C 和 C++ 语法规则不同的代码产生不符合 C 语言规范的运行结果。一定要确保使用 .c 文件。

分不清数字和数字字符

在 C 语言中(或者说,几乎所有编程语言中),数值 0 和字符 '0' 是完全不同的。

int x = 0;
char ch = '0';
printf("%d", (int)ch);

试图级联比较运算符

误以为 C 语言的比较运算和数学的比较运算一样,例如:

int a = 2, b = 1, c = 3;
if (a < b < c) {
    printf("a < b < c");
}

事实上 C 语言并不支持级联比较运算符,上述代码并不会像你想象中那样执行。因为 < 是自左向右结合的,所以上述代码事实上等价于:

int a = 2, b = 1, c = 3;
if ((a < b) < c) {
    printf("a < b < c");
}

先求出 a<ba < b 的结果为 0011 ,再将该值与 cc 相比较,因此相当于求 0 < 3 ,结果为真。

未重视运算符优先级

例如在并用 ||&& 时,如有必要需加括号。

混淆数组的定义和使用

在传递数组首地址时误用为数组访问,例如:

int f(int arr[]) {
    // ...
}
 
int a[5] = {1, 3, 5, 4, 2};
int x = max(a[5]);

混淆字符和字符串

在 C 语言中,字符和字符串是完全不同的概念(在其它语言中未必如此),例如:

char ch = 'a';
char arr[1] = "abc";
char * str = "abc";

在这里,ch 是一个 char 类型的变量,arr 是一个长为 33 的字符数组,str 是一个指向常量字符串 "abc" 的指针,三者是截然不同的。

字符串未给 \0 字符预留空间

误以为字符串的实际存储长度等同于你输入的字符个数,例如:

char s[5] = "abcde";
printf("%s\n", s);

由于未给 \0 字符留出位置,下面调用 printf 函数时会触发未定义行为。应当将 ss 的长度设为 66 或更大。

类型转换相关问题

对隐式类型转换的规则不了解,例如:

int x = 1;
double y = x / 2;
printf("%.1f\n", y);

上述代码的运行结果如下:

0.0

根据 C 语言的规则,上述代码第 22 行会先计算 x / 2 的结果,然后再将结果赋给变量 yy 。由于变量 xx 和字面量 2 都是整数类型,运算结果将向下取整得到 0

若想得到 0.5 的结果,应当使用下列表达式之一:

double y = (double)x / 2;
double y = x / 2.0;
double y = 1.0 * x / 2;

注意下面的代码是不行的,因为 x / 2 仍然会先被计算:

double y = (double)(x / 2);

关于类型转换的更多细节,详见 C 语言的 04 - Type Conversion 文档。

输入输出相关问题

详见 C 语言的 03 - Input and Output Streams 文档。

指针相关问题

详见 C 语言的 02 - Pointer 文档。

表达式未定义行为相关问题

一些初学者可能会无意间写出如下看起来很正常的代码:

while (i < n) {
    a[i] = i++;
}

事实上,代码第 22 行会触发未定义行为,这触及了 C 语言的黑暗面。这对于初学者来说可能有些难以理解,因此你只需要记住:

  • 不要在一个表达式中修改一个变量两次及以上;

  • 不要在一个表达式中修改一个变量,并在表达式内的别处读取该变量的值。

诸如此类的奇奇怪怪的表达式都是违法的:

int i = 1;
int a = i++ + ++i;
int b = i++ + i++;
int c = i + i++;
int d = i + ++i;
int arr[10];
arr[i] = i++;

你可能见过一些垃圾教学者夸夸其谈地分析此类代码的运行结果,并且看起来十分有道理。如果是这样的话,那么很不幸,你被坑了。事实上,这些代码的执行结果是不确定的。

对于相同的代码,在不同的编译环境下,甚至是不同的代码上下文中,你都可能会得到不同的运行结果。在 C 语言标准中明确规定了这些代码都是“错误的”。

关于未定义行为的更多知识,详见 C 语言的 03 - Undefined Behavior and Unspecified Behavior 文档。


更新记录

更新时间更新内容
2020.3.1追加新的常见错误条目
2020.7.15追加新的常见错误条目
2022.3.1追加新的常见错误条目