Fortran Arrays

Arrays are not a library in Fortran — they are the language. A whole array can appear in expressions, slice with :, grow with allocatable, and reduce with a single intrinsic. This lesson covers the mechanics that make every later lesson — from vectorized physics to MPI domain decomposition — work: declaration, memory order, slicing, and the where/mask family.

Declaration and construction

Declare with a dimension: real :: v(100) is a fixed 100-element array. Array constructors build values inline with square brackets: v = [1.0, 2.0, 3.0]. A constructor can use an implied-do loop, the closest Fortran has to a list comprehension: [(i*i, i = 1, 10)] builds squares. The rank (number of dimensions) is fixed at compile time even when sizes are dynamic.

program array_basics
  implicit none
  integer :: squares(5)          ! fixed-size, index 1..5
  integer :: i
  squares = [(i*i, i = 1, size(squares))]   ! implied-do constructor
  print *, squares               ! 1 4 9 16 25
  print *, squares(2)            ! element access
  print *, size(squares)         ! 5 — number of elements
  print *, shape(squares)        ! 5 — shape vector
end program array_basics

Column-major memory layout

Array elements occupy one contiguous block, and the first subscript varies fastest in memory. This is the opposite of C, and everything — cache efficiency, BLAS calls, MPI buffers — depends on knowing it. When you loop, iterate with the first index innermost:

Column-major memory order of a two-dimensional array

Fig. 1 — Consecutive memory addresses run down the first index; loop that way to stay in cache.

program layout
  implicit none
  real :: a(1000, 1000)
  integer :: i, j
  ! FAST: inner loop runs down the first index = contiguous addresses.
  do j = 1, 1000
     do i = 1, 1000
        a(i, j) = 0.0
     end do
  end do
  ! SLOW (10-100x on real data): a(i,j) with i outer jumps a column each step.
end program layout

Slicing and striding

The triple-dot-and-colon syntax v(lo:hi:step) extracts a section — without copying in the common case, so slices double as compile-time-safe views into a parent array. Negative steps reverse. Sections of any rank can appear anywhere a full array can, including as actual arguments.

program slices
  implicit none
  integer :: v(10) = [1,2,3,4,5,6,7,8,9,10]
  print *, v(3:6)          ! elements 3..6: 3 4 5 6
  print *, v(2:8:2)        ! stride 2:      2 4 6 8
  print *, v(10:1:-2)      ! reversed:      10 8 6 4 2
  print *, v(:5)           ! from the start: 1..5
  v(1:5) = 0              ! assign to a whole section at once
  print *, v              ! 0 0 0 0 0 6 7 8 9 10
end program slices

A two-dimensional section selects rows and columns independently: a(2:4, 3) is the sub-vector of rows 2–4 in column 3. This one rule — "a colon is a free dimension selector" — is the foundation of the stencil operations that dominate numerical code.

Whole-array operations and WHERE

Arithmetic on whole arrays is element-wise and shape-checked at compile time. where (mask) extends the idea to conditional assignment — the array version of an if, with elsewhere for the complement:

program masked
  implicit none
  real :: v(5) = [1.0, -2.0, 3.0, -4.0, 5.0]
  where (v < 0.0)
     v = 0.0                ! clamp negatives to zero
  elsewhere
     v = v * 2.0            ! double the positives
  end where
  print *, v                ! 2.0 0.0 6.0 0.0 10.0
end program masked

The sibling forall exists but is effectively legacy — compilers treat it as a do loop, while do concurrent (control lesson) gives the modern, optimization-friendly spelling.

Allocatable arrays: grow at runtime

allocatable arrays defer sizing to runtime: declare real, allocatable :: v(:), then allocate(v(n)). Fortran 2003 added automatic reallocation on assignment — v = [1.0, 2.0] sizes it for you — and move_alloc transfers allocation without copying, the building block of growable containers. Deallocation is automatic when the variable goes out of scope, eliminating the leak class that C programmers accept with a shrug.

program dynamic_arrays
  implicit none
  real, allocatable :: v(:), w(:)
  v = [1.0, 2.0, 3.0]            ! auto-allocation on assignment
  allocate (w(size(v) * 2))      ! explicit sizing for a known shape
  w = v                          ! shape mismatch handled per element
  deallocate (w)                 ! explicit release; also auto on exit
  call move_alloc(v, w)          ! steal the allocation, nothing copied
  print *, size(w), allocated(v)
end program dynamic_arrays

Linear algebra building blocks

Intrinsics cover the small linear algebra: dot_product, matmul, transpose, maxloc/minloc. For dense systems beyond a toy size the ecosystem standard is BLAS/LAPACK, called through wrappers — the BLAS level-1 idioms (axpy, norms, dot) appear in your own code constantly:

program linalg
  implicit none
  real :: m(2, 2) = reshape([1.0, 2.0, 3.0, 4.0], [2, 2]) ! column-major!
  real :: x(2) = [1.0, 1.0]
  print *, matmul(m, x)          ! (4.0, 6.0): m*x
  print *, dot_product(x, x)     ! 2.0
  print *, maxloc(m)             ! row/col of the largest element
end program linalg