计算机编程语言简介

 

一、   什么是编程语言?

编程语言是用来表达机器指令的工具吗?是程序员之间交流的桥梁吗?是表达高层设计的载体吗?是算法的符号吗?是表达概念之间关系的途径吗?是实验用的器材吗?是控制计算设备的方法吗?我的结论是,一个多用途(general-purpose)编程语言在上述这些问题的回答都应该为“是”,以满足不同用户的需要。编程语言不能成为——仅仅一些“优雅”特性的集合(a mere collection of “neat” features)

   -Stroustrup [1994], commenting one the apparent lack of “agreement on what a language really is and what its main purpose is supposed to be.”

       编程语言是由用来表达计算的符号所组成。它可以被看作是用来定义程序的语法(syntax)及语义(semantic)规则。作为人类与计算机交流的重要桥梁,它的出现与发展,与计算机科学本身息息相关。时至今日,计算机的广泛使用已使上百种编程语言被创造了出来。

       编程语言的设计者一直试图在下面两个方面找到平衡点:

²        让语言本身更方便的使用

²        更有效的(efficient)利用计算资源

这里“方便”往往是第一位的,如果语言本身不能够方便地使用,那么效率就无从谈起。

       而不同的侧重点及设计哲学,也让众多编程语言之间相去甚远。

二、   基本概念

语法(syntax

语法是编程语言的外壳,程序展现给读者的外在表现就是由语法所决定的。不同的编程语言在语法上会表现出很大的不同。语法给出了一个关于程序合法性的精确定义。通常程序语言对语法的定义结合了正则表达式(regular expressions)和巴克斯-诺尔范式(Backus-Naur Form[1]

语义(semantic

尽管编程语言往往是通过语法给人们留下的第一印象,语义却是编程语言更实质的部分(因此也更加难以描述)。语义定义了一段合法的程序在运行要采用哪种表现(behavior)。

在编程语言中,语义通常用下面几种方式来描述:

²        公理型语义(Axiomatic semantics

²        指示型语义(Denotational semantics

²        操作型语义(Operational semantics

²        自然语言描述(Natural language descriptions

²        参照工具定义(Reference implementations

²        测试数据定义(Test suites

前三种是用数学的方法描述语义。他们的优点是精确、简洁、且能够方便地完成程序正确性的证明。然而,对于庞大的编程语言,用这些数学方法精确定义使语义的描述难以实现。另一方面,自然语言就会使编程语言设计者的描述工作大大减轻。但与此同时,由于自然语言本身的限制,这样的描述往往会表现出不精确的一面。而且、相对于数学方法、自然语言会使语义的描述显得冗长。如:使用自然语言描述的The Java Language Specification, 3rd Ed.596页那么长。而用操作型语义描述的The Definition of Standard ML, Revised只有114页。

第五种描述方法是参照工具定义,就是说首先将该语言实现出来,并称某个用来实现的工具为“权威的”。而这门编程语言就可以由该程序完整地定义出来。这种描述方法有很多吸引人的特性:精确、无需人去解释,当人们对一段程序该如何表现产生争议的时候只需把这段程序用那个“权威的”工具运行一下就可以判断谁对谁错。然而这种描述方式同样也存在很多缺陷,最大的缺陷是假如这个“权威的”工具本身有Bug,那么这个Bug就不可避免地成为了该语言中的一部分。

第六种描述方法是测试数据定义,这种方法就是,语言的设计者写出大量的例程出来,然后去说明它们应该如何表现(通常将这些例程的输出写出来)。这种描述方法的优点是方便测试语言的实现是否合法(只要把这些例程拿出来运行,看与输出是否匹配即可)。然而用这种方法是无法充分地描述语义的,因此通常这种方法要结合前五种方法一起使用。

实现(implementation

通常由人写出来的程序是不能够被计算机直接运行的,因此需要一个工具来完成编程语言与机器指令之间的转化。这个转化的过程叫做翻译(translation),实现翻译过程的工具叫做翻译器(translator),人写出来的程序叫做源程序(source program),翻译过后的机器代码叫做目标码(target code)。

实现翻译的有两种方法,编译(compile)和解释(interpret)。对应的翻译器叫做编译器(compiler)和解释器(interpreter)。它们之间的区别可以下面的图示形象地表示出来:

 

(a)COMPILE

(b)INTERPRET

Application Level…

 

 

Language Level…

Source program

Program

Interpreter

Machine Level…

Target code

Machine

Machine

图表 1: 计算机程序有两种实现的方法:(a)编译,自上而下,将源程序转化为可执行的机器代码,然后由机器直接执行。(b)解释,自下而上,制造一个更高级的“机器”(即解释器),来完成对源程序直接执行。

       由于可以直接执行源程序,解释型语言相对于编译型语言有着更大的灵活性,它运行在程序的运行过程中改变程序(如临时插入一段代码)。但是由于解释型语言需要每次运行的时候对源程序进行解析,因此在效率上编译型语言要优于解释型语言。

       通常我们把源程序本身具备的性质叫做程序的静态(static)属性,而程序在运行中体现出来的性质叫做程序的动态(dynamic)属性。编译型语言更注重程序的静态属性,因为当编译结束之后,机器执行的代码就被确定了下来。而解释型语言可以更多地处理程序的动态属性。

       常见的编译型语言有:FORTRAN, C/C++, Pascal, Java[2]等。

       常见的解释型语言有:BASIC, Python, MATLAB, 及大部分脚本语言。

       数据类型(data types

       计算机内部用二进制状态存储数据,而真实的世界中需要存储的数据多种多样,从人名、银行帐号,再到度量数值。低级的二进制数据在编程语言中被组织起来用以表示高级概念。大部分编程语言用不同的数据类型来表示各种各样的数据。设计和研究类型的专门的理论叫做类型论(type theory)。

根据类型是在程序运行之前进行检查还是在程序运行时进行检查,可分为静态类型语言(static typing)和动态类型语言(dynamic typing)。静态类型语言要求源程序规定每一个数据值都被分配一个确定的类型,在整个程序的运行过程中部发生改变。而动态类型语言的数据类型是在程序运行时才决定的,而且可以随着程序的运行而改变,实际上每个数据的类型是绑定在它的数据值里面,一起在内存中存储。[3]

通常情况下编译型语言都是静态类型语言,解释型语言都是动态类型语言。

另外,跟据编程语言对类型检查的严格程度,可以分为强类型语言(strongly-typed)和弱类型语言(weakly-typed,强类型语言试图引入严格的类型检查来增强程序安全性,包括静态的类型检查和动态类型检查。排除因程序员犯的类型错误而产生的不安全因素。

例如:ML是静态强类型语言,C是静态弱类型语言,Tcl是动态弱类型语言,Python是动态强类型语言。

三、   编程语言的历史

史前时代:λ-演算与图灵机指令

图表 2:阿伦佐·丘奇

编程语言的发展史要追溯的现代电子计算机的发明以前。在众多具有现代编程语言特点的语言中,最具影响力的要数在二十世纪三十年代被创造出来的λ-演算与图灵机指令了。λ-演算是由普林斯顿大学的阿伦佐·丘奇(Alonzo Church)为了研究函数定义,函数性质及递归函数而创造出来的形式化系统(formal system)。而图灵机是英国数学家阿兰·图灵(Alan Turing)假想出的一台有无限存储空间的计算设备,图灵机指令可以看成是汇编语言(assembly language)的前身。这两个类似于现代编程语言概念的提出,起初是想尝试将一阶逻辑命题的判定建立在算法(algorithm)的基础之上,但是随后此二人在各自的系统上分别证明了能够对所有一阶逻辑命题进行判断的算法是不存在的。此后人们便用λ-演算和图灵机去定义可计算性(computability)这个概念。

当电子计算机被发明以后,人们用可读性稍强的汇编语言代替机器代码,但是汇编语言仅仅是机器代码的“直接替换”,因此不可避免的要受限于机器本身,一段汇编代码往往只能在某些特定的机型上运行。

二十世纪五十年代:最初三语言,及Algol

在五十年代初的时候,最初的三个高级语言被创造了出来,他们是

²        LISP (the LISt Processor)

²        FORTRAN (the FORmula TRANslator)

²        COBOL (the COmmon Business Oriented Language)

这三种至今仍有使用,其中Lisp(名字被改为仅首字母大写)对后续编程语言的影响要更大一些。

19581960期间被设计出来的AlgolALGOrithmic Language)语言,堪称编程语言发展史上的里程碑。很多编程语言的概念在这个语言中被提了出来。Algol的两个主要创新是:

²        首创性地使用巴克斯-诺尔范式描述该语言的语法,几乎所有的后续语言都采用了这种描述语法的方式

²        对程序中出现的标识符(identifier)引入了作用域(scoping)的概念

尽管Algol没有在北美获得广泛的使用(部分由于政治原因,部分由于该语言本身不提供I/O)。然而,Algol却对后续的语言有着深远的影响,包括Simula, Pascal, SchemeModula等,而且很多算法教材采用类Algol语言作为伪代码(pseudocode)。

19671978:编程范型(programming paradigm)的建立

在这段期间,各种各样的编程语言如雨后春笋般地涌现,而各种编程范型也是在这个时代被建立起来,其中包括:

²        NygaardDahlSimula语言中提出了面向对象程序设计的概念,Simula是一门基于Algol的侧重于做仿真系统的语言。随后精巧的Smalltalk语言进一步地将面向对象的概念深化。

²        C,早期的系统编程语言,由Dennis RitchieKen Thompson1969年~1973年在贝尔实验室开发。

²        Prolog,由Colmerauer, RousselKowalski1972年设计的第一门逻辑编程语言。

²        ML,将多类型系统引进Lisp,成为了第一门静态类型的函数化编程语言。

绝大多数的现代编程语言是他们中至少一门的后续。

在六十和七十年代有一场关于选择“结构化编程”还是继续使用“GOTO”的旷日持久的争论。而这场争论与当时编程语言的设计及当时的编程风格息息相关,尽管那场争论在当时十分火热,今天的程序员都承认,除了极少数情况,即便是语言本身支持GOTO语法,使用GOTO都是一个差的编程风格。事实上,后来的编程语言设计者都觉得这场争论不仅无聊,而且令人难以理解。

二十世纪八十年代:巩固、模块化、性能

相比上一个时代,八十年代编程语言开始巩固。C++将面向对象的编程范型与C的系统编程结合了起来。美国政府标准化了Ada, 这是一门用于美国国防部的系统编程语言。在日本等国家,大量的预算被投入研发所谓的“第五代”编程语言(构建于逻辑框架的语言)。在函数化编程方面,MLLisp进行了标准化的工作。这个时代并没有创造出新的编程范型出来,相反,人们把原有的范型强化了。

另外,随着程序设计越来越多地应用于大型软件工程,这个时期的编程语言开始引入的“模块化”(modules)。即,把一个大型的程序分成一些小的部分,各个击破,再通过一些办法把这些小的部分组织起来。ModulaAdaML都在这个时期开发的自己的模块系统。

在这个时期很多原有的编程范型得到了扩充。例如,ArgusEmerald系统下的编程语言将分布式系统(distributed systems)的概念带入了面向对象程序设计。

编程语言的实现在这个时期也得到了极大的改善。RISC(精简指令集计算机)运动强调硬件的内部构建应该为编译器设计而不是为汇编程序设计。处理器速度的提高大大促进了编译技术的发展。而RISC运动则大大激发了人们发展编译技术的热情。

这种发展一直持续到了九十年代。到了九十年代,一场计算机史上的大革命开始爆发了。

二十世纪九十年代:互联网时代

九十年代中叶互联网的迅速发展将编程语言引向了一个新的时代。由于开放了一个全新的平台,互联网时代为新兴的语言流行提供了机遇。特别是,Java由于被整合进了Netscape Navigator浏览器而普及。而众多脚本语言(scripting languages)则广泛地应用于网络服务器上开发定制的各种应用程序。然而这些语言的出现和发展并没有动摇编程语言设计思路的根基,但是诸如垃圾回收器(garbage collection),强静态类型检查(strong static typing)等新特性逐渐成为主流。

当今编程语言的趋势:

²        增强校验程序安全性和可靠性的机制:如增强类型检查,静态线程(thread)安全性检查。

²        增强编程语言模块化的机制:mixins, delegates, aspects

²        面向组件的软件开发模式。

²        更强调程序的分布式特性和可移植性。

²        与数据库的整合,包括XML与关系数据库(relational databases)。

²        开放源代码的设计哲学被引入到了新兴的编程语言中去,如:PythonRubyLinux操作系统和Squeak

四、   基本编程范型

命令化编程(Imperative Programming

像自然语言中的祈使句那样,命令化编程是面向操作(action)的。即,将计算看成是一系列操作的执行。它可以将计算抽象成程序的状态(state)及改变程序状态的语句,因此可以看成是图灵机的思想在电子计算机上的实现。

一般认为最早的命令化编程语言是FORTRAN。但是用FORTRAN语言写出来的程序要求精确编写格式(与我们所熟悉的C, Pascal有所不同),而且比较大地依赖大量GOTO的使用。而大量的GOTO直接导致了日趋复杂的程序难以实现。为了解决这场程序设计的危机,以Dijkstra为首的计算机科学家开始倡导结构化编程(structured programming)这个概念。结构化编程通过三种基本类型的语句来代替GOTO的使用:

²        顺序语句:执行A

²        条件语句:IF (expressions) THEN A ELSE B

²        循环语句:WHILE (expressions) DO A

通常结构化编程也支持把一系列语句看成一个“块”(block,然后把这个“块”内嵌到两个标识符中间(比如说:在ALGOL中的beginend)。这样,就可以用这三种语句构建出所有用GOTO可以实现的程序控制流(control flow)了。

结构化编程倡导了一种复杂问题的解决方法。当人们面对的问题很复杂以至于无法用简单的办法去解决的时候,可以将复杂的问题分成若干个模块,每一个模块对应一个相对简单的问题。当这些模块完成的时候,这个复杂的问题也就随之解决。而每一个模块又可以分解成更小的模块。这样的过程不断持续,不断地将问题细化。最后如果问题细化到可以用代码实现出来的时候,也就找到了解决这个问题的办法。

结构化编程往往与过程型编程(procedural programming)联系起来。所谓过程型编程,就是把某个实现特定功能的代码段看作一个过程(procedure[4],如要实现这个特定的功能,只需要调用这个过程(call)就可以了。这样一段代码可以在不同的地方使用,也就是实现了代码的重用(reuse)。根据不同的需要,一个过程可以有一系列输入及一个输出,通常把输入作为这个过程的参数在调用的时候传递进来,然后过程会根据需要按照计算的结果向外界返回一个值,作为这个过程的输出。

调用过程的一个很重要的应用就是递归(recursion)。在程序设计里面所谓的递归,就是一个过程对自身的直接或是间接的调用。递归在计算机科学中的应用无处不在,大量的算法本质上是将一个问题的求解化归为一个或者若干个更小问题的求解。而应用递归,只需要调用自身,就可以方便地让计算机自动完成这样化归的一个过程。

也有人将过程的调用和递归看成是结构化编程第四种类型的语句。

面向对象编程(Object-Oriented Programming

面向对象编程打破了长期以来人们对命令化编程就是一系列在计算机上执行的指令的认识。面向对象编程主张:一个程序由一系列的单位元素组成,我们称这个单位元素为对象(object)。而这些对象又是相互作用的,每一个对象可以接受外界的信息,处理自身的数据,以及向外界发送信息。

通常认为最早的面向对象编程语言是Simula,它是将对象(object)、类(class)、继承(inheritance)和动态类型(dynamic typing)这些概念扩充到Algol中去。正如名字所暗示的那样,Simula起初是一门被用作模拟仿真的语言。而正是因为这样的需要使诸如类和对象这些面向对象的元素被添加进来。后来人们发现面向对象编程比结构化编程更适合大型的软件工程,有如下的原因:

²        结构化编程要求在编写程序之前就对要解决的问题有一个全面的认识,才能够对问题不断细化直至细化到代码的阶段。而这些在实际的应用中往往是做不到的,特别是,当程序的编写者想到代码的一个改进的时候,他做出的改动往往会影响到整个程序中去。

²        结构化编程用解空间(solution space)描述问题,而面向对象编程用问题空间(problem space)描述问题。

²        面向对象编程有它一系列的机制更好地实现代码重用(reuse),相比结构化编程,它能够更进一步支持模块化编程。

下面让我们来看看面向对象是什么:

²        类与对象(class & object

类与对象是面向对象编程最基本的两个概念。类是面向对象编程中最基本的模块。它是被认为是在问题空间中出现的有某些特定的性质和行为的事物的抽象。对象是在程序运行过程中类的实例(instance,它可以存储自己的数据,但是与类中其他的实例有着相同的表现(即,共享相同的代码)。

²        封装(encapsulation

封装是构建一个类的最基本途径。通过封装,可以把对象的内部结构和外部接口分离出来。类在经过封装之后,对外呈现出一个接口,外界与这个类中的对象仅能通过接口交互(即,可以理解为向这个类中的对象发送消息)。而类的内部结构仅有该类的设计者所掌握,外界不会因类的内部结构改变而受到影响。封装这个过程让代码有了更好的可扩展性,只要保证对外的接口不变,人们可以不断地对类的内部结构进行改良,而不影响这个类中对象的使用。

²        继承(inheritance

继承支持在已有的类中创建派生类(derived class)的操作。继承机制提供了一个对已有的类进行外部扩展的机制。子类拥有基类(base class)的一切外部接口,同时可以加入自身独有的外部接口。在问题空间上,继承所表达的是一种“is-a”的关系,比如,狗是哺乳动物,那么可以说“狗”这个类可以继承“哺乳动物”的类。

²        多态(polymorphism

多态为面向对象编程提供了动态机制。多态的含义是:对于发送过来的相同的一个信息,不同类型的对象会跟据自身的类型,来完成相应类型的操作。也就是说,多态是为不同的类找到一个公共的接口,外界可以通过这个公共接口对这些类的对象发送信息,而不管这些对象具体是属于哪个类的。对象会根据自己属于哪个类完成自己的操作,这一系列的操作都是面向对象的设计方法保证自动完成的,无需人工地进行类型的判断。在面向对象的编程语言中,多态一般是建立在继承的基础之上的。当一个派生类从基类那里继承出来的时候,它可以选择性的重写(override)基类的一些代码。有的时候,我们会把基类当作一个公共的接口,它的不同派生类会对这个公共接口重写不同的代码,因此就完成了多态。[5]

C++将面向对象的元素融入到了系统编程语言C中去,在二十世纪九十年代以前一直是最流行的面向对象编程语言。它提供了多重继承(multiple inheritance)、异常处理(exception handling)、模版(template)、运算符重载(operator overloading)及名字空间(namespaces)等强大的语言特性。它能够广泛流行的另一个原因就是对C几乎完全的向下兼容。

图表 3Java的吉祥物,Duke

进入九十年代以后,Java成为了使用最为广泛的面向对象编程语言。Java首先简化了C++中一些庞大而繁杂的特性。为了防止C++中经常出现的内存泄露(memory leak[6]问题,Java引入了自动垃圾回收( automatic garbage collection)机制,将内存的分配与回收自动化,减轻程序员的负担。此外,Java通过自己的一套API及在各个操作系统上的Java虚拟机,真正地实现了平台独立(platform independence)的特性,即实现了“一经写出,到处运行”(write-once/run anywhere)的原则。此外,作为新出现的编程语言,Java对于图形(graphics),线程(threading)及分布式编程(distributed programming)等有着更好的支持。

函数化编程(Functional Programming

函数化编程将计算的过程看作是数学函数的求值。它可以排除过程型编程过程的调用对整台机器状态产生的影响。在函数化编程语言看来,一个函数只受它自身的输入影响,对外的影响也只能表现在自身的输出上面。此外,在函数化编程语言中,函数本身也可以作为参数传递到其他函数中去,同样也可以作为返回值传递出来。数学函数由于其本身比较好的性质,为函数化编程带来了一些好处。首先就是更容易证明程序的正确性,另外由于函数化编程消除了副作用(side-effect[7],因此它为并行计算(parallel programming)提供了更好的支持。

函数化编程在学术界及爱好者之间有着一定范围的使用。但是在软件工程界,却甚少应用。这主要是基于两个原因。首先是由于种种原因,函数化编程在效率上没有办法和命令化编程(包括面向对象)相比。其次对于已经习惯命令化编程的人来说,函数化编程是很难适应的。

逻辑化编程(Logic Programming

逻辑化编程是一种由程序的编写者提出规则,然后自动由这些规则进行的推理的编程范型。当数学家和哲学家们发现了逻辑是理论分析的有效工具时,人们便自然地想到了将逻辑风格带入到编程语言中去。当进行逻辑化编程的时候,程序的编写者只需将理论的规则输入到程序中去,然后描述一个问题,这个程序运行时,会对这个问题的真伪进行判断,并建立相应的证明。逻辑推理的过程在逻辑化编程语言中是自动完成的。逻辑化编程与人工智能领域的关系密不可分。

逻辑化编程在一些领域有着它的应用:

²        专家系统,程序从一个巨大的模型中产生一个建议或答案。

²        自动化证明定理,程序产生一写新定理来扩充现有的理论。

五、   结论

程序设计的广泛用途,促成了编程语言的千变万化。从数值计算、系统编程,到大型的应用软件,再到网络编程。编程语言的每一次进步与发展,都与人们的需求密不可分。而人们的需要,又无时无刻不伴随着计算机科学自身的发展。因此,要想学习一门编程语言,仅仅了解其语法、语义,或是仅仅能用其写出一些程序,是远远不够的。若能够深入剖析某门编程语言的特性,就会发现这些特性所影射出来的设计哲学,进而看到当今或是某时代的计算机科学发展状况。希望本文能够起到一个抛砖引玉的作用,激发起同学们探索、学习的热情,提升自己的视野,让自己站在一个更高的位置上去,从而为计算机科学做出自己的贡献


[1] 有关正则表达式和巴克斯-诺尔范式的介绍以及他们是如何精确定义程序的,参见形式语言理论(formal language theory)的相关知识。

[2] Java语言不应该被算作严格意义上的编译型语言,它先把源程序编译成java码,再通过java虚拟机解释执行java码,以实现其跨平台的特性。

[3] 面向对象编程语言为实现程序的多态性而引入的RTTIRun-Time Type Identification)机制也是将数据的类型信息在程序运行的时候储存在内存里。通常我们认为这是静态类型语言引入的动态机制。

[4] C语言中被称作函数(function),尽管这里面函数的概念与数学上的是不同的。

[5] 对于编译器(或解释器)来说,实现多态并没有看起来那样直接,它需要使用一个叫做动态绑定(dynamic binding)的技术才能实现多态

[6] 内存泄露,是指向申请内存空间之后没有及时地释放,造成一段内存资源不可再用。

[7] 在程序设计中的所谓“副作用”,是指某段程序在运行过程中对机器中不属于这段程序的状态的改变。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值