天天看点

语言设计之控制流

  顺序机制分为八个主要类别:顺序执行、选择、迭代、过程抽象、递归、并发、异常处理和推断、非确定性。

  迭代:反复执行一段给定代码,或者是执行一定次数,或者是执行到某个运行时条件成立。迭代结构包括for/do、while/repeat循环。

  1 表达式求值

  表达式:一个表达式或者是一个简单对象,或者是应用于一组运算对象或参数的一个运算符或函数,而这些运算对象和参数也是表达式。

  1.1 优先级和结合性

  在任何特定语言中,在各种不同求职顺序之间的选择,依赖于运算符的优先级和结合性。在前缀和后缀记法中没有优先级和结合性的问题。多使用括号吧,少年!

  1.2 赋值

  命令式语言中的计算通常是通过对内存中变量值的顺序的一系列修改而完成的。赋值提供了完成这种修改的最基本手段。

  一般而言,如果程序设计语言中的一种结构除了返回由外围上下文所用的值之外,还能以其他方式影响后续计算,我们就说这种结构有副作用。赋值也许是最基本的副作用:虽然有时对赋值的求值过程本身也许会得到一个值,但是我们真正关心的却是它改变了一个变量的值,从而会影响任何用到该变量的后续计算的结果。

  大多数命令式语言都严格区分表达式和语句。前者总产生一个,但可能有也可能没有副作用;而后者的执行就是为了产生副作用,并不返回任何有用的值。

  变量的值模型和引用模型,前者认为变量是值的命名容器,后者认为变量是对值的命名引用。

  1.3 初始化

  由于已经提供了一种为变量设置值的结构(赋值语句),因此并不是所有的命令式语言中都提供在声明变量时指定初始值的方式。提供初始值的机制是有益处的:1)子程序的局部静态变量需要一个初始值;2)对于静态分配的变量,在声明的上下文中指定初始值可以由编译器放入全局内存,从而避免了在运行时赋予初始值造成的开销;3)意外的使用未初始化的的变量,是一种最常见的程序设计错误。

  初始化只是对那些静态分配的变量节约时间,对运行中在栈和堆中分配的变量只能等到运行时进行初始化。使用未初始化变量的问题不仅出现在变量加工之后,也可能出现在变量原有值被破坏,但却没有为它提供新值的操作之后。

  如果声明的变量没有明确的被赋予初始值,有些语言也可以指定一个默认(在某些场景下)。

  语言实现可以对未初始化变量的使用作为动态语义错误,在运行时捕捉这种错误。优点是能够找出程序中的错误,这些错误可能由于存在默认值而被掩盖或者纠正了。不幸的是,在大多数机器上,对于大多数语言而言,在运行时捕捉所有使用未初始化变量情况的代价非常的高。动态检查需要给每一个值分配额外空间记录是否初始化,每次加工变量,修改对应的初始化标识。

  C++小心地区分了初始化和赋值。它将初始化解释为调用变量所属类型的构造函数,以初始值作为参数。在没有强制的情况下,赋值被解释为调用相关类型的赋值运算符,而如果没有定义这种运算符,则将赋值操作右部的值简单的按位赋值过来。对于需要执行特定的储存管理操作的用户定义的抽象类型而言,区分初始化和赋值尤为重要。例如长度可变的字符串,赋值在为新枝分配空间之前释放该字符串的原值所占的空间。初始化只需要分配空间。

  1.4 表达式中的顺序问题

  虽然优先级和结合性规则定义了二元中缀运算符的应用顺序,但它们没有明确说明给定运算符的各个运算对象的求值顺序。(参数的求值顺序也是不确定的)

  求值顺序的重要性的原因:1)副作用:如果其中一个运算对象会修改另一个运算对象的值,二者求值的先后顺序影响表达式的值。 2)代码改进:子表达式的求值顺序对寄存器的分配及指令的次序安排都有影响。在表达式a*b + f(c)中,或许希望先调用f,因为如果先计算a*b,那么在调用f期间就需要保存这个乘积,而f也许希望使用所有的寄存器。

  1.5 短路求值(骤死性)

  针对布尔表达式,当后续表达式的结果不影响整个表达式的结果时不计算后续表达式的值,在某些情况下可以节约大量的时间。

  2 结构化和非结构化的流程

  goto被抛弃了,结构化的控制流语句可以完全替代goto的功能,而且使得代码更加简洁美观。

  3 顺序执行

  除非那些不提供返回值的子表达式有副作用,否则这种顺序执行是没有任何作用的。

  即使在命令式语言中,人们对某些副作用的价值也有争论。无副作用的性质保证了该语言的函数是幂等的:如果使用同一组参数重复的调用,则总会返回同样的值。此外,一个子表达式中的函数调用不会对其他子表达式中的函数调用不会对其他子表达式的求值结果造成影响。可惜的是有些情况下人们特别希望函数可以有副作用,比如伪随机数生成器的典型借口,用户希望每次调用时返回不同的值。

  4 选择

  if else;

  条件是一个布尔表达式,但通常没有必要求出这个表示的值存入寄存器。大多数机器都提供了条件分支指令,用于利用简单比较的结果。

  case/switch语句版本确实比对应的if/else版本更简短一些,但还不是程序设计语言提供case语句的最主要动力。最主要的动力是为了生成效率更高的目标代码。对于case语句,我们并不是顺序地用其表达式与一系列可能的值比较,而是要计算出一个地址,以便通过一条跳转指令转调过来。在汇编层面生层转跳表(地址表)。

  线性跳转表的速度很快。当整个跳转表标号密集且不包含很大的区间时,这种方式很有效。计算分支地址其他方式包括顺序检查,散列和折半查找等。

  5 迭代

  迭代和递归是使计算机能够重复执行一系列类似操作的两种机制。命令式语言的程序员更倾向于使用迭代而不是递归。大多数语言中的迭代都以循环的形式出现。循环有两种变种:枚举控制的循环、逻辑控制的循环。

  6 递归

  有时有人说迭代比递归效率更高。更准确的说法应该是,迭代的朴素实现的效率通常比递归的朴素实现高一些。如果递归的实现确实通过实实在在的子程序调用,在运行栈商为局部变量和簿记信息分配空间,那么迭代实现的效率就会比递归高一些。然而一个“优化”编译器,特别是专门为函数式语言设计的编译器,常常能为递归生成优异的代码,尤其是为尾递归生成优异的代码。尾递归:在递归调用之后再也没有其他计算的函数,其返回值就是递归调用的返回值。即使是那些非尾递归的函数,通常通过很简单的变换就可以产生尾递归代码。