每个程序员都写过 printf("hello, world\n");,但几乎没有人在写下这行代码时想过一个问题——它最早根本不是用 C 写的。
Computerphile 在 2026 年 4 月发布的这期《Original Hello World in B Programming Language》,把镜头对准了一个被忽视到几乎成了"常识盲区"的事实:那段全人类最熟悉的入门程序,它的第一版诞生于 1972 年,用的是一门今天大多数人听都没听过的语言——B 语言。而 B 语言最反直觉的地方在于:它根本没有"类型"这个概念。整个世界里只有一种数据,那就是一个机器字(machine word)。
这个视频值得每个写代码的人停下来看一遍,原因不是猎奇,而是它戳穿了一个根深蒂固的误解:我们总以为 C 语言的"类型系统"是这门语言的灵魂,是它天生就有的东西。但真相恰恰相反——类型是 C 后来才硬塞进去的补丁,C 的语法骨架几乎全是 B 留下的遗产。换句话说,那段 int main()、那些花括号、那个 for 循环、那个 i++,它们的老祖宗活在了一个"不认识 char、不认识 float、连字符串都没有"的世界里。
这篇解读要做的,就是把 Computerphile 这短短十几分钟里讲到的线索展开,还原一段从 BCPL → B → C 的演化史,并回答一个对现代开发者真正有用的问题:为什么理解 B 语言,能让你重新理解今天手里写的每一行 C/C++/Java/Go 代码。
| 项目 | 内容 |
|---|---|
| 视频标题 | Original Hello World in B Programming Language |
| 频道 | Computerphile(Brady Haran 出品的计算机科学科普频道) |
| 发布日期 | 2026 年 4 月 17 日 |
| 时长 | 约 12 分钟(Computerphile 标准深度科普长度) |
| 播放量 | 10 万+(截至解读时约 10.2 万次) |
| 核心主题 | "hello, world" 的真实起源,以及 B 语言如何演化为 C |
这是整个视频最锋利的一刀。我们理所当然地认为 printf("hello, world\n") 是 C 的发明,因为 1978 年那本被称为"K&R"的《The C Programming Language》里用它做了第一个示例。但 Computerphile 指出,这个例子真正第一次出现,是在 Brian Kernighan 1972 年写的一份内部备忘录《Tutorial Introduction to the Language B》里——比 C 语言正式定型整整早了大约两年。
这段历史被忽略,很大程度上是因为"hello, world"太成功了。它成功到脱离了语境,变成了一个符号、一种仪式,所有语言教学的开场白。符号化让它失去了出处。视频做了一件朴素但重要的事:把这段程序"重新放回它诞生的土壤"里。一旦放回去,你会发现土壤和我们想的完全不同——那是一片没有类型、没有标准库、连换行符都长得不一样的土地。
(视频原话线索)Kernighan 在 1972 年的 B 教程中演示了"hello, world",之后 1974 年的 C 教程备忘录沿用了它,最终在 1978 年的 K&R 书中定型为全世界程序员的共同记忆。
延伸分析: "hello, world"能穿越半个世纪从 B 活到 Python、Rust、Swift,本身说明了一件事——真正好的"最小可用示例"是不依赖具体语法的。它只承载一个意图:证明这台机器、这套工具链、这个语言,能跑通。这恰恰是 B 语言哲学的内核:别管类型、别管抽象,先让它动起来。
要理解原版 hello world 为什么长得那么怪,必须先理解 B 的世界观。B 是 Ken Thompson 1969 年在贝尔实验室创造的,最初跑在 PDP-7 上。它从 Martin Richards 1967 年的 BCPL 简化而来。BCPL 已经是"无类型"的,B 把这种纯粹推到了极致:
B 只有一种数据类型——machine word(机器字)。 没有整数和字符之分,没有浮点数,没有结构体。所有的变量,a 也好、b 也好,统统是一个机器字宽度的存储单元,里面装的就是一个二进制数。至于你把它当成数字还是当成字符,完全由你对它做的操作决定,而不是由它的"类型"决定。
这听起来对今天的程序员像天方夜谭。我们习惯了 int x 和 char c 是两种东西,编译器会阻止你把字符当数字乱来。但在 B 里,这种"保护"不存在,因为根本没有"种类"需要保护。一切都只是字(word)。
延伸分析: 这种"万物皆字"的设计,其实是一种非常诚实的计算观。CPU 本身就是不认识类型的——寄存器里装的就是 0 和 1,是你(或编译器)赋予了它"这是整数"还是"这是地址"的含义。B 几乎是直接把硬件的这一层裸露给了程序员。C 后来加上的类型系统,与其说是"进步",不如说是给这层裸露的硬件包上了一层语义的糖衣,让你少犯错、让编译器能做检查。理解了这一点,你才会明白为什么 C 里"类型"很多时候是"可被绕过"的(比如 void * 和强制转换)——因为底层从来就没有真正的类型墙。
这是视频里最让人会心一笑的瞬间。如果 B 没有字符串、没有 printf,那它怎么打印 "hello, world"?答案是:硬塞。Kernighan 1972 年的 B 版本大致是这样的:
main( ) {
extrn a, b, c;
putchar( a ); putchar( b ); putchar( c ); putchar( '*n' );
}
a 'hell'; b 'o, w'; c 'orld';
逐行翻译:main() 是入口(注意没有返回类型,因为 B 没有类型)。extrn a, b, c; 声明这三个变量是外部的(定义在函数外面)。关键在最后三行:a 'hell'; 的意思是——把四个字符 h e l l 打包进一个机器字里,赋给 a。b 'o, w'; 把 o , 空格 w 打包进一个字,c 'orld'; 把 o r l d 打包进一个字。然后用 putchar 把这三个字逐个吐出来,最后 putchar('*n') 输出一个换行(B 用 *n 表示换行,C 后来改成了 \n)。
也就是说,原版的 "hello, world" 是被劈成三段、每段四个字符、像装集装箱一样塞进三个机器字里打印出来的。没有 %s,没有字符串字面量,没有 \n。它赤裸地展示了 B 的本质:字符串不是一等公民,它只是被人为切分、塞进字里的一堆位。
延伸分析: 这段代码是理解"类型从何而来"的活化石。注意 'hell' 这种"四字符常量"——它把四个 ASCII 码拼成一个整数。这正是后来 C 里 int 可以装下 'ABCD' 这种多字符常量(multicharacter constant)的由来,也是 C 标准里那个出了名的"实现定义行为"(implementation-defined)。很多老代码里用 'TAG' 这种整数当魔数,根源就在 B 的这个打包习惯里。你今天在十六进制 dump、文件格式魔数(magic number)里看到的 0x49492a00(TIFF)之类的,精神上都和 'hell' 同源。
这是视频隐含、但读者必须抓牢的一条暗线:你以为的"C 语言特色",大部分是 B 发明的。 让我们清点一下从 B 直接继承到 C 的东西:
{ } 作为语句块分隔符——B 用的,C 沿用,后来 Java、JavaScript、C#、Go 全跟着用。for 循环 for(init; cond; step) 的三段式结构——B 的。++ / --——这是 Ken Thompson 的得意之作,因为他想利用 PDP-7 硬件自带的"自增寻址"内存单元,让 *p++ 这种操作能编译成单条机器指令。+=、-=——B 引入的,让 a = a + b 简写成 a =+ b(早期写成 =+,后来因歧义改成 +=)。name(args) { ... }——B 的。/* */——B 的。也就是说,C 真正"新发明"的,不是这些语法糖,而是类型系统。C 在 B 的语法骨架上,嫁接了一套数据类型的概念。这个认知翻转很重要:它解释了为什么 C 的类型系统总给人一种"后贴上去、不够整齐"的感觉——因为它真的是后贴的,B 的无类型基因一直潜伏在 C 的底层行为里(比如数组退化成指针、函数默认返回 int、隐式类型转换)。
延伸分析: Thompson 当年设计 ++ 是为了让 PDP-7 的特定硬件指令有用武之地。这是"硬件驱动语言设计"的典范。今天的语言设计者不再有这种"贴近某款 CPU"的冲动,但 B 的教训还在:好的语法糖往往是从具体的工程约束里长出来的,而不是从美学里凭空设计的。 Rust 的生命周期标注、Go 的 goroutine 语法,本质上都是某种"约束的语法化"。
B 死于一次硬件升级。Unix 早期跑在 PDP-7(18 位字、按字编址)上,B 的"一切皆字"在那里如鱼得水。但当 Unix 迁移到 PDP-11(16 位、按字节编址 byte-addressable)时,灾难来了:
在按字节编址的机器上,一个"字"是 2 个字节。你想处理一个字符(1 字节),在 B 里得先取出整个字、再做掩码和移位把它抠出来。B 的无类型模型让编译器根本不知道你取出来的是要当字符还是当字,于是只能生成低效的、总是按字搬运再切分的代码。性能一塌糊涂。
Dennis Ritchie(C 之父)意识到:要让这门语言在 PDP-11 上活得下去,必须让语言"知道"数据有多大。于是他开始在 B(准确地说是它的后继 NB,New B)里引入类型——首先是 char(明确这是 1 字节的东西),然后是 int,以及让数组"记住"自己元素的类型。这一步是被迫的、工程性的、为硬件服务的。类型不是为了"优雅",而是为了"高效"。
延伸分析: 这条历史对今天的程序员是个提醒:很多语言特性,是某种硬件约束被时间风化后留下的化石。 我们今天写 Java 的 byte/int/long,写 Python 时不用担心,是因为底层机器早已统一到字节编址、编译器和虚拟机替我们处理了 PDP-11 时代的烦恼。但化石还在——C 的 char 默认既可能是 signed 也可能是 unsigned(实现定义),就是当年硬件分裂的遗迹。理解这一点,你写跨平台代码时才会主动写 uint8_t 而不是依赖 char 的符号性。
视频和这段历史里,C 相对 B 最重磅的增量其实不是 char,而是 struct。时间线是这样的:
1973 年,Ritchie 和 Thompson 做了一件惊天动地的事——把整个 Unix 操作系统内核从汇编语言重写成 C。这是历史上第一个用高级语言写成的操作系统内核。为了写内核,他们需要描述大量结构化数据:inode 表、进程控制块(PCB)、缓冲区头、设备表……这些东西用 B 的"裸机器字"根本描述不了,你需要"把几个不同类型的字段捆在一起"的能力。
于是 Ritchie 在 1973 年前后发明了 struct(结构体)。struct 让你能定义一个"由若干命名字段组成的复合类型"。这看似简单,实则是一次抽象层的跃迁——程序员第一次可以在语言层面直接建模"一个进程""一个文件""一个设备",而不是靠一堆全局数组和偏移量硬凑。
这一步在计算机科学史上的分量,怎么强调都不过分:没有
struct,Unix 内核就不可能用 C 写出来;Unix 不用 C 写,整个现代操作系统的传承(Linux、macOS、Android)就要重写剧本。
延伸分析: struct 的诞生揭示了一个普遍规律:语言特性往往是被某个"杀手级应用"逼出来的。 B 没有 struct,是因为它只用来写工具程序;C 需要 struct,是因为它要写操作系统内核。今天我们看一门语言是否成熟,往往就看它能不能支撑起一个"复杂的、状态密集的、需要长期演进的系统"。Go 加 interface、Rust 加 trait、Zig 加 comptime——每一个重大特性背后,都站着一个"非它不可"的真实工程。
最后,把镜头拉回今天。B 虽然死了,但它的 DNA 一直活在 C 和所有受 C 影响的语言里。几个最明显的"幽灵":
数组名会退化成指针。在 C 里,把数组传给函数时,它退化成一个指向首元素的指针,丢失了长度信息。为什么?因为 B 里数组本就等于指针——a[i] 在 B 里就是 *(a+i) 的语法糖。C 嫁接了类型,却没改掉这个底层假设,于是数组长度得靠你另外传一个 size 参数。这一设计缺陷,间接催生了无数缓冲区溢出漏洞。
隐式 int。在很长的历史里,C 的函数如果不写返回类型,默认返回 int;变量在某些声明里也默认 int。这也是 B 的遗风——B 里根本没有类型声明,"不写就是"。直到 C99 才废止了隐式 int。
Go 的精神回响。Ken Thompson 后来(2009 年)参与设计了 Go 语言。Go 刻意做减法、强调简洁、++ 还在(虽然只能是语句不是表达式)——某种意义上,Go 是 B 精神的远亲:少即是多,把语言做薄,把复杂度留给库和运行时。
延伸分析: 一个语言的"尸体"不会真的消失,它会以语法习惯、默认行为、历史包袱的形式,活在后继者身上。这也是为什么学一点语言演化史,比背语法手册更值钱——你看到的不只是"为什么这么写",而是"为什么只能这么写、还改不掉"。
flowchart TD
A["BCPL (1967)<br/>Martin Richards<br/>无类型 · 万物皆 word"] --> B["B (1969)<br/>Ken Thompson<br/>PDP-7 · 纯机器字"]
B --> B1["B 的贡献<br/>{ } 花括号 · for 循环<br/>++ / -- · += · /* */ 注释"]
B --> HW["🏆 1972 原版 hello, world<br/>Kernighan 的 B 教程<br/>三个机器字塞进 hell/o, w/orld"]
B -->|"PDP-11 按字节编址<br/>无类型模型性能崩盘"| C0["NB → C<br/>Dennis Ritchie"]
C0 --> C1["C 的核心增量<br/>① char 类型(高效处理字节)<br/>② struct 结构体(1973)"]
C1 --> UNIX["1973 Unix 内核<br/>用 C 重写<br/>首个高级语言 OS 内核"]
C1 --> C2["C 的遗产<br/>数组退化为指针(B基因)<br/>隐式 int(B基因)<br/>多字符常量 'hell'(B基因)"]
UNIX --> MODERN["现代传承<br/>Linux · macOS · Android<br/>Java · C++ · Go · Rust"]
HW -.->|"穿越50年"| TODAY["今天每种语言的<br/>第一个程序仍是 hello, world"]
style HW fill:#ffe082
style UNIX fill:#a5d6a7
style A fill:#bbdefb
style B fill:#90caf9
style C1 fill:#ce93d8
读完这段历史,你可能会问:我又不用 B,这对我写代码有什么用?答案是——它改变你看待"类型"和"指针"的方式,而这恰恰是 C/C++ 最容易踩坑的地方。
第一,重新理解指针与数组。 既然 C 的"数组退化成指针"是 B 留下的基因,你就不会再天真地以为 sizeof(arr) 在函数参数里能拿到数组大小。你会主动用 std::array(C++)、std::span(C++20)或显式传长度。写跨函数边界的代码时,你会本能地把"长度信息"当成一等公民显式传递,而不是依赖语言替你记住——因为底层根本记不住。
第二,对"类型安全"保持谦卑。 B 到 C 的演化告诉我们,类型系统是"加上去"的,而不是"天然存在"的。这意味着在 C/C++ 里,类型墙是可以被绕过的(reinterpret_cast、void*、memcpy)。你不会盲目相信"编译器通过就一定安全",而会在涉及内存重解释、网络协议解析、文件格式魔数识别时,格外小心地对齐、检查字节序、用 static_assert 守住布局假设。
第三,学会"无类型思维"这个调试工具。 遇到诡异的内存 bug 时,试着退回 B 的视角看问题:这片内存里到底装的是什么字节序列?我的代码在不同地方把它当成了什么类型?这两种解读是否一致? 很多"未定义行为"(UB)引发的玄学 bug,用这种"剥掉类型、只看字节"的思路反而能看清楚。
第四,重新审视你正在用的语言。 当你下一次学一门新语言(比如 Rust、Zig),不妨问一句 Ken Thompson 式的问题:它的哪些语法,是被某种具体工程约束逼出来的?它的"类型墙"有多厚、能不能被打穿? 这种追问,比"它语法好不好看"有价值得多。因为真正决定一门语言寿命的,是它底层对计算本质的刻画是否诚实——而 B 语言,恰恰是最诚实的那一种。
"hello, world"的真身写在一门没有类型的语言里——它提醒我们,C 的类型系统是后补的补丁,而真正的代码骨架,五十年来一直是 B 留下的那一副。
本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
