C 语言在诞生后的头 27 年里都没有布尔类型。这不是疏忽,而是一个设计决定,它带来的后果你在第一天就会遇到,还直接引出了一个著名的 bug。
零为假,其他一切为真
C 语言的条件规则里没有专门的真/假值。if、while、for、&&、|| 和 ! 都只问一个问题:这个值是零吗?
负数为真。小数为真。非空指针为真。只有零,不论是什么类型,才为假。
这就是为什么 C 代码里满是这些惯用写法,在你知道这条规则之前看起来都很晦涩:
if (count) /* if count is not zero */
if (!count) /* if count is zero */
if (p) /* if p is not NULL */
if (!strcmp(a, b)) /* if the strings are equal - strcmp returns 0 for a match */
最后一个常常让人上当。字符串相同时 strcmp 返回 0,所以在 if 里,“相等”读起来却是“非”。写成 if (strcmp(a, b) == 0) 更清楚,效果完全一样。
比较运算的结果是 int
C 语言中的比较并不是一种特殊的表达式。它的结果是一个普通的 int,值为 1 或 0,你可以打印它、存储它,或者拿它做算术运算:
! 运算符也是同样的道理:操作数为零时结果是 1,否则是 0。所以 !!x 是一个经典惯用写法,能把任何值规范成恰好 0 或 1。
bool、true 和 false(C99)
C99 新增了一个真正的布尔类型 _Bool,以及一个给它起了易读名字的头文件:
<stdbool.h> 非常小:它把 bool 定义为 _Bool,把 true 定义为 1,把 false 定义为 0。整个头文件就这些。但用了它,代码的意图就一目了然,这是 int flag = 1; 永远做不到的。
_Bool 有一个真正与众不同的行为:它只存储 0 或 1。给它赋任何非零值,都会转换成恰好 1:
printf 仍然没有用于布尔值的 %b。用 %d 打印,或者自己转换成文本:
到了 C23,这件事变得更简单了:bool、true 和 false 成了真正的关键字,所以不再需要包含头文件。不过如今大多数代码的目标标准仍是 C17,所以请保留 #include <stdbool.h>。
从函数返回布尔值
这是 bool 最能体现价值的地方:
bool 返回类型告诉调用者这个值是什么意思。一个叫 check_file 的函数如果返回 int,它可能是布尔值、计数,也可能是错误码,而按照 C 库的惯例,往往是第三种:0 表示成功。写明类型就不用再猜了。
= 与 == 混用的 bug
因为赋值是一个表达式,会产生被赋的值,又因为任何非零值都为真,所以下面的代码能通过编译:
这里同时出了两个问题。条件把 5 赋给了 x,破坏了原来的值,然后检查 5,它非零,所以分支执行了。默认情况下,这个程序既没有错误也没有警告。
更糟的是,用 0 的版本永远不会执行它的分支:
if (found = 0) { /* never taken - and found is now 0 */ }
三种防御方法,按实用程度排序:
开启警告编译。-Wall 能发现它:
warning: suggest parentheses around assignment used as truth value
把警告清零才是真正的解决办法,而这只需要一个编译选项。
把常量写在前面。这种“尤达条件”(Yoda condition)会让这个笔误变成编译错误:
if (5 == x) /* correct */
if (5 = x) /* error: not an lvalue - the compiler stops you */
有些团队很喜欢这种写法,有些则觉得难以阅读;两种立场都说得通,但它确实有效。
确实要赋值时就写得明明白白。在条件里赋值对读取输入非常有用:
int c;
while ((c = getchar()) != EOF) { ... }
c = getchar() 外面多出的那层括号是运算符优先级所必需的,同时也表明这个赋值是有意为之。这就是惯用写法:如果你在条件里的赋值看起来不像这样,那它很可能是个笔误。
常见的布尔错误
和 true 比较。if (flag == true) 在使用 stdbool.h 时没问题,但一旦 flag 是一个存着 42 的 int,它就出错了,因为 42 == 1 为假。应该写 if (flag)。
连续比较。if (0 < x < 10) 永远为真:0 < x 得到 0 或 1,而这两个数都小于 10。应该写 if (x > 0 && x < 10)。
用了按位运算而不是逻辑运算。& 和 | 不是 && 和 ||。它们不会短路求值,而且是按位组合的:
两个操作数都是“真”,可 a & b 却是 0。在本该用 && 的地方用了 &,得到的条件只在某些值上出错,这是最难发现的一类 bug。
比较浮点数是否相等。if (0.1 + 0.2 == 0.3) 为假。应该和一个容差范围比较,原因详见数据类型。
常见问题
C 语言有布尔类型吗?
从 C99 起就有了:_Bool 是内置类型,包含 <stdbool.h> 后就能用更友好的写法 bool、true 和 false。C99 之前根本没有布尔类型,程序用 int 表示,0 为假,1 为真,这种写法至今仍然有效,也依然常见。
C 语言中什么算真?
任何非零值。在条件中,1、-5、0.01 和非空指针都为真;只有 0、0.0 和空指针为假。没有单独的布尔检查,if (x) 只是检查 x 是否非零。
C 语言中怎么使用 bool?
加上 #include <stdbool.h>,之后 bool ready = true; 就能按你预期的方式工作。在底层,bool 就是 _Bool,它只存储 0 或 1:给它赋任何非零值,存进去的都恰好是 1。
C 语言中 = 和 == 有什么区别?
= 是赋值,== 是比较。if (x = 5) 先把 5 赋给 x,再检查 5,它非零,所以这个分支总会执行。if (x == 5) 才是检查是否相等。前者能顺利编译,不报任何错误,这就是 -Wall 和 if (5 == x) 这种写法习惯存在的原因。