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 写成 mian ,例如:
程序会报告编译错误,但是有些初学者读不懂这个报错。上述代码在我本机的编译错误如下,不同运行环境中的结果可能有所不同,例如有时可能会是 Exit 1 status
之类的,但都是表示 main 函数未定义。
漏打表达式末尾的分号,例如:
上述代码的编译错误如下:
取决于出错语句的下文,通常会在出错语句的下一个词法单元处报错。
忘记对变量取地址,例如:
这将触发未定义行为(非法内存访问), scanf 会以 中此时的值作为地址,并试图向该地址传输数据。此时程序很有可能会崩溃,也可能不会,并将数值读入到一个随机的地方。
没有使用和变量类型正确对应的格式化字符,例如:
这将触发未定义行为,不同运行环境会出现不同的结果,可能会导致程序直接崩溃,可能会正确读入该变量、但是下一个变量的读入出现古怪的错误,可能会读入一个奇怪的值(零或随机乱数),可能会维持 被读入之前的值不变。
没有按照自己 scanf 中所写的格式进行输入。在默认情况下 scanf 使用空格和换行作为分隔符,但如果希望使用逗号或其它符号作为分隔符,则需要特殊指定。
对于如下代码:
正确的输入格式为:
对于如下代码:
正确的输入格式为:
或:
或:
对于如下代码:
正确的输入格式同第二份代码。
使用变量前未初始化,例如:
这将触发未定义行为,局部变量 未经初始化,不同运行环境会出现不同的结果,可能其初始值为 ,可能其初始值是一个随机乱数,甚至可能会导致程序直接崩溃。
有时在本地编译无法暴露出此问题,例如:
局部变量 的初始值有时会是 ,这导致本地运行结果显得十分正常,但是在其它人的电脑上或是在线上评测机就会产生错误,对初学者来说是最常见的难排查的问题之一。
但是下面的代码是没有问题的:
因为此时 是全局变量,它将被默认初始化为 ,因此 的值为 。
在使用相等比较运算符时少打一个等号,例如:
此时赋值运算符会先对变量进行赋值,然后根据变量值是否为 作为布尔表达式真假的判定标准。
上述代码的运行结果如下:
在 if/for/while 等语句末尾不小心多加一个分号,例如:
此时大括号中的内容与 if 语句无关,由于分号的存在,使得第 行代码自成一体。
有时这种错误还会导致死循环,因此需要格外小心,例如:
通常只有习惯于大括号换行的同学才会犯下该错误,因为如果大括号不换行,该错误是比较容易发现的:
由于未加大括号且缩进混乱,导致把多层 if/else 结构看错,例如:
如果输入 1 2
,则输出结果为:
如果输入 1 3
,则输出结果为:
如果输入 2 2
,则程序不会输出任何内容。
在不使用大括号的情况下, else 默认与最近的 if 结合,与你如何缩进没有任何关系。要想达成我们所需的效果,至少应当写成:
为避免引起混淆,建议初学者在任何情况下都为 if/for/while 等语句加上大括号:
在 for 循环中误用逗号而不是分号,例如:
上述代码的编译错误如下:
如果是在 C99 标准下,可以在 for 循环内部定义局部变量 ,例如:
上述代码的编译错误如下:
在 switch 中忘记使用 break 语句,例如:
当你输入 B
时,上述代码的运行结果如下:
如果不使用 break 语句,结果并不是你臆想中的只执行第一个满足条件的 case 分支中的内容。事实上,从第一个满足条件的 case 分支开始,后面的代码会一直继续执行,直到全部执行完毕或是遇到一个 break 语句为止。
在程序中使用全角字符,例如:
上述代码在我本机的编译错误如下,不同运行环境中的结果可能有所不同:
一般看到类似这种莫名其妙的报错,多半是因为不小心使用了全角字符,下面给出易混淆的几个字符的对照表,供初学者加以区分:
半角字符 | 全角字符 |
---|---|
( | ( |
) | ) |
; | ; |
: | : |
! | ! |
, | , |
除了这些以外,最麻烦的就是全角空格,因为从外观上完全看不出来差异。一般使用 tab 缩进的人不太会被这个坑到,但如果习惯用空格缩进则有可能会踩坑。请看下面这段代码:
上述代码在我本机的编译错误如下,不同运行环境中的结果可能有所不同:
请读者试着从上述代码中找到所有的全角空格,共有 个。
处理这种令人头疼的问题有两个比较好的办法,一是全文查找全角空格字符“ ”,二是利用 IDE 的解缩进快捷键(例如 CodeBlocks 是 Shift + Tab 键)对全文解缩进,看哪些行无法解除,则说明存在全角空格。
另外有时也会因为在输入时误用了全角字符,导致输入错误,例如:
如果输入:
则会触发未定义行为,输入的结果将是不确定的错误结果。
误用全角字符多半是因为在编程时不经意间切换到了中文输入法,因此请读者小心注意自己的输入法。当然,一劳永逸的方法还是记住半角字符和全角字符在外表上的区别。
字符串未初始化,例如:
上述代码是非常危险的,这将触发未定义行为,在一些编译环境中,你或许就能看到臭名昭著的“烫烫烫的锟斤拷”了。
因为我们没有初始化 ,即便你手动设置了 的值,由于 未初始化,它不一定是 \0
字符,此时 printf 函数就不能准确识别字符串的末尾,它很可能会输出大量的随机混乱结果,也可能会导致程序崩溃。
但如果 是由输入得到的,因为 scanf 函数在输入字符串时会自动在结尾追加 \0
字符,只要输入的字符串长度不超过范围就不会出问题。
在返回类型不是 void 的函数中,未给函数设置返回值,或是没有保证任何情况下函数总是有返回值,例如:
这属于未定义行为,可能会导致不可预料的结果。可能会返回一个 ,但也可能返回一个乱数,或是程序崩溃,或是直接编译错误。
注意,C 和 C++ 是完全不同的语言,即便它们的语法极其相似,甚至它们的大部分语法特性都是相同的,但并非总是如此。
萌新在写代码的时候注意自己新建的文件的后缀是 .c 还是 .cpp ,你用的编程软件一般会根据你的后缀决定把你的代码视为 C 还是 C++ 。
如果你是在学习 C 语言,就不要在后缀为 .cpp 的文件中写代码,那样会导致一些本来在 C 语言中不正确的语法被编译通过,也会导致一些在 C 和 C++ 语法规则不同的代码产生不符合 C 语言规范的运行结果。一定要确保使用 .c 文件。
在 C 语言中(或者说,几乎所有编程语言中),数值 0
和字符 '0'
是完全不同的。
误以为 C 语言的比较运算和数学的比较运算一样,例如:
事实上 C 语言并不支持级联比较运算符,上述代码并不会像你想象中那样执行。因为 <
是自左向右结合的,所以上述代码事实上等价于:
先求出 的结果为 或 ,再将该值与 相比较,因此相当于求 0 < 3
,结果为真。
例如在并用 ||
和 &&
时,如有必要需加括号。
在传递数组首地址时误用为数组访问,例如:
在 C 语言中,字符和字符串是完全不同的概念(在其它语言中未必如此),例如:
在这里,ch
是一个 char 类型的变量,arr
是一个长为 的字符数组,str
是一个指向常量字符串 "abc"
的指针,三者是截然不同的。
误以为字符串的实际存储长度等同于你输入的字符个数,例如:
由于未给 \0
字符留出位置,下面调用 printf 函数时会触发未定义行为。应当将 的长度设为 或更大。
对隐式类型转换的规则不了解,例如:
上述代码的运行结果如下:
根据 C 语言的规则,上述代码第 行会先计算 x / 2
的结果,然后再将结果赋给变量 。由于变量 和字面量 2
都是整数类型,运算结果将向下取整得到 0
。
若想得到 0.5
的结果,应当使用下列表达式之一:
注意下面的代码是不行的,因为 x / 2
仍然会先被计算:
关于类型转换的更多细节,详见 C 语言的 04 - Type Conversion 文档。
详见 C 语言的 03 - Input and Output Streams 文档。
详见 C 语言的 02 - Pointer 文档。
一些初学者可能会无意间写出如下看起来很正常的代码:
事实上,代码第 行会触发未定义行为,这触及了 C 语言的黑暗面。这对于初学者来说可能有些难以理解,因此你只需要记住:
不要在一个表达式中修改一个变量两次及以上;
不要在一个表达式中修改一个变量,并在表达式内的别处读取该变量的值。
诸如此类的奇奇怪怪的表达式都是违法的:
你可能见过一些垃圾教学者夸夸其谈地分析此类代码的运行结果,并且看起来十分有道理。如果是这样的话,那么很不幸,你被坑了。事实上,这些代码的执行结果是不确定的。
对于相同的代码,在不同的编译环境下,甚至是不同的代码上下文中,你都可能会得到不同的运行结果。在 C 语言标准中明确规定了这些代码都是“错误的”。
关于未定义行为的更多知识,详见 C 语言的 03 - Undefined Behavior and Unspecified Behavior 文档。
更新时间 | 更新内容 |
---|---|
2020.3.1 | 追加新的常见错误条目 |
2020.7.15 | 追加新的常见错误条目 |
2022.3.1 | 追加新的常见错误条目 |