前言
程序单元包括主程序、子例程、模块、函数子程序。
在Fortran 中,大型程序可以拆分成多个独立运行和调试的子任务,即程序单元 (亦称为外部过程 )。
Fortran 中有两种外部过程 :子例程和函数子程序。这种机制的优点是:
子任务单独测试,相互之间不影响;
避免重复造轮子,调用即可;
将实现某一功能的代码封装起来,避免不经意修改导致代码错误而不自知。
Fortran子例程(SUBROUTINE)
1) 使用方式
子例程 (亦可称为子程序 )是一个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,实参 。实参的个数、顺序与类型必须 和形参的个数、顺序与类型相匹配。
主程序和子例程之间采用地址传递 进行参数传递,具体过程是:由于主程序中的实参有具体的内存存储位置,当调用子例程时,主程序将生成多个指针来指向各个实参所对应的存储位置,并将指针传递给子例程,子例程调用的是参数的内存位置,而非实参数据本身。
2) 子例程示意
已知三角形的两条直边,计算斜边。要求计算过程用子例程,主程序直接输入相应数据后直接调用。
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
3) INTENT属性
INTENT属性在子例程的形参声明 时使用。
INTENT属性的格式 如下:
INTENT(IN),形参仅用于向子程序传递输入数据;
INTENT(OUT),形参仅用于将结果返回给调用程序;
INTENT(INOUT) 或 INTENT(IN OUT),形参既用来向子程序输入数据,也用来向调用程序返回结果。
属性特点 :
对于每一个形参 来说,都应该声明一个合适的INTENT属性;
INTENT属性仅 对过程的形参有效,如果用来声明子例程的局部变量或主程序的变量则会出错;
对于每一个过程,都应该声明每一个形参的INTENT属性。形参的INTENT属性也可以用独立的语句来声明,如:INTENT(IN) :: arg1 , arg2,...。
4) 传递数组给子例程
如前所述,调用参数实际上是通过传递指向该实参的内存位置指针来传递给子例程。对于实参是一个数组,其指针是指向数组中的第一个值。然而,子例程需要同时知道数组的地址和大小,保证不会发生越界,才能进行数组操作。
在子例程中有三种方式来指明形参数组的大小:
显式结构形参数组
数组的维度大小需要作为参数进行传递,一维数组为例:
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
4) 传递可分配数组给子例程
可分配数组 作为参数传递给子例程时,必须要结合显式接口 。
注意事项 :
例子 :
取自《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
5) 传递字符变量给子例程
当一个字符变量被作为子例程的形参时,用*号来声明字符变量的长度。当调用子例程时,形参的长度将是实参的长度。如:
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 ! 一个与形参相同大小的临时变量,子例程调用结束时会被销毁
6) 子例程作为参数传递
当子例程a作为实参时,传递给过程A(如另一子例程)一个指向该子例程a的指针。执行该过程A时,参数表中的子例程a将作为形参进入到过程A的编译当中。
要想实现子例程传递功能,必须要使用EXTERNAL属性,将子例程声明为外部,此时编译器才会知道参数表中传递的是独立的已编译子例程,而不是常规变量。
EXTERNAL属性需要在声明部分中使用,格式如下:
TYPE, EXTERNAL ::sub_1 , sub_2 ! TYPE是指具体数据类型
或者
EXTERNAL ::sub_1 , sub_2
同时,在过程中需要结合CALL语句,以调用过程中子例程形参 。
7) 其它注意事项
不要 在子例程中使用STOP语句,因为一旦调用子例程时,会停止程序。如果调用多个子例程(每个子例程都有STOP语句),则程序永远不会执行成功;
如果子例程中存在可能引发错误的条件,应该对错误进行检测,并设置正确/错误标志 ,返回给调用程序(即作为形参其中之一),用于实时判断预设条件的执行成功与否(即可定义为一种状态用于判断)。