杨飞扬
(北京邮电大学 国际学院, 北京 102209)
计算机语言本质上是一种形式语言。形式语言在数学和计算机科学中是用精确的数学或机器可处理的公式定义的语言,如同人类语言学中的语言一样包含两个方面:语法(Syntax)和语义(Semantics)。专门研究形式语言语法的数学和计算机科学分支叫做形式语言理论,它只研究语言的语法而忽略它的语义。
在形式语言理论中,形式语言是一个字母表上的有限长度的字符串集合。一个形式语言可以包含无限多字符串。每一种程序设计语言可以看作是一套包含语法、词汇和含义的正式规范。这些规范通常包括标识符和命名常规、数据和数据结构、指令及流程控制、引用机制和重用。
1.语法(Syntax):一种编程语言的表面形式称为语法。大多数编程语言是纯粹的文字,它们使用的文字序列包括文字、数字和标点符号,如同自然语言。另一方面,也有一些编程语言使用图形而非文字,即用符号之间的视觉关系来描述和定义程序,此类语言成为可视化编程语言,比如微软的机器人开发者工作室(Microsoft Robotics Developer Studio)软件就包括一种用于描述数据流的可视化编程语言。[1]
2.语义(Semantics):语义有静态和动态之分。静态语义定义了对合法的文本中难以用标准语法表达的结构的限制。例如,检查每一个标识符是否在被调用前已经被定义过,或者检查在案例语句中的标识是否唯一。动态语义规定了语言的各种构件怎样以及何时生成合理的程序行为。比如在计算机通信环境下,可以规定一个远程调用最多被执行一次,或者某个操作可以被执行,也可以不被执行,还可以被部分执行。动态语义赋予了程序更大的灵活性,但也容易导致歧义,并由此产生不易查出的程序错误。
3.数据类型(Data Types):虽然计算机内部的数据都只以二进制代码方式储存,即0-1或者开-关模式,但现实生活中人们更习惯于将代表各种信息的数据分类成整数、实数、字符、日期、货币等类型。与此对应,人们在计算机语言中也加入了数据类型和类型体系。
类型体系规定怎样对各种数据和表达式分类。类型体系可以帮助发现不合规范的操作,例如整数和字符串的乘法操作,从而在一定程度上保证用该语言写出的程序的正确性。很多语言都有类型检验,有的是静态检验,比如大多数编译语言都在编译时报告此类错误。有些是动态检验,比如Javascript、Perl和Python,只在程序运行时检查类型错误。当然也有些语言没有类型体系,比如汇编语言,因为它所有的数据都是不同长度的二进制码。[2]
对类型的检验也有强弱之分。弱类型检验容许某个类型的值被当作其他类型来操作,比如字符串可以当成一个数。强类型检验就不容许这样的处理。C和 Java是强类型语言的代表,Perl和Javascript是弱类型语言的代表。
很多语言还在内置类型的基础上组合出复杂的数据结构型态(使用数组、列表、堆栈、文件等)。面向对象语言允许程序员定义新的数据型态,即“对象”或“物件”,以及运行于该对象的函数和方法。常见的数据结构有堆栈(Stack)、链表(LinkedList)、树(Tree)、图(Graph)、堆(Heap)、散列(Hash)等。[3]
4.赋值语句(Assignment Statements):赋值语句用于给一个变量赋值,形式一般是“a=<表达式>;”。几乎每一种语言都有赋值语句。完成一个赋值语句一般包括三步操作:计算等号右边的表达式的值;计算等式左边变量在内存中的地址;把第一步得到的值复制到第二步找到的内存地址中。[4]
5.控制语句(Control Statements):控制语句用来控制赋值语句执行的顺序。常用的控制结构包括条件转移语句(if-then-else语句,case-switch语句)、循环语句(for-loop语句,while-loop语句等)。条件转移语句用来在两个或者更多的执行顺序中选择一个来执行,该条件一般是一个表达式。具体选择哪一个分支,要按照指定的表达式的结果来执行。循环语句用来重复执行一个或者一组语句,一般都有循环步数的初值、终值和步幅的设置。[4]
6.子程序(Subprograms):子程序用来定义一组可被重复调用的数据声明和程序语句,有时也称为过程、函数、子例程、方法。其目的是可以重用现有程序代码,从而节省开发和调试的时间,已经成为现代编程语言中不可或缺的构成要素之一。当子程序被调用时,需要传递给它一组数值,通常叫参数。子程序按照输入的参数执行相应的运算或者操作,并且把所得的结果返回给调用者。[3]
7.模块(Modules):相关的子程序组织在一起就构成模块。模块是软件工程的核心概念之一。大中型的软件系统常有成千上万行的语句和数以百计的子程序,把它们按功能或者逻辑关系分割成不同的模块有助于开发、调试和维护。通过合理的设计和实现,软件模块也可以像子程序那样被其他模块反复调用,从而有助于在更高层次上实现代码重用。[3]
8.标准库:大多数编程语言都有一个相关的核心库,也被称为“标准库”,因为它往往是语言标准的一部分。核心库通常包括定义为常用算法、数据结构以及用于输入和输出的机制。例如在Java中,一个字符串被定义为java.lang.String中的类的实例。同样,在Smalltalk中,一个匿名函数表达式(“块”),构建了图书馆的BlockContext类的一个实例。[1]
要编写可在计算机上运行的程序,程序员需要懂至少一种编程语言。除此之外,他还需要有一个编程环境,会使用一些工具,才能把他头脑中的想法和算法用编程语言表达成计算机程序,并且最终变成可执行代码在计算机上运行。常用编程工具如下。
1.编辑器(Editor):编辑器是用于创建和修改源程序的工具。源程序通常以字符文件的形式保存在磁盘上。编译器通常接受由任何生成标准字符文件(例如ASCII文件)的编辑器编写的源程序。现在很多编辑器都加入了内置编译器,目的是让程序员在程序的编写时而不是编译时就得知部分语法错误。有的编辑器还允许从中调用编译器以及其他程序,这样程序员无需离开编辑器就可执行某些开发调试任务。
2.编译器(Compiler):编译器将高级程序设计语言变换成计算机硬件所能识别的机器语言,以便计算机进行处理。它可以做语言和文法检查、词法分析、语法分析、语法制导翻译、中间代码生成、存储管理、代码优化和目标代码生成。
3.解释器(Interpreter):解释程序是如同编译器的一种语言翻译程序。解释器直接执行程序的源代码,而不是翻译成目标文件。它与编译器的不同之处在于:它立即执行源程序而不是生成在翻译完成之后才执行的目标代码。从原理上讲,任何程序设计语言都可被解释或被编译,但是根据所使用的语言和翻译情况,很可能会选用解释程序而不用编译器。例如,我们经常解释BASIC语言而不是去编译它。类似地,诸如LISP的函数语言也常常是被解释的。
解释程序也经常用于软件的开发,此处的程序很有可能被翻译若干次。而另一方面,当执行的速度是最为重要的因素时就使用编译器。这是因为被编译的目标代码比被解释的源代码要快10倍或更多。但是,解释程序具有许多与编译器共享的操作,两者间有一些混合之处。
4.汇编器(Assembler):汇编程序是用于特定计算机上的汇编语言的翻译程序。汇编语言是计算机的机器语言的符号形式,它极易翻译。有时,编译器会生成汇编语言以作为其目标语言,然后再由一个汇编程序将它翻译成目标代码。
5.连接器(Linker):连接器收集程序的组件的目标文件和解析外部来自一个组件的引用到另一个,以便形成一个可执行文件。编译器和汇编程序都经常依赖于连接程序,它将分别在不同的目标文件中编译或汇编的代码收集到一个可直接执行的文件中。在这种情况下,目标代码,即还未被连接的机器代码,与可执行的机器代码之间就有了区别。连接程序还连接目标程序和用于标准库函数的代码,以及连接目标程序和由计算机的操作系统提供的资源(例如存储分配程序及输入与输出设备)。连接程序现在正在完成编译器最早的一个主要活动(这也是“编译”一词的用法,即通过收集不同的来源来构造),连接过程对操作系统和处理器有极大的依赖性。
6.装载器(Loader):装载器的任务是拷贝可执行文件从磁盘到内存,并初始化电脑前执行该程序。编译器、汇编程序或连接程序生成的代码经常不完全适用或不能执行,但它们的主要存储器访问却可以在存储器的任何位置中与一个不确定的起始位置相关。这样的代码可重定位,装入程序可处理所有的与指定的基地址或起始地址有关的可重定位的地址。装入程序使得可执行代码更加灵活,但是装入处理通常是在后台或与连接相联合时才发生,装入程序极少会是实际的独立程序。
7.预处理器(Preprocessor):预处理器是在真正的翻译开始之前由编译器调用的独立程序。预处理器可以删除注释、包含其他文件以及执行宏替代。预处理器可由语言(如C)要求或以后作为提供额外功能的附加软件。
8.调试器 (Debugger):调试器是一个测试工具。它允许程序员来控制程序的执行,以方便查找程序中的错误。调试程序是可在被编译了的程序中判定执行错误的程序,它也经常与编译器一起放在IDE中。运行一个带有调试程序的程序与直接执行不同,这是因为调试程序保存着所有的或大多数源代码信息,诸如行数、变量名和过程。它还可以在预先指定的位置(称为断点)暂停执行,并提供有关已调用的函数以及变量的当前值的信息。
9.描述器(Profiler):描述器的用途是探查测量程序的每个组件执行所花费的时间和内存资源,从而查出决定程序执行效率的瓶颈所在。描述器是在程序执行中对各个组件所耗费的时间和资源进行搜集和统计。程序员特别感兴趣的统计是每一个过程的调用次数和每一个过程执行时间所占的百分比。这样的统计对于帮助程序员提高程序的执行速度极为有用。有时编译器甚至无需程序员的干涉就可利用描述器的输出来自动改进目标代码。
10.测试工具(Testing Tools):测试工具通过创建和运行自动化软件对目标软件的各方面进行测试并分析测试结果,一般都强调测试过程的自动化和测试项目的系统化。著名的测试工具有Junit,它为Java程序员提供了自动化和系统化测试的框架和工具。
近年来,人们开发了各种集成开发环境将上述工具捆绑在一起,构成一个集成环境,大大提高了软件开发的效率。其中著名的有微软的VisualStudio,支持使用 Visual C++、Visual Basic、C#等语言开发Windows和.Net程序。
编程语言作为人机交流的载体跟自然语言有类似的属性和目的,都有一个与语义分离的语法,并且都有语言的语系分支关系。但是,做为人工构建的语言,编程语言与人类使用过程中不断进化的自然语言有根本的区别,不同之处在于表达的精确度和完整性。程序设计语言可以被充分地描述,并可以整体研究,因为它具有一个精确而有限的定义。[5]
相比之下,自然语言的含意随着使用者和使用环境而改变。当使用自然语言与人沟通,即使作者和演讲者表达不那么明确,或偶尔出点错误,他们的意图仍然可以被多数人理解。然而计算机只能严格执行程序员所写的代码,而不能理解程序员所写代码的意图,也不能跳过程序中的错误。
人们已经创造了数以千计各种各样的编程语言。许多编程语言都是从零开始设计的,然后被改进以适应新的需求,并加入其他语言的功能和特点。还有许多编程语言最终无人使用而被遗忘。那么为什么不创造一种能够包括所有功能和实现所有目标的通用万能的编程语言?理论上这一想法似乎可行,曾经也有人尝试过,但到目前为止从未成功过。原因很多,其中之一可能是使用环境。使用环境的多样性自然而然地造成了对不同编程语言的需要。有些程序很小可以由爱好者个人编写,而有些大系统需要数百程序员的集体努力。新手程序员喜欢简单高于一切,而编程老手可以坦然面对相当大的复杂性。从微控制器到超级计算机,不同的系统有不同的速度和大小,写出的程序必须在这些因素里找到平衡。有的程序可以写一次,以后几代人都可以不加改变地继续使用,而有些程序必须不断地修改。再者,不同的程序员有不同喜好,他们可能习惯于用一种特定的语言来讨论问题和表达见解。也有人提出设计自然语言处理器来消除对专门编程语言的需要,然而目前还停留在讨论阶段,这个目标看起来仍然遥远。[1]
在已经问世的数以千计不同的编程语言中,还有几百种在不同程度地被使用。为了确定哪一种是最广泛使用的程序设计语言,人们提出了各种测量语言流行程度的方法。比如,可以根据招工网站招聘广告中提到的语言的数量,各种计算机编程书籍的出售数量,各种语言所写代码行数的估计等对各种语言的使用流行程度进行排名。
在各种排行榜中,较有名气的是TIOBE排行榜。[6]它是根据互联网上有经验的程序员、课程和厂商的数量,使用搜索引擎(如 Google、Bing、Yahoo、百度)以及 Wikipedia、Amazon、YouTube统计出排名数据。虽然排名只是反映某个编程语言的热门程度,并不能说明该语言好不好,或者该语言所编写的代码数量多少,但对世界范围内开发语言的走势仍具有重要参考意义。
表1列出了从2003-2013间TIOBE排名前20的语言。从中可以看出,C和Java一直占据头两位,PHP、Python、Javascript等网页编程常用的脚本语言也基本稳居前10位。从90年代中期开始流行的Perl有下滑并跌出前10的趋势。图1展示了10种当前流行的语言在2002—2013年间的在全部语言使用总量中所占比重的变化。结合表1可以看出,虽然C和Java一直稳居前列,但其所占比重却逐年下滑;而新兴语言如C#和Ruby却显示出加速上升的趋势。值得注意的是排名靠前的语言几乎都被著名大公司使用和推广,比如微软的VB,VC++和C#,苹果公司的 Objective-C,Oracle的 Java和PL/SQL等。
表1 2003-2013年TIOBE排名前20的语言
图1 2002-2013年10种常用语言TIOBE排名变化
回顾编程语言的发展历程,可以看出其总体发展趋势是提高语言的抽象层次和表达能力、容易学习和使用以及能支持更灵活的定制,从而提高程序员编写代码的效率,即用更少的代码完成更多的工作。未来编程语言将继续沿着这一大方向发展,具体有以下两方面值得关注。
上世纪50年代后期就有了函数式编程语言Lisp,其后一直停留在学术界,直到最近它才被工业界重视,多种主流语言如C#、Java,甚至C++都相继加入对函数式编程的支持。另外,纯函数式编程语言如Scala、Clojure和Haskell也都站稳了脚跟。函数式编程的最大优点是强大的抽象能力,因为它隐藏了许多操作细节(比如迭代),而它曾经的缺点(占用消耗大量CPU和内存)在计算机硬件高速发展的今天已经显得不那么突出,这就是它逐渐获得重视的原因。混合语言如Scala或F#有可能成为主流语言。
对很多程序员来说,并发编程是一个痛苦的差事。目前CPU发展的总体趋势是强调多核。正如微软高级技术员Anders?Heljsberg所讲,多核革命的一个有趣之处在于它会要求并发的思维方式有所改变。传统的并发思维,是在单个CPU上执行多个逻辑任务,使用旧有的分时方式或是时间片模型来执行多个任务。但是如今的并发场景则正好相反,是要将一个逻辑上的任务放在多个CPU上执行。这改变了编写程序的方式,意味着对于编程语言而言,需要有办法来分解任务,把它拆分成多个小任务后独立地执行,而传统的编程语言中并不关注这一点。对多核环境下并行编程的支持将是近期发展的热点之一。