计算机概论 - 程式语言 Programming Languages

如果程式都必须以机械语言撰写,那么现在複杂的程式系统发展,如作业系统、网路软体和市面上各种应用软体都不可能实现,因为以机器语言来署理这些複杂的程式细节同时又要组织複杂的系统是非常困难的,因此许多程式语言的诞生让演算法的表达式不仅可以让人容易阅读也可以轻易地转换成机器语言指令,本章将探讨计算机科学领域中关于设计与实作这些程式语言的相关议题。

img

历史观

首先先介绍程式语言发整的一些过往与历史。

早期程式语言

以机器语言撰写程式码是一件非常繁琐的工作且很容易发生错误,以致于需要找出并更正这些错误,这个程序就是俗称的除错 (debugging)

在 1940 年代研究者发展了标记法用来简化程式设计的流程,让指令可以以通俗的名称表示,常使用的描述性名称 Price, ShippingCharge, TotalCost 通常称为程式变数识别符号 (identifier),而有了这些通俗的形式后便发展出了组译器 (assembler) 它可以将通俗的表示法转换成机械语言指令,而这种通俗表达程式的方式称为组合语言 (assembly language),他被认为是第二代程式语言,第一在则是机器语言。

虽然使用组合语言可以比机械语言开发来的简单,但还是需要程式设计师被迫以机器语言的角度思考,后来的设计者觉得最终产品会使用到的基本原是不需要在设计产品的阶段就使用,设计过程应该使用更高级的原式,一但这些高级的原式设计完成后就能转以为较低等级的语言或是概念,于是第三代程式语言就诞生了,他使用更高阶的原式且与机械无关 (machime independent) 亦即他们不依赖特定机器架构。

要发展好这些高阶语言就需要一种称为转译器 (translator) 的程式将高阶原式转换成机器语言程式,他与第二代语言的组译器很像不同的是转译器经常需要将数个机器指令编译成段序列以还原单一高阶原式所要求的操作,因此这些转译器经常被称为编译器 (compiler)

有一种编译器称为直译器 (interpreter) 是另一种实作第三代语言的方法,与转译器很相似但直译器是在程式执行的时候进行转译,并不会另外与转译的结果一样储存起来以便日后使用,与其产生一份机械语言版本以供日后执行,直译器可以让高阶程式直接执行。

程式设计方法

程式语言会随着不同的程式设计方法 (programming paradigm) 问世而有不同的发展路径,比较常见的程式语言发展路径分别为函数式, 物件导向式, 命令式, 宣告式等等。

img

命令式程式法 (imperative paradigm)

也称为程序式程式法 (procedural paradiam),代表传统程式设计的方法,顾名思义命令式程式法是发展一系列的命令,用来处理资料以产生期望的结果,所以命令式程式法的程式设计过程是找出能解题的演算法,并将该演算法转换为一系列的命令陈述。

宣告式程式法 (declarative paradigm)

宣告式程式法 是要求程式设计师在程式中描述需要解决的问题而非演算法,準确来说宣告式程式法使用预先建置好的通用解题演算法来解决程式设计师所描述的问题,因此程式设计师的任务就是精準的描述问题而不需要描述解决此问题的演算法。

函数式程式法 (functional paradigm)

程式被视为能接受输入并产生一个实体,数学家称这样的实体为函数 (function),在此法下程式是藉由连结数个预先定义好的程式单元(预先定义好的函数)所构成,每个城市单元的输出可作为其他城程式单元的输入,简而言之函数式程式法的程式开发过程就是以简地的函数构建出複杂的函数,某种意义上来说函数式程式就好比一群协调一致的工厂,每间工厂只负责生产其他工厂所预定的产品,生产出的产品会立即送往需要的工厂,不需要中途储存的仓库。

物件导向式程式法 (object-oriented paradigm)

物件导向式程式法与之相关联的程式设计称为物件导向程式设计 (object-oriented programming, OOP),这种程式法一个软体系统被视为一群物件 (object)的组合,每个物件都能执行与自身相关的功能以及请求其他物件的功能,对于物件特性的描述称为类别 (class),一但建立了类别就能在任何有需要的时候使用该类别建立带有这些特性的物件,所以可以建立数个物件都基于相同类别,他们虽然是不同实体但却有相同的特性,因为他们建立于同一个模板,由某个特定的类别所创立的物件,称为该物件为此类别的实例 (instance)


传统程式设计概念

在本章中将探讨命令式程式语言物件导向程式语言的一些概念,选用了一些较为普遍或经典的程式语言进行介绍,C 语言是一种第三代程式语言,C++ 是延伸 C 语言而发展出来的物件导向程式语言JavaC# 是衍伸于 C++ 的物件导向式程式语言。

一般来说程式包含着一群陈述,这些陈述分为三大类,宣告式陈述 (declarative statement) 定义了一些自定的名称已变成是后续使用,命令式陈述 (imperative statement) 描述所有的演算法步骤,注解 (comment) 是以更通俗的方式说明程式中难以理解的细节以增加程式的可读性。

变数与资料型态

高阶程式语言使用描述性的名称来代表资料于主记忆体中的位置而不是用数字来表示,这样的名称称为变数 (variable)

命令式程式语言有个分支称为脚本式程式语言 (scripting language) 一般是用来执行一个管理层次的任务,并非用来发展複杂的程式,这种程式的表示型态称为一个脚本 (srcipt)

资料型别 (data type) 代表资料元素使用的编码方式以及资料能够进行什么样的运算,,比如整数 (integer) 资料型态可以处理整数类的资料,字串 (string) 则是处理文字类型的资料。

资料型别包含在程式语言的原式中,比如 int 代表整数型别,char 代表字元型别,这些资料型别称为基本资料型别 (primitive data type)

资料结构

除了资料型别之外,有些变数也会连接到资料结构 (data structure),资料结构是资料的意向或资料排列的方式。

阵列 (array) 是一种常见的资料型态,是一组具有相同型别的元素所组成,一但阵列宣告后就可以在程式中以其名称表示,或个别的阵列元素项目可以使用索引值 (indices) 来指定特定行列以做为识别。

相较阵列的所有资料项目是同的类型,一种聚合类型 (aggregate type) 是一组具有不同型别的元素集合,他也被称为 structure, record异质阵列 (heterogeneous array),在 C 语言中宣告如下:

struct {    char Name[25];    int Age;    float SkillRating;} Employee;

在电脑的储存空间中,资料结构的真实排列方式可能与意象的排列方式有很大的不同。

img

常数与定数

有时候程式会需要用到某个固定且预设好的数值,像这样明确的数值在程式中称为定数 (literal),在程式陈述式中使用定数

EffectiveAlt = Altimeter + 645;

其中 EffectiveAlt 与 Altimeter 是变数而 645 则是一个定数,而上面的式子也可以称为一个运算式 (expression)

不过如果是一个多人开发的大型专案的话,可能除了你之外其他所有人都不会知道 645 是什么东西,为了解决这个问题程式语言允许使用否过名称只定为不会变动的某数值,这种就称为常数 (constant),比如

const int AirportAlt = 645;

这样代表名称 AirportAlt 是个具有固定数值的 645 的东西。

指定陈述

一但某个特殊名称要被宣告在程式中使用时,就可以开始使用命令陈述来为这个变数指定一个值(精準点是说,指定给该变数所代表的记忆体位置一个值),这种基本的命令陈述称为指定陈述 (assignment statement),指定陈述句的重心在等号的右半部。

控制陈述

控制陈述 (control statement) 可改变程式的执行顺序,比如最受争议的 goto 陈述,他可以让程式的执行跳到任意指定位置。

注解

无论一个程式被设计得多好多巧妙,当有人需要详细研究这份程式时,一些附加的讯息会对这个研究的过程非常有帮助,因此程式设计师可以在程式中添加一些说明,称为注解 (comment),这些注解会被解释器所略过,因此以电脑的角度来说有没有写注解是完全不影响程式运行。


程序单元

在前几章中介绍了将大型程式分割为小单元的好处,在本章终将着重介绍函数的概念,函数是在命令式程式语言让程式模组化的主要方式。

函数

函数 (function) 是执行某个特定任务的一群指令,使用时控制权会转移到函数上,等函数完成后再将控制权交回到原来的程式单元,转移控制权给函数的过程称为呼叫 (calling)调用 (invoking),而请求执行函数的程式单元称为呼叫单元 (calling unit)

函数通常是作为个别的程式单元,这个程式单元一开始的陈述称为函数的标头 (header) 亦即函数的名称,而通常在函数内部所宣告的变数称为区域变数 (local variable),意味着这个变数只能在这个函数内部使用,这样可以避免函数外洩或函数名称重複的问题,变数在程式中有效的使用範围称为範畴 (scope),有兴趣的话可以看看我写的 You Don't Know JavaScript [Scope & Closures] - What is Scope? 会提到一些关于 Scope 的内容。

img

参数

通常函数会使用一般性方式转写,等到函数真正执行时才会比较具体,如果想要使用某个函数的话则需要将某变数指向函数指定的内容,这个指向函数内容的东西称为参数 (parameter),更精确的说在函数中所使用的名称为形式参数 (formal parameter) 而函数执行时指定给这些形式参数实际名称的称为实际参数 (actual parameter)

实际参数与形式参数之间进行资料传递的任务在不同程式语言中有不同的方式,有些程式语言会複製一份实际参数给函数使用,使用这种方式的话函数中资料的任何异动都只会影响到这一份複製的内容,而呼叫单元的资料不会有改变,这种方式称为传值 (passed by value)

不过如果当参数是一组大量资料时传值参数的效率就会很不好,比较有效的方式是让函数直接存取实际参数,让呼叫单元告诉函数实际参数在主记忆体中的位置,这种方式称为传址 (passed vy reference),这种传递地址的方式由于是直接操作地址中的内容,所以在函式中的异动会随之影响到呼叫单元的内容。

Passed by value
img

Passed by reference
img

事件驱动软体系统

在某些情况下函数可以透过事件的发生来启动,比如说在 GUI 中点击某个按钮时,函数不是透过其他程式单元的呼叫而是因为按键被点击而启动执行,函数经由事件的发生而启动执行的软体系统称为事件驱动 (event-driven) 系统,简而言之事件驱动软体系统描述不同事件触发时会发生的结果,当系统执行时函数是静止的直到相对应的事件发生后函数才会启动,函数启动且执行完成后便会再次回到静止状态。


程式语言实作

在本章中会探讨高阶程式语言所撰写的程式被转译成机器可执行的形式过程

转译过程

将某一种程式语言所撰写的程式转换为另一种语言的过程就称为转译 (translation),原本的程式称为原始程式 (source program) 而转译过的程式目的程式 (object program),转译过程包含三个步骤 - 词法解析 (lexical analysis), 语法解析 (parsing)程式产生 (code generation),并由转译器里面一个称为词法分析器 (lexical analyzer), 语法解析器 (parser)程式生产器 (code generator) 的元件负责执行。

img

词法分析

词法分析是便是原始程式中哪些字串代表单一实体或标记 (token),比如说 153 不能被转译为 1, 53 而应该要被解析为单一数值,因此词法分析汇一个符号接着一个符号的读取程式码,辨识哪些符号构成一个标记并根据标记分类为数值,字词算术运算子等等,词法分析器会对每个标记和他的分类进行编译并交移给语法解析器,在这个过程中注解会全部被跳过。

语法解析

语法解析器以语词单元 (token) 来检视程式而非个别符号,语法解析器将这些语词单元组成陈述,早期的程式语言强调每一句程式码的陈述必须在页面上以特定的方式定位,这种程式语言称为固定格式 (fixed-formal) 语言,现在许多程式都是自由格式 (free-formal)语言,亦即陈述的定位并不重要,因此可以使用缩排来帮助开发者很快的知道陈述的结构因此与其

if Cost < CashOnHand then pay with cash else use credit card

可以把它更改为较好看懂的形式

if Cost < CashOnHand    then pay with cash     else use credit card

大多数自由格式语言使用分号来标示陈述的结束,以及使用关键字 (key word) 比如 if else, then 等等来标示个别陈述的起头,这些关键字通常是保留字 (reserved word),代表他们不能被开发者作为其他变数使用。

解析过程是基于程式语言的语法规则,这些规则整体称为文法 (grammar),描述这些规则的方式之一是使用语法图 (syntax diagram),这是一种以图形表示程式语言的文法结构。

img

上面的语法图说明了 python 的 if-else 陈述,要注意的是在这个语法图中实际出现的专有名词是以椭圆表示,而需要进一部描述的地方则使用矩形比如布林运式或缩排陈述句,这些需要进一部描述的地方称为非终端点 (nonterminal),而已椭圆表示的称为终端点 (terminal),而特殊字串对应到语法图的方法可以使用解析树 (parse tree) 来表达。

img

解析程式的过程基本上就是建构城市解析树的过程,事实上解析树代表程式文法组成的解析结果,因此描述程式文法结构的语法规则不能允许同一个字串有两种不同的解析树因为会造成语法解析器的混淆。

程式产生

转译过程的最后一个步骤就是程式产生 (code generation),这是建构出机械语言指令以实作解析器辨识的陈述程序,在这个程序中有一个重要的任务那就是产生有效率的机器语言指令,比如

x = y + z;w = x + z;

如果这两个陈述个别转译成机器语言指令,在执行加法前每陈述都需要将资料从主记忆体传送到 CPU 中,但如果第一个陈述已经执行完毕并把 x 与 z 的内容存于 CPU 的通用暂存器中,这样当要进行第二次加法运算前就可以将资料从记忆体中载入,这样洞悉陈述以实作机器指令的方式称为程式码最佳化 (code optimization)

最后要注意的是词法解析 (lexical analysis), 语法解析 (parsing)程式产生 (code generation) 并没有固定的步骤也没有强制的顺序,在程式转译的过程中这些动作会互相交织再一起。


平行化程序的程式设计

让多个程式同时执行就称为平行化处理 (parallel processing)并行处理 (concurrent processing),真正的平行话处例需要多个 CPU 核心,每个核心都用来执行一个程式,不过如果只有一个 CPU 的话就会像前面提到的利用处理器分时的方式产生平行话处理的错觉。

每种程式语言从自身角度诠释平行化处理的方式造成不同的用语,比如 Ada 同时执行多个动作需要多个 task,在 Java 中启动多个程式称为 thread,不管是哪一种其结果类似于多工作业系统控制的程序,产生多个工作同时执行的效果,本章会採用 Java 的用语以 thread 来介绍。

传通呼叫函数执行时呼叫程式单元只有在函数终止后才能继续动作,而在平行化处理下呼叫函数执行后呼叫单元可以同时继续执行,

img

平行化处理中比较複杂的问题是要处理 thread 之间的沟通,这种沟通的需求一直都是计算机科学研究的主题之一,有一种方式是每次只允许一个 thread 存取的资料被称为互斥存取,而实作互斥存取的方式之一是写一个包含 thread 描述的程式元件,当有一个 thread 在存取资料时他会阻挡其他 thread 进行资料存取直到该 thread 结束存取任务,不过在过往的经验中这种方式会造成控制互斥的程式码遍布程式中,一个不小心就会让整个系统垮掉基于这个原因许多人认为比较好的方式是共享资料的情况下可以控制本身的存取,简而言之就是与其仰赖 thread 来避免资料的多重存取倒不如让资料自己控制,因此存去控制就能集中在程式的单一位置而不会扩散到整个程式中,能够自我控制存取的资料称为监控器 (monitor)

Reference

computer science - AN OVERVIEW

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章