上一篇文章《C 语言的发展史(上)》 介绍了C语言诞生前的编程语言环境。本篇将聚焦于B语言面临的挑战,以及为了克服这些挑战,C语言核心机制(尤其是其独特的类型系统)是如何被一步步设计和创造出来的。
更多历史
汤普森成功用B语言重写了B编译器,完成了“自举”。但开发过程充满了与内存限制的斗争:每加入一个新特性,编译器就几乎装不进内存,而一旦利用新特性重写代码,体积又会缩小。举个例子,B语言引入了广义赋值运算符,比如用 x=+y 表示将 y 加到 x 上。这个记法源自Algol 68,经由麦克罗伊引入他的TMG版本。(有趣的是,在B和早期C中,这个运算符写作 =+ 而非 +=,这个错误直到1976年才修正。)
汤普森更进一步,发明了 ++ 和 -- 运算符。很多人猜测这是为了利用DEC PDP-11的自增/自减寻址模式,但这在时间线上不可能——B诞生时PDP-11还没出现。更可能的原因是,汤普森观察到 ++x 的编译结果比 x=x+1 更紧凑。这个小小的语法糖,后来成为了C语言的标志之一。
在PDP-7上,B编译器并不直接生成机器指令,而是生成一种叫做“线索码”的解释执行方案,这导致程序运行速度很慢。因此,除了B语言自身,很少有其他程序用B编写。机器太小,重写整个操作系统和工具集代价太高。
尽管如此,汤普森还是进行了一些雄心勃勃的尝试,比如开发了一个能够在PDP-7上为大型机GE-635生成机器指令的交叉编译器。这完全得益于B语言及其运行时系统的简洁性。
到1970年,Unix项目前景看好,团队购置了新的DEC PDP-11。在等待磁盘到货的三个月里,汤普森迅速用PDP-11汇编语言重写了Unix内核和基本命令。磁盘到货后,系统很快被迁移过来,已有的B程序也被移植。随着用户增多,大家希望有更便捷的开发工具。尽管B语言性能不佳,团队还是为其补充了小型库,并越来越多地用B编写新程序,包括史蒂夫·约翰逊的首个 yacc 版本。
B语言的问题
随着在PDP-11上的使用,B语言语义模型的不足暴露无遗:
- 字符处理笨拙:从BCPL继承的机制在PDP-11这种字节寻址机器上显得繁琐。
- 缺乏浮点支持:虽然初代PDP-11没有浮点硬件,但厂商承诺会提供。
- 指针开销大:B/BCPL将指针定义为字数组索引,每次引用都需要在运行时将索引转换为字节地址。
因此,引入类型系统变得势在必行——主要是为了支持字符和字节寻址,并为浮点运算做准备。当时的重点并非类型安全,而是解决实际硬件问题。此外,B编译器的线索码技术导致程序性能远不如汇编,这使得用B重写操作系统核心的想法被放弃了。
1971年,我开始着手扩展B语言:加入字符类型,并重写编译器以生成高效的PDP-11机器指令,而不再是解释性的线索码。这个过渡期的语言,我称之为 NB(“New B”)。
萌芽期的C(Embryonic C)
NB存在时间很短,但它首次引入了 int 和 char 类型,以及它们的数组和指针。声明风格是这样的:
int i, j;
char c, d;
int iarray[10];
int ipointer[];
char carray[10];
char cpointer[];
此时,数组和指针变量中存储的值已经是以字节为单位的机器地址,指针解引用不再有运行时开销。但是,数组下标和指针算术的机器代码开始依赖于类型——计算 iarray[i] 需要将下标 i 按整型大小进行缩放。
这看起来是B语言的自然演进,我实验了几个月。然而,当我试图扩展类型系统,尤其是加入结构体时,问题出现了。
结构体应该能直观地映射到机器内存。但如果结构体里包含数组,比如早期的Unix目录项:
struct {
int inumber;
char name[14];
};
编译器该把指向 name 的指针藏在哪里?即使能隐藏,在分配复杂对象时又该如何正确初始化这些隐藏的指针?
这个困境的解决方案,构成了从无类型BCPL到有类型C的关键一跃:
不再在存储中物化指针,而是在数组名出现在表达式中时,动态生成指向其首元素的指针。
这条规则沿用至今,并成为C语言最著名的特性之一:数组类型的值在表达式中会自动转换为指向其首元素的指针。这个创新让大部分B代码无需修改就能运行,更重要的是,它为建立一套全面而实用的类型系统扫清了障碍。
C语言的核心创新:声明语法
C语言的第二大创新,是其更丰富的类型系统,以及反映这一系统的声明语法。NB只有基本类型和它们的指针、数组,缺乏组合机制。我们需要一种方法描述:对于任何类型T,可以创建T的数组、返回T的函数、指向T的指针。
如何设计声明这些组合类型的语法呢?灵感来源于一个简单的类比:变量声明应模仿其在表达式中的使用形式。
例如:
int i, *pi, **ppi;
或者分开写:
int i;
int *pi;
int **ppi;
这里声明了一个整数、一个指向整数的指针、一个指向“整数指针”的指针。语法明确反映:在表达式中,i、*pi、**ppi 最终得到的都是一个 int 类型的值。
同理:
int f(), *f(), (*f)();
声明了一个返回整数的函数、一个返回整数指针的函数、一个指向“返回整数的函数”的指针。
int *api[10], (*pai)[10];
声明了一个由10个整数指针组成的数组、一个指向“10个整数数组”的指针。
这套将类型构造与表达式使用形式挂钩的方案,深受Algol 68的影响。它为C语言提供了强大而灵活的类型描述能力,尽管其语法对于初学者来说颇具挑战。
当这套类型系统、相关语法以及能够生成高效代码的新编译器完成后,我觉得它值得一个新名字。“NB”听起来像是个临时编号。我决定延续单字母的传统,称它为 C。至于C是代表字母表上的进步(B之后是C),还是BCPL缩写上的进步(B取自BCPL,C是下一个字母),就留给后人去解读了。
至此,一门兼具高效率、灵活性和硬件亲和力的新语言——C,正式登上了历史舞台。它的设计充满了对实际问题的务实解决,而非纯粹的学术构想,这也为其日后在系统编程领域的统治地位奠定了基础。想了解更多编程语言和底层技术的历史与设计,欢迎在云栈社区交流探讨。