程序单元包括主程序、子例程、模块、函数子程序。
在Fortran中,大型程序可以拆分成多个独立运行和调试的子任务,即程序单元(亦称为外部过程)。
Fortran中有两种外部过程:子例程和函数子程序。这种机制的优点是:
子例程(亦可称为子程序)是一个Fortran过程,通过CALL
语句进行调用,并通过参数表获取输入数值和返回结果。
定义语句格式:
SUBROUTINE subroutine_name( argument_list_dum ) ! 定义子例程名和相关参数表
(声明部分)
...
(执行部分)
...
RETURN
END SUBROUTINE [ subroutine_name ] ! []表示可选
注意事项:
subroutine_name
由字母、数字和下划线组成 ,最大长度可达63个字符,第一个字符为字母;argument_list_dum
,形参,一系列变量和/或数组,从调用程序传递给子例程;SUBROUTINE
,结束于END SUBROUTINE
,其中的局部变量名和语句标号(“行号”)可以在其它地方复用(不用担心重名);调用语句格式:
CALL subroutine_name( argument_list_act )
注意事项:
argument_list_act
,实参。实参的个数、顺序与类型必须和形参的个数、顺序与类型相匹配。已知三角形的两条直边,计算斜边。要求计算过程用子例程,主程序直接输入相应数据后直接调用。
PROGRAM calc_hypotenuse_test ! 主程序
IMPLICIT NONE
REAL :: s1
REAL :: s2
REAL :: hypot
WRITE(*,*) '测试计算斜边的子例程'
WRITE(*,*)'输入第一条直边的长度:'
READ(*,*) s1
WRITE(*,*)'输入第二条直边的长度:'
READ(*,*) s2
CALL calc_hypotenuse( s1, s2 , hypot ) ! 调用子例程
WRITE( *, 100 ) hypot
100 FORMAT('斜边长度为:' , F10.4)
STOP
END PROGRAM calc_hypotenuse_test
SUBROUTINE calc_hypotenuse(side_1 , side_2 , hypotenuse) ! 子例程
IMPLICIT NONE
REAL , INTENT(IN)::side_1 ! 第一条直边长度,输入,INTENT用法见下述
REAL , INTENT(IN)::side_2 ! 第二条直边长度,输入
REAL , INTENT(OUT)::hypotenuse ! 斜边长度,输出
REAL::temp ! 声明局部变量
temp = side_1**2 + side_2**2
hypotenuse = SQRT( temp )
END SUBROUTINE calc_hypotenuse
INTENT
属性在子例程的形参声明时使用。
INTENT
属性的格式如下:
INTENT(IN)
,形参仅用于向子程序传递输入数据;INTENT(OUT)
,形参仅用于将结果返回给调用程序;INTENT(INOUT)
或 INTENT(IN OUT)
,形参既用来向子程序输入数据,也用来向调用程序返回结果。属性特点:
INTENT
属性;INTENT
属性仅对过程的形参有效,如果用来声明子例程的局部变量或主程序的变量则会出错;INTENT
属性。形参的INTENT
属性也可以用独立的语句来声明,如:INTENT(IN) :: arg1 , arg2,...
。如前所述,调用参数实际上是通过传递指向该实参的内存位置指针来传递给子例程。对于实参是一个数组,其指针是指向数组中的第一个值。然而,子例程需要同时知道数组的地址和大小,保证不会发生越界,才能进行数组操作。
在子例程中有三种方式来指明形参数组的大小:
显式结构形参数组
数组的维度大小需要作为参数进行传递,一维数组为例:
SUBROUTINE process(data1 , data2 , n , nvals)
INTEGER , INTENT(IN) :: n , nvals ! n为数组的大小, nvals是数组操作的个数
REAL , INTENT(IN) , DIMENSION(n)::data1
REAL , INTENT(OUT) , DIMENSION(n)::data2
INTEGER::i
DO i = 1 , nvals
data2( i ) = 3.*data1( i ) ! 将data1数组的数值乘以3,赋值为data2数组
END DO
END SUBROUTINE process
二维数组为例:
SUBROUTINE process1(data1 , data2 , m , n)
INTEGER , INTENT(IN) :: m ,n ! m×n为数组的大小
REAL , INTENT(IN) , DIMENSION(m,n)::data1
REAL , INTENT(OUT) , DIMENSION(m,n)::data2
data2 = 3.*data1 ! 将data1数组的数值乘以3,赋值为data2数组,直接对数组进行操作
END SUBROUTINE process1
由于形参数组的大小和结构都已经清晰,可以对形参数组进行数组操作,以及切片操作。
不定结构形参数组
把子例程中的所有形参数组声明为不定结构(数组的每个下标用:
来代替)的形参数组,只有当子例程具有显式接口时,才能使用这种数组。因此,显示接口能够给编译器提供每个数组的大小、结构等详细信息,在调用的时候不会出错。
需注意的是,定义形参数组时只有它的结构(但用:
替代),没有具体下标范围。因此,在把实参数组传递至形参数组时,只传递了结构,并没有传递实参数组每个维度的下标取值范围。此时,可以用查询函数获取不定结构数组的结构。
如果不需要将数组的每个维度下标边界从调用程序传递给子例程,则不定结构形参数组比显式结构形参数组更方便使用。 二维数组的例子:
MODULE module_process
CONTAINS
SUBROUTINE process2(data1 , data2 )
REAL , INTENT(IN) , DIMENSION(:,:)::data1
REAL , INTENT(OUT) , DIMENSION(:,:)::data2
data2 = 3.*data1 ! 将data1数组的数值乘以3,赋值为data2数组,直接对数组进行操作
END SUBROUTINE process2
END MODULE module_process
不定大小形参数组
古老且过时的方法,用星号*
来声明形参数组的长度,表示大小不确定。因此不清楚数组的实际大小和结构,容易运行错误,且很难调试,建议不要使用。例子如下:
SUBROUTINE process(data1 , data2 , nvals)
INTEGER , INTENT(IN) :: nvals ! nvals是数组操作的个数,因此数组的数据个数至少要为nvals
REAL , INTENT(IN) , DIMENSION(*)::data1 ! 不定大小
REAL , INTENT(OUT) , DIMENSION(*)::data2 ! 不定大小
INTEGER::i
DO i = 1 , nvals
data2( i ) = 3.*data1( i ) ! 将data1数组的元素乘以3,赋值为data2数组
END DO
END SUBROUTINE process
可分配数组作为参数传递给子例程时,必须要结合显式接口。
注意事项:
当传递的参数是可分配数组时,子例程中形参声明 和 调用子程序的实参声明必须都是可分配的;
可分配形参可以使用INTENT
属性,但INTENT
属性的具体参数可能会影响子例程中的操作:
INTENT(IN)
,在子例程中不允许对输入可分配数组进行重分配或者释放内存空间;INTENT(INOUT)
,调用子例程时,如果参数只有数组这一个,那么实际上会将数组的状态(是否可分配)和相应数据传递至子例程中(实参——>形参),在子例程中可以对形参修改、释放内存、重分配等操作。形参的最终状态和数据返回至调用程序中(相应实参结果被修改了)。具体见下例;INTENT(OUT)
,调用子例程时,实参在入口处被自动释放掉,相应的数据清除掉,可以在子例程中对其进行操作,然后将形参的最终状态和数据返回至调用程序中。例子:
取自《Fortran for Scientists and Engineers(4th) by Stephen J. Chapman》中的9-5例题,有少量修改,复制可运行。
PROGRAM test_allocatable_arguments ! 主程序
USE test_module ! 先调用模块
IMPLICIT NONE
REAL,ALLOCATABLE,DIMENSION(:) :: a_main ! 定义旧版的可分配数组.
INTEGER :: istat ! 分配的状态
! 指定大小,分配
ALLOCATE( a_main(6), STAT=istat )
! 初始化数组
a_main = [ 1., 2., 3., 4., 5., 6. ]
! 输出调用前主程序中可分配数组
WRITE (*,'(A,6F4.1)') ' 调用子程序前主程序中的数组:', a_main
! 再调用子例程
CALL test_allocate(a_main)
! 输出调用后主程序中可分配数组
WRITE (*,'(A,6F4.1)') '调用子程序后主程序中的数组: ', a_main
END PROGRAM test_allocatable_arguments
MODULE test_module ! 显式接口
CONTAINS
SUBROUTINE test_allocate(array) ! 子例程
IMPLICIT NONE
! 过程中形参变量
REAL,DIMENSION(:),ALLOCATABLE,INTENT(INOUT) :: array ! 作为形参的可分配一维数组,这是属于自动分配内存的定义
! 过程中局部变量
INTEGER :: i ! 循环下标
INTEGER :: istat ! 分配的状态
! 判断数组的状态
IF ( ALLOCATED(array) ) THEN
WRITE (*,'(A)') '子程序分配成功!'
WRITE (*,'(A,6F4.1)') '子程序输入为: ', array
ELSE
WRITE (*,*) '子程序没有被分配'
END IF
! 释放可分配数组的内存(因为在声明部分已经分配好了)
IF ( ALLOCATED(array) ) THEN
DEALLOCATE( array, STAT=istat )
END IF
! 重新分配,按照5个元素的一维数组
ALLOCATE(array(5), STAT=istat )
! 往重分配的一维数组填入数据
DO i = 1, 5
array(i) = 6 - i
END DO
! 展示重分配后的数组结果
WRITE (*,'(A,6F4.1)') '子程序中输出的数组 ', array
END SUBROUTINE test_allocate
END MODULE test_module
相应的结果为:
调用子程序前主程序中的数组: 1.0 2.0 3.0 4.0 5.0 6.0
子程序分配成功!
子程序输入为: 1.0 2.0 3.0 4.0 5.0 6.0
子程序中输出的数组 5.0 4.0 3.0 2.0 1.0
调用子程序后主程序中的数组: 5.0 4.0 3.0 2.0 1.0
当一个字符变量被作为子例程的形参时,用*
号来声明字符变量的长度。当调用子例程时,形参的长度将是实参的长度。如:
SUBROUTINE example( string )
CHARACTER( len = * ) , INTENT(IN) :: string ! *号表示完全复制实参的长度
WRITE(*,*) 'The lengrh of string : ',LEN(string) ! 可以在子例程内部实时返回实参的长度
END SUBROUTINE example
如自动数组一般,创建自动字符变量:
SUBROUTINE sample ( string )
CHARACTER(len=*) :: string
CHARACTER(len=len(string)) :: temp ! 一个与形参相同大小的临时变量,子例程调用结束时会被销毁
当子例程a
作为实参时,传递给过程A
(如另一子例程)一个指向该子例程a
的指针。执行该过程A
时,参数表中的子例程a
将作为形参进入到过程A
的编译当中。
要想实现子例程传递功能,必须要使用EXTERNAL
属性,将子例程声明为外部,此时编译器才会知道参数表中传递的是独立的已编译子例程,而不是常规变量。
EXTERNAL
属性需要在声明部分中使用,格式如下:
TYPE, EXTERNAL ::sub_1 , sub_2 ! TYPE是指具体数据类型
或者
EXTERNAL ::sub_1 , sub_2
同时,在过程中需要结合CALL
语句,以调用过程中子例程形参。
STOP
语句,因为一旦调用子例程时,会停止程序。如果调用多个子例程(每个子例程都有STOP
语句),则程序永远不会执行成功;