Home Map Index Search News Archives Links About LF
[Top bar]
[Bottom bar]
[Photo of the Author]
by Emre Demiralp

About the author:

我是伊斯坦布尔美洲罗伯特学院的一名学生,同时也是伊斯坦布尔理工大学科学艺术系计算机实验室的管理员之一。这些实验室的主流操作系统是LINUX。兴趣: PovRay,PostScript,动画,CD设计,编程,全息摄影,等等。1994年起成为LINUX用户。

Content:

  1. 引言
  2. 数组及数组操作符
  3. 字符和变量
  4. 循环
  5. 过程/宏定义
  6. 练习

Postscript之三:PostScript中的操作堆栈:定义数组、变量、循环及宏

[Ilustration]

Abstract:

作者进一步阐述 PostScripe 语言中的操作堆栈。本文将详细的讲述数组和数组操作符的定义、变量的定义以及循环和宏的定义,并配有示例和习题。下一篇文章将给出这些习题的答案。关于堆栈操作的介绍还没有结束,作者将在以后的文章里继续讨论这个内容。



 

引言

本文是一系列关于 PostScript 文章中的第三篇。这里将继续介绍 PostScript 的操作堆栈,重点放在数组和数组操作符的定义、变量的定义及循环和宏的定义上。作者力图深入浅出的讨论这三个主题。文中还附了一些很有说明性的示例。关于操作堆栈的其他内容将会陆续出现在以后的文章中。

 

数组及数组操作符

前面的几篇文章提及操作堆栈的结构和可以改变其结构的操作符。除了那个用于创建操作堆栈参考点的特殊元素之外,其余存贮在操作堆栈中的元素都是整形的。这个特殊的元素被称作“标记符”(marktype)。栈操作符 cleartomark 和 counttomark 的删除或计数操作就是针对栈顶到这个元素之间所有元素的。毫无疑问,这样可以实现简单分组。当然这不是唯一的分组方式。这使得创建含有多个元素的单实体成为可能。此实体被称作“数组”。PostScript 语言中的“数组操作符”可以用来操作数组中的元素。下面作者将依次详述每个数组操作符并配以示例。

[:在堆栈中创建一个标记符元素。如果与它对应的操作符 ] 没有给出,它就会与 mark 命令扮演相同的角色。尽管在操作堆栈里存在一个参考标记点,跟在这个操作符之后进入操作堆栈的所有元素都将被认为是单个元素。下面的操作解释了 [ 和 mark 命令之间的关系:

GS>[ pstack
-marktype
GS<1>2 3 mark pstack
-marktype-
3
2
-marktype-
GS<4>

]:此操作符对应于上一个操作符 [。在使用此操作符之前,堆栈里面必须含有一个标记符元素。实际上,此操作符被认为是数组的结束标志用以完成数组的创建。由于结尾必须对应于开头,PostScript 在 ] 进入操作堆栈时将会查找它对应的开始操作符 [。如果 [ 不存在,PostScript 将返回出错信息并取消该操作。如果此元素紧跟着 [ 进入操作堆栈,那么将创建一个空数组并作为一个单实体(single entitiy)存贮于操作堆栈中(现存的标记符元素就转变成该数组的一部分,因此出现了一个另一种类型的元素)。请看下面的示例:

GS>[ pstack
-marktype-
GS<1>] pstack
[]
GS<1>

如上的操作后,操作堆栈将只包含一个元素——空数组。一个非空数组可以用 [ 和 ] 之间包含数组的元素来直接创建。在创建时所有这些包括 [、] 和数组元素都要同时输入。示例如下:

GS>[ 1 2 3 ] pstack
[1 2 3]
GS<1>

显而易见,数组被当作了一个单实体而进入操作堆栈中。

我们还可以创建一个指定了元素个数但不指明每个元素内容的数组。下面这个示例创建了一个含有两个 null 元素的数组,其中 null 代表什么也没有:

GS>[ null null ] pstack
[null null]
GS<1>

array:此命令需要带一个整形参数,其带参数的命令格式为:n array。运行此操作后,一个含有 n 个 null 元素的数组将被创建。此命令同 [、]用于定义数组时的功能相同。例如:3 array 和 [ null null null ] 是一样的。它将把它的参数定位在操作堆栈的最顶端。如果在该命令中带有参数,则此参数将被带入操作堆栈中并称为最顶端的元素。如果没有参数,该命令将根据操作堆栈中的最顶端的元素属性来决定其行为。如果此顶端元素为整形,则作为该命令的参数,否则显示不兼容出错信息。

length:此操作符用于求数组元素的个数。null 元素也计算在内。该操作符需要一个数组参数,而且只能是数组参数。这个参数将被当作操作堆栈中的最顶端元素。在此操作完成之后,该参数将从操作堆栈中消失。因此,如果在命令之前给定了一个数组参数,那么在命令正常执行之后这个数组的元素个数将作为操作堆栈的一个元素放置在最顶端。例如:

GS>[ 1 2 3 4 5 ] length pstack
5
GS<1>

如果没有没有给出 length 操作符的参数,那么或者是把操作堆栈的最顶端元素(应该是个数组)作为参数执行 length 操作并把该数组元素替换为此数组的元素个数,如下所示:

GS<1>pstack
[1 2 3 6 7]
GS<1>length pstack
5
GS<1>

或者是由于操作堆栈当前的最顶端元素不是数组而显示出错信息。

get:此操作符需要两个参数,分别为数组和整形数。该操作将从数组中取出指定的元素,而元素的位置由第二个参数给定。第二个参数——指定位置的参数只能是自然数,就是说从零开始的整数。事实上此规则适用于所有的操作符参数。这些命令参数将被移出操作堆栈。而它们的类型必须为自然数。这一点以后将不再强调。下面是一个示例:

GS[1 6 3 0 9] 0 get pstack
1
GS<1>

put:此命令需要三个参数:数组、位置索引和插入元素。此命令用于在数组中插入元素。执行的过程是:从第一个参数中取出数组,然后定位到第二个参数给定的位置,再用第三个参数替换该位置的元素。然而,结果数组不会存贮在操作堆栈中。因此,为了明确的使用 put 操作符,我们可以定义一个数组变量(用 PostScript 术语称作 key )。然后,相应的操作可以在数组变量上进行,其结果存入操作堆栈并从中显示出来。请看下面的示例:

GS>[1 2 3] 0 8 put
GS>

操作堆栈里好象什么也没有发生,却又没有出错信息。事实上,put 命令已经完成了,但是结果并没有存贮到操作堆栈里。如果想得到该结果,我们可以按照如下的方法来做:

GS>/ar [ 1 2 3 ] def
GS>ar 0 8 put
GS>ar pstack
[8 2 3]
GS<1>

这里,第一行是一个数组变量(用 PostScript 术语说就是 key )的定义,变量名是 ar。第二步用 put 操作符(命令)把数组的第一个元素(由位置索引参数 0 来指定的)替换成 8。之后的 ar pstack 命令则把数组变量 ar 的值插入到操作堆栈中并显示其中的内容。我们应该注意本文后面的关于变量定义的内容。此外,在后续的文章里还将涉及到词典(dictionaries)及词典堆栈(dictionary stack)。

getinterval:此操作符用于创建给定数组的子数组(subarray)。它需要三个参数,分别是:数组、子数组的开始位置索引和子数组元素个数。其执行结果是将数组(由第一个参数给定)中从指定位置(由第二个参数给定)开始一定数目(由第三个参数给定)的元素拷贝到一个新的数组里面。这个新数组将被插入到操作堆栈中。例如:

GS>[1 2 3 4 5 6 7 8 9] 2 3 getinterval pstack
[3 4 5]
2GS<1>

putinterval:用给定的一个数组替换另一个指定数组的子数组。它需要三个参数:第一个是被替换数组;第二个是一个整数,用于指示被替换子数组在被替换数组中的位置;第三个是用于替换的子数组。此命令与 put 命令非常相似,它也不把结果放入操作堆栈中。从下面的示例可以看到该怎样显示其结果:

GS>/ar [1 2 3 4 5 6 7 8 9] def
GS>ar 3 [0 0 0] putinterval
GS>ar pstack
[1 2 3 0 0 0 7 8 9]
GS<1>

aload:此命令以一个数组作为参数并把它的元素作为单实体拷贝到操作堆栈中。执行结束后,此数组成为操作堆栈中的第一个元素。见下面:

GS>[1 2 3] aload pstack
[1 2 3]
3
2
1
GS<4>

astore:用给定的一个元素序列来替换第二个参数指定的数组中的所有元素,二者的元素个数相等。结果为替换元素组成的新的数组。

GS>1 2 3 [null null null] astore pstack
[1 2 3]
GS<1>

copy:拷贝由第一个参数所给定的数组到第二个参数所指定数组中从第一个元素开始的子数组。显示的结果是被拷贝的数组而不是第二个参数指定的数组。如果希望看到第二个数组的最终形式可以定义一个数组变量,见下面的示例:

GS>[1 2 3] [4 5 6 7 8] copy pstack
[1 2 3]
GS<1>/ar [4 5 6 7 8] def
GS<1>[1 2 3] ar copy
GS<2>ar pstack
[1 2 3 7 8]
[1 2 3]
[1 2 3]
GS<3>

数组元素不一定是整数,数组也可以作为数组的元素。这就意味着在 PostScript 中允许数组的嵌套。这个优势使矩阵操作和矩阵宏定义成为可能。理论上甚至可以处理张量或高维序列。目前我们有能力处理这个问题。

 

字符和变量

所有的编程语言都可以定义变量。使用变量处理数据可以不必考虑它们在内存中的位置。获取内存段中的数据可以通过给定地址或者给出该内容的变量名(key)来实现。前一种方法是象 C 那样使用指针。如果你不想使用地址那么后一种使用变量名(key)的方法对你就足够了。然而,语言的编译器或者解释器必须要考虑内存的存取及其它相关操作。由此,你才可以定义一个字或者一个英文字符串,然后给这个实体赋值。事实上,所有这些动作都是为了让使用者方便的告诉编程语言想定义什么变量及变量值是多少。编译器或者解释器将为用户的变量指定一个内存段并把所有的关于该变量的信息都作为数据保存到此处。PostScript 也有类似的结构。PostScript 的词典包含了变量名及其相关的一些定义。在 PostScript 术语中,词典(dictionary)由这样的 key-value 对组成:对的第一个元素被称作名字(key)而第二个被称作值(value)。例如,add 是一个名字(key)它的功能是做算术加法(value)。因为 add 被存贮在 systemdict 这个系统词典中,因而 PostScript 可以知道 add 的含义。当用户输入 1 2 add pstack 这个命令时,会看到结果 3。这是因为 PostScript 先查找前三个字然后又做了下面这些操作。它先查找 1 然后是 2 、aad。前两个对象由于是整数而被存贮到操作堆栈中。第三个对象为字符串,它有可能是个 key 名字。于是 PostScript 便开始在它的字典里面查找这个名字。如果找到,其对应的动作将发生。由于 add 存在于系统词典 systemdict 中,PostScript 可以获得对应于这个名字(key)的动作(value)。然后顺序执行两个最顶端元素出栈操作、计算它们的算术和,再把结果压入操作堆栈中使其成为顶端元素。这个命令中剩下的字符串 pstack 在系统词典中的意思就是:“显示当前操作堆栈中的内容到标准输出设备中”。我们看到这个动作也发生了。另一方面我们可以故意的给解释器一个错误的命令:1 2 dad pstack。解释器会报告出 PostScript 中没有此 key 名字定义的出错信息。

当然,PostScript 没有把我们限定在这些已存在于系统词典中的定义里。用户可以把一些过程或者命令标识定义成自己的命令。如果定义的是一个标识那么我们就可以调用该名字或者 key 作为变量,尽管它不属于PostScript 术语。我们这样做的目的是实现其它常用编程语言的回调。对于变量的定义我们必须采用如下命令格式:/x value def,其中 value 可以是整数、数组、字符串等 PostScript 对象。下面让我们看看解释器对于命令 /x 12 def 的执行情况。在解释器中输入该命令后,PostScript 解释器从中取出 /x、12和 def 三个对象。由斜杠 / 开始的对象将被解释器当作 key 来处理。它们总都能被插入到操作堆栈即词典堆栈中而不必考虑它们是否存在于词典中。def 命令是 PostScript 系统词典中的一个 key-value 对。它需要两个参数:第一个是定义过的 key 名字,第二个是将被赋给第一个 key 的值。因此,当命令 /x 12 def 执行之后 PostScript 会创建一个 key-value 对并把它保存到指定的缺省词典—— current 词典中。值得注意的是:此 key-value 对将成为 current 词典堆栈的最顶端元素。这一点以后将会用到。至此,在整个会话过程中PostScript 将把 x 当作 12 处理。

从理论上说,任何以斜杠 / 开头的字符串都将被当作 key,然而最好还是避免使用那些字母和数字以外的字符。否则,当象斜杠这样的标点符号被定义成 key 时,将会产生一些无法预料的结果。因为这些字符在 PostScript 中有其特殊的作用。key 名字的字符数受用户所使用的解释器限制。事实上,尽管允许上百个字符长的 key 名字存在,但是这样的名字看起来是很不方便的。PostScript 对大小写敏感这一特点给用户带来了很多方便。还有,用户定义的 key 名字不要和 PostScript 中的系统 key 名字相同。否则新定义的 key 将会覆盖掉系统命令。这是很让人伤脑筋的。比如,如果你输入了 /add 13 def 这个命令,那么 add 将被转换成一个常量,同时在这个会话余下的部分里 PostScript 的加法功能将失去。由于篇幅所限,还有很多这方面的内容将放到后续的文章中介绍。

 

循环

PostScript 的循环(loops)是一种实现重复运算的结构。它提供了一个执行大量相同类型操作的方法。这些操作的数目可以是非常巨大的。全部这些重复操作可以用一个命令来实现。下面的内容将涉及到循环及其命令。

repeat:此命令需要两个参数,第一个参数是一个整数用以指示循环的次数;第二个参数是由块定界符 { 和 } 所定义的一个 PostScript 程序块。这个块里可以连续的包含一系列指令或者命令。该命令格式形如:n { ... } repeat。如果给定了参数,PostScript 将把第一个参数插入到操作堆栈中。然后再读取并解析 { ... } 里面的指令。最后,PostScript 在系统词典里面查找到 repeat 并开始执行它的动作。其结果就是第二个参数指定的动作块被重复执行了 n 次。下面的示例给出了详细的说明:

GS>3 {1} repeat
1
1
1
GS<3>

此命令将三个整数值 1 插入到了操作堆栈里面。事实上,这里的程序非常简单,仅仅是插入 1 操作。下面还有一个稍微复杂点的程序:

GS>1 5 {1 add} repeat pstack
6
GS<1>

这个程序中,第一个数 1 先进入到操作堆栈中,然后程序块 { 1 add } 被执行 5 次。这 5 次操作是这样的。第一步,执行 1 add 操作。本来 add 需要两个参数,第二个参数已经给出了,第一个参数(也就是 PostScript 术语中的操作数)是从操作堆栈中取出的。因此,第一步循环实际执行的是 1 1 add 操作。第一步的结果是将操作堆栈中唯一的元素 1 替换成了 2。接下来,第二步执行 2 1 add 操作把堆栈中的唯一元素替换成了 3。如此类推,余下的三步分别是:3 1 add,4 1 add 和 5 1 add。所以在此 repeat 命令执行之后操作堆栈中有一个唯一的元素——6。

for:此命令使用一个整形变量来控制给定程序块的循环次数。该控制变量从初始值开始每执行一次循环体便累加一次,直到超出给定的界值。所以此命令需要四个操作数,前三个分别是:初始值,步长和界值。这三个数必须都是数值值,或者是整数或者是十进制数。第四个参数是一段程序,可以是一个命令也可以是由 { 和 } 所定义的一个程序块。此命令的完整形式为:Initial Increment Limit Procedure for。此命令执行时,PostScript 先创建一个临时计数变量(用 PostScript 的术语说就是控制变量)并把初始值 Initial 赋给它。同时此值将被插入操作堆栈中。它也许会被 Procedure 参数中的某个命令使用,如果被使用,它将被移出操作堆栈。否则它将一直保存在堆栈中。计数变量赋初始值后,循环体 Procedure 将被执行一次。紧跟着计数变量累加一个步长——由 Increment 参数给定的值。如此循环执行下去,直到增加后的计数变量值超出了界值 Limit。超出的意思就是不在初始值 Initial 和界值 Limit 所确定的区间了。如果步长 Increment 是正的,那么 for 命令将在计数变量大于界值 Limit 时停止。反之,当步长 Increment 为负时,for 命令将在计数变量小于界值时停止。计数变量的变化区间必须是非空的。也就是说,当步长 Increment 为正时初始值 Initial 必须小于界值 Limit,反之亦然。下面的示例给出了比较详细的说明:

GS>1 -0.5 -1 {} for pstack
-1.0
-0.5
0.0
0.5
1.0
GS<5>clear 0 1 1 23 {add} for pstack
276
GS<1>

第一个命令什么也没有做是因为循环体为空。因此计数变量的所有值都因为没有被使用而保存在操作堆栈中。第二个命令的循环体包含了 add 命令。此命令需要两个操作数:第一个操作数通常是操作堆栈中的最顶端元素而第二个操作数则是该循环每一步插入操作堆栈的计数变量的值。第二个命令实际上求出了前 23 个正整数的和。此命令需要一个额外的初始值用于加法运算。因此在 for 命令之前给了一个 0,注意它不属于 for 命令。

forall:此命令将给定程序块依次作用于给定数组的每一个元素。为了实现这个目的,数组元素是从第 0 个开始一一被访问。此命令用一个临时计数变量来控制循环。该计数变量的初始值为 0,步长为 1,界值为给定数组的长度。循环方式同 for 命令基本上一样。唯一的差别就是:由数组元素来取代计数变量成为循环体的操作数。此命令需要两个操作数:第一个是被循环体使用的操作数数组;第二个则是循环体程序块。forall 命令的完整形式为:Array Procedure forall。下面的示例中第一个 forall 命令用于计算给定数组的元素和,第二个命令是为了进一步说明此命令是如何把给定数组的元素压入操作堆栈的。

GS>0 [11 23 45 -89 26 12 0 -34] {add} forall pstack
-6
GS<1>[1 22 -12 0] {} forall pstack
0
-12
22
1
-6
GS<5>

loop:此命令只有一个循环体操作数。当它执行时,循环体将不停的被执行。换句话说,就是执行了一个无限循环命令。如果循环体内没有特殊的终止语句,那么结束该命令只有通过象 Ctrl-C 这样的外部中断来实现了。如果循环体内含有的 exit 或者 stop 命令被执行时,PostScript 将跳出该循环转而去解释下一个命令对象。此命令的表达形式为:Procedure loop。

 

过程/宏定义

在 PostScript 中,过程或者说宏指的是一个对象顺序集。这些对象必须包含在一对定界符({ 和 })之间。过程和宏的命名可以通过 key 定义来实现,如:/Macro1 {1 add 2 mul} def。这样做了之后,key Macro1 和它的值 {1 add 2 mul} 将以 key-value 对的形式加入到词典中并位于词典堆栈的最顶端。然后,当对象 Macro1 出现在解释器的命令行中时,它的动作将被执行。块定界符中的内容可以根据你的需要或者简单或者复杂。在后续的文章中我们将增加关于宏的内容。目前,这些简单的说明对于我们的来说已经足够了。

 

练习

下面一些练习供读者加深对本文内容的了解。这些问题的答案将在下一篇文章里给出。

  • 1)  写一个程序,该程序能读入一个整形操作数并计算从 1 到该操作数之间所有整数的平方和。

  • 2)  写一个程序,该程序能读入两个整形操作数并计算这两个数之间所有整数的立方。这些立方必须作为元素存放到一个数组中。

  • 3)  写一个程序,该程序能读入一个数组操作数并计算该数组中所有元素的算术平均值。然后再求出数组元素平方和的平方根。

  • 4)  假设 PostScrip 没有 exp 函数,现在让你编写一个程序实现这个功能。该程序有两个数值操作数,第一个作为底数,第二个作为指数,并计算出这个幂值。操作结束后这两个操作数必须保存在操作堆栈中。

  • 5)  数学对象——矩阵——可以被看作是数组的数组。一个 N 阶方阵可以表示成一个 N-元(N-element)数组,其中的每个元素也是一个 N-元数组。这种数组的数组被称为“方阵”。写一个程序,其操作数为一个方阵并计算其对角元素之和。结果和初始方阵都要保留。

  • 6)  写一个程序,其操作数为一个方阵并计算其转置矩阵。操作结束后结果和初始方阵要保留。

  • 7)  写一个程序,该程序能读入两个方阵操作数并计算二者之和。操作之后结果和初始方阵都要保留。

  • 8)  写一个程序,该程序以两个方阵为操作数并计算二者的乘积。操作之后结果和初始方阵都要保留。


  • Webpages maintained by the LinuxFocus Editor team
    © Emre Demiralp
    LinuxFocus 1999

    1999-08-24, generated by lfparser version 0.6