在前三篇笔记,学习了 Fortran 作为一个编程语言,最基本的内容:变量,输入输出,流程控制和程序结构。接下来是 Fortran 的数组以及基于数组的数值计算,我认为这是 Fortran 语言最有价值的精华部分,因此特意放在了学习笔记靠后的部分,在学习了基本的语法和子程序等之后。注意,Fortran 的字符集不包括中括号[],因此与 c 语言的风格不同,Fortran 对数组分量的操作全都是使用小括号()的。

因为这部分内容比较重要,不像前几篇对 Fortran 77 的上古语法大部分进行了忽略,这一篇对于 Fortran 77 的语法也进行介绍。

一维数组

最基本的一维数组声明如下

1
2
3
integer :: nums(10)
integer, parameter :: len = 20
real :: datas(len)

一维数组的类型可以是 integer, real, complex, logical 四种基本类型,(也可以是字符或者自定义类型,暂时不管)一维数组的长度可以是字面值常量,也可以是声明为 parameter 的整数——和 c 语言一样,数组的长度需要在编译时确定。(与 c/c++语言不同,我们不需要纠结 Fortran 声明和定义的区别,全部称为声明)

1
2
3
nums(1) = 0
a = 2
nums(a) = nums(1) + 1

数组分量的用法如上,数组分量的索引可以是整数常量或者整数变量,编译器不会进行索引的越界检查,越界检查需要程序员自行负责。可以使用其他语法进行数组的声明,在 Fortran 77 中没有双冒号,而且需要两条命令分别确定数组元素的类型和数组的尺寸。

1
2
3
4
5
6
7
8
9
! 基本的用法
integer :: a(10)

! 这是另一种用法
integer, dimension(10) :: a

! 这是Fortran 77 的语法
integer a
dimension a(10)

二维数组与高维数组

与一维数组同理,二维数组的定义如下

1
2
3
4
5
6
7
8
9
! 基本的用法
real :: a(5,10)

! 这是另一种用法
real, dimension(5,10) :: a

! 这是Fortran 77的语法
read a
dimension a(10,10)

Fortran 原生支持最多 7 维的数组。

1
2
real :: a(2,2)
a(1,1) = 1

特别需要注意的是,Fortran 的下标从 1 开始!Fortran 对于高维数组在内存中的连续存储方式和 c 语言是相反的,分别为列优先和行优先。Matlab 对数组的处理继承了 Fortran 的风格,也是下标从 1 开始,列优先。 列优先:只有第一个分量变化的元素在内存中连续排列; 行优先:只有最后一个分量变化的元素在内存中连续排列。

1
2
3
integer :: a(3,2)
! 数据在内存中的连续分布
! a(1,1) => a(2,1) => a(3,1) => a(1,2) => a(2,2) => a(3,2)

自定义索引

索引默认从 1 开始,但是也支持显式指定数组的合法索引范围,范围的左右是闭区间。例如

1
2
3
4
5
6
7
8
9
10
11
integer a(0:5)
! 合法元素 a(0) a(1) a(2) a(3) a(4) a(5)

integer b(2:3,-1:1) ! 甚至允许负值作为索引
! 合法元素
! b(2,-1) b(2,0) b(2,1)
! b(3,-1) b(3,0) b(3,1)

integer c(1:5)
! 等效于基本的 integer c(5) 把从1开始省略
! 合法元素 c(1) c(2) c(3) c(4) c(5)

批量设置初值

Fortran 77 使用 data 命令赋初值,例如

1
2
3
4
5
6
7
8
9
10
integer a
dimension a(5)
data a /1,2,3,4,5/
! a(1)=1 a(2)=2 a(3)=3 a(4)=4 a(5)=5

integer i
integer b
dimension b(10)
data (b(i), i=2,4) /10,20,30/ ! 一种隐式循环语法
! b(2)=10 b(3)=20 b(4)=30

Fortran 90 可以抛弃 data 命令,对隐式循环语法也有更强的支持。

1
2
3
4
5
6
integer i
integer :: a(5) =(/ (i,i=1,5) /)
! a(1)=1 a(2)=2 a(3)=3 a(4)=4 a(5)=5

integer :: b(3) =(/ 10,20,30 /)
! b(1)=10 b(2)=20 b(3)=30

这里使用(/ /)结构省略了 data 命令,直接批量赋初值。

对数组的所有元素赋同一个初值,有如下的语法糖

1
2
integer :: a(3) = 5
! a(1)=5 a(2)=5 a(3)=5

数组整体运算

Fortran 90 提供了很多语法糖——原生支持对数组整体进行的运算,相比于 c 语言更加方便,不需要依赖循环语句实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
integer :: a(10)
integer :: b(10)
integer :: c(10)

! 对所有元素都赋值为5
a = 5 ! a(i)=5

! 逐个分量操作
! 要求a,b或者a,b,c为尺寸完全相同的数组,自动遍历所有元素
a = b ! a(i)=b(i)
a = b+c ! a(i)=b(i)+c(i)
a = b-c ! a(i)=b(i)-c(i)
a = b*c ! a(i)=b(i)*c(i)
a = b/c ! a(i)=b(i)/c(i)
a = sin(b) ! a(i) = sin(b(i)) 内置函数如sin等支持此类操作

以上对于高维数组也是一样的。

数组部分运算

这也是 Fortran 90 之后的语法,和 python 的 numpy 等的数组切片操作很类似,或者说 numpy 的切片继承了 Fortran 的语法风格。

1
2
3
4
5
6
7
8
9
10
11
12
integer :: a(10)
integer :: b(20)
integer :: c(10,10)

a(3:5)=5 ! a(3)=5 a(4)=5 a(5)=5
a(3: )=5 ! a(3)=...=a(10)=5 可以缺省一侧的下标范围
a(3:5)=(/ 3,4,5 /) ! a(3)=3 a(4)=4 a(5)=5 左右必须一样多

a(1:3)=b(4:6) ! a(1)=b(4) a(2)=b(5) a(3)=b(6) 左右必须一样多
a(:)=b(11:) ! a(1)=b(11) ... a(10)=b(20)

a(:)=c(:,1) ! a(1)=c(1,1) ... a(10)=c(10,1) 限制c的第二个分量为1对a进行赋值

还可以有更复杂的,使用步长,步长"a : b : c"相当于 c 语言风格的

1
2
3
4
5
// c>0
for(i=a;i<=b;i=i+c){ ... }

// c<0
for(i=a;i>=b;i=i+c){ ... }

示例如下

1
2
3
4
5
6
7
8
9
10
11
12
integer :: a(2,3)
integer :: b(2,3)

b = a(2:1:-1, 3:1:-1)
! 对于a的前面的维度视作内层循环,多层循环依次赋值
! 对b视作b(:,:)按照内存的列优先顺序依次被赋值
! b(1,1) = a(2,3)
! b(2,1) = a(1,3)
! b(1,2) = a(2,2)
! b(2,2) = a(1,2)
! b(1,3) = a(2,1)
! b(2,3) = a(1,1)

上述针对数组的整体运算和部分运算放在赋值的左侧和右侧均可,相当于隐含的循环展开。write(,)语句也支持。

1
2
3
4
5
6
7
8
9
10
11
integer :: a(2,3)
! 对a赋值,运算

write(*,*) a
! 输出a(1,1) a(2,1) a(1,2) a(2,2) a(1,3) a(2,3)与内存顺序一致

write(*,*) a(1,:)
! 输出a(1,1) a(1,2) a(1,3)

write(*,*) a(1,3:1:-1)
! 输出a(1,3) a(1,2) a(1,1)

动态数组

Fortran 77 不支持动态数组,数组尺寸必须在编译期间确定,只能在代码中使用足够大的 N 作为数组尺寸。Fortran 90 开始支持动态数组(可变长数组),数组尺寸可以在运行期间确定。

使用 allocatable 声明一个动态数组

1
2
integer, allocatable :: a(:) ! 声明一个一维数组a, 尺寸待定
integer, allocatable :: b(:,:) ! 声明一个二维数组b, 尺寸待定

在源代码的声明部分不需要明确数组的尺寸,在源代码的运算部分使用该数组之前,使用 allocate 命令明确数组尺寸,分配相应的内存。(相当于 c 语言的 malloc)

1
2
3
4
5
6
7
8
9
integer :: len
integer, allocatable :: a(:)
! 也可以写作如下形式
integer, allocatable, dimension(:) :: a

read(*,*) len ! 获取动态数组需要的尺寸
allocate(a(len)) ! 为动态数组分配内存

! 可以正常使用数组a

和 c 语言一样,Fortran 在运行期间分配内存 allocate 存在是否成功的问题,以及使用完成后及时释放内存 deallocate 的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
integer :: error ! 事先声明好的整型变量,用来记录标识
integer :: len1,len2
integer, allocatable :: a(:,:)

... ! 获取len1,len2

! 完整的allocate语句,包含一个标识记录是否成功分配内存
! allocate会通过stat传递给error一个数值
! error == 0 代表成功分配,error /= 0 代表失败
allocate(a(len1,len2), stat=error)
if(error /= 0)
! 未成功对数组a分配内存
end if

! 也可以使用allocated语句,判断当前动态数组是否成功分配内存,返回一个逻辑值
if(.not. allocated(a))
! 未成功对数组a分配内存
end if

... ! 使用数组a

! 释放内存,此后仍然可以继续allocate与deallocate,相当于重新设置数组尺寸。也可以使用标识
deallocate(a,stat=error)
! 或者直接deallocate(a)

固定尺寸的数组和动态数组的本质区别,就像 c/c++中的一样:固定尺寸的数组在栈上分配内存,不需要手动释放;动态数组在堆上分配内存,需要手动释放,相比于栈可使用的空间更多。对大规模的数据存储需求,倾向于在主程序中使用动态数组,由主程序负责分配和释放。

注:之前的笔记遗漏了一部分——显式指定参数,以改变多个参数的匹配顺序。

1
2
3
4
5
6
7
8
9
subroutine fun(x1,x2,x3)
...
end subroutine fun

! 使用fun(a,b,c)调用,则默认按照顺序对应
! x1=a x2=b x3=c

! 可以如下显式改变参数的匹配
! fun(x1=a,x3=b,x2=c)

数组作为参数传递

和 c 语言类似,直接把数组 a 作为实参传递给子程序 subroutine 或者函数 function 等,相当于把第一个元素的内存地址传递过去。如果子程序把这个形参定义为整数,则子程序得到的是内存地址对应的整数。此时对整数的修改会导致调用者丢失整个数组,非常危险。如果子程序把这个形参定义为数组,则会根据形参数组的尺寸处理实参对应的部分内存,实质还是传地址,因此对分量的修改会反馈给调用者。

以一个例子说明,主程序

1
2
3
4
5
6
7
8
program main
implicit none
integer :: a(5) = (/ 1,2,3,4,5 /)

call sub_num(a) ! 把a第一个元素的地址当作整数传过去
call sub_array5(a) ! 把a当作一个尺寸为5的一维数组传过去
call sub_array22(a) ! 把a当作一个尺寸为2*2的二维数值传过去
end program main

三个子程序为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
subroutine sub_num(a)
implicit none
integer :: a
write(*,*) a ! 读出的是a(1)的内存地址
end subroutine sub_num

subroutine sub_array5(a)
implicit none
integer :: a(5)
write(*,*) a ! 读出的是a在内存中的全部元素
! a(1) a(2) a(3) a(4) a(5)
end subroutine sub_array5

subroutine sub_array22(a)
implicit none
integer :: a(2,2)
write(*,*) a ! 读出的是a在内存中的前4个元素
! a(1,1) a(2,1) a(1,2) a(2,2)
end subroutine sub_array22

将数组作为参数传递,本质上是把数组变量(也就是连续内存部分的第一个元素的地址)以址传递的形式传过来,而子程序/函数的接收和处理方式,取决于自己对形参的定义:如果视作一个整数则只能访问和修改地址,如果视作数组则会进一步访问到数组中的连续内存部分,依照自己理解的尺寸进行处理。

通常为了安全,将数组作为参数传递时,也会把尺寸作为若干整数变量一起传递给子程序/函数。

指针

Fortran 实际上还有指针 pointer,与 c 语言的指针相比感觉非常鸡肋:

  1. 我们没有用 Fortran 建立链表之类的动态需求,动态数组完全够用。
  2. 语法比 c 语言更繁琐而且更弱,需要 target 形容的变量才能被指针指向,也没有*p这种运算。
  3. 各种 Fortran 编译器对于指针的实现可能有差异或麻烦,我们倾向于完全避免使用指针。

Fortran 的指针 pointer 需要配套 target 使用,target 表明变量可以被指针指向,pointer 表明这个变量是指针。

一个指针的简单例子如下

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
program main
implicit none
integer, target :: a=1 ! 声明一个可以被当作目标的整数变量
integer, pointer :: p ! 声明一个可以指向整数的指针
logical :: b

! 把指针初始化时赋给null, 可以更安全, 表明这个指针是不可访问的
integer, pointer :: p2 => null()

p=>a ! => 将指针p指向目标变量a,

! 可以通过指针直接访问目标变量
write(*,*) p ! 1

a=2 ! 对目标变量的修改也会体现在指针访问时
write(*,*) p ! 2

p=3 ! 基于指针的修改也会体现在原始变量上
write(*,*) a ! 3

! 可以检查当前的指针是否可以访问
b = associated(p)
! 可以检查当前的指针是否绑定到当前的目标变量
b = associated(p,a)
! 可以用nullify命令把指针设置为不可访问的
nullify(p)

stop
end program main