2.3 动态内存管理与资源生命周期


2.3 动态内存管理与资源生命周期

本节摘要:类型系统解决了"数据是什么",本节解决"数据归谁管、活多久"。allocatable 用所有权语义换来自动释放的安全网,pointer 用灵活关联换来悬空指针与泄漏的风险。加上 move_alloc 的零拷贝交接,本节给出一条明确的设计路线:默认 allocatable,必要时指针,交接用 move_alloc。读完你能为任何一块动态内存说清它的出生、归属与销毁。

内存不是无限的吗,为什么还要管

数值模拟的问题规模是运行时才知道的:网格细化后节点数翻倍,粒子注入后数量暴涨。声明一个固定上限的数组,要么浪费内存,要么运行时溢出崩溃。动态内存是绕不开的,而 Fortran 给了两条路:allocatable 与 pointer。两条路长得像,哲学截然不同——一条是"我拥有这块内存",另一条是"我只是看着它"。选错路,轻则泄漏,重则悬空。

allocatable:所有权明确的安全路线

allocatable 变量拥有自己的内存:分配后由它独占,离开作用域时自动释放,不需要你记得 deallocate。这就是 RAII 思路——资源的生命周期绑定到变量的生命周期。

subroutine work_on_grid(n) use iso_fortran_env, only: real64 implicit none integer, intent(in) :: n real(real64), allocatable :: field(:) allocate(field(n)) ! 按需分配 field = 1.0_real64 write(*,*) 'size =', size(field) ! 子程序结束,field 自动释放,无需手动 deallocate end subroutine work_on_grid

allocated() 可以查询分配状态,这是防御性编程的基础:访问之前先确认内存真的在。

if (.not. allocated(field)) allocate(field(n)) field = field + 1.0_real64

注意 allocatable 数组的赋值是深拷贝b = a 会把 a 的内容整体复制进 b 自己的内存。语义安全,但大数组赋值有拷贝开销,这引出后面 move_alloc 的存在理由。

pointer:灵活但危险

pointer 的核心语义是"关联"而非"拥有"。它指向某个 target,不持有内存;超出作用域时,被指的内存不会自动释放。两份危险就此产生:内存泄漏(指向动态内存的指针丢了,那块内存成了孤岛)和悬空指针(目标已释放,指针还留着旧地址)。

subroutine risky(n) use iso_fortran_env, only: real64 implicit none integer, intent(in) :: n real(real64), pointer :: p(:) real(real64), allocatable, target :: storage(:) allocate(storage(n)) p => storage ! 指针关联到 target p = 0.0_real64 deallocate(storage) ! storage 没了 ! p 现在是悬空指针:再访问就是未定义行为 end subroutine risky

指针的两大用途是值得保留的:一是构建链表、树这类结构(节点之间靠关联而不是连续存储),二是多态引用(第 5 章里 class(*) 的载体必须是指针或 allocatable 多态)。用指针就要立规矩:声明时初始化为 null(),解除关联后立即重置,每次解引用前用 associated() 检查。

move_alloc:零拷贝交接

把一个大数组从函数里"运"出来,传统做法是复制——数组越大,代价越高。move_alloc 干的是另一件事:把源数组的内存描述符直接转给目标,源变成未分配。代价 O(1),不搬数据。这在迭代求解器交换新旧缓冲区时尤其划算。

subroutine swap_buffers(a, b) use iso_fortran_env, only: real64 implicit none real(real64), allocatable, intent(inout) :: a(:), b(:) real(real64), allocatable :: tmp(:) call move_alloc(a, tmp) ! a 的内存 → tmp,a 变未分配 call move_alloc(b, a) ! b 的内存 → a call move_alloc(tmp, b) ! tmp 的内存 → b end subroutine swap_buffers

三行代码完成两块缓冲区的角色互换,全程零数据拷贝。代价是契约:move_alloc 之后源对象不可再用。把"内存归谁"写清楚,是这类代码能长期活着的前提。

图:动态内存的生命周期状态流转

图:动态内存的生命周期状态流转

内存策略的三条经验法则

第一,类型组件优先 allocatable——自动释放、布局连续,对 SIMD 友好;只有实现多态或链表树结构才用指针。第二,热循环内部禁止分配与释放,系统调用与堆管理开销会被放大成性能灾难;做法是在循环外一次性分配工作数组,循环内复用。第三,长期运行的模拟考虑内存池思路:预分配一大块,逻辑上切成子区域按需分发,减少堆碎片化。这三点配合 move_alloc 交接,就能把"内存归谁管"这件最容易烂尾的事做成明确的契约。

学习目标

阅读完本节,你应当能够:

  1. 说出 allocatable 与 pointer 在所有权、自动释放与赋值行为上的三点差异
  2. 用 allocated 与 associated 检查内存状态,写出防御性的访问代码
  3. 用 move_alloc 完成一次零拷贝的数组交接,并解释其 O(1) 代价
  4. 说明为什么热循环内部不宜频繁分配与释放
  5. 为一个复杂数据结构选定内存策略:allocatable、pointer 或预分配复用

本节要点回顾

  • allocatable 拥有内存:自动释放、深拷贝语义、布局连续,是第一选择
  • pointer 只做关联:灵活但不拥有,泄漏与悬空是两大事故源
  • 防御三件套:指针初始化为 null、解引用前 associated、解除后立即重置
  • move_alloc 是零拷贝交接:O(1) 换缓冲区,代价是源对象失效的契约
  • 热循环禁止动态分配:循环外分配、循环内复用是性能底线
  • 类型组件默认 allocatable:指针只留给多态与链式结构

内存管好了,下一步是 Fortran 的看家本领——数组编程。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U