本文由知乎答主木子穿叶提供
在前三篇笔记,学习了Fortran作为一个编程语言,最基本的内容:变量,输入输出,流程控制和程序结构。接下来是Fortran的数组,我认为这是Fortran语言最有价值的精华部分,因此特意放在了学习笔记靠后的部分,在学习了基本的语法和子程序等之后。注意,Fortran的字符集不包括中括号[],因此与c语言的风格不同,Fortran对数组分量的操作全都是使用小括号()的。
因为这部分内容比较重要,不像前几篇对Fortran 77的上古语法大部分进行了忽略,这一篇对于Fortran 77的语法也进行介绍。
一维数组
最基本的一维数组声明如下
代码语言:javascript复制integer :: nums(10)
integer, parameter :: len = 20
real :: datas(len)
一维数组的类型可以是integer, real, complex, logical四种基本类型,(也可以是字符或者自定义类型,暂时不管)一维数组的长度可以是字面值常量,也可以是声明为parameter的整数——和c语言一样,数组的长度需要在编译时确定。(与c/c 语言不同,我们不需要纠结Fortran声明和定义的区别,全部称为声明)
代码语言:javascript复制nums(1) = 0
a = 2
nums(a) = nums(1) 1
数组分量的用法如上,数组分量的索引可以是整数常量或者整数变量,编译器不会进行索引的越界检查,越界检查需要程序员自行负责。
可以使用其他语法进行数组的声明,在Fortran 77中没有双冒号,而且需要两条命令分别确定数组元素的类型和数组的尺寸。
代码语言:javascript复制! 基本的用法
integer :: a(10)
! 这是另一种用法
integer, dimension(10) :: a
! 这是Fortran 77 的语法
integer a
dimension a(10)
二维数组与高维数组
与一维数组同理,二维数组的定义如下
代码语言:javascript复制! 基本的用法
real :: a(5,10)
! 这是另一种用法
real, dimension(5,10) :: a
! 这是Fortran 77的语法
read a
dimension a(10,10)
Fortran原生支持最多7维的数组。
代码语言:javascript复制real :: a(2,2)
a(1,1) = 1
特别需要注意的是,Fortran的下标从1开始!Fortran对于高维数组在内存中的连续存储方式和c语言是相反的,分别为列优先和行优先。Matlab对数组的处理继承了Fortran的风格,也是下标从1开始,列优先。 列优先:只有第一个分量变化的元素在内存中连续排列;行优先:只有最后一个分量变化的元素在内存中连续排列。
代码语言:javascript复制integer :: a(3,2)
! 数据在内存中的连续分布
! a(1,1) => a(2,1) => a(3,1) => a(1,2) => a(2,2) => a(3,2)
自定义索引
索引默认从1开始,但是也支持显式指定数组的合法索引范围,范围的左右是闭区间。例如
代码语言:javascript复制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命令赋初值,例如
代码语言:javascript复制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命令,对隐式循环语法也有更强的支持。
代码语言:javascript复制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命令,直接批量赋初值。
对数组的所有元素赋同一个初值,有如下的语法糖
代码语言:javascript复制integer :: a(3) = 5
! a(1)=5 a(2)=5 a(3)=5
数组整体运算
Fortran 90 提供了很多语法糖——原生支持对数组整体进行的运算,相比于c语言更加方便,不需要依赖循环语句实现。
代码语言:javascript复制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的语法风格。
代码语言:javascript复制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语言风格的
代码语言:javascript复制// c>0
for(i=a;i<=b;i=i c){ ... }
// c<0
for(i=a;i>=b;i=i c){ ... }
示例如下
代码语言:javascript复制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(,)语句也支持。
代码语言:javascript复制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声明一个动态数组
代码语言:javascript复制integer, allocatable :: a(:) ! 声明一个一维数组a, 尺寸待定
integer, allocatable :: b(:,:) ! 声明一个二维数组b, 尺寸待定
在源代码的声明部分不需要明确数组的尺寸,在源代码的运算部分使用该数组之前,使用allocate命令明确数组尺寸,分配相应的内存。(相当于c语言的malloc)
代码语言:javascript复制integer :: len
integer, allocatable :: a(:)
! 也可以写作如下形式
integer, allocatable, dimension(:) :: a
read(*,*) len ! 获取动态数组需要的尺寸
allocate(a(len)) ! 为动态数组分配内存
! 可以正常使用数组a
和c语言一样,Fortran在运行期间分配内存allocate存在是否成功的问题,以及使用完成后及时释放内存deallocate的问题。
代码语言:javascript复制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 中的一样:固定尺寸的数组在栈上分配内存,不需要手动释放;动态数组在堆上分配内存,需要手动释放,相比于栈可使用的空间更多。对大规模的数据存储需求,倾向于在主程序中使用动态数组,由主程序负责分配和释放。
注:之前的笔记遗漏了一部分——显式指定参数,以改变多个参数的匹配顺序。
代码语言:javascript复制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等,相当于把第一个元素的内存地址传递过去。如果子程序把这个形参定义为整数,则子程序得到的是内存地址对应的整数。此时对整数的修改会导致调用者丢失整个数组,非常危险。如果子程序把这个形参定义为数组,则会根据形参数组的尺寸处理实参对应的部分内存,实质还是传地址,因此对分量的修改会反馈给调用者。
以一个例子说明,主程序
代码语言:javascript复制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
三个子程序为
代码语言:javascript复制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表明这个变量是指针。
一个指针的简单例子如下
代码语言:javascript复制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