陷阱#

Fortran 语法通常简单且一致,但与许多其他语言一样,它也有一些缺陷。有时是由于遗留原因(Fortran 标准的演变强烈强调向后兼容性,因为大量的遗留代码仍在积极使用),有时是由于真正的缺陷(在某些时候做出了糟糕的选择,如果不破坏向后兼容性,则很难甚至不可能纠正),有时仅仅是因为 Fortran 不是 C 或 C++ 或 Python 或其他任何语言:它有自己的逻辑,其功能有时可能会让习惯于其他语言的开发人员感到惊讶。

所有代码片段均使用 gfortran 13 编译。

隐式类型#

program foo
    integer :: nbofchildrenperwoman, nbofchildren, nbofwomen
    nbofwomen = 10
    nbofchildrenperwoman = 2
    nbofchildren = nbofwomen * nbofchildrenperwoman
    print*, "number of children:", nbofchildrem
end program

程序编译并执行,结果为

 number of children:           0

等等……Fortran 无法将两个整数相乘?当然不是……问题在于打印变量名称时输入错误:nbofchildreM 而不是 nbofchildreN。但是为什么编译器没有发现这个错误呢?好吧,因为默认情况下 Fortran 使用隐式类型:当遇到尚未显式声明类型的变量时,编译器会根据名称的首字母推断类型。以 I、J、K、L、M、N 开头的变量名称类型为 INTEGER,其他所有变量名称类型为 REAL(因此有了经典的笑话“GODREAL,除非声明为 INTEGER”)。

隐式类型与 Fortran 一样古老,在那个时代没有显式类型。尽管它在快速编写一些测试代码时仍然很方便,但这种做法很容易出错,因此不建议使用。强烈建议的最佳实践是在所有程序单元(主程序、模块和独立例程)的开头始终禁用隐式类型,方法是声明 implicit none(在 Fortran 90 中引入)。

program foo
implicit none
    integer :: nbofchildrenperwoman, nbofchildren, nbofwomen
    nbofwomen = 10
    nbofchildrenperwoman = 2
    nbofchildren = nbofwomen * nbofchildrenperwoman
    print*, "number of children:", nbofchildrem
end program

现在编译失败了,可以快速更正输入错误

    7 |     print*, "number of children:", nbofchildrem
      |                                               1
Error: Symbol 'nbofchildrem' at (1) has no IMPLICIT type; did you mean 'nbofchildren'?

隐式保存#

subroutine foo()
implicit none
    integer :: c=0

    c = c+1
    print*, c
end subroutine

program main
implicit none
    integer :: i

    do i = 1, 5
        call foo()
    end do
end program

习惯于 C/C++ 的人预计此程序会打印 5 次 1,因为他们将 integer :: c=0 解释为声明和赋值的串联,就像它一样

integer :: c
c = 0

但事实并非如此。此程序实际上输出

1
2
3
4
5

integer :: c=0 实际上是一次性的编译时初始化,它使变量在对 foo() 的调用之间保持持久性。它实际上等价于

integer, save :: c=0

save 属性等效于 C 的 static 属性,用于使变量持久化,并且在变量初始化的情况下隐式使用。与遗留(并且仍然有效)语法相比,这是一种现代化的语法(在 Fortran 90 中引入)。

integer c
data c /0/

老 Fortran 程序员只知道现代化语法等效于遗留语法,即使未指定 save。但事实上,隐式保存可能会误导习惯于 C 逻辑的新手。因此,通常建议始终指定 save 属性

integer, save :: c=0   ! save could be omitted, but it's clearer with it

注意:派生类型组件的初始化表达式是完全不同的情况

type bar
    integer :: c = 0
end type

在此,c 组件在每次实例化 type(bar) 变量时初始化为零(运行时初始化)。

浮点字面常量#

以下代码片段定义了一个双精度常量 x(在大多数系统上,它是一个 IEEE754 64 位浮点数,具有 15 位有效数字)

program foo
implicit none
    integer, parameter :: dp = kind(0d0)
    real(kind=dp), parameter :: x = 9.3
    print*, precision(x), x
end program

输出为

          15   9.3000001907348633

因此,x 具有预期的 15 位有效数字,但打印的值从第 8 位开始就错误了。原因是浮点字面常量隐式具有默认的实数种类,该种类通常是 IEEE754 单精度浮点数(约 7 位有效数字)。实数 \(9.3\) 没有精确的浮点数表示,因此首先将其近似为单精度直至第 7 位,然后在分配给 x 之前将其转换为双精度。但是,先前丢失的数字显然不会恢复。

解决方案是显式指定常量的种类

real(kind=dp), parameter :: x = 9.3_dp

现在输出在第 15 位是正确的

          15   9.3000000000000007     

浮点字面常量(再次)#

现在假设您需要一个浮点常量为 1/3(三分之一)。您可以编写

program foo
implicit none
    integer, parameter :: dp = kind(0d0)
    real(dp), parameter :: onethird = 1_dp / 3_dp
    print*, onethird
end program

然后输出为(!)

   0.0000000000000000     

原因是 1_dp3_dp整数字面常量,尽管 _dp 后缀应该表示浮点种类。因此,除法是整数除法,结果为 0。这里的陷阱是标准允许编译器对 REALINTEGER 类型使用相同的种类值。例如,使用 gfortran,在大多数平台上,值 \(8\) 既是双精度种类又是 64 位整数种类,因此 1_dp 是一个完全有效的整数常量。相比之下,NAG 编译器默认使用唯一的种类值,因此在上面的示例中,1_dp 会产生编译错误。

表示浮点常量的正确方法是始终包含点

    real(dp), parameter :: onethird = 1.0_dp / 3.0_dp

然后输出为

  0.33333333333333331     

打印中的前导空格#

program foo
implicit none
    print*, "Hello world!"
end program

输出

% gfortran hello.f90 && ./a.out
 Hello world!

请注意额外的前导空格,它在源代码的字符串中不存在。从历史上看,第一个字符包含早期打印机的回车控制代码,并且本身不会打印。空格“ ”指示打印机在打印内容之前执行 CR+LF 序列,并由 Fortran print* 语句自动添加。一些编译器仍然这样做,尽管现代输出设备既不拦截也不使用控制字符,因此该字符被“打印”。如果此前导空格存在问题(很少出现),则可以使用显式格式而不是 *(表示“让编译器决定如何格式化输出”)。

    print "(A)", "Hello world!"

在这种情况下,编译器不再添加前导空格

% gfortran hello.f90 && ./a.out
Hello world!

文件名扩展名#

假设我们将上面的“Hello world”程序放在源文件 hello.f 中。大多数编译器都会产生许多编译错误

% gfortran hello.f
hello.f:1:1:

 program foo
 1
Error: Non-numeric character in statement label at (1)
hello.f:1:1:

 implicit none
 1
Error: Non-numeric character in statement label at (1)
hello.f:2:1:

     print*, "Hello world!"
     1
Error: Non-numeric character in statement label at (1)

...[truncated]

原因是 .f 扩展名根据广泛接受的约定“保留”用于遗留的“固定源形式”,该形式是为穿孔卡片系统设计的。特别是,第 1-6 列保留用于标签、延续字符和注释,实际的语句和指令必须位于第 7-72 列。自由源形式消除了固定源形式的所有限制,但由于后者与前者共存,因此大多数编译器采用的约定是默认使用 .f90 扩展名表示自由形式源。请注意,这通常可以通过一些编译器开关更改,并且 Fortran 包管理器 (fpm) 的最新版本默认认为所有源都是自由形式,而不管扩展名如何。

注意:一个常见的误解是,.f90 源文件仅限于 Fortran 90 标准修订版,不能包含在较新修订版(Fortran 95/2003/2008/2018)中引入的功能。这是错误的,并且完全没有关系:选择 .f90 的唯一原因是在 Fortran 90 修订版中引入了自由格式。 .f.f90 源文件都可以包含来自任何标准修订版的功能。