与 c 语言只有函数不同,Fortran 提供了子程序 subroutine,函数 function,模块 module 等多种结构。

子程序 subroutine

首先,回顾之前的 HelloWorld

1
2
3
4
program main
implicit none
write(*,*) "hello,world!"
end program main

其中使用了名称为 main 的主程序 program,一个可执行程序只允许有一个 program,为了分离和复用某个功能,可以使用子程序 subroutine。

1
2
3
4
5
6
7
8
9
10
11
12
program main
implicit none
write(*,*) "hello,world!"

call hello() ! call 调用子程序
end program main

subroutine hello()
implicit none
write(*,*) "hello,subroutine!"

end subroutine hello

运行结果

1
2
hello,world!
hello,subroutine!

主程序和子程序的相对位置无关,子程序不需要写在 call 调用它之前,主程序也不需要写在开头。子程序可以 call 调用其他子程序,也可以递归调用自身。子程序可以用 return 语句结束,或者遇到 end subroutine 自动结束。

子程序拥有绝对独立的变量和运行环境等,与主程序完全隔离,不存在变量覆盖或者重名等问题。因此在子程序中也需要 implicit none 语句。当然子程序需要和主程序产生交互,主程序可以传递指定的变量给子程序,形参和实参可以名称各异,注意形参同样需要声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
program main
implicit none
real :: a=1.0
write(*,*) "hello,world!"

write(*,*) "a=",a
call hello(a) ! call 调用子程序
write(*,*) "a=",a
end program main

subroutine hello(b)
implicit none
real :: b ! 形参也需要子程序进行声明
write(*,*) "hello,subroutine!"
b = 2.0
end subroutine hello

注意 Fortran 传递参数默认使用传址调用,因此对于参数变量,主程序和子程序共享一个内存,修改可以反馈给主程序,这也是子程序存在的意义。运行结果:

1
2
3
4
hello,world!
a= 1.00000000
hello,subroutine!
a= 2.00000000

因为默认使用了传址调用,但子程序的实参允许使用字面值常量,如果在子程序中尝试进行了修改,程序会直接崩溃。

形参属性

为了明确形参的使用意图,可以在形参声明时指定参数属性,与属性冲突时编译会警告或报错。

1
2
3
real,intent(in) :: a ! 只读形参,修改会编译报错
real,intent(out) :: b ! 只写形参,如果没有语句对变量进行修改,则会编译警告
real,intent(inout) :: c ! 读写形参,默认

形参缺省

1
2
3
4
5
6
7
8
9
10
subroutine test(a,b)
implicit none
integer :: a
integer, optional :: b ! 可选参数

if(present(b)) then
! 判断是否给了形参b
end if

end subroutine test

对于这种特殊情形,在使用前必须要使用 interface 更具体地描述接口,见下文。

变量生存周期

通常情况下,子程序结束时变量全部清除,但是在变量声明时使用 save 可以设置局部静态变量,变量会保留到下一次调用。

1
integer,save :: count = 1

注意:有的编译器可能默认对所有的局部变量都视作局部静态的。

函数 function

与子程序主要有两点不同:函数在主程序内进行使用之前需要声明,函数有一个返回值。似乎 subroutine 的应用比 function 更广泛。作为子程序 subroutine 和函数 function 的对比,建议在 function 中不对形参做任何处理。在 function 中也需要使用 implicit none。

第一种,标准形式,返回值和函数同名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
program main
implicit none
real(kind=4) :: a=1.0,b=2.0
real,external :: add
! 声明add是一个返回值为real的自定义函数
! external 可以缺省, 但是建议保留

write(*,*) "hello,world!",a,b
write(*,*) "a+b=", add(a,b)
end program main

function add(a,b)
implicit none
real :: add ! 声明本函数的返回值为real, 返回值与函数同名
real :: a,b
add = a+b
return ! return返回可缺省
end

这里 external 表明是一个自定义函数,类似地可以用关键词 intrinsic 表明是一个库函数。

1
2
real, intrinsic :: sin ! 声明sin是一个库函数
real, external :: mysin ! 声明mysin是一个自定义函数

第二种,函数在定义时直接指出返回值类型。

1
2
3
4
5
6
real function add(a,b) ! 自动声明本函数的返回值为real
implicit none
real :: a,b
add = a+b
return
end

第三种,简短的函数定义,只能在当前区域被使用。

1
2
3
4
5
6
7
8
9
10
11
12
program main
implicit none
real(kind=4) :: a=1.0,b=2.0
real :: add
! 声明add是一个返回值为real的自定义函数

add(a,b) = a+b
! 直接在此处定义add函数

write(*,*) "hello,world!",a,b
write(*,*) "a+b=", add(a,b)
end program main

第四种,在函数内部指明返回值的名称,对于调用方则没有任何影响。

1
2
3
4
5
6
7
real function add(a,b) result(ans)
implicit none
real :: a,b

ans = a+b
return
end

在递归函数中,必须要把返回值和函数名分开,同时还需要 recursive 关键词。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
program main
implicit none
integer :: n=5, result
integer, external :: fact

result = fact(n)
write(*,*) "10!=",result

end program main

recursive integer function fact(n) result(ans)
implicit none
integer,intent(in) :: n

if(n < 0) then
ans = -1
else if(n <= 1) then
ans = 1
else
ans = n * fact(n-1)
end if

return
end

第五种,内部函数:为了限制子程序/自定义函数的应用范围,可以使用 contains 命令把定义直接附在主程序内的最后部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
program main
implicit none
integer :: m=5, result
!integer, external :: fact 不再需要前置的函数声明

result = fact(m)
write(*,*) "10!=",result

! 直接包含
contains
recursive integer function fact(n) result(ans)
implicit none
integer,intent(in) :: n

if(n < 0) then
ans = -1
else if(n <= 1) then
ans = 1
else
ans = n * fact(n-1)
end if

return
end
end program main

模块 module

模块 module 最基本的用法是封装与共享全局静态变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module global
implicit none
integer, save :: a,b
end module global

program main
use global ! use语句还需要在implicit none之前
implicit none
a = 1
b = 2
call sub()
end program main

subroutine sub()
use global
implicit none
write(*,*) a,b
return
end subroutine sub

这里名为 global 的模块 module 需要在源文件的开头部分,在 use global 之前。主程序和子程序无须声明变量即可使用,基于 save 属性(或者 common)可以实现更好的全局静态变量的效果。如果不使用 save 则不能共享变量。

为了更全面地了解 module,必须结合 Fortran 的面向对象进行,module 内部的变量和函数/子程序表现的非常像类的结构。(Fortran 甚至还可以进行简单的运算符重载,函数重载和 module 继承,是为了得到面向对象风格进行的尝试,但可能存在各种麻烦,不去管它们)

module 实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
module bank
implicit none

private :: money ! 声明这是私有的
! 变量会被module的各个子程序/方法共享
public :: LoadMoney, SaveMoney, Report ! 声明这是公开的, 缺省时默认公开

integer :: money = 100

contains
subroutine LoadMoney(num)
implicit none
integer :: num
money = money - num
return
end subroutine

subroutine SaveMoney(num)
implicit none
integer :: num
money = money + num
return
end subroutine

subroutine Report()
implicit none
write(*,*) "now money=", money
return
end subroutine

end module bank

program main
use bank ! 使用bank这个module
implicit none

call LoadMoney(10) ! 传入常量是有风险的, 如果子程序修改了形参会崩溃,除非确定形参不会被修改
call Report()
call SaveMoney(100)
call Report()

stop ! 强制结束, 可以不写
end program main

运行结果为

1
2
now money=          90
now money= 190

private 和 public:需要使用单独的语句强调变量或者子程序/函数是私有还是公开的,默认是 public 的。

单行的 public/private 描述符是针对全局的,除非显式声明为另一种,下文只有 c 是公开的,对于函数同样适用。

1
2
3
4
5
6
7
8
module hello
implicit none
private

real :: a
integer :: b
public real :: c
end module hello

use 命令

上述的 module 还是非常鸡肋的面向对象效果,因为没有实现类和对象的分层,use bank 就会直接地使用它。module 内部 use 其他模块,但是不建议使用。

重名变量的改名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module A
implicit none
integer :: va
end module A

module B
implicit none
integer :: va
end module B

program main
use A
use B, bva=>va ! 导入模块B,对B内部的变量va改名为bva使用。
end program main

如上,模块 A 和 B 同时被使用,则产生了 va 重名的问题,可以改名加以区分。

变量的限制导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module A
implicit none
integer :: va,vb
end module A

module B
implicit none
integer :: va,vb,vc
end module B

program main
use A ! 导入了模块A的全部变量,va,vb
use B, only: vc ! 只导入B模块的vc变量,B模块的其他变量都不能在此处使用
! 也可以在限制导入的同时改名
! use B, only : bvc=>vc
end program main

建议使用下列语法进行安全的 use

1
2
3
use A, only Ax=>x
! 如果还需要别的
use A, only Ay=>y

补充

有几种过时的语法,提一句:

  1. common 声明变量是全局的,在多个主程序/子程序中都使用 common 对同一个变量进行声明,则可以共享同一个全局变量。
  2. data 和 block data 用来批量地赋初值。

多文件编程

现代 Fortran 建议以主程序 program 和多个 module 组成。所有的函数 function 和子程序 subroutine 应该封装在 module 中。这样既可以避免同名函数导致的链接错误,更能使编译器在链接时帮忙检查参数列表。

include 命令

相当于 c 语言的 include,是简单直接的代码注入,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
! main.f90
program main
implicit none
call sub()
stop
end program main

include 'sub.f90'

! sub.f90
subroutine sub()
write(*,*) "hello ,world"
return
end subroutine

此时不能把 main.f90 和 sub.f90 一起编译,否则会产生重复定义的问题,只能对 main.f90 进行编译,与 c 语言类似。这种 include 方式一般在 Fortran 77 中使用,在 Fortran 90 之后,建议基于 module 进行多文件编程。

module 多文件组织

建议除了主程序源文件包含一个 program 之外,其他单个源文件都由一个 module 组成,把子程序和函数都封装在 module 里,上例重写如下,此时两个源文件都需要参与编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
! main.f90
program main
use hello
implicit none
call sub()
stop
end program main

! sub.f90
module hello
implicit none
contains
subroutine sub()
write(*,*) "hello ,world"
return
end subroutine
end module hello

注意,编译会在目录中产生 hello.mod 文件,这是由 hello 这个模块产生的二进制文件。