C语言的发展
—Dennis M.Ritchie (就是K&R中的R)
dmr@bell-labs.com
翻译:daizisheng,水平有限,敬请原谅
概述:
70年代初,c语言以系统实现语言(system implementation language)的角色开始
出现在早期的UNIX系统上。它由无类型(typeless)的BCPL语言发展而来,并引入了
丰富的“类型”,而他的产生最初是为了改进小型机(tiny machine)的编程环境。
今天,c语言已经成为了一门极具优势的语言,这篇文章着眼于c语言的整个不断发展,
不断革新的历史过程。
简介:
这篇文章着眼于介绍c语言的发展过程,它的影响,以及它产生的条件。为了简短,
我省略了对c语言本身的详细描述,同样也省略了对其前身B,以及B的前身BCPL语言的详细
描述。取而代之,我们将关注每种语言的主要特征以及他们是怎样变迁的。
c语言在1969-1973这段时间产生了出来。这段时间刚好也是UNIX系统早期发展的时间。
而这几年中最具有创造性的时间出现在1972年。另外一个比较大的改变发生在1977-1979,
这个时候UNIX系统的可移植性已经被证实。在这个时段的中期,第一部关于c语言的详细
描述的书籍问世了,这就是<>,这本书通常也被称为”白皮书
“或者”K&R”。最终,在80年代中期,c语言被ANSI X3J11正式标准化。这个标准后来也得到
了修正。直到80年代初期,虽然c语言的编译器出现在了各种不同的体系结构以及操作系统
上,但是它仍然是几乎和UNIX系统捆绑在一起的,直到更晚的一些时候,c语言才扩散到
越来越广泛的体系上。到了今天,c语言已经成为了一门在整个计算机业上的普遍应
用的语言了.
历史:
60年代后期对于贝尔电话实验室是一个混乱的(turbulent)年代。公司被卷入了一个由
MIT,通用电气以及贝尔实验室联合发起的有点探险性质的Multics项目。到了1969年,贝
尔试验的管理层以及研究人员们开始感觉到Multics项目如果要实现,将会拖到很晚,
并且将会付出高昂的代价。就是在GE-645 Multics机被从计划中取消之前,一个由Ken
Thompson私人领导的非正式的团队就开始改变他们的目光了。
Thompson想通过他自己的设计,利用一切可以利用的办法,实现一个比较令人满意的
计算机环境。这个计划包含了许多Multics的创新思想,比如清楚的把一个进程当作一个
控制的轨迹,树型结构的文件系统,一个用户级的命令解释器,文本文件的简单表示,
设备的通用存取。他们也剔除了一些其它的东西,比如统一的内存存取以及文件存取。
最开始,Thompson以及我们其他的人推迟了Multics的另外一个创新特点,也就是以一种
高级语言书写其中的大多部分。对PL/I–Multics的实现语言,我们没有作太多的尝试。
我们同时使用其他的语言,包括BCPL。我们感到很遗憾,这样失去了以汇编语言书
写程序的很多优点,比如书写容易,易于理解。在那个时候,我们并没有很注重可移
植性,直到后来,我们才对这个发生了兴趣。
Thompson当时面对的硬件环境即使在当时也是很难懂,体系很简单的:DEC PDP-7。
1968年他开始的时候是一台拥有8K 18-bit字内存并且没有任何可以使用的软件的机器。
在希望使用高级语言的同时,他使用PDP-7汇编语言写了起初的UNIX系统。在最开始,
他甚至就没有对PDP-7本身写程序,而是在一台GE-635机器上使用一堆GEMAP汇编器的宏。
一个postprocessor(??)将这些生成为PDP-7可读的纸带(paper tape)。
这些纸带知道后来才被放到PDP-7上测试,这个时候一个简单的UNIX内核,一个编辑器
,一个汇编器,一个简单的shell以及一些有用的工具已经完成了。这个时候,整个操作系统
就是自支持的了:程序的书写以及测试再也不需要求助于纸带了,整个系统的开发就可以在
PDP-7系统本身上做了。
Thompson的汇编器在简单性上即使是对DEC的也有过之而无不足,它对表达式进行求
值并改变(emit)相应的位。这个时候没有库,没有装载器,没有连接器,程序的整个源代码
都送给汇编器,并且输出程序有着一个固定的名字以显示它是可以直接执行的。
(这个名字是a.out,这就是解释了现在UNIX操作系统的一个特点:即使在后来系统引入了
连接器以及能够明确指明另外一个名字,但是这个仍然被保留为编译后默认输出的可
执行结果的名字。)
在1969年第一个UNIX系统在PDP-7上开始运行不久,Doug Mcllroy为这个新的系统创
建了一门高级语言:一个McClure的TMG的实现。TMG(TransMoGrifiers)是一种用于书写
编译器的语言。他使用自上而下的递归风格(stytle),并结合了上下文无关文法属性。
Mcllory和Bob Morris就是使用TMG为Multics写了早期的PL/I编译器的.
面对Mcllroy重写TMG功绩的挑战,Thompson决定系统(当时还没有命名)需要一种系
统语言。在快速的尝试Fortran后,它创造了一门他自己的语言,这就是B。B可以被认为
是没有类型的C语言,更准确地说,它就是将BCPL压缩到8k bytes的内存,当然是经过
Thompson自己的改进了的。它的名字很可能就表达了它是BCLP的压缩。另外一些不同
的观点则认为B是从Bon设计而来得,这里的Bon是Thompson在做Multics项目的时候设
计的一门语言,而Bon这个名字这是由他的夫人Bonnie而来得,不过有一份手册表明
Bon来自于一个宗教,在这个教里面有低语的宗教规则。
起源
(这篇涉及到BCPL和B中的很多术语,有的地方真的搞不懂)
BCPL由Martin Richards于60年代中期,也就是他正visit MIT的时候,设计
出来,并在70年代早期广泛的用于很多的项目当中,这些项目包括Oxford的OS6
操作系统,Xerox PARC的早期工作(seminal Alto work)。我们后来熟悉它,完
全是归功于MIT CTSS系统,Richards自己就是使用这个系统作Multics开发的。
早期的BCPL汇编器被移植到了Multics以及GE-635 GECOS系统上,这些工作是由
Rudd Canaday完成的,而Bell实验室则将它移植到了更多的其他系统上。还在
Multics在Bell实验室的最后的一段时间以及不久以后,它便成为了这群不久就
卷入了UNIX的人们的选择。
BCPL,B,C和Fortran以及Algol 60一样都属于过程性语言。相比之下,他们
面向系统程序设计,精小,很容易被描述,并且很容易被简单的编译器编译。
他们更加靠近机器本身,因为他们所引入的抽象很容易基于传统计算机系统所
提供的数据类型以及操作,同时他们依靠库例程进行基本的输入输出以及其他
和操作系统的交互。他们也使用库例程做一些其它的控制结构,比如协同过程,
过程结束等等。同时,他们的抽象都处于一个充分高的层次,在各个可以使用
的系统之间具有可移植性。
BCPL,B,C在语法上有很多的细节不同,但是在很多的方面他们是类似的。
程序由全局数据的声明和函数(过程)的声明序列构成。在BCPL中,过程是可以
嵌套的,但是不可以使用包含过程的过程的非静态声明的数据。B,C则使用更加
严格的做法从而抛弃了这一限制:在B和C中不允许使用这一嵌套结构。这些语言
都支持分开编译,(除了早期的B外),并且提供了一种办法通过名字将其它的
文件包含进程序里面。
BCPL中的许多的语法词法机制和B和C相比起来更加的文雅和正规。例如,
BCPL的过程和数据声明有一个更加正规的结构,并且它提供了一个更加完整的
循环结构集合。虽然,BCPL程序概念上是一个无界的字符流,但是聪明的规则
使的大多数紧跟在行边界语句后的分号被忽略掉(??)。B,C则没有使用这个做法,
它们使用分号来结束大多数语句。尽管有很多的不同,大多数的BCPL的操作和
表达都在B和C语言中都有相应的对应。
BCPL和B的一些结构上的差异主要在于中间内存(internediate memory)的限制。
例如,BCPL的声明可以使用这种形式:
let P1 be command
and P2 be command
and P3 be command
在这里command所代表的程序段包含了整个过程。而有and所引出的声明,比如上
面的P3,被认为是P1的内部过程声明。类似的,BCPL能够将一组声明包装在一个
有值的表达式里面,例如:
E1 := valof $( declarations ; commands ; results E2 $) + 1
BCPL的编译器能够通过输出前在内存中存储和分析整个程序来处理这些结构。由于
存储空间的限制,B语言中要求一种one-pass技术来尽快的输出结果,而由此而来
的语法上的重新设计也被带入了C语言中。
BCPL的另外一些不尽人意的方面也是技术问题,这些在B语言的设计的时候被剔除
掉了。举一个例子,BCPL使用一种“全局表”的机制来处理分开编译的程序之间
的交互。在这种机制下面,程序员必须明确的将程序的任何外部可见的数据和过程
和这个表的一个偏移量联系在了一起,而连接器则使用这些偏移量来完成连接过
程。B语言在最开始的时候主张整个程序应该以一个整体提交给编译器,从而避开了
这种机制。而后的B以及所有的C语言的实现则使用了连接器来解决这些分开编译的
程序的外部符号的问题,从而把这些繁复的给名字分配偏移量的工作从程序员手中
接了过来。
其它的一些从BCPL到B的改变是作为一种尝试引进来得,其中的一些仍然是有争议
的。比如B中使用=号作为代替原来的:=的赋值符号。类似的比如B使用/**/来包含
注释,而在BCPL中则是使用//把紧跟在后面的直到行的结束作为注释,PL/I就继承了
BCPL的这一点,这里也可以看到C++也回到了BCPL的这种注释机制上来了。同时,
Fortran也影响了B的声明语法:B的声明以specifier比如auto,static等开始,紧接
着是一个名字串,而在C中不仅仅跟随了这种风格,而且还引入了类型关键字放在
声明的开始部分。
并不是所有从BCPL来得改变在Richard的书中都记录了下来,B是经过深思熟虑的。
我们使用的是BCPL的一个比较早的版本。例如,用于switchon结构退出的endcase在
我们60年代使用的版本就没有。所以使用break关键字来退出B和C的switch结构归咎
于分支结构的发展而不是从BCPL发展而来的。
与B的设计的时候从BCPL来得语法变迁相比,BCPL的核心–它的类型的结构以及它
的表达式赋值规则–在B中都保留了下来。它们都是无类型的,或者说就只有一种数
据类型”word”或者”cell”,这些都是固定bit长度的。这些语言中的内存都是这些基本
单元的线性数组,而这些基本单元中的内容取决于使用的操作。以+操作为例,简单
的使用机器整数加指令来完成操作数的相加,其他的算术操作则同样对操作数的实际
意义并不在意。因为内存是一个线性数组,所以使用一个索引来引用内存中的一个基
本单元便成为了可能,并且在BCPL中提供了一个操作符号来实现这种想法。在最开始,
它使用rv,接着是!,而在B中则是使用*。因此,如果p是一个单元的索引,*p则就是所
指向单元的实际的值,这个既可以作为表达式中的一个值,也可以作为赋值的对象。
因为BCPL和B中的指针就是一个代表内存数组索引的整数,所以在它上面的算术操作
就是有意义的。这种约定是这些语言中的数组语法的基础。在BCPL中的
let V = vec 10
和B中的
auto V[10];
的效果都是一样的:一个名字被称为V的内存单元被分配,并且有一个10长的连续内存
单元序列被分配到其旁边,这个序列的第一个单元的内存索引被放在V中。在通常的
规则下,B中的表达式:
*(V+i)
把V和i相加来引用V后面的第i个单元。在BCPL以及B中都增加了另外的语法来方便这种
内存的存取;在B中上面的表达式等价于
V
而在BCPL中则是
V!i
即使在在当时数组的这种处理方法也不是很通用的,而C则以更加方便的做法吸收
了这一做法。
BCPL,B,C没有哪一个对字符串有比较强的支持,他们都把字符串当作整数向量(
vectors of integers)并且提供了一些通用的规则来提供一些使用上的便利。在BCPL
和B中一个字符串代表的是一个地址,这个地址指向一个静态区域,而这个静态区域被
初始化为了这个字符串中的所有字符。在BCPL中这个区域的第一个字节代表字符串的
长度;在B中则没有这个长度字节,取而代之的是一个终结符,B中称之为‘*e’。这
种改变一方面是用来摆脱一个8-bit 或者是9-bit字节的长度字节所带来的字符串长度
的限制,另一方面因为在我们经验中保存长度的办法相比之下没有使用终结符来结束
字符串的办法方便。
BCPL中单个的字符经常是通过把字符串扩展到另外的数组里面来存取的,这种扩展
将每个字符映射到一个内存单元里面,并在稍后进行重新的打包压缩;B提供了
相应的处理过程,但是人们通常使用另外的库例程来存取或替换字符串里面的单个字
符。
更多的历史:
当B的TMG版本开始运行后,Thompson使用B重写了B本身。在开发过程中,
他继续和有限的内存容量作较量:每次语言的扩充都会使编译器变大,而
每次重写编译器又会缩小编译器。例如,B引入了一类通用的赋值操作符,像
使用x=+y来在x上增加y。这个想法来自经由Mcllroy的Algol 68,Mcllroy就
把这个结合到了他自己的TMG版本当中。(在B以及早期的C中,这些操作符
使用=+,而不是现在的+=,这个“错误”在1976年被修改掉,而这种做法则是
因为当时的B词法分析器拥有一种吸引人的简单的处理方法。)
Thompson更进一步引进了++,–操作符来对操作数进行加一或减一的操
作,并由他们出现的位置是前缀还是后缀来决定改变操作数的时机–使用
操作数之前还是之后。他们并不是一开始就出现在最早的B中,而是在后面
在逐渐发展起来的。人们都猜测当初引入他们是为了利用使得C以及UNIX变
得流行的PDP-11体系所提供的自动增减的地址模式(auto-increment and
auto-decrement memory modes)。但是这种猜测在历史上是错误的,因为
B语言设计出来的时候根本就没有PDP-11。在PDP-7上倒是有一些自动增的
内存单元,这些单元在通过他们的内存间接引用的时候自动增加其值。这
种性质也许为当初Thompson的这些操作符的创建提供了灵感,但是这些操作
符的可以前缀也可以后缀的通用性则是属于他自己的想法。事实上,这些
自动增加的内存单元并没有在这些操作符的实现中直接用到,一种更强烈
的促使这种创新的原因或许是他意识到++x比x=x+1更加的短小。
PDP-7上的B编译器并不生成机器指令,而是生成“threaded code”。
这是一种解释型的代码,是编译器输出的一串由基本操作代码片断,而这些
操作则是运行在一个简单的栈式机上的。
当时的在PDP-7上的UNIX系统只有很少的包括B本身在内的部分是用B写
的,因为当时的机器很小很慢,除了试验以外什么也不能够作。使用B来重
写整个操作系统以及其他系统部分代价将会十分的昂贵,并且看起来也是不
可行的。在某一点上,Thompson通过提供一个“虚拟B”编译器来克服地址空
间的限制。他通过使用解释器分页程序的代码和数据来允许被解释的程序
占用多余8K的内存空间。但是这种机制太慢了,不能够广泛的用于实际的
应用中。不过,还是有一些利用B写的应用出现在那个时候,比如为UNIX用户
所熟悉的可变精度计算器dc的早期版本。我所处理到的最具雄心的计划是
一个实在的将B语言转换成GE-635机器指令而不是前面的的“threaded code”
的交叉编译器。这是一个小型的“tour de force”:一个完整的B编译器,
他用他自己的语言写成,声称36位的大型机的代码,并且运行在一个18位
的并且只有4k用户地址空间的机器上。这个项目之所以可能就是归功于B
语言的简单以及他的运行系统。
虽然我们偶然有实现当时的主流语言比如Fortran,PL/I,Algol 68,
但这看起来就我们的资源来说是无望的:我们只有十分简单的小工具可以
利用。所有的这些语言都影响着我们的工作,但是我们倒觉得使用我们
自己的东西更有趣。
到1970年,UNIX项目已经展示出了足够的潜力,我们也能够获得新的
DEC PDP-11了。处理器是DEC运来的第一批产品之一,3个月后他的磁盘也
运来了。为了使B程序的“threaded code”能够在上面运行,我们只是需要
为那些操作写对应的代码片断以及一个后来由我完成的汇编器。不久,dc
成为了在PDP-11上运行的第一个比较有趣的程序,而在这时,机器上还没
有任何的操作系统。很快的,还在磁盘运抵之前Thompson就已经将UNIX内
核以及一些基本的命令使用PDP-11的汇编语言重新编码。在机器上的24K
字节的内存中,早期的PDP-11操作系统使用12K来供操作系统使用,另外
把一段很小的空间供用户程序适用,而剩下的作为RAM disk。这个系统
只用来做测试使用,并不是为了做什么实际的工作;机器通过枚举可变
棋盘上的闭合骑士行迹来测试时间。当磁盘出现后,我们搞懂了PDP-11的
汇编语言后就迅速的转向了他,并把B中已经有的东西移植了过去。
到1971年我们这个小计算机中心开始有了用户。我们所有的人都希望
更加容易的编写一些有趣的软件。使用汇编器是十分烦躁的,所以B开始
提供了一个关于一些有用的服务例程的小型库,这个库接着被用于越来越
多的新的程序里面。Steve Johnson的第一个yacc版本就是在那个时代的
这些程序中比较著名的之一。
B的问题:
我们最开始使用BCPL以及B的机器都是字编址(word-addressed)的,并且这些
语言只有一种数据类型,那就是“cell”,对应于及其本身的字。PDP-11的出现
暴露出了B语言语法模型的很多不足。首先是他的字符处理机制,这种从BCPL经过
很少变化而来得机制是笨拙的—使用库例程来扩展数组到分立的内存单元然后
在重新打包压缩回去或者是存取并替换单独的字符都开始显示的十分笨拙,甚至
是愚蠢,因为这是一种面向字符的机器。
其二,虽然早期的PDP-11并没有提供浮点计算,但是制造商说明这个将在不
久后的产品中添加进来。浮点操作在我们的Multics的BCPL以及GCOS编译器上已经
被引入了,这是通过定义一些特殊的操作符来完成的,但是这种机制之所以能行
是因为在这些相关的机器上,一个字就能够容下一个浮点数,在我们的16位的
PDP-11上这却是不行的。
最后,B以及BCPL模型意味着这样处理指针:通过定义指针为一个字数组的
索引而强迫指针被表达为字索引。因此,每个指针引用都产生一个运行时间的向
硬件需要的字节地址的转换。
由于这些原因,一些机制比如处理字符和字节地址以及为即将到来的浮点数
处理做准备看起来是有必要的。而其他方面,特别是类型安全和界面检查,在那
个事后还没有显示出其后来的重要性。
和这些问题一起的还有B语言本身。B编译器的“threaded code”技术产生
程序和相应的汇编语言程序相比实在是太慢了,基于这个原因我们对使用B来对
操作系统以及一些系统机制重新编码的做法大打折扣。
1971年我开始扩展B语言,增加字符类型,同时也把B的编译器修改为生成
PDP-11的机器指令而不是原来的threaded code。因此可以说,从B到C的转变和
创建一个有能力生成与汇编语言相比足够小足够快的代码的编译器是同一个时间
的事情。我称这个稍稍扩展的语言叫“NB”,也就是“new B”。
早期的c
NB存在的如此之短以至于没有完整的关于他的描述。他提供了一些简单的类型:
int,char,以及int,char数组和指针。典型的声明是这样的:
int i,j;
char c,d;
int iarray[10];
int ipointer[];
char carray[10];
char cpointer[];
关于数组的语法和B以及BCPL保持了一致:iarray,carray的声明分别产生一个内存
单元,这个单元分别被初始化为连续的10个整数和10个字符的第一个元素的指针。
而指针ipointer,cpointer声明的时候则不提供类似上10的尺寸,从而用以说明不必
自动分配空间。在过程当中,指针的解释和数组变量的解释相同:指针的声明和
数组的声明都创建一个单元,而他们的不同仅仅在于在前者程序员被期望给这个单元
赋予一个指针值,而后者的这个工作则是由编译器自动分配空间来初始化完成的。
在数组和指针名字中(也就是上面分配的那个内存单元中)存储的值是机器地
址,以字节为单位,并在内存中有对应的存储区域。因此,通过指针的间接引用不会
导致运行时间的指针的从字偏移到字节偏移的转换。另一方面,数组下标以及指针的
运算对应的机器代码将和他们所代表的具体的数据类型有关了,具体一点,为了完成
iarray或者ipointer+i的引用就必须在对应的基址上加上i倍所使用的对象的数据
类型的大小。
这些语法很容易的就从B转换了过来,我也在这些方面花了几个月的时间来不停
的实验。当我试着扩充类型,特别是结构体类型的时候,一些问题就出来了。结构体
看起来可以直觉上的映射到物理内存中去,然而如果结构体中包含了数组,就没有一
个好的地方来存放这个隐藏的指向数组基址的指针,也没有一种好的办法来初始化他。
例如,早期的UNIX系统的目录项可以用C描述如下:
strucr {
int inumber;
char name[14];
};
我所希望的结构体不仅仅是指一个抽象的对象,而且应该是一个能够从目录里面读取
的bit集。编译器在哪里隐藏这些语法上需要的name呢?即使结构体被考虑得更加的
抽象而使得这个指针可以隐藏起来,但是当分配一个更加复杂的对象的时候,合适的
初始化这些指针仍然是一个技术难题—这些复杂的对象可能是包含有数组的结构体
有包含了结构体,而这种包含可以达到任意的深度。
而这些问题的解决构成了从无类型的BCPL到有类型的C的变化过程的至关重要的
环节。这些解决办法消除了这些指针的实际的存储,取而代之的是当数组在表达式
中被引用的时候才创建这些指针。这些在今天的c中仍然保留的规则是说:当数组出
现在表达式的时候,数组的值转变成为了指向构成数组的第一个元素的指针。
这个改变尽管改变了原来的语法,但是还是能够使当时已经存在的大多数B代码
能够继续工作。而有一些B和BCPL程序通过改变数组名字的值来重新对齐数组,这些
程序在C中是没有意义的,但是很容易就可以被修改过来。更加重要的是,这门新的
语言保留了和原来数组语法的一致的和可以运作的解释,并且为更加全面的结构体
类型敞开了大门。
第二个区别C和他的前身的创新是更加丰富的类型结构以及这些类型的语法声明。
NB提供基本的数据类型int,char,以及他们的数组和指针,而再也没有其他的了。我
们需要更完善的通用性:给定一种数据类型,就能够描述一种新的类型来代表这种给
定的类型的数组,也应能够从函数中返回这种类型,以及能够描述指向这种类型数据
的指针。
对上面提到过的类型,已经有了一套办法来描述这些提出的对象:数组,访问函
数,使用指针的间接引用。类似的原因导致了相应的一些名字的声明语法,而这些
语法则反映了这些名字在表达式中出现的典型位置。因此,
int i,*pi,**ppi;
声明了一个整数,一个指向整型变量的指针,以及一个指向这样的指针的指针。这些
声明语法则反映出i,*pi,**ppi在表达式中都代表一个整数值。类似的,
int f(),*f(),(*f)();
则分别声明了分别返回整数的函数,返回整型指针的函数,和一个返回整数的函数
的指针;
int *api[10],(*pai)[10];
则分别声明了一个整型指针数组和一个指向整型数组的指针。所有的这些变量的声
明都和变量在表达式中的使用方法类似,而这种表达式的类型就是声明的最开始的
那个东东。
c所采用的这种类型书写办法很大程度上归功于Algol 68,尽管它并不是和Algol
的拥护者所愿望的一样。我从Algol上所获得主要思想是基于原子类型(atomic types)
的,可以被集合成数组的类型结构,指针,函数。Algol 68的联合和casts(??)的概念
也对后C中出现的相应的东东有所影响。
创建了类型体系,以及相关的语法以及这个新的语言的编译器之后,我感到这个
新的语言应该设计一个名字了。NB看起来不够独特,所以我决定继续使用单个字母的
风格并且称之为C,而这个C代表的是字母表顺序还是在BCPL中的顺序我也搞不清出了。
新生的C
当这门语言被命名之后很快就发生了变化,例如引入了&&和||操作符。在
BCPL和B中表达式的取值取决于上下文:if和其他的条件语句把表达式和0进行
比较,而在这些语言中给予and(&),or(|)操作符以特殊的解释。在普通的情况
下,他们是位操作,但是在B语句:
if(e1 & e2) …
编译器对e1取值,如果非零再对e2取值,如果仍然不是零,详细的描述就取决
于if语句。“需求”在e1,e2的&和|操作符上递归下降处理(??)。这些在
真值上下文(”truth-value” context)中的Bool操作符看起来是需要的,但是
这些操作符的重复使用(overloading)导致其难于理解和使用。在Alan Snyder
的建议下,我引入了&&和||来使这种机制更加的清楚明了。
它们的后来引入解释c的优先级规则的一些不合适的地方。在B中使用
if(a == b & c) …
来检查a等于b或者c非零。在这个条件表达式中&比==的优先级更低。在将B转变
到C后,便希望在这个表达式中使用&&来代替&来消除原来的麻烦,我们决定
保持&相对于==的优先级,并且使得&&的优先级稍微比&低。今天看来好像改变&
和==的优先级关系将更加的合适,这种改变可以简化c的一个习惯用法:使用
mask来测试另外一个值,我们会这样书写
if((a&mask) == b) …
然而内层的那个必须的括号却是我们很容易遗忘的。
在1972年到1973年,c语言发生了很多的改变,而这其中最为重要的是预处
理程序,这个一部分是由于Alan Snyder的促进,同时也是意识到BCPL和PL/I上
已有的文件包含机制十分的有用。最早的版本提供的机制十分的简单,只是提供
了文件包含和简单的字符串替换:#include和没有参数的#define宏。不久以后
这个得到了扩展,开始有了有参数的宏替换和条件编译,这些大多数是由Mike
Lesk,John Reiser完成的。预处理器在最开始被认为是语言本身的可选的附属
品。实际上,在很多年里,除非在程序的开始有明显的标志否则它是不会被调
用的。这种态度保持着,这就解释了早期的预处理器的语法的不完整以及在早
期的语言手册里面没有对预处理器的精确的描述。
标准化
到了1982年,C语言需要标准化已经成为一个共识。K&R的第一版,作为当时被认为最
接近标准的版本,已经不能描述实际中使用的语言。特别地,它没有提到void和enum类
型。虽然它预言了对structure的实现,语言对structure的具体支持是在其出版之
后的事。尽管AT&T发布的另一个编译器体现了这些变化, 而且大多数并非基於pcc的
编译器也很快吸收了这些新特性,但缺乏一个对C语言完整权威的描述的问题并没有
得到解决。
由於K&R在语言的许多细节上也没有达到足够精确的要求,把pcc作为参照编译器的
做法逐渐不大可行了。他甚至没有体现出K&R所描述的语言本身,更不必说后来的扩
展版本了。最后,C 开始被逐渐应用到政府和商业使得官方标准的正式出版变得重
要。 因此 (在M,D.McIlroy的敦促下),本着建立一个C标准的目的,ANSI于1983年
夏天成立了一个在CBEMA指导下的X3J11委员会。该委员会与1989年底发布了ANSI89报
告,随即,该标准被ISO接纳为ISO/IEC9899-1990。
从一开始,X3J11委员会对C语言及其扩展进行了广泛细致的考察。让我十分满意的
是,他们很认真地为了这样一个目标而努力:”为C程序语言建立一个清楚,
consistent,
无歧义的标准, 从而系统地阐述对公认的现有的C定义,提高用户程序的可移植性
” 显然他们知道仅仅靠发布标准并不能带来根本的变化。
X3J11 对C语言所引入的真正重要的改变只有一处, 即参考C++[Stroustrup86]的做
法, 在函数原型 (signature) 中增加了形参的类型说明。
另外, X3J11还引入了一些细小的改进。比如说,增添了类型描述字const和volatile,
在类型提升的规则上有少许变动。 但是,标准化并没有改变C的特性。
而且新制定的C标准没有试图去正式地制定C的语义,因此在细节处仍然存在争议。
然而它的出现成功地解决了自C诞生以来在使用中出现的变化。而且足够精确使得
实现。
因此,C的核心部分在标准化的进程中保持不变。C标准的出台与其说是新的发明不
如说是一次更好更仔细的修订。大部分重要的修改发生在语言的外层:
预处理器和库函数。 预处理器主要是执行宏替换的功能,使用着与语言其他部分截
然不同的惯例。它与编译器之间的交互在这之前从没有得到很好的描述。
X3J11试图解决该问题。显然,结果比K&R第一版给的解释好得多。 除了更全面之外,
他还提供了一些仅在一些实现中出现的操作比如记号连接。
X3J11相信对标准C函数全面细致的描述与对语言的共作同等重要。C语言本身并不提
供输入输出以及其他与外界交互的手段。他依赖与一套标准函数。在
K&R面世的时候,C被认为是UNIX系统编程语言。 虽然我们提供了可以被容易地移植
到其他平台的函数例程,UNIX的支持是不言理解的。因此,X3J11 委员会投入了大
量时间用于设计和文档化一套库函数从而可以用于所有的一致实现里。
在标准化的过程中,X3J11目前的活动局限于解释现有的标准。值得一提的是,NCEG(Num
erical
C Extensions Group), 一个最初由Rex Jasechke发起的非正式小
组被正式接受成为X3J11.1的一个分组。他们继续考虑对C的扩充。
从其名字可以看出,主要的扩充着眼于使C语言更适合数值计算
的需要。比如,可动态分配的多维数组,对……
演变
Successors:
尽管比不上Pascal语言, C语言乃至B语言都有若干个从其直接繁衍而生
的分支。有一支的发展历史甚至早于C的诞生。Steve Johnson于1972年曾赴University
of Waterloo做访问教授,B语言也随他到了加拿大,在那里的Honeywel机器上备受
欢迎,其后还衍生出了Eh和Zed。
当1973年他回到Bell Labs时,他发现B语言已经在其故乡演化成了C语言,
连他的Yacc程序也被Alan Snyder用C语言重写。
C语言更近一些的分支里包括Concurrent C, Objective C, C*以及C++。
而且许多编译器还采用C语言作为其编译阶段的中间语言。这其中包括C的直接分
支
C++以及独立发展的语言Modula 3和Eiffel
>> 本文固定链接: http://www.vcgood.com/archives/963