目录
九、程序设计语言
9.1 演化
对计算机而言,要编写程序就要使用计算机语言。计算机语言是指编写程序时,根据事先定义的规则(语法)写出的预定语句集合。计算机语言经过多年的发展已经从机器语言演化到高级语言。
9.1.1 机器语言
在计算机发展的早期,机器语言是唯一的编程语言。每台计算机都有自己的机器语言,这种语言由“0”和“1”的序列组成。机器语言是计算机唯一能理解识别的语言,它由具有两种状态的电子开关构成:开(表示1)、关(表示0)。
虽然机器语言编写的程序真实的表示了数据是如何被计算机操作的,但是它至少有两个缺点:首先,它依赖于计算机,两个计算机如果使用的硬件不同,它们的机器语言就不同;其次,这种语言编写程序是非常乏味的,而且容易出错。现在我们将机器语言时代称为编程语言的第一代。
下图为假想计算机进行两个数加法的机器语言代码:
9.1.2 汇编语言
编程语言接下来的发展伴随着使用带符号或助记符的指令和地址来代替二进制码而发生的。因为它们使用符号,所以这些语言首先被称为符号语言。汇编程序用于将汇编语言代码翻译成机器语言。
下图是假想计算机进行两个数加法的汇编语言代码:
9.1.3 高级语言
汇编语言提高了编程的效率,但是程序员仍然需要了解所使用的硬件,并且汇编语言与机器语言类似,每条机器指令都需要编码。为了使程序员更关注问题本身,促进了高级语言的发展。高级语言必须通过编译或解释转化为机器语言才能被计算机执行。
9.2 翻译
高级语言要被计算机执行,需要转化为机器语言。高级语言程序被称为源程序,被翻译成的机器语言程序称为目标程序。有两种方法用来转换:编译和解释。
9.2.1 编译
编译程序通常把整个源程序翻译为目标程序。
9.2.2 解释
有些计算机使用解释器把源程序翻译为目标程序。解释是指把源程序的每一行翻译成目标程序中相应的行,并执行它的过程。解释有两种方式:在Java语言以前的部分语言使用的解释和Java使用的解释。
1. 解释程序的第一种方法
这种方法是一个慢的过程,在这种方法中,源程序的每一行被翻译为目标程序的相应行并执行。如果在翻译或执行中出现错误,就会报错,并中止后续过程。当修改源程序后,再次从头开始执行。
2. 解释程序的第二种方法
Java使用另一种方法来解释程序,源程序到目标程序的翻译分为两步:编译和解释。Java源程序首先被编译,创建Java的字节代码,字节代码看起来像机器语言中的代码,但它并不是目标代码,它是一种虚拟机的目标代码,这个虚拟机称为Java虚拟机或JVM。字节代码能被任何能运行JVM的计算机编译或解释。
9.2.3 翻译过程
编译在执行前翻译整个源程序,而解释一次只翻译执行一行,但它们翻译的过程是相同的。
词法分析器:分析源程序中的符号,并创建助记符表。例如,将源程序中的 f、o、r组合起来,形成for助记符。
语法分析器:通过助记符表,对源程序进行分析,找出指令。例如,将 "x" "=" "0",三个助记符组合成"x=0"这条指令。
语义分析器:检查语法分析器创建的句子,确保它们没有二义性。由于当前的编程语言通常没有二义性,所以这个步骤要么被删除,要么责任最小化。
代码生成器:由指令生成能在该计算机上运行的机器语言(二进制代码)。
9.3 编程模式
9.3.1 面向过程模式
在面向过程模式(强制性模式)中,我们把程序看成是操纵被动对象的主动主体。
在面向过程模式中有两个实体:过程(动作)、数据。
过程与程序要区分开,程序不定义过程,过程必须是预先存在的。程序只是调用(触发)过程来对数据进行操作。过程就是为了完成特定动作的语句或语句集合(比如加法就是一个过程)。
操纵被动对象的主动主体可以理解为:数据就像一个石头,现在我们需要将这块石头运到某个地方,那么首先需要拿起这块石头,然后走到目的地,放下石头。这三步就可以看作三个过程,在语言里用函数来实现,这三步也可以看作是共同实现将石头运达目的地这一个过程的实现,人就是主动主体。
程序不定义过程可以理解为:程序只是调用过程,就像加法,程序只是调用特定指令实现加法,加法过程本身是已经存在的,并不是程序定义的。
如果我们考虑过程和操作对象,那么过程式语言的概念就变得更为简单。这种模式的程序分为三部分:对象创建部分、一组过程调用和每个过程的一组代码。有些过程在语言中已经定义,通过这些语言,开发者可以开发新的过程。
一些过程式语言:
FORTRAN(FORmula TRANslation):由Jack Backus领导下的一批IBM工程师所设计,于1957年变为商用。FORTRAN是第一代高级语言。FORTRAN所具备的高精度算法、处理复杂数据的能力和指数运算等特征使它仍然是科学或工程应用中的理想语言。
COBOL:由一批计算机专家在美国海军的Grace Hopper指导下设计出来,COBOL有一个特定的设计目标:作为商业编程语言使用。商业中程序设计要求概括如下:快速访问文件和数据库、快速更新文件和数据库、生成大量的报表。界面友好的格式化的输出。
Pascal:由Niklaus Wirth在1971年于瑞士的苏黎世发明,它的设计目标是通过强调结构化编程方法来教初学者编程。现在的过程式语言归功于Pascal。
C:C语言是由贝尔实验室的Dennis Ritchie在20世纪70年代初期发明的。最初用于编写操作系统和系统软件。后来,由于以下原因在程序员中流行:C有一个结构化的高级编程语言应有的高级指令,是程序员无需直到硬件细节;C也有一些低级指令,是程序员能够直接快速地访问硬件。相对于其他高级语言,C语言更接近于汇编语言;C是非常有效的语言,指令短。
Ada:是根据Lord Byron的女儿和助手的名字来的。Ada是为美国国防部(DoD)而开发的,并成为所有DoD承包人使用的统一语言,Ada有以下三个特征使其成为DoD和工业的流行语言:Ada有其他过程式语言那样的高级指令;Ada允许实时处理的指令,从而便于过程控制;Ada具有并行处理能力,可以在具有多处理器的主机上运行。
9.3.2 面向对象模式
面向对象模式处理活动对象,而不是被动对象。在这些对象上执行的操作(方法)都包含在对象中,只需要有合适的外界刺激就可以执行某个动作。比如说,将石头捡起来,走到目的地,放下石头是石头本身包含的动作,那么我们要完成运送石头任务时,只需要给石头一个激励,石头就会自己完成这些工作,这些动作在面向对象模式中称为方法。再例如,打印文件、删除文件等操作都包含在文件本身,这时候只需要给文件一个刺激,文件就可以自己完成打印等工作。
比较过程式模式和面向对象模式可以发现,在过程式模式中,过程是独立的实体,而在面向对象模式中,方法是属于对象领地的。
类:相同类型的对象需要一组方法,这些方法显示了这类对象对来自对象领地外的刺激的反应。为了创建这些方法,面向对象语言使用称为类的单元。
方法:方法的格式与有些过程式语言使用的函数类似。每个方法都有它的头、局部变量和语句。我们可以认为面向对象语言实际上是带有新的理念和新的特性的过程式语言的扩展。比如,C++就是面向对象的C语言扩展。
继承性:在面向对象模式中,一个对象(子类)能从另一个对象(父类)继承,这个概念称为继承性。当一般类被定义后,就可以定义继承了一般类中一些特性的具体类,同时具有一些新的特性。比如,定义一个几何形状类,我们就可以定义圆、矩形等具有几何形状类特性以及新特性的类。
多态性:在面向对象中的多态性是指我们可以定义一些具有相同名字的操作,而这些操作在相关类中做不同的事情。例如,定义一个求面积方法,但在矩形类和圆类中,这个方法的操作不同。
一些面向对象语言:
C++:由贝尔实验室Bjarne Stroustrup等人开发出来,是比C语言更高级的一种语言。它使用类来定义相似对象的通用属性以及可以用于它们本身的各种操作。C++语言的设计遵循三条基本原则特性:封装、继承、多态。
Java:由Sun Microsystems公司开发的,它在C和C++的基础上发展而来,但是C++的一些特性(如多重继承等)从语言中被移除,从而使Java更健壮,而且Java是完全面向类操作的,不像C++中可以不使用类就可以解决问题。Java程序可以是一个应用程序或是小程序(嵌入在网页)。Java自带丰富的类库,并且允许多线程执行代码(线程是指按顺序执行的动作序列)。
9.3.3 函数式模式
在函数式模式中程序被看成一个数学函数,关于这一点,函数是把一组输入映射到一组输出的黑盒子。
函数式语言主要实现下面的功能:函数式语言预定义一系列可供任何程序员调用的原始(原子)函数;函数式语言允许程序员通过若干原始函数的组合创建新函数。
函数式语言相对过程式语言具有的两方面优势:它支持模块化编程并且允许程序员使用已经存在的函数来开发新的函数。
一些函数式语言:
LISP:表处理解释语言(LISt Programming)是20世纪60年代早期由麻省理工学院科研小组设计开发的,它是一种把表作为处理对象的语言。
Scheme:表处理解释语言没有统一的标准化,不久之后,就有许多版本流传于世。实际使用的标准是由麻省理工学院在20世纪70年代初早期开发的,称为Scheme。
9.3.4 声明式模式
声明式模式依据逻辑推理的原则响应查询。它是由希腊数学家定义的规范的逻辑基础上发展而来的,并后来发展成为一阶谓词演算。
逻辑推理以推导为基础。逻辑学家根据已知正确的一些论断,运用逻辑推理的可靠的准则推导出新的论断。程序员需要学习有关主题领域的知识或是向该领域专家获取事实才能正确做出逻辑推理。
声明性语言有自身的缺憾,那就是有关特殊领域的程序要收集大量的事实信息而变得非常庞大,这也是为什么声明式语言目前只用于人工智能领域的原因。
Prolog:最著名的声明式语言是Prolog,由法国人 A.Colmerauer于1972年设计开发。Prolog中的程序全部由事实和规则组成。例如,关于人类最初事实可陈述如下:
9.4 共同概念
给出一些过程式语言的共同概念,有的概念在面向对象语言也可以使用,因为创建方法时使用的也是过程式模式。
1. 标识符
标识符允许给程序中的对象命名。例如,数据都是存储在内存单元中的,如果没有标识符给这个内存单元命名,那么程序员在使用这个数据的时候,就需要知道它真实的物理地址在使用它。而使用标识符对这个内存单元命名,或是对这个内存单元的地址命名,就可以让编译器根据标识符去追踪它。
2. 数据类型
数据类型定义了一系列值和应用于这些值的操作。每种数据类型值得集合称为数据类型得域。
简单数据类型:不能分解为更小数据类型的数据类型。强制性语言定义了一些简单数据类型:整型、浮点型(实数)、布尔类型、字符型。
复杂数据类型:一组元素,这些元素可以是简单数据类型或是复杂数据类型。大多数语言定义了如下复杂数据类型:数组(一组元素,每个元素都具有相同的数据类型)、记录(一组元素,其中的元素可以是不同的数据类型)。
3. 变量
变量是内存单元的名字。每个内存单元都有一个地址,但程序员直接使用内部地址是非常麻烦的,首先,程序员不一定知道该内存单元的地址,其次,一个数据可能占据多个内存单元。使用变量命名数据的内存单元,就可以直接利用变量名找到对应的数据。
大多数语言要求变量在使用前需要声明,告诉计算机这个变量将要在程序中使用,计算机要预留出相应的存储区域,并命名它。
虽然变量的值可能会在程序运行中改变,但大多数语言允许变量在声明时初始化。初始化就是在声明时,向变量中存储一个值。
4. 字面值
字面值是程序中使用的预定义的值。大多数编程语言都有整数、实数、布尔、字符和字符串五种类型的字面值。例如,指令"x=1",1就是整型字面值。为了和标识符区分开了,字符字面值需要用单引号括起来,字符串字面值需要用双引号括起来。
5. 常量
虽然字面值可以作为常量使用,但有些时候常量是会随着时间改变的。例如银行的利率等,这个值可能在一段时间内是不变的(常量),但是一段时间之后,它可能是另一个值。如果使用字面值,那么程序中所有用到这个值的地方都需要修改,所以大多数语言定义了常量。当这个值变化时,只需要修改常量定义处的值即可。常量是有类型的,在声明时要说明它的类型。
6. 输入和输出
几乎所有的程序都需要输入和输出数据,大多数语言使用一些预先定义好的函数完成输入输出。
输入:数据可以通过语句或预先定义的函数来完成输入。例如C语言中的格式化输入函数,它从键盘读取数据并格式化,把它储存在一个变量中:
%d是占位符,告诉程序输入为整数,&num是变量的地址,告诉程序将输入的整数放在这个地址的内存单元中。
输出:数据可以通过语句或预先定义的函数来完成输出。C语言中的格式化输出函数:
7. 表达式
表达式是由一系列运算符和操作数构成的式子,它的值是运算后的结果。
运算符有算术运算符、逻辑运算符和关系运算符等。
操作数接收一个运算符的动作。一个运算符可能有1个或几个操作数。
8. 语句
每条语句都使程序执行相应的动作,它被直接翻译成一条或多条计算机可执行的指令。
赋值语句:存储一个值到变量中,该变量是声明部分已经创建的。大多数语言使用"=”来赋值,一些语言使用“:=”来赋值。
复合语句:复合语句是包含0条或多条语句的代码单元,也被称为块,复合语句使得一组语句成为一个整体。复合语句一般包括一个左大括号、一个右大括号以及一些语句。
控制语句:控制语句是一些语句的集合。通常语句是被一条一条执行的,但有时候想要改变这种执行顺序。有些语句需要在满足条件时执行,有些语句需要重复执行,这时候就需要控制语句。在机器语言中,使用jump来改变执行顺序;早期的强制性语言使用go to语句,但在现在已经不提倡使用。结构化编程强烈推荐使用三种结构:顺序、判断(选择)、循环。控制语句也就有选择语句和循环语句等。
选择语句:大多数语言都有两路和多路选择语句。两路选择通过if-else实现;多路选择通过switch实现。但switch语句的选择条件只能是整数,例如整型或字符型。
循环语句:
while循环是一个预先检查的循环,它检查测试表达式的值。如果值为真,则进入循环迭代一次,然后再检测。while循环被认为是事件控制循环,循环将一直持续到一个事件发生,即表达式从真变到假。
for循环也是一个预先检查的循环,它会先检查条件是否为真再判断是否要循环。for是一个计数器控制循环,计数器被初始化一个值,这个值在每次迭代中增加或减少,直到不满足条件。
do-while是一个事件控制循环,但它是后测试循环,循环语句在执行一次后才判断条件是否为真,也就是说,do-while循环的循环语句至少执行一次。
9. 子程序
子程序的概念在过程式语言中及其重要,在面向对象语言中的作用要少些。用过程式语言写的程序通常是预先定义的一组过程,为了完成同一任务的一些过程组成的集合放在它们自己的程序中,这就是子程序。子程序使得程序更加结构化,完成任务的子程序能一次编写,多次使用。
在增量程序开发中,程序员可以通过在每一步增加一个子程序一步步测试程序,在编写下一个子程序前进行测试,能帮助检查错误。
1. 局部变量:在过程式语言中,就像主程序一样,子程序能调用预先定义的过程,在局部对象上操作。当子程序被调用时,这些局部变量被创建,当子程序运行结束后,局部变量被销毁。
2. 参数:子程序一般需要被主程序调用使用,主程序需要子程序创建一些局部变量来帮助自己完成任务,在这种情况下,主程序中的数据就需要和子程序中的局部变量有联系,就需要使用参数。主程序中的参数叫实际参数,子程序中称为形式参数。
传递参数有两种方式:传值调用和传址(引用)调用。
传值调用:由于作用域的不同,子程序中的变量名可以与主程序中的相同或不同。主程序为了让子程序帮自己处理数据,将数据的值传递给子程序中的局部变量,子程序再对局部变量进行操作,最后完成任务。在传值调用中,子程序接收的仅仅是实际参数的值,并不能改变实际参数本身,当程序需要改变实际参数的值时,传值调用就不是那么好用了。
传址(引用)调用:在传址调用中,主程序将要处理数据的地址传递给子程序的局部变量,这时候,主程序中的实际参数和子程序中的局部变量指向的就是同一个内存空间,因为它们保存的地址是一样的,就可以在子程序中对主程序的数据进行改动。
3. 返回值
子程序可以有一个返回值(可以是值或地址等),调用子程序的程序可以使用这个返回值。
4. 实现
子程序的概念在不同的语言中被不同的实现,在C语言中子程序实现为函数。