C语言的发展史-2

不满足网上的译文,自己翻译一下。备用。【】是译注、补充。


更多的历史

在TMG版本的B工作之后,Thompson利用B重写了B(的编译器)(一个自举步骤)。在开发途中,他不断地与内存限制作斗争:每一个语言[特性]的添加都会使编译器膨胀从而令内存几乎不够用,但每次利用该特性的优点的重写又减少了编译器的大小。例如,B引入复合(广义)赋值运算符,x=+y用于将y加到x。这一标记通过McIlroy——他将它合并到他实现的一个TMG版本中——从Algol 68[Wijngaarden 75]中获得。(在B和早期C中,该运算符被拼作=+而非+=;这个错误——在1976年被修复——是因为第一种形式在B的词法分析中更为简单)。

Thompson通过发明自增++和自减--运算符,又向前走了一步。它们的前缀或后缀位置决定了变化是发生在计算操作数的值之前或之后。它们并没有出现在B的最早版本中,而是在发展途中出现的。人们经常猜测,它们之所以出现,是因为C和Unix刚开始流行于DEC PDP-11时机器提供的自增和自减地址模式【如同很多人把垃圾回收看成是Java的贡献】。从时间上看,这是不可能的,因为B被发明的时候还没有PDP-11。当然,PDP-7的确有少量“自增”内存单元,具有通过它们一个间接内存引用能够自增该单元的特性。该特征或许暗示了Thompson创造那些运算符;但能够前缀和后缀则完全是他个人的创造。实际上,自增单元并没有直接用于该操作符的实现,这种创新的一个更强烈的动机可能是他发现,翻译++x比x=x+1尺寸上小。

PDP-7上的B编译器并不产生机器指令而是“线索码”(threaded code)[Bell 72]【不知道有没有好的术语翻译,参考】,这是一种解释器方案,编译器的输出由一系列执行基本操作的代码片段的地址构成。这些操作通常运行于一个简单堆栈上,特别对B而言。

PDP-7的Unix系统上,除了B本身外,只有很少的东西是B编写的,因为这个机器除了试验外,做什么都显得太小和太慢;用B完整地重写操作系统和库,从可行性上看代价太高昂;在某种程度上,Thompson通过所提供的“virtual B”编译器解决地址空间拥挤问题、通过对解释器中的代码和数据分页,使得被解释(执行)的程序拥有超过8K字节(的空间),但是对于通用库的实用而言,这样太慢了。尽管如此,还是出现一些用B写的工具,包括Unix用户熟悉的可变精度计算器dc的一个早期版本[McIlroy 79]。【看来,Ritchie很自觉地当Thompson一个小弟】我所承担的最有意义的工作是一个真正的编译器,将B程序翻译为GE-635机器指令而非线索码。它是一个小杰作:一个用本身语言写的完全的B编译器,它为36位大型机生成代码,运行于有4k字长的用户地址空间的18位机器上。这个项目之所以可能,完全是因为B的简单性和它的运行时系统。

尽管我们时而意淫一下去实现一个如同当时的Fortran, PL/I或Algol 68那样的主流语言,相对于我们的资源——可用的是如此简单和小的工具,这样的项目是令人绝望地庞大。所有这些语言都影响了我们的工作,但是用我们自己(的语言)做事情则更有趣。

到了1970年,Unix项目显得很有(成功的)希望,我们才能够申请新的DEC PDP-11。该处理器是DEC生产线的第一批产品,而且过了三个月磁盘才到。使用线索码技术为它编写B程序,仅需要为运算符编写代码段和一个简单的汇编器——我用B编写的。不久,在(拥有)操作系统之前,dc成为我们的PDP-11上被测试的第一个有趣的程序。几乎是最快的(速度),还在等待磁盘时,Thompson用PDP-11的汇编语言重写了Unix内核和一些基本命令。从该机器的24KB内存中,最早的PDP-11 Unix系统给了操作系统12KB,一个很小的空间分给用户程序,剩下的作为RAM磁盘。这个版本仅用于测试而非实际工作;the machine marked time by enumerating closed knights tours on chess boards of various sizes.磁盘到达后,我们把汇编语言转换为PDP-11上的方言后,很快移植到它上面,也移植了那些已经用B编写的程序。 到1971年时,我们这个小小的计算机中心开始有了用户。我们都希望更容易地编写有趣的软件。使用汇编太让人郁闷了,因而即使B有性能问题,B已经有了一个包含有用服务例程的小例程库,并被越来越多的新程序使用。这段时期的最著名的成果之一,是Steve Johnson编写的yacc解析-生成器的第一个版本[Johnson 79a]。

B的问题

    最开始,我们在 按字编址的机器上使用BCPL和B语言,这些语言的单一数据类型——“单元”,轻松地等价硬件的机器字。PDP-11的到来暴露了B的语义模型的一些不足。首先,它继承于BCPL的、仅作出少量改变的字符处理机制是笨拙的:在一个 面向字节的机器上,使用库例程把打包的字符串展开到一些独立的单元然后再次包装,或者访问或替换单个字符,开始变得讨嫌甚至无聊。
    其次,尽管最初的PDP-11没有提供浮点算术运算,但制造商承诺将很快提供。在我们的Multics项目和GCOS编译器中,通过定义特别的运算符,浮点运算已经被添加到BCPL,但是该机制仅在适当的机器上可行——单个字长足够包含一个浮点数;这在16位的PDP-11上是不成立的。
    最后,B和BCPL模型在处理指针时隐含着系统开销:该语言的规则——将一个指针定义为字长数组的索引,强迫这些指针被表示为字索引。每个指针引用会导致一个运行时尺度转换,从指针变为硬件希望的字节地址。
   因为上述因素,一个 类型模式看来很有必要,以便为处理字符和字节编址,以及为即将到来的浮点硬件作好准备。其他议题,特别是 类型安全性和接口检查,当时并不显得如同后来那样的重要。【也是摸着石头过河啊】
最开始,我们在 按字编址的机器上使用BCPL和B语言,这些语言的单一数据类型——“单元”,轻松地等价硬件的机器字。PDP-11的到来暴露了B的语义模型的一些不足。首先,它继承于BCPL的、仅作出少量改变的字符处理机制是笨拙的:在一个 面向字节的机器上,使用库例程把打包的字符串展开到一些独立的单元然后再次包装,或者访问或替换单个字符,开始变得讨嫌甚至无聊。 其次,尽管最初的PDP-11没有提供浮点算术运算,但制造商承诺将很快提供。在我们的Multics项目和GCOS编译器中,通过定义特别的运算符,浮点运算已经被添加到BCPL,但是该机制仅在适当的机器上可行——单个字长足够包含一个浮点数;这在16位的PDP-11上是不成立的。 最后,B和BCPL模型在处理指针时隐含着系统开销:该语言的规则——将一个指针定义为字长数组的索引,强迫这些指针被表示为字索引。每个指针引用会导致一个运行时尺度转换,从指针变为硬件希望的字节地址。 因为上述因素,一个 类型模式看来很有必要,以便为处理字符和字节编址,以及为即将到来的浮点硬件作好准备。其他议题,特别是 类型安全性和接口检查,当时并不显得如同后来那样的重要。【也是摸着石头过河啊】
   抛开语言本身的这些问题,B编译器的线索码技术使得程序比它们对应的汇编语言版本慢得太多,使得我们看不到将操作系统或它的核心库用B来重写的可能性。
在1971年,通过添加一个字符类型和重写它的编译器以生成PDP-11机器指令而非线索码,我开始扩展B语言。因此,从B到C的过渡是与一个编译器的创作同时进行的,该编译器要能够 产生足够快和小的程序以抗争汇编语言。我称这个轻微扩展的语言为NB,表示“牛逼 ”(new B)。
抛开语言本身的这些问题,B编译器的线索码技术使得程序比它们对应的汇编语言版本慢得太多,使得我们看不到将操作系统或它的核心库用B来重写的可能性。 在1971年,通过添加一个字符类型和重写它的编译器以生成PDP-11机器指令而非线索码,我开始扩展B语言。因此,从B到C的过渡是与一个编译器的创作同时进行的,该编译器要能够 产生足够快和小的程序以抗争汇编语言。我称这个轻微扩展的语言为NB,表示“牛逼 ”(new B)。

C的萌芽

NB存在的时间太短以致于没有编写它的完整描述。它提供了int和char类型、它们的数组和指向它们的指针,声明的典型风格如下:
    int i, j;
    char c, d;
    int iarray[10];
    int ipoint[];
    char carray[10];
    char cpoint[];
数组的语义与B和BCPL的完全一样:声明iarray和carray将分配单元,分别被所指向的十个整数或字符序列中的第一个的(地址)值所动态初始化。对ipointer和cpointer的声明省略了尺寸,以表明没有存储(空间)被自动分配。在处理时,语言的解释对指针和数组变量是同样的:一个指针声明所产生的一个单元与数组声明所产生的单元,区别仅在于(前者)由程序员给它赋值其所指,而不是让编译器分配空间和初始化该单元。
NB存在的时间太短以致于没有编写它的完整描述。它提供了int和char类型、它们的数组和指向它们的指针,声明的典型风格如下: int i, j; char c, d; int iarray[10]; int ipoint[]; char carray[10]; char cpoint[]; 数组的语义与B和BCPL的完全一样:声明iarray和carray将分配单元,分别被所指向的十个整数或字符序列中的第一个的(地址)值所动态初始化。对ipointer和cpointer的声明省略了尺寸,以表明没有存储(空间)被自动分配。在处理时,语言的解释对指针和数组变量是同样的:一个指针声明所产生的一个单元与数组声明所产生的单元,区别仅在于(前者)由程序员给它赋值其所指,而不是让编译器分配空间和初始化该单元。
    与数组和指针名字绑定的单元所存储的值,是对应存储区的机器地址,按字节度量。因而,通过指针的间接引用,不导致将指针从字长伸缩为字节的运行时开销。另一方面,数组下标和指针算术的机器代码,现在依赖于数组或指针的类型:计算iarray[i]或ipointer+i表示移动加数i乘以被指向对象尺寸。
与数组和指针名字绑定的单元所存储的值,是对应存储区的机器地址,按字节度量。因而,通过指针的间接引用,不导致将指针从字长伸缩为字节的运行时开销。另一方面,数组下标和指针算术的机器代码,现在依赖于数组或指针的类型:计算iarray[i]或ipointer+i表示移动加数i乘以被指向对象尺寸。
这些语义意味着从B转换较容易,而我花了几个月测试它们。当我尝试扩展类型标识,特别是添加结构体(记录)类型时,麻烦比较明显了。结构体似乎应该直观地映射到机器的内存,但是,当结构体包含一个数组时, 没有一个好位置存储包含数组基地址的指针,也没有便捷的方式将它初始化。例如,早期unix系统的目录条目,在C可以被描述为
    struct {
        int  inumber;
        char name[14];
    };
这些语义意味着从B转换较容易,而我花了几个月测试它们。当我尝试扩展类型标识,特别是添加结构体(记录)类型时,麻烦比较明显了。结构体似乎应该直观地映射到机器的内存,但是,当结构体包含一个数组时, 没有一个好位置存储包含数组基地址的指针,也没有便捷的方式将它初始化。例如,早期unix系统的目录条目,在C可以被描述为 struct { int inumber; char name[14]; };
我希望,结构体不仅表示抽象对象,也要描述那些可以从目录读到的比特的集合。编译器能把语义所要求的name的指针藏在哪里呢?即使结构体被更抽象地看待而且指针的空间也能以某种方式隐藏,我又该如何处理分配一个复杂对象时初始化那些指针的技术问题呢?某人或许定义的结构体包含数组而该数组却包含结构体,直到任意的深度。
该解决方案成为无类型BCPL到类型化C进化链上的一个重要飞跃。 取消在内存中该指针的存在,而是当数组名出现在表达式中时才生成指针。这个规则——延续到如今的C,含义是 当数组类型出现在表达式中时,数组类型的值被转换成指向组成数组的对象中的第一个对象的指针
我希望,结构体不仅表示抽象对象,也要描述那些可以从目录读到的比特的集合。编译器能把语义所要求的name的指针藏在哪里呢?即使结构体被更抽象地看待而且指针的空间也能以某种方式隐藏,我又该如何处理分配一个复杂对象时初始化那些指针的技术问题呢?某人或许定义的结构体包含数组而该数组却包含结构体,直到任意的深度。 该解决方案成为无类型BCPL到类型化C进化链上的一个重要飞跃。 取消在内存中该指针的存在,而是当数组名出现在表达式中时才生成指针。这个规则——延续到如今的C,含义是 当数组类型出现在表达式中时,数组类型的值被转换成指向组成数组的对象中的第一个对象的指针。
尽管在这些语言的语义上根本不同,这个发明使得已有的B代码能继续工作。较少的、给数组名赋新值以调整其原点——在B和BCPL中是可能的,在C中无意义——的程序能够很容易地修改。更重要的是,新语言使用了对数组语义的一种一致和可行(或许不常见)的解释,因而打开了通往更复杂类型结构的大门。

将C与其前辈极其明显地区别开来的第二个创新,是其较完整的类型结构,尤其是(表现)声明语句的语法(格式)的表达式。NB提供基本的类型int和char,和它们的数组,指向它们的指针,但没有更进一步的组合。更一般地:给定任意类型的一个对象,应该能够描述将若干个(对象)聚集为一个数组、从一个函数求出或指向它的指针之类的新对象。对于这种复合类型的每一个对象,已经有一种涉及低层对象的途径:索引数组,调用函数,指针上使用间接操作符。类比推理导致了声明语法,名字镜像表达式语法中名字经常出现。因此,
    int i, *pi, **ppi;
声明一个整数(integer),一个指向整数的指针(pointer to an integer),一个指向整数的指针的指针(a pointer to a pointer to an integer)。这些声明语法反应了一种现象即当用于一个表达式时i,*pi和**pi都产生于一个int类型。类似地,
    int f(), *f(), (*f)();
声明一个返回整型值的函数(function),返回整型指针的函数,一个指向函数的指针而返回函数的返回值为int;
    int *api[10], (*pai)[10];
声明一个整型指针数组(an array of pointers to integers),一个指向整型数组的指针(a pointer to an array of integers)。在所有这些情况中,一个变量的声明类似于它在表达式中的用法,其类型是置于声明语句开头的那个。
C语言采用的类型组合模式归功于Algol 68,尽管它或许没有以Algol信徒认可的形式出现。我从Algol吸取的主要概念,是一个基于原子类型(包括结构)的类型结构,组合成数组,指针(引用),和函数(过程)。Algol 68关于union(共用体)和造型(cast/类型转换)概念的影响,也在后来表现出来。
尽管在这些语言的语义上根本不同,这个发明使得已有的B代码能继续工作。较少的、给数组名赋新值以调整其原点——在B和BCPL中是可能的,在C中无意义——的程序能够很容易地修改。更重要的是,新语言使用了对数组语义的一种一致和可行(或许不常见)的解释,因而打开了通往更复杂类型结构的大门。 将C与其前辈极其明显地区别开来的第二个创新,是其较完整的类型结构,尤其是(表现)声明语句的语法(格式)的表达式。NB提供基本的类型int和char,和它们的数组,指向它们的指针,但没有更进一步的组合。更一般地:给定任意类型的一个对象,应该能够描述将若干个(对象)聚集为一个数组、从一个函数求出或指向它的指针之类的新对象。对于这种复合类型的每一个对象,已经有一种涉及低层对象的途径:索引数组,调用函数,指针上使用间接操作符。类比推理导致了声明语法,名字镜像表达式语法中名字经常出现。因此, int i, *pi, **ppi; 声明一个整数(integer),一个指向整数的指针(pointer to an integer),一个指向整数的指针的指针(a pointer to a pointer to an integer)。这些声明语法反应了一种现象即当用于一个表达式时i,*pi和**pi都产生于一个int类型。类似地, int f(), *f(), (*f)(); 声明一个返回整型值的函数(function),返回整型指针的函数,一个指向函数的指针而返回函数的返回值为int; int *api[10], (*pai)[10]; 声明一个整型指针数组(an array of pointers to integers),一个指向整型数组的指针(a pointer to an array of integers)。在所有这些情况中,一个变量的声明类似于它在表达式中的用法,其类型是置于声明语句开头的那个。 C语言采用的类型组合模式归功于Algol 68,尽管它或许没有以Algol信徒认可的形式出现。我从Algol吸取的主要概念,是一个基于原子类型(包括结构)的类型结构,组合成数组,指针(引用),和函数(过程)。Algol 68关于union(共用体)和造型(cast/类型转换)概念的影响,也在后来表现出来。
   创造了类型系统、相关的语法和新语言的编译器之后,我认为它该有个新名字;NB看起来不够独特。我决定延用单字母风格并为之取名为C,这也引发了一个问题:这个名字表示的是字母表顺序还是BCPL中的字母顺序呢?【D语言按照的是字母表顺序,而有人认为C的下一个语言应该是P】
创造了类型系统、相关的语法和新语言的编译器之后,我认为它该有个新名字;NB看起来不够独特。我决定延用单字母风格并为之取名为C,这也引发了一个问题:这个名字表示的是字母表顺序还是BCPL中的字母顺序呢?【D语言按照的是字母表顺序,而有人认为C的下一个语言应该是P】

经验分享 程序员 微信小程序 职场和发展