Fortran中的数组操作完全指南
Fortran 中的数组基础
数组的定义与声明
在 Fortran 语言中,数组是一种用于存储多个相同类型数据的结构。声明数组时,需要指定数组的类型、名称以及维度。
例如,声明一个一维整数数组 a
,它包含 10 个元素:
integer, dimension(10) :: a
这里,integer
表示数组元素的类型为整数,dimension(10)
表示数组的维度为 10,即该数组有 10 个元素。数组的下标默认从 1 开始,这与一些其他编程语言(如 C 语言从 0 开始下标)不同。
声明二维数组 b
,它是一个 3 行 4 列的实数数组:
real, dimension(3, 4) :: b
同样,real
是元素类型,dimension(3, 4)
定义了数组的行数和列数。
数组的初始化
- 显式初始化 可以在声明数组的同时进行初始化。对于一维数组:
integer, dimension(5) :: a = [1, 2, 3, 4, 5]
对于二维数组:
integer, dimension(2, 3) :: b = reshape([1, 2, 3, 4, 5, 6], [2, 3])
这里使用了 reshape
函数,它将一个一维数组按照指定的维度重新排列成多维数组。[1, 2, 3, 4, 5, 6]
是要重新排列的一维数组,[2, 3]
是目标二维数组的维度。
- 使用
data
语句初始化 在 Fortran 较老的标准中,也可以使用data
语句进行初始化:
integer, dimension(5) :: a
data a /1, 2, 3, 4, 5/
这种方式在现代 Fortran 中仍然可用,但使用 =
进行初始化更为常见。
数组的访问与索引
一维数组的访问
访问一维数组的元素通过下标进行。例如,要访问前面声明的数组 a
的第 3 个元素:
integer, dimension(5) :: a = [1, 2, 3, 4, 5]
integer :: value
value = a(3)
这里,a(3)
表示访问数组 a
的第 3 个元素,将其值赋给变量 value
。
多维数组的访问
对于多维数组,需要指定每个维度的下标。以二维数组 b
为例,访问第 2 行第 3 列的元素:
integer, dimension(2, 3) :: b = reshape([1, 2, 3, 4, 5, 6], [2, 3])
integer :: element
element = b(2, 3)
在 Fortran 中,多维数组在内存中按列存储(这与 C 语言等按行存储不同)。这意味着在访问和操作多维数组时,要注意这种存储顺序对性能的影响。
数组切片
- 一维数组切片
可以通过指定下标范围来获取数组的一部分,即切片。例如,获取数组
a
的第 2 到第 4 个元素:
integer, dimension(5) :: a = [1, 2, 3, 4, 5]
integer, dimension(3) :: slice
slice = a(2:4)
这里,a(2:4)
表示从第 2 个元素到第 4 个元素的切片,:
用于指定范围。
- 多维数组切片
对于多维数组,同样可以对每个维度进行切片操作。例如,对于二维数组
b
,获取第 1 行的所有元素:
integer, dimension(2, 3) :: b = reshape([1, 2, 3, 4, 5, 6], [2, 3])
integer, dimension(3) :: row_slice
row_slice = b(1, :)
这里,b(1, :)
表示获取第 1 行的所有元素,:
表示该维度的所有元素。获取第 2 列的所有元素:
integer, dimension(2) :: col_slice
col_slice = b(:, 2)
还可以对多个维度同时进行切片,比如获取 b
的左上角 2x2 的子数组:
integer, dimension(2, 2) :: sub_array
sub_array = b(1:2, 1:2)
数组操作函数
数组的数学运算
- 元素级运算 Fortran 支持对数组进行元素级的数学运算。例如,对两个相同维度的数组进行加法运算:
integer, dimension(3) :: a = [1, 2, 3]
integer, dimension(3) :: b = [4, 5, 6]
integer, dimension(3) :: c
c = a + b
这里,数组 c
的每个元素是数组 a
和 b
对应元素相加的结果,即 c = [5, 7, 9]
。同样,也支持减法(-
)、乘法(*
)、除法(/
)等元素级运算。
- 数组的聚合运算 聚合运算对数组的所有元素进行操作并返回一个单一的值。例如,计算数组元素的总和:
integer, dimension(5) :: a = [1, 2, 3, 4, 5]
integer :: sum_value
sum_value = sum(a)
sum
函数返回数组 a
所有元素的总和,这里 sum_value
的值为 15。类似的聚合函数还有 product
(计算数组元素的乘积)、maxval
(返回数组的最大值)、minval
(返回数组的最小值)等。
数组形状操作函数
- 获取数组维度
可以使用
size
函数获取数组的元素总数,使用shape
函数获取数组每个维度的大小。例如:
integer, dimension(2, 3) :: b
integer :: total_size
integer, dimension(2) :: dim_sizes
total_size = size(b)
dim_sizes = shape(b)
这里,total_size
的值为 6(2x3),dim_sizes
是一个一维数组,包含 [2, 3]
,分别是数组 b
的行数和列数。
- 数组重塑
如前面提到的
reshape
函数,可以将一个数组按照指定的维度重新排列。例如,将一个 6 元素的一维数组重塑为 2x3 的二维数组:
integer, dimension(6) :: one_d = [1, 2, 3, 4, 5, 6]
integer, dimension(2, 3) :: two_d
two_d = reshape(one_d, [2, 3])
reshape
函数的第一个参数是要重塑的数组,第二个参数是目标数组的维度。
数组查找与排序
- 查找数组元素
可以使用
index
函数在数组中查找特定元素的位置。例如,在数组a
中查找值为 3 的元素位置:
integer, dimension(5) :: a = [1, 2, 3, 4, 5]
integer :: position
position = index(a, 3)
这里,position
的值为 3,表示元素 3 在数组 a
中的位置。如果元素不存在,index
函数返回 0。
- 数组排序
Fortran 提供了
sort
函数对数组进行排序。例如,对数组a
进行升序排序:
integer, dimension(5) :: a = [5, 4, 3, 2, 1]
sort(a)
执行上述代码后,数组 a
变为 [1, 2, 3, 4, 5]
。
动态数组
动态数组的声明与分配
在 Fortran 中,可以声明动态数组,其大小在运行时确定。声明动态数组时,使用 allocatable
关键字。例如,声明一个动态一维整数数组:
integer, allocatable :: a(:)
这里,a(:)
表示 a
是一个一维动态数组。要使用动态数组,需要先分配内存。例如,分配 10 个元素的空间:
allocate(a(10))
分配内存后,就可以像使用普通数组一样访问和操作动态数组。
动态数组的重新分配与释放
- 重新分配
如果需要改变动态数组的大小,可以使用
reallocate
语句。例如,将数组a
的大小从 10 增加到 20:
integer, allocatable :: a(:)
allocate(a(10))
! 使用数组 a
deallocate(a)
allocate(a(20))
这里先释放了原来的 10 个元素的数组空间,然后重新分配了 20 个元素的空间。注意,在重新分配前需要先释放原来的数组,否则会导致内存泄漏。
- 释放动态数组 使用完动态数组后,应该释放其占用的内存,以避免内存泄漏。例如:
integer, allocatable :: a(:)
allocate(a(10))
! 使用数组 a
deallocate(a)
deallocate
语句释放了数组 a
占用的内存。
数组与子程序
数组作为子程序参数
- 传递一维数组 可以将数组作为参数传递给子程序。例如,编写一个计算数组元素总和的子程序:
program main
integer, dimension(5) :: a = [1, 2, 3, 4, 5]
integer :: sum_value
call sum_array(a, sum_value)
print *, 'Sum of array:', sum_value
end program main
subroutine sum_array(arr, result)
integer, dimension(:), intent(in) :: arr
integer, intent(out) :: result
result = sum(arr)
end subroutine sum_array
在这个例子中,sum_array
子程序接受一个一维整数数组 arr
和一个用于存储结果的变量 result
。intent(in)
表示数组 arr
是输入参数,intent(out)
表示 result
是输出参数。
- 传递多维数组 传递多维数组的方式类似。例如,编写一个计算二维数组元素总和的子程序:
program main
integer, dimension(2, 3) :: b = reshape([1, 2, 3, 4, 5, 6], [2, 3])
integer :: sum_value
call sum_2d_array(b, sum_value)
print *, 'Sum of 2D array:', sum_value
end program main
subroutine sum_2d_array(arr, result)
integer, dimension(:, :), intent(in) :: arr
integer, intent(out) :: result
result = sum(arr)
end subroutine sum_2d_array
这里,sum_2d_array
子程序接受一个二维整数数组 arr
并计算其元素总和。
子程序中数组的局部性
在子程序中声明的数组是局部的,除非使用 save
语句。例如:
subroutine local_array_example
integer, dimension(5) :: local_arr = [1, 2, 3, 4, 5]
integer :: i
do i = 1, 5
local_arr(i) = local_arr(i) * 2
end do
print *, 'Local array:', local_arr
end subroutine local_array_example
每次调用 local_array_example
子程序时,local_arr
都会重新初始化。如果希望在多次调用之间保留数组的值,可以使用 save
语句:
subroutine saved_array_example
integer, dimension(5), save :: saved_arr
if (.not. allocated(saved_arr)) then
allocate(saved_arr)
saved_arr = [1, 2, 3, 4, 5]
end if
integer :: i
do i = 1, 5
saved_arr(i) = saved_arr(i) * 2
end do
print *, 'Saved array:', saved_arr
end subroutine saved_array_example
在这个例子中,saved_arr
使用了 save
语句,并且在首次调用时进行初始化,之后每次调用都会保留其修改后的值。
数组与并行计算
数组操作在并行计算中的应用
- 共享内存并行 在共享内存并行模型(如 OpenMP)中,数组操作可以很方便地并行化。例如,对数组元素进行加法运算:
program parallel_array_add
use omp_lib
implicit none
integer, dimension(1000) :: a, b, c
integer :: i
a = [(i, i = 1, 1000)]
b = [(i, i = 1, 1000)]
!$omp parallel do
do i = 1, 1000
c(i) = a(i) + b(i)
end do
!$omp end parallel do
! 输出结果(这里仅作示例,实际应用中可能不需要输出整个数组)
do i = 1, 10
print *, c(i)
end do
end program parallel_array_add
在这个例子中,使用 OpenMP 的 parallel do
指令并行化了数组加法操作。!$omp parallel do
表示下面的 do
循环将并行执行,每个线程处理数组的一部分元素。
- 分布式内存并行 在分布式内存并行模型(如 MPI)中,数组操作需要考虑数据的分布。例如,在 MPI 中进行数组求和:
program mpi_array_sum
use mpi
implicit none
integer :: ierr, rank, size
integer, dimension(100) :: local_array
integer :: local_sum, global_sum
integer :: i
call MPI_Init(ierr)
call MPI_Comm_rank(MPI_COMM_WORLD, rank, ierr)
call MPI_Comm_size(MPI_COMM_WORLD, size, ierr)
local_array = [(rank * 100 + i, i = 1, 100)]
local_sum = sum(local_array)
call MPI_Reduce(local_sum, global_sum, 1, MPI_INTEGER, MPI_SUM, 0, MPI_COMM_WORLD, ierr)
if (rank == 0) then
print *, 'Global sum:', global_sum
end if
call MPI_Finalize(ierr)
end program mpi_array_sum
在这个例子中,每个 MPI 进程都有自己的局部数组 local_array
,计算局部数组的和 local_sum
,然后使用 MPI_Reduce
函数将所有进程的局部和汇总到根进程(rank == 0
),得到全局和 global_sum
。
并行数组操作的性能优化
-
数据局部性优化 在并行计算中,合理的数据布局可以提高性能。例如,在多维数组操作中,根据内存存储顺序(Fortran 按列存储)来组织并行任务,使每个线程访问的数据在内存中更连续,减少缓存缺失。
-
负载均衡 确保每个并行任务的工作量大致相同,避免某个任务成为性能瓶颈。例如,在 MPI 中分配数组元素给各个进程时,要根据进程数量均匀分配。
通过合理应用并行计算技术,可以显著提高数组操作的性能,特别是在处理大规模数组时。
数组与文件操作
数组写入文件
- 顺序文件写入 可以将数组数据写入顺序文件。例如,将一维整数数组写入文件:
program write_array_to_file
integer, dimension(5) :: a = [1, 2, 3, 4, 5]
integer :: unit_num = 10
open(unit = unit_num, file = 'array_data.txt', status = 'replace')
do i = 1, 5
write(unit_num, *) a(i)
end do
close(unit_num)
end program write_array_to_file
这里,使用 open
语句打开一个文件,write
语句将数组元素逐行写入文件,最后使用 close
语句关闭文件。
- 直接文件写入 对于直接文件,写入数组时可以指定记录号。例如,将二维数组写入直接文件:
program write_2d_array_direct
integer, dimension(2, 3) :: b = reshape([1, 2, 3, 4, 5, 6], [2, 3])
integer :: unit_num = 20
open(unit = unit_num, file = '2d_array_direct.dat', access = 'direct', recl = 4 * 3)
do i = 1, 2
write(unit_num, rec = i) b(i, :)
end do
close(unit_num)
end program write_2d_array_direct
这里,access = 'direct'
表示打开直接文件,recl
指定每条记录的长度(这里假设每个整数占 4 字节,二维数组每行 3 个元素),write
语句的 rec
选项指定记录号。
从文件读取数组
- 顺序文件读取 从顺序文件读取数组数据。例如,读取前面写入的一维数组:
program read_array_from_file
integer, dimension(5) :: a
integer :: unit_num = 10
open(unit = unit_num, file = 'array_data.txt')
do i = 1, 5
read(unit_num, *) a(i)
end do
close(unit_num)
print *, 'Read array:', a
end program read_array_from_file
read
语句从文件中逐行读取数据并赋值给数组元素。
- 直接文件读取 从直接文件读取二维数组:
program read_2d_array_direct
integer, dimension(2, 3) :: b
integer :: unit_num = 20
open(unit = unit_num, file = '2d_array_direct.dat', access = 'direct', recl = 4 * 3)
do i = 1, 2
read(unit_num, rec = i) b(i, :)
end do
close(unit_num)
print *, 'Read 2D array:'
do i = 1, 2
print *, b(i, :)
end do
end program read_2d_array_direct
同样,通过 read
语句根据记录号读取直接文件中的数据到数组。
通过文件操作,可以方便地保存和读取数组数据,便于数据的持久化和在不同程序间共享。
数组操作的常见问题与解决方法
数组越界问题
-
原因分析 数组越界是指访问数组元素时使用的下标超出了数组声明的范围。例如,对于声明为
integer, dimension(5) :: a
的数组,访问a(6)
就会导致数组越界。这通常是由于错误的循环边界计算、错误的下标值传递等原因造成的。 -
解决方法 在访问数组元素前,仔细检查下标值的范围。使用
if
语句进行边界检查,例如:
integer, dimension(5) :: a = [1, 2, 3, 4, 5]
integer :: index = 6
if (index >= 1.and. index <= size(a)) then
print *, a(index)
else
print *, 'Index out of bounds'
end if
在编写循环访问数组时,确保循环变量的范围与数组大小匹配,例如:
integer, dimension(5) :: a = [1, 2, 3, 4, 5]
integer :: i
do i = 1, size(a)
print *, a(i)
end do
内存分配与释放问题
- 动态数组未分配或未释放 对于动态数组,如果在使用前未进行分配,会导致运行时错误。例如:
integer, allocatable :: a(:)
! 未分配就尝试访问
print *, a(1)
同样,如果动态数组使用后未释放,会导致内存泄漏。例如:
integer, allocatable :: a(:)
allocate(a(10))
! 使用数组 a
! 未释放
- 解决方法
在使用动态数组前,确保使用
allocate
语句进行分配。例如:
integer, allocatable :: a(:)
allocate(a(10))
! 使用数组 a
deallocate(a)
可以编写辅助函数或子程序来管理动态数组的分配和释放,以提高代码的可维护性。例如:
subroutine allocate_array(arr, size)
integer, allocatable, intent(out) :: arr(:)
integer, intent(in) :: size
allocate(arr(size))
end subroutine allocate_array
subroutine deallocate_array(arr)
integer, allocatable, intent(inout) :: arr(:)
if (allocated(arr)) then
deallocate(arr)
end if
end subroutine deallocate_array
在主程序中:
program main
integer, allocatable :: a(:)
call allocate_array(a, 10)
! 使用数组 a
call deallocate_array(a)
end program main
数组操作的性能问题
-
性能瓶颈分析 在处理大规模数组时,性能问题可能会出现。例如,频繁的数组元素访问、不合理的内存布局(在并行计算中)、过多的数组重塑操作等都可能导致性能下降。
-
优化方法 减少不必要的数组元素访问,尽量批量处理数组。例如,使用数组的聚合运算而不是逐个元素计算。在并行计算中,优化数据布局以提高缓存命中率。对于频繁重塑数组的情况,可以提前规划好数组的形状,减少重塑操作的次数。
通过注意这些常见问题并采取相应的解决方法,可以编写更健壮、高效的 Fortran 数组操作代码。