【Fortran】过程设计之四 (其它高级特性)

目录

    • 前言
      • (1) SAVE的使用
      • (2) 纯过程、逐元过程和不纯逐元过程
        • 1) 纯过程
        • 2) 逐元过程
        • 3) 不纯逐元过程
      • (3) 内部过程
      • (4) 子模块


前言

过程设计的基础内容,可参考:

过程设计之一(子例程SUBROUTINE)

过程设计之二(模块MODULE)

过程设计之三(函数FUNCTION)

(1) SAVE的使用

1、 定义说明
当过程执行完毕后,其中的所有局部变量(或数组)的值都会清除,成为未定义的值,使得再次调用时与上次调用时的值可能一样也可能不一样。

Fortran中,SAVE能够保证过程在调用前后保存其中的局部变量(或数组)。

SAVE属性:类型 , SAVE::变量1,变量2,...类型是指各种数据类型;
SAVE语句:SAVE [变量1,变量2,...] ,其中[]表示可选。

2、注意事项:

  • 程序只要不结束,局部变量在连续的调用期间中不会改变(如果不修改数值);
  • 如果SAVE指定具体变量,则该变量连续调用时不会变,如果没有指定(仅一条SAVE语句),则所有局部变量都会被保存起来;
  • 在声明部分中经过初始化的局部变量相当于是隐式地使用了SAVE属性,如REAL::a=1等同于REAL,SAVE::a=1,其中的SAVE可加也可不加;
  • SAVE不能用于过程关联的形参或PARAMETER中;
  • 任何共享数据的模块都应该使用SAVE语句,确保模块中的数值在调用过程中保持完整。

3、例子(取自《Fortran for Scientists and Engineers(4th) by Stephen J. Chapman》中的9-3例题,有少量修改,复制可运行)

程序的功能是:通过读取文件中的数据,返回不同情况下的平均值、标准差及当前计算所采用的数据个数。不同情况是指,当文件中有3个数时,逐一返回第1个、第1-2个,第1-3个的平均值、标准差和数据个数;当文件中有100个数时,逐一返回第1个、第1-2个,…,第1-100个的平均值、标准差和数据个数。因此反复调用计算平均值和标准差的子程序时,需要保存某些累计值。

代码中有解释,不过多解释。READIOSTAT子句参考具体使用方式。

主程序:

PROGRAM test_running_average
IMPLICIT NONE    
INTEGER :: istat1   ! 文件打开状态    
INTEGER :: istat2
REAL :: ave         ! 平均值
REAL :: std_dev   ! 标准差
CHARACTER(len=80) :: msg ! 文件打开错误返回信息
INTEGER :: nvals ! 数值的个数
REAL :: x ! 输入值
CHARACTER(len=20) :: file_name ! 文件名

CALL running_average ( 1., ave, std_dev, nvals, .TRUE. )   ! 清除累加值,这时候第一个参数可填入任意值,不影响结果(可对照子程序定义来看)

WRITE (*,*) ' 输入文件名: '
READ (*, 100 ) file_name
100 FORMAT(A20)

OPEN ( UNIT=11, FILE=file_name, STATUS='OLD', ACTION='READ', IOSTAT=istat1, IOMSG=msg ) ! 只读方式打开

openok: IF ( istat1 == 0 ) THEN   ! 打开成功,则

    calc: DO
        READ (11,*,IOSTAT=istat2) x ! 获取数据
        IF ( istat2 /= 0 ) EXIT ! 读取无效,则退出数据读取

        CALL running_average ( x, ave, std_dev, nvals, .FALSE. )  ! 调用子程序计算

        WRITE (*,200) 'Value = ', x, ' Ave = ', ave,  ' Std_dev = ', std_dev,  ' Nvals = ', nvals
        200 FORMAT (3(A,F10.4),A,I6)   ! 这里多了A

    END DO calc
ELSE openok                 
    WRITE (*,300) msg    ! 文件打开失败,则
    300 FORMAT (' 文件打开失败: ', A)
END IF openok
END PROGRAM test_running_average    

子程序(子例程):

SUBROUTINE running_average ( x, ave, std_dev, nvals, reset )
IMPLICIT NONE    
             ! 形参的定义
REAL, INTENT(IN) :: x ! 输入数据值
REAL, INTENT(OUT) :: ave ! 计算平均值
REAL, INTENT(OUT) :: std_dev ! 计算标准方差
INTEGER, INTENT(OUT) :: nvals ! 当前数值个数
LOGICAL, INTENT(IN) :: reset !  复位标志,如果为真,清楚求出的和
              ! 局部变量的定义
INTEGER, SAVE :: n ! 输入值的个数
REAL, SAVE :: sum_x ! 输入值的和
REAL, SAVE :: sum_x2 ! 输入值的平方和

! REAL :: sum_x ! 输入值的和
! REAL :: sum_x2 ! 输入值的平方和   ! 如果是这样,由于没有保存,结果有问题

calc_sums: IF ( reset ) THEN    ! 如果需要重置,则所有变量的值均为0,记住,只能将内部变量和输出值设为0
    n = 0
    sum_x = 0.
    sum_x2 = 0.
    ave = 0.
    std_dev = 0.
    nvals = 0
ELSE
                        ! 不用重置,则计算
    n = n + 1
    sum_x = sum_x + x
    sum_x2 = sum_x2 + x**2

    ave = sum_x / REAL(n)    ! 计算平均值

    IF ( n >= 2 ) THEN
    std_dev = SQRT( (REAL(n) * sum_x2 - sum_x**2)   / (REAL(n) * REAL(n-1)) )  ! 计算标准差,标准公式拆成这样,注意是样本(分母为n-1),而不是总体
    ELSE
    std_dev = 0.
    END IF

    nvals = n       ! 数据值的个数
END IF calc_sums
END SUBROUTINE running_average

需要打开的文件:

! read.txt
3.0
2.0
3.0
4.0
2.8

相应的运行结果为:

输入文件名:
read.txt
Value =     3.0000 Ave =     3.0000 Std_dev =     0.0000 Nvals =      1
Value =     2.0000 Ave =     2.5000 Std_dev =     0.7071 Nvals =      2
Value =     3.0000 Ave =     2.6667 Std_dev =     0.5774 Nvals =      3
Value =     4.0000 Ave =     3.0000 Std_dev =     0.8165 Nvals =      4
Value =     2.8000 Ave =     2.9600 Std_dev =     0.7127 Nvals =      5

(2) 纯过程、逐元过程和不纯逐元过程

1) 纯过程

是指没有任何负面影响的过程。换言之,调用纯过程不用担心输入参数会被修改,也不会修改在过程外部可见的其它数据(如模块中的数据)。

① 定义: 在过程定义语句前加上PURE

② 注意事项:

  • 由于不会影响外部参数,可以安全的使用FORALL结构,可以实现并行处理;
  • 在纯过程调用的其它过程必须属于纯过程;
  • 过程中局部变量不能使用SAVE属性,也不能在类型声明中初始化局部变量(隐式SAVE);
  • 不能有外部文件的I/O操作;
  • 不能包含STOP语句;
  • 对于纯函数,每个参数都必须定义为INTENT(IN);对于纯子例程,可以有INTENT(IN)INTENT(OUT)INTENT(INOUT)。除此之外,函数和子例程所受的限制是相同的。

③ 例子: 较简单,不过多解释。

PROGRAM func_test   ! 主程序

IMPLICIT NONE
REAL :: rec_area_p   ! 记得要声明函数名的类型,因为相当于是变量
REAL ::x1,y1

WRITE(*,*)'分别输入矩形两条边长:'
READ(*,*) x1,y1

WRITE(*,*) '矩形面积为:',rec_area_p(x1, y1)  ! 调用函数

END PROGRAM func_test
PURE FUNCTION rec_area_p(x, y)  ! 纯函数
IMPLICIT NONE
REAL, INTENT(IN) :: x, y
REAL :: rec_area_p
rec_area_p = x*y
END FUNCTION rec_area_p

相应的结果为:

分别输入矩形两条边长:
20,3
矩形面积为:   60.00000

2) 逐元过程

逐元过程是为标量参数指定的过程,也适用于数组参数。参数与返回值具有相同的类型(详见例子)。

① 定义: 在过程定义语句前加上ELEMENTAL

② 注意事项:

  • 前提必须是纯过程;
  • 所有形参和返回值必须是标量,不能带有指针POINTER属性;
  • 形参不能用在类型声明语句中(限制了自动数组的使用)。

③ 例子: 较简单,不过多解释。

PROGRAM func_test2   ! 主程序

IMPLICIT NONE
REAL :: rec_area_e   ! 记得要声明函数名的类型,因为相当于是变量

REAL::x1,y1
REAL,DIMENSION(3) ::x2,y2

x1 = 2.
y1 = 4.

x2 = [1.,5.,3.]
y2 = [2.,2.,2.]

WRITE(*,100) rec_area_e(x1, y1)  ! 调用函数
100 FORMAT('单个标量型:矩形面积为:',F5.1)

WRITE(*,200) rec_area_e(x2, y2)  ! 调用函数
200 FORMAT('数组型:矩形面积分别为:',3F5.1)
END PROGRAM func_test2
ELEMENTAL FUNCTION rec_area_e(x, y)   ! 逐元函数
IMPLICIT NONE
REAL, INTENT(IN) :: x, y
REAL :: rec_area_e
rec_area_e = x*y
END FUNCTION rec_area_e

相应的结果为:

单个标量型:矩形面积为:  8.0
数组型:矩形面积分别为:  2.0 10.0  6.0

3) 不纯逐元过程

在逐元过程基础上,新增条件使得可以修改形参(这样操作会改变外部的数据,详见例子),这类过程必须使用IMPURE关键词声明,并且被修改的参数类型必须使用INTENT(INOUT)来进行声明。

③ 例子: 较简单,不过多解释。

PROGRAM func_test3   ! 主程序

IMPLICIT NONE
REAL :: rec_area_ie   ! 记得要声明函数名的类型,因为相当于是变量

REAL::x1,y1
REAL,DIMENSION(3) ::x2,y2

x1 = 2.
y1 = 4.

x2 = [1.,5.,3.]
y2 = [2.,2.,2.]

WRITE(*,100) rec_area_ie(x1, y1)  ! 调用函数
100 FORMAT('单个标量型:矩形面积为:',F5.1)

WRITE(*,200) rec_area_ie(x2, y2)  ! 调用函数
200 FORMAT('数组型:矩形面积分别为:',3F5.1) 
      
END PROGRAM func_test3
IMPURE ELEMENTAL FUNCTION rec_area_ie(x, y)   ! 不纯逐元函数
IMPLICIT NONE
REAL, INTENT(INOUT) :: x, y
REAL :: rec_area_ie

x = x + 1     ! 对形参进行修改,实参也会变化
y = y + 1
rec_area_ie = x*y
END FUNCTION rec_area_ie

相应的结果为:

单个标量型:矩形面积为: 15.0
数组型:矩形面积分别为:  6.0 18.0 12.0

(3) 内部过程

除了外部过程(过程作为参数传递,如函数和子例程)和模块过程,在Fortran中还存在第三种过程——内部过程

简单来说,内部过程是指程序单元A(称为host过程)中含有一个过程B(可多个),但只能从程序单元A中调用过程B,外部过程不能直接调用过程B过程BCONTAINS引入。(熟悉python的同学可知,内部过程与if __name__ =='__main__'具有相似效用)

一般来说,使用内部过程完成一些只需要在一个程序单元中执行,且必须重复执行的低级操作。

注意事项:

  • 内部过程只能被host过程调用,其它过程不能直接访问该内部过程;
  • 内部过程的名字不能作为命令行参数传递给其他的过程;
  • 内部过程可以使用host过程中定义的变量和参数;
  • 内部过程中参数名和host过程中参数名相同,如在内部过程中是a=100,在host过程中是a=200时,调用内部过程时,a的数据为100。

例子: 可直接复制运行。

PROGRAM test   ! 主程序

IMPLICIT NONE
REAL :: rec_area_test   ! 记得要声明函数名的类型,因为相当于是变量

REAL::x1,y1

x1 = 2.
y1 = 4.

WRITE(*,100) rec_area_test(x1, y1)  ! 调用函数
100 FORMAT('矩形面积为:',F5.1)  

END PROGRAM test
FUNCTION rec_area_test(x, y)   ! 调用的函数
IMPLICIT NONE
REAL, INTENT(IN) :: x, y
REAL :: rec_area_test
REAL,PARAMETER::a = 100.

rec_area_test = rec_area(x,y)

CONTAINS    
    REAL FUNCTION rec_area(x0, y0)    ! 函数中的内部过程(函数)

    REAL, INTENT(IN) :: x0 , y0
    REAL,PARAMETER::a = 200.   ! 如果将这句删去,则结果变成108.0
    rec_area = x0*y0 + a 
    END FUNCTION rec_area        

END FUNCTION rec_area_test

相应的结果为:

矩形面积为:208.0

如果在主程序PROGRAM直接调用内部函数rec_area,则会报错:

error LNK2019: 无法解析的外部符号 _REC_AREA,该符号在函数 _MAIN__ 中被引用		

(4) 子模块

对于模块过程,如果有多个过程,每个过程执行部分都很长,当对某一过程进行修改时,会不断地对模块进行修改,有时会不经意的将其它部分进行重写而不自知。

因此,Fortran提供了子模块功能,将模块中的过程拆分出来,形成新的模块(称为子模块)。

示例

MODULE name1     ! 模块
IMPLICIT NONE
INTERFACE             ! 接口模块
	MODULE SUBROUTINE name11(a, b, c)   ! 子程序子模块
	IMPLICIT NONE
		REAL,INTENT(IN) :: a    ! 形参定义
		REAL,INTENT(IN) :: b
		REAL,INTENT(OUT) :: c
	END SUBROUTINE procedure1
	
	MODULE REAL FUNCTION name12(a, b)    ! 函数子模块
	IMPLICIT NONE
		REAL,INTENT(IN) :: a    ! 形参定义
		REAL,INTENT(IN) :: b
	END FUNCTION func2
END INTERFACE
END MODULE name1
SUBMODULE (name1) name2   ! 子模块
IMPLICIT NONE
CONTAINS
MODULE PROCEDURE name11
...                       ! 局部变量定义、执行部分
END PROCEDURE name11
MODULE PROCEDURE name12
...                      ! 局部变量定义、执行部分
END PROCEDURE name12
END SUBMODULE name2

调用:

PROGRAM
USE name1     ! 调用模块
...
CALL name11   ! 调用子程序模块
...
END PROGRAM

注意事项:

  • 子模块的功能相当于是把模块拆成两部分:

    • 第一部分是模块自身,其中包含了每个模块过程的接口(形参);
    • 第二部分是包含过程的实际可执行代码(局部变量和执行部分)。
  • 如果是接口需要更改,即更改调用参数,则模块及子模块都需要修改;如果是某个过程中执行部分需要修改,则仅子模块需要修改;

  • 模块中包含了INTERFACE块,每个过程的接口用MODULE来引入;

  • 模块中没有CONTAINS语句,CONTAINS语句在子模块中。

你可能感兴趣的:(fortran)