过程

6.2.1 什么是过程

Fortran编译系统提供内在函数,可以在任意程序单元中引用。但是在许多时候仅仅使用系统的内在函数并不能满足程序设计的需要,因此就需要自行编制相应的函数或子程序来扩充程序的处理能力。

过程是在程序的执行中可被直接调用的、封装在一起的、进行计算或处理的语句序列。它是任何一种过程型程序设计语言的重要组成部分,对Fortran语言也不例外。F90中,一个过程的定义就是指它是一个函数或是一个子程序。

过程的引用就是调用一个过程。建立过程的目的就是建立可多次重复执行的程序段,以便多次调用它们。通常过程是带有参数的,在Fortran中把参数称为变元(实元或哑元),过程定义中的变元是哑元,过程引用中的变元是实元。在调用过程时,要用实元代替哑元,这就是哑实结合

a)    分类

过程包括下面几种类型:

*      外部过程:它是在某个外部程序单元中定义的独立过程,或是用非Fortran语言编写的过程。

*      内部过程:在程序单元内部定义而且只能被该程序单元调用。

*      内在过程:由编译系统内部定义,不用任何附加声明或说明就可以可直接引用。

*      模块过程:它在模块中定义,可以被所有使用该模块的程序调用。包含过程的模块称为宿主。

*      过程  :如果一个哑元被指明为过程或作为过程名出现在过程引用中,那么该哑元代表的过程为一个过程。

*      语句函数:它是由单个语句定义的函数,其形式为:函数名([哑元名表])=标量表达式F90不推荐使用,因为它不符合过程的一般规则。

b)    特性

过程的特性包括,将过程分为函数和子程序的分类特性和它的哑元的特性,对于函数还包括有结果的特性。

一个哑元可以是一个虚拟数据对象、哑过程或作为选择返回指示符的星号。当一个哑元不是星号时,它可有OPTIONAL属性,表示对该过程引用时不需要有实元与该哑元结合。一个元数据对象的主要特性包括它的类型、种别值、形状、输入输出意向(INTENT)、是否可选(OPTIONAL)、是否一个指针(POINTER)或目标(TARGET)。过程的特性包括其接口是否显式给出、作为过程的特性(如果其接口显式给出)以及它是否可选。函数结果的主要特性包括它的类型、类别值、以及是否指针等。

6.2.2外部过程

外部过程是程序员编写的函数或子程序,它独立于主程序外部。外部过程可以单独以源文件存储和编译,也可以包括在主程序源代码的END语句后。外部过程本身也可以包含有内部函数或内部子程序。

a)    子程序

子程序以SUBROURTINE语句开始,END语句结束的过程。其一般形式为:

[前缀]SUBROUTINE子程序[(哑元列表)]

...

END [SUBROUTINE[子程序名]]

其中,哑元可以是变量名、数组名、过程名、指针名等均作为哑元哑元表内列出本程序体内要用到的全部哑元,列在括号内,彼此用逗号分隔(哑元都要在说明语句中说明类型)。前缀F90中新增的,它可以是:类型说明 [关键词]关键词 [类型说明]关键词是下面之一:RECURSIVE(F90),PURE(F95),ELEMENTAL(F95)。RECURSIVE表示过程时可以直接或间接地调用自身,即递归调用,其过程是递归过程。

子程序名只是来作标识,不代表任何值。哑元列表是子程序与调用单元之间进行数据传送的主要渠道,当有一个以上哑元时,它们之间用逗号隔开,如果没有哑元,则一对括号可以省略。从SUBROUTINE语句后面一直到END语句则是子程序体。它的说明部分应包括对哑元和子程序中所用变量、数组等的说明,它的执行语句部分完成运算和操作功能。其中的END语句或RETURN语句使执行流程返回到调用单元。

哑元表中的哑元个数,理论上不受限制,但从软件工程观点看,不宜过多,一般不应超过六、七个。如果太多,意味着该子程序的算法较复杂,应该把该过程再分解为几个子功能分别编写成几个子程序。

调用子程序时必须用一条独立的CALL语句,其形式为:CALL子程序名[(实元列表)]

例:读入3个整数,按大小順序重排。

INTEGER :: i,j,k

READ *, i,j,k

IF(i<j) CALL swap(i,j)

IF(j<k) CALL swap(j,k)

IF(i<j) CALL swap(i,j)

PRINT *,i,j,k

END

!

SUBROUTINE swap(p,q)

INTEGER :: p,q,r

r=p;p=q;q=r

RETURN

END

调用子程序时的实元是和哑元相同类型的变量、数组元素、数组和常数。当用CALL语句予以调用时,哑元和实元才按列表顺序一一对应,取得同一数值。但使用常数时要避免下例中的常数替换式,否则将导致不可预料的错误,应尽可能用变量作实元。

例:SUBROUTINE bai(x)

REAL :: x

x = 2. * x

END

 

s=1.

CALL bai(1.)

PRINT*,s

END

b)    函数

自定义函数和子程序比较相近,共同的特征是作为过程的集合,子程序的很多规则也可应用。两者的不同之处在于函数返回一个值,且通常不改变哑元的值,因此,它代表数学上的一个函数。而子程序通常完成一项更为复杂的任务,通过变元或其他手段返回几个结果。函数的一般形式为:

[前缀] FUNCTION函数([哑元列表])[RESULT(结果名)]

...

END [FUNCTION函数]

如果没有哑元,则哑元表是一个空括号。RESULT是关键字,要照写,后面括号内的变量名就是函数计算的结果值。函数结果变量名有值,必须在说明语句中说明类型,在程序执行部分中至少赋值一次。在引用时,它的值就是函数值。函数结果名不可列入哑元表。如果没有结果名,则函数名就是结果名。F90中之所以增添了结果名功能,就是为了区别函数字面上的名称(函数名)和实际运算结果变量的名称。

函数调用与调用内在函数形式一样,在主调程序任何处,作为表达式的一项写下:函数名(实元表)就完成调用。如果函数无哑元,则调用形式是在表达式中写上函数名,后跟空括号:函数名()。系统在该位置上返回以该实元为自变量的函数结果值,参加表达式的运算。

例如,下面都是合法的FUNCTION语句:

FUNCTION FUN1

FUNCTION FUN2()

INTEGER FUNCTION FUN3(A,B)

RECURSIVE FUNCTION FUN4(X,Y,Z)

REAL RECURSIVE FUNCTION FUN5(M,N) RESULT(R_FUN5)

例:将一个4字节的整数用16进制表示出来。[e_622_01.f90]

FUNCTION hex(n)

CHARACTER(LEN=8) :: hex

CHARACTER(LEN=1) :: h(0:15)=(/'0','1','2','3','4','5','6',&

'7','8','9','A','B','C','D','E','F'/)

INTEGER :: n,j,nn

hex = ' '

DO j=8,1,-1

nn=n/16

hex(j:j)=h(n-16*nn)

IF(nn==0) EXIT

n=nn                 ! 将n指定为INTENT(IN)将导致错误

END DO

END FUNCTION

 

PROGRAM main

CHARACTER(LEN=8)::hex    !函数名的类型在调用单元也须加以声明

INTEGER::i

PRINT *,'Input a positive integer, or negative one to stop:'

DO

READ *,i; IF(i<0) EXIT

PRINT *,hex(i)

END DO

END

在本例中,函数值是赋给函数名的,如果要将值赋给非函数名的另一结果名,则

FUNCTION hex(n) RESULT(hx)

CHARACTER(LEN=8)::hx

hx(j:j)=

但在此例中就没有必要用函数了,可以用子程序,如:

SUBROUTINE hex(n,hx)

CHARACTER(LEN=8),INTENT(OUT)::hx

例:分形图形的计算。分形(fractal)是Mandelbrot将自然界的复杂图形(如海岸线,树叶形,雪花结晶型)进行数学理想化后提出的种概念,其核心是图形的任意细小部分都与图形的整体具有自相似性,这种图形的维数不是整数,而是有分数维的。一个典型例子是Koch曲线,它有雪花形,可通过对一段直线反复进行某一简单操作而得到,把这个过程用数学语言描述,即为在复空间定义的一种简单迭代过程,它是一个图形的缩小映射,从而产生自相似曲线。一种简单的缩小映射是:

迭代过程是用作为初始值代入得,然后反复将得到的值作为初始值代入得序列复数值,,,,…。然后将此序列复数在复空间中绘出即得到相似曲线。程序中要计算迭代到n阶所需的迭代函数,其个数为:用0记,01记

1阶:0,1

2阶:00,01,10,11

3阶:000,001,010,011,100,101,110,111[e_622_02.f]

c)    EXTERNAL属性和哑过程

指定EXTERNAL语句或属性说明实元实际上是外部过程。其一般形式为:

类型定义语句:类型,EXTERNAL :: 外部函数名[,外部函数名]…

EXTERNAL语句:EXTERNAL [外部函数名][,子程序名][,块数据名]…

哑元也可以是一个过程,这时作为哑元的过程称为过程。只有在多层调用(至少两层调用)时才能用哑过程。如果要用外部过程名作实元,则过程名必须出现在一个EXTERNAL语句中,或被该作用范围中的一个类型语句指明具有EXTERNAL属性,或在该作用范围中被一个接口块INTERFACE声明为一个过程。这就是说,在主调程序中,除了把一个实际存在的过程名作实元与哑过程名结合外,还需对该实过程名作特别说明,以便让编译系统明白,该实元不是一般的简单变量,而是一个函数(或子程序),或者用接口块来通知编译系统。

例:REAL FUNCTION CALCULATE(A,B,FUNC)

EXTERNAL FUNC

REAL,INTENT(IN) :: A,B

REAL F,X

F=FUNC(X)  !调用自定义的外部函数

END FUNCTION CALCULATE

程序中的EXTERNAL语句说明函数FUNC是外部函数,也可用类型声明语句说明函数FUNC具有EXTERNAL属性,把程序改写为:

例:REAL FUNCTION CALCULATE(A,B,FUNC)

REAL EXTERNAL :: FUNC

也可以用显式的接口块来代替EXTERNAL语句,把上面的程序改写为:

例:REAL FUNCTION CALCULATE(A,B,FUNC)

INTERFACE

REAL FUNCTION FUNC(X)

REAL,INTENT(IN) :: A,B

END FUNCTION FUNC

END INTERFACE

REAL F,X

F=FUNC(X)  !调用自定义的外部函数

END FUNCTION CALCULATE

例:采用梯形公式近似求[a,b]区间上函数f(x)的定积分

FUNCTION INTEGRAL(F,A,B,N) RESULT(INT_RES)

IMPLICIT NONE

REAL :: INT_RES

INTERFACE

FUNCTION F(X)

REAL :: F,X

END FUNCTION

END INTERFACE

REAL,INTENT(IN) :: A,B

INTEGER,INTENT(IN) :: N

REAL :: H,SUM

INTEGER :: I

H=(B-A)/N

SUM=(F(A)+F(B))/2

DO I=1,N-1

SUM=SUM+F(A+I*H)

END DO

INT_RES=H*SUM

END FUNCTION INTEGRAL

在上面的函数INTEGRAL中,哑元F被一个接口块说明为一个过程,因此它是一个过程。在调用该过程时,必须对该哑元提供一个另外定义的函数过程作为实元与之结合,同时也需要写上接口块。

当用EXTERNAL语句说明SIN,COS等名字时,它们被解释为自定义函数,而非内在函数(三角函数)。

d)    ENTRY语句

ENTRY语句允许在特定的可执行语句中插入外部过程或模块过程。内部过程不能有ENTRY语句。使用语句的目的是,把多个有条件进入过程的相关函数或子程序组织起来。它的形式和用法与函数和子程序类似:语句包括一个入口点的名称,一个可选哑元列表,一个可选的RESULT结果名(在函数的情况下)。一个过程可以有一个或多个ENTRY语句。F90的块结构如CASE语句所实现的控制流方式比通过ENTRY方式为好,故F90中并不提倡用ENTRY语句。

函数过程中的ENTRY语句:定义了另一个函数,函数名由ENTRY语句指定,结果名即函数名或由RESULT指定。

例:计算双曲三角函数sinhcoshtanh的函数过程。

REAL FUNCTION TANH(X)

TSINH(X)=EXP(X)-EXP(-X)

TCOSH(X)=EXP(X)+EXP(-X)

 

TANH=TSINH(X)/TCOSH(X)

RETURN

 

ENTRY SINH(X)

SINH=TSINH(X)/2.

RETURN

 

ENTRY COSH(X)

COSH=TCOSH(X)/2.

RETURN

END

函数过程中的ENTRY语句,定义了另外两个函数,函数名由ENTRY语句指定。

例:PROGRAM TEST

...

CALL WAHAHA(A,B,C,D)

...

END

SUBROUTINE HAHA(X,Y,Z)

...

ENTRY WAHAHA(Q,R,S,T)

...

END SUBROUTINE

6.2.3 变元的性质

a)    INTENT属性

哑实结合是在两个程序单元间传递数值的主要手段,主程序中实元2.0与过程中哑元X结合,就使X有值2.0,也即把主程序中2.0的值传递给子程序中的X,该值可供子程序运算。反之,如果子程序中的变量Y在子程序执行完后有值3.0,它与实元R结合后则使调用程序单元中的实元变量R得值3.0。

F77中,不能确切地说明哑元的目的。它们到底是用于把数据传入到过程中的,还是用于把数据传出到调用它的程序单元中的,或是两者兼而有之的,这个概念是含糊的。在F90中,为了避免当过程内部变量值变化后返回到引用的程序单元时可能造成的混淆情况,在过程的变量类型的定义中,可以对哑元指定意图说明的INTENT属性。哑元按数据传输特性可分为输入输出两用、仅用于输入和仅用于输出。其一般形式为:

在类型定义语句中:类型,INTENT(意图说明符) :: 哑元名表

或用INTENT语句 :INTENT(意图说明符) :: 哑元名表

意图说明符为以下字符串:

IN    指明哑元仅用于向过程提供数据,过程的执行期间哑元不能被重定义或成为未定义的,相联合的实元可以是常数、变量、数组以及它们的算术表达式。

OUT   指明哑元用于把过程中的数据传回调用过程的程序,与之相结合的实元只允许是变量,不得为常数或算术表达式。

INOUT 指明哑元既可以用于向过程提供数据,也可用于向调用程序返回数据,与之相结合的实元只允许是变量。

INTENT属性不能在主程序说明语句中出现,只能在过程的哑元说明语句中使用。它是可选的,可省略。但现代特性的编程中应提倡使用INTENT属性,因为这样能增加可读性和可维护性,还能防止编程中的一些错误。因为一旦哑实结合,哑元和实元始终是同一个值,如果过程中给有属性INTENT(IN)的哑元重新赋值,也将改变调用程序单元中实元的值,而这是不应该的。这样,如在程序执行部分中误把有INTENT(IN)属性的哑元赋值时,操作系统就会提示。

例:给出10个步长的分布值,打印分布图形。[e_623_01.f90]

b)    SAVE属性

在过程中变量的定义和取值当过程被调用结束后有可能变为不确定的,因此当过程再次被调用时,变量的取值在不同编译器下可能取值不同。为了避免这种情况的出现,在F77中可用SAVE语句,在F90中对变量增加了SAVE属性,其形式为:

在类型定义语句中:类型,SAVE, [其它属性] :: 变量名表

或用SAVE语句   SAVE [变量名表]

在过程中设定初始值时要注意,类型定义中的初始值赋值法隐含了SAVE属性。[e_623_02.f90]

c)    关键字变元

哑实结合必须遵循三个一致的原则,否则运行出错:哑元与实元位置一致;哑元与实元个数一致;哑元与实元类型一致。这就要求记住每个哑元的名及其位置,阅读实元表时对其中每个表达式要追溯到它原来的哑元是什么,非常不便,为此F90可以通过如下方法放宽这三个一致的原则:用关键字变元放宽位置一致;用可选择变元放宽个数一致;用类属过程放宽类型一致。

关键字变元是调用过程时变元的一种现代形式,它的写法是:哑元名=实元表达式。调用时,实元表中不仅要写出实元表达式,还要写出它对应的哑元变量名,这个哑元变量称为关键字,并用‘=’号与实元连接。使用关键字后,就不必记住哑元原来的次序,填写的实元次序可以任意。例如,对于子程序语句(对函数一样可用):

SUBROUTINE FACTORIAL(N,F_VALUE)

主调程序中,调用语句使用关键字变元时形式如下:

CALL FACTORIAL(N=M,F_VALUE=F_M)

CALL FACTORIAL(F_VALUE=F_M,N=M)

F90也允许在调用语句中,前面部分实元不用关键字变元,只从某一个变元开始用关键字变元。此时,前面未使用关键字变元仍要保持与原来哑元次序相同,后面使用关键字变元的部分可以按任意次序排列。例如,对于

SUBROUTINE TEST(A,B,C,D)

调用时可以使用如下形式:

CALL TEST(1,10,100,1000)

CALL TEST(1,10,D=1000,C=100)

CALL TEST(D=1000,C=100,A=1,B=10)

但是,以下形式是错误的:

CALL TEST(10,1,C=100,D=1000)      头两个实元次序颠倒

CALL TEST(1,10,C=100,1000)        关键字变元后面都要写成关键字变元形式

主调程序中如采用关键字变元调用过程,就必须写出被调子程序的接口块。

d)    可选择变元与OPTIONAL属性

某些过程中,虽然哑元表中列出好几个哑元,但在实际调用时不一定每次都全部用到。这种情况下,F90允许只对哑元表中部分哑元作哑实结合,另一部分哑元则按需要可选择结合,称为选择变元。例如内在数组函数SUM,它的完整的函数及哑元表为SUM(ARRAY,DIM,MASK),其中,后二个哑元DIM、MASK就是可选择变元。主调程序调用时,可以不选后两个变元,只对第一个变元作哑实结合,如SUM(A);也可选上第二个哑元,如SUM(A,DIM=2);或后两个可选变元都选中,如SUM(A,DIM=2,MASK=A>0)。

编写有可选择变元的过程时,可选择变元必须说明具有OPTIONAL属性,并要使用PRESET内在函数。一个哑元是否可选哑元,看它是否有OPTIONAL属性,有OPTIONAL属性的哑元是可选变元,没有OPTIONAL属性的哑元是必选的。内在函数PRESET用来反映它的自变量是否在程序执行部分中出现。PRESET(A)的值是一个逻辑值,当A出现(被使用到)时,函数值为真,否则为假。利用PRESET函数的真假值,可以通过IF构造作出变元是否出现时的不同算法。

例如,要求编子程序,既能求四边形同长(A+B+C+D)的值,也能求三角形周长(A+B+C)的值。此时D就是可选择变元,并规定当D不出现时,置D值为零。子程序如下:

SUBROUTINE SUM(S,A,B,C,D)

IMPLICIT NONE

REAL,INTENT(IN) :: A,B,C

REAL,INTENT(IN),OPTIONAL :: D

REAL,INTENT(OUT) :: S

REAL :: TEMP

IF(PRESET(D)) THEN

TEMP=D

ELSE      

TEMP=0.

END IF

S=A+B+C+TEMP

END SUBROUTINE SUM

e)    哑元改名

过程的优点是具有广泛通用性,一旦编好,解各种问题的主程序都可调用它。但是在为不同目的而使用时,具体问题的物理名称不同。为了加强可读性与可维护性,在不同领域使用过程时,需把哑元名称改为与该领域中的物理名称一致,而F90允许改变变元名称。名称的改变是在接口块中进行的,所以主调程序中要写出接口块。

例如,对于上面求边长的子程序,如调用时欲把哑元名A,B,C,D改为物理意义明确的名称UPPER,DOWN,LEFT,RIGHT,只需在主调程序中写入接口块,在接口块的哑元表中用新的哑元名即可:

PROGRAM SUMMATION

INTERFACE

SUBROUTINE SUM(S,UPPER,DOWN,LEFT,RIGHT)

IMPLICIT NONE

REAL,INTENT(IN) :: UPPER,DOWN,LEFT

REAL,INTENT(IN),OPTIONAL :: RIGHT

REAL,INTENT(OUT) :: S

REAL :: TEMP

END SUBROUTINE SUM

END INTERFACE

READ *, UPPER,DOWN,LEFT,RIGHT

CALL SUBROUTINE SUM(S,UPPER,DOWN,LEFT,RIGHT)

……

END PROGRAM SUMMATION

f)    INTRINSIC属性

EXTERNAL语句或属性说明的实元是外部过程相对应,INTRINSIC语句或属性用来说明实元实际上是内在过程。其一般形式为:

类型定义语句:类型,INTRINSIC :: 内在函数名[,内在函数名]…

INTRINSIC语句:INTRINSIC 内在过程名[,内在过程名]…

内在过程名必须是内在过程的通用名或专用名。如果是专用名,则可以在其作用范围单元中作为一个过程的实元,但它必须出现在一个INTRINSIC语句中,或被该单元中的一个类型声明语句指明具有INTRINSIC属性。需要注意的是,一个内在过程名只能在INTRINSIC语句中出现一次,并且不能同时出现在INTRINSIC语句和EXTERNAL语句中。

例:PROGRAM MAIN

REAL F

REAL,INTRINSIC :: ALOG

F=CALCULATE(0.,1.,ALOG)   !使用内在函数ALOG作实元

END PROGRAM

注意这里必须用专用名ALOG,而不能用通用名LOG。

6.2.4 其它过程

a)    内部过程

内部过程是包含在程序单元里的CONTAINS结构中的函数过程和子程序过程,只有包含它们的程序单元才能够使用该过程。CONTAINS结构标志着程序中可执行部分和程序包含的任意内部子程序之间的分界,它将内部过程与主过程分开。如果程序包含内部子程序,则必须有CONTAINS结构。在F77中,—个源文件可以包含一个主程序和几个分别独立的函数或子程序(相当于F90中的外部过程)。在F90中,可以将若干个过程用CONTAINS结构包含在主程序里,它们与宿主程序单元共享变量名。而且,外部过程等其它程序单元都可以有自己的内部过程。

例:program internal

real a,b,c

call find

print *,c

contains

subroutine find

read *, a,b

c=sqrt(a**2+b**2)

end subroutine find

end

使用内部过程的规则:在宿主中不要定义子程序名和函数名的类型,也不能指定它们是有EXTERNAL属性。宿主中的变量名和数组名等在内部过程中有效,有相同的数值。但同一名若在内部过程中又进行了类型声明,则此名被视为其过程中的独立变量,无相同的数值。内部过程中也可引用另一内部过程。

因此,写成内部过程的子程序有一个重要特征,即它们通常没有说明语句。它们使用到的变量等实体的说明已统一出现在主程序说明部分中,它们的变量与主程序同名变量的值是相通的。主程序内可以直接引用内部过程变量的值,或赋之以值,也即主程序内定义的变量是全局的,作用于以PROGRAM语句到END PROGRAM语句之间的所有场合。内部过程的第二个重要特征是它们一般没有哑元。主程序调用时也不需要哑实结合,因为如果内部过程的变量在主程序中被说明,它们就可以直接引用,无需通过哑实结合来传递值。调用内部过程时只要简单地写一个过程名。这是内部过程与外部过程的很大区别。在特殊情况下,内部过程也可保留由自己单独说明的少量哑元。如果变量是在内部子程序中单独说明的,它只是局部变量,作用域只能是局部的,对其主程序的其它部分不起作用。主程序调用时仍需通过哑实结合来传送数据。

b)    递归过程

F90允许过程递归调用,即在过程内又调用过程自身,这种过程称为递归过程。递归过程包括递归函数和递归子程序,它只是外部过程或内部过程的一种特殊情况,则过程定义语句的前缀中必须出现关键字RECURSIVE。递归调用时,过程直接或间接引用自身或由该过程中的ENTRY语句定义的函数。

几乎所有递归程序都可用一般程序替代。但递归程序简单明白,清晰易懂,有利于阅读与维护。某些问题当用一般程序编写显得太复杂时,可以用递归程序来实现。递归程序的缺点是效率低,比起一般程序来,在CPU时间与占用内存等方面的开销都要大得多。因此,要根据具体问题的需要,决定是写成递归程序还是一般程序。

下面是一个经典的递归例子,求N的阶乘N!。若用递归过程编程,可使用程序简单明了。因为N!=N(N-1)!=…=N(N-1)(N-2)…2*1,如果设N!的函数过程名为FACTORIAL(N),则求(N-1)!调用FACTORIAL(N-1),如此递推下去,直到求得FACTORIAL(1)=1!为止。然后,系统又自动地一层层向上回归,最后求得FACTORIAL(N)的值。上述递推与回归的过程即为递归过程。[e_624_01.f90]

调用递归函数的主程序写法与非递归函数的写法没有什么两样,只是因被调用的是递归函数,最好写明递归函数接口块。在具体调用时,过程只需调用一次,填进实元即可,各级递归调用与回归会自动进行。

为了加深对递归过程的理解,再来看一个关于汉诺塔(The Tower of Hanoi)的例子。据传说,在汉诺有座寺庙,里面有一个举行仪式的塔,它由3个柱子和柱子上的64个金盘组成。这些金盘大小不一,在修建寺庙时,64个金盘放在A柱子上,并且最大的在最下面,往上依次减小,最小的在最上面。

        |                    |                   |
       (1)                   |                   |
      (_2_)                  |                   |
     (__3__)                 |                   |
    (___4___)                |                   |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        A                    B                   C

庙里的和尚的工作就是移动这些金盘,直到64个金盘被移到B柱子上,到那时就是世界末日来临之时。移动这些金盘必须遵循两个规则:把金盘从一个柱子移到另一个柱子上,一次只能移动一个金盘;一个大的金盘永远不能压在一个较小的金盘上面。这个问题可以用递归子程序来解决。其思路是:为了将上面4个盘子从A移到B,需先将上3个盘子从A移到C,然后将第4个盘子从A移到B,再将上3个盘子从C移到B。为了完成3个盘子的移动,又需先完成2个盘子的移动....。这个递归过程可用递归子程序完成。[e_624_02.f90]

例:求二项式的展开系数。已知二项式的展开式为:,共有N+1个系数,其系数呈杨辉三角形形式:

                   1

                 1     1

              1     2     1

           1     3     3     1

        1     4     6     4     1

     1     5    10    10     5     1

这些系数间有明显的规律,即除了首尾两项系数为1外,当N>1时,N阶中间各项系数是N-1阶的相应两项系数之和,也即如果把N阶的系数列为数组C(I,N),则除了C(1,N)和C(N+1,N)恒为1外,C(I,N)=C(I,N-1)+C(I-1,N-1)。当N=1时,只有两个取值为1的系数C(1,1)和C(2,1)。因此,任意N阶的系数可由N-1阶的系数求得,直到N=1为止,可写成递归子程序。[e_624_03.f90]

c)    类属过程

类属过程是过程的一种,它允许用不同类型的实元与同一个哑元结合,也即放宽了哑实元结合时类型必须一致的条件。在内在基本函数中,已经有许多类属过程,例如求绝对值的函数ABS(X),哑元为X,与它结合的实元可以是整型、实型与复型,也即使用同一个过程名ABS,调用函数ABS(-1)、ABS(-1.)、ABS(-1.,1.)都是合法的,并且一般来说,其函数值类型就取实元的类型,如上述前两个引用的函数值是1与1.0。

编写类属过程的方法是先编写着干个功能相同的过程,它们分别以整型、实型、复型等作哑元类型。而后在主调程序中编写接口,为接口取一个统一的名,接口内分别列出哑元类型不同的过程说明部分语句。这个统一的接口名就是类属过程名,可以在后面执行部分中用不同类型的实元作哑实结合。

例如,要编写求两数之和的类属函数时,分别编写哑元是实型和整型的函数:

FUNCTION SUM_REAL(A,B) RESULT(SUM_REAL_RESULT)

REAL :: A,B,SUM_REAL_RESULT

SUM_REAL_RESULT=A+B

END FUNCTION SUM_REAL

FUNCTION SUM_INTEGER(A,B) RESULT(SUM_INTEGER_RESULT)

INTEGER :: A,B,SUM_INTEGER_RESULT

SUM_INTEGER_RESULT=A+B

END FUNCTION SUM_INTEGER

现在把这两个函数过程综合成一个类属函数,类属函数名取为MY_SUM,在主调程序应写明如下接口:

PROGRAM SUMMATION

INTERFACE MY_SUM

FUNCTION SUM_REAL(A,B) RESULT(SUM_REAL_RESULT)

REAL :: A,B,SUM_REAL_RESULT

END FUNCTION SUM_REAL

FUNCTION SUM_INTEGER(A,B) RESULT(SUM_INTEGER_RESULT)

INTEGER :: A,B,SUM_INTEGER_RESULT

END FUNCTION SUM_INTEGER

END INTERFACE

IMPLICIT NONE

REAL :: X,Y

INTEGER :: I,J

READ *, X,Y,I,J

PRINT *, MY_SUM(X,Y),MY_SUM(I,J)

END PROGRAM SUMMATION

d)    多层调用

主程序调用过程,称为第一层调用。被调用的过程可能也要调用更小的下属过程,这种调用称为第二层调用,依此类推。这样逐层调用下属过程,称为多层调用。

在多层调用中,函数可以调用下属的函数过程或子程序过程,子程序过程也可调用下属的函数过程或子程序过程,并不要求被调用的过程与自己的性质相同。多层调用中的程序控制流程是:控制从主程序(第0层)的第一个执行语句开始,顺次往下执行,当遇到调用第一个第一层过程时,控制转入该过程的第一条可执行语句,并把第0层中的实元值赋给该第一层过程的哑元,顺次往下执行。如果再遇到调用第二层过程的语句,控制又转入该层过程的第一条可执行语句,并把第一层的实元值赋给第二层的哑元。如果最下一层子程序内没有调用语句,则控制遇到过程结束语句时,返回上层过程调用语句后,继续执行尚未执行的语句,直到第一层过程结束语句,控制再返回到主程序的调用语句,继续执行完后面的语句。直至遇到主程序结束语句,结束控制。

因此,这个控制机制是按顺序进行的,这一点也可由执行Visual Fortran的Debug功能时,跟踪过程的顺序执行和变量的变化可见。

6.2.5 过程接口

a)    接口形式

一个内部过程总是由程序单元中的语句来调用的。一般来讲,编译程序知道内部过程的一切情况,如知道该过程是一个函数或子程序、过程名、哑元的名字、变量类型和属性、函数结果的特性等等。这个信息的集合被称为过程的接口(interface)。对于内部过程、内在过程和模块,过程接口对编译程序而言是己知的和显式给出的,故称显式接口。

由于主调程序与被调过程是分别编译的,而F90扩充了过程的许多功能,这些功能单靠简单的调用语句无法反映,编译系统也无法知晓。如在调用一个外部过程或一个哑过程时,编译系统通常不知道该过程的各种情况,这种接口是隐式的。在F77中可用EXTERNAL语句来指明一个外部过程或哑过程,但此语句仅说明每一个外部名是一个外部过程名或哑过程名,并没有指明过程的接口,所以接口仍是隐式的。为了全面准确地通知编译系统,在主调程序中有时需要加入接口块,以说明主调程序与被调程序的接口。接口块是F90中引进的新颖程序块,它显式指明了过程接口的机制。通过接口块可用为一个外部过程或哑过程指明一个显式的接口。这比EXTERNAL语句提供了更多的信息,也提高了程序的可读性。

过程接口确定过程被调用的形式,它由过程的特性、过程名、各哑元的名字和特性以及过程的类属标识符(可以省略)组成,一般它们都被写在一个过程的开头部分。此接口块被放在主调程序的说明部分中,通常还应写在类型说明语句之前,它的内容是被调用的过程中的说明部分,功能是通知编译系统,主调程序调用的过程中各种变元的类型、属性、性质等。

b)    INTERFACE语句

接口块应写在主调程序(主程序或过程)的说明部分中,一般是写在最前面:

PROGRAM 程序名

接口块

主调程序内变元说明

执行语句

END PROGRAM

过程接口块的—般形式为:

INTERFACE [类属说明]

[接口体]…

[模块过程语句]…

END INTERFACE [类属说明]

其中类属说明的形式为:

*          类属名                 ->     类属过程

*          OPERATOR               ->     超载操作符、自定义操作符

*          ASSIGNMENT(=)          ->     超载赋值号

接口体的形式为:

        函数语句

[说明部分]

函数END语句

        子程序语句

[说明部分]

子程序END语句

模块过程语句的形式为:MODULE PROCEDURE 过程名表

这里,接口体是过程头的精确拷贝,定义了过程中的变量和函数结果。使用接口块时应该注意的是:

*        接口块以INTERFACE语句开始,END INTERFACE语句结束,块内只能取被调用过程中的说明部分,不允许出现任何可执行语句。接口块内的语句构成接口体。

*        接口体中不能含有ENTRY语句、DATA语句、FORMAT语句、语句函数语句。

*        接口块不允许出现在BLOCK DATA程序单元中。

*        接口块中可以有多个接口体,即一个接口块中可以说明多个被调用过程,每个过程用自己的开始语句与结束语句定界,排列次序任意。

例:interface

subroutine swap(x,y)

real x,y

end subroutine

end interface

real a,b

read *,a,b

call swap(a,b)

end

subroutine swap(x,y)

real x,y

z=x;x=y;y=z

end subroutine

例:求正方矩阵对角元的和(trace)。[e_625_01.f90]

例:INTERFACE

SUBROUTINE EXT1(X,Y,Z)

REAL,DIMENSI0N(100,100)::X,Y,Z

END SUBROUTINE EXT1

SUBROUTINE EXT2(X,Z)

REAL X

COMPLEX(KIND=4) Z(2000)

END SUBROUTINE EXT2

FUNCTION EXT3(P,Q)

LOGICAL EXT3

INTEGER P(1000)

LOGICAL Q(1000)

END FUNCTION EXT3

END INTERFACE

这个接口块对于三个外部过程EXT1、EXT2和EXT3说明了显式接口。它是无类属名的接口块。

c)    必需接口

接口块并不是每个主调程序都必须写的。若仅使用F77语言编写的子程序,则无需在主调程序单元中写接口块。但若使用F90提供的现代化手段编写程序,通常需要在引用程序单元中写入接口块,否则编译容易出错,而且F90不提倡使用COMMON语句进行单元间的数据传递,其功能已由模块中的接口块代替。确切地说,凡遇下列情况之一时,主调程序必须有接口块:

1、如果外部过程具有以下特征:

*        过程的哑元有可选择属性。

*        过程的哑元是假定形数组、指针变量、目标变量。

*        函数过程的结果是数组或指针。

*        对于字符型函数过程的结果、其长度不是常数,也非假定长度(*)。

2、如果调用过程时出现:

*        实元是关键字变元。

*        用一个类属名调用。

*        用超载赋值号(对于子程序)。

*        用超载操作符(对于函数)。

3、如果过程前缀关键词是ELEMENTAL。

d)    超载操作符

超载操作符的形式仍是系统内部操作符,如+-、*、/等,但通过编出恰当的过程,可以扩充这些操作符的功能。例如;‘+’本来用于对数值作算术操作,但经恰当的编程后‘+’也可用于字符型操作,这就像车辆超载一样,故称为超载操作符。定义超载操作符,需先编写一个实现此超载(扩充)功能的函数过程,再在主调程序中编写过程的接口,在接口语句后加上超载说明,其一般形式为:

INTERFACE OPERATOR(被超载使用的操作符号)

例如,要使‘+’超载能执行如下操作:把两个字符串的最后一个字母接起来。实现超载时,先编一个能实现该操作功能的函数:

FUNCTION ADD(A,B) RESULT(ADD_RESULT)

IMPLICIT NONE

CHARACTER(LEN=*),INTENT(IN) :: A,B

CHARACTER(LEN=2) :: ADD_RESULT

ADD_RESULT=A(LEN_TRIM(A):LEN_TRIM(A))//B(LEN_TRIM(B):LEN_TRIM(B))

END FUNCTION ADD

其中内在函数LEN_TRIM为字符串去掉了尾部空格后的实际长度。现在要把这个函数功能定义为超载的操作符‘+’,则应在主调程序中编写如下接口块:

INTERFACE OPERATOR(+)

FUNCTION ADD(A,B) RESULT(ADD_RESULT)

IMPLICIT NONE

CHARACTER(LEN=*),INTENT(IN) :: A,B

CHARACTER(LEN=2) :: ADD_RESULT

END FUNCTION ADD

END INTERFACE

接口的作用是向编译系统提示,遇到操作符‘+’时,如果操作数不是数值,就不是原来意义的加法,操作含义要到 FUNCTION ADD中找。当主调程序有了上述接口块后,在下面执行部分中执行字符串操作CH1+CH2时,+号作超载用。[e_625_02.f90]

e)    自定义操作符

如果用内部操作符超载后仍不能达到算法要求的功能,还可自定义一个新形式的操作符作崭新的操作。自定义新操作符可由字母组成,两边用小数点(.)作定界符,以便与一般变量用关键字区别开来。它的形式与逻辑操作符.AND.、.OR.等形式一致。与我们可定义名义上的加法.add.,减法.minus.等。操作符名最多有31个字符组成,名称不得与FORTRAN关键字相同,如不可取.AND.或.GT.等。[pi.f90]

f)    超载赋值号

赋值号赋值号(=)也可超载。根据规定,赋值号两边的类型必须相同,如两边都是数值型,或者都是逻辑型、字符型等。但F9O也允许使用超载赋值号,即允许把一个类型的表达式赋给另一类型的变量。例如,可以把逻辑表达式赋给一个数值型变量,并使之构成—一对应关系。

实现赋值号超载的方法是,先编一个实现两个不同类型变量值之间—一对应关系的子程序过程(非函数过程),而后在主调程序中编一个接口,用ASSIGNMENT接口语句实现给赋值号定义不同的语义。接口语句的形式是:

INTERFACE ASSIGNMENT(=)

在具有ASSIGMENT选择的接口块中,包含的子程序过程只有两个变元,第一个具有INTENT(OUT)或INTENT(INOUT)属性,第二个具有INTENT(IN)属性。作赋值运算时,第一个变元为被赋值的对象。

注意:要实现超载赋值号功能,必须编成子程序过程形式。相反,要实现超载操作符功能,必须编成函数过程形式,不可混用。

例:编一程序把逻辑量超载赋值给整型变量。先编一个实现这一功能的子程序,

SUBROUTINE LOG_INT(I,L)

IMPLICIT NONE

LOGICAL, INTENT(IN) :: L

INTEGER, INTENT(OUT):: I

IF(L) I=1

IF(.NOT.L) I=0

END SUBROUTINE LOG_INT

再在主程序内编写接口,

INTERFACE ASSIGNMENT(=)

SUBROUTINE LOG_INT(I,L)

IMPLICIT NONE

LOGICAL, INTENT(IN) :: L

INTEGER, INTENT(OUT):: I

END SUBROUTINE LOG_INT

END INTERFACE

此后,在主调程序内可任意赋值。此接口块定义的赋值运算与通常的赋值含义不同,但在程序中仍可用等号“=”来使用。如I=1是通常的赋值,而I=.TRUE.是超载赋值,但I的取值认为1。

6.2.6 作用域

a)    作用域单元

在用带有语句标号的GOTO语句实现语句间的转移时会遇到这样一个问题,即程序中在什么情况下可以使用相同的语句标号而不会产生歧义?这就是语句标号的作用域问题。实际上主程序或每个过程都有—套独立的语句标号,包括宿主程序含有几个内部过程的情况。类似地,对变量名称也会有这样的问题,这就是名称的作用域问题。对于语句标号来说,它的作用域是主程序或过程,但不包括它含有的内部过程,相同的语句标号可以用在宿主程序和内部过程里而不会产生歧义。

F90标准以作用域单元的形式来定义名称。作用域单元是一个程序或程序的一部分,在作用域单元中定义一个名称,这个名称在作用域单元中有效。作用域单元可以是整个程序、程序单元、一个单独的语句或语句的一部分。名称可以是常量名、变量名、过程名、操作符或任何其它名称。名称可以用于程序、过程、变量、数组、哑元、命名常量、派生类型或块结构。名称有三种:全局名称、局部名称和语句名称。

作用域单元有以下几种:1、派生类型定义;2、过程接口体,不包括其内部的派生类型定义和过程接口体;3、程序单元或过程,不包括内部的派生类型定义、过程接口体以及内部过程。

下面是包括5个作用域单元的例子,作用域单元可以包含其他作用域单元。

MODULE SCOPE1                            ! Scoping unit 1

...                                  ! Scoping unit 1

CONTAINS                                 ! Scoping unit 1

FUNCTION SCOPE2                      ! Scoping unit 2

TYPE SCOPE3                      ! Scoping unit 3

...                          ! Scoping unit 3

END TYPE SCOPE3                  ! Scoping unit 3

INTERFACE                        ! Scoping unit 3

...                          ! Scoping unit 4

END INTERFACE                    ! Scoping unit 3

...                              ! Scoping unit 2

CONTAINS                             ! Scoping unit 2

SUBROUTINE SCOPE5                ! Scoping unit 5

...                          ! Scoping unit 5

END SUBROUTINE SCOPE5            ! Scoping unit 5

END FUNCTION SCOPE2                  ! Scoping unit 2

END MODULE SCOPE1                         ! Scoping unit 1

b)    名称的作用域

*          全局名称

全局名称用来识别程序单元、公共块和外部过程。全局名称在程序的任何地方都是有效的,所以只能在程序中定义一次。例如,如果用户在一个程序中使用了名为Son的子程序,就不能再在该程序中使用名为Son的公共块或函数。

*          局部名称

局部名称是用来识别变量(标量和数组)、常量、命名常量、语句函数、内部过程、模块过程、哑元过程、内在过程、一般标识符、派生类型和名称列表组的名称。派生数据类型的成员和关键字变元(哑元)也是局部名称。局部名称可以覆盖全局名称和同一程序单元中的其它局部名称(关键字变元、类属名称和公共块名称除外)。如果一个名称对某个程序单元是局部的,同样的名称即可以作为全局名称又可以作为其它程序单元中的局部名称。

*          内在过程的双重名称

因为FORTRAN语言的关键字不予保留,所以用户可以创建名称和FORTRAN内在过程名称一样的变量、常量或过程。一旦创建了这样的名称,原来的内在函数就不能再被访问。例如,下面定义了新的函数sin:

SUBROUTINE sub

...

CONTAINS

FUNCTION sin(x)

...

END FUNCTION sin

END SUBROUTINE sub

任何在子程序sub中对sin的引用都会调用其中定义的内部函数,而不是原来的内在函数。

类似地,任何与同名内在过程的标准类型不同的类型声明都会产生一个局部名称。下面的例子声明了一个名为sin的变量:

CHARACTER(LEN=5) :: sin

任何使用这个字符变量的程序或内部过程都不能再使用原来的内在函数。如果该变量在模块中以PRIVATE属性声明,模块外的程序单元仍可以使用内在函数sin。

*          语句名称

语句名称的作用域是一条语句。语句名称可以出现在语句函数的语句中、一个DATA语句的隐DO循环中、一个数组构造器中。在语句函数语句中作为哑元出现的变量名,在其出现之处的范围就是语句域。DO变量的域(必须是整数)是隐DO列表。例如:

DIMENSION x(10)

Add(a,b)=a+b

DO n=1,10

x(n)=add(y,z)

END DO

在此例中,a和b的域被限制在语句函数内部。n的域是整个DO循环。

*          公共块名称

公共块名称是全局名称。因为局部名称可以和全局名称重名,在局部实体中对这个名称的引用指的是局部名称。当公共块在SAVE语句中命名时,它应该用斜杠围起来以和其它同名的局部变量区分。例如:

COMMON/happy/cat,dog,mouse

CHARACTER(20) happy

SAVE /happy/  !SAVE的是公共块而不是变量

*          函数结果名

函数结果是另一个允许出现重名的例子。对每一个在函数过程中的FUNCTION语句或ENTRY语句都可有一个结果变量。如果没有用RESULT指定另外的变量名,结果变量和函数定义时同名。

*          派生类型成员名称

如果一个变量是其它名称的成员,它的域和包含该变量的名称的域一样。例如,一个模块中定义的派生类型和模块的域一致,也和任何使用该模块的域一致;承认派生类型的的程序同样承认派生类型的成员;对于数组的情况也是类似的,在数组适用的域内,数组片段也是有效的。

*          其它情形

其它实体,例如语句标号、I/O单元、操作符和赋值号等都有域的概念。对于这些实体应遵守下面的规则:1、语句标号始终被认为是局部的,两个域相同的单元不能使用相同的标号;2、内在操作符(例如+,-,*,**,或/)是全局的,但自定义的操作符是局部实体,特殊操作符的域由定义该操作符的过程的范围决定。可以通过使用过程接口块使自定义的操作符成为全局的;3、赋值符号(=)是全局实体,可以在一个接口块中确定附加的一般赋值操作。