Fortran学习笔记——4.数组,指针与数值计算
在前三篇笔记,学习了 Fortran 作为一个编程语言,最基本的内容:变量,输入输出,流程控制和程序结构。接下来是 Fortran 的数组以及基于数组的数值计算,我认为这是 Fortran 语言最有价值的精华部分,因此特意放在了学习笔记靠后的部分,在学习了基本的语法和子程序等之后。注意,Fortran 的字符集不包括中括号[],因此与 c 语言的风格不同,Fortran 对数组分量的操作全都是使用小括号()的。
因为这部分内容比较重要,不像前几篇对 Fortran 77 的上古语法大部分进行了忽略,这一篇对于 Fortran 77 的语法也进行介绍。
一维数组
最基本的一维数组声明如下
1 | integer :: nums(10) |
一维数组的类型可以是 integer, real, complex, logical 四种基本类型,(也可以是字符或者自定义类型,暂时不管)一维数组的长度可以是字面值常量,也可以是声明为 parameter 的整数——和 c 语言一样,数组的长度需要在编译时确定。(与 c/c++语言不同,我们不需要纠结 Fortran 声明和定义的区别,全部称为声明)
1 | nums(1) = 0 |
数组分量的用法如上,数组分量的索引可以是整数常量或者整数变量,编译器不会进行索引的越界检查,越界检查需要程序员自行负责。可以使用其他语法进行数组的声明,在 Fortran 77 中没有双冒号,而且需要两条命令分别确定数组元素的类型和数组的尺寸。
1 | ! 基本的用法 |
二维数组与高维数组
与一维数组同理,二维数组的定义如下
1 | ! 基本的用法 |
Fortran 原生支持最多 7 维的数组。
1 | real :: a(2,2) |
特别需要注意的是,Fortran 的下标从 1 开始!Fortran 对于高维数组在内存中的连续存储方式和 c 语言是相反的,分别为列优先和行优先。Matlab 对数组的处理继承了 Fortran 的风格,也是下标从 1 开始,列优先。 列优先:只有第一个分量变化的元素在内存中连续排列; 行优先:只有最后一个分量变化的元素在内存中连续排列。
1 | integer :: a(3,2) |
自定义索引
索引默认从 1 开始,但是也支持显式指定数组的合法索引范围,范围的左右是闭区间。例如
1 | integer a(0:5) |
批量设置初值
Fortran 77 使用 data 命令赋初值,例如
1 | integer a |
Fortran 90 可以抛弃 data 命令,对隐式循环语法也有更强的支持。
1 | integer i |
这里使用(/ /)结构省略了 data 命令,直接批量赋初值。
对数组的所有元素赋同一个初值,有如下的语法糖
1 | integer :: a(3) = 5 |
数组整体运算
Fortran 90 提供了很多语法糖——原生支持对数组整体进行的运算,相比于 c 语言更加方便,不需要依赖循环语句实现。
1 | integer :: a(10) |
以上对于高维数组也是一样的。
数组部分运算
这也是 Fortran 90 之后的语法,和 python 的 numpy 等的数组切片操作很类似,或者说 numpy 的切片继承了 Fortran 的语法风格。
1 | integer :: a(10) |
还可以有更复杂的,使用步长,步长"a : b : c"相当于 c 语言风格的
1 | // c>0 |
示例如下
1 | integer :: a(2,3) |
上述针对数组的整体运算和部分运算放在赋值的左侧和右侧均可,相当于隐含的循环展开。write(,)语句也支持。
1 | integer :: a(2,3) |
动态数组
Fortran 77 不支持动态数组,数组尺寸必须在编译期间确定,只能在代码中使用足够大的 N 作为数组尺寸。Fortran 90 开始支持动态数组(可变长数组),数组尺寸可以在运行期间确定。
使用 allocatable 声明一个动态数组
1 | integer, allocatable :: a(:) ! 声明一个一维数组a, 尺寸待定 |
在源代码的声明部分不需要明确数组的尺寸,在源代码的运算部分使用该数组之前,使用 allocate 命令明确数组尺寸,分配相应的内存。(相当于 c 语言的 malloc)
1 | integer :: len |
和 c 语言一样,Fortran 在运行期间分配内存 allocate 存在是否成功的问题,以及使用完成后及时释放内存 deallocate 的问题。
1 | integer :: error ! 事先声明好的整型变量,用来记录标识 |
固定尺寸的数组和动态数组的本质区别,就像 c/c++中的一样:固定尺寸的数组在栈上分配内存,不需要手动释放;动态数组在堆上分配内存,需要手动释放,相比于栈可使用的空间更多。对大规模的数据存储需求,倾向于在主程序中使用动态数组,由主程序负责分配和释放。
注:之前的笔记遗漏了一部分——显式指定参数,以改变多个参数的匹配顺序。
1 | subroutine fun(x1,x2,x3) |
数组作为参数传递
和 c 语言类似,直接把数组 a 作为实参传递给子程序 subroutine 或者函数 function 等,相当于把第一个元素的内存地址传递过去。如果子程序把这个形参定义为整数,则子程序得到的是内存地址对应的整数。此时对整数的修改会导致调用者丢失整个数组,非常危险。如果子程序把这个形参定义为数组,则会根据形参数组的尺寸处理实参对应的部分内存,实质还是传地址,因此对分量的修改会反馈给调用者。
以一个例子说明,主程序
1 | program main |
三个子程序为
1 | subroutine sub_num(a) |
将数组作为参数传递,本质上是把数组变量(也就是连续内存部分的第一个元素的地址)以址传递的形式传过来,而子程序/函数的接收和处理方式,取决于自己对形参的定义:如果视作一个整数则只能访问和修改地址,如果视作数组则会进一步访问到数组中的连续内存部分,依照自己理解的尺寸进行处理。
通常为了安全,将数组作为参数传递时,也会把尺寸作为若干整数变量一起传递给子程序/函数。
指针
Fortran 实际上还有指针 pointer,与 c 语言的指针相比感觉非常鸡肋:
- 我们没有用 Fortran 建立链表之类的动态需求,动态数组完全够用。
- 语法比 c 语言更繁琐而且更弱,需要 target
形容的变量才能被指针指向,也没有
*p
这种运算。 - 各种 Fortran 编译器对于指针的实现可能有差异或麻烦,我们倾向于完全避免使用指针。
Fortran 的指针 pointer 需要配套 target 使用,target 表明变量可以被指针指向,pointer 表明这个变量是指针。
一个指针的简单例子如下
1 | program main |