计算机概论 - 程序语言 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


<<:  Python & Celery 学习笔记_周期任务 (Beat)

>>:  10. STM32-SPI介绍

Dungeon Mizarka 023

这几天可能不会有什麽进展,今天先将之前试验建置VS的部份做个整理。整理有关於Code strippi...

[第16天]理财达人Mx. Ada-已实现损益

前言 本文说明帐户已实现损益资讯。 程序实作 程序 # 特定时间已实现损益 profit_loss ...

Day 11 - 自订事件

昨天我们介绍了props的用法,要注意的是,props是单向数据流,所以只能从上传到下,要由下传到上...

EP 12: Implement and Use a Custom ValueConveter

Hello, 各位 iT邦帮忙 的粉丝们大家好~~~ 本篇是 Re: 从零开始用 Xamarin 技...

[day23]加入购物车 & 库存检查

简单设计一个库存与订单设计,用白话一点来说就是推一台购物车,购物车上可以放上各种商品,推去结帐时这台...