北航程序设计语言原理教材第05章Word文档下载推荐.docx
- 文档编号:19010239
- 上传时间:2023-01-03
- 格式:DOCX
- 页数:24
- 大小:73.45KB
北航程序设计语言原理教材第05章Word文档下载推荐.docx
《北航程序设计语言原理教材第05章Word文档下载推荐.docx》由会员分享,可在线阅读,更多相关《北航程序设计语言原理教材第05章Word文档下载推荐.docx(24页珍藏版)》请在冰豆网上搜索。
符号表运行时内存
类型名字地址存储对象
reallength(首地址)
array[1..4]ofintegerage(首地址)
图5-1符号表和束定
编译根据类型为名字分配适合大小的存储对象,按相对地址算出被束定对象的首地址,填入表,然而,运行时只有存储对象,符号表在执行代码中是没有的,多数语言此时均销毁。
所以说,它跨越时间。
编译时开始,运行开始时(动态)到程序结束运行时(静态)结束。
静态束定运行前完成,一旦束定不再改变(符号表已销毁)。
静态束定也可以实现多重束定,如下例:
例5-1FORTRAN的等价语句
DIMENSIONP1(3)
EQUIVALENCE(P1,P2),(P1
(2),P3)
DIMESION语句声明了一三元素实型数组(P隐含声明实型)P1(3)。
分配存储后实现P1束定。
紧接着EQUIVALENCE又指明束定。
名字的意思是P2就是数组P1,P3是P1数组的第二元素。
如图5-2所示:
p1p2p3
图5-2多重束定
其他语言的多重束定是:
COBOLREDEFINES子句
Pascal无标签域的变体记录
C联合类型
5.2.2动态束定
动态束定是程序对象标识符在运行中分配存储并束定,而且可在运行之中改变束定。
程序中的声明到运行时才束定显然要根据运行值判定。
无类型语言解释执行,动态束定很自然(见5.2.4)。
类型语言在运行中解释类型并束定变量效率较低。
方法是一样的。
FORTH是类型语言,它完全动态束定。
不过为了提高效率它同时有编译器和解释器,相互切换使用。
编译器不完全生成目标码,只生成解释起来更快一些的中间码。
FORTH的程序对象是有类型的字词(word)。
在统一的字典中处理。
字典是一个大的堆栈。
系统的、预定义的、以前定义过的字都压在栈底,新的应用压在栈顶。
新、老字都可以使用。
每当处理一项声明,字典创建一个项存储用户定义的字。
每个项分四个域:
名字域:
放名字,第一字节是名字长度;
链接域:
使该项成为可查找的数据结构(实为指向以前项的指针);
代码域:
相当于类型域,标识名字是哪类程序对象(函数、变量、常量、用户定义的类型);
参数域:
或称体存放字的特定含义,如常量就放纯值,变量放存储对象,函数则放可解释的代码。
代码域实则是指向运行例程的指针,这个例程就束定为该名字的语义,代码域定义了该类型对象的解释方法。
最初的类型仅有“函数”、“变量”,用户可不断增加,每当声明一新的类型符,则给出两段代码,一个是为该类型对象分配足够的空间并初始化,另一段是该类型的执行例程。
指向这个新类型的指针成了该类型唯一的标识符,也成了今后声明该类型对象代码域中的内容。
现举例说明如下:
例5-2FORTH的动态束定
0:
2by3array('
:
'
表示编译开始,后为类型声明符)
1create(编译动作:
将类型声明符装入字典项)
22,3(在字典项中存入2×
3维数)
312allot(为六个短整数分配12个字节)
4does>
(运行时动作指令,取下标)
5rangecheck(函数调用,检查下标)
6if(如果不越界)
7linearsub(函数调用,计算线性下标值)
8then(给出数组基地址和位移)
9;
('
;
切换成解释执行,数据类型定义毕)
10
112by3arraybox(声明并分配名为box的数组变量)
121012box(给box(1,2)赋值10)
CREATE起到第3行和DOES起到第8行即为上述两段代码。
按类型声明符2by3array的束定,将编译后的1-8行压入字典堆栈的项中。
解释执行时,按2by3array指针找到编译块,运行CREATE分配box的存储对象(在该项的顶端),接着解释执行第12行,运行does检查(1,2)是否越界。
若未越界计算下标准确值,在该地址下赋值10。
如果第12行以后用户把box定义为另一类型变量也可以,因为类型名束定于指向另一编译块的指针,再出现box则按最新释义。
当见到用户的FORGET命令则将至此定义的新字全部撤消,栈顶指针退下一项。
如无FORGET命令,栈越积越大,可重用的字越多。
5.2.3块结构束定
块结构束定使名字和反映操作语义的代码联系,更为复杂。
它和FORTH动态束定不同的是,新程序块中的名字可以和外块相同即重新定义,但出了该块还要恢复该名字和原来束定。
详细的实现机制见5.4节。
在此处我们先介绍一个有趣的问题,即常量束定会因块结构动态运行机制,变成常量非恒值。
程序中为常量给出一个名字方便程序维护。
因为若常量有了改动只需改定义常量名的那一次,程序中出现数十次都用名字代替了不用改。
有的语言还扩充了常量表达式:
例5-3Ada的常量束定
P1:
constantFLOAT:
=3.1416;
N:
INTEGER:
=5;
M:
INTEGER;
MAX_LENGTH:
constantINTEGER:
=N*100;
MAX_INDEX:
=MAX_LENGTH-1;
最后两行都是要计算的常量表达式。
它们通常在装入内存后运行前计算。
结果值束定于常量名。
P1,MAX_LENGTH,MAX_INDEX都成了只读变量(由编译加指令保护),而符号对照表运行时已撤消,这是正常情况。
如果把这组常量声明放在过程PROC内。
而调用PROC的主子程序MAIN内又把N定义为一般变量。
且N和PROC都在一循环体内,而循环次数不知,每循环一次读一次N,由于采用堆栈式管理,每调用一次装入一次PROC并计算一次常量表达式(确立时,简单常量编译时已赋值不必计算)。
此外,由于PROC中的N和
procedureMAINis
…
loop
GET(N);
PROC;
when(A>
B)exit;
endloop;
endMAIN
MAIN中的N同名,按作用域规则它们束定于同一存储对象,出了PROC还原。
显然,因‘全局’量N取决于输入,常量非恒值。
有时这种非恒值常量还非常有用。
5.2.4无类型语言的束定
无类型语言更加依赖束定,因为一个变量名可完全动态地束定到任何类型的值或操作集上。
完全动态束定只可用于解释类型语言,或像LISP那样只有简单、统一的类型结构,这种语言类型直接和存储对象结合,而不通过名字,并和对象一起存在内存。
符号表中就没有类型这个域了,如图5-3所示:
符号表运行时内存存储对象:
类型标签
名字束定
length地址:
scalarnumber
age地址:
arrayof4number
图5-3APL的束定
由于无类型,一个名字即使在一个块内,只要程序员发出命令即可束定到另一个存储对象上,与存储对象大小无关。
APL、SNOBOL就是这种无类型语言,只有按当时束定解释它的意义。
无类型语言“变量”是不用声明的。
它只有定义(简单声明),即将标识符束定到表达式的结果值上,第一次引用则此标识符即为该值类型的“变量”。
APL甚至连赋值概念也没有,程序员显式操纵束定,达到计算并改变值的效果。
如下例:
例5-4APL计算税金的程序
TAXCALC
[1]'
ENTERGROSSPAY'
[2]GROSS←□//输入什么值GROSS就是什么类型,□表示终端输入
[3]→LESS×
GROSS<
18000//条件表达式为真转LESS标号句
[4]TAX←.25×
GROSS//表达式结果值束定到TAX
[5]→DISPLAY//转到标号DISPLAY句
[6]LESS:
TAX←.22×
GROSS
[7]DISPLAY:
'
THETAXIS$'
,TAX
[8]
←'
即为束定,把表达式结果值所据的存储单元束定到TAX。
→'
相当于goto,
’×
指示后面表达式为条件,'
□'
为终端输入,'
显示TAX的十进制值。
引号中的字符串是照录显示出来的。
程序员可把标识符束定在任何表达式上,不同类型值都可以。
5.3声明
声明指明了本程序用到的所有程序对象。
实质上,它给出预想的束定集合(实现世界),即每个标识符和什么样的存储对象束定。
声明的作用,一方面供翻译器处理时所需信息,一方面为人们阅读便于调试。
对于类型语言,一般有显式声明部分或声明语句。
强类型语言每个标识符都要显式声明。
对于其它类型语言允许隐含声明。
例如,FORTRAN的标识符第一个字符在(I..N)范围为整型,其余为实型,无类型语言无显式声明部分,直接给出束定(通过表达式)或定义。
或隐含由上下文给出束定。
即使是强类型语言为使程序清晰也有隐含声明。
例5-5Ada的显式和隐式声明
Ada是静态强类型语言,它的声明有典型性,且最丰富,它的显式声明有:
基本声明:
=对象_声明|数_声明
|类型_声明|子类型_声明
|子程序_声明|包_声明
|任务_声明|类属_声明
|异常_声明|类属_设例_声明
|换名_声明|延迟_常量_声明
Ada的隐式声明包括:
块的名字、循环名字、语句标号(且为可选)以及循环控制变量。
有些操作也可以直接出现不用声明,如预定义运算符,派生子程序等。
声明的确立产生事实上的束定,下文讨论中为了方便不强调确立过程。
声明就有了静态束定。
5.3.1声明的种类
这里讨论的不是声明的程序对象有哪些类别,而是声明本身的类别,有:
·
定义
顺序声明
并行声明
递归声明
我们详细说明如下:
(1)定义
定义为标识符束定提供完整信息,使标识符可束定于确定的存储对象上。
定义就是声明,而声明不完全等于定义。
例如,C语言的外部变量声明,Ada的带有(with)子句声明都是给编译提供信息的。
所以,一般说来,一个标识符可以声明多次而定义只能一次。
否则产生名字冲突。
在这个意义上,定义是简单的声明。
具有完整定义的声明为完全声明,否则为不完全声明,不完全声明在相互递归的类型定义中是常见的,例如,二叉树的每个结点由结点值和左、右两指针组成,是先定义指向结点的指针类型,(此时结点是什么不知道),还是先定义结点(以记录类型表达)的类型(此时指针成份未定义)?
所以,只能先说一半再说整个,如例5-6。
实现上不会有困难。
读者想想为什么?
有些语言把定义仅限于用已知信息定义程序中需用的信息,而声明可创建新信息。
例5-6ML的类型定义与声明
ML定义两个类型
typebook=string*int//书名和版本号
typeauthor=string*int//作者名和出版年号
book,author两个名字都束定于string和int组成的结构存储对象上。
这是不安全的,编译后两标识符去掉后容易出错。
ML还有新类型声明:
datatypebook=bkofstring*int
datatypeauthor=auofstring*int
它创建了两个新类型,除束定于string*int的存储对象上外另有标记au,bk则与原有的类型都不同了。
前者按结构等价,后者按名等价。
Pascal,Ada,C++则认为每个类型构造子,如record...end,array[]of...,都引入新类型,定义只把标识符束定于类型。
如果把变量定义的概念仅限于用已有变量定义新变量,则许多语言为防止别名的混乱没有变量定义,只有引入新变量的变量声明,只有Ada有变量定义,即它的换名声明:
POPULA:
INTEGERrenamePOPULATION(STATE);
将以国名为下标的人口数组元素换名为POPULA。
与此相反,函数式语言没有变量概念,当然也没有变量声明,只有和值束定的参数(变元)。
即只有定义,无类型动态语言更是如此,全靠定义。
例5-7ML的值定义
valcount=ref0
ref是引用相当于传统语言分配算符new,即将标识符count束定于数0,因而引入了该变元。
例5-8ML的函数定义和值定义
valeven=fn(n:
int)=>
(nmod2=0)
┗━━┛┗━━━┛
函数型构函数体
┗━━━━━┛
函数定义
┗━━━━┛
值定义
以上是标识符even的值定义(因有val)。
将标识符even束定于函数,其变元n为整数,体的表达式返回真值,则even的值为Integer→Truth_Value。
以上还说明ML的函数抽象(even)是第一类值可用作值束定。
如果定义为函数也是可以的:
funeven(n:
int)=(nmod2=0)
函数抽象函数体
<
束定>
但值定义更好用,因为=>
右边可以是任何值表达式。
(2)顺序声明与并行声明
程序讲究的就是次序,所以声明一开始都有一个隐含约定:
声明是顺序的,即后声明的声明符(declerator)可立即使用刚声明的声明符。
如果把声明符集写作D则有顺序声明:
D1;
D2
声明确立次序先D1后D2(分号表示)。
因此,D1可以影响到D2的声明
例5-9Ada程序包声明
packageMANAGERis--声明程序包规格说明
typePASSWORDisprivate;
--声明私有类型未定义
NULL_PASSWORD:
coustantPASSWORD;
--立即用私有类型声明变量
functionGETreturnPASSWORD;
--返回私有类型函数
functionIS_VALID(P:
inPASSWORD)returnBOOLEAN;
Private
typePASSWORDisrange0..700;
--定义私有类型
constantPASSWORD:
=0;
--此时才定义
endMANAGER;
尽管PASSWORD在第2行声明时未定义,但第3,4,5行就立即用它声明其它对象。
endMANAGER,表示程序包结束,以上声明都限于本包,如果第3行像第8行那样赋了初值,本声明就出错了。
并行声明不怕次序调换。
ML中就有并行声明,其一般形式是:
D1┃D2或D1andD2
两个子声明D1,D2是独立的,即它们确立相互无影响,确立先后不会改变声明的意图,因此,就不能立即声明立即用了。
例5-10ML的并行声明
valpi=3.14159
andsin=fn(x:
real)=>
…
andcos=fn(x:
real)=>
是合法的,加上
andtan=fn(x:
real)=>
sin(x)/cos(x)
就不合法了,因为在确立tan时它也许最先。
对于讲究副作用、次序的命令式语言是不会采用并行声明的。
除非同一程序描述并行的子程序部分。
而函数式、逻辑式因排除副作用,故可采用。
(3)递归声明
递归声明是标识符以自身束定的声明,一般形式是:
D=…D…//D是包含标识符D的声明符
或D1=…D2…//D1是间接递归或称相互递归
D2=…D1…
递归声明通常限于类型、过程、函数、值定义。
至少到目前还没有扩大到更大的方面,如类、模块、程序包、类属、异常等。
有的语言采用自动递归,有的语言则由程序员显式指明递归,后者当有重名时程序员有主动性。
例5-11Pascal和ML的递归
FUNCTIONeof(VARf:
Text):
Boolean;
BEGIN
eof:
=eof(f)OR(f↑='
*'
)
END;
这个Pascal程序原意为用标准函数eof检索正文文件f的内容,当该文件以'
结束时检索结束。
如不递归调用怎么也到不了'
。
但本程序自动递归调用自己定义的eof,而不会调用标准的eof(其中有f↑),所以什么也查不到。
ML可显式指明递归:
valrecpower=
fn(x:
real,n:
ifn=0then1.0
elseifn<
0then1.0/power(x,-n)
elsex*power(x,n-1)
其中rec是显式指明符。
5.3.2声明的作用域
在程序正文中声明有效的范围称为作用域(scope)。
作用域由所在程序块起止符标识。
声明自出了该声明符的句子(已产生束定),即开始生效,直至所在块的终止符,所以早期语言声明部分均在块的起始处。
即使在块的起始处,简单的、并行的、顺序的和递归的声明作用域都略有差别:
简单声明的作用域,从本声明结束至块末。
顺序声明,每一子声明结束即起作用。
并行声明,所有子声明结束才起作用。
递归声明,只要遇见与标识符相同的声明符,则被声明标识符即起作用。
(1)嵌套块与可见性
声明局部于表达式则称块表达式。
文件、主程序、函数或过程中的声明局限于该程序单元,早期语言Algol60首先提出块结构,即在程序单元中另立可再作局部声明的块。
于是产生嵌套块或称块结构程序。
可见性(Visiability)为用简单标识符即可引用该程序对象。
这对任何无嵌套声明结构是显而易见的。
如果是嵌套块则有名字冲突,如下例:
例5-12Pascal的嵌套声明名字冲突
PROGRAMA;
VARx,y:
Integer;
FUNCTIONB(d:
Integer):
(A)
BEGINB:
=x+dEND;
(B)
FUNCTIONC(d:
Ingeter):
Real;
VARx:
(C)
BEGINx:
=5.1;
C:
=x+B(d+1)END;
=3;
y:
=0;
writeln(C(y))END.
图5-4Pascal的嵌套声明名字冲突
这个典型的Pascal程序A,内嵌B块、C块。
A中声明的x作用域被B域复盖,因故B中未声明,第二行的x引用的是A.x,而C中的两处x都是C.x而不是A.x,因为块结构约定就近声明优先原则,在C中指的是C域中的x,要引用A.x必须加前缀xxx.这就不是简单名了。
所以,虽在A.x的作用域内但不可见(被掩蔽),必须加前缀名才可见。
C和C++用'
作用域分辨符。
(2)标识符和名字
由于一个标识符在嵌套块结构程序中可以多次声明或定义,而束定只能将名字和存储对象结合一次,故而把标识符和内部名字分开,这样,编译器按标识符所在的作用域全部换上内部名字,束定就唯一了,不致产生语义混乱。
例如,上段例子A域中的标识符一律变成名字A.x,A.y,B域中的名字为B.d,C域中为C.x,C.d。
每个(带前缀的)全名的作用域为所在的最小封包声明的块。
全名只用于束定,即符号表中登记的是全名,运行时照样撤消。
尽管如此,概念上分清是十分必要的,今后本书把程序正文上的名字叫标识符,它翻译为全名后入符号表,与存储对象束定。
5.3.3块声明
程序均由模块组成,我们暂且把嵌套块和并存块的声明作用域,以及由此而引起的程序对象生存期问题放在下节讨论。
这里先说程序单元中定义的块。
Algol60最早引入的分程序实则是块命令BEGIND;
CEND;
即把一个语句(命令)扩大到可有自己声明集D,命令集C的局部块,前文我们已经介绍过了。
并且把这个局部性的思想用于表达式(表达式中有局部声明)即块表达式,这里进一步问,声明集也是一个块,能不能再带有声明呢
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 北航 程序设计语言 原理 教材 05