The Flang Compiler

C 程序员的 Fortran 指南

« Design Guideline :: Contents :: Flang Fortran Standards Support »

C 程序员的 Fortran 指南

这份笔记仅限于关于 Fortran 的基本信息,以便 C 或 C++ 程序员可以更快地开始使用这门语言(至少作为读者),并避免在开始编写或修改 Fortran 代码时出现一些常见的陷阱。 请查阅其他资料,了解 Fortran 的丰富历史、当前应用以及新代码中的现代最佳实践。

至少要知道这些

Rosetta Stone 对照表

Fortran 的语言标准和其他文档以一些可能不熟悉的特定方式使用一些术语。 Fortran | English ---|--- Association | 使名称引用其他内容 Assumed | 参数或接口的某些属性,直到进行调用时才知道 Companion processor | C 编译器 Component | 类成员 Deferred | 变量的某些属性,直到分配或赋值时才知道 Derived type | C++ 类 Dummy argument | C++ 引用参数 Final procedure | C++ 析构函数 Generic | 重载函数,由实际参数解析 Host procedure | 包含嵌套子程序的子程序 Implied DO | 语句内部有一个循环 Interface | 原型 Internal I/O | sscanfsnprintf Intrinsic | 内置类型或函数 Polymorphic | 动态类型 Processor | Fortran 编译器 Rank | 数组具有的维度数 SAVE attribute | 静态分配 Type-bound procedure | 一种 C++ 成员函数,但实际上不是 Unformatted | 原始二进制

数据类型

有五个内置(“固有”)类型:INTEGERREALCOMPLEXLOGICALCHARACTER。 它们使用“kind”值进行参数化,应将其视为不可移植的整数代码,尽管在今天的实践中,这些是数据的大小(以字节为单位)。 (对于 COMPLEX,kind 类型参数值是两个 REAL 组件之一的大小(以字节为单位),或者总大小的一半。) 传统的 DOUBLE PRECISION 固有类型是 REAL 的 kind 的别名,它应该比默认的 REAL 更精确且更大。 COMPLEX 是一个简单的结构,由两个 REAL 组件组成。 CHARACTER 数据也具有长度,该长度可能在编译时已知,也可能未知。 CHARACTER 变量是固定长度的字符串,当未完全赋值时,它们会用空格字符填充。 用户定义(“派生”)的数据类型可以从固有类型和先前定义的用户类型合成,很像 C struct。 派生类型可以使用整数值进行参数化,这些整数值必须在编译时是常量(“kind”参数)或延迟到执行时(“len”参数)。 派生类型最多可以从另一个派生类型继承(“extend”)。 它们可以具有用户定义的析构函数(FINAL 过程)。 它们可以为其组件指定默认初始值。 通过一些工作,还可以指定一个通用构造函数,因为 Fortran 允许通用接口与派生类型的名称相同。 最后,有一些“无类型”二进制常量,可以在一些情况下使用,例如静态数据初始化或立即转换,其中类型不是必需的。

数组

数组在 Fortran 中不是类型。 成为数组是对象或函数的属性,而不是类型的属性。 与 C 不同,不能拥有数组的数组或指针数组,但可以拥有一个派生类型数组,该数组具有数组或指针作为组件。 数组是多维的,维度数称为数组的 rank。 在存储中,数组的存储方式使得最后一个下标在内存中具有最大的步幅,例如,A(1,1) 之后是 A(2,1),而不是 A(1,2)。 是的,每个维度的默认下限是 1,而不是 0。 表达式可以将数组作为多维值进行操作,并且编译器将创建必要的循环。

可分配对象

现代 Fortran 程序广泛使用 ALLOCATABLE 数据。 这样的变量和派生类型组件是动态分配的。 当它们超出范围时,它们会自动取消分配,很像 C++ 的 std::vector<> 类模板实例。 数组边界、派生类型 LEN 参数,甚至可分配对象的类型都可以延迟到运行时。 (如果您真的想了解所有关于现代 Fortran 的知识,我建议您研究可以使用 ALLOCATABLE 数据完成的所有事情,并跟进文档中从 ALLOCATABLE 的描述到其他主题的所有引用; 它是一项与语言其余部分交互的功能。)

I/O

Fortran 的输入/输出功能构建到语言的语法中,而不是像 C 和 C++ 那样由库接口定义。 有原始二进制 I/O 和“格式化”传输到字符表示的方法。 有使用固定大小记录的随机访问 I/O 以及顺序 I/O 的方法。 可以通过“内部”格式化 I/O 从 CHARACTER 变量扫描数据或将数据格式化为 CHARACTER 变量。 从文件到文件以及从文件到文件的 I/O 使用一个整数“单元”编号方案,该方案类似于 UNIX 的打开文件描述符; 也就是说,打开一个文件并为其分配一个单元编号,然后在后续的 READWRITE 语句中使用该单元编号。 格式化 I/O 依赖于格式规范将值映射到字符字段,类似于 C 的 printf 系列标准库函数使用的格式字符串。 这些格式规范可以出现在 FORMAT 语句中,并通过它们的标签、直接在 I/O 语句中的字符文字或字符变量中引用。 还可以使用编译器生成的格式在“列表导向”I/O 中,其中编译器根据数据类型推导出合理的默认格式。

子程序

Fortran 既有 FUNCTION 子程序,也有 SUBROUTINE 子程序。 它们共享相同的命名空间,但函数不能作为子程序调用,反之亦然。 子程序使用 CALL 语句调用,而函数使用表达式中的函数引用调用。 有一个级别的子程序嵌套。 函数、子程序或主程序可以具有嵌套在其中的函数和子程序,但是这些“内部”过程本身不能具有自己的内部过程。 与 C++ lambda 表达式的情况一样,内部过程可以引用来自其宿主子程序的名称。

模块

现代 Fortran 对单独编译和命名空间管理有很好的支持。 模块 是编译的基本单元,当然,独立的子程序仍然存在,主程序也是如此。 模块定义类型、常量、接口和嵌套子程序。 来自模块的对象可以通过 USE 语句在其他编译单元中使用,该语句具有限制可用对象以及重命名对象的选项。 对模块中对象的所有引用都使用直接名称或已添加到本地范围的别名完成,因为 Fortran 没有使用模块名称限定引用的方法。

参数

函数和子程序具有“虚”参数,这些参数在调用期间与实际参数动态关联。 本质上,Fortran 中的所有参数传递都是按引用传递,而不是按值传递。 可以通过声明虚参数具有 INTENT(IN) 来限制对参数数据的访问,但这对应于在 C++ 中使用 const 引用,并不意味着复制数据; 使用 VALUE 来实现该目的。 当无法将对象的引用或对象的稀疏规则数组部分作为实际参数传递时,Fortran 编译器必须分配临时空间以在调用期间保存实际参数。 当实际参数括在括号中时,始终保证会发生这种情况。 编译器可以自由地假设虚参数与其他数据之间的任何别名都是安全的。 换句话说,如果某些对象可以使用一个名称写入,则永远不会使用同一范围中的其他名称读取或写入该对象。

 SUBROUTINE FOO(X,Y,Z)
 X = 3.14159
 Y = 2.1828
 Z = 2 * X ! 可以在编译时折叠
 END

这与 C 或 C++ 编译器在尝试使用指针优化代码时必须承担的假设相反。

重载

Fortran 通过其接口功能支持一种重载形式。 默认情况下,接口是指定一组子程序和函数的原型的手段。 但是,当接口被命名时,该名称将成为其特定子程序的 通用 名称,并且通过通用名称的调用在编译时会根据实际参数的类型、kind 和 rank 映射到其中一个特定子程序。 类似的功能可用于通用类型绑定过程。 此功能也可用于重载内置运算符和一些 I/O 语句。

多态

可以使用 CLASS 编写 Fortran 代码以接受某些派生类型或其任何扩展的数据,从而将实际类型延迟到执行,而不是通常的 TYPE 语法。 这有点类似于在 c++ 中使用 virtual 函数。 必要时,Fortran 的 SELECT TYPE 构造用于动态区分可能的特定类型。 这有点像 C++17 中对判别联合的 std::visit()

指针

指针是 Fortran 中的对象,而不是数据类型。 指针可以指向数据、数组和子程序。 指针只能指向具有 TARGET 属性的数据。 在指针赋值语句 (P=>X) 以及一些固有函数和使用指针虚参数的情况之外,指针会被隐式取消引用,并且使用它们的名称是对它们指向的数据的引用。 与 C 不同,指针不能指向指针 本身,也不能用于实现对可分配对象管理结构的间接级别。 如果您分配给 Fortran 指针以使其指向另一个指针,则您正在使该指针指向另一个指针指向的数据(如果有)。 同样,如果您分配给 Fortran 指针以使其指向可分配对象,则您正在使该指针指向可分配对象的当前内容,而不是指向管理可分配对象的元数据。 与可分配对象不同,指针不会在超出范围时取消分配其数据。 一个遗留功能“Cray 指针”使用存储在另一个变量中的地址实现一个变量的动态基址寻址。

预处理

没有标准的预处理功能,但是每个真正的 Fortran 实现都支持通过标准 C 源代码预处理器的变体传递 Fortran 源代码。 由于 Fortran 在词法级别上与 C 非常不同(例如,行继续、Hollerith 文字、没有保留字、固定格式),因此在 Fortran 源代码上使用标准的现代 C 预处理器可能会很困难。 预处理行为因实现而异,不应依赖于太多的可移植性。 预处理通常通过使用大写的文件名后缀(例如,“foo.F90”)或编译器命令行选项来请求。 (由于 F18 编译器始终运行其内置的预处理阶段,因此不需要特殊的选项或文件名后缀。)

“面向对象”编程

Fortran 没有像 C++ 那样的成员函数(或子程序),在 C++ 中,函数可以直接访问派生类型的特定实例的成员。 但是 Fortran 确实有一个类似于 C++ 的 this 的东西,即 类型绑定过程。 这是一种将特定子程序名称绑定到派生类型的方式,可能使用别名,以便可以像它是该类型的组件一样调用该子程序(例如,X%F(Y)),并接收 % 左侧的对象作为额外的实际参数,就好像该调用已写成 F(X,Y) 一样。 默认情况下,该对象作为第一个参数传递,但是可以更改该对象; 实际上,通过选择不同的虚参数作为传递对象,可以将相同的特定子程序用于多个类型绑定过程。 通过声明不将任何参数与通过 NOPASS 的对象关联,也可以使用等效于 static 成员函数的功能。 关于类型绑定过程还有很多要说的(例如,它们如何支持重载),但这应该足以让您开始使用最常见的用法。

陷阱

变量初始化器,例如 INTEGER :: J=123,是 静态 初始化器! 它们意味着变量存储在静态存储中,而不是堆栈上,并且初始化的值仅持续到变量被赋值为止。 必须使用赋值语句来实现动态初始化器,该初始化器将应用于变量的每个新实例。 在使用最新的 BLOCK 构造中的初始化器时要特别小心,该构造会延续对静态数据的解释。 (但是,派生类型组件初始化器的作用与预期的一样。) 如果您看到对从未声明为数组的数组的赋值,则它可能是 语句函数 的定义,该语句函数类似于参数化的宏定义,例如 A(X)=SQRT(X)**3。 在最初的 Fortran 语言中,这是用户函数定义的唯一手段。 当然,今天应该使用外部或内部函数来代替。 Fortran 表达式的绑定方式与 C 的绑定方式不完全相同。 注意使用 ** 的求幂,C 当然缺少求幂; 它的绑定比求反更紧密(例如,-2**2 是 -4),并且它的绑定在右侧,这与任何其他 Fortran 和大多数 C 运算符所做的不同; 例如,2**2**3 是 256,而不是 64。 逻辑值必须与特殊的逻辑等价关系(.EQV..NEQV.)而不是通常的相等运算符进行比较。 允许 Fortran 编译器短路表达式求值,但不是必须这样做。 如果需要保护对 OPTIONAL 参数或可能分离的指针的使用,请使用 IF 语句,而不是逻辑 .AND. 运算。 实际上,如果不需要函数的值来确定表达式结果的值,则 Fortran 可以从表达式中删除函数调用; 例如,如果在函数 F 中有一个 PRINT 语句,则它可能会或可能不会被赋值语句 X=0*F() 执行。 (好吧,实际上可能会,但在实践中,编译器始终保留更好地优化的权利。) 除非它们具有显式后缀(1.0_82.0_8)或 D 指数(3.0D0),否则 Fortran 中的实数文字常量具有默认的 REAL 类型——不是 像 C 和 C++ 中的 double 那样。 如果不小心,您可能会在编译时从常量值中丢失精度,而永远不会知道。 « Design Guideline :: Contents :: Flang Fortran Standards Support » © Copyright 2017-2025, The Flang Team. Last updated on May 17, 2025. Created using Sphinx 7.2.6.