有些成了总结工程教训的宝库,C语言失败的项目

尽管听起来很荒谬,但 C 语言是从一个失败的项目中诞生的。1969 年,通用电气、麻省理工学院和贝尔实验室共同创立了一个庞大的项目,即 Multics 项目。该项目的目的是创建一个操作系统,但它显然遇到了麻烦:它不仅没有提供它承诺的快速简便的在线系统,甚至没有产生任何有用的东西。尽管开发团队最终设法让 Multics 正常工作,但他们仍然陷入困境,就像 IBM 在 OS/360 上所做的那样。他们正在尝试构建一个非常大的操作系统,可以应用于非常小的硬件系统。Multics成为工程课程的宝库,但也为C体现“小即美”铺平了道路。

当幻想破灭的贝尔实验室专家退出 Multics 项目时,他们开始寻找其他任务。其中一位研究人员 Ken Thompson 对另一种操作系统很感兴趣,他多次向贝尔管理层提出,但都被拒绝了。在等待官方批准期间,汤普森和他的同事丹尼斯·里奇将汤普森的“太空旅行”软件移植到不太常用的 PDP-7 系统上,以此自娱自乐。太空旅行软件模拟太阳系的主要恒星,将它们显示在图形屏幕上,并创建可以在各个行星上飞行和着陆的航天飞机。与此同时,汤普森开始为 PDP-7 编写一个简单的新操作系统。它比 Multics 更简单、更轻量。整个系统是用汇编语言编写的。布赖恩·克尼汉,他在 1970 年将其命名为 UNIX,自嘲地总结了 Multics 中不应该学习的教训。图 1-1 描述了早期 C、UNIX 和相关硬件系统之间的关系。

哪个先出现,C 还是 UNIX?当涉及到这一点时,很容易陷入先有鸡还是先有蛋的陷阱。准确地说,UNIX 出现的时间比 C 语言更早(这就是为什么 UNIX 的系统时间是从 1970 年 1 月 1 日,也就是它的生成时间开始计算的秒数)。然而,我们在这里谈论的不是家禽轶事,而是编程故事。用汇编语言编写UNIX笨拙,在编程数据结构上浪费大量时间,而且系统难以调试和理解。Thompson 想利用高级语言的一些优点,但又不想像 PL/I [1] 那样低效,也不想遇到他在 Multics 中遇到的复杂问题。在对 Fortran 进行了短暂但不成功的尝试后,Thompson 创建了 B 语言,这简化了研究语言 BCPL[2],因此 B 的解释器可以驻留在 PDP-7 中,只有 8KB 的内存。B 语言从未真正成功过,因为硬件系统的内存限制,它只允许放置解释器,而不是编译器,由此产生的低效率阻碍了 B 语言用于 UNIX 本身的系统编程。

编译器设计师的黄金法则:效率(几乎)就是一切

在编译器中,效率几乎就是一切。当然还有其他事情需要关心,比如有意义的错误消息、良好的文档和产品支持。但与用户要求的速度相比,这些因素相形见绌。编译器的效率包括两个方面:运行效率(代码运行的速度)和编译效率(生成可执行代码的速度)。除了一些发展和学习环境,运营效率起着决定性的作用。

有许多编译优化可以增加编译时间但减少运行时间。还有一些优化(例如删除死代码和忽略运行时检查等)可以减少编译时间和运行时间,同时也减少内存使用。这些优化的缺点是可能无法发现程序中无效的运行结果。转译代码时优化本身非常小心,但如果程序员编写无效代码(例如:跨数组边界引用对象,因为他们“知道”附近有他们需要的变量)可能会导致错误的结果。

这就是为什么效率几乎是一切,但不是绝对的。如果您得到的结果不正确,那么提高效率有什么意义?编译器设计者通常会提供一些编译器选项。这样,每个程序员都可以选择自己想要的优化措施。B 语言没有成功,但 Dennis Ritchie 创造的注重效率的“New B”却成功了,充分证明了编译器设计者的黄金法则。

B 语言通过省略一些特性(如嵌套过程和一些循环结构)来简化 BCPL 语言,并提倡“引用数组元素等同于引用指针加偏移量”的思想。B 语言还保持了 BCPL 语言的无类型特性,其唯一的操作数是机器的字。Thomposon 发明了 ++ 和 — 运算符并将它们添加到 PDP-7 的 B 编译器中。它们在C语言中仍然存在,很多人天真地认为这是由于PDP-11对应的自动递增/递减地址模型,这是错误的!自动增减机构的出现早于 PDP-11 硬件系统的出现。尽管在 C 中,复制字符串中的字符的语句:

*p++ = *s++;

可以非常高效地编译为 PDP-11 代码:

moveb (r0)+, (r1)+

这让很多人误以为前者的句型是故意按照后者设计的。

当开发平台在 1970 年迁移到 PDP-11 时,无类型语言很快就过时了。该处理器具有对 B 语言无法表达的几种不同长度的数据类型的硬件支持。效率也是一个问题,这也迫使 Thompson 在 PDP-11 上重新实现 UNIX。Dennis Ritchie 利用 PDP-11 的强大功能创建了能够同时处理多种数据类型和效率的“New B”(名称很快变成“C”)语言,使用编译模式而不是解释模式,并引入了类型系统,每个变量必须在使用前声明。

早期的C语言经验

添加类型系统的主要目的是帮助编译器设计人员区分较新的 PDP-11 机器拥有的不同数据类型,例如单精度浮点数、双精度浮点数和字符。这与 Pascal 等其他一些语言形成鲜明对比。在 Pascal 中,类型系统的目的是保护程序员免于对数据进行无效操作。由于设计理念不同,C语言拒绝强类型化,它允许程序员在需要的时候在不同类型的对象之间进行赋值。添加类型系统可以说是事后才想到的,并且从未在可用性方面进行过认真评估和严格测试。直到今天,很多 C 程序员仍然认为“强类型”

除了类型系统之外,C 语言的许多其他特性都是为了方便编译器设计者而创建的(为什么不呢?C 语言最初几年的主要客户是那些编译器设计者)。根据编译器设计者的想法开发的语言特性有:

为了方便 C 编译器设计者,还构建了许多其他语言功能。这本身并不是一件坏事,它极大地简化了 C 语言本身,并且通过避免一些复杂的语言元素(如 Ada 中的泛型和任务、PL/I 中的字符串处理、C++ 中的模板和多重继承),C 语言是更容易学习和实施c语言是系统软件么,而且效率很高。

与大多数其他语言不同,C 有一个漫长的进化过程。在形成现在的形式之前,它经历了许多中间状态。多年来,它已经从一种实用程序发展成为一种久经考验的语言。第一个 C 编译器出现在 1970 年左右,也就是 20 多年前 [3]。时光荏苒,以UNIX系统为基础的应用越来越广泛,C语言也蓬勃发展。它强调由硬件直接支持的低级操作带来了极大的效率和可移植性,这反过来又帮助 UNIX 取得了巨大的成功。

K&R C

到 1970 年代中期,C 非常接近我们今天所知道和喜爱的形式。更多的改进仍然存在,但大多只是微小的变化(比如允许函数返回结构值)和一些扩展基本类型以适应新硬件变化的改进。(例如添加关键字 unsigned 和 long)。1978 年,Steve Johnson 编写了 pcc,一个可移植的 C 编译器。它的源代码对贝尔实验室外部开放,并被广泛移植,形成了整整一代 C 编译器的基础。C语言的演变如图1-2所示。

图 1-2 后期 C

软件信条

一个不寻常的错误

C 语言继承了 Algol-68 的一个特性,即复合赋值运算符。它允许重复的操作数只被写入一次而不是两次,给代码生成器一个提示,即操作数寻址可以同样紧凑。这方面的一个例子是使用 b+=3 作为 b=b+3 的缩写。原来复合赋值运算符的写法是先写赋值运算符,再写运算符,如:b=+3。B 词法分析器中有一个技巧,它使得实现 =op 形式比实现当前使用的 op= 形式更简单。但是这种形式会引起混乱,很容易把

b=-3;/* b 减去 3 */

b= -3;/* 将 -3 赋值给 b */

迷惑。

因此,此功能已修改为当前使用的形式。作为修改的一部分,还修改了代码格式化程序 indent 以确定复合赋值运算符的过时形式,并交换两者的位置以将其转换为相应的标准形式。这是一个非常糟糕的决定,格式化程序不应修改程序中除空白之外的任何内容。令人不快的是,这种方法引入了一个错误,如果它出现在赋值运算符之后,几乎任何东西(只要它不是变量)都会与赋值运算符交换位置。

如果幸运的话,这个错误可能会导致语法错误,例如:

ε=.0001;

将兑换为:

ε.=0001;

该语句将使编译器失败,您将立即发现错误。但源语句也可能如下所示:

阀门=!打开;/*valve 设置为 open 的逻辑逆*/

将被默默地交换为:

阀门!=打开;/*valve和open是不等比较*/

这条语句也可以编译,但它的作用与源语句有很大不同,它不会改变valve的值。

在后一种情况下,错误会潜伏并且不会立即被检测到。在赋值后放一个空格是很自然的,因此随着复合赋值的过时形式变得越来越少,人们忘记了缩进程序曾经被用来“改进”这种过时的形式。这个由 indent 程序引起的 bug 直到 1980 年代中期才从各种 C 编译器中消失。这是应该坚决拒绝的事情!

1978年,经典的C语言经典《The C Programming Language》出版。这本书广受好评,其作者Brian Kernighan 和Dennis Ritchie 也因此而闻名,因此这个版本的C 语言被称为“K&R C”。出版商最初估计这本书将售出约 1,000 册。到 1994 年,该书已售出约 150 万册(见图 1-3)。C 成为过去 20 年中最成功的编程语言之一,可能是最成功的。但随着 C 的广泛传播该语言的流行导致许多尝试从 C 生成其他变体。

图 1-3。像猫王一样,C 无处不在

本段摘自《C专家编程》

UNIX系统

既然 C 语言诞生并流行于 UNIX 系统,那么我们先从 UNIX 系统开始(注意:我们提到的 UNIX 还包括其他系统,例如 FreeBSD,它是 UNIX 的一个分支,但出于法律原因不使用名称)。

1. 在 UNIX 系统上编辑

UNIX C 没有自己的编辑器,但可以使用通用 UNIX 编辑器,例如 emacs、jove、vi 或 X Window System 文本编辑器。

作为程序员,您有责任输入正确的程序并为存储该程序的文件提供适当的文件名。如前所述,文件名应以 .c 结尾。请注意,UNIX 区分大小写。所以budget.c、BUDGET.c 和Budget.c 是3 个不同但都是有效的C 源文件名。但是 BUDGET.C 是一个无效的文件名,因为扩展名使用大写 C 而不是小写 c。

假设我们在 vi 编译器中编写了以下程序,并将其存储在 inform.c 文件中:

#include 
int main(void)
{
     printf("A .c is used to end a C program filename.\n");
     return 0;
}

以上文字为源代码,inform.c为源文件。请注意,源文件是整个编译过程的开始,而不是结束。

2. 在 UNIX 系统上编译

虽然程序对我们来说看起来完美无瑕,但对计算机来说却是一堆乱码。计算机不明白#include 和 printf 是什么(也许你现在不明白,但你以后会知道,计算机不明白)。如前所述,我们需要一个编译器将我们编写的代码(源代码)翻译成计算机可以理解的代码(机器代码)。生成的可执行文件包含计算机完成其工作所需的所有机器代码。

以前,UNIX C 编译器调用语言定义的 cc 命令。然而,它并没有跟上标准发展的步伐,退出了历史舞台。但是,UNIX 系统提供的 C 编译器通常来自其他来源,然后使用 cc 命令作为编译器的别名。因此,尽管在不同的系统上调用了不同的编译器,但用户可以继续使用相同的命令。

要编译 inform.c,请输入以下命令:

cc inform.c

几秒钟后,将返回一个 UNIX 提示,告诉用户任务已完成。如果程序编写不正确,您可能会看到警告或错误消息,但我们假设程序编写正确(如果编译器报告 void 错误,则您的系统尚未更新为 ANSI C 编译器c语言是系统软件么,只需删除 void 即可) . 如果你使用 ls 命令列出文件,你会发现一个 a.out 文件(见图 1.5)。这个文件是一个包含翻译(或编译)程序的可执行文件。运行该文件,只需键入:

a.out

输出如下:

A .c is used to end a C program filename.

图1.5 用UNIX准备一个C程序

如果要存储可执行文件 (a.out),则应重命名它。否则,该文件将被下次编译程序时生成的新 a.out 文件替换。

如何处理目标代码?C 编译器创建一个目标代码文件,其基本名称与源代码相同,但扩展名为 .o。在本例中,目标代码文件是 inform.o。但是,找不到此文件,因为一旦链接器生成了完整的可执行文件,它就会被删除。如果原始程序有多个源代码文件,则保留目标代码文件。当您稍后了解多文件程序时,您会看到这样做的好处。

Linux系统

Linux 是一种开源、流行的类 UNIX 操作系统,可在不同平台(包括 PC 和 Mac)上运行。在 Linux 中编写 C 程序与在 UNIX 系统中几乎相同,只是使用了 GNU 提供的 GCC 公有域 C 编译器。编译命令如下所示:

gcc inform.c

注意安装Linux时,可以选择是否安装GC​​C。如果之前没有安装 GCC,则必须安装它。通常,安装过程会使用 cc 作为 gcc 的别名,因此可以在命令行中使用 cc 代替 gcc。

本段摘自《C Primer Plus(第6版)中文版》

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论