不满足网上的译文,yqj2065自己翻译一下。备用。

【】中的文字是我的译注、补充。

如果原文链接失效,请google。


The Development of the C Language*

Dennis M. Ritchie
Bell Labs/Lucent Technologies
Murray Hill, NJ 07974 USA
[email protected]

概要

在1970s早期,C编程语言是作为新生的Unix操作系统的系统实现语言而设计的。衍生于无类型(typeless)语言BCPL,它进化出了一个类型结构【类型系统】;在弱小的机器上,作为改善简陋编程环境工具的这一创新,它已然成为当今主流语言之一。本文研究它的进化。

NOTE: *Copyright 1993 Association for Computing Machinery, Inc. This electronic reprint made available by the author as a courtesy. For further publication rights contact ACM or the author. This article was presented at Second History of Programming Languages conference, Cambridge, Mass., April, 1993.
It was then collected in the conference proceedings: History of Programming Languages-II ed. Thomas J. Bergin, Jr. and Richard G. Gibson, Jr. ACM Press (New York) and Addison-Wesley (Reading, Mass), 1996; ISBN 0-201-89502-1.

简介

本文讲述C程序设计语言的发展、它所受到的影响以及它诞生的条件。简洁起见,我省略了对C自身、它的父亲B[Johnson 73]和祖父BCPL[Richards 79]的完整描述,而是关注于每一种语言的特性要素以及它们是如何演化的。
在1969-1973年间,C的出现,伴随着Unix操作系统的早期发展,而最富创造性的时期是1972年。另外一个(发生)一连串改变的高峰期是1977到1979年间,此时Unix系统的可移植性得以证实。第二个阶段期间,出现了第一份被广为传播的该语言的描述:The C Programming Language,通常被称为‘白皮书’或K&R[Kernighan 78]。最后,在1980年代中期,该语言被ANSI X3J11委员会正式标准化,并作出了进一步的修改。截止1980年代早期,尽管已有各种机器结构及操作系统的【C的各种】编译器,该语言几乎与Unix特别密切关联;更近一些,它的使用传播得更广,而今天它是整个计算机产业中最广泛使用的语言之一。

历史背景

1960s晚期,是Bell Telephone Laboratories(贝尔电话实验室)的计算机系统研究(中心)的动荡岁月[Ritchie 78] [Ritchie 84]。计算机被从Multics项目组拖走[Organick 78],该项目是MIT(麻省理工学院)、General Electric(通用电气公司)和贝尔实验室的合作项目。到1969年,贝尔实验室管理层、甚至研究人员都认为,Multics项目不能按期完成并且代价高昂。甚至在GE-645 Multics机器被撤走之前,一个非正式小组——早期由Ken Thompson领导,已经着手一些其它的研究。
Thompson希望按照自己的设计、使用可用的任何方式,创造一个舒适的计算环境。事后诸葛亮地说,他的计划集成了Multics的许多创新方面,包括关于进程的清晰概念——控制块,树型结构的文件系统、作为用户级程序的命令解释器、文本文件的简单表示和访问设备的通用化。他们排除了其余特性,比如对内存和文件的统一访问。此外,开始的时候,他与我们这些俗人遵循[推迟?]着Multics的另一个先驱性(虽然不是原创)的特性,即几乎仅用高级语言来编程。PL/I【Programming Language One,IBM公司在1950s发明的高级编程语言】——Multics的实现语言,不太符合我们的口味,因而我们也使用其他语言,包括BCPL,我们担心【regretted?】失去使用在汇编程序的级别以上的语言进行编程的优势,即容易编写、易于理解。当时我们并未特别关注可移植性,到后来才有了这方面的兴趣。
Thompson面临的硬件环境,即使在那个时代也是又拥挤又简陋:他从1968年就开始使用的DEC PDP-7,只有8K的18bit字(长)的内存,并且没有对他有用的软件。虽然心想着使用高级语言,他还是用PDP-7汇编器编写了最初的Unix系统。刚开始的时候,他甚至并未在PDP-7上编程,而是在一台GE-635机器上使用GEMAP汇编器的一些宏。一个后处理器(postprocessor)生成PDP-7可读的纸带。
这些纸带从GE机器拿到PDP-7上进行测试,直到一个原始的Unix内核(kernel)、一个编辑器、汇编器、一个简单的外壳(shell)(命令解释器)和一些工具(像Unix rm, cat, cp命令)被完成。此后,这个操作系统可以自我支持:可以编写和测试程序,勿需借助纸带,并且程序开发可以在PDP-7上持续进行。
Thompson的PDP-7汇编器在简明性上甚至优于DEC的;它对表达式求值并得到对应的比特流【二进制源代码】。没有库、没有装载器或没有链接器:程序的全部源文件提交给汇编器,而其输出文件(the output file)——有一个固定名字——是可以直接执行的(这个名字,a.out,道出了一点Unix渊源;它是汇编器的输出。甚至在系统有了链接器和有了显式指定另一个名字的方式之后,它仍被保留作为编译器的默认可执行文件(名))。
Unix在PDP-7上首次运行后不久,1969年Doug McIlroy创造了这一新系统的第一个高级语言:一个McClure的TMG实现[McClure 65]。TMG是一种自顶而下,递归降解(top-down, recursive-descent)风格的编写编译器(更一般地,TransMoGrifiers)的语言,它将上下文无关的语法表示法与过程(式程序)元素相结合。McIlroy和Bob Morris使用TMG为Multics编写了早期的PL/I编译器。
受McIlroy重造TMG事迹的刺激,Thmopson认为Unix(当时可能还没有取这个名字)需要一种系统编程语言。经过用Fortran的短暂而受阻的尝试后,他创造了一门他自己的语言,他称之为B。B可被视为没有类型的C。更准确地,它是塞进8K字节内存,经过Thompson大脑过滤后的BCPL。它的名字最有可能表示为BCPL的缩写,尽管另一种理论认为它源自于Bon[Thompson 69],Thompson在Multics的那些日子创造的一门不相关的语言。Bon进而二中其一,可能是以他妻子Bonnie的名字,或者(根据它的操作手册中的一个百科全书般的引用)以一种宗教命名,该教仪式涉及咕隆咕隆的神奇咒语。【不知道是不是西藏的原始宗教:苯教(Bon)】【太难搞了

图0:[外文翻译] C语言的发展史(The Development of the C Language)

起源:这些语言

BCPL是由Martin Richards于1960年代中期,在访问麻省理工学院时设计的;在1970年代早期它被用在几个有趣的项目中,其中包括位于牛津的OS6操作系统[Stoy 72],和施乐公司PARC研究中心的创造性项目Alto的一部分[Thacker 79]。我们熟悉该语言,是因为Richards工作过的MIT CTSS系统[Corbato 62]被用于Multics开发。最初的BCPL编译器被Rudd Canaday和贝尔实验室的一些人们移植到Multics和GE-635 GECOS系统上[Canaday 69];在贝尔实验室的Multics项目奄奄一息的阶段,它成为随后转入Unix的一帮人选择的语言。
BCPL、B和C都归属于以Fortran和Algol 60为代表的传统过程式家族。它们格外地倾向面向系统编程、小巧、描述简洁,而且可被简单的编译器轻易地翻译。它们接近机器,它们所引入的抽象以传统计算机所提供的具体数据类型和操作为基础,它们依赖于例程库以输入输出以及与操作系统的其它交互。尽管不太成功,它们还使用库程序指定其他有趣的控制结构,如协程和过程关闭。同时,它们的抽象层次足够高,足够用心的话,能达到机器间的可移植性。
BCPL, B和C在很多细节上存在语法差异,但总体上它们是相似的。程序由一系列的全局声明和函数(过程)声明组成。BCPL中,过程能够嵌套,但不能引用定义在外包过程中的非静态对象。B和C通过更强行的限制避免了它:基本就没有嵌套过程。每一种语言(除了早期的B版本)都认可(文件的)分别编译,并提供了从指定文件中包含(including)文本的方式。
BCPL中的若干语法和词法机制较B和C中的更优雅和正式。例如,BCPL的过程和数据声明拥有更一致的结构,并且它提供了一套更完整的循环结构。尽管BCPL程序名义上是由分界的字符流构成,聪明的规则使得以每一行结束的语句可以省略其分号。B和C忽视了这种便利,大多数语句以分号来结束。刨除这些差异,BCPL的大多数语句和操作符直接对应B和C中的相应物。
BCPL和B之间的一些结构化的差异源于介质存储的限制。比如,BCPL声明采用这样的形式
let P1 be command
and P2 be command
and P3 be command

此处的由命令表示的程序文本包含完整过程。这些子声明相互关联而且同时出现,所以名字P3在过程P1内可见。相似地,BCPL能在求得一个值的表达式里包含一组声明和语句,例如
E1 := valof ( declarations ; commands ; resultis E2 ) + 1

BCPL编译器在产生输出前,通过存储和分析内存中该完整程序的解析过的表示,可以容易地处理此类构造。B编译器所受的存储限制决定了一种一次通过( one-pass)技术,由此尽可能快生成输出,而这一语法上的重新设计将这种可能带入到C。
BCPL中少量的、归结于技术问题的讨嫌方面,在B的设计中被有意地避免了。例如,BCPL使用一个“全局向量”(global vector)机制用于在分别编译的程序之间通信。在这种模式下、程序员要显式地将每一个外部可见的过程和数据对象的名字,与全局向量中的一个数值偏移量关联起来;被编译的代码通过使用这些数值偏移量完成链接。B避免了这种麻烦,起初是将整个程序一次性地全部提交给编译器。B的后期实现——C也如此,使用一个传统的链接器来解析分别编译的文件中的外部名字,而不是把指定偏移量的负担推给程序员。
从BCPL到B迁移的某些搞法,源于偏好而且存在争议。例如使用单个=字符代替:=表示赋值的决定。还有,B使用/**/括起注释,而BCPL使用//以注释掉直至行末的文本。这显然是PL/I的传统。(C++重新启用了BCPL的注释方式)【C语言也启用了//注释】Fortran影响了声明的语法:B的声明语句先是一个auto或static这种修饰符,后面是名字列表;C不仅遵循这种风格,还把类型关键字放在声明语句的最前面。
并非Richards的书[Richards 79]规范的BCPL与B的每一个不同都是有意的。我们起步于BCPL的一个早期版本[Richards 67]。例如,在我们于1960年代开始学习它时,从 BCPL的switchon语句跳出的endcase并没有出现,因而B和C中都使用的跳离switch语句的关键字break,只能说是殊路同归而非有意的改变。

相对于创建B的过程中产生的诸多语法变化,BCPL的核心语义内容——其类型结构和表达式求值规则——保持不变。两者都是无类型的(typeless),或者说只有单一的数据类型,“字”(word)或“单元”(cell)——一个固定长度的位模式【只有机器字】。这些语言中,内存由这些cell线性数组构成,每一个单元【保存】的内容的含义由所用的操作决定。例如+操作符,使用机器的整数加法指令,将两个操作数简单相加,其它的算术运算也同样不管它们的操作数的实际含义【操作数就是一个01串,不管它是int、char还是位图数据】。由于内存是一个线性数组,就可以将单元的值解释为该数组的索引,而且BCPL为此提供了一个操作符。开始被拼作rv,后为!,而B使用一元*。因此,如果p是这样的单元,保存另一个单元的索引(或地址,或指向它的指针),则*p表示被指向单元的内容,不管*p是作为表达式的值【右值】还是赋值语句的目标【左值】。
因为BCPL和B中指针不过是内存数组的整数索引,对它们进行算术运算是有意义的:如果p是一个单元的地址,那么p+1是下一个单元的地址。这种约定是两种语言中数组的语义的依据。用BCPL编写
let V = vec 10
或用B,
auto V[10];
效果是一样的:命名为V的单元被分配,然后再拔出另一组10个连续单元,而它们中第一个的内存索引被存放在V中。【这不是C的搞法】按照一般规则,B中的表达式 *(V+i)把V和i相加,并指向V后第i个位置。BCPL和B都增加了特别的符号以简化这种对数组的访问;在B中的等价表达式是
V[i]
在BCPL中是
V!i
这种引用数组的方法甚至在当时也不常见;后来C以稍微不同的方式沿用了这一方式。

BCPL,B或C都没有在语言中对字符数据提供强烈的支持;它们都把字符串当作整型的向量[数组]处理,并通过几个约定补充[数组的]通用规则。在BCPL和B中,字符串字面值/文字表示由字符串的字符所初始化的一个静态区的地址,(这些字符)被塞入那些内存单元。BCPL中,第一个被塞的字节包含串所拥有的字符个数;而B,没有该计数而字符串终结于一个特别的字符,在B中被拼写为 `*e’。作出这一改变的部分原因,是避免用一个8位或9位槽(slot)保存串的计数会导致的字符串长度的限制,部分原因是维护该计数似乎,按我们的经验,不如使用一个终结符方便。

BCPL串中单个的字符通常被如此处理:字符串被展开为另一个数组,每单元一个字符,然后再次打包;B提供了相应的例程,但是人们更多地使用其他的库函数以访问和替换一个串中的单个的字符。

在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]【yqj2065不知道有没有好的术语翻译,参考http://en.wikipedia.org/wiki/Threaded_code】,这是一种解释器方案,编译器的输出由一系列执行基本操作的代码片段的地址构成。这些操作通常运行于一个简单堆栈上,特别对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 knight’s 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模型在处理指针时隐含着系统开销:该语言的规则——将一个指针定义为字长数组的索引,强迫这些指针被表示为字索引。每个指针引用会导致一个运行时尺度转换,从指针变为硬件希望的字节地址。
因为上述因素,一个类型模式看来很有必要,以便为处理字符和字节编址,以及为即将到来的浮点硬件作好准备。其他议题,特别是类型安全性和接口检查,当时并不显得如同后来那样的重要。【也是摸着石头过河啊】

抛开语言本身的这些问题,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的声明省略了尺寸,以表明没有存储(空间)被自动分配。在处理时,语言的解释对指针和数组变量是同样的:一个指针声明所产生的一个单元与数组声明所产生的单元,区别仅在于(前者)由程序员给它赋值其所指,而不是让编译器分配空间和初始化该单元。

与数组和指针名字绑定的单元所存储的值,是对应存储区的机器地址,按字节度量。因而,通过指针的间接引用,不导致将指针从字长伸缩为字节的运行时开销。另一方面,数组下标和指针算术的机器代码,现在依赖于数组或指针的类型:计算iarray[i]或ipointer+i表示移动加数i乘以被指向对象尺寸。

这些语义意味着从B转换较容易,而我花了几个月测试它们。当我尝试扩展类型标识,特别是添加结构体(记录)类型时,麻烦比较明显了。结构体似乎应该直观地映射到机器的内存,但是,当结构体包含一个数组时,没有一个好位置存储包含数组基地址的指针,也没有便捷的方式将它初始化。例如,早期unix系统的目录条目,在C可以被描述为
struct {
int  inumber;
char name[14];
};

我希望,结构体不仅表示抽象对象,也要描述那些可以从目录读到的比特的集合。编译器能把语义所要求的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/类型转换)概念的影响,也在后来表现出来。

创造了类型系统、相关的语法和新语言的编译器之后,我认为它该有个新名字;NB看起来不够独特。我决定延用单字母风格并为之取名为C,这也引发了一个问题:这个名字表示的是字母表顺序还是BCPL中的字母顺序呢?【D语言按照的是字母表顺序,而有人认为C的下一个语言应该是P】

C初生

在本语言取名后,快速地变更不停,例如&&和||操作符的引入。在BCPL和B中,表达式求值依赖于上下文:在if和其它条件语句中比较一个表达式的值与零,这些语言对与(&)和或(|)运算符给与了特别的解释。在普通上下文中,它们按位运算,而在这个B语句中
if (e1 & e2) …
编译器必须对e1求值而且如果值为非零值,对e2求值,而且如果它也非零,则执行依赖if的语句。对于e1和e2内部的&和|操作符要求递归地计算。在这种true-值环境中的布尔运算符的短路(short-circuit)语义似乎令人满意,但是运算符的重载难以解释和运用。在Alan Snyder的建议下,我引入了&&和||运算符,使这种机制更明了。
它们迟到地引入解释了C优先级规则上的一个不幸。B中某人编写
if (a==b & c) …
来检测a和b是否相等并且c是否非零;在这样的条件表达式中,&比==的优先级低比较好。在从B转变为C时,某人希望在这种表达式中用&&代替&;为了使这种转换不那么痛苦,我们决定保持运算符&与==有相等的优先级,而仅仅把&&的优先级与&的做了细微区分。今天看来,变动&和==的相对优先级会更好,这样就能简化一个C通用的惯用法:为了测试一个掩码值和另一个值,人们必须这样写
if ((a&mask) == b) …
那个内层的圆括弧是需要的,但容易被忘记。
许多其它改变发生在1972-73年,但最重要的是引入了预处理器,部分原因是Alan Snyder[Snyder 74]的建议,也(因为)沿用在BCPL和PL/I中已存在的文件包含机制。它的原始版本极之简单,仅提供文件包含和简单字符串替换:#include和参数化宏#define。之后很快对它进行了扩展,合并了宏与参数还有条件编译。之后不久,它被扩展为带参数宏和条件编译,主要是Mike Lesk的工作,后来是John Reiser。预处理原本是作为语言本身的一个可选附件。许多年来,除了源代码在开始处包含一些特别符号外,它们不被使用。这种看法持续至今,也解释了在早期参考手册中,预处理语法与语言其它部分描述不完整,和对它的不精确描述。

可移植性

到1973年早期,现代C的基础部分已经完成。在那年夏天,该语言和编译器已足够强大,以允许我们用C为PDP-11重写Unix内核。(在1972年,Thompson已经简短尝试用C的早期版本——结构体之前,生成系统代码,但放弃了那次努力)也是在这段时期,编译器被转向了其它附近的机器,特别是Honeywell 635和IBM 360/370;因为语言不能独自存在,现代库的原型被开发。特别是Lesk写了一个“可移植I/O包”[Lesk 72],后来被修改成为C的“标准I/O”例程。在1978年,Brian Kernighan和我出版了The C Programming Language(C程序设计语言)[Kernighan 78]。尽管这本书没有描述一些后来变成通用的附属项,该书充当语言标准达十多年,直到一个正式的标准被采纳。尽管我们在本书上密切共事,但是分工明确:Kernighan编写了几乎所有的说明内容,而我负责包含参考内容的附录,和Unix系统的接口的那一章。
在1973-1980年间,语言又有了一些发展:类型结构增加了unsigned, long, union和枚举类型,并且结构体几乎成为第一类对象(仅缺少一个字面值符号)。同样重要的发展出现在它的环境和其它伴随技术上。用C编写Unix内核使我们对该语言的有用性、效率有足够的信心,去重写系统的库和工具,并且随后把其中最有趣的东西移到其它平台。像在[Johnson 78a]中描述的那样,我们发现传播Unix工具的最困难的问题,并不是C语言和新硬件的接口,而在于适应其它操作系统上的现有软件。因此Steve Johnson开始了pcc——一个C编译器的工作,目的是容易移植到新机器上[Johnson 78b],在他,Thompson和我开始把Unix系统移到Interdata 8/32计算机的时候。

在这时期,语言的变化——特别是1977年左右,主要集中在可移植性和类型安全的考虑上,努力处理在移植大量的代码到新的Interdate平台时我们预见和观察到的问题。那时的C依旧强烈地呈现出无类型根源的特征。例如在早期语言手册或存在的代码中,指针与整型内存索引几乎没有区别;字符指针和无符号整数的算术特性的相似性,使得很难抵御将它们看成同一个东西的诱惑。添加unsigned类型使得无符号算术能够不混淆于指针运算。类似地,早期语言容忍整数与指针间的赋值,但这种作法开始变得不受鼓励;一个类型转换符号(在Algol 68的例子中被称作“casts”)被发明,以更明显指示类型转换。受PL/I例子的诱惑,早期C没有将结构体指针强烈绑定到它们所指向的结构体,而是允许程序员用pointer->member但几乎不用考虑指针的类型;这样的表达式不经大脑地用作被指针指派的内存区域的引用,而成员名字仅表示一个偏移量和类型。

尽管K&R第一版描述了把C的类型结构用于其呈现形式的大部分规则,许多程序的编写(使用了)遗留的旧式、更松散风格,而且编译器还得容忍它们。为了鼓励人们更加关注于正式的语言规则、检测合法但可疑的写法、并帮助发现分离编译中简单机制难以检测的接口的错误匹配,Steve Johnson修改其pcc而产生了lint[Johnson 79b],用于扫描一系列文件并标记可疑的写法。

在使用中成长

我们在Interdata 8/32的可移植性试验的成功,很快导致了Tom London和John Reiser在DEC VAX 11/780上的另一次成功。这种机器比Interdata变得更流行,并且Unix和C语言开始在AT&T公司内部和外面快速传播。尽管到1970年代中期,Unix已经被用于各种项目,包括贝尔系统的,以及我们公司外的一小群以研究为目的的机构、学院和政府机构,它的真正成长开始于达到可移植性之后。值得特别记住的是刚出现的、基于公司开发和研究团队的AT&T的计算机系统部门(推出的)System III和System V版本系统,和加州大学伯克利分校继承自Bell实验室研究组的BSD系列。
在1980年代,C语言的使用广泛传播,并且几乎每一种机器体系结构和操作系统都有编译器;特别是它变成一种个人计算机上流行的编程工具,包括对这些机器的商业软件制造商和对编程有兴趣的终端用户。在那十年开始时,几乎每一种编译器都是基于Johnson的pcc;直到1985年,才有了许多独立开发的编译器产品。

标准化

到1982年,很明显,C需要正式的标准化。最近似于标准的K&R第一版,不再反应实际使用中的语言;尤其是它没有提及void和enum类型。虽然它预见了(构成)结构体的新方式,只是该书发表后语言才支持对其赋值、将其传递给函数和从函数返回它、以及将成员名字与包含它们的结构体或union严格关联。尽管AT&T发布的编译器包含了这些变化,尽管非基于pcc的编译器的大多数供应商很快添加了它们,但仍没有一个完整、权威的语言描述。

K&R第一版在很多语言细节上也不够精确,而且对于pcc这个“参照编译器”来说,K&R日益显得不切实际;pcc甚至也没有忠实的体现被K&R描述的语言,更别说后续的扩展。最后,开始将C用于商业和政府合同项目意味着批准一个正式标准很重要。因此(在M. D. McIlroy的催促下),在1983年夏天,ANSI建立了接受CBEMA的指导的X3J11委员会,目标是制订一个C标准。X3J11在1989年末提出了一个他们的报告[ANSI 89],后来这个标准被ISO接受为ISO/IEC 9899-1990。

从最开始,X3J11委员会在语言扩展上采取了谨慎、保守的态度。我极其满意,他们严肃地对待他们的目标:“为C程序设计语言制订一个清晰、一致和无二义性的标准,规范C的通用、现行的定义,促进不同C语言环境下的用户程序的可移植性。”[ANSI 89] 委员会意识到,仅仅靠发布一个标准并不会改变这个世界。

X3J11仅给语言本身引入了一个但真正重要的改变:它使用从C++[Stroustrup 86]借鉴的语法,把形式参数的类型添加到函数的类型签名中。用以前的风格,外部函数是这样声明的:
double sin();
仅说明sin是一个函数,返回一个double(即双精度的浮点数)值。在新的风格中,这个更好的声明
double sin(double);
使参数类型明显因而鼓励更好的类型检查和适当的转换。即使这个添加,尽管它产生了明显更好的语言,也引起了困难。委员会有理由认为,虽然新风格更好,简单地取缔旧式风格的函数定义和声明不合适。这种必然的妥协自然产生了,尽管允许两种形式使语言复杂化,并且可移植软件的作者必须应付不符合标准的编译器。

X3J11也引入了一大堆较小的附加和修改,例如,类型限定词const和volatile,和稍有不同的类型提升规则。尽管如此,标准化过程没有改变语言的特征。特别是,该C标准没有试图正规化定义语言语义,因而在一些小细节上还可能存在争议;然而,它很好地说明了自最初描述以来(该语言)在使用中的改变,并且对于基于它的实现而言是足够精确的。

因此,C语言核心在标准化过程中几乎毫发无伤,并且该标准更多地表现为一个更好的、周到的法典而非一次新发明。许多重要的改变发生在语言的环境中:预处理器和库。预处理器处理宏替换,使用一些约定以区别于语言的其余部分,它与编译器的交互从未被很好描述,X3J11企图纠正这种情形。其结果明显好于K&R第一版中的解释;除了更全面,它提供了一些操作,如标记的串接,以前只能够在少数实现中可用。

X3J11非常正确地认识到一个完整和仔细的标准C库(standard C library)的描述与它所服务的语言本身(的描述)同等的重要。C语言本身没有提供输入-输出或任何其它与外界交互(的方式),而依赖于一套标准过程。出版K&R的时候,C基本上被视为Unix的系统编程语言;尽管我们提供了库例程例子,该库例程的目标是容易转换到其它操作系统,但来自Unix的底层支持却是默认的。因此,X3J11委员会花了大量时间来设计和归档了一套库例程,它们必须在所有符合标准的实现中可用。

按照标准过程的规则,X3J11委员会的当前活动限定于发布对已有标准的解释。然而,最初由Rex Jaeschke召集的叫NCEG(C数值扩展小组)的一个非正式组织,被正式接受为分组X3J11.1而且他们继续考虑对C的扩展。正如这个名字暗示的,许多可能的扩展打算使该语言更合适于数值的使用:例如,边界被动态检测的多维数组,加入处理IEEE算术的工具,以及使语言在具有向量或其它高级结构特征的机器上更有效。并非所有这些可能的扩展都是数值相关的;也包括一个结构字面值的符号。

继承者

C,甚至B,有一些直接的后代,尽管它们比不过Pascal的繁殖能力。有一个分支发展得很早。当1972年Steve Johnson在休假期间访问滑铁卢大学(University of Waterloo,加拿大)时,他携带了B。它在那儿的Honeywell机器上变得流行,而且产下了Eh和Zed(对“B之后是什么?”的加拿大答案)。当Johnson在1973年返回贝尔实验室时,他莫名其妙地发现,他带到加拿大的种子,该语言已经发展回到(自己的)老巢;甚至他自己的yacc程序已经由Alan Snyder用C重写了。

C的更新的后代可能包括Concurrent C(并发C) [Gehani 89]、Objective C [Cox 86]、C* [Thinking 90],尤其是C++ [Stroustrup 86]。本语言也被广泛用作各种编译器的中间表示(基本上,用作可移植汇编语言),既用于如C++这样的直接后代,也用于Modula 3[Nelson 91]和Eiffel[Meyer 88]的这样的无关的语言。

批评

对比同类语言,最具C语言特色的两个思想是:数组和指针的关系,和声明语法以何种方式模拟表达式语法。它们也是最频繁受到抨击的特性之一,也常常成为初学者的绊脚石。对此两者,历史因素和错误加剧了它们的困难。最为重要的原因是C编译器对类型上的错误的容忍。从上述历史应该清楚,C由无类型语言进化而来。它不是作为拥有自己规则的全新的语言突然出现在其最初的用户和开发者面前;实际上,在语言发展中,我们不得不不断地改编已有的程序,并且要照顾已有代码。(后来,ANSI X3J11委员会在标准化C时面临同样的问题。)

1977年甚至之后的编译器,没有对一些用法给出警告:在整数和指针间的赋值、使用错误类型的对象来引用结构体的成员。尽管在K&R第一版中给出的语言定义,在类型规则的处理上是相当(尽管不完全)自洽,但那本书允许现有的编译器不实施这些规则。此外,某些规则用于简化早期的转换而导致后期的混乱。例如,函数声明中的空方括号
int f(a) int a[]; { … }
就是一个活化石,是声明指针的NB方式的遗迹;在C中,a仅在这个特殊的情况下才被被解释为一个指针。该表示法之所以残留,部分源于兼容性,部分是屈从这样的理论——它允许程序员告诉其读者这一意图,传递给f的指针产生于数组而不是引用简单的整型。不幸地是,它对学习者的迷惑与对读者的提示一样多。
在K&R C中,为函数调用提供正确类型的参数是程序员的责任,而已有的编译器不检查类型一致性。原始语言在函数的类型签名中缺少参数类型,是一个重大缺陷,确实是一个需要X3J11委员会以最果敢和最痛苦的精神去修复(的玩意)。早期的设计说明(如果不是证明)了我对技术问题的逃避——特别是分离编译的源文件之间的交叉检查,和我对从无类型到类型语言迁移的含义的不透彻地把握。lint程序,前面提及过,尝试缓解这个问题:作为lint的功能之一,通过扫描一系列源文件、比较调用时和定义时的函数参数类型,lint检查整个程序的一致性和完备性。这种用于表达式的风格沿用于声明语句,所以这些名字可以声明为
int *fp();
int (*pf)();
在更为装逼但仍现实的例子中,事情变得更糟糕:
int *(*pfp)();
是一个函数的指针,函数的返回值是一个整型指针(a pointer to a function returning a pointer to an integer)。有两个因素在起作用。最重要的(因素),C有相对丰富的方式(比如说,跟Pascal比较)进行类型描述。在和C一样的表达型——如Algol 68——语言中,声明语句描述对象同样难以理解,仅仅因为对象它们本身很复杂。第二个作用归因于语法细节。C的声明应该用一个”从内到外(inside-out)”的风格来阅读,该风格被很多人认为难以掌握[Anderson 80]。Sethi[Sethi 81]发现,许多嵌套的声明和表达式会较为简明,如果间接【indirection ,即*】运算符被当作后缀而非前缀时,但是到了(他指出的)那时,再做改变已经太晚了。【君生我未生,我生君已老 君恨我生迟,我恨君生早】[我生君未生,君生我已老。我离君天涯,君隔我海角]

尽管它有些困难,我认为C的声明方式仍然是合理的,我也对此满意;这是一个有用的一致性原则。

C的另一个特征,对数组的处理,尽管它确实很棒但是从实践的角度看,它【较第一点】更令人疑惑。虽然指针和数组间的关系不是通常(形式),(程序员)还是能学会的。而且,该语言在描述一些重要概念方面显示了强大能力,例如,仅仅使用几个基本规则和惯例,(描述了)在运行时长度可变的向量。特别是将字符串按照所有其他数组相同的机制处理,加上一个约定——空字符结束一个字符串。将C的方式与两个同时期的语言Algol68和Pascal[Jensen 74]比较,会很有趣。Algol 68中数组要么有固定边界,要么是“柔性的(flexible)”:对于语言定义和编译器都需要复杂的机制(支持)以适应柔性数组(而且并非所有编译器都完全实现了它们)。原始的Pascal只有固定尺寸的数组和串,这已被证明是有局限的[Kernighan 81]。后来它被部分修复(this was partially fixed),但是最终的语言没有广泛运用。

C把串视为字符的数组,并约定以一个标记结束。除了用字符串文字进行初始化有一个特殊规则,字符串的语义完全包含在管理所有数组的一般性规则中,因此该语言比起其他将字符串当作独立数据类型的语言来,更易于描述和翻译。这一方式会增加一下成本:某些字符串操作比在其它设计中开销更大,因为应用代码或库例程必须时不时地查找串尾、因为没有可用的内置运算,也因为字符串的存储管理的责任压在了用户肩上。不管怎么说,C的操作符方式工作良好。

另一方面,C对数组的处理,总体上(不仅仅是字符串)对优化和未来的扩展有不幸的影响。C程序中指针的盛行,不论是显式声明的还是由数组产生的,意味着优化必须很小心,而且必须谨慎地使用数据流技巧以达到满意的结果。高端的编译器能够懂得大多数可能改变的指针,但是一些重要的用法依然难以分析。例如以产生于数组的指针为参数的函数很难编译生成向量机上有效的代码,因为几乎不可能检测另一个参数指针与另一个参数是否有重迭的数据,或者外部可访问。更重要的是,C的定义非常明确地描述了数组的语义,因而将数组作为更原始对象处理、允许将它们作为整体来操作[这样的]改变和扩展,很难适合现在的语言。甚至声明和使用尺寸动态确定的多维数组的扩展,也曲折难行[MacDonald 89] [Ritchie 90],虽然它们能够使得用C编写数值例程库[变得]更为容易。因而,C通过一个一致而简单的机制,覆盖了实践中需要的字符串和数组的最重要的用法,但是将高效率实现和扩展的问题留了下来。
除了上面讨论的,当然,语言及其描述中还存在许多较小的瑕疵。也有一些不纠缠于细节的一般性批评被提出。主要是语言和它要求的基本环境基本上没有为编写大型系统提供帮助。命名结构仅提供了两个主要的级别:“external”(到处可见)和“internal”(在单个过程内)。可见性的一个中间级别(在单个文件内的数据和过程)与语言定义关系很微弱。因而,没有对模块化的直接支持,项目设计师将不得不使用自己的约定。
类似,C本身两种存储生命期:“automatic”对象,当控制流程存在或低于过程时存在,“static”存在于程序的全部执行期。Off-stack,动态分配存储由一个库例程提供,而管理它们的责任落在程序员身上:C对自动垃圾回收不感冒

成功原因?

C的成功远远超出了早期的任何预期。哪些品质促进它得到广泛使用呢?
毫无疑问,Unix本身的成功无疑是最重要的因素;它让这个语言可以被成千上万的人使用。另一方面,C在Unix中的使用,和它的到品种繁多的机器上的可移植性,对[Unix]系统的成功非常重要。而语言对其他环境的侵占揭示了更重要的价值。
尽管对初学者甚至偶尔对老手而言,某些方面很难解,C不失为一个简单和小巧的语言,可被简单和小的编译器翻译。它的类型和操作充分依据真实机器所提供[的对应物],人们用它理解机器如何工作,学会编写时间和空间高效的程序也不困难。同时,语言充分抽象于机器细节,程序可移植性也可以达到。
同样重要的是,C和它的主要库支持,总是保证能存在于一个真实环境中。它不是被设计用来孤立验证某一点,或作为一个例子,而是作为一个用来写有用程序的工具;它总是意味着同一个大型操作系统交互,并作为工具来创建更大工具。这一吝啬而务实的途径 影响了C所包含的东西:它覆盖了多数程序员的基本需要,但不试图提供太多东西。

最后,从它的首次发布的、确实非正式和不完整的描述起,不论它经受何种变化,真实的C语言,正如数百万使用各种不同编译器的用户所见证的,比起那些同样流行的语言如Pascal和Fortra,它保持着显著的稳定性和一致性。虽然存在许多不同的C的方言——最显著的,较早的K&R和较新的标准C所描述的——但总体上,C比其它语言保持着更自由的属性扩展。或许最重要的扩展,是用于处理某些Intel处理器的特性的“far”和“near”指针限定符。尽管C原始设计中没有将可移植性作为一个主要目标,它在编写程序,也包含操作系统上取得了成功,从最小的个人计算机到最大的超级计算机。

C诡异离奇、缺陷重重,却获得了巨大的成功。历史的机缘确有帮助,它显然满足了对足够有效以取代汇编的系统实现语言的需要,也足够抽象和流畅地描述算法,以及同各种各样的环境交互。

致谢(略)
参考
[ANSI 89]American National Standards Institute, American National Standard for Information Systems—Programming Language C, X3.159-1989.
[Anderson 80]B. Anderson, `Type syntax in the language C: an object lesson in syntactic innovation,’ SIGPLAN Notices 15 (3), March, 1980, pp. 21-27.
[Bell 72]J. R. Bell, `Threaded Code,’ C. ACM 16 (6), pp. 370-372.
[Canaday 69]R. H. Canaday and D. M. Ritchie, `Bell Laboratories BCPL,’ AT&T Bell Laboratories internal memorandum, May, 1969.
[Corbato 62]F. J. Corbato, M. Merwin-Dagget, R. C. Daley, `An Experimental Time-sharing System,’ AFIPS Conf. Proc. SJCC, 1962, pp. 335-344.
[Cox 86]B. J. Cox and A. J. Novobilski, Object-Oriented Programming: An Evolutionary Approach, Addison-Wesley: Reading, Mass., 1986. Second edition, 1991.
[Gehani 89]N. H. Gehani and W. D. Roome, Concurrent C, Silicon Press: Summit, NJ, 1989.
[Jensen 74]K. Jensen and N. Wirth, Pascal User Manual and Report, Springer-Verlag: New York, Heidelberg, Berlin. Second Edition, 1974.
[Johnson 73]S. C. Johnson and B. W. Kernighan, `The Programming Language B,’ Comp. Sci. Tech. Report #8, AT&T Bell Laboratories (January 1973).
[Johnson 78a]S. C. Johnson and D. M. Ritchie, `Portability of C Programs and the UNIX System,’ Bell Sys. Tech. J. 57 (6) (part 2), July-Aug, 1978.
[Johnson 78b]S. C. Johnson, `A Portable Compiler: Theory and Practice,’ Proc. 5th ACM POPL Symposium (January 1978).
[Johnson 79a]S. C. Johnson, `Yet another compiler-compiler,’ in Unix Programmer’s Manual, Seventh Edition, Vol. 2A, M. D. McIlroy and B. W. Kernighan, eds. AT&T Bell Laboratories: Murray Hill, NJ, 1979.
[Johnson 79b]S. C. Johnson, `Lint, a Program Checker,’ in Unix Programmer’s Manual, Seventh Edition, Vol. 2B, M. D. McIlroy and B. W. Kernighan, eds. AT&T Bell Laboratories: Murray Hill, NJ, 1979.
[Kernighan 78]B. W. Kernighan and D. M. Ritchie, The C Programming Language, Prentice-Hall: Englewood Cliffs, NJ, 1978. Second edition, 1988.
[Kernighan 81]B. W. Kernighan, `Why Pascal is not my favorite programming language,’ Comp. Sci. Tech. Rep. #100, AT&T Bell Laboratories, 1981.
[Lesk 73]M. E. Lesk, `A Portable I/O Package,’ AT&T Bell Laboratories internal memorandum ca. 1973.
[MacDonald 89]T. MacDonald, `Arrays of variable length,’ J. C Lang. Trans 1 (3), Dec. 1989, pp. 215-233.
[McClure 65]R. M. McClure, `TMG—A Syntax Directed Compiler,’ Proc. 20th ACM National Conf. (1965), pp. 262-274.
[McIlroy 60]M. D. McIlroy, `Macro Instruction Extensions of Compiler Languages,’ C. ACM 3 (4), pp. 214-220.
[McIlroy 79]M. D. McIlroy and B. W. Kernighan, eds, Unix Programmer’s Manual, Seventh Edition, Vol. I, AT&T Bell Laboratories: Murray Hill, NJ, 1979.
[Meyer 88]B. Meyer, Object-oriented Software Construction, Prentice-Hall: Englewood Cliffs, NJ, 1988.
[Nelson 91]G. Nelson, Systems Programming with Modula-3, Prentice-Hall: Englewood Cliffs, NJ, 1991.
[Organick 75]E. I. Organick, The Multics System: An Examination of its Structure, MIT Press: Cambridge, Mass., 1975.
[Richards 67]M. Richards, `The BCPL Reference Manual,’ MIT Project MAC Memorandum M-352, July 1967.
[Richards 79]M. Richards and C. Whitbey-Strevens, BCPL: The Language and its Compiler, Cambridge Univ. Press: Cambridge, 1979.
[Ritchie 78]D. M. Ritchie, `UNIX: A Retrospective,’ Bell Sys. Tech. J. 57 (6) (part 2), July-Aug, 1978.
[Ritchie 84]D. M. Ritchie, `The Evolution of the UNIX Time-sharing System,’ AT&T Bell Labs. Tech. J. 63 (8) (part 2), Oct. 1984.
[Ritchie 90]D. M. Ritchie, `Variable-size arrays in C,’ J. C Lang. Trans. 2 (2), Sept. 1990, pp. 81-86.
[Sethi 81]R. Sethi, `Uniform syntax for type expressions and declarators,’ Softw. Prac. and Exp. 11 (6), June 1981, pp. 623-628.
[Snyder 74]A. Snyder, A Portable Compiler for the Language C, MIT: Cambridge, Mass., 1974.
[Stoy 72]J. E. Stoy and C. Strachey, `OS6—An experimental operating system for a small computer. Part I: General principles and structure,’ Comp J. 15, (Aug. 1972), pp. 117-124.
[Stroustrup 86]B. Stroustrup, The C++ Programming Language, Addison-Wesley: Reading, Mass., 1986. Second edition, 1991.
[Thacker 79]C. P. Thacker, E. M. McCreight, B. W. Lampson, R. F. Sproull, D. R. Boggs, `Alto: A Personal Computer,’ in Computer Structures: Principles and Examples, D. Sieworek, C. G. Bell, A. Newell, McGraw-Hill: New York, 1982.
[Thinking 90]C* Programming Guide, Thinking Machines Corp.: Cambridge Mass., 1990.
[Thompson 69]K. Thompson, `Bon—an Interactive Language,’ undated AT&T Bell Laboratories internal memorandum (ca. 1969).
[Wijngaarden 75]A. van Wijngaarden, B. J. Mailloux, J. E. Peck, C. H. Koster, M. Sintzoff, C. Lindsey, L. G. Meertens, R. G. Fisker, `Revised report on the algorithmic language Algol 68,’ Acta Informatica 5, pp. 1-236.
Copyright ? 2003 Lucent Technologies Inc. All rights reserved.

余下全文(1/3)
分享这篇文章:

请关注我们:

发表评论

电子邮件地址不会被公开。 必填项已用*标注