MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Fortran中的数组操作完全指南

2024-09-202.9k 阅读

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) 定义了数组的行数和列数。

数组的初始化

  1. 显式初始化 可以在声明数组的同时进行初始化。对于一维数组:
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] 是目标二维数组的维度。

  1. 使用 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 语言等按行存储不同)。这意味着在访问和操作多维数组时,要注意这种存储顺序对性能的影响。

数组切片

  1. 一维数组切片 可以通过指定下标范围来获取数组的一部分,即切片。例如,获取数组 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 个元素的切片,: 用于指定范围。

  1. 多维数组切片 对于多维数组,同样可以对每个维度进行切片操作。例如,对于二维数组 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)

数组操作函数

数组的数学运算

  1. 元素级运算 Fortran 支持对数组进行元素级的数学运算。例如,对两个相同维度的数组进行加法运算:
integer, dimension(3) :: a = [1, 2, 3]
integer, dimension(3) :: b = [4, 5, 6]
integer, dimension(3) :: c
c = a + b

这里,数组 c 的每个元素是数组 ab 对应元素相加的结果,即 c = [5, 7, 9]。同样,也支持减法(-)、乘法(*)、除法(/)等元素级运算。

  1. 数组的聚合运算 聚合运算对数组的所有元素进行操作并返回一个单一的值。例如,计算数组元素的总和:
integer, dimension(5) :: a = [1, 2, 3, 4, 5]
integer :: sum_value
sum_value = sum(a)

sum 函数返回数组 a 所有元素的总和,这里 sum_value 的值为 15。类似的聚合函数还有 product(计算数组元素的乘积)、maxval(返回数组的最大值)、minval(返回数组的最小值)等。

数组形状操作函数

  1. 获取数组维度 可以使用 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 的行数和列数。

  1. 数组重塑 如前面提到的 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 函数的第一个参数是要重塑的数组,第二个参数是目标数组的维度。

数组查找与排序

  1. 查找数组元素 可以使用 index 函数在数组中查找特定元素的位置。例如,在数组 a 中查找值为 3 的元素位置:
integer, dimension(5) :: a = [1, 2, 3, 4, 5]
integer :: position
position = index(a, 3)

这里,position 的值为 3,表示元素 3 在数组 a 中的位置。如果元素不存在,index 函数返回 0。

  1. 数组排序 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))

分配内存后,就可以像使用普通数组一样访问和操作动态数组。

动态数组的重新分配与释放

  1. 重新分配 如果需要改变动态数组的大小,可以使用 reallocate 语句。例如,将数组 a 的大小从 10 增加到 20:
integer, allocatable :: a(:)
allocate(a(10))
! 使用数组 a
deallocate(a)
allocate(a(20))

这里先释放了原来的 10 个元素的数组空间,然后重新分配了 20 个元素的空间。注意,在重新分配前需要先释放原来的数组,否则会导致内存泄漏。

  1. 释放动态数组 使用完动态数组后,应该释放其占用的内存,以避免内存泄漏。例如:
integer, allocatable :: a(:)
allocate(a(10))
! 使用数组 a
deallocate(a)

deallocate 语句释放了数组 a 占用的内存。

数组与子程序

数组作为子程序参数

  1. 传递一维数组 可以将数组作为参数传递给子程序。例如,编写一个计算数组元素总和的子程序:
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 和一个用于存储结果的变量 resultintent(in) 表示数组 arr 是输入参数,intent(out) 表示 result 是输出参数。

  1. 传递多维数组 传递多维数组的方式类似。例如,编写一个计算二维数组元素总和的子程序:
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 语句,并且在首次调用时进行初始化,之后每次调用都会保留其修改后的值。

数组与并行计算

数组操作在并行计算中的应用

  1. 共享内存并行 在共享内存并行模型(如 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 循环将并行执行,每个线程处理数组的一部分元素。

  1. 分布式内存并行 在分布式内存并行模型(如 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

并行数组操作的性能优化

  1. 数据局部性优化 在并行计算中,合理的数据布局可以提高性能。例如,在多维数组操作中,根据内存存储顺序(Fortran 按列存储)来组织并行任务,使每个线程访问的数据在内存中更连续,减少缓存缺失。

  2. 负载均衡 确保每个并行任务的工作量大致相同,避免某个任务成为性能瓶颈。例如,在 MPI 中分配数组元素给各个进程时,要根据进程数量均匀分配。

通过合理应用并行计算技术,可以显著提高数组操作的性能,特别是在处理大规模数组时。

数组与文件操作

数组写入文件

  1. 顺序文件写入 可以将数组数据写入顺序文件。例如,将一维整数数组写入文件:
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 语句关闭文件。

  1. 直接文件写入 对于直接文件,写入数组时可以指定记录号。例如,将二维数组写入直接文件:
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 选项指定记录号。

从文件读取数组

  1. 顺序文件读取 从顺序文件读取数组数据。例如,读取前面写入的一维数组:
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 语句从文件中逐行读取数据并赋值给数组元素。

  1. 直接文件读取 从直接文件读取二维数组:
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 语句根据记录号读取直接文件中的数据到数组。

通过文件操作,可以方便地保存和读取数组数据,便于数据的持久化和在不同程序间共享。

数组操作的常见问题与解决方法

数组越界问题

  1. 原因分析 数组越界是指访问数组元素时使用的下标超出了数组声明的范围。例如,对于声明为 integer, dimension(5) :: a 的数组,访问 a(6) 就会导致数组越界。这通常是由于错误的循环边界计算、错误的下标值传递等原因造成的。

  2. 解决方法 在访问数组元素前,仔细检查下标值的范围。使用 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

内存分配与释放问题

  1. 动态数组未分配或未释放 对于动态数组,如果在使用前未进行分配,会导致运行时错误。例如:
integer, allocatable :: a(:)
! 未分配就尝试访问
print *, a(1)

同样,如果动态数组使用后未释放,会导致内存泄漏。例如:

integer, allocatable :: a(:)
allocate(a(10))
! 使用数组 a
! 未释放
  1. 解决方法 在使用动态数组前,确保使用 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

数组操作的性能问题

  1. 性能瓶颈分析 在处理大规模数组时,性能问题可能会出现。例如,频繁的数组元素访问、不合理的内存布局(在并行计算中)、过多的数组重塑操作等都可能导致性能下降。

  2. 优化方法 减少不必要的数组元素访问,尽量批量处理数组。例如,使用数组的聚合运算而不是逐个元素计算。在并行计算中,优化数据布局以提高缓存命中率。对于频繁重塑数组的情况,可以提前规划好数组的形状,减少重塑操作的次数。

通过注意这些常见问题并采取相应的解决方法,可以编写更健壮、高效的 Fortran 数组操作代码。