计算的过程除了指令的序列之外,还有一个极端重要的部分,就是通讯。
试想一下人工的计算过程,几乎每做一个计算步骤,都有可能需要引用很多东西,例如其他计算的结果,问题的初始值,其他的现成公式,或已经推导出来的公式,或者引用别人的计算成果等等,这种引用在本质上就是通讯,即一个相对独立的计算步骤,或计算过程,都不可能是真正独立的,它总是需要得到来自外部的信息,无论是外部的数据,还是外部的计算,只要是叫得出名字的东西,都有可能被用到。而该计算步骤或计算过程本身,不也是有可能被别的计算所使用吗!当然这一切都是通过草稿纸,通过我们的记忆,通过论文来进行的。这就是计算当中的通讯,通过所有这些通讯才能使得计算成为一个整体,才能使得计算真正是面向了本来是一个整体的真实世界的问题。
相应的,当我们使用FORTRAN语言来描述计算任务的时候,它同样需要描述其中的通讯过程,而且还得是严格地把一切通讯过程都毫无遗漏地描述出来。同样地,一个FORTRAN程序正是通过这些通讯而把所有程序单位,乃至所有的语句或指令单位,都连接起来,才能构成一个能够解决问题的计算过程。
那么这种通讯利用FORTRAN语言是如何进行的呢?还是让我们回到人工计算的过程,因为本质上,一个FORTRAN计算程序描述的就是一个人工计算过程。
首先我们需要对想使用的对象命名,才能称呼它们,然后才谈得上进行引用。
然而我们不能够指望给世界上的每一个具体对象都给予一个唯一的名称,那么做既不现实,也忽略了事物之间所具有的紧密联系。因此在同一个名称可以称呼不同的对象的情况下,我们需要一个约定,就是一个名称必定跟随有关该名称的有效范围的说明。
在给所有的对象起了合适的名称之后,就需要考虑建立在不同的场合里面出现的对象之间的联系,例如在波函数里面出现的正弦函数与在交流电电流公式里面出现的正弦函数,因为它们的名称一样,就可以认为是同一个函数。而波函数里面的振幅和交流电电流公式里面的振幅,尽管名称一样,却不能够应用于同样的公式,例如波函数里面的振幅可以引用到几率幅公式里面,而几率幅公式对于交流电却没有任何意义。
所以建立合适的,明确的关联是第3个重要的任务。
最后,我们可能还得检验某个对象是否具备进行通讯的资格,例如在有关交流电的计算任务当中,如果不小心引用了几率幅的概念,显然它没有任何用处,只会捣乱,因此检验一个对象是否具备良好的定义,是我们在进行计算通讯的同时不可遗忘的任务。
好了,上面分析出来的4个概念:命名,名称的有效范围,关联,良好的定义,构成了FORTRAN语言描述通讯时最核心的概念。运用FORTRAN语言来表达相应的意思就是:
● 名称;
● 作用域;
● 关联;
● 定义状态。
即任何的FORTRAN程序里面的对象都需要给予一个名称,而所有的名称都能够按照变量与值的关系,建立相对的层级结构。
作用域就是一个FORTRAN程序对象能够被识别,被访问的程序单位的范围。
关联就是在相同名称或不同名称下,进行相应的值的交换。
定义状态就是必须能够随时确定一个变量是否具有良好的定义,从而能够判别基于该变量的通讯是否有效。
可以形象地说,这4个概念完备地描述了程序的数据流的一言一行,一动一止。
下面我们就分别说明通讯的这4个核心概念。
给对象命名是进行有关对象的通讯的前提。(试想一下我们要告诉别人“起火了”,如果我们不知道描述“火”的词汇,那该如何说清楚意思呢?)
为了保证通讯时不发生歧义,最极端的做法就是给世界上的每一个对象给予一个唯一的名称。然而这是不可能实现的,因为世界是无限大的。不过即使在有限的范围内,也是非常不明智的。
例如在FORTRAN语言里面已经预先定义了名称SUM的涵义,即用名称SUM来命名一个固有函数,表示一种作用于数组的运算。然而如果在一个程序里面,并没有涉及到任何与数组有关的计算,那么我们完全可以安全地使用名称SUM来形象地命名一个自定义的运算,例如两个属于派生类型的对象的组合。运用容易记忆的英语词汇或缩写来表示具有相应涵义的对象,这是一种良好的编程习惯,不能为了避免名称的重复而被迫记忆大量的名称。
又例如一个大型的FORTRAN程序,事实上是无法由一个人编写完成的,那么如果由多个人分块编写的话,又如何能够保证他们使用的名称是绝对一致的呢?即相同的对象具有相同的名称,不同的对象具有不同的名称。这对于每个小程序单位都有成千上万个语句的大型程序,是绝对不现实的要求,也是完全不必要的要求。
因为不同对象的重名问题,可以很轻易地通过约定名称的有效范围而获得解决,这就是下节讨论的作用域的概念。
任何具有名称的对象都具有相应的作用域,也就是存在一个明确的约定,该名称在FORTRAN程序的哪个范围内,是被识别为该对象,而且引用该名称就意味着访问该对象。
在一个FORTRAN程序里面明显具有名称的对象包括变量,常量,过程,数据块子程序,模块,名称列表集合等。它们都具有相应的作用域。
还有其他的对象,例如算符,赋值符号,标签,输入输出单位编目等,它们都已经不再需要额外的名称,因为这些对象在被定义出来时,就已经具有天然的名称,因此它们也具有相应的作用域。
对象的作用域可以大到包括整个程序,也可以小到只有一个语句的某个部分。当一个对象的作用域为整个程序时,该对象就称为全局对象。例如一个程序里面的外部过程,在程序的任意位置,都可以随时引用它的名称,而不会发生无法访问该外部过程的情形。
当一个对象的作用域还没有达到整个程序时,该对象就称为局部对象。例如语句标签,一般只在该语句所处的程序单位里面有效。
当一个对象的作用域只是该对象所处的语句,甚至只是语句的一个部分时,该对象就称为语句对象。例如语句函数的哑元就是典型的语句对象。
一般说来,如果一个对象的作用域恰好构成一个程序单位,那么相应的作用域也称为作用域单位,这样一来,许多更小型的作用域,例如语句函数,FORALL结构,隐式DO语句等,就不能称为作用域单位了。
作用域单位一般有如下三种情形:
● 派生类型定义是一个作用域单位;
● 一个过程界面体,在排除其中可能的派生类型定义,以及包含在派生类型定义里面的过程界面体之后,构成一个作用域单位;
● 一个程序单位,或者子程序,在排除其中可能的派生类型定义,过程界面体以及包含在其中的子程序之后,构成一个作用域单位。
在一个作用域单位里面还可以包含其他的作用域单位,不过这种包含关系不完全与结构之间的嵌套关系相同。因为具有包含与被包含关系的两个作用域单位可以出现如下两种情形:
● 被包含的作用域单位,可以继承包含它的作用域单位的数据环境;例如派生类型定义,模块过程,内部过程等都继承包含自身的作用域单位的隐式类型规则。
● 被包含的作用域单位不继承包含它的作用域单位的数据环境。
例如界面块不继承包含自身的作用域单位的隐式类型规则。
因此在某些情形下,一个作用域需要排除包含其中的作用域单位之后才构成一个作用域单位。
作用域与作用域单位的相互关系参见示意图15-1里面的表示。
图15-1 作用域与作用域单位的相互关系
在上面的示意图15-1当中,最大的方框表示一个FORTRAN程序,它表示了一个整体,显然它不构成一个作用域单位。
然后是其中的4个方框,分别表示1个主程序,2个外部过程A和D,1个模块E。它们都是作用域单位,但是得排除它们内部出现的方框,即内部过程B和C,界面块,派生类型定义,以及模块过程F和G。因为它们同样是作用域单位。
由于在上面的FORTRAN程序例子当中,内部过程B和C,界面块,派生类型定义,以及模块过程F和G的内部不再出现内部过程,界面块,派生类型定义,以及模块过程,所以它们都不再需要排除什么,就构成一个作用域单位。不过一般在它们内部还包含其他的作用域单位是非常常见的。
一个名称的作用域存在如下4种情形:
● 整个程序,即该名称为全局性的;
● 一个作用域单位,即该名称为局部性的;
● 一个结构,即一个FORTRAN结构的作用域;
● 一个语句,即一个FORTRAN语句或语句的某个部分作为作用域。
下面按照范围从大到小的顺序分别予以说明。
能够具有全局性名称的对象包括:
● 主程序;
● 外部过程;
● 模块;
● 数据块程序单位;
● 公用块。
正是因为这些对象的名称具有全局性,因此它们在一个FORTRAN程序内部不能使用相同的名称。
能够具有局部性名称的对象包括如下3个类别:
● 变量,常量,控制结构,语句函数,内部过程,模块过程,哑过程,固有过程,自定义类过程,派生类型和名称列表集合。
● 派生类型的成员。
之所以把派生类型的名称与它的成员的名称分为不同的类别,是因为不同名称的派生类型,完全可以具有相同名称的成员。
● 变元关键词。
之所以把具有显式界面的过程的名称与其变元关键词的名称分为不同的类别,是因为不同名称的过程,完全可以具有相同名称的变元关键词。
局部性名称的作用域的一般规则如下:
● 在一个作用域单位里面可以访问的全局性对象的名称,不能用来作为一个该作用域内部的局部性对象的名称,除非该局部性对象和一个公用块同名。
● 一个非类型的局部性对象的名称,在它的作用域单位里面,以及它的同一个名称类别里面,必须是唯一的,只有在不同的作用域单位,或者不同的名称类别里面,才可以把该名称用于另外一个不同的对象。在其他作用域单位里面的对象,不管其使用的名称是否与此作用域单位里面的对象所使用的名称相同,都有可能通过关联,而导致实际上是同一个对象。
● 一个类型的局部性名称,可以用于同一个作用域单位里面的不同过程。
● 在一个作用域单位里面的同一个名称可以用来表示不同名称类别里面的对象。例如一个变元关键词可以和一个逻辑型变量具有相同的名称。
● 派生类型的成员的作用域就是该类型定义,当该成员被用于结构引用时,它的作用域就是该结构引用。
● 变元关键词属于一个单独的名称类别,因此一个变元关键词的名称可以在同一个作用域单位里面,用作其他类别对象的名称,例如其他过程的变元关键词,过程,或变量的名称。
● 如果在程序当中出现了一个公用块与一个局部性对象的名称的雷同,那么该名称只有在表示公用块时,才是全局性的。在引用该名称时,它所指称的对象的唯一性由该名称的上下文保证。
● 一个固有过程的名称可以在不出现该固有过程的作用域单位里面用作局部性名称。也就是说一旦某个固有过程的名称在某个作用域单位里面被用来作为一个局部性名称,比方说一个变量的名称,那么在整个该作用域单位里面,该固有过程本身都不能出现了。
● 对于每个函数以及不具有结果变量的函数对象,在它的子程序作用域单位里面,都默认存在一个与该函数或函数对象同名的一个局部变量,该局部变量用来在本作用域单位里面定义该函数或函数对象的结果值。
● 对于每个内部过程或模块过程,它的名称对于它的宿主作用域单位而言都是局部的。类似地,对于模块过程内部的任意对象名称,对于它的宿主作用域单位而言,也都是局部的。
在FORTRAN里面,可以用作作用域的结构只有FORALL结构。
在FORALL语句或结构里面的指标变量的名称具有的作用域为该FORALL语句或结构的赋值语句的作用域。
该指标变量的名称在其他地方都是用作标量变量名称,而在作用域单位的任何位置它都是整型变量。
FORALL语句或结构里面的指标变量是具有一个大于一个语句同时又小于一个作用域单位的作用域的唯一的例子。
一个名称的作用域只限于一个语句或语句的一个部分的情形有以下两种:
● 一个语句函数的哑元的名称;
● 在一个DATA语句里面的隐式DO列表或一个数组构造器里面的DO变量名称。
这些名称都可以在该语句的作用域单位的其他地方用作标量变量名称或公用块名称,而不至于出现任何的名称冲突。
语句里面的名称的一般规则如下:
● 一个语句函数的哑元的名称的作用域就是该语句,它的类型与种别参数都在该语句所在的作用域单位里面得到声明。
● 在一个DATA语句里面的隐式DO循环或一个数组构造器里面的DO变量名称的作用域就是该DO变量所在的语句部分。该DO变量的类型必须是整型,而它的种别参数则由它所在的作用域单位声明。
● 在隐式DO列表里面的DO变量的作用域只是它所在语句的一部分,而在一个DO结构里面的DO变量的作用域则是该结构所在的作用域单位。这两种是完全不同的。
● 在输入输出项列表当中的隐式DO里面的DO变量的作用域,为该输入输出语句所在的程序单位。
标签总是局部性的。也就是说在一个作用域单位里面一个标签总是指称同一个语句,而在不同的作用域单位里面,可以同时使用同一个标签来指称不同的语句。
外部输入输出单位的编目总是全局性的。也就是说,在一个FORTRAN程序里面,无论一个输入输出单位的编目出现在什么位置,它都指称同一个输入输出单位。
算符分为固有算符和自定义算符两种。
固有算符是全局性的;而自定义算符是局部性的。
一个固有算符的符号也可以用作自定义算符的符号;而且算符还可以具有类的属性,即同一个算符可以用来指称几个运算符号,它们具有不同的涵义。而程序是通过相应算元的类型,种别参数以及秩来获得对它们的辨别的。
赋值分为固有赋值和自定义赋值两种。
固有赋值是全局性的;而自定义赋值是局部性的。
赋值符号(=)总是全局性的,同时也可以具有局部性的定义;而且赋值还可以具有类的属性,即不同的赋值运算可以使用同一个赋值运算符号来指称,它们具有不同的涵义。而程序是通过赋值符号=左右两边的对象的类型,种别参数以及秩来获得对它们具体涵义的辨别的。
所谓非歧义的过程引用,是指引用的过程名称在相应的作用域单位里面,是一个种过程名称,它与该作用域单位里面的任何类过程名称都不相同。
非歧义的过程引用包括如下的情形:
● 引用内部过程;
● 引用哑过程;
● 引用语句函数;
● 引用属于固有过程的与类过程不重名的种过程名称;
● 引用模块过程或外部过程,它不能够出现在具有所在作用域单位的类声明的界面块里面,或者是可以通过使用关联或宿主关联来访问的。
只有属于外部过程的种过程名称是全局性的,所有其他的种过程名称都是局部性的。
在满足以下条件的情形下,涉及到类过程名称的过程引用也可以是非歧义的。
即如果任意两个过程具有同一个类过程名称,那么在它们的哑元列表当中,至少得有一个非可选的哑元同时满足下面两个条件:
● 或者是在另外的哑元列表里面与该哑元所处列表位置相同的位置不存在相应哑元,或者是与相应哑元具有不同的类型,种别以及秩的不同组合模式。
● 或者是该哑元具有与另外那个哑元列表里面的所有哑元都不同的名称,或者是即使存在同名的哑元,也与同名哑元具有不同的类型,种别以及秩的组合模式。
一个非逐元的种过程有可能引用一个类过程作为它的哑元,同时一个与类过程重名的逐元过程也引用类过程作为它的变元,在这种情况下,该非逐元种过程就用于解决类过程引用所带来的名称歧义问题。
不管类名称是固有的,还是由具有类说明的界面块所定义的,上面的规则都是适用的。而且它们还适用于算符类的名称以及赋值符号类。对于固有函数来说,固有函数的类名称是全局性的,而自定义的类名称为局部性的。
在FORTRAN程序里面,一个过程引用发生在如下几种情形里面:
● 情形1.运行一个CALL语句时;
● 情形2.运行一个经过定义的赋值语句时;
● 情形3.运行一个经过定义的运算时;
● 情形4.运行一个包含函数引用的表达式时。
在情形2里面,涉及到了作为类名称的赋值符号(=),这时在相应作用域单位里面必定存在一个具有ASSIGNMENT类说明的界面块,或者是通过使用关联或宿主关联可以访问定义了该赋值的外部子例行程序或模块子例行程序,在这种情况下,就必须运用13.7.7节,13.8.5节以及15.2.6节的相应规则来决定究竟引用哪个种子例行程序。
在情形3里面,涉及到了作为类名称的运算符号,这时在相应作用域单位里面必定存在一个具有OPERATOR类说明的界面块,或者是通过使用关联或宿主关联可以访问定义了该运算的函数或模块函数,在这种情况下,就必须运用13.7.7节,13.8.4节以及15.2.6节的相应规则来决定究竟引用哪个种函数。
在情形4里面,如果表达式的某个项的形式是由一个名称后接用括号括起来的表达式与过程名称的列表,其中的过程名称在本作用域单位里面没有被声明为数组名称,那么该表达式就包含了一个函数引用。
在情形1与情形4里面,对于有可能出现的过程名称引用歧义问题,必须按照以下的规则序列,逐步地来决定应该被引用的种过程。
● 如果引用的过程名称是所在作用域单位里面的一个哑元,那么该哑元就是一个哑过程,并且实际应该引用相关联的作为实元的过程。
● 如果引用的过程名称是出现在所在作用域单位里面的一个EXTERNAL语句当中,那么就引用该名称所指称的外部过程。
● 如果引用的过程名称是指向一个可访问的内部过程或语句函数,那么就引用该内部过程或语句函数。
● 如果引用的过程名称是类过程名称,而该类过程名称是在一个所在作用域单位里面的界面里出现,或者是在一个通过使用关联或宿主关联可以访问界面块里面出现,同时该引用还包含一个有关该类过程名称的种性质的类型,种别以及秩的组合模式的说明,那么就引用具有该类型,种别以及秩的组合模式的相应种过程,运用13.7节的规则就决定了该种过程,而13.7.7节以及15.2.6节的规则则保证了这样的种过程至多只存在一个。
● 如果过程引用是由一个引用逐元过程的逐元引用组成,而同时又不存在非逐元的过程能够匹配相应的类型,种别以及秩的组合模式,那么就引用该逐元过程。
● 如果引用过程名称出现在所在作用域单位里面的一个INTRINSIC语句当中,那么就引用该名称所指称的相应固有种过程。
● 如果引用的过程名称,是可以通过使用关联获得访问的,那么就引用相应的种过程。另外,由于可能出现改名现象,引用的过程名称可能会与模块里面的过程名称不一致。
● 如果引用所在的作用域单位还具有一个宿主作用域单位,而如果引用歧义问题已经在其宿主作用域单位里面通过上面的规则加以解决,那么在本作用域单位里面也就不存在歧义问题了。
● 如果引用的过程名称同时是类过程和种过程的名称,而实元与其中特定的某个固有过程的特征相匹配,那么就引用相应的固有种过程。
● 如果引用过程名称不是一个类过程名称,那么就引用具有该名称的外部过程。
● 如果以上规则都不能解决问题,那么该引用就是非标准的,或非法的引用。
在明确了每一个名称或对象的作用域之后,进行通讯的主要手段,就是在不同的作用域之间建立对象之间的关联。
所谓建立关联,同样是以对象的名称为基础的,因为按照直观的理解,无论是什么形式的通讯,都是以能够进行名称的传递为前提的。只不过在不同形式的通讯里面,名称一般也具有不同的形式而已。
一个FORTRAN程序的运行过程就是通过名称对相应对象的操作过程,名称的作用在于为程序辨认相应对象的属性提供依据,或者还可以为程序指出对象的存储位置。因此在程序当中进行的通讯无非就是一些交换名称的过程,即在不同的作用域单位之间给出指称同一个对象的名称,或者在同一个作用域单位里面给出指称同一个对象的不同名称,就是所谓通讯过程的实质,而关联的含义就是用来完成这两种任务。
关联具有以下4种形式:
● 名称关联
即直接使用名称进行关联,这种关联必定是在不同的作用域单位之间进行。
名称关联包括变元关联,使用关联,宿主关联3种类型。
● 指针关联
所谓指针关联的功能就是能够在一个作用域单位里面实现名称的动态关联。
● 存储关联
存储关联通过存储序列实现数据对象之间的关联。
在相同的作用域单位里面是通过EQUIVALENCE来实现存储关联的,而在不同的作用域单位之间则是通过COMMON来实现关联的。
● 序列关联
所谓序列关联其实就是名称关联与存储关联的组合,也是属于名称关联的变元关联的一种特殊形式。
序列关联在不同作用域单位的名称之间实现关联。
在下面的示意图15-2,和图15-3里面,给出了两个实现各种关联的可执行程序的例子。分别是在两个非模块作用域单位之间的关联和一个模块作用域单位与另一个非模块作用域单位之间的关联。
图15-2 两个非模块作用域单位之间的关联
图15-3 一个模块作用域单位与另一个非模块作用域单位之间的关联
尽管从实质看来,关联总是要通过名称来进行的,不过我们单独把在不同作用域单位之间相同或不同名称实现的关联称为名称关联。
名称关联包括如下3种形式:
● 变元关联;
● 使用关联;
● 宿主关联。
下面分别予以说明。
所谓变元关联就是在一个包含了过程引用的作用域单位里面的实元和另一个包含了对被引用过程的定义的作用域单位里面的虚元之间所建立的关联,即把可能相同也可能不同的处于不同作用域单位的名称,指称同一个实体对象。
通过变元关联而实现了实哑通讯的实元,是一个变量,或过程,或表达式的名称,而相应的哑元则是出现在过程定义当中的用来引用实元的名称或表达式。
这种关联在程序的执行离开该过程之后,就被撤消了,即在被引用过程运行当中具有关联的实元名称与哑元名称在程序退出该过程之后,就不具有关联关系了。
使用关联在一个模块作用域单位和一个通过USE语句引用了该模块的作用域单位的相同名称或不同名称之间建立关联。
通过这种关联就实现了使用USE语句的作用域单位对被引用模块内部的对象的访问。按照默认的约定,模块内部所有的具有公用属性的对象都能够接受访问。
在USE语句里面可以选择性地对需要访问的对象进行重命名,还可以运用ONLY选项来挑选需要访问的对象。
使用关联的一般规则如下:
● 如果在USE语句当中使用了重命名,那么相应局部名称的一切属性都向模块里面的被关联属性看齐。而且对于USE语句所属作用域单位里面的能够访问被引用模块的对象,除了可以改变它的访问能力之外,都不具有重说明属性的能力。
● 在USE语句里面对一个对象进行了改名,那么模块里面的原始名称就可以在该USE语句所属作用域单位里面作为局部名称使用,而不会导致名称冲突。
● 模块内部的对象的PUBLIC与PRIVATE属性都只能够由模块自身来规定,引用模块的作用域单位不能够改变模块内部对象的可访问性。
宿主关联是在一个内部过程,或模块过程,或派生类型定义里面的对象名称与它的宿主作用域单位里面的对象名称之间建立关联。
直观而言,宿主关联就是为了使得一个宿主作用域单位里面的名称都能够被其内部的过程或派生类型定义所访问,当然前提是这些过程和派生类型定义都是能够被其宿主作用域单位所访问的。
尽管宿主关联不具有使用关联那样的改名机制,以防止出现名称冲突,但是宿主关联可以很容易地通过在内部过程,或模块过程,或派生类型定义里面,对某个名称进行局部声明,而防止它所在的作用域单位访问其宿主作用域单位里面的同名对象。从而也可以实行名称冲突管理。
之所以通过局部声明来防止与宿主对象名称的冲突,是因为FORTRAN有一个基本的约定,就是任何的作用域单位内部的局部声明总是优先于其所属的宿主作用域单位里面同名的声明。
特别是如果同时在内部作用域单位与其宿主作用域单位里面使用IMPLICIT NONE语句的话,就能够强迫在它们内部的所有对象都必须具有相应的显式局部声明,这时,内部作用域单位的显式局部声明总是优先于其所属宿主作用域单位里面的相应显式局部声明。
如果只是在宿主作用域单位里面使用了IMPLICIT NONE语句,那么它内部的所有作用域单位在默认情况下也是受到IMPLICIT NONE的作用的,不过在内部作用域单位里面还可以使用IMPLICIT语句进行单独的约定,这时,就会改变部分默认隐式规则。
【例15-1】如果在一个宿主作用域单位里面使用了IMPLICIT NONE语句,然后在它的某个内部作用域单位里面使用了如下的IMPLICIT语句:
IMPLICIT COMPLEX(O,Z)
IMPLICIT LOGICAL(A-C)
这样就使得该内部作用域单位里面所有以O和Z开头的名称都是隐式复型,而以A,B,C开头的名称都是隐式逻辑型,至于以其他字母开头的名称则需要依靠显式声明才能确定其类型。
一旦在程序当中使用隐式声明的时候,那么某个对象名称可能在其所属的作用域单位的说明部分就不会出现对于该名称的显式声明,这就会给名称的引用管理带来一定的复杂性。考虑所有的宿主与其内部作用域单位的同一个对象名称的显式与隐式声明的组合,一共有如下4种情况:
1. 在宿主与其内部作用域单位内都是显式;
2. 在宿主内没有出现隐式声明和显式声明,在其任何内部作用域单位内都是隐式;
3. 在宿主的说明部分出现显式声明,而在其内部作用域单位内是隐式;
4. 在宿主作用域单位内是隐式,而在其内部作用域单位内的说明部分出现显式声明。
对于情况1,它们都是局部的,都只是在各自的作用域单位里面有效,不发生宿主关联关系。
对于情况2,只要该对象名称在宿主的出现不是以被引用的方式,那么它们都是局部的,都只是在各自的作用域单位里面有效,不发生宿主关联关系。
对于情况3,则该对象名称是宿主对象,其内部作用域单位内的同名对象需要通过宿主关联来访问它。如果该对象名称在宿主作用域单位内部只是以被引用的方式出现,那么同样看成是一个显式声明。
对于情况4,则在其内部作用域单位内是该作用域单位的局部名称,不需要访问其宿主作用域单位。
隐式声明不管是在宿主作用域单位还是在内部作用域单位,它都是依据隐式类型规则与IMPLICIT语句约定的,约定内容如下:
● 一个名称如果在内部作用域单位里面得到显式声明,那么它就是一个局部名称,与其宿主作用域单位无关。
内部作用域单位里面的哑元本身被认为就是一个显式局部声明,尽管该哑元的名称可能是根据其所在作用域单位的隐式类型规则进行隐式规定的。
● 如果一个内部作用域单位里面的对象名称没有得到显式声明,那么只有在该名称在其宿主作用域单位里面根本不出现的情况下,该名称才依据其所在作用域单位的隐式声明成为局部对象名称。
● 如果一个内部作用域单位里面的对象名称依据上面的两个规则仍然不是局部的,那么它就是需要获得宿主关联的非局部名称。
● 内部作用域单位里面如果不出现IMPLICIT语句的话,它的默认隐式类型规则就继承其宿主作用域单位的隐式规则,即其宿主作用域单位的默认隐式规则加上可能的IMPLICIT语句的修改。
● 如果内部作用域单位里面出现IMPLICIT语句,那么它对内部作用域单位从其宿主作用域单位继承来的默认隐式规则进行修改,并且同样作用于其中的哑元。
● 一个内部函数或模块函数的函数名称或对象名称的类型,或者由显示声明决定,或者由该函数的隐式类型规则决定。
【例15-2】
PROGRAM HOST
USE GLOBAL_DATA
IMPLICIT LOGICAL (E-J)
!隐式类型规则为:A-D为默认实型;
! E-J为默认逻辑型;
! K-N为默认整型;
! O-Z为默认实型
REAL A,B
…
READ *, P
!该引用继续P在宿主里面的隐式规则
…
CALL CALC (Z )
!该引用继续Z在宿主里面的隐式规则
…
CONTAINS
…
SUBROUTINE CALC (X)
IMPLICIT REAL (G-I)
!隐式类型规则为:A-D为默认实型;
! E-F为默认逻辑型;
! G-I为默认实型;
! J为默认逻辑型;
! K-N为默认整型;
! O-Z为默认实型
REAL B
…
X=A+B+P+Q+Y
!在SUBROUTINE CALC里面它们都是实型
!X为局部哑元;
!A获得宿主关联;
!B为得到显式声明的局部名称;
!P获得宿主关联;
!Q为得到隐式声明的局部名称;
!Y从模块获得使用关联.
…
END SUBROUTINE
…
END PROGRAM HOST
在上面的例子当中,表达式X=A+B+P+Q+Y里面的各个算元依据不同的类型规则。
前面讨论的所有的关联机制都使得被关联的名称在其所在的作用域单位运行时总是具有一个确定的关联对象,但是指针关联却是一种动态关联机制,具有指针属性的名称在其所在的作用域单位运行时,可以具有三种状态,即:
● 去定义状态;
● 去关联状态;
● 关联状态。
因此指针关联在本质上就是一种动态关联机制,在其所在的作用域单位的运行期间,随时可以根据运行的需要而变换其状态。
指针关联的一般规则如下:
● 没有经过初始化的指针总是处于去定义状态,在经过初始化之后,才具有去关联状态。
● 指针获得关联可以通过如下2种方式:
● 通过指针赋值,使得指针变量和其他指针,或者和具有TARGET属性的标量或数组数据对象建立关联。
● 通过执行ALLOCATE语句,使得指针变量和此前无名的空间建立关联。
● 处于关联状态的指针变量可以通过如下3种方式去关联:
● 在NULLIFY语句的作用下;
● 在DEALLOCATE语句的作用下;
● 被赋值为一个去关联的指针。
存储关联和名称关联以及指针关联的目的一样,都是使得不同的名称能够共享同一个数据实体,如果说名称关联和指针关联的关联途径都是通过访问名称来实现的,那么存储关联则是通过直接访问数据对象的存储空间来实现的。
所谓数据对象的存储空间表面看来是属于计算机的内存的物理层面的一个概念,但是在FORTRAN语言里面所谓的存储空间并不完全就是一个物理层面的概念,而是对物理空间的一种模式描述。这种描述的基本概念如下:
● 存储单位。
一个存储单位就是指存储了单个FORTRAN数据值的内存空间。
根据存储单位所存储的FORTRAN数据值的不同类型,存储单位分为数值存储单位,字符存储单位和不定存储单位。
● 存储序列。
任意多个连续的数据单位就构成了一个存储序列,存储序列的大小就是序列所包含的存储单位的数目。
要描述FORTRAN数据的存储,就必须说明每种类型的数据对象所占据的存储单位的数目,因此下面我们给出所有类型的FORTRAN数据对象所对应的存储单位分配模式。
● 一个默认整型,默认实型,默认逻辑型的标量数据对象占据一个数值存储单位;一个默认复型,双精度实型数据占据2个连续的数值存储单位,其中复型数据的实部所占据的存储单位位于虚部所占据的存储单位。
● 一个默认字符占据一个字符存储单位。
● 所有的其他类型的数据值,包括指针和非默认类型的数据值都占据不定存储单位。
如果是数据对象由多个或多种数据单位组成,那么该数据对象的存储模式由其具体数据形式决定,一般有以下几种形式:
● 长度为len的默认标量字符对象占据连续的len个字符存储单位,构成一个存储序列。
● 一个数组对象占据一个存储序列,该数组的每个元素占据一个适当类型的存储单位,存储序列里面存储单位的顺序就是该数组里面元素的顺序。
● 一个具有序列机构的派生类型对象占据一个存储序列,该派生类型对象的每个成员占据一个适当类型的存储单位,存储序列里面存储单位的顺序就是该派生类型对象定义里面成员的顺序。
● 一个公用块占据一个存储序列,参见7.11节。
● 函数子程序里面的ENTRY语句给出的结果占据一个存储序列,参见13.6节。
● EQUIVALENCE语句里面的等价列表的所有对象占据一个存储序列。
两个数据对象如果共享了同一个存储序列,那么它们就称为具有存储关联的关系。如果它们只是共享了同一个存储序列的部分存储单位,那么它们称为具有部分存储关联的关系。
能够建立部分存储关联的数据对象只能是字符型数据以及在COMMON,EQUIVALENCE,或ENTRY语句里面给出的复合数据对象。因为只有这样的复合序列形式的数据对象的子对象作为子串。才是有意义的数据对象。
【例15-3】
考虑下面给出的几个数据值:
REAL ::X(0:3)
COMPLEX ::Z
EQUIVALENCE(Z,X(2))
那么X所占据的存储序列就是由4个存储单位组成,每个存储单位为一个数值存储单位:
X(0) |
X(1) |
X(2) |
X(3) |
而Z所占据的存储序列由2个存o储单位组成,分别作为实部与虚部的存储单位:
Zr |
Zi |
EQUIVALENCE语句指定了Z的存储序列从X的下标为2的元素所占据的存储单位开始共享存储空间,也就是下面的形式:
X(0) |
X(1) |
X(2) |
X(3) |
|
Zr |
Zi |
而如果上面的EQUIVALENCE语句是如下的形式:
EQUIVALENCE(Z,X(3))
那么Z和X就构成了如下的部分存储关联形式:
X(0) |
X(1) |
X(2) |
X(3) |
|
|
|
Zr |
Zi |
序列关联就是一种特殊形式的变元关联,用于对具有序列结构的数据对象,例如字符串,数组等建立关联,具体的讨论参见13.7.2节。
固然通过关联,就可以说已经完成了通讯的过程,但是通讯的实现过程当中还有一个重要的问题还没有讨论,就是如何保障使用名称所指称的值时的安全性,也就是如何保障名称的取值是定义明确并且具有可预知的行为。
之所以还会出现这样的问题,是因为在程序运行过程当中,会有很多的事件能够导致变量的病态定义,或者是取值的不可预知性。例如,如果一个整型变量与一个实型变量具有存储关联关联,那么对于整型变量的赋值常常导致相应实型变量的取值的不可预知性;更常见的是一个输入输出的错误,很容易就导致变量的取值的中断。
因此为了保证通讯的顺利实现,我们还必须监视变量在程序运行过程中的取值行为,也就是变量的定义状态。
对于一个在程序运行过程当中的变量来说,它的定义状态就是如下两种:
● 良定义的;
● 去定义的。
所谓良定义的就是指一个变量能够在程序的运行过程中,通过一些语句的执行而获得明确的取值;所谓去定义就是指一个变量失去了值。
一个变量在通过DATA语句,或类型声明语句,或默认初始化语句进行初始化取值之前,总是默认为去定义的,然后在进入程序的运行进程之后,总会出现某些事件能够定义变量的取值,或者再对变量进行去定义。显然处于去定义状态的变量就不能够引用到需要使用它的值的环境当中去。
对于在程序运行当中有可能出现的异常的变量去定义状态,编译器常常并不能够检查出来,因为要求编译器总是去检查任何变量的定义状态,是一件花销特别庞大的工作,因此主要的避免出现变量的异常定义状态的任务,必须由程序编写者来承担,做到在程序的编写过程中尽量避免能够导致变量状态定义异常的事件的出现。
对于由一些子对象组成的数据对象,它们的定义状态是由它们的所有子对象的定义状态决定,即只有每个子对象都是良定义的,整个数据对象才是良定义的;只要它的一个子对象是去定义的,那么整个数据对象就是去定义的。
这样一些数据对象以及子对象的例子包括:
● 复型数据及其实部和虚部
● 数组及其元素或数组片断;
● 字符型变量及其字符子串;
● 结构及其成员。
如果一个能够具有子对象的数据对象的子对象数目是0,那么约定它是良定义的,例如0尺度的数组和0长度的字符串。
一个程序的执行过程,实际上就是一个不断地对其中的变量进行赋值,引用,再赋值的过程,因此几乎每一个可执行语句完成后,都会导致某些变量取值的变化,这种取值的变化也就影响了变量的定义状态,即或者是保持变量的定义状态不变,或者是改变了变量的定义状态。
下面我们分2节分别讨论在程序运行过程当中,能够导致变量良定义的事件,与能够导致变量去定义的事件。
在程序运行过程当中的如下的事件能够导致变量获得良定义:
● 在固有赋值语句当中,除了过滤数组赋值与指标并行数组赋值之外,都能够在执行后使得等号前面的变量获得良定义。
● 过滤数组赋值与指标并行数组赋值的执行,都能够使得赋值语句当中的数组的部分元素或全部元素获得良定义。
● 在执行输入语句的过程当中,从输入文件获得赋值的每个变量都在数据传递到该变量时获得良定义。执行一个单位说明符指向一个内部文件的WRITE语句时后,被写入的每个纪录都获得良定义。
● 执行一个DO语句之后,其中可能有的任何DO变量都获得良定义。
● 在FORALL结构或语句当中,当指标名称值集经过计算之后,其指标名称就获得良定义。
● 执行一个输入输出语句内部由一个隐式DO列表给出的指令之后,隐式DO变量就获得良定义。
● 如果与一个过程的哑元相关联的实元获得非语句标签的良定义,那么引用该过程后,该哑元数据对象就获得了良定义。如果与该哑元相应的实元只是部分子对象获得非语句标签的良定义,那么那么引用该过程后,也只有该哑元数据对象相应的部分子对象获得良定义。
● 执行一个具有输入输出说明符IOSTAT=的输入输出语句使得被指定的整型变量获得良定义。
● 执行一个包含输入输出说明符SIZE=的READ语句使得被指定的整型变量获得良定义。
● 在没有错误条件存在的情况下,执行一个INQUIRE语句,使得在执行语句过程中被赋值的任何变量都获得良定义。
● 当一个字符存储单位获得良定义之后,所有通过该存储单位而建立存储关联的字符数据对象都获得良定义。
● 当一个数值存储单位获得良定义之后,所有通过该存储单位而建立存储关联的相同类型的数值数据对象都获得良定义。
● 当一个存储双精度实型数据的存储序列获得良定义之后,所有通过该存储序列而建立存储关联的双精度实型数据对象都获得良定义。
● 当一个不定存储单位获得良定义之后,所有通过该不定存储单位而建立存储关联的数据对象都获得良定义。
● 当一个存储默认复型数据的存储序列获得良定义之后,所有通过该存储序列而建立部分存储关联的默认实型数据对象都获得良定义。
● 如果一个默认复型数据的存储序列的各个部分都与其他的获得良定义的默认实型数据或默认复型数据建立了部分关联,那么该默认复型数据对象就获得了良定义。
● 如果一个数值序列结构或字符序列结构的所有成员都与其他的获得良定义的数据对象建立了部分关联,那么序列结构也就获得了良定义。
● 执行一个包含说明符STAT=的ALLOCATE语句或DEALLOCATE语句,使得被说明符STAT=指定的变量获得良定义。
● 执行在一个指针与良定义的目标之间建立关联的指针语句,就使得该指针获得良定义。
● 零尺度数组经过分配就使得该数组获得良定义。
● 如果一个派生类型对象的所有非指针直接成员(参见第6章)都具有默认初始化,那么该派生类型对象的分配导致其成员获得良定义。
● 调用一个过程使得其中所有零尺度的动态对象都获得良定义。
● 如果一个过程包含了一个非保留局部对象,该对象不是一个哑元,不能通过使用关联或宿主关联得到访问,也不具有ALLOCATABLE以及POINTER属性,并且属于派生类型,其中所有直接成员都指定为默认初始化,那么调用该过程,将导致该对象的成员获得良定义。
● 如果一个过程具包含一个具有INTENT(OUT)属性的哑元,并且该哑元作为一个派生类型对象,它的所有非指针直接成员都具有默认初始化,那么该哑元的成员都获得良定义。
● 如果一个非指针函数是一个派生类型对象,它的所有非指针直接成员都具有默认初始化,那么调用该函数将导致函数结果的成员获得良定义。
在程序运行过程当中的如下的事件能够导致变量的定义状态为去定义状态:
● 如果一个具有给定类型的变量获得了良定义,那么就导致与该变量相关联的具有不同类型的变量去定义。不过还有如下2种特殊的例外情况:
● 如果一个默认实型变量与一个默认复型变量部分关联,当实型变量获得良定义,同时实型变量在复型变量获得良定义时并不会去定义,那么该复型变量也不会去定义。
● 如果一个默认复型变量与另外一个复型变量部分关联,那么其中一个复型变量得到良定义不会导致另一个复型变量去定义。
● 如果一个函数经过计算,导致函数的实元或者是模块或公用块里面的变量获得了良定义,同时如果该函数还被一个表达式引用,而该函数的值在该表达式里面并不参与决定表达式的值,那么在该表达式完成计算之后,就导致那个实元或变量去定义。
● 执行了一个子程序里面的RETURN语句或END语句之后,就导致该子程序的所有作用域单位里面的局部变量,或属于递归调用的当前作用域单位里面的局部变量都为去定义状态。不过以下情况为例外:
● 具有SAVE属性的变量例外;
● 具有空公用属性的变量例外;
● 从宿主作用域单位访问得到的变量例外;
● 同时出现在一个子程序以及另外一个直接或间接引用了该子程序的作用域单位里的命名公用块里面的变量例外;
● 从一个模块访问得到的变量例外,同时要求该模块至少被另一个直接或间接引用该模块的作用域单位所引用。
● 在一个已经获得初始定义然而还没有得到进一步定义或重定义的命名公用块里面的变量例外。
● 如果在一个输入语句的执行过程中,出现了错误条件或者文件终止条件,那么由该语句的输入列表或名称列表集合所给出的所有变量都去定义。
● 如果在一个输入输出语句的执行过程中,出现了错误条件或者文件终止条件,那么部分或全部的隐式DO变量去定义。
● 执行一个自定义赋值语句可能导致其等号之前的部分或全部变量去定义。
● 执行一个说明了还未写入的纪录的直接访问输入语句,将导致由语句的输入列表所给出的所有变量去定义。
● 执行INQUIRE语句使得NAME=,RECL=,NEXTREC=变量去定义。
● 当一个字符存储单位去定义,那么所有与其相关联的字符数据对象单位去定义。
● 如果一个数值存储单位去定义,那么所有与其相关联的数值数据对象单位去定义。唯一的例外是导致数值存储单位的去定义的是由于给该单位定义了一个具有不同类型的数值数据对象。
● 如果一个双精度实型对象去定义,那么所有与其完全关联的双精度实型对象都去定义。
● 如果一个不定存储单位去定义,那么所有与其相关联的数据对象都去定义。
● 如果一个可分配数组去分配,那么它也就去定义。
● 针对一个还没有指定默认初始化的非零尺度对象执行ALLOCATE语句,如果执行成功,那么将产生一个去定义的对象。
● 如果在执行一个INQUIRE语句时出现错误条件,那么所有查询说明符变量去定义,在说明符IOSTAT=里面的变量例外。
● 调用一个过程会导致如下几种去定义情形:
● 没有关联任何实元的可选哑元去定义;
● 具有INTENT(OUT)属性的哑元去定义,除非是那些指定了默认初始化的变元的成员。
● 与一个具有INTENT(OUT)属性的哑元相关联的实元去定义。
● 如果一个哑元的子对象所关联的实元子对象去定义,那么它自身也去定义。
● 函数的结果变量去定义,例外是指定了默认初始化的结果的成员。
● 当一个指针的关联状态去定义或去关联,那么该指针即去定义。
● 当FORALL语句或结构执行完毕之后,指标名称去定义。而如果在相应的程序单位里面存在与之同名的变量,则该变量不受FORALL语句或结构的影响。