高级语言与低级语言
使用机器码编写程序就如同牙签吃东西,伸出手臂使出较大的力气刺向食物,但每次都只获取到小小的一块,这个过程是辛苦且漫长的。同样的,每个机器码字节所能我完成的工作,是你能想象到的最微小且最简单的工作——从内存获取一个数,之后加在到处理器,再把它与另一个数相加,最终将运算结果保存到内存等——正因为如此,很难想象如何使用这些机器码构成一个完整的程序。
目前为止,至少对于在第22章讨论的原始模型阶段来说,我们已经取得了一定的进步,在那个阶段,我们使用过控制面板上的开关将二进制数据输入到存储器。在第22章中,介绍如何编写一段简单的程序,让我们可以利用键盘将十六进制机器码输入计算机,以及通过视频显示设备来检查这些代码。这种改进固然可去,但仍不是我们的终极目标。
汇编语言
前面的章节介绍过,可以使用某些较短的助记符来关联机器码字节,这些助记符包括MOV,ADD,CALL,HLT等,通过这些类似的英文的符号我们可以较方便地引用机器码。通过这些助记符的后面会跟着操作数,这可以进一步指明它所关联的机器码指令的功能。例如8080机器码字节46h,它的功能是令处理器将存储在内存特定地址的字节转移至寄存器B,而该地址由寄存器对HL中的16位数寻址.这个操作可以简单地写作:
MOV B, [HL]
显然,使用汇编语言编写程序要比使用机器语言简单得多,但微处理器并不能解释汇编语言。在前面得章节中我们已经学习了如何在纸上编写汇编程序,但只有当你确实准备在微处理器上运行汇编程序,才会手工对其进行汇编,这样就可以将汇编语言程序得语句转成了机器语言代码,并把它们输入内存。
当然,我们希望最好由计算机能独自完成语言转换得工作。如果你得8080计算机正在运行CP/M操作系统,而且你已经拥有了所有必须得工作,那就再好不好了,因为下面我们将介绍其工作原理。
第一步,建立一个文本文件,并将汇编语言程序输入到该文本文件中。这项工作可以使用CP/M的应用程序ED.COM来完成。该程序是一个可以用来创建,修改文本文件的编辑器。假设你把该文本文件命名为PROGRAM1.ASM,其中ASM是文件类型,用来指明该文本文件de内容是由汇编语言程序组成。这个文件的内容如下:
ORG 0100h
LXI DE, Text
MVI C, 9
CALL 5
RET
Text: DB 'Hello!$'
END
这个文件中有两条语句我们从未接触过。第一条语句是ORG(origin),它不与任何8080指令对应,其功能是用来指明下面语句的地址从0100h地址处开始。如前所述,该地址是CP/M将程序装入内存的起始地址。
第二条语句是LXI(Load Extended Immediate)指令,其功能是将一个16位数加载到寄存器DE。本例中,该16位数使用标记Text提供的。该标记在程序底端的附近,位于DB(Data Byte)语句之前。DB语句我们也是第一次遇到,其后可以跟着一些字节,这些字节以逗号分隔或者用单引号括起来(如本例)。
MVI(Move Immediate)语句将数值9转移到寄存器C。
CALL 5语句实现CP/M的函数调用功能。函数5的作用是:显示以寄存器对DE给出的地址为起始处的字符串,直到遇到$结束(可以看出,在程序的结尾处使用了美元符号“$”作为文本的结束标记,并把控制权交还给CP/M(实际上,这只是结束CP/M程序的方法之一)。
END语句用来指明汇编语言文件已经结束。
现在我们已经有了一个包含7行语句的文本文件,下一步要做的就是对其进行汇编,即将其转换成机器语言代码。以前这项工作是通过手工完成的,但现在我们的机器运行的是CP/M系统,可以利用CP/M中一个叫做ASM.COM的模块来完成这项工作。该模块是CP/M的汇编器(assembler)。可以在CP/M命令行中使用下面的语句运行ASM.COM文件:
ASM PROGRAM1.ASM
ASM对PROGRAM1.ASM文件进行汇编,产生一个名为PROGRAM1.COM的新文件,PROGRAM1.COM包含了与我们编写的汇编程序相对应的机器码(实际上,该过程还包含另一个步骤,但在该操作中并不很重要)。现在就可以使用CP/M的命令行来运行PROGRAM1.COM文件,程序运行的结果是显示字符串“Hello!”然后结束。
PROGRAM1.COM文件包含以下16个字节:
11 09 01 OE 09 CD 05 00 C9 48 65 6C 6C 6F 21 24
开始的3个字节是LXI指令,其后的两个字节是MVI指令,接下来的三个字节是CALL指令,紧随其后的一个字节是RET指令,最后的7个字节是ASCII码,包括5个字母“Hello”,感叹号“!”以及美元符号“$”.
像ASM.COm这样的汇编器所做的工作是:读取一个汇编文件(source-code, 通常称作源代码文件),将其转换得到一个包含机器码的文件——可执行(executable file)。从宏观的角度来看,汇编器是非常简单的,因为构成汇编语言的助记符和机器码之间是一一对应的。汇编器拥有一张包括所有可能助记符及其参数的表,它逐行读取汇编语言程序,把每一行都分解成为助记符和参数,然后把这些短小的单词和字符与表中的内容匹配。通过这种匹配的过程,每一个语句都会找到与其对应的机器码指令。
注意,汇编器如何直到LXI指令必须将寄存器DE的值设置为地址0109h(Text的地址)。如果LXI指令本身被存放在地址0100h处(CP/M将程序加载至内存开始运行时的起始地址),而0109h则是Text字符串的起始地址。一般来说,程序员在使用汇编器时有很多方便之处,其中一点就是不需要关心汇编程序各部分在内存中的存放地址。
第一个编写汇编器的人需要手工对程序汇编。如果要为机器写一个新的汇编器(或者对其修改),则可以使用汇编语言编写该程序,然后使用原有的汇编器对其汇编。一旦新的汇编器通过汇编,则它也就可以对自身进行汇编。
每当一种新的微处理器面世,就需要为其编写新的汇编器。然而,新的汇编器可以在已有的计算机上编写,并利用其汇编器进行汇编。这种方式成为交叉汇编(cross-assembler),即利用计算机A的汇编器对运行在计算机B上的程序汇编。
虽然汇编器的引入消除了汇编语言变化成中重复性的劳动部分(即手动汇编部分),但汇编语言仍然存在两个主要问题。第一个问题(也许你已经意识到了),使用汇编语言编程非常乏味,因为这是在微处理器芯片级的编程,因此不得不考虑每一个微小的细节。
汇编语言存在的第二个问题是不可“移植”(protable)。如果你为Intel 8080写了一个汇编语言程序,则该程序不能在Motorola 6800上运行,你必须在6800上重写一个相同功能的汇编语言程序。编写类似程序的过程也许没有编写第一个程序那么困难,因为你已经解决了程序组织和算法问题,但仍然还有很多工作要做。
高级程序设计语言
上一章介绍了现代微处理器集成浮点运算机器码的原理。不可否认,这已经为我们带来了很大的便利,但仍不能令人满意。一种更好的方式是:完全放弃那些实现每个基本操作的机器码指令,这些指令与处理器相关,因而导致程序缺乏移植性。我们采用的替代策略是使用一些经典的数学表达式来描述复杂的数学运算。下面是一个表达式的例子:
A x Sin(2xPI+B)/C
上式中A,B,C代表数字,而PI=3.14159.
这看起来不错,为什么不动手尝试以下呢?假设在某个文本文件中有这样一个表达式,那么我们可以尝试编写一个汇编语言程序来读取该文本文件,并将其中的数学表达式转换为机器码。
如果只需要计算一次该表达式,那么可以手工计算或借助计算器来完成。如果需要对A,B,C取不同的值多次计算该表达式,那么你可能要考虑使用计算机来完成这些计算。因此,代数表达式不会孤立地出现,必须考虑其前后的语句,这些语句使表达式对不同的值进行运算。
现在你所创建的东西已经触及所谓的高级程序设计语言(high-level programming language).我们一直在介绍的汇编语言称作低级语言(low-level programming language), 因为它与计算机硬件的关系相当紧密。尽管除了汇编语言以外的其他程序设计语言都可以称为“高级语言”,但它们之间还是有高低之分的。
人类语言通常都是经历了千百年复杂的互相影响,偶然演变以及不断吐故纳新才形成的,就算一些人工语言如世界语(Esperanto),也处处显露出与现实语言的渊源。但高级程序设计语言是经过深思熟虑的设计,更加概念化的语言。设计程序语言所面临的一大挑战就是:如何让语言更具吸引力。因为语言定义了人们向计算机传送指令的方式,只有更易用的方式才能让人们对语言产生兴趣。据1993年的一项估算,从1950年到1993年大约有1000多种高级程序设计语言被发明出来并被应用。
然而,仅仅定义(define)高级语言,包括定义语言的语法(syntax)来表达该语言可以描述的一切事物,还远远不够;我们还需要为其编写一个编译器(compiler),编译器可以将高级语言的程序语句转换为机器码指令。同汇编器类似,编译器也是逐字逐句地读取源文件并将其分解成短语,符号和数字的,而实现过程要比汇编器更加复杂。从某些方面来看,编译器相对简单,因为汇编语言的语句和机器码是一一对应的。而一般的高级语言却不具备这种对应关系,编译器通常必须把一条语句转换多个机器码指令。编译器的编写非常复杂,许多书都是用全部的篇幅来讲解如何设计和构造编译器。
当然,任何事物都具有两面性,高级语言也不例外,它有很多优势但也存在不少缺陷。高级语言最基本的有点在于它比汇编语言更易于学习而且更容易编写程序,用高级语言编写的程序通常更加清晰简明——与汇编语言不同,高级语言通常不依赖于特定的处理器,因此它们通常具有良好的可移植性。因为这种特点,使用高级语言的程序员不再需要关心最终运行程序的计算机的底层结构。当然,如果要在不同类型处理器上运行程序,则需要用处理器对应的编译器将程序转换成对应的机器码。因此,最后生成的可执行文件仍然只适用于特定的处理器。
另一方面,有一种普遍现象:一个优秀的汇编程序员所编写的程序比编译器所产生的代码更加有效率。也就是说 ,从高级语言程序生成的可执行程序比相同功能的汇编语言更大,并且运行速度更慢(但从近年的发展来看,这种差别已不再明显,因为微处理器变得更加复杂,而且编译器在优化代码方面也更加成熟)。
此外,虽然高级语言提高了处理器的易用性,但并没有让其变得更强大。微处理器的任何一个功能都可以通过汇编语言实现,因此汇编语言可以高度利用处理器的功能。因为高级语言必须转换成机器码,所有它只会降低微处理器的能力。事实上,如果某种高级语言具有真正意义的可移植性,那么它将不能使用某些处理器的特有功能。
例如,许多微处理器都有移位指令。如前所述,这些指令能将累加器中的字节的每一位向左或向右移动。但事实上,几乎没有哪一种高级语言包含这种操作。如果再程序中需要进行移位操作,则必须通常乘2或除2来模拟该过程(则并不是什么坏事:事实上,许多现代编译器都是利用处理器的移位指令来实现乘以2或除以2的幂的)。除此之外,许多高级语言也不包括按位逻辑运算。
在早期的家用计算机中,大部分应用程序都是用汇编语言写的,而现在除了一些特殊的应用场合之外,汇编语言已经很少使用了。而今处理器引入了一些新的硬件,可以实现流水线技术——同时有若干个指令码渐次执行——这使得汇编语言变得更加复杂且不易处理。与此同时,编译器却变得更加成熟,越来越多的程序开始使用高级语言来编写。现代计算机大容量的存储器也作为一个重要的角色,推动了这种趋势:程序员不再局限于编写运行在小内存和小磁盘上的程序。
早期的计算机设计者都曾尝试用数学符号来描述问题,但公认的第一个真正可以工作的编译器是A-0,它是为UNIVAC开发的编译器,于1952年由雷顿兰德公司(Remington-Rand)的格瑞斯·穆雷·霍泊(Grace Murray Hopper,1906-1992)开发完成。Hopper博士早期计算机研究工作始于1944年,那时她效力于霍华德·艾肯(Howard Aiken),主要研究Mark I。在她八十多岁的时候,仍然孜孜不倦地在计算机界工作,当时她在DEC(Digital Equipment Corporation)公司从事公关事务。
FORTRAN
FORTRAN语言是目前仍在使用的最古老的高级语言(虽然这些年来人们对其进行了大量修改)。你可能注意到了,很多计算机语言都是以大写字母命名的,这是因为它们的名字大都是由几个单词的首字母组成。FORTRAN这个名字来源于FORmula的前三个字母和TRANslation的前四个字母的组合,它由IBM在20世纪59年代中期开发,主要应用于704系列计算机。
自其发布的几十年来,FORTRAN一直被认为是科学和工程应用程序开发的首选语言。它广泛地支持浮点运算,甚至支持非常复杂的数的运算(即我们上一章讲到的由实数和虚数构成的复数)。
ALGOL
任何一种计算机程序设计语言都有其支持者和批评者,而且人们通常只对自己喜欢的语言有热情。本书尽量以一种客观的态度来讨论某种语言,这里选取了一种语言作为原型,通过它来解释那些几乎已经销声匿迹的程序设计概念。我们选择的是ALGOL(即ALGOrithmic的缩写,有趣的是,ALGOL也是仙女座第二亮的恒星的名字)。ALGOL作为过去40年中许多曾经流行一时的通用高级语言的直接鼻祖,也非常适合用来研究高级程序设计语言的本质,该语言可看做是一粒种子,它的成长最终形成了高级语言这棵大树。直到今天,人们仍然在使用“类ALGOL”程序设计语言的概念。
ALGOL语言的原版由某国际委员会在1957至1958年间设计,它被称作ALGOL 58.两年后,也就是在1960年,ALGOL 58的改进版ALGOL 60面世,其最终版本是ALGOL 68.本章所采用的版本在Revised Report on the Algorithmic language ALGOL 60说明文档中有具体描述,该文档于1962年完成并在1963年首次发行。
让我们开始写第一个ALGOL程序。假设我们所使用的操作系统平台是CP/M或MS-DOS,并且安装了一个名为ALGOL.COM的编译器。该程序是一个文本文件,命名为FIRST.ALG。注意,文件类型名是ALG。
ALGOL程序以begin开始,以end作为结尾,程序的主要内容被包括在这两个语句之间。下面的程序用来显示一行文本:
begin
print ('This is my fist ALGOL program!');
ende
通过在命令行运行ALGOL编译器对FIRST.ALG文件进行编译,其格式如下:
ALGOL FIRST.ALG
ALGOL编译器对这条命令很可能做出这种响应,在显示设备上给出以下提示信息:
Line 3: Unrecognized keyword 'ende'.
ALGOL编译器对拼写的检查非常严格,它在这一点上比传统的语文教师更甚。因为输入程序时,误把”end”拼写做”ende”,所以编译器通过提示信息告诉我们程序中有语法错误(syntax error)。当编译器检查到“ende”时,它期待能遇到一个可识别的关键字(keyword),但由于上述错误,编译不能通过。
将程序中的错误改正之后,可以再次执行编译命令。由于系统平台和编译器版本的不同,有时编译器会直接生成一个可执行文件(CP/M 平台下此文件名为FIRST.COM, MS-DOS平台下名为FIRST.EXE);有时还需要再执行一个步骤才可以完成。无论是哪种情况,最后你都可以再命令行执行FIRST程序:
FIRST
FIRST程序会对此响应,并显示以下内容:
This is my fist ALGOL program!
注意,这里还有一个拼写错误:first被误做fist!编译器没有检查出这个错误,因此它被称为运行时错误(run-time error)——程序被执行时才出现的错误。
很明显,我们的第一个ALGOL程序中,print语句的功能是把一些信息显示到屏幕上,本程序中是显示一行文本(从功能的角度来看,该程序与本章开始所给出的汇编程序是等价的)。ALGOL语言的正式规范中并不包括print语句,但我们假设所使用的特定ALGOL编译器包括这个便利的工具,它有时候也被称作内部函数(built-in function).除了begin和end之外的大部分ALGOL语句都要以分号结尾。你可能注意到了print语句使用了向右缩进的格式,这并不是必要的,其作用只是为了让程序的结构更加清晰。
假设现在要编写一个用于两个数相乘的程序。每一种程序设计语言都包括变量(variable)的概念。程序中的变量可以是一个字母,一个短的字母序列,也可以是一个单词,由程序员自己决定。变量名实际上对应内存的一个存储单元,但在程序中是通过名字来访问该存储单元的,而不是直接使用存储单元的地址值。下面的程序定义了三个变量,分别命名为a,b,c:
begin
real a, b, c;
a := 535.43;
b := 289.771;
c := a x b;
print ('The product of', a, 'and ', b, ' is ', c);
end
real语句称为声明(declaration)语句,用来指明程序中要定义的变量。在该程序中,变量a,b,c被定义为实数(real)类型或浮点数类型(同时,ALGOL语言也支持使用integer关键字来定义整数型变量)。程序设计语言中的变量名通常以字母开头,变量名也可以包括数字,但前提是第一个字符必须是字母。变量名必须是字母数字下划线,并且数字不能打头。通常编译器会规定变量名的最大长度,本章用到的变量一律以当个字母命名。
假如我们使用的特定ALGOL编译器支持IEEE浮点数标准,则本程序中所定义的三个变量每一个需要4个字节的存储空间(采用单精度格式)或8个字节的存储空间(采用双精度格式)。
声明语句后面的三个语句是赋值语句(assignment)。在ALGOL语言中,赋值语句很容易被识别,因为它的格式很固定,总是在冒号后面跟着一个等号(在大多数计算机语言中,赋值语句通常只包括等号)。赋值语句的冒号左边是一个变量,而等号右边是一个表达式,表达式的计算结果将被赋值给左边的变量。前两条赋值语句指明,变量a,b将分别被赋予一个特定的值;第三条赋值语句指明,将a和b的乘积赋值给变量c。
时至今日,我们所熟悉的乘法符号“x”已经不允许出现在程序设计语言中了,因为它没有被包括在ASCII和EBDCIC字符集中。大多数程序设计语言使用星号*
来替代它作为程序中的乘号标记。尽管ALGOL使用了普遍使用的斜杠/
作为除法标记,但在该语言仍然可以使用÷
标记整数除法。用来指明被除数与除数的倍数关系。ALGOL还使用了另一个非ASCII字符↑
,该箭头符号用来做乘方运算。
最后的print语句用来显示所有变量的值。它包含文本和变量,并以逗号分隔。print语句的主要工作并不是用来显示ASCII码值的,但本程序中却做了更多的工作:将浮点数也转换成ASCII码并显示:
The product of 535.43 and 289.771 is 155152.08653
接着会执行end语句,程序终止并将控制权交还给操作系统。
如果要将另外两个数相乘,则需要做以下工作:修改程序,改变变量的值,重新编译并重新运行程序,这将是一件非常繁琐的工作。为了避免这些重复的工作,我们可以借助另一个内部函数read。修改后的程序如下:
begin
real a, b, c;
print ('Enter the first number: ');
read (a);
print ('Enter the second number: ');
read (b);
c := a x b;
print ('The product of ', a, ' and ', b, ' is ', c);
end
read语句的功能是读取从键盘键入的ASCII码值,并将其转换成浮点数。
循环(loop)是高级语言的重要组成部分。循环使得程序可以对同一个变量得不同取值反复执行相同得操作。假设我们要写一段程序用来计算3,5,7,9各自得平方,可以这样编写程序:
begin
real a, b;
for a := 3, 5, 7, 9 do
begin
b := a x a x a;
print ('The cube of ', a, 'is ', b);
end
end
for语句将变量a的值第一次设为3,然后执行do关键字后面的语句。如果do后面要执行的语句不止一条(如本例),则必须将他们置于begin和end之间,这两个关键字定义了一个语句块(block)。第一次循环之后,for语句会一次为a赋值5,7,9并执行相同的语句块。
下面的程序中采用了for语句的另一种使用方式,这段程序用来计算3~99之间所有奇数的立方。
begin
real a, b;
for a := 3 step 2 until 99 do
begin
b := a x a x a;
print ('The cube of ', a, ' is ', b);
end
end
for语句将变量a初始化为3,并执行for后面的语句块。第一次循环结束后,变量a与step关键字后面的增量相加,这里是2.新得到的a的值是5,它将用于第二次执行语句块。变量a继续增加2并用于下一次循环,直到a的值超过99,这时for循环结束。
一般而言,程序设计语言对语法都有着非常严格的要求。在ALGOL 60中,就关键字for而言,其语法格式是:for的后面跟着一个变量名。而英语中的这种限制宽松的多,单词for的后面可以跟所有类型的单词,例如“for example”,“for can”等。尽管编译器是非常复杂的程序,但其所能解释的语言显然要比人类的语言简单得多。
大部分程序设计语言得另一个重要特征体现在条件(conditional)语句的使用。条件语句的特点是,只有当某个条件成立时才会执行另一条对应的语句。在下面的例子中,我们使用ALGOL的内部函数sqrt来计算一些数的平方根。sqrt函数的阐述不能是复苏,因此要在程序中通过条件测试避免这种情况。
```ALGOL
begin
real a, b;
print ('Enter a number: ');
read (a);
if a < 0 then
print ('Sorry, the number was negative.');
else
begin
b = sqrt(a);
print ('The square root of ', a, ' is ', b);
end end ``` 左尖括号(<)是小于号。如果程序的使用者输入的是一个小于0的数,if语句中的判断为真,因此第一个print语句将会被执行。反之,如果该数大于或等于0,则else关键字后面的语句块则会被执行。
本章目前所用到的变量都是一个变量对应一个值,我们也可以用一个变量对应多个值,数组(array)就是一个很好的选择。在ALGOL程序中可以这样声明一个数组:
real array a[1:100];
该语句定义一个数组变量a,它可以用来存放100个不同的浮点数,这些数被称作数组元素。可以使用数组名加标号的方式来引用数组元素,例如,第一个数组元素是a[1],第二个是a[2], 最后一个是a[100].方括号中的数字称作数组下标(index)。
下面的程序用来计算1~100所有数的平方根,将结果保存在一个数组中,然后再通过循环将这些结果显示出来。代码如下:
begin
real array a[1:100];
integer i;
for i := 1 step 1 until 100 do
a[i] := sqrt(i);
for i := 1 step 1 until 100 do
print ('The square root of ', i, ' is ', a[i]);
end
程序中还定义了一个整型变量i(由于它是integer的首字母,经常被程序员用作整型变量)。第一个for循环的执行过程中,每个数组元素被赋值为其下标的平方根;第二个for循环执行过程中,数组的每一个元素被显示出来。
变量的类型有很多,除了我们已经介绍过的实型和整型之外,变量还可以被声明为布尔型(Boolean,该名称是为了纪念第10章提到的乔治·布尔)。布尔变量的取值可能有两种,即true和false。在本章的最后将介绍一个用到布尔数组的例子(这个例子也将用到目前所介绍的大部分内容),来实现一个寻找素数的著名算法——爱拉托逊斯筛法(Sieve of Eratosthenes)。爱拉托逊斯(约公元前276-196年)传说是亚历山大图书馆的管理员,他因准确计算出地球的周长而永载史册。
素数是只能被1及其本身整出的一类整数。第一个素数是2(也是唯一的偶数素数),其他的素数还包括3,5,7,11,13,17,等等。
爱拉托逊斯方法以2开始的整数表开始,因为2是素数,因此所有可能被2整出的数都排除掉(即除了2之外的全部偶数)。接下来是3,因为3是素数,因此所有能被3整出的数也被排除掉。因为4在第一个步骤中已经被排除掉,所有下一个要考虑的数是5,即排除所有5的倍数。按这种方式不断循环,最后剩下的都是素数。
下面的ALGOL程序用来筛选2~10 000之间的所有素数,程序中定义了一个布尔数组,用来对所有的数进行标识。该程序如下:
begin
Boolean array a[2:10000];
integer i, j;
for i := 2 step 1 until 10000 do
a[i] := true;
for i := 2 step 1 until 100 do
if a[i] then
for j := 2 step 1 until 10000 / i do
a[i x j] := false;
for i := 2 step 1 until 10000 do
if a[i] then
print (i);
end
第一个for循环将数组a的每一个元素的初始值设置为布尔值true。这里的true表示该位置的数是素数,因此现在程序默认所有的数都是素数。第二个for循环的范围是1~100(100刚好是10000的平方根)。在第二个for循环中,如果判断条件成立,该数为素数,即a[i]为true,则第三个for循环则会把该数的所有小于或等于10000的倍数(除了其本身)设置为false,因为这些数都不是素数。最后的for循环用来输出所有的素数,这里的判断条件是:若a[i]为true,则i为素数。
程序设计的思考
程序设计到底是一门科学还是一门艺术呢?这的确是一个有趣的问题,一些人甚至还为此争论不休:一方面,你或许在大学里系统地学习了计算机科学(Computer Science)课程;另一方面,你又读过如唐纳德·克努斯(Donald Knuth)的名著《计算机编程艺术系列》(The Art of Computer Programming series)等著作。然而物理学家理查德·费叶曼(Richard Feynman)曾这样写道:“从某种程度上看计算机科学像是一种工程,它的工作范畴是利用一些事物去实现其他事物。”
在程序设计中有一种现象:如果让100个人来编写输出 素数的程序,你可能会得到100个不同的解决方法。就算所有的程序员都使用“爱拉托逊斯筛法”来解决这个问题,其最后所写的程序也不一定与本书所写程序完全相同。如果说程序设计是一门科学,那么就不应该出现如此多的解法,而不正确的方法将会非常明显。偶尔,一个程序设计问题会诱发出极富创造性的火花或洞若观火般的觉察力,这就是所谓的程序设计的“艺术”。但是,程序设计的更多的时候是设计和建造,就像修建一座大桥的过程。
程序设计语言的历史
早期的程序设计对编程人员的要求很高,所有很多早期的程序员都是科学家或工程师,他们通常利用FORTRAN或ALGOL中的数学算法来描述并解决各自领域的问题。回顾程序设计语言发展的整个历程时,我们会发现,人们一直努力开发一种能为更大范围的人群所使用的语言。
COBOL
第一个为商务系统设计的成功语言是COBOL(common business oriented language),今天仍被广泛使用。由美国工业和国防部组成的委员会于1959年早期推出了COBOL,它受到了Grace Hopper的早期编译程序的影响。从某种意义上说,COBOL使得管理人员——可能并不具体设计编码——至少可以看懂程序代码并且能够检查代码是否按所预定的去工作(在现实生活中,这种情况很少发生)。
COBOL广泛支持记录(record)和生成报表(report)。记录是按照统一方式归类整理的信息的集合,例如:保险公司可能要维持包含有它所卖的所有险种的一个大文件,每一险种为一单独记录,包括客户姓名、出生日期和其他信息。早期的许多COBOL程序设计成能处理存储在IBM穿孔卡片上的80列记录,为了尽可能少孔洞占用卡片空间,日期中的年份通常用2位编码而不是4位,这导致了随着2000年的到来而普遍出现的“千年虫”问题(millennium bug)。
PL/I
20世纪60年代中期,伴随着System/360项目的开发,IBM公司开发了名为PL/I的程序设计语言(I是罗马数字1,PL/I表示programminglanguagenumberone)。PL/I试图把ALGOL的块结构、FORTRAN的科学和数学计算功能以及COBOL的记录和报表能力结合起来。但是,它却远没有像FORTRAN和COBOL那样流行。
BASIC
尽管FORTRAN、ALGOL、COBOL和PL/I都有适用于家用计算机的版本,但它们对于小型计算机的影响远没有BASIC语言那么深远。
BASIC(beginner’s all-purpose symbolic instruction code)是Dartmouth数学系的John Kemeny和Thomas Kurtz在1964年为Dartmouth的分时系统开发的。Dartmouth的许多学生并非主修数学或工程课程,所以他们不应该在穿孔卡片和很难的程序设计语法上花费很多时间。Dartmouth的学生坐在终端前,只需在数字之后简单地敲入BASIC语句,即可建立BASIC程序。数字表明程序中语句的顺序。没有数字在前的语句是对系统的命令,如SAVE(存储BASIC程序到磁盘)、LIST(按顺序显示行)和RUN(编译和执行程序)。第一批印刷的BASIC指令手册中的第一个BASIC程序为:
10 LET X = (7 + 8) / 3
20 RTINT X
30 END
与ALGOL语言不同,BASIC不要求程序员指定变量的存储类型,究竟一个变量是保存为整型还是浮点型并不需要程序员担心,大部分数默认都是以浮点数格式存储的。
很多BASIC的后续版本都是解释型(interpreter)而不是编译型(compiler)。如前所述,编译器读取源文件并生成一个可执行文件;而解释器却采取了边读边执行的方式,不会产生新的文件。解释器比编译器的原理简单一些,因此更容易编写,但其运行程序的速度要比后者要慢。BASIC语言应用于家用计算机的时间较晚,1975年,比尔盖茨(Bill Gates,生于1955年)和其好友保罗艾伦(Paul Allen,生于1953年)为Altair 8800编写BASIC解释器,这一事件可以视为BASIC在此领域的开端,同一年他们创建了微软公司(Microsoft Corporation)。
Pascal
Pascal程序设计语言继承了ALGOL的许多结构,但也包括了COBOL的记录处理程序。该语言由瑞士计算机科学教授Niklaus Wirth(生于1934年)在20世纪60年代后期设计而成。Pascal在IBM PC程序设计员中很受欢迎,而备受欢迎的Pascal版本却是大名鼎鼎的Turbo Pascal。该版本于1983年由Borland公司推出,售价为$49.95。Turbo Pascal(由丹麦学生Anders Hejlsberg(生于1960年)编写)是Pascal的一个版本,提供了完整的集成化开发环境。文本编辑器和编译程序集成在一个程序里,这样方便了程序的调试和运算,大大加速了程序的开发速度。集成化开发环境以前主要用于在大型机上,但Turbo Pascal却首先在小机器上实现了。
Pascal对Ada也有很大影响。Ada是为美国国防部开发使用的一种语言,是以Augusta Ada Byron命名的。第18章中已提到过这个人,他是查尔斯·巴贝芝的解析机发展历程的记录者。
C
接下来就是C,一种深受喜爱的程序设计语言。C语言主要是由贝尔电话实验室的丹尼斯·M·里奇(Dennis M.Ritchie)开发的,从1969年开始设计并于1973年开发完成。人们常常对为什么以C来命名该语言感兴趣,答案其实很简单,它是一种早期的程序设计语言B的后继者。B是BCPL(Basic CPL)语言的一种精简版本,而BCPL来源于CPL(Combined Programming Language)。
如第22章所述,UNIX操作系统在设计过程中充分考虑到了可移植性。当时的许多操作系统都是基于某种处理器的,并且使用汇编语言编写,基本上没有可移植性可言。1973年,UNIX采用C语言编写(更准确地所,应该是重写)成功,从此以后UNIX操作系统和C语言就变得密不可分了。
C是一种风格非常简洁的语言。例如,ALGOL和Pascal使用关键字begin和end来界定程序块,而在C中这两个单词被一对大括号“{}”取代。下面给出一个例子,程序员常常会把一个常量和一个变量相加,比如:
i = i + 5;
在C程序中,你可以将上面的语句简写为:
i += 5;
如果只需要把变量加1(即增量),则该语句还可以精简曾下面这样:
i++;
在16位或32位处理器中,i++这种语句仅需要一条机器码就可以执行。
在本章的前面曾讲过,很多高级语言都不支持移位操作和按位布尔运算操作,而许多处理器其实支持这类操作,C语言打破了这种局限,它广泛地支持这类运算。除此之外,C语言的另一个重要特征是对于指针(pointer)的支持,指针本质是数字化描述的内存地址。C语言的很多操作与通用处理器的指令非常相似,因此C也被称为高级汇编语言(high-level assembly language)。与类ALGOL语言相比,C的操作集与通用处理器的指令集接近程度更高,或者说远胜过它们。
非冯·诺伊曼体系程序语言 LISP
但是,所有的类ALGOL语言——即大多数常用程序设计语言——其实设计模式都是基于冯·诺伊曼计算机体系的。设计一种非冯·诺伊曼体系的程序设计语言并非易事,而让人们接受并使用这种语言则更加困难。LISP(List Processing)是一种非冯·诺伊曼体系程序设计语言,它主要应用于人工智能领域,由约翰·麦卡锡(John McCarthy)在20世纪50年代末期开发完成。
APL
APL(A Programming Language)是另一种全新的语言,与LISP完全不同,它同样完成于20世纪50年代末期,由肯尼斯·艾弗森(Kenneth Iverson)开发。APL的特殊之处在于,它使用一个特殊的符号集,利用其中的符号可以一次性对整个数组里的数字完成操作。
类ALGOL语言一直在程序语言领域占据重要地位。而且近年来,此类语言在一些方面进行了改进,导致面向对象的程序设计语言(object-oriented language)的产生。面向对象语言主要应用在图形化操作系统中,我们将会在下一章介绍这种操作系统。