1972年4月,剑桥公园的樱花刚开,麻省理工的 AI Lab 却因为 ARPANET 太慢集体罢工。 在纽约以南 300 公里的贝尔实验室,丹尼斯·里奇(Dennis Ritchie)正蹲在一台 PDP-11/20 前面,屏幕是绿色的,内存只有 24KB,硬盘比现在的 U 盘还小。他面前的任务很简单:把整个 Unix 操作系统从汇编改写成一种高级一点的语言,好让它能跑在别的机器上。
他给自己定了一个近乎残酷的目标: “这门语言写出来的代码,生成的汇编要和手写汇编一样快,一样小,哪怕只多一个字节都不行。”
于是,C 诞生了。
那是个连“函数原型”都没有的时代。 那是个你写 int flag,然后直接写 if (flag = 1) 都编译不报错的时代。 那是个程序员人均自封硬件通、抽着烟喝着咖啡、用纸带打孔的时代。 在他们眼里,布尔值?那不就是 0 和 1 吗? CPU 的条件跳转指令本来就只认零和非零,你要什么布尔类型? 多一个类型,多一次类型转换,多浪费一个周期——那等于谋杀。
1973年,Unix 已经用 C 重写了 90%,内核只有 9000 行,运行内存 50KB 不到。 里奇和汤普森把这事当战绩到处吹: “我们用 50KB 的内存跑起了多用户分时系统,IBM 用几 MB 都做不到!” 在这种谁更省谁是爹的氛围里, 谁敢提“我想要一个 bool 类型”, 下一秒就会被所有人用看弱智的眼神围观。
1974年,Unix 第一次离开贝尔实验室,跑到大学里。 学生们拿到源码后,第一反应不是“哇好牛逼”, 而是“卧槽,这什么鬼,if (x=1) 居然能过编译?!” 但没人敢改,因为改了就比别人多占 2 个字节, 在那个磁带都贵的年代, 多 2 个字节等于多花 2 美金, 2 美金够买 4 加仑汽油,够开着雪佛兰去新泽西兜风了。
1978年,《The C Programming Language》(K&R圣经)出版,那本薄薄的蓝色小书里,有一行字给全行业下了道圣旨:“There is no need for a separate Boolean type.”翻译过来就是: 布尔值?不存在的。能用 int 解决的事,永远不要增加新类型。那时候程序员圈子小,大多是搞系统开发的硬核玩家。写汇编出身的人,对“用42当true”这种事不仅不觉得怪,反而觉得灵活。
1980年代初,C语言开始往嵌入式领域渗透。某汽车电子厂商的工程师在开发发动机控制程序时,直接用“转速>3000”的返回值当bool判断——结果某次测试,传感器故障返回了-1,程序照样判定为真,差点让测试车飙到爆表。工程师事后拍着大腿笑:还好C够野,换别的语言早崩了。这种野路子在当时不是bug,是经验。但麻烦也在悄悄发酵。
1983年,微软开始用C写DOS内核,程序员们发现用int代bool在大型项目里容易踩坑,比如一个函数返回“-1表示错误,0表示成功”,结果被新手当成bool判断,直接写成“if(函数())”,错误场景反而判定为“真”。微软的解决方案很粗暴:自己定义个BOOL类型,typedef int BOOL,再#define TRUE 1,#define FALSE 0。这边微软刚搞出私有标准,那边Unix阵营也没闲着。
1985年,Linux内核的雏形刚出现,林纳斯·托瓦兹(Linus Torvalds)觉得微软的BOOL太冗余,直接在头文件里写“#define true 1”,连类型都懒得定义。更乱的是嵌入式厂商:摩托罗拉的68000开发包用“BOOLEAN”,德州仪器的DSP开发工具用“Bool”,大小写、拼写全不统一。
1987年,一桩血案让矛盾浮出水面——某银行的交易系统升级,程序员把Windows平台的代码移植到Unix服务器。Windows的BOOL是int(4字节),Unix的Bool被typedef成了char(1字节)。数据传输时,4字节的TRUE(0x00000001)被截断成1字节,到了Unix端变成真;但返回的错误码0x000000FF,在Windows端被当成int判断为真,在Unix端被当成char判断为假,直接导致100多笔交易记录丢失。
银行程序员对着日志骂了三天三夜,最后在代码里加了一堆强制类型转换才搞定。这件事在行业内传开后,越来越多人开始呼吁“给C一个标准bool”。
于是,1989年 ANSI C(C89)标准化会议被提上日程,“是否加入布尔类型”成了核心议题之一。
支持方拍桌子:“现在都写几万行代码了,再用int当bool就是给自己挖坑!”反对方是一群嵌入式和系统厂商的代表,拿出的理由和后来C99时如出一辙:“我们有1979年写的核电站控制代码,里面全是用flag=5当true的逻辑,改了谁负责?”
更关键的是,当时C语言的“极简神话”还没破,不少老专家坚持“C是给聪明人设的语言,不需要给新手做保姆式设计”的观点。有位参与过Unix开发的老工程师放话:“当年用汇编写Unix的时候,连int都没有,不照样搞定?现在的年轻人就是娇气。”
最终投票结果,“暂不加入bool类型”以微弱优势胜出。C89标准里只提了一句:“用整数类型表示布尔值时,0为假,非0为真”,等于把锅又踢回给了程序员。标准发布后,某技术杂志的评论很扎心:“C语言的灵活,本质是把所有风险都丢给了使用者。”
1990年代初,情况更糟了。C++开始崛起,1990年C++标准草案里明确加入了bool类型,Bjarne Stroustrup(C++之父)在演讲里直言:“C的类型系统太松散,bool是给代码加的安全锁。”这一下,跨语言开发的矛盾爆发了——C代码里的“int flag=2”传给C++函数,C++直接判定为true,但程序员本意可能是“2代表某种状态”,不是逻辑真。
1995年,Java诞生,自带boolean类型,连新手都知道“true就是true,不是1”。这时候再用C写商业代码的程序员,已经快被逼疯了:同一个团队里,C++开发者用bool,Java开发者用boolean,就C开发者抱着int死撑,代码评审时光解释“为什么用3当true”就要花十分钟。
到了1998年,C++98标准正式发布,bool成了和int平级的关键字。与此同时,互联网行业爆发式增长,十万行、百万行的C语言商业代码遍地都是——用int当bool导致的bug越来越离谱:电商网站的库存判断,因为库存=-1(缺货)被当成true,显示有货;通信设备的状态监控,信号强度=0(无信号)被当成false,跳过了告警逻辑。
1999年,C标准委员会终于憋不住了。会场上发生了 C 语言历史上最尴尬的一幕:有人提议:干脆把 bool、true、false 直接加进关键字列表吧,像 C++ 那样。 话音刚落,整个会议室陷入死寂。
然后一位来自老牌嵌入式厂商的代表慢悠悠举手: “不好意思,我们公司还有 1987 年写的 300 万行汇编+C 混合代码,里面把 bool 当变量名用了上千次。如果 bool 变成关键字,明天我们整个产品线就全编译不过了。”
另一位来自电信巨头的代表补刀: “我们也有 200 万行代码,宏定义里写了 #define bool int,改关键字等于让我们重写历史。”
最后一位头发花白、曾经参与过 ANSI C89 的老专家叹了口气,说了一句让全场沉默的话: “当年我们为了省一个字节没加 bool,今天要为这一个字节赔上整个生态。”
于是,他们做出了 C 语言历史上最“体面”的投降:
新增一个底层类型叫 _Bool;
提供一个可选头文件 <stdbool.h>,里面用宏把 _Bool 别名成 bool,把 1 别名成 true,把 0 别名成 false;
你爱用就 #include,不用就继续过你的 int 生活,谁也别得罪。
1999年12月,C99 标准正式发布。 没人敢笑出声,因为大家都心知肚明: 这就是标准的宿命——它永远只能在历史遗留代码的尸体上跳舞。
C99发布后的第一个月,技术论坛上的吐槽帖能编成一本《半吊子解决方案吐槽集》。有人截了段代码发出来:“#include <stdbool.h> bool flag = true;”下面配文“终于不用写int flag = 1;了,但总觉得像偷用C++的东西”;更有人晒出跨平台代码的截图——Windows端要兼容老代码不敢加头文件,Unix端新代码全靠头文件撑着,中间硬生生插了段“#ifdef _WIN32 #define bool int #endif”的兼容宏,丑得让人眼疼。
最哭笑不得的是嵌入式开发者。某军工企业的工程师在论坛吐槽:“我们的导弹控制系统还用C89编译器,领导说稳定比什么都重要,结果我写新模块想用_Bool,编译器直接报‘unknown type name’,最后还是乖乖写回int flag; 这标准跟我们没关系。”
后面这十年,是C程序员最社死的十年。
2005年,Google 刚火,内部代码规范里直接写死: “禁止在 C 代码中使用 bool,全部用 int,理由是历史代码太多,统一好维护。”这条反C99的规范,硬生生执行到2018年才删除。
2008年,iPhone 发布,Objective-C 大火。 苹果在Foundation框架的头文件里出现了一行让全世界 C 程序员破防的代码:
typedef signed char BOOL; // 注意:YES = 1, NO = 0,-1 也是 YES!
这意味着-1、255这些在C里算“真”的值,到了Objective-C里依然是YES,但存储时会被当成1字节的signed char处理。
这下彻底乱了套:C的bool是_Bool宏(通常1字节),C++的bool是原生类型(固定1字节),Objective-C的BOOL是signed char(1字节但符号位敏感)。有开发者做过测试:把C代码里的“bool flag = 255;”传给Objective-C函数,C里判定为真,Objective-C里因255超出signed char范围(-128~127),被解析为-1,照样判定为YES;但把Objective-C的BOOL值(-1)传回C++,C++会把-1当成int转成bool,依然是true。看似结果一致,但调试时变量内存值完全对不上,跨语言接口排错能让人熬三个通宵。
时间一晃到了2011年,C11标准发布,有人提议把bool扶正成关键字,结果刚提出来就被否决了。这时候全球在用的C代码早以亿行计,当年那300万行嵌入式代码可能还在某工厂的设备里跑着,谁都不敢动这个雷。C11能做的,只是在C99的基础上补了句“_Bool类型的取值只能是0或1”,算是给这个半吊子类型加了层安全锁。
新的编程语言像雨后春笋一样冒出来,每一家都在打 C 的脸:
Go(2009):原生 bool
Rust(2015):bool 只有 true/false
Swift(2014):连 Optional 都给你安排得明明白白
每出来一个新语言,C 程序员就要被教育一次: “看,人家连头文件都不用加。”
而 C 标准委员会呢? 他们整整沉默了 25 年。
直到 2024年10月31日,ISO/IEC 9899:2024(也就是 C23)正式发布,布尔类型的扶正议题被重新摆上桌面。此时距离C99已有25年,当年让委员会束手束脚的百万行旧代码,多数已随设备迭代退役,而新生代开发者对类型安全的诉求早已盖过历史包袱。
那天,WG14 的会议室里,一个 70 多岁的老委员(当年投过反对票的那批人之一)站起来,声音有点抖:
“各位……我代表我们那一代人,向全世界程序员说一句, 对不起。 当年我们为了省一个字节,把你们坑了半个世纪。 今天,bool、true、false 终于进关键字列表了。”
C23对布尔类型的改动,走的是温和革命路线,没敢彻底推翻C99的框架,却精准补上了最痛的短板:首先,bool、true、false被正式确立为关键字,但加了道安全阀门:如果代码里用#define提前定义过这些名字,编译器会优先使用用户定义,避免旧代码直接崩溃;其次,<stdbool.h>不再是可选头文件,而是被纳入标准库的隐式依赖,哪怕不写#include,也能直接用bool类型,彻底告别了加头文件才合规的尴尬。 更贴心的是,C23还解决了C99留下的大小模糊问题:明确规定sizeof(bool)等于1字节,和C++的bool完全对齐。这一下,跨语言开发的程序员终于松了口气:以前传布尔值要先强制转int的操作,现在不用了,C的bool和C++的bool能直接无缝对接,调试时少了大半因为类型差异踩的坑。
消息传到技术论坛,程序员们的反应很微妙。有人晒出C23的测试代码:bool success = (a == b); if (success) { ... },配文“活了半辈子,终于等到C有原生bool了”;也有嵌入式老炮吐槽:“我们的工业控制器还在跑C99编译器,厂商说升级要加钱,C23再好,跟我们也没关系”。
总而言之,bool 终于自由了。
如今再写C代码,你可以选C23的原生bool,享受类型安全;也可以守着int flag,兼容那些跑了几十年的旧设备。就像布尔值的本质永远是0和1,C语言的本质也永远是适配当下,兼容过往。
这个从字节之争开始的故事,最终以兼容收尾。
C语言从来不是最优雅的语言,却是最懂生存法则的语言。
或许这就是编程语言的终极智慧:不是追求一步到位的完美,而是在时代的迭代中,始终保持着向前的韧性。