分享

Fortran95/2003高级技巧

 csutjf 2012-09-03

Fortran95/2003高级技巧

(2012-03-08 20:38:01)
Fortran95/2003高级技巧的整理,主要依据Stephen J. Chapman著,刘瑾等译的《Fortran95/2003程序设计》。选择的都是平常少用又重要的高级特性。粗体表示重点或需注意的地方,蓝色表示Fortran2003才有的新特性。

1. 隐式DO循环
隐式DO循环可以用在数组初始化或读写文件中。格式为
(arg1, arg2, ..., index = istart, iend, incr)
可见,隐式DO循环可以一次返回多个值(arg1, arg2, ...)。以此可以产生复杂的模式。例如下面的语句
integer, dimension(25) :: array = (/ ((0, i = 1, 4), 5 * j, j = 1, 5) /)
初始化array中不能整除5的元素为0,其他元素为5的倍数。结果是
0, 0, 0, 0, 5, 0, 0, 0, 0, 10, 0, 0, 0, 0, 15, 0, 0, 0, 0, 20, 0, 0, 0, 0, 25

2. 静态变量
声明一个变量时如果附加save属性,则这个变量成为静态变量,其值一旦初始化后一直存在。也可以用单独的save语句来声明静态变量。
2.1 子程序中的静态变量
在子程序中声明的静态变量,其值在退出子程序后仍然保留,当下次调用子程序时,可以直接使用上次保留的值。例如
subroutine test(reset)
    logical, intent(in) :: reset
    integer, save :: cnt

    if (reset) then
        cnt = 0
    else
        cnt = cnt + 1
    endif
    write(*, *) cnt
end subroutine test
在主程序中调用它:
program main
implicit none
integer :: i

call test(.true.)
do i=1, 3
    call test(.false.)
end do
end program
程序的输出将是
0
1
2
3
因为上次调用test后变量cnt的值保留,下次调用时直接使用。

需要注意的是,在子程序中变量声明时直接初始化的变量隐含具有save属性。
integer :: cnt = 0
等价于
integer, save :: cnt = 0
这种变量的初始化操作只会进行一次,而不是每次进入子程序都初始化!所以下面的程序
subroutine test()
    integer :: cnt = 0
    
    write(*, *) cnt
    cnt = cnt + 1
end subroutine test

program main
implicit none
integer :: i 

do i = 1, 3
    call test
end do
end program
结果为
1
2
3

2.2 模块中的静态变量
在模块中用save命令,则其后定义的变量均成为静态变量。所有引用该模块的程序都可以使用并改变静态变量的值,在其他模块中此改变继续有效。事实上,这种静态变量相当于其他语言中的全局变量。

3. where结构
where结构的作用是对于给定逻辑数组中每个真值,对目的数组的相应元素进行某中操作。例如
where (a > 0)
    b = 1
elsewhere
    b = 0
end where
相当于
b(a>0) = 1
b(a<=0) = 0
但where结构运行效率更高。
使用where结构的限制是对于每个元素的操作都不应该与其他元素有关。

4. forall结构
forall结构严格来讲不是一种循环结构,而是使编译器能够对每个数组元素启动一个单独的进程来处理,从而能够实现并行运算。例如
forall (i = 1 : n, j = 1 : n, i < j)
    a(i, j) = sqrt(a(i, j))
end forall
使得编译器对于a中i<j的每个元素启动一个单独的进程求其平方根从而提高效率。对于大型计算机或有多个CPU的计算机,当数组很大时,能够显著地提高效率。
因为每个元素独立运算,forall结构与where类似,其中不应该涉及与其他元素有关的操作。如果forall结构中包括了不止一条语句,那么处理机会等第一条语句的每个进程都完成后才进行下一条操作。
另外,forall结构中调用的函数或过程必须是纯的(见第6条)。

5. 动态数组
5.1 动态数组的分配和赋值
动态数组声明时具有allocatable属性,在需要的时候用allocate语句分配空间,并用deallocate语句销毁。
allocate语句格式为:
allocate(arr1(3, 3), stat = status, errmsg = err_msg)
或者
allocate(arr1, source = arr2, stat = status, errmsg = err_msg)
其中第一种格式将arr1数组分配为3*3的大小;第二种格式则将arr1分配为与arr2相同形状,并复制arr2的值,实际上相当于将arr2进行了拷贝。
stat和errmsg关键字说明,操作结束后,status变量包含了状态代码,err_msg变量则包含了错误信息。如果操作成功,status=0,err_msg为空;否则status为非零整数,err_msg则包含错误信息。

向动态数组赋值时,如果它未曾分配过空间,则自动为其分配合适的空间;如果它已经分配了空间,则自动释放原空间,并重新分配合适的空间。因此动态数组可以无缝使用。不过当数组很大时这个操作比较费时,应尽量预先分配空间并不再改变。

5.2 动态的静态数组
在一个子程序中,如果动态数组声明时具有save属性,则只有第一次调用该子程序时才利用allocate语句分配内存。即allocate语句相当于变成了
if (.not. allocated(arr)) then
    allocate(arr)
endif
不具有save属性的动态数组,每次进入子程序时都必须分配内存,并且在退出子程序时自动销毁。

6. 纯过程和纯函数
纯函数是没有任何负面影响的函数。也就是说它满足下面条件:
1) 所有参数都声明为intent(in),不修改任何参数
2) 没有静态变量,即没有save属性,也不在声明变量同时进行初始化
3) 被它所调用的任何函数或过程都必须是纯的
4) 不能有文件IO
5) 不能使用stop语句来突然终止程序
纯过程与纯函数类似,只不过允许它们修改用intent(out)和intent(inout)声明的参数
纯函数和纯过程是没有任何负面影响的子程序,因此他们可以安全地放到forall结构中进行并行处理。
要声明一个函数或过程是纯的,只要在function或subroutine前面加上pure即可。

7. 逐元函数和逐元过程
逐元函数是对标量进行操作,且返回标量的函数。逐元函数的特点是虽定义为标量操作,但如果传入数组参数,则对每个数组元素都应用该函数且返回修改后的数组。于是可以很方便地定义对数组每个元素进行操作的函数。
逐元函数必须满足下列条件:
1) 逐元函数必须是纯函数
2) 所有的形参都是标量,不能是指针
3) 返回值也是标量,不能是指针
4) 形参不能在数组声明语句中用于声明数组的形状
逐元函数的定义方式只要在function前面加上elemental即可。逐元过程与之类似。

8. 内部子程序
内部子程序是在一个函数或过程内部定义的函数或过程。例如
subroutine father()
    ...
    contains
        subroutine son()
            ....
        end subroutine son
end subroutine father
son就是father的内部子程序。内部子程序能够使用和改变其父程序中的任何变量,但如果内部子程序中定义了与父程序中同名的变量,则该变量不再代表父程序中的变量。

9. 以平台无关的方式比较字符串
字符串比较运算符<、<=、>、>=的比较结果与计算机上采用的字符集有关,在不同的平台上不能保证结果一致。而相对应的函数llt、lle、lgt、lge能保证总是使用相同的字符集来进行操作,因此能够保证运行结果在不同的平台上保持一致。

10. 内部文件
内部文件就是指字符串。在文件操作时以字符串代替文件号,就可以从字符串读取数据,或者将数据写入字符串。这就提供了一种字符串和数字(或其他类型)进行转换的方式。例如
character(len=5) :: input = '123.4'
real::value
read(input, *) value
将字符串123.4转换成实数。
内部函数不能打开、关闭、重定位。

11. 参数化派生数据类型
定义派生数据类型(即结构体)时可以带入参数,如
type :: vector (kind, n)
    integer :: kind = kind(0.)
    integer :: n = 3
    real(kind), dimension(n) :: v
end type vector
定义了一个派生数据类型vector。其中含有一个实型数组,但数组的种别和大小都是由参数决定的。种别默认为单精度,大小默认为3. 声明该类型的实例时的语法为
type(vector) :: v1
type(vector(kind(0.))) :: v2
type(vector(kind(0.), 3)) :: v3

12. associate结构
associate结构用于在一个代码段执行过程中,临时用一个别名代替某变量或表达式的原名。当变量或表达式的名字很长时这种结构能够简化代码。相当于VB中的with结构。例如
associate(x => active_tracks(i)%x, y => active_tracks(i)%y)
    dist = sqrt((rada%x - x) ** 2 + (rada%y - y) ** 2)
end associate

13. 接口块
很多情况下(如使用了可选参数等),要求函数或过程必须具有显式接口。这时就需要编写接口块。现代fortran编程中可以使用模块,模块中定义的子程序自动具有显式接口,因此不需再明确定义。然而某些特殊情况下(如外部函数是用C语言写的)仍然需要接口块。接口块的定义方式为:
interface
    subroutine sub1(a, b)
        integer :: a
        real :: b
    end subroutine sub1
    subroutine sub2 ...
        ...
end interface

14. 函数(或过程)重载
很多情况下想让某个函数既能接受整数参数,又能接受实数参数,或者其他类型的参数,那么就可以用函数重载。函数重载是通过接口块来实现的。例如
interface sort
    function sorti(array) result(res)
        integer, dimension(:), intent(in) :: array
        integer, dimension(:), allocatable :: res
    end function sorti

    function sortr(array) result(res)
        real, dimension(:), intent(in) :: array
        real, dimension(:), allocatable :: res
    end function sortr
end interface sort
定义了一个重载函数sort,它既能对实型数组进行排序,也能处理整型数组。sorti和sortr分别用于两种数据类型的处理,而调用时只需调用sort,就能根据传递的参数类型自动选择合适的函数进行处理。
如果函数或过程在模块中,则简单许多:
interface sort
    module procedure sorti
    module procedure sortr
end interface sort

15. 运算符重载
运算符其实相当于函数。运算符也可以重载,方法为:
interface operator(operator_symbol)
    module procedure function_1
end interface
其中operator_symbol可以是内置的运算符+-*/><等,也可以是开头和结尾都有点号的自定义运算符如.operator.。function_1是一个模块函数,它的参数就是出现在运算符两边的运算数,返回值就是表达式的结果。对于二元运算符,运算符左边的运算数是函数的第一个参数,右边的是第二个。参数必须用intent(in)声明。
赋值运算符的重载方式为
interface assignment(=)
    module procedure subroutine_1
end interface
接口体指向过程而不是函数。过程必须有两个参数,第一个是赋值语句的输出,相当于赋值号左边的变量,用intent(out)声明;第二个是赋值语句的输入,相当于赋值号右边的数据,用intent(in)声明。

16. 访问权限
在模块中用一个单独的private语句指明默认情况下变量和子程序都是私有的,在其他模块中是不可见的。如果要暴露某个变量或过程,用单独的public语句或属性指明。
定义派生数据类型时也可以声明某个字段为公有或私有。

17. use语句的高级选项
use语句用于导入其他模块中的变量或子程序。可以指定仅导入模块中的某些内容,即
use module_name, only: var1, var1, function1, type1...
也可以为外部模块中的某变量指定别名,即
use module_name, newname => oldname
use module_name, only: newname => oldname

18. 命令行参数
fortran2003以前,获取命令行参数是很难的,只能通过编译器提供的扩展功能实现。不同编译器提供的扩展方式不同,导致fortran程序缺乏可移植性。fortran2003提供了命令行参数的内部支持。
函数command_argument_count()返回命令行参数的个数;
过程get_command(command, length, status)返回全部命令行参数。字符变量command保存了全部命令行参数,整型变量length为参数字符串的长度,整数变量status保存成功与否的状态,成功为0;
过程get_command_argument(number, value, length, status)返回单个命令行参数。整型变量number指定返回哪个命令行参数。

19. 环境变量
子程序get_environment_variable(name, value, length, status, trim_name)返回系统环境变量的值。name为环境变量名,value返回环境变量值,trim_name是逻辑参数,为真表示忽略末尾空格。

20. 高级输入输出格式控制符
EN -- 工程计数法,1-1000之间的数乘以10的N次方,N为3的倍数。方便用”微“”毫“”千“”兆“等单位。
ES -- 科学计数法,整数部分为1位。
B -- 二进制
O -- 八进制
Z -- 十六进制
RU / RD / RZ / RN / RC / RP -- 舍入方式,向上舍入(向正方向),向下舍入(向负方向),向零,四舍六入五留双,四舍五入,系统默认
DC / DP -- 小数分隔符,逗号/小数点
nX -- 水平空n格
/ -- 换行
Tc -- 跳至第c列
TLn / TRn -- 向左/右移n列
SP/SS -- 整数显示/不显示+号

21. 普通文件操作
21.1 open语句
open语句的格式为
open(unit=..., file=..., iostat=..., ... = ..., )
其中unit、file等为关键字。open语句支持的关键字中,较高级的有:
iomsg -- 当操作成功时为空,否则包含错误信息
decimal -- 小数分隔符(逗号/小数点)
encoding -- 字符编码方式,utf-8 / default
round -- 舍入方式,up / down / zero / nearest / compatible / processor defined:与RU等格式控制符相同
sign -- 是否显示正号,plus / suppress / processor defined:正号/没有/默认
delim -- 字符串分隔符,apostrophe / quote / none:空格/引号/没有
position -- 打开文件后定位的位置,rewind / append / asis:文件头/追加/随便
另外,status关键字除熟悉的new、replace之外,还可以是scratch,表示创建一个临时文件,文件关闭时自动删除。

21.2 inquire语句
inquire语句用于查询文件信息。其关键字与open类似。未打开的文件需指定file关键字,打开过的文件可指定file也可指定unit关键字。与open不同的高级关键字有:
exist -- 文件是否存在
opened -- 文件是否打开
number -- 文件号(仅限于已经打开的文件)
named -- 是否有名(临时文件无名)
另外,由于直接访问文件的记录长度的单位与平台有关,可能是字节,也可能是别的,inquire语句提供了一种与平台无关的确定直接访问文件记录长度的方法:
inquire(iolength=int_var)output-list
其中output-list是变量、常量和表达式的列表,类似于write语句中出现的;int_var返回记录的长度。于是在打开直接访问文件之前,用上述inquire语句确定欲写入的数据(一个记录)在该平台上的长度,就可以在open语句中正确的指定recl参数。

21.3 read语句
read语句与open类似,为它的关键字指定值可以暂时覆盖open语句指定的整个文件的设置。
另外read语句的advance关键字可以指定一条read命令结束后是否换行,取值为yes或no。只对顺序访问文件有效。

21.4 rewind / backspace / endfile
rewind用于回到文件头,backspace用于回到上一条记录处,endfile用于在文件中写入一个文件结束符。

22. namelist I/O
namelist是一种方便的数据读写方法,它使用固定的格式,可以用作程序的配置文件。
定义namelist的方法:
namelist /namelist_name/ var1, var2, ...
这属于一条声明语句,与其他声明语句一样,要放在第一条可执行语句之前。同一个namelist的多次定义会将所有的定义合在一起,相当于一条长的namelist定义语句。
读入或写出namelist到文件时,在read或write语句中用nml关键字指明namelist的名称。如果namelist文件中只设置了一部分变量的值,则其他变量保持不变。数组在输出时会把所有元素全部输出,类似a(1)=3., a(2)=1.等;读入时如果文件中只设置了数组的部分元素,则其他元素保持不变。
需要注意的是delim关键字设置了字符串是否放在引号中。

23. 流访问模式
fortran2003在传统的顺序访问模式、直接访问模式的基础上新增加了流访问模式。顺序访问模式则以行为单位,一行是一个记录,换行符视作记录的结束。而流访问模式类似于C语言的方式,以字节为单位,对其中的控制字符(如换行)也当作普通字符,并不区别对待。
在open语句中access关键字设为stream即可以流访问方式打开文件。同样使用write语句写入数据,但一条write语句结束后并不换行。要插入换行符(类似于C语言的\n),需要用换行命令newline()。

24. 异步访问模式
对于超大文件的读写,传统的方式在read或write语句开始执行后就一直等待其结束,期间程序处于阻塞状态。fortran2003开始支持异步访问模式,它允许在read或write语句开始后就立刻返回,回到程序中继续执行下面的语句,而文件的读写在单独的进程中同时进行。这就使得程序在I/O的同时仍处于慢速运转。
要开启异步传输模式,在open语句中将asynchronous关键字设为yes。即使文件已经用异步模式打开,read和write语句仍然默认为传统输出方式,需要在read和write语句用也用asynchronous关键字指明当前的读写操作采用异步方式。
另外,inquire语句也有一个asynchronous关键字,返回该文件是否支持异步模式,值为yes或no。在使用异步模式之前应该总是使用inquire查询。

由于异步读写方式在read或write语句之后立刻返回,所以所读取的变量还没有完全读取,所写的文件也没有完全写完,有些操作就不能马上进行。比如异步read操作结束之前就使用读取的变量的值是没有意义的,异步write操作结束之前就移动文件也是不对的。那么怎样知道某一步异步读写是否已经完成呢?答案是在每一个异步read / write语句中都应该用id关键字返回一个唯一的标识,然后在需要的地方使用
inquire(id= ..., pending = ...)
根据pending的值(.true.或.false.)判断该操作是否结束。或者使用
wait(unit=...)
语句,等待指定文件号的文件读写完成。只有该文件中的所有读写操作都已完成时,控制权才会交给下一条语句继续执行。如果试图在wait语句之前就使用读取的变量,编译器将给出警告。

25. ISO_FORTRAN_ENV模块
fortran2003包括一个新的内置模块iso_fortran_env,其中定义了一些平台无关的环境常量,如:
input_unit / output_unit / error_unit -- 标准输入、标准输出、标准错误的文件代号
iostat_end -- 当达到文件结尾时,read语句的iostat关键字返回的值,可用于判断是否到达文件尾
iostat_eor -- 当到达记录结尾时,read语句的iostat关键字返回的值,可用于判断记录是否结束
另外,fortran2003还提供了两个函数is_iostat_end()和is_iostat_eor()用于判断某整数是否等于文件结尾和记录结尾标志。

26. 指针
26.1 指针的定义和使用
fortran的指针其实不是真正的C语言式的指针,而仅仅是C++、Java或Python中的引用。定义方法为在类型声明语句中加入pointer属性,可以被指针指向的对象也必须声明为target属性,这是为了提高编译性能。指针的声明类型指出了它能指向什么类型的变量。对于数组,指针声明时需要给出维数,而不需指定每一维的长度。
指针声明后,未指向任何目标。可以通过=>运算符将指针指向某变量,也可以在声明的同时初始化,即
integer, target :: i
integer, pointer :: p => i
最好在指针声明的同时初始化其指向,如果确实不能确定要指向的目标,可以将指针赋值为null()函数的返回值。null()函数返回一个空指针,它是一个指针,有自己的指向目标,只不过它的目标不是任何变量,所以称之为空。

指针一旦指向了某变量,则该指针就可以看作该变量的一个别名,完全代替该变量使用。例如,加入指针p1和p2分别指向变量a1和a2,则
p1 = p2
完全等价于
a1 = a2
p1 => p2
相当于
p1 => a2
即此时p1和p2都指向a2.

内置过程nullify可以断开指针的指向。例如
nullify(p1, p2)
将p1和p2各自与他们的目标断开,则p1和p2都成为未指向任何目标的状态。应该总是在nullify之后使用p1=null()将指针置空。

内置函数associated(p1)返回逻辑值,指明指针p1是否有目标,而associated(p1, a1)则返回p1是否指向a1.

对于数组,指针可以指向整个数组,或者数组的切片。而用向量下标所获取的部分数组不能作为指针的对象。因为切片是原数组的视图,可以仅仅在原数组中移动就可获得;而向量下标不能保证所获取的部分数组中没有重复的部分,所以不能简单的用指针的移动来获得。

26.2 指针用于动态内存分配
指针可以用于allocate语句中用于动态内存分配:
allocate(pointer1(3,3), stat=status, errmsg = err_msg)
可见其用法与动态数组一样。回收内存时同样使用deallocate语句,该语句回收内存空间的同时,也取消指针的指向,使其成为没有目标的状态。应该总是在deallocate之后将指针赋值为null()。

使用指针动态分配内存空间是危险的,必须十分小心。一方面,因为如果忘记回收内存空间就将指针指向别的对象或将其空置,则内存中将会存在已经分配、不能被其他程序所用、本程序中又没有用到的空间,即“内存碎片”。另一方面,如果多个指针指向同一块内存空间,而这块内存空间被其中一个指针销毁了,则其他指针不会自动取消关联,他们仍然指向这块内存空间。而该内存空间已经被操作系统回收,用于别的目的了,其中的数据是未知的,因此如果试图访问甚至修改这些数据,将有可能造成很严重的问题。这种指针称为“野指针”。

在程序中应该注意避免内存碎片和野指针的出现。应该总是在改变指针指向之前回收其内存空间,或将其目标赋予别的指针,保证一定能回收这块内存空间;销毁一块内存空间后空置所有指向这块内存空间这指针;并且程序退出之前应该总是销毁所使用的所有内存空间。另外用指针分配的内存空间与动态数组不同,它在程序退出时不能自动回收,因此也会造成内存碎片。

27. 时间和日期
内置过程cpu_time(time)返回当前cpu时间,于是可以通过比较程序运行前后的cpu时间的方法来计算程序的耗时。对于多cpu的机器,time是一个数组,包括了每个cpu各自的处理时间。
内置过程date_and_time(date, time, zone, values)返回日期和时间,所有的参数都是可选的,但至少应该给定一个。参数的含义为:
date -- 日期,YYYYMMDD
time -- 时间,HHMMSS.SSS
zone -- 时区,+-HHMM,当地时间与UCT或GMT标准时间的差
values(1) -- 年,YYYY
values(2) -- 月,1-12
values(3) -- 日,1-31
values(4) -- 时区,以分为单位
values(5) -- 时,0-23
values(6) -- 分,0-59
values(7) -- 秒,0-60
values(8) -- 微秒,0-999

28. 面向对象(OOP)
另文论述。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多