购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.5 程序语言的控制结构

程序设计语言的控制结构提供了一个将操作和数据组合成程序和程序组的基本框架,考虑的是如何组织数据和操作,使其成为一个可执行的程序,这包括两个方面的内容,一是对操作执行次序的控制,我们称其为顺序控制;二是对程序中的过程间数据传递的控制,我们称其为数据控制。

控制结构可以简单地分为3类。

● 表达式是程序语句的基本组成,体现了程序控制和数据改变的方法。

● 用在语句间或一组语句中的结构,如条件语句和循环语句。

● 过程结构,如过程调用和协同程序。

2.5.1 表达式

1.表达式的前缀、后缀、中缀表示法
1)前缀表示法

在前缀表示法中,是以从左到右的顺序先写操作符后写操作数,如果操作数本身是一个具有操作数的操作,则对其使用同样的规则。例如,公式 (a+b)(a-b) ,使用前缀表达式表示将变为* + a b – a b。函数调用可以看做一种前缀表达式,因为一般是把操作符函数名写在它的参数的左边,像f(a, b)这样。

以前缀表示的表达式可以在一次扫描后计算出值,前提是要明确知道每个操作符的参数的数目。用以下的算法通过使用一个执行栈,计算给定的前缀表达式P。

如果P的下一项是一个操作符,将它压入栈,并把参数计数器设置为该操作符所需要的参数的数目 n

● 如果P的下一项是一个操作数,把它压入栈。

● 如果栈顶操作数的个数为 n ,则把操作符作用于这 n 个操作数,得出的结果替换该操作符和它所有的参数,作为操作数压入栈。

前缀表达式的计算方法意味着在每个操作数压入栈后都必须检查操作数的数目是否满足最近栈顶的操作符的要求,而后缀表达式就无须做这种检查。

2)后缀表示法

后缀表示法类似于前缀表示法,不同之处在于操作符跟在操作数之后。前面的公式使用后缀表示法时表示为a b + a b– * 。由于这一点不同,在计算后缀表达式的时候,当扫描到操作符时,栈中已压入了它的操作数。因此计算后缀表达式的算法如下。

● 如果P的下一项是操作数,将它压入栈。

● 如果P的下一项是 n 元操作符(即需要参数个数为 n 的操作符),那么它的参数就是栈顶的 n 项,把该操作符作用于这 n 项,得到的结果作为操作数替代栈顶的 n 项。

由于后缀表达式的计算是直接的,并且易于实现,因此它是很多翻译器产生表达式代码的基础。

3)中缀表示法

中缀表示法是日常最通用的用法,但是在程序语言中使用中缀表示法会产生下面的问题。

● 由于中缀表示法仅适合于二元操作符,一种语言不能只使用中缀表示法,而必须结合中缀与前缀表示法。这种混合使用会使翻译过程相对复杂。

● 当一个以上的中缀操作符出现在表达式中时,如果不使用括号就有可能产生二义性。

考虑表达式10–2×5的值,我们会认为其值是0,而这仅仅是因为我们已经习惯把一个隐含规则应用于表达式的求值,这就是先乘除后加减。若我们把规则定义为先加减后乘除,则表达式值应为40。括号可以消除任何表达式的二义性,但在复杂的表达式中,将会导致深层次的括号嵌套而产生混乱。因此,程序语言通常都引入隐含的控制规则,使得大多数的括号的使用成为不必要,这就是操作符的优先级和结合性。

2.操作符的优先级和结合性

操作符的优先规则是指可以出现在表达式中的操作符的优先次序,操作符在该次序中的级别就是该操作符的优先级。在有处于一个以上优先级的操作符的表达式中,具有较高优先级的操作符先执行。

结合性规定了相同等级的多个操作符的操作次序。例如,在a–b–c中,应是第一个减法还是第二个减法先完成呢?通常的隐含规则是从左到右的结合。因此,a–b–c被看做(a–b)–c。如表2-6所示是C语言中的操作符的优先级和结合性。

表2-6 C语言中的操作符的优先级和结合性

由于程序员都知道表达式语义的基本数学模型,因此,通常的算法表达式都有良好的合理性。但是不少语言都以不同的方式扩充了操作符集合,优先性常常会被破坏。

2.5.2 语句间的顺序控制

表达式的顺序控制是把操作数和操作符看做基本单位,研究操作符的计算顺序。而语句间的顺序控制是把程序语句作为基本单位,研究语句执行的顺序。程序语言对语句的执行都遵循一条隐含规则,这就是在没有其他顺序控制结构规定的情况下,按照语句在程序中的物理位置执行程序,也就是顺序执行。改变这种语句执行次序的方法是使用程序顺序控制结构,这些控制结构有跳转结构、选择结构和循环结构。

1.跳转结构

跳转结构就是令程序控制无条件地从当前语句转向给定的语句执行的控制结构,跳转语句的执行非常有效,它反映了计算机本身硬件的转移指令,如x86指令中的jmp指令。通常的跳转语句都有如下形式:goto<标号>,Fortran和C语言等都提供了goto语句。当程序控制遇到goto语句时,会转移到标号所指出的相应语句继续执行。

虽然goto语句的使用十分简单和高效,但是大量的使用会令程序控制逻辑混乱,程序变得难以理解和维护。人们已经证明可以使用顺序结构、选择结构和循环结构组成任何程序,而抛弃掉“有害的”goto语句。目前比较一致的观点是,程序员必须谨慎地使用goto语句,使用时必须考虑是否可以用更好的结构来代替。

2.选择结构

选择结构是对给定条件进行判断,然后根据结果执行不同的语句或语句块的结构。最典型的选择结构的形式如下:

意味着如果expr条件为真,则执行statements1语句块,否则执行statements2语句块。在某些复杂的情况下,需要对多个条件进行判断,则if-then-else语句会进一步复杂,演化为if-then-elseif-then-else等。

在两个分支的选择结构基础上,多数语言也会提供多分支的选择结构,它在许多情况下可以改善程序的可读性。典型的多分支选择结构如下:

虽然case控制结构的功能可以由if-then-else结构来模拟,但是case控制结构能提供更清晰的计算过程的反映。

C/C++的情况比较特别,在case结构中使用break语句表示跳出结构的控制,如果在其中一个case中没有使用break语句,则控制会顺序执行至下一个case中的语句。这个特点在为程序员带来方便的同时,也为程序员带来了麻烦。程序员疏忽漏掉的break语句会导致程序有意想不到的执行结构。因此,在C#中不允许这种case的“贯穿”,而强制程序员使用goto语句跳转至相应的case标号,以保证程序员清楚地知道程序控制的行为。

3.循环结构

循环结构是根据条件重复执行指定语句的控制结构。循环结构是由循环头和循环体组成的。循环头就是循环的条件,用于控制循环的次数,循环体则是提供动作的语句。典型的循环头结构有以下几种。

1)计数器循环

这种结构需要说明一个循环计数器,并且在头部说明计数器的初值、终值和增量。典型的计数器循环的结构是Pascal的计数器循环:

该循环的头部说明了计数器为I,其初值为0,终值为30,增量为2,循环的执行次数为16次。

2)条件循环

条件循环是指在给出的条件表达式成立时,重复执行循环体的循环结构,它的头部说明了该条件表达式。这种循环期望在循环体执行的时候会改变条件测试表达式中的某个变量的值,否则循环将永不终止。典型的条件循环的结构有两种,一种是:

另一种是:

前者是先测试条件,然后执行循环体,循环体执行零次或零次以上。而后者是先执行循环体,再测试条件,循环体执行一次或以上。

3)基于数据的循环

基于数据的循环的循环次数是由数据格式决定的,例如,C#中的foreach结构:

对于每一次循环,变量o都会取得数据集中的下一个值,数据集元素的个数决定了循环的次数。

4)不定循环

如果循环结束条件过于复杂,不容易在头部表示,通常会使用在循环头部没有显示终止测试的无限循环,然后在循环体中通过条件判断退出循环。C/C++中有两种典型的不定循环结构,一是:for (;;)<statements>,二是:while(1) <statements>。

2.5.3 过程控制

1.过程简介

在程序设计中,习惯把程序看做是层次结构,程序从主程序开始执行,然后进入各层次的过程执行,到最后返回主程序结束。过程为程序员提供了一种抽象手段,其实际上是一组输入到一组输出的映射。

过程通常有四个要素,分别为过程名,过程体,形式参数列表,返回值类型。例如C语言中的函数(即C语言中的过程)如下:

其中Function1为函数名,(int x, int y)为形式参数列表,int是返回值类型。

2.参数传递方式

当用户调用一个过程时,就会发生通过参数传递信息的过程之间的通信。形式参数就是过程定义中用于命名所传递的数据或其他信息的标识符,而实际参数是在调用点表示向被调用过程传递的数据或其他信息的表达式。在大多数的语言中,形式参数和实际参数之间的对应关系通常按位置来确定。程序语言传递参数的方式通常有传值调用、引用调用和传值–结果调用。

1)传值调用

在按传值调用时,过程的形式参数取得的是实际参数的值。在这种情况下,形式参数实际上是过程中的局部量,其值的改变不会导致调用点所传送的实际参数的值发生改变,也就是说数据的传送是单向的。在C语言中只有按值调用的过程参数传递方式。

2)引用调用

在按引用调用时,过程的形式参数取得的是实际参数所在单元的地址。在过程中,对该形式参数的引用相当于对实际参数所在的存储单元的地址引用。任何改变形式参数值的操作会反映在该存储单元中,也就是反映在实际参数中,因此,数据的传送是双向的。C++语言既支持按值调用,也支持按引用调用。

3)传值–结果调用

传值–结果调用也称为拷入/拷出,因为初始时实际参数被拷贝到形式参数,而在过程调用结束时再把形式参数拷贝回实际参数。

3.标识符的作用域规则与活动记录

在程序设计语言中,标识符可以用于标识任何东西。某个标识符的声明为其赋予了一个新的含义,而标识符的作用域规则决定了当程序中遇到x时,适用的是哪一个声明。

作用域规则分为词法作用域规则和动态作用域规则。在词法作用域规则下,标识符出现和声明之间的约束可以在编译时静态完成,对语言的所有程序都可以做好。词法作用域规则也被称为静态作用域规则。在动态作用域规则下,名字出现与声明的约束是动态建立的,在运行中确定。

与标识符作用域的概念紧密关联的是运行时刻存储管理问题。存储管理指的是目标程序在运行时对内存的使用和再使用的方法。每个过程的活动都需要具备变量的存储,与过程的每个活动相关联的是一块为过程中声明的变量而用的存储,这一存储块被称为活动记录。活动记录通常要包含以下几部分信息:

● 控制链

● 访问链

● 调用点机器状态,如返回地址和寄存器值

● 参数

● 返回值

● 局部变量区

1)活动及其局部数据

不同的活动需要对形式参数和局部变量有不同的存储,因此,不同的活动需要有不同的活动记录,在允许递归过程的语言中,每个活动都必须有自己的活动记录。过程中的声明促使为该过程的活动记录分配存储空间。一个过程的所有活动记录布局都相同。变量的布局由其类型决定,基本数据类型用单个的存储位置,而数组则需要一系列的存储位置。

2)控制与访问链

通过在活动记录之间维护两条链的方式对它们进行管理。

● 控制链,也称为动态链,指向运行时调用者的活动记录。

● 访问链,也称为静态链,用于实现采用词法作用域的语言,指向嵌套着该过程的外层过程的活动记录。

我们使用一个下推栈来存储活动记录的信息。每次过程调用时,就生成该过程的一个活动记录,然后下推入栈,让过程的每次调用与活动记录相对应。过程执行结束就把栈顶的活动记录弹出。为了使过程能在运行时访问本次活动记录中的数据,我们设立一个指针SP,始终指向当前正在执行的过程的活动记录的某一特定单元,那么过程访问本次执行的活动记录中的数据就可以通过SP+数据偏移量来进行。

过程执行结束后,程序流通过活动记录中的控制链寻找到调用者的活动记录,把SP值设置到相应活动记录的特定单元。为了程序能够在调用点正确执行下去,在过程执行结束时还必须根据活动记录中保留的内容,恢复调用点的机器状态。

当过程需要访问使用标识符表示的外部或全局变量时,可以通过访问链,逐层地回溯至相应的外层过程的活动记录,根据变量的偏移量找出需要访问的标识符所对应的变量。例如,在下面的Pascal程序中:

设现在运行至过程p中的a:=c+d一句,则单看p1与p2的活动记录,p2活动记录的控制链指向p1的活动记录,访问链也是指向p1的活动记录。而p1活动记录的控制链指向调用p1的过程的活动记录,而访问链则指向全局记录。当p2需要访问变量c时,程序源访问链回溯至p1的活动记录,根据c的偏移找出局部变量,然后取得其值。

我们将变量在过程中的偏移地址和该变量在访问链中所属的层(称为静态层)保存在符号表的登记项中,以便在代码生成阶段正确形成地址。 0ga1q5hyx+moCoOPgbrRx40HPWsxP7IIBkqDLsNuoWLh1ZLbRxLX5Ph8e22UoN0Z

点击中间区域
呼出菜单
上一章
目录
下一章
×