> *"I call it my billion-dollar mistake: the invention of the null reference in 1965. … It has led to
Prerequisites
- 5
- 6
- 8
- 9
Learning Objectives
- Distinguish pointer assignment (=>) from value assignment (=), and predict which object each one modifies.
- Declare pointer and target entities and query a pointer's association status with associated, disassociating it with nullify or => null().
- Identify and avoid the two pointer hazards — undefined status and dangling pointers — and free the nodes of a linked structure without leaking memory.
- Build a linked list with pointers, and explain why a contiguous array usually outperforms a linked structure in Fortran.
- Choose between allocatable and pointer with a clear rule, and justify the default preference for allocatable by automatic deallocation and the no-aliasing optimization.
- Apply the contiguous attribute (and the is_contiguous intrinsic) to an array pointer or dummy argument, and state exactly what promise it makes to the compiler.
In This Chapter
- Overview
- Learning Paths
- 11.1 pointer, target, and the Two Kinds of Assignment
- 11.2 Association Status: associated, nullify, and Two Ways to Go Wrong
- 11.3 Dynamic Structures: A Linked List, a Tree, and Why Arrays Usually Win
- 11.4 allocatable vs pointer: The Rule
- 11.5 When You Do Need Pointers — and the contiguous Attribute
- Project Checkpoint
- Summary
- Spaced Review
- What's Next
Chapter 11: Pointers, Targets, and Dynamic Data Structures
"I call it my billion-dollar mistake: the invention of the null reference in 1965. … It has led to innumerable errors, vulnerabilities, and system crashes, probably a billion dollars of pain and damage in the last forty years." — C. A. R. (Tony) Hoare, reflecting on null references, QCon London 2009
Overview
Every chapter so far has handed you a tool and told you to reach for it. This one hands you a tool and
spends most of its length telling you not to. That is not a contradiction — it is the single most
important thing to understand about pointers in Fortran. Fortran has pointers, they are powerful, and in
day-to-day numerical code you should reach for them rarely, because the language gives you something
better for almost everything you were tempted to use them for: the allocatable arrays and components you
already met in Chapter 5 and
Chapter 9.
A pointer is a variable that does not hold a value of its own; instead it is an alias — a second name
for some other object, or for a block of memory you allocated at run time. If you come from C, you already
have a mental picture, and it is mostly wrong for Fortran: a Fortran pointer is dereferenced
automatically (you never write *p), it carries the shape of an array it points at, and it can only point
at things that gave it explicit permission. If you come from Python, the picture is different again: in
Python essentially every variable is a reference, so b = a for a list makes two names for one object.
Fortran is the opposite — assignment copies values by default, and aliasing happens only when you ask for
it, with a distinct operator. That distinction, => versus =, is where this chapter begins, and getting
it exactly right is what separates code that works from code that silently corrupts.
We will build the real thing — a linked list, node by node, with pointers — because you must know how, and
because you will inherit code that does it. But we will then make the case, honestly and with the
performance reasoning this book is built on, that in Fortran a plain array usually wins: it is faster,
safer, and simpler. By the end you will have a rule you can apply without thinking — prefer
allocatable; reach for pointer only when you genuinely need aliasing, polymorphic containers,
callbacks, or C interoperability — and you will understand the one performance attribute, contiguous,
that pointers bring to the table when you do need them.
In this chapter, you will learn to:
- Declare
pointerandtargetentities, and tell pointer assignment (p => a, aliasing) apart from value assignment (p = a, copying through the alias). - Track a pointer's association status, test it with
associated, and clear it withnullifyor=> null()— and recognize the two ways a pointer goes bad: undefined and dangling. - Assemble and tear down a dynamic structure (a linked list) without leaking a single node.
- Decide between
allocatableandpointerfrom a short, principled rule — and explain why the compiler makes fast code from the former. - Use the
contiguousattribute to promise the compiler a strided pointer is really a solid block of memory, and verify that promise withis_contiguous.
Learning Paths
How to read this chapter by track. - 🔬 Scientist ("my code just needs to run fast and be right") — read §11.1–11.2 for the mechanics, then §11.4 closely; it is the rule that will keep your numerical code both fast and leak-free. You can skim the linked-list mechanics in §11.3 and take its conclusion on faith. - 📖 Standard — read straight through; pointers are a corner of the language you should understand fully even though you will use them sparingly. - 🔧 Legacy ("I inherited old code") — §11.2 and §11.3 are your survival kit: pointer-based structures and their bugs (leaks, dangling aliases) are exactly what you will find and have to fix. - ⚡ HPC ("I need parallel code") — §11.4 (no aliasing → optimization) and §11.5 (
contiguous) are your sections; they connect directly to Chapter 27 and Chapter 29.
11.1 pointer, target, and the Two Kinds of Assignment
Start with the mental model, because everything else is a consequence of it. A normal variable is a box that holds a value. A pointer is not a box; it is a label on a string that you can tie to some other box — or to nothing at all. Tie it to a box, and from then on the pointer's name means "the box I am tied to." Untie it and retie it elsewhere, and the same name now means a different box. The pointer never holds the value itself; it only ever refers.
Definition (pointer). A pointer is a variable declared with the
pointerattribute that holds no value of its own but can be associated with — made an alias for — another object, or with a block of memory obtained byallocate. Wherever you use the pointer's name in an expression, Fortran automatically uses the object it is associated with; there is no explicit dereference operator like C's*p. A pointer to an array carries that array's shape and bounds, so you can slice and index it exactly as if it were the array.
There is a matching permission on the other side. A pointer cannot alias just any variable — only one that
has volunteered to be aliased, by carrying the target attribute.
Definition (target). The
targetattribute marks an object as something a pointer is allowed to point at. Aiming a pointer at a variable that lacks bothtargetandpointeris not allowed. The rule exists for the compiler's benefit: an ordinary variable, not markedtarget, is guaranteed to have no pointer aliasing it, so the compiler may keep it in a register and optimize around it freely.targetis you telling the compiler, "give up that assumption for this object — a pointer may be watching it."
Here is the smallest program that shows a pointer being tied to a target and used:
program pointer_intro
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
real(dp), target :: a = 1.0_dp ! a volunteers to be pointed at
real(dp), pointer :: p => null() ! p starts tied to nothing
p => a ! POINTER assignment: p is now an alias for a
print '(a, f0.1)', 'a via p = ', p ! reading p reads a -> 1.0
end program pointer_intro
$ gfortran -std=f2018 -Wall pointer_intro.f90 -o pintro && ./pintro
a via p = 1.0
Notice three things. The target a was declared target; the pointer p was declared pointer and
initialized => null() (more on why in §11.2); and the association was made with =>, not =. That
operator choice is the crux of the whole chapter.
=> aliases; = copies
Fortran gives you two assignment operators near a pointer, and they do completely different things.
Definition (pointer assignment). Pointer assignment, written with the arrow
p => a, associates the pointerpwith the targeta— it makespan alias fora, changing whatprefers to but touching no numerical value. Ordinary value assignment, writtenp = a, does not change whatppoints to; ifpis already associated, it copies the value ofathroughp, into the targetpcurrently refers to. One arrow changes the alias; one equals sign changes the data.
Read that twice, then watch it happen. This program ties p to a, writes through the alias, re-ties
p to b, and writes again — and we track exactly which real value changes at each step:
program alias_vs_copy
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
real(dp), target :: a = 1.0_dp, b = 2.0_dp
real(dp), pointer :: p => null()
p => a ! p aliases a (=> changes the alias)
p = 5.0_dp ! writes 5 THROUGH p into a (= changes the value)
print '(a, f0.1, a, f0.1)', 'after p=>a, p=5: a = ', a, ', b = ', b
p => b ! now p aliases b instead; a is left completely alone
p = 9.0_dp ! writes 9 into b
print '(a, f0.1, a, f0.1)', 'after p=>b, p=9: a = ', a, ', b = ', b
end program alias_vs_copy
$ gfortran -std=f2018 -Wall alias_vs_copy.f90 -o alias && ./alias
after p=>a, p=5: a = 5.0, b = 2.0
after p=>b, p=9: a = 5.0, b = 9.0
Trace it against the two definitions. p => a aliases a; p = 5.0_dp writes 5 through the alias, so
a becomes 5 while b is untouched at 2. Then p => b re-aims the alias at b — crucially, this does
not copy a into b; it only changes what p means — so a stays 5. Finally p = 9.0_dp writes 9
into b. End state: a = 5.0, b = 9.0. If you had written p = b instead of p => b at the third
step, you would have copied b's value into a (through the still-a-aliased p), a completely
different result. The operator is not a stylistic choice; it selects the operation.
💡 Intuition: think of
=>as moving a sticky note and=as writing on the page the sticky note is stuck to. Moving the note (p => b) changes nothing on any page; it just relabels which pagepnames. Writing through the note (p = 9) changes the page under it. Almost every pointer bug in existence is someone who moved the note when they meant to write, or wrote when they meant to move.🐍 Python Comparison: Python has the opposite default. In Python,
b = afor a mutable object (a list, a NumPy array) makesba second reference to the same object — aliasing is what you get unless you deliberately copy. In Fortran,b = acopies, and aliasing is what you get only when you deliberately ask, with=>. This is why Fortran numerical code is easier to reason about — and easier for the compiler to optimize: it does not have to assume every assignment might have created a hidden alias. The Python default is convenient for scripting; the Fortran default is a gift to the optimizer.📜 From History: Fortran had
allocatablearrays and pointers arrive close together, both in the Fortran 90 modernization, but for different jobs:allocatablefor "I need an array whose size I learn at run time," andpointerfor "I need aliasing and linked structures." Over the following standards the committee kept expanding whatallocatablecould do — allocatable components (2003), allocatable dummy arguments and function results, automatic (re)allocation on assignment — precisely so that pointers could retreat to the narrow set of jobs only they can do. The language's own evolution is an argument for the rule this chapter teaches.
11.2 Association Status: associated, nullify, and Two Ways to Go Wrong
A pointer is always in exactly one of three states, and knowing which is the difference between a correct program and one with a landmine in it.
Definition (association status). A pointer's association status is one of: associated — it is currently a valid alias for some target or allocated memory; disassociated — it is explicitly tied to nothing, the state produced by
nullify(p)or by initializingp => null(); or undefined — it has never been given a status at all, the state of a freshly declared pointer with no initialization. The intrinsicassociated(p)returns.true.for the first and.false.for the second — but you must never call it on an undefined pointer, because there is nothing valid for it to inspect.
The associated intrinsic comes in two forms. associated(p) asks "is p tied to anything?"
associated(p, tgt) asks the sharper question "is p tied to this specific target tgt?" Both are
constantly useful — the first to guard against using a null pointer, the second to check identity. And
nullify(p) is how you deliberately untie a pointer, setting it to the clean, testable disassociated
state.
program association_status
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
real(dp), target :: a = 1.0_dp, b = 2.0_dp
real(dp), pointer :: p => null() ! DISASSOCIATED from the start — good habit
print '(a, l1)', 'associated(p) at start = ', associated(p) ! F
p => a
print '(a, l1)', 'associated(p) after p=>a = ', associated(p) ! T
print '(a, l1)', 'associated(p, a) same target= ', associated(p, a) ! T
print '(a, l1)', 'associated(p, b) same target= ', associated(p, b) ! F
nullify(p)
print '(a, l1)', 'associated(p) after nullify = ', associated(p) ! F
end program association_status
$ gfortran -std=f2018 -Wall association_status.f90 -o assoc && ./assoc
associated(p) at start = F
associated(p) after p=>a = T
associated(p, a) same target= T
associated(p, b) same target= F
associated(p) after nullify = F
Every line is checkable against the definition: p begins disassociated (we initialized it that way), so
associated(p) is F; after p => a it is T; it is tied to a and not to b, so the two-argument
form reports T then F; and after nullify(p) it is disassociated again, back to F.
Hazard one: the undefined pointer
Look again at the declaration real(dp), pointer :: p => null(). That => null() is not decoration. A
pointer declared without it —
real(dp), pointer :: p ! no initializer: p has UNDEFINED status
— has undefined association status. It is neither associated nor disassociated; it holds garbage. And
here is the trap: associated(p) on an undefined pointer is itself undefined behavior. It may return
.true., it may return .false., it may crash — and worst of all, it may do the right thing on your
machine today and the wrong thing on the cluster tomorrow. You cannot test your way out of an undefined
pointer, because the test is part of the undefined behavior.
⚠️ Common Pitfall — undefined and dangling pointers. These are the two ways a Fortran pointer betrays you, and both are silent.
Undefined: a pointer you never initialized. The fix is a one-time habit — always give a pointer a status the moment it is born, by initializing
=> null()in its declaration or callingnullifybefore first use. Thenassociated(p)is always a meaningful question.Dangling: a pointer that was valid but whose target no longer exists — you deallocated the memory through another pointer, or the target went out of scope. The pointer still looks associated;
associated(p)may cheerfully return.true.; but it now aliases freed or vanished memory, and touching it is undefined behavior. This one is worse than undefined, because it passes every test you can write. The fix is discipline: after youdeallocatethrough one pointer,nullifyevery other pointer that aliased the same memory — the language will not do it for you.
The dangling case deserves to be seen, because it is the bug that eats afternoons. This fragment is wrong — do not model code on it:
real(dp), pointer :: p => null(), q => null()
allocate(p) ! p now owns a fresh real
q => p ! q aliases the SAME allocated memory
deallocate(p) ! frees the memory; the standard disassociates p...
! ...but q is untouched, and now DANGLES.
print *, associated(q) ! may print T — a lie; q points at freed memory
print *, q ! undefined behavior: reading freed memory
We show it as a fragment, and we do not paste an "expected output," precisely because there is no defined
output — that is the whole point. After deallocate(p), the standard sets p to disassociated, but q is
a separate pointer variable that the deallocation never heard of. q still carries the old address; it
dangles. The honest fix is to treat the deallocation as the death of every alias: deallocate(p) then
nullify(q), and never touch q's data again.
🔗 Connection: This is exactly why the
allocatablearrays of Chapter 5 cannot dangle: an allocatable is the sole owner of its memory, there is no second alias to be left behind, and when it dies the memory dies with it, automatically. Hold that thought — it is half of the argument in §11.4. When allocation errors themselves must be handled (a grid too large for memory), thestat=/errmsg=machinery you will meet in Chapter 13 applies to both pointers and allocatables.🔄 Check Your Understanding. 1. What are the three possible association statuses of a pointer, and which one must you never pass to
associated? 2. Why is initializing a pointer=> null()in its declaration a good default? 3. Afterdeallocate(p)whereq => phad aliased the same memory, what is wrong withq, and what must you do?
Answers
1. Associated, disassociated, and undefined. Never callassociatedon an undefined pointer — the result is itself undefined behavior. 2. It gives the pointer a defined (disassociated) status from birth, soassociated(p)is always a meaningful test and you can never accidentally use an undefined pointer. 3.qdangles — it aliases freed memory while possibly still reportingassociated(q) == .true.. You mustnullify(q)and never dereference it again; the language will not clean up aliases for you.
11.3 Dynamic Structures: A Linked List, a Tree, and Why Arrays Usually Win
Here is the classic reason people reach for pointers: dynamic data structures whose shape is not known until run time and changes as the program runs — a list that grows, a tree that branches. The enabling trick is that a derived type may contain a pointer to its own type, which lets you chain nodes together.
A linked list
A singly linked list is a chain of nodes, each holding a value and a pointer to the next node; the last node's pointer is disassociated, marking the end.
head --> [ 3 | *-]--> [ 2 | *-]--> [ 1 | / ]
val next val next val next=null()
The node type and the operations are short. We build the list by prepending (cheap: no traversal), which means values come out in reverse — push 1, 2, 3 and the list reads 3, 2, 1, the last-in-first-out order of a stack:
program linked_list
implicit none
type :: node_t
integer :: val
type(node_t), pointer :: next => null() ! a pointer to the SAME type
end type node_t
type(node_t), pointer :: head => null()
integer :: k
do k = 1, 3 ! prepend 1, 2, 3 -> list becomes 3 -> 2 -> 1
call prepend(head, k)
end do
call show(head)
call destroy(head) ! free every node; leaves head disassociated
print '(a, l1)', 'associated(head) after destroy = ', associated(head)
contains
subroutine prepend(list, value)
type(node_t), pointer, intent(inout) :: list
integer, intent(in) :: value
type(node_t), pointer :: fresh
allocate(fresh) ! one node's worth of memory, at run time
fresh%val = value
fresh%next => list ! new node points at the old head
list => fresh ! head becomes the new node
end subroutine prepend
subroutine show(list)
type(node_t), pointer, intent(in) :: list
type(node_t), pointer :: p
integer :: length, total
length = 0
total = 0
write(*, '(a)', advance='no') 'values head->tail:'
p => list
do while (associated(p)) ! walk until the end-of-list null
write(*, '(a, i0)', advance='no') ' ', p%val
length = length + 1
total = total + p%val
p => p%next ! step to the next node
end do
write(*, '(a)') '' ! end the line
print '(a, i0, a, i0)', 'length = ', length, ', sum = ', total
end subroutine show
subroutine destroy(list)
type(node_t), pointer, intent(inout) :: list
type(node_t), pointer :: p, nxt
p => list
do while (associated(p))
nxt => p%next ! save the next pointer BEFORE freeing p...
deallocate(p) ! ...or we would be reading freed memory
p => nxt
end do
nullify(list) ! the head is now invalid; make it honest
end subroutine destroy
end program linked_list
$ gfortran -std=f2018 -Wall linked_list.f90 -o list && ./list
values head->tail: 3 2 1
length = 3, sum = 6
Read the three routines against the picture. prepend allocates one fresh node, points its next at the
current head, and makes the head the new node — that is the whole insert, and it is $O(1)$. show walks
the chain with a traversal pointer p, following p => p%next until associated(p) goes false at the
terminating null, accumulating the length (3) and the sum ($3+2+1=6$) as it goes. destroy is the one to
study: it saves p%next into nxt before calling deallocate(p), because after the node is freed its
next field is gone — reading it would be the dangling-pointer bug from §11.2. Free first, read never;
save, free, step.
🐛 Find the Bug. A colleague simplifies
destroyto save a line:
fortran p => list do while (associated(p)) deallocate(p) ! <-- frees the node... p => p%next ! <-- ...then reads p%next from the freed node end doWhat goes wrong, and what is the fix?
Diagnosis
Afterdeallocate(p), the node's memory — including itsnextfield — is gone, sop => p%nextreads freed memory: undefined behavior (a crash if you are lucky, silent corruption if you are not). The order must be save the next pointer, then free: introducenxt => p%nextbeforedeallocate(p), thenp => nxt. You cannot read a node after you have freed it.
A tree
The same idea, with two child pointers instead of one next, gives a binary tree — the backbone of sorted
sets, expression trees, and spatial partitions:
type :: tree_t
real(dp) :: key
type(tree_t), pointer :: left => null()
type(tree_t), pointer :: right => null()
end type tree_t
Insertion walks down from the root, going left for smaller keys and right for larger, allocating a new node
at the empty branch it reaches; traversal recurses into left, visits the node, recurses into right. The
mechanics are a direct extension of the list, so we will not belabor a full program — the important lesson
is not how to build a tree in Fortran but whether you should, and the answer, more often than newcomers
expect, is no.
Why arrays usually win
Everything you have just seen works. It is also, for most numerical work in Fortran, the slow way to solve the problem — and understanding why is worth more than the syntax.
A linked structure scatters its nodes across memory: each allocate hands you a node from wherever the
allocator happens to have room, so consecutive nodes are, in general, nowhere near each other. Walking the
list therefore means pointer chasing — load a node, read its next address, jump to a random new
location, stall while the processor fetches a cache line it cannot predict, repeat. This defeats the two
things modern hardware is fastest at: prefetching (the CPU cannot guess where p%next leads, so it
cannot fetch ahead) and vectorization (you cannot apply one SIMD instruction across nodes that are not
adjacent). On top of that, every node costs a separate allocation, and allocation is not free.
Contrast a plain array. Its elements are contiguous, so a sweep walks memory in a straight line the prefetcher loves; the compiler can vectorize the loop; and there is exactly one allocation for the whole thing. This is the array superpower from Chapter 5, seen from the other side: the very contiguity that makes arrays fast is what a linked list throws away.
🚪 Threshold Concept — in Fortran, reach for an array first. In a language built for pointer-linked structures, a linked list is the natural default and an array is a special case. Fortran is the reverse. Its whole performance story is contiguous memory swept in order, and a pointer-linked structure opts out of that story on every node. So invert the instinct you may have brought from C or Java: do not ask "which linked structure fits this problem?" Ask "can this be an array?" — and it can, far more often than you would think. A stack or a growable list is an
allocatablearray you resize (you will build exactly that in this chapter's second case study). A sparse graph is often an index-based structure: store each node's "next" as an integer index into an array, not a pointer, and you keep the dynamic shape while regaining contiguity, prefetching, and the ability to save the whole thing to a file as one block. The linked list is a tool of last resort in Fortran, not first.⚡ Performance Note: The gap is not subtle. Summing ten million integers stored in a contiguous array versus the same ten million in a linked list is, on typical hardware, an order-of-magnitude difference in favor of the array — not because the arithmetic differs (it is identical) but because the array sweep hits cache and vectorizes while the list sweep stalls on unpredictable memory (a Tier 2 order-of-magnitude, not a promise; the direction, though, is never in doubt). When a genuinely dynamic, frequently-restructured graph really does call for links, the index-based array representation usually still beats raw pointers, and it is what large scientific codes overwhelmingly use.
🔄 Check Your Understanding. 1. In
destroy, why mustnxt => p%nextcome beforedeallocate(p)? 2. Name two hardware advantages a contiguous array has over a linked list when you sweep through it. 3. What is an "index-based" alternative to a pointer-linked list, and what does it recover?
Answers
1. Becausedeallocate(p)frees the node, including itsnextfield; readingp%nextafterwards reads freed memory (a dangling access). Save the next pointer while the node is still alive. 2. Prefetching (contiguous access is predictable, so the CPU fetches ahead) and vectorization (one SIMD instruction can process adjacent elements). A scattered list allows neither. 3. Store the nodes in an array and represent "next" as an integer index into that array instead of a pointer. It recovers contiguity, prefetching, vectorizability, and one-block file I/O, while keeping a dynamic, linked logical shape.
11.4 allocatable vs pointer: The Rule
You now have both dynamic-memory tools in hand — the allocatable arrays and components from Chapters 5
and 9, and the pointer of this chapter — and they overlap: both can obtain memory at run time with
allocate. So which do you use? For numerical Fortran the answer is a rule you can apply almost without
thinking, and it is worth stating in bold before we justify it: prefer allocatable; use pointer only
when you actually need what only a pointer can do. Here is the side-by-side that earns the rule.
allocatable |
pointer |
|
|---|---|---|
| Ownership | Sole owner of its memory | May alias other pointers/targets |
| Aliasing | None possible (unless also target) |
Yes — that is its purpose |
| Deallocation | Automatic on scope exit | Manual — you must deallocate, or leak |
| Dangling | Impossible | Possible (freed/out-of-scope target) |
| Undefined status | No — allocated() is always valid |
Yes — undefined until initialized |
Assignment b = a |
Deep copy, auto-(re)allocated to fit | Copies the value through, if associated |
| Compiler optimization | Full no-aliasing freedom | Conservative (must assume aliasing) |
| Can point at existing data | No | Yes |
| Linked structures | Awkward | Natural |
Three of those rows are the whole argument, and each is a theme of this book.
Automatic deallocation (safety). A local allocatable array or component is freed automatically when
its scope ends — you cannot leak it by forgetting, and you cannot make it dangle because nothing else
aliases it. A pointer, like C's malloc, is yours to free by hand on every exit path, and the error
branch you forgot is the leak you ship. This is the "modern Fortran is a modern language" theme made
concrete: the language manages the memory so you do not have to.
No aliasing (speed). This is the deep one. Because a plain allocatable array can never be aliased by
a pointer, the compiler knows that writing to it cannot change any other array — so it may keep values in
registers, reorder loads and stores, and vectorize freely. A pointer, by contrast, might alias another
pointer, so when the compiler sees writes through one and reads through another it must assume the worst
and generate cautious, slower code. This is the no-aliasing advantage that
Chapter 27 is devoted to — the same
property, first met in Chapter 1, that makes
Fortran outrun C on numerical kernels. Reaching for a pointer where an allocatable would do can forfeit
that advantage, quietly, for no benefit. Performance is not accidental; here it is a direct consequence of
which attribute you typed.
⚡ Performance Note: Even giving an array the
targetattribute — not making it a pointer, just permitting pointers to alias it — can cost you optimization, because it tells the compiler "a pointer may be watching this," switching off some of the no-aliasing freedom. So the counsel is stronger than "prefer allocatable": keep your hot arrays plainallocatable, with neitherpointernortarget, unless you have a concrete reason to relax that. Your heat solver's field, as the Project Checkpoint shows, stays exactly that plain.
Assignment does the right thing. For an allocatable b, the statement b = a deep-copies a,
allocating or reallocating b to the correct shape automatically (the F2003 behavior gfortran gives you by
default at -std=f2018). You get a genuine independent copy with one equals sign. For pointers, b = a
copies values through whatever b and a already point at — a different operation with different
preconditions, and a common source of surprise.
🔗 Connection — allocatable components beat pointer components. You met this already in Chapter 9: a derived type that needs a run-time-sized array inside it uses an
allocatablecomponent, not a pointer component, for exactly the reasons above — the component is deep-copied when the whole object is assigned, and freed automatically when the object dies. The heat solver'sfield_t, with itsreal(dp), allocatable :: u(:,:), is the canonical example, and this chapter is why it is written that way.
So when is a pointer the right tool? When you need one of the things only aliasing can give — and that is the entire subject of §11.5.
🔄 Check Your Understanding. 1. Give the one-line rule for choosing between
allocatableandpointer. 2. Why can the compiler optimize a plainallocatablearray more aggressively than apointer? 3. What doesb = ado when both areallocatablearrays of different sizes?
Answers
1. Preferallocatable; usepointeronly when you genuinely need aliasing, a linked structure, a polymorphic container, a callback, or C interoperability. 2. A plain allocatable cannot be aliased by any pointer, so the compiler knows a write to it cannot change any other array — the no-aliasing advantage — and may reorder and vectorize freely. A pointer might alias, forcing conservative code. 3. It deep-copiesaintob, automatically reallocatingbtoa's shape first — an independent copy, correctly sized, from one assignment.
11.5 When You Do Need Pointers — and the contiguous Attribute
The rule is "prefer allocatable," not "never use pointers." There are jobs only a pointer can do, and a professional reaches for one without guilt when the job is one of these:
- Aliasing an existing object — giving a compact name to a piece of a larger structure, or letting two parts of a program share one object. An allocatable owns memory and cannot point at data that already exists; a pointer can.
- Genuine linked structures — when the problem really is a frequently-restructured graph or list and an index-based array will not serve (§11.3), pointers are the honest tool.
- Polymorphic containers — a list or array meant to hold objects of different dynamic types uses
class(...)-pointer or allocatable components; this is the OOP machinery of Chapter 10, whereselect typesorts out what you actually have at run time. - Callbacks — a procedure pointer lets you store which function to call in a variable, so one generic routine can be handed different behaviors. It is how you pass a right-hand-side function to a generic ODE integrator (the method-of-lines idea of Chapter 23), or a comparison function to a sort.
- C interoperability — the C world is built on pointers, so talking to it (§Chapter 14) means handling
C addresses as
type(c_ptr)and turning them into usable Fortran pointers withc_f_pointer.
Procedure pointers: storing "which function to call"
A procedure pointer points at a procedure instead of at data. You declare it against an interface, aim it at any procedure matching that interface, and call through it — and you can re-aim it at run time.
program callback_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
abstract interface
function unary(x) result(y) ! the shape every target must match
import :: dp
real(dp), intent(in) :: x
real(dp) :: y
end function unary
end interface
procedure(unary), pointer :: f => null()
f => square
print '(a, f0.1)', 'f => square; f(3.0) = ', f(3.0_dp) ! 9.0
f => negate
print '(a, f0.1)', 'f => negate; f(3.0) = ', f(3.0_dp) ! -3.0
contains
function square(x) result(y)
real(dp), intent(in) :: x
real(dp) :: y
y = x * x
end function square
function negate(x) result(y)
real(dp), intent(in) :: x
real(dp) :: y
y = -x
end function negate
end program callback_demo
$ gfortran -std=f2018 -Wall callback_demo.f90 -o cb && ./cb
f => square; f(3.0) = 9.0
f => negate; f(3.0) = -3.0
The pointer f is aimed at square, called (giving $3^2 = 9$), then re-aimed at negate and called
again (giving $-3$) — one call site, two behaviors, chosen at run time. That is the callback pattern, and
it is genuinely a pointer's job: there is no allocatable equivalent, because what varies is not data but
code to run. (Note the import :: dp inside the interface body: an interface is its own scope and does
not see the host's used entities unless you import them.)
C interoperability, in one line
When Fortran receives a bare memory address from a C library — the return of a C malloc, say, arriving as
a type(c_ptr) — you cannot index it directly; you convert it to a real Fortran pointer of a known shape
with c_f_pointer:
real(dp), pointer :: buf(:)
call c_f_pointer(c_handle, buf, [n]) ! now buf(1:n) aliases the C array
This is illustrative — the full story, with iso_c_binding, type(c_ptr), and passing arrays and structs
across the language boundary, is Chapter 14.
The point for now is only that here the pointer is unavoidable: C hands you an address, and a pointer is
Fortran's word for "an address I can use as an array."
The contiguous attribute
When you do use an array pointer, you inherit a performance problem the compiler cannot solve on its own.
A pointer may be aimed at a strided slice of an array — every third element, or a row of a column-major
matrix — whose elements are not adjacent in memory. Because the compiler must assume any array pointer
might be strided, it generates general, slower code for it, even when at run time the pointer always
happens to point at a solid block. The contiguous attribute is how you close that gap.
Definition (contiguous). The
contiguousattribute (Fortran 2008), applied to an array pointer or an assumed-shape dummy argument, is your promise to the compiler that the array occupies a single unbroken block of memory — no strides, no gaps. It changes no results; it licenses optimization, letting the compiler treat the array as the simple contiguous span it is, which is what makes vectorization and efficient memory streaming possible. It is a promise you must keep: aim acontiguouspointer at a genuinely strided slice and the program is nonconforming (the compiler may silently copy, or misbehave). The intrinsicis_contiguous(a)reports, at run time, whether an array actually is contiguous.
In column-major storage (Chapter 5), a full column of a matrix is contiguous, while a row is strided —
so a contiguous pointer may legitimately view a column but not a row. This program aims a contiguous
pointer at each column of a small grid, and uses is_contiguous to confirm the column/row distinction:
program contiguous_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
integer, parameter :: n = 3
real(dp), allocatable, target :: field(:,:)
real(dp), pointer, contiguous :: col(:)
integer :: i, j
allocate(field(n, n))
do j = 1, n
do i = 1, n
field(i, j) = real(10*i + j, dp) ! field(i,j) announces its own address
end do
end do
! A whole column is contiguous in column-major order; a whole row is strided.
print '(a, l1)', 'is_contiguous(field(:,2)) [column] = ', is_contiguous(field(:,2))
print '(a, l1)', 'is_contiguous(field(2,:)) [row] = ', is_contiguous(field(2,:))
do j = 1, n
col => field(:, j) ! legal: a column IS contiguous, so the promise holds
print '(a, i0, a, f0.1)', 'sum of column ', j, ' = ', sum(col)
end do
nullify(col)
deallocate(field)
end program contiguous_demo
$ gfortran -std=f2018 -Wall contiguous_demo.f90 -o contig && ./contig
is_contiguous(field(:,2)) [column] = T
is_contiguous(field(2,:)) [row] = F
sum of column 1 = 63.0
sum of column 2 = 66.0
sum of column 3 = 69.0
With field(i,j) = 10i + j, column 1 holds 11, 21, 31 (sum 63), column 2 holds 12, 22, 32 (sum 66),
and column 3 holds 13, 23, 33 (sum 69) — each a hand-checkable total. And is_contiguous confirms the
rule that governs when the contiguous promise is safe to make: a column comes back T, a row F.
🔗 Connection:
contiguousis a first taste of a performance tool you will use in anger in Chapter 29, where the more common application is on an assumed-shape dummy argument —real(dp), intent(inout), contiguous :: u(:,:)— to promise a kernel that its caller always passes solid storage, so the compiler can vectorize the sweep. There it is one of the levers, alongside loop ordering and cache blocking, that turns correct code into fast code. You are meeting the attribute here, where pointers live; you will spend it there.🔄 Check Your Understanding. 1. Name three situations where a pointer is genuinely the right tool, not an allocatable. 2. What promise does
contiguousmake, and what happens if you break it? 3. For a column-major matrixa(n,n), isa(:,3)contiguous? Isa(3,:)?
Answers
1. Aliasing an existing object; a genuine (index-based won't do) linked structure; a polymorphic container; a callback via a procedure pointer; C interoperability (c_f_pointer). Any three. 2. That the array is a single unbroken block of memory (no strides/gaps), which licenses vectorization and efficient streaming. Break it — aim acontiguouspointer at a strided slice — and the program is nonconforming; the compiler may copy or misbehave. 3.a(:,3)(a full column) is contiguous;a(3,:)(a row) is strided, hence not contiguous, because the first index varies fastest in column-major storage.
Project Checkpoint
This checkpoint is a decision, revisited and now justified. Back in
Chapter 9 you gave the heat solver a field_t derived type, and
its grid was declared
real(dp), allocatable :: u(:,:)
— an allocatable component, not a pointer component. This chapter is where you can finally say exactly
why, in one sentence per reason:
- Automatic deallocation. When a
field_tgoes out of scope, itsuarray is freed automatically — no manualdeallocateon every exit path, no leak on the error branch you forgot. A pointer component would put that bookkeeping (and its bugs) on you. - No aliasing → a faster stencil. A plain
allocatablearray cannot be aliased by any pointer, so when Chapter 24 sweeps the five-point Laplacian acrossu, the compiler knows the writes to the new field cannot secretly change the old one, and it vectorizes and reorders freely. That is the no-aliasing advantage of Chapter 27 — and a pointer field would forfeit it.
The following short program makes the choice concrete. The field is allocatable (given target here
only so we can demonstrate a pointer view); a contiguous pointer sweeps its columns; and there is no
deallocate at the end, because none is needed:
program checkpoint_field_is_allocatable
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
real(dp), allocatable, target :: u(:,:) ! `target` only for the demo below
real(dp), pointer, contiguous :: col(:)
integer :: j
allocate(u(3,3))
u = reshape([real(dp) :: 1, 2, 3, 4, 5, 6, 7, 8, 9], [3, 3]) ! column-major
do j = 1, 3
col => u(:, j) ! a column is contiguous, so the promise holds
print '(a, i0, a, f0.1)', 'column ', j, ' sum = ', sum(col)
end do
nullify(col)
! No deallocate(u): it is freed automatically when the program unit ends.
end program checkpoint_field_is_allocatable
$ gfortran -std=f2018 -Wall project-checkpoint.f90 -o checkpoint && ./checkpoint
column 1 sum = 6.0
column 2 sum = 15.0
column 3 sum = 24.0
The reshape fills u column-major, so column 1 is [1, 2, 3] (sum 6), column 2 is [4, 5, 6] (sum 15),
and column 3 is [7, 8, 9] (sum 24) — verify each against the printed line. One honest caveat that is
itself part of the lesson: in the real solver, u carries no target attribute, because — as the
Performance Note in §11.4 warned — permitting pointer aliasing can cost optimization. We added target
here purely to show contiguous in action. When Chapter 29 optimizes the sweep, it applies contiguous
not to a pointer aliasing the field but to the kernel's assumed-shape dummy argument, keeping the field
itself pristine. Record in your heat-solver/ notes the one-line rule you will live by for the rest of the
book: the field is allocatable, never a pointer.
Summary
Pointers are a real part of Fortran and a small part of good numerical Fortran. This chapter taught the mechanics so you can read and fix any pointer code — and taught the judgment to write very little of it.
| Idea | The short version |
|---|---|
pointer / target |
A pointer is an alias for another object; the object must be target (or itself pointer) to be aliased. |
=> vs = |
p => a changes the alias (points p at a); p = a changes the value (copies through an associated p). |
| Association status | associated / disassociated / undefined; associated(p) tests the first two, is illegal on the third. |
nullify / => null() |
Set a pointer to the clean disassociated state; initialize every pointer this way at birth. |
| Undefined pointer | Never initialized; associated on it is undefined behavior. Fix: always => null() or nullify first. |
| Dangling pointer | Aliases freed/out-of-scope memory; may still report associated. Fix: nullify every alias after deallocate. |
| Linked structures | A derived type with a pointer to its own type; free nodes save-then-deallocate, never read a freed node. |
| Arrays usually win | Contiguous memory prefetches and vectorizes; pointer-chasing does neither. Prefer arrays / index-based structures. |
allocatable vs pointer |
Prefer allocatable: automatic deallocation, no dangling, no aliasing → full optimization. |
When to use pointer |
Aliasing, genuine linked structures, polymorphic containers, callbacks (procedure pointers), C interop. |
contiguous |
A promise that an array pointer / dummy is one solid block; licenses vectorization. is_contiguous tests it. |
The two things to memorize. First, the operator distinction: => moves the alias, = writes the
value — confuse them and you corrupt data silently. Second, the rule that governs the whole chapter:
prefer allocatable; a pointer is a last resort you reach for only when you need aliasing, links,
polymorphism, callbacks, or C. Everything else here is detail in service of those two sentences.
Spaced Review
Retrieval practice on the two chapters this one builds on most directly — arrays (Ch. 5) and derived types (Ch. 9). Answer before peeking.
-
(Ch. 5) Why does a local
allocatablearray not leak memory even if you never calldeallocate, and how does that contrast with apointer?
Answer
A local allocatable is automatically deallocated when its procedure returns — the language frees it for you on scope exit. A `pointer` has no such guarantee: you must `deallocate` it by hand on every exit path, and forgetting leaks. This is one of the pillars of §11.4's "prefer allocatable" rule. -
(Ch. 5) In Fortran's column-major layout, which is contiguous in memory — a row
a(i,:)or a columna(:,j)— and why does this determine when acontiguouspointer is legal?
Answer
A column `a(:,j)` is contiguous, because the first index varies fastest, so a whole column sits in one unbroken run of memory; a row `a(i,:)` is strided. You may aim a `contiguous` pointer at a column but not at a row — the promise must actually hold. (Confirmed with `is_contiguous` in §11.5.) -
(Ch. 5) What does the array section
u(2:n-1, 2:n-1)select, and is it something a pointer could alias?
Answer
It selects the interior of the grid — every element except the outermost row and column on each side — as a first-class rank-2 array value. A pointer can alias such a section (if the parent has `target`), but the section is strided in general, so a plain array pointer, not a `contiguous` one, is what fits. -
(Ch. 9) In a derived type that needs a run-time-sized array inside it, why is an
allocatablecomponent preferred over apointercomponent?
Answer
The allocatable component is deep-copied when the whole object is assigned and freed automatically when the object is destroyed, with no aliasing and no dangling — the same three advantages as §11.4, now at the component level. A pointer component would demand manual cleanup and could alias or dangle. The heat solver's `field_t` uses `real(dp), allocatable :: u(:,:)` for exactly this reason. -
(Ch. 9) A
field_tbundlesnx, ny, dx, dyand the gridu. If you assignb = afor twofield_tvalues, what happens toa's data — isb%ua copy or an alias ofa%u?
Answer
It is an independent copy. Intrinsic assignment of a derived type deep-copies each component, and because `u` is allocatable, `b%u` is allocated to `a%u`'s shape and its values copied — two separate arrays. Had `u` been a pointer component, the default assignment would have made `b%u` alias `a%u` instead (pointer components are copied by association, not by value) — a classic aliasing surprise, and one more reason the component is allocatable.
What's Next
You can now alias when you must, and — more importantly — you know when not to. That completes the pair of
dynamic-memory tools: allocatable for owning data (the default) and pointer for referring to it (the
exception). Chapter 12 turns to a data type we have used only in
passing — text. You will meet deferred-length (character(:), allocatable) strings, which are just the
allocatable idea of this part applied to characters; the string intrinsics that trim, scan, and search;
and internal files, which let you read numbers out of a string and write them back in. It is where your
solver learns to build output filenames like heat_000123.vtk — and where you will be glad, one more time,
that Fortran's default is to own its memory and clean up after itself.