Program Structure & Output
Hello, World
The shape is familiar: a named entry point with a beginning and an end. What is missing is the include — the printing intrinsic is part of the language, not a library you declare.
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}program hello
implicit none
print '(a)', "Hello, World!"
end program helloThe
'(a)' is a format specification meaning "one character string", and it is what keeps the output free of the leading blank that print *, would add. The program name after program and end program is documentation — it does not have to be main and nothing links against it. Falling off the end returns a zero exit status; stop 3 is how you return something else.implicit none: The Line You Must Not Omit
This is the first thing to know about Fortran. Without
implicit none, a name that was never declared is silently given a type based on its first letter — i through n are integers and everything else is real — so a typo becomes a new variable rather than an error.#include <stdio.h>
int main(void) {
int counter = 0;
counter = counter + 1;
/* countr = 5; does not compile: undeclared identifier.
Every name must be declared, always. */
printf("counter is %d\n", counter);
return 0;
}program declarations
implicit none ! without this line, a typo compiles
integer :: counter
counter = 0
counter = counter + 1
! With implicit none, "countr = 5" is an error.
! Without it, countr would be a brand new integer.
print '(a, i0)', "counter is ", counter
end program declarationsThe implicit-typing rule is why
i, j, k, m and n became the conventional loop-counter names — they were the letters that defaulted to integer. It is also why implicit none is the first line of every modern Fortran program and of every example on this page: it turns the language’s oldest misfeature off. There is no way to enable it globally; you write it in every program unit and every module.Formatted Output: Edit Descriptors Instead of Conversions
A
printf format string interleaves literal text and conversions. A Fortran format is a comma-separated list of edit descriptors, and the values come as a separate list — so the format describes the shape of the line and nothing else.#include <stdio.h>
int main(void) {
const char *name = "Ada";
int count = 3;
double average = 2.5;
printf("%s has %d items averaging %.2f\n", name, count, average);
printf("%10s|%10s|\n", "left", "right");
printf("%05d %9.3e\n", 42, 1234.5);
return 0;
}program formatting
implicit none
character(len=*), parameter :: name = "Ada"
integer, parameter :: count = 3
real, parameter :: average = 2.5
print '(a, a, i0, a, f0.2)', name, " has ", count, &
" items averaging ", average
print '(a10, "|", a10, "|")', "left", "right"
print '(i5.5, 1x, es9.3e2)', 42, 1234.5
end program formattingThe descriptors read straightforwardly once you know the letters:
a is character, i integer, f fixed-point real, es scientific, 1x one blank. Widths come after the letter, so i5 is a five-wide integer and i0 means "exactly as wide as it needs to be" — the closest thing to %d. Two things have no counterpart: the a descriptor always right-justifies, so there is no %-10s to be had — you pad with trim and blanks instead — and the exponent letter comes out capital, so 1.234E+03 where C prints 1.234e+03. Note also the & at the end of the continued line: Fortran statements end at the newline unless you say otherwise.Command-Line Arguments Without argc and argv
There is no argument list on the entry point. Two intrinsics ask the runtime instead, which means every procedure can reach them, not just
main.#include <stdio.h>
int main(int argc, char *argv[]) {
printf("argument count: %d\n", argc - 1);
for (int index = 1; index < argc; index++) {
printf(" %d: %s\n", index, argv[index]);
}
return 0;
}program arguments
implicit none
integer :: count, index, length
character(len=256) :: value
count = command_argument_count()
print '(a, i0)', "argument count: ", count
do index = 1, count
call get_command_argument(index, value, length)
print '(a, i0, a, a)', " ", index, ": ", value(1:length)
end do
end program argumentsThe count excludes the program name, which
argc includes — so the two columns print the same number only because the C one subtracts one. get_command_argument writes into a buffer you sized and reports the true length separately, which is the same discipline as snprintf; asking for argument zero gives the program name.Types, Kinds, and No Unsigned
Kinds Instead of short, long and long long
Fortran has one integer type and one real type, each parameterized by a kind number. The named kinds in
iso_fortran_env spell the width directly, which is the counterpart of <stdint.h> rather than of short and long.#include <stdio.h>
#include <stdint.h>
int main(void) {
int16_t small = 300;
int32_t medium = 70000;
int64_t large = 5000000000;
float single = 1.0f / 3.0f;
double doubled = 1.0 / 3.0;
printf("%d %d %lld\n", small, medium, (long long)large);
printf("%zu %zu %zu\n",
sizeof small, sizeof medium, sizeof large);
printf("%.9f\n%.17f\n", (double)single, doubled);
return 0;
}program kinds
use, intrinsic :: iso_fortran_env, only: int16, int32, int64, real32, real64
implicit none
integer(int16) :: small = 300_int16
integer(int32) :: medium = 70000_int32
integer(int64) :: large = 5000000000_int64
real(real32) :: single = 1.0_real32 / 3.0_real32
real(real64) :: doubled = 1.0_real64 / 3.0_real64
print '(i0, 1x, i0, 1x, i0)', small, medium, large
print '(i0, 1x, i0, 1x, i0)', &
storage_size(small) / 8, storage_size(medium) / 8, storage_size(large) / 8
print '(f11.9)', single
print '(f19.17)', doubled
end program kindsThe suffix on a literal —
5000000000_int64 — is the counterpart of C’s LL, and it matters for the same reason: without it the literal is default kind and may not fit. storage_size reports bits rather than bytes, which is why the second line divides by eight; there is no sizeof, because a Fortran program is not expected to know how anything is laid out.There Is No Unsigned Type
Every Fortran integer is signed. The whole family of signed/unsigned conversion surprises you know from C simply does not exist — and neither does the ability to use the top bit as magnitude.
#include <stdio.h>
int main(void) {
unsigned int large = 4000000000u;
printf("as unsigned %u\n", large);
printf("as signed %d\n", (int)large);
/* The classic trap: the signed operand converts, so this is FALSE. */
int negative = -1;
unsigned int zero = 0u;
printf("-1 < 0u ? %d\n", negative < zero);
return 0;
}program signedness
use, intrinsic :: iso_fortran_env, only: int32, int64
implicit none
integer(int64) :: large = 4000000000_int64
integer(int32) :: negative = -1
print '(a, i0)', "as int64 ", large
print '(a, i0)', "as int32 ", int(large - 4294967296_int64, int32)
print '(a, l1)', "-1 < 0 ? ", negative < 0
print '(a, i0)', "huge(int32) ", huge(0_int32)
end program signednessThe C column’s last line prints
0, which is correct and almost never what the author meant. Fortran has no way to write that bug, at the cost of needing a wider type whenever a value would only fit unsigned. Note also that overflow is not defined here either — it is simply not specified, exactly as in C, and gfortran will happily fold it away. huge(x) is the largest value of x’s kind, which is INT_MAX spelled as a question about a variable.A logical Is Not a Number
C’s conditions are integers and zero means false, which is why
if (count) works. Fortran has a distinct logical type with its own operators, and no implicit conversion in either direction.#include <stdio.h>
int main(void) {
int count = 0;
if (!count) {
printf("count is zero\n");
}
/* A comparison yields an int, so this is arithmetic: */
int flag = (count == 0);
printf("flag as int: %d\n", flag + 1);
return 0;
}program logicals
implicit none
integer :: count = 0
logical :: flag
if (count == 0) then
print '(a)', "count is zero"
end if
flag = (count == 0)
print '(a, l1)', "flag: ", flag
! flag + 1 is a type error; there is no arithmetic on logical.
print '(a, i0)', "as an integer: ", merge(1, 0, flag)
end program logicalsThe operators are different too:
.and., .or., .not., .eqv. and .neqv. take logicals, while <, == and friends produce them. merge(a, b, mask) is the ternary operator, and it works element-wise on arrays as well — which is a capability C’s ?: has no equivalent of.Integer Division Truncates, and the Types Decide
This is the one place where the two languages agree almost exactly: the operand types choose integer or floating division, and integer division truncates toward zero.
#include <stdio.h>
int main(void) {
printf("7 / 2 = %d\n", 7 / 2);
printf("7.0 / 2.0 = %.1f\n", 7.0 / 2.0);
printf("7 / 2.0 = %.1f\n", 7 / 2.0);
printf("-17 / 5 = %d\n", -17 / 5);
printf("-17 %% 5 = %d\n", -17 % 5);
return 0;
}program division
implicit none
print '(a, i0)', "7 / 2 = ", 7 / 2
print '(a, f0.1)', "7.0 / 2.0 = ", 7.0 / 2.0
print '(a, f0.1)', "7 / 2.0 = ", 7 / 2.0
print '(a, i0)', "-17 / 5 = ", -17 / 5
print '(a, i0)', "-17 mod 5 = ", mod(-17, 5)
print '(a, i0)', "-17 modulo 5= ", modulo(-17, 5)
end program division
The one addition is the pair
mod and modulo: mod takes the sign of the numerator and matches C’s % exactly, while modulo takes the sign of the divisor and gives 3 for modulo(-17, 5). Having both spelled separately is a small kindness — in C you write the adjustment by hand every time you want the second one.Arrays Start at 1 and Store by Column
Subscripts Start at 1 — and the Lower Bound Is Yours
The default lower bound is 1, not 0. More usefully, the bound is part of the declaration: an array can run from −5 to 5, or from 1900 to 2000, and the subscripts mean what they say.
#include <stdio.h>
int main(void) {
int numbers[5] = { 10, 20, 30, 40, 50 };
printf("first %d, last %d\n", numbers[0], numbers[4]);
/* An array indexed by year means subtracting the base by hand. */
int population[3]; /* 1990, 1991, 1992 */
population[1990 - 1990] = 100;
population[1991 - 1990] = 110;
population[1992 - 1990] = 120;
printf("1991: %d\n", population[1991 - 1990]);
return 0;
}program bounds
implicit none
integer :: numbers(5) = [10, 20, 30, 40, 50]
integer :: population(1990:1992)
print '(a, i0, a, i0)', "first ", numbers(1), ", last ", numbers(5)
population(1990) = 100
population(1991) = 110
population(1992) = 120
print '(a, i0)', "1991: ", population(1991)
print '(a, i0, a, i0)', "bounds ", lbound(population, 1), &
" to ", ubound(population, 1)
end program boundsRound brackets are used for both subscripting and calling, which takes some getting used to —
numbers(1) and sin(x) look identical, and the compiler tells them apart from the declarations. The arbitrary lower bound removes a whole category of off-by-one arithmetic: no subtracting a base, and lbound/ubound report what the array actually is.Two Dimensions Are Stored by Column
This is the difference that causes the most real bugs when the two languages meet. C stores a two-dimensional array row by row, so the last subscript varies fastest in memory. Fortran stores it column by column, so the first subscript does.
#include <stdio.h>
int main(void) {
int grid[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
/* Written the way the layout wants: last subscript innermost. */
for (int row = 0; row < 2; row++) {
for (int column = 0; column < 3; column++) {
printf("%d ", grid[row][column]);
}
}
printf("\n");
/* The same six ints, read straight out of memory in order: */
const int *flat = &grid[0][0];
for (int index = 0; index < 6; index++) {
printf("%d ", flat[index]);
}
printf("\n");
return 0;
}program storage_order
implicit none
integer :: grid(2, 3)
integer :: row, column
grid = reshape([1, 4, 2, 5, 3, 6], [2, 3])
! Written the way the layout wants: FIRST subscript innermost.
do column = 1, 3
do row = 1, 2
write(*, '(i0, 1x)', advance="no") grid(row, column)
end do
end do
print *
! The same six integers, read straight out of memory in order:
print '(6(i0, 1x))', reshape(grid, [6])
end program storage_orderBoth columns hold the same matrix — rows
1 2 3 and 4 5 6 — and both walk it in the order their own memory layout prefers, and they print different sequences: 1 2 3 4 5 6 against 1 4 2 5 3 6. Each column’s two lines agree with each other, which is the proof that each really is reading memory in order. The practical consequence is the loop nesting: the innermost loop should vary the last subscript in C and the first in Fortran, and getting it backwards walks memory with a stride instead of sequentially — on a large matrix that is the difference between running at cache speed and not.An Array Argument Knows Its Own Shape
A C array decays to a pointer the moment it is passed, which is why every function that takes one also takes a length. A Fortran assumed-shape dummy argument carries the bounds with it, so the procedure can ask.
#include <stdio.h>
static int sum_of(const int *values, size_t count) {
int total = 0;
for (size_t index = 0; index < count; index++) {
total += values[index];
}
return total;
}
int main(void) {
int numbers[] = { 3, 1, 4, 1, 5 };
size_t count = sizeof numbers / sizeof numbers[0];
printf("count %zu, sum %d\n", count, sum_of(numbers, count));
return 0;
}program shapes
implicit none
integer :: numbers(5) = [3, 1, 4, 1, 5]
print '(a, i0, a, i0)', "count ", size(numbers), ", sum ", sum_of(numbers)
contains
integer function sum_of(values)
integer, intent(in) :: values(:) ! assumed shape: bounds come along
integer :: index
sum_of = 0
do index = 1, size(values)
sum_of = sum_of + values(index)
end do
end function sum_of
end program shapesThe colon in
values(:) is what makes it assumed-shape, and it requires an explicit interface — which is why the procedure lives after contains inside the program, or in a module. The older style, values(count) with the length passed separately, is exactly the C convention and still works; it is what you will see in code written before 1990.The Array Is a First-Class Value
Arithmetic on Whole Arrays, With No Loop
This is the feature Fortran exists for. An operator applied to two arrays of the same shape works element by element and produces an array, so the loop is not written, not indexed, and not able to go wrong.
#include <stdio.h>
int main(void) {
double first[4] = { 1.0, 2.0, 3.0, 4.0 };
double second[4] = { 10.0, 20.0, 30.0, 40.0 };
double result[4];
for (int index = 0; index < 4; index++) {
result[index] = first[index] + second[index] * 2.0;
}
for (int index = 0; index < 4; index++) {
printf("%.1f ", result[index]);
}
printf("\n");
return 0;
}program whole_array
implicit none
real :: first(4) = [1.0, 2.0, 3.0, 4.0]
real :: second(4) = [10.0, 20.0, 30.0, 40.0]
real :: result(4)
result = first + second * 2.0
print '(4(f0.1, 1x))', result
print '(a, f0.1)', "total ", sum(result)
print '(4(f0.1, 1x))', sqrt(first) ! intrinsics are elemental too
end program whole_arrayEvery mathematical intrinsic is elemental:
sqrt of an array is an array of square roots, with no loop and no map. Because the compiler knows the whole operation, it can vectorize it, fuse the expression, and skip the temporary — which is a large part of why numerical code is still written in this language. The one rule is shape conformance: adding a 4-element array to a 5-element one is a compile error, not a buffer overrun.Sections and Strides Instead of Pointer Arithmetic
A slice of a C array is a pointer plus a count, and a strided slice is a loop. Fortran has subscript-triplet notation — start, end, stride — and the result is an array that can be assigned, passed, or operated on.
#include <stdio.h>
int main(void) {
int numbers[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
/* Elements 2..5 */
for (int index = 2; index <= 5; index++) {
printf("%d ", numbers[index]);
}
printf("\n");
/* Every second element */
for (int index = 0; index < 10; index += 2) {
printf("%d ", numbers[index]);
}
printf("\n");
/* Reversed */
for (int index = 9; index >= 0; index--) {
printf("%d ", numbers[index]);
}
printf("\n");
return 0;
}program sections
implicit none
integer :: numbers(10) = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print '(4(i0, 1x))', numbers(3:6) ! elements 3..6
print '(5(i0, 1x))', numbers(1:10:2) ! every second
print '(10(i0, 1x))', numbers(10:1:-1) ! reversed
numbers(1:3) = 0 ! assign to a whole section
print '(10(i0, 1x))', numbers
end program sectionsA section is a first-class value: it can be passed to a procedure, assigned to, or used in an expression, and the compiler either passes a descriptor or makes a temporary copy as needed. That last assignment — setting three elements to one scalar — is broadcasting, and it is what C spells as a
for loop or a memset that only works for zero.The Reductions Are Built In
Summing, finding a maximum, locating it, and taking a dot product are four loops in C. Each is one intrinsic here, and each accepts an optional mask so "the sum of the positive elements" is still one call.
#include <stdio.h>
int main(void) {
int numbers[8] = { 3, 1, 4, 1, 5, 9, 2, 6 };
int total = 0, largest = numbers[0], position = 0, evenTotal = 0;
for (int index = 0; index < 8; index++) {
total += numbers[index];
if (numbers[index] > largest) {
largest = numbers[index];
position = index;
}
if (numbers[index] % 2 == 0) {
evenTotal += numbers[index];
}
}
printf("sum %d, max %d at %d, even sum %d\n",
total, largest, position + 1, evenTotal);
return 0;
}program reductions
implicit none
integer :: numbers(8) = [3, 1, 4, 1, 5, 9, 2, 6]
print '(a, i0, a, i0, a, i0, a, i0)', &
"sum ", sum(numbers), &
", max ", maxval(numbers), &
" at ", maxloc(numbers, 1), &
", even sum ", sum(numbers, mask=mod(numbers, 2) == 0)
print '(a, i0)', "dot product ", dot_product(numbers, numbers)
print '(a, l1)', "any above 8 ", any(numbers > 8)
print '(a, l1)', "all positive ", all(numbers > 0)
end program reductionsThe
mask= argument is the part with no concise C equivalent: it takes a logical array — here produced by comparing the whole array against a scalar — and reduces only where it is true. maxloc returns a position, which is 1-based like every other subscript, so the C column has to add one to agree. count, minloc, product, any and all round out the family.Matrix Multiply Is an Operator’s Worth of Code
The triple loop is the canonical C exercise. It is an intrinsic here, and so is transposition — both of which the compiler is free to hand to an optimized library.
#include <stdio.h>
int main(void) {
double left[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
double right[3][2] = { { 7, 8 }, { 9, 10 }, { 11, 12 } };
double product[2][2] = { { 0 } };
for (int row = 0; row < 2; row++) {
for (int column = 0; column < 2; column++) {
for (int inner = 0; inner < 3; inner++) {
product[row][column] += left[row][inner] * right[inner][column];
}
}
}
for (int row = 0; row < 2; row++) {
printf("%6.1f %6.1f\n", product[row][0], product[row][1]);
}
return 0;
}program matrices
implicit none
real :: left(2, 3), right(3, 2), product(2, 2)
integer :: row
left = reshape([1., 4., 2., 5., 3., 6.], [2, 3])
right = reshape([7., 9., 11., 8., 10., 12.], [3, 2])
product = matmul(left, right)
do row = 1, 2
print '(f6.1, 1x, f6.1)', product(row, 1), product(row, 2)
end do
end program matricesThe
reshape literals are written in column order, which is why they look shuffled next to the C initializers holding the same matrices — that is the storage-order difference from two rows back, showing up in the source. transpose, dot_product and matmul are the three that matter; anything larger goes to BLAS, and the compiler will often call it for you.A character Has a Fixed Length
A character Variable Has a Length, and Pads With Blanks
This is neither a
char * nor a counted string. A character(len=10) is exactly ten characters, always; assigning a shorter value pads it with blanks and assigning a longer one truncates, both silently.#include <stdio.h>
#include <string.h>
int main(void) {
char buffer[11]; /* 10 characters plus a terminator */
strncpy(buffer, "Ada", sizeof buffer - 1);
buffer[sizeof buffer - 1] = '\0';
printf("[%s] length %zu\n", buffer, strlen(buffer));
strncpy(buffer, "a much longer name", sizeof buffer - 1);
buffer[sizeof buffer - 1] = '\0';
printf("[%s] length %zu\n", buffer, strlen(buffer));
return 0;
}program fixed_length
implicit none
character(len=10) :: buffer
buffer = "Ada"
print '(a, a, a, i0, a, i0)', "[", buffer, "] len ", len(buffer), &
" len_trim ", len_trim(buffer)
buffer = "a much longer name" ! silently truncated to 10
print '(a, a, a, i0)', "[", buffer, "] len ", len(buffer)
end program fixed_lengthThe two lengths are the point:
len is the declared size and never changes, while len_trim is the length ignoring trailing blanks — the closest thing to strlen. That padding is why trim() appears in almost every line of real Fortran that prints a string, and why comparing two character values of different lengths pads the shorter one rather than reporting a difference.Concatenation, Substrings, and Searching
The operations are all here and none of them takes a destination buffer. Concatenation is an operator, a substring is subscript-triplet notation on a character variable, and
index is strstr returning a position.#include <stdio.h>
#include <string.h>
int main(void) {
const char *text = "the quick brown fox";
printf("length %zu\n", strlen(text));
printf("find %ld\n", (long)(strstr(text, "quick") - text));
char piece[6];
strncpy(piece, text + 4, 5);
piece[5] = '\0';
printf("substr %s\n", piece);
char joined[64];
snprintf(joined, sizeof joined, "%s%s", "hello, ", "world");
printf("joined %s\n", joined);
return 0;
}program string_operations
implicit none
character(len=*), parameter :: text = "the quick brown fox"
print '(a, i0)', "length ", len(text)
print '(a, i0)', "find ", index(text, "quick")
print '(a, a)', "substr ", text(5:9)
print '(a, a)', "joined ", "hello, " // "world"
print '(a, a)', "upper ", to_upper(text)
contains
function to_upper(input) result(output)
character(len=*), intent(in) :: input
character(len=len(input)) :: output
integer :: position, code
do position = 1, len(input)
code = iachar(input(position:position))
if (code >= iachar("a") .and. code <= iachar("z")) then
output(position:position) = achar(code - 32)
else
output(position:position) = input(position:position)
end if
end do
end function to_upper
end program string_operationsindex returns a 1-based position and 0 when the substring is absent, which is cleaner than strstr’s pointer-or-NULL — and it is why the two columns print 4 and 5 for the same match. Note that there is no uppercase intrinsic — the helper above is the standard way, and it is the same loop you would write in C, because iachar and achar are exactly the cast to and from a character code. The character(len=len(input)) declaration on the result is an automatic length: the returned string is sized from an argument.Strings That Size Themselves
A
character(len=:), allocatable variable takes the length of whatever is assigned to it. That is the closest Fortran comes to a growable string, and it removes the "how big should the buffer be" question from the places that most need it.#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
/* A result whose length is not known up front means malloc. */
const char *first = "hello";
const char *second = ", world";
size_t length = strlen(first) + strlen(second);
char *joined = malloc(length + 1);
if (joined == NULL) return 1;
strcpy(joined, first);
strcat(joined, second);
printf("[%s] length %zu\n", joined, strlen(joined));
free(joined);
return 0;
}program deferred_length
implicit none
character(len=:), allocatable :: joined
joined = "hello" // ", world"
print '(a, a, a, i0)', "[", joined, "] length ", len(joined)
joined = "shorter" ! reallocated to fit
print '(a, a, a, i0)', "[", joined, "] length ", len(joined)
end program deferred_lengthNo
malloc, no length arithmetic, no free — the assignment allocates, reallocates on the next assignment, and deallocates at end of scope. This is the modern replacement for the fixed character(len=256) buffer that older code passes everywhere, and it is worth reaching for whenever the length is not genuinely fixed.Arguments Arrive by Reference
Arguments Arrive by Reference, So There Is No &
C passes everything by value, so writing back to a caller’s variable means passing its address and dereferencing. Fortran passes by reference by default: assign to the dummy argument and the caller’s variable changes.
#include <stdio.h>
static void swap(int *left, int *right) {
int held = *left;
*left = *right;
*right = held;
}
int main(void) {
int first = 1, second = 2;
swap(&first, &second);
printf("%d %d\n", first, second);
return 0;
}program by_reference
implicit none
integer :: first = 1, second = 2
call swap(first, second)
print '(i0, 1x, i0)', first, second
contains
subroutine swap(left, right)
integer, intent(inout) :: left, right
integer :: held
held = left
left = right
right = held
end subroutine swap
end program by_referenceThere is no
& at the call site and no * in the body, which is convenient and also means you cannot tell from a call whether an argument will be modified — that is what intent, in the next row, is for. The historical consequence to be aware of: passing a literal to a procedure that modifies it used to corrupt the constant, which is why compilers now pass a copy for expressions and literals.intent Instead of const
C’s
const on a parameter says "I will not write through this pointer". Fortran’s intent says more: in means read-only, out means the value on entry is undefined and you must set it, and inout means both.#include <stdio.h>
/* const documents intent for the pointee — and only for the pointee. */
static int sum_of(const int *values, size_t count) {
int total = 0;
for (size_t index = 0; index < count; index++) {
total += values[index];
}
return total;
}
static void fill(int *values, size_t count) {
/* Nothing says the previous contents are meaningless. */
for (size_t index = 0; index < count; index++) {
values[index] = (int)index;
}
}
int main(void) {
int numbers[4];
fill(numbers, 4);
printf("%d\n", sum_of(numbers, 4));
return 0;
}program intents
implicit none
integer :: numbers(4)
call fill(numbers)
print '(i0)', sum_of(numbers)
contains
integer function sum_of(values)
integer, intent(in) :: values(:) ! assigning to values is an error
sum_of = sum(values)
end function sum_of
subroutine fill(values)
integer, intent(out) :: values(:) ! contents on entry are undefined
integer :: index
do index = 1, size(values)
values(index) = index - 1
end do
end subroutine fill
end program intentsintent(out) is the one with no C counterpart, and it is not just documentation: the compiler may skip copying the argument in, and it will warn if a path through the procedure leaves it unset. Combined with pure, which forbids side effects entirely, these are the declarations that let a Fortran compiler reorder and parallelize loops that a C compiler must assume might alias.Two Kinds of Procedure
C has one: a function, which may return
void. Fortran distinguishes a function, which produces a value and is used in an expression, from a subroutine, which does something and is invoked with call.#include <stdio.h>
static double area(double width, double height) {
return width * height;
}
static void report(double value) {
printf("value %.1f\n", value);
}
int main(void) {
/* Both are called the same way; only the use of the result differs. */
report(area(3.0, 4.0));
/* And a result can be ignored with no warning: */
area(1.0, 2.0);
return 0;
}program procedures
implicit none
call report(area(3.0, 4.0))
! area(1.0, 2.0) is a syntax error on its own: a function call
! is an expression, and an expression is not a statement.
contains
real function area(width, height)
real, intent(in) :: width, height
area = width * height ! assign to the function's own name
end function area
subroutine report(value)
real, intent(in) :: value
print '(a, f0.1)', "value ", value
end subroutine report
end program proceduresThe result is delivered by assigning to the function’s own name, which is unusual and worth reading twice;
function area(...) result(computed) lets you name it something else, and is the common style. The split matters because a function call cannot be a statement — so the C habit of calling something for its side effect and dropping the result has no spelling, and a procedure that does work is declared as the subroutine it is.Optional Arguments, and Calling by Name
C’s answer to an optional parameter is a second function or a sentinel value. Fortran marks the argument
optional and gives you present() to ask, and any argument can be supplied by name at the call.#include <stdio.h>
static void log_message(const char *text, const char *level) {
printf("[%s] %s\n", level, text);
}
/* The one-argument form is a separate function. */
static void log_simple(const char *text) {
log_message(text, "info");
}
int main(void) {
log_simple("started");
log_message("careful", "warn");
return 0;
}program optional_arguments
implicit none
call log_message("started")
call log_message("careful", "warn")
call log_message(level="debug", text="named arguments work too")
contains
subroutine log_message(text, level)
character(len=*), intent(in) :: text
character(len=*), intent(in), optional :: level
if (present(level)) then
print '(a, a, a, a)', "[", level, "] ", text
else
print '(a, a)', "[info] ", text
end if
end subroutine log_message
end program optional_argumentsA missing optional argument is genuinely absent rather than defaulted, which is why
present() exists — there is no way to declare a default value, so the "what to do when it is missing" logic lives in the body. Keyword arguments work for every procedure with an explicit interface, and they are what makes the intrinsic library readable: sum(numbers, mask=...) and reshape(source, shape, pad=...) both rely on them.elemental: Write It for One, Get It for Arrays
A C function operating on a scalar needs a wrapper loop to apply to an array. An
elemental procedure is defined for the scalar case and applies element-wise to arrays of any rank automatically.#include <stdio.h>
static double celsius_to_fahrenheit(double celsius) {
return celsius * 9.0 / 5.0 + 32.0;
}
int main(void) {
double readings[4] = { 0.0, 20.0, 37.0, 100.0 };
double converted[4];
for (int index = 0; index < 4; index++) {
converted[index] = celsius_to_fahrenheit(readings[index]);
}
for (int index = 0; index < 4; index++) {
printf("%.1f ", converted[index]);
}
printf("\n");
return 0;
}program elementals
implicit none
real :: readings(4) = [0.0, 20.0, 37.0, 100.0]
print '(4(f0.1, 1x))', celsius_to_fahrenheit(readings)
print '(a, f0.1)', "one value: ", celsius_to_fahrenheit(37.0)
contains
elemental real function celsius_to_fahrenheit(celsius)
real, intent(in) :: celsius
celsius_to_fahrenheit = celsius * 9.0 / 5.0 + 32.0
end function celsius_to_fahrenheit
end program elementalsThe same procedure now takes a scalar, a vector, or a three-dimensional array, with no overloading and no template.
elemental implies pure — no side effects, no I/O, all arguments intent(in) — which is precisely the contract that lets the compiler evaluate the elements in any order or in parallel. Every mathematical intrinsic in the language is declared this way.A Procedure Interface Is Checked, Not Trusted
A C prototype is a promise you make in a header; if the definition disagrees, the linker will happily connect them and the program misbehaves. When a Fortran procedure has an explicit interface — from a module or from
contains — the compiler checks every call against the real definition.#include <stdio.h>
/* If this prototype and the definition disagree, C's separate
compilation model means nothing notices until run time. */
static long factorial(int value);
static long factorial(int value) {
return value <= 1 ? 1L : value * factorial(value - 1);
}
int main(void) {
printf("%ld\n", factorial(10));
/* factorial(1.5, 2); would be caught here — but only because the
prototype is in the same file. */
return 0;
}program interfaces
implicit none
print '(i0)', factorial(10)
! factorial(1.5) is rejected at compile time: the interface is
! explicit, so the argument type is checked against the definition.
contains
recursive integer function factorial(value) result(computed)
integer, intent(in) :: value
if (value <= 1) then
computed = 1
else
computed = value * factorial(value - 1)
end if
end function factorial
end program interfacesRecursion needs no keyword since Fortran 2018 —
recursive is the default now and the word is optional, having been required before. The interface point is the important one: a procedure in a module is checked at every call site in every file that uses it, which is the checking C’s header files only approximate. A procedure with no interface at all — an external one, declared the old way — is trusted exactly as a C prototype is.allocatable Instead of malloc
allocatable Instead of malloc and free
An allocatable array is sized at run time and released automatically when it goes out of scope. There is no pointer, nothing to check for null, and nothing to forget — the compiler emits the deallocation.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int count = 5;
int *numbers = malloc((size_t)count * sizeof *numbers);
if (numbers == NULL) return 1;
for (int index = 0; index < count; index++) {
numbers[index] = index * index;
}
for (int index = 0; index < count; index++) {
printf("%d ", numbers[index]);
}
printf("\n");
free(numbers); /* exactly one, on every path out */
return 0;
}program allocation
implicit none
integer, allocatable :: numbers(:)
integer :: count = 5, index
allocate(numbers(count))
do index = 1, count
numbers(index) = (index - 1) ** 2
end do
print '(5(i0, 1x))', numbers
print '(a, l1)', "allocated: ", allocated(numbers)
! No deallocate needed: it happens at end of scope.
end program allocationYou can write
deallocate explicitly when you want the memory back sooner, and allocated() asks whether it currently holds anything — which is the null check, made into a question about the variable rather than about a pointer. An allocate that fails aborts the program unless you supply stat=, which is the same choice C makes you handle at every call.An Array Sized by an Argument
C99 added variable-length arrays and C11 made them optional, so portable C uses
malloc. Fortran has had automatic arrays since 1990: a local array whose bounds come from the arguments, allocated on entry and released on return.#include <stdio.h>
#include <stdlib.h>
static double mean_of(const double *values, size_t count) {
/* A scratch buffer of the argument's size needs malloc to be
portable — VLAs are optional in C11. */
double *scratch = malloc(count * sizeof *scratch);
if (scratch == NULL) return 0.0;
double total = 0.0;
for (size_t index = 0; index < count; index++) {
scratch[index] = values[index] * 2.0;
total += scratch[index];
}
free(scratch);
return total / (double)count;
}
int main(void) {
double readings[4] = { 1.0, 2.0, 3.0, 4.0 };
printf("%.2f\n", mean_of(readings, 4));
return 0;
}program automatic_arrays
implicit none
real :: readings(4) = [1.0, 2.0, 3.0, 4.0]
print '(f0.2)', mean_of(readings)
contains
real function mean_of(values)
real, intent(in) :: values(:)
real :: scratch(size(values)) ! automatic: sized on entry
scratch = values * 2.0
mean_of = sum(scratch) / size(values)
end function mean_of
end program automatic_arraysThe scratch array is typically placed on the stack, which makes it fast and also means a very large one can overflow it — the same trade as
alloca, with the same advice: use allocatable above a few tens of kilobytes. The whole-array assignment scratch = values * 2.0 is doing the loop as well, which is why the Fortran body is four lines against the C column’s dozen.A Fortran pointer Is an Alias, Not an Address
The word is the same and the thing is not. A Fortran
pointer is associated with a target rather than holding an address you can compute with — there is no arithmetic, no cast, and a variable must be declared a target before anything may point at it.#include <stdio.h>
int main(void) {
int numbers[5] = { 1, 2, 3, 4, 5 };
int *walk = numbers;
/* Arithmetic on the pointer is the whole idiom. */
walk += 2;
printf("third element %d\n", *walk);
/* And nothing stops this: */
walk += 10;
printf("now pointing somewhere undefined\n");
return 0;
}program pointers
implicit none
integer, target :: numbers(5) = [1, 2, 3, 4, 5]
integer, pointer :: view(:)
view => numbers(2:4) ! associated with a SECTION
print '(a, 3(i0, 1x))', "view is ", view
view(1) = 99 ! writes through to numbers
print '(a, 5(i0, 1x))', "numbers ", numbers
print '(a, l1)', "associated: ", associated(view)
view => null()
print '(a, l1)', "after null: ", associated(view)
end program pointersThe
=> operator is pointer assignment and = is ordinary assignment through the pointer, which is a distinction C makes with * and its absence. Because a pointer can be associated with an array section, it carries a stride as well as an address — so view => numbers(1:5:2) is a strided view, which C can only express as an index calculation. Pointers are rare in modern Fortran; allocatable covers most of what C uses pointers for.Modules Instead of Headers
A Module Instead of a Header and a Source File
There is no textual inclusion. A module is compiled once, producing an interface file the compiler reads when another unit
uses it — so there is no header to keep in step, no include guard, and no way for a declaration and a definition to disagree./* geometry.h */
#ifndef GEOMETRY_H
#define GEOMETRY_H
#define PI 3.14159265358979323846
double circle_area(double radius);
#endif
/* geometry.c */
#include "geometry.h"
double circle_area(double radius) { return PI * radius * radius; }
/* main.c */
#include <stdio.h>
#include "geometry.h"
int main(void) {
printf("%.4f\n", circle_area(1.0));
return 0;
}module geometry
implicit none
private
public :: circle_area, pi
real, parameter :: pi = 3.14159265358979323846
contains
real function circle_area(radius)
real, intent(in) :: radius
circle_area = pi * radius ** 2
end function circle_area
end module geometry
program main
use geometry, only: circle_area
implicit none
print '(f0.4)', circle_area(1.0)
end program mainThe
private line makes everything hidden by default and public lists the exceptions, which is the opposite of C’s default and the same effect as marking every internal function static. use geometry, only: circle_area imports one name — worth doing, because a bare use brings in everything and two modules exporting the same name collide. Both program units can live in one file, as here, or in separate ones compiled in dependency order.parameter Instead of #define
A
#define is text substituted before the compiler sees it: no type, no scope, and no existence at run time. A parameter is a real named constant with a type, and it can be an array or a derived type as easily as a number.#include <stdio.h>
#define MAXIMUM_RETRIES 3
#define GREETING "hello"
/* An array constant needs a static variable, not a macro. */
static const int WEIGHTS[3] = { 1, 2, 3 };
int main(void) {
printf("%d %s %d\n", MAXIMUM_RETRIES, GREETING, WEIGHTS[2]);
return 0;
}program constants
implicit none
integer, parameter :: maximum_retries = 3
character(len=*), parameter :: greeting = "hello"
integer, parameter :: weights(3) = [1, 2, 3]
print '(i0, 1x, a, 1x, i0)', maximum_retries, greeting, weights(3)
print '(a, i0)', "sum of weights ", sum(weights)
end program constantsThe
character(len=*) on a parameter means "as long as the value", so the length is inferred rather than counted by hand. Because a parameter is a value rather than text, it can be used where a constant expression is required — including as an array bound — and the compiler folds it away exactly as it would a macro, with none of the double-evaluation hazards macros bring.Derived Types Instead of struct
A Derived Type Instead of a struct
The same idea with different punctuation: fields are declared between
type and end type, and reached with % rather than .. Assignment copies the whole thing, exactly as it does in C.#include <stdio.h>
struct Point {
double x;
double y;
};
int main(void) {
struct Point origin = { 0.0, 0.0 };
struct Point corner = { 3.0, 4.0 };
struct Point copy = corner; /* a real copy */
copy.x = 99.0;
printf("corner %4.1f %4.1f\n", corner.x, corner.y);
printf("copy %4.1f %4.1f\n", copy.x, copy.y);
printf("origin %4.1f %4.1f\n", origin.x, origin.y);
return 0;
}program derived_types
implicit none
type :: point
real :: x = 0.0 ! default initializer
real :: y = 0.0
end type point
type(point) :: origin, corner, copy
corner = point(3.0, 4.0) ! the structure constructor
copy = corner ! a real copy
copy%x = 99.0
print '(a, f4.1, 1x, f4.1)', "corner ", corner%x, corner%y
print '(a, f4.1, 1x, f4.1)', "copy ", copy%x, copy%y
print '(a, f4.1, 1x, f4.1)', "origin ", origin%x, origin%y
end program derived_typesThe default initializers mean
origin is (0.0, 0.0) without being assigned, which C can only manage with an explicit = {0}. point(3.0, 4.0) is the structure constructor, and it accepts keywords too — point(y=4.0, x=3.0). The one thing to know before interop: the standard does not fix the field order in memory unless the type is declared bind(c), which the last section covers.Procedures Attached to a Type
The C pattern is a struct and a family of functions taking a pointer to it. A type-bound procedure moves the function inside the type declaration and makes the receiver an explicit dummy argument called whatever you like.
#include <stdio.h>
struct Rectangle { double width; double height; };
static double rectangle_area(const struct Rectangle *self) {
return self->width * self->height;
}
static void rectangle_scale(struct Rectangle *self, double factor) {
self->width *= factor;
self->height *= factor;
}
int main(void) {
struct Rectangle shape = { 3.0, 4.0 };
printf("area %.1f\n", rectangle_area(&shape));
rectangle_scale(&shape, 2.0);
printf("scaled area %.1f\n", rectangle_area(&shape));
return 0;
}module shapes
implicit none
private
public :: rectangle
type :: rectangle
real :: width = 0.0, height = 0.0
contains
procedure :: area
procedure :: scale
end type rectangle
contains
real function area(self)
class(rectangle), intent(in) :: self
area = self%width * self%height
end function area
subroutine scale(self, factor)
class(rectangle), intent(inout) :: self
real, intent(in) :: factor
self%width = self%width * factor
self%height = self%height * factor
end subroutine scale
end module shapes
program main
use shapes, only: rectangle
implicit none
type(rectangle) :: shape
shape = rectangle(3.0, 4.0)
print '(a, f0.1)', "area ", shape%area()
call shape%scale(2.0)
print '(a, f0.1)', "scaled area ", shape%area()
end program mainNote
class(rectangle) rather than type(rectangle) on the receiver: class means "this type or anything extending it", which is what makes the procedure inheritable and overridable. That is the whole object system — extends for inheritance, deferred for an abstract method — and it is exactly what the C column builds by hand out of a struct of function pointers.Loops and select case
The do Loop Counts, and the Count Is Fixed on Entry
A C
for loop re-evaluates its condition every pass, so changing the bound inside the body changes the loop. A Fortran do loop computes its trip count once, before the first iteration.#include <stdio.h>
int main(void) {
int limit = 3;
for (int index = 1; index <= limit; index++) {
printf("%d ", index);
limit = 5; /* the loop notices */
}
printf("\n");
for (int index = 10; index >= 1; index -= 3) {
printf("%d ", index);
}
printf("\n");
return 0;
}program loops
implicit none
integer :: index, limit
limit = 3
do index = 1, limit
write(*, '(i0, 1x)', advance="no") index
limit = 5 ! the loop does NOT notice
end do
print *
do index = 10, 1, -3
write(*, '(i0, 1x)', advance="no") index
end do
print *
index = 0
do while (index < 3)
index = index + 1
end do
print '(a, i0)', "after do while: ", index
end program loopsThe two columns print different first lines on purpose: C runs five times because the bound is re-read, Fortran three times because the trip count was fixed at entry. That fixed count is what allows the loop to be parallelized or unrolled without analysis, and it also means modifying the loop variable inside the body is forbidden rather than merely unwise.
exit and cycle are break and continue, and both can name a labelled loop to leave a nested one.select case Does Not Fall Through
C’s
switch falls through unless you write break, which is the source of a well-known family of bugs. Fortran’s select case runs exactly one block, and its labels can be ranges and lists.#include <stdio.h>
static const char *describe(int code) {
switch (code) {
case 1:
case 2:
return "low";
case 3: case 4: case 5:
return "medium";
default:
return "high";
}
}
int main(void) {
printf("%s %s %s\n", describe(1), describe(4), describe(9));
/* The classic bug: no break. */
int total = 0;
switch (2) {
case 2: total += 1;
case 3: total += 10;
default: total += 100;
}
printf("fell through to %d\n", total);
return 0;
}program cases
implicit none
print '(a, 1x, a, 1x, a)', trim(describe(1)), trim(describe(4)), &
trim(describe(9))
print '(a)', "no fallthrough is possible"
contains
function describe(code) result(text)
integer, intent(in) :: code
character(len=6) :: text
select case (code)
case (1, 2)
text = "low"
case (3:5) ! a range
text = "medium"
case default
text = "high"
end select
end function describe
end program casesThe range form
case (3:5) and the open forms case (:0) and case (10:) are what C makes you spell as a chain of ifs or a run of empty labels. The selector may be an integer, a character or a logical, but not a real — because equality on floating point is not something the language wants to encourage.where: An if for Whole Arrays
Applying a condition element by element is a loop with an
if in it. where is that construct as a single statement: the mask selects which elements are assigned, and the shape does the iterating.#include <stdio.h>
int main(void) {
double values[6] = { -3.0, 1.0, -1.5, 4.0, 0.0, -8.0 };
double clamped[6];
for (int index = 0; index < 6; index++) {
if (values[index] < 0.0) {
clamped[index] = 0.0;
} else {
clamped[index] = values[index];
}
}
for (int index = 0; index < 6; index++) {
printf("%5.1f", clamped[index]);
}
printf("\n");
return 0;
}program masks
implicit none
real :: values(6) = [-3.0, 1.0, -1.5, 4.0, 0.0, -8.0]
real :: clamped(6)
where (values < 0.0)
clamped = 0.0
elsewhere
clamped = values
end where
print '(6(f5.1))', clamped
! merge is the same idea as an expression:
print '(6(f5.1))', merge(0.0, values, values < 0.0)
print '(a, i0)', "how many negative: ", count(values < 0.0)
end program masksThe mask
values < 0.0 is itself an array — of logical, the same shape as values — which is why it can be counted, combined with .and., or handed to sum as its mask= argument. merge is the expression form and works anywhere a value is wanted, which makes it the element-wise ?: that C has no equivalent of.do concurrent: A Promise About Independence
A C compiler must assume two pointers might alias, which is why
restrict exists and why so many loops go unvectorized. do concurrent is the programmer asserting that the iterations do not depend on each other and may run in any order.#include <stdio.h>
/* restrict is the assertion that these do not overlap, which is what
lets the compiler vectorize the loop. */
static void scale(double * restrict output,
const double * restrict input,
size_t count, double factor) {
for (size_t index = 0; index < count; index++) {
output[index] = input[index] * factor;
}
}
int main(void) {
double input[5] = { 1.0, 2.0, 3.0, 4.0, 5.0 };
double output[5];
scale(output, input, 5, 2.5);
for (int index = 0; index < 5; index++) {
printf("%.1f ", output[index]);
}
printf("\n");
return 0;
}program concurrency
implicit none
real :: input(5) = [1.0, 2.0, 3.0, 4.0, 5.0]
real :: output(5)
integer :: index
do concurrent (index = 1:5)
output(index) = input(index) * 2.5
end do
print '(5(f0.1, 1x))', output
! Which is also just:
print '(5(f0.1, 1x))', input * 2.5
end program concurrencyThe aliasing question that
restrict answers barely arises here — Fortran’s argument rules already forbid a procedure from being passed the same array twice in a way that lets it alias, which is a large part of why Fortran has historically outrun C on numerical loops. do concurrent adds the loop-level promise, and with the right flag a compiler will offload it to threads or a GPU. The last line is the reminder that most such loops did not need to be written at all.Numeric Facts the Language Knows
The Language Knows Its Own Limits
C puts these facts in
<limits.h> and <float.h> as macros with fixed names, one per type. Fortran makes them inquiry functions you apply to a variable, so the same expression works whatever kind that variable has.#include <stdio.h>
#include <limits.h>
#include <float.h>
int main(void) {
printf("int max %d\n", INT_MAX);
printf("double eps %.17g\n", DBL_EPSILON);
printf("double max %g\n", DBL_MAX);
printf("double dig %d\n", DBL_DIG);
return 0;
}program inquiry
use, intrinsic :: iso_fortran_env, only: int32, real64
implicit none
integer(int32) :: whole = 0
real(real64) :: fraction = 0.0_real64
print '(a, i0)', "int max ", huge(whole)
print '(a, es23.16e2)', "double eps ", epsilon(fraction)
print '(a, es13.5e3)', "double max ", huge(fraction)
print '(a, i0)', "double dig ", precision(fraction)
print '(a, i0)', "radix ", radix(fraction)
end program inquiryThe argument is only there to say which kind you are asking about — its value is ignored, so
huge(0_int32) works as well as a variable. That design is what makes generic numerical code possible: a procedure written for real(wp) can use epsilon(x) and get the right answer for whatever wp turns out to be, where the C equivalent needs a different macro per type.Infinity and NaN, With a Standard Module
Both languages defer to IEEE 754 for floating point, so the values and their behavior are identical. Fortran wraps the questions in an intrinsic module rather than in
<math.h> macros.#include <stdio.h>
#include <math.h>
int main(void) {
double zero = 0.0;
double infinite = 1.0 / zero;
double undefined = zero / zero;
printf("isinf %d\n", isinf(infinite));
printf("isnan %d\n", isnan(undefined));
printf("nan == nan %d\n", undefined == undefined);
printf("value %f\n", infinite);
return 0;
}program ieee
use, intrinsic :: ieee_arithmetic
implicit none
real :: zero = 0.0, infinite, undefined
infinite = ieee_value(zero, ieee_positive_inf)
undefined = ieee_value(zero, ieee_quiet_nan)
print '(a, l1)', "is_finite ", ieee_is_finite(infinite)
print '(a, l1)', "is_nan ", ieee_is_nan(undefined)
print '(a, l1)', "nan == nan ", undefined == undefined
print '(a, l1)', "supported ", ieee_support_datatype(zero)
end program ieeeThe Fortran column constructs the values with
ieee_value rather than by dividing by zero, because dividing by zero is not required to produce infinity — a conforming implementation may stop the program instead, and the ieee_arithmetic module is how you turn that behavior on and off. Note the first line inverts the C question: there is ieee_is_finite rather than an "is infinite", which is the same information the other way up.Formatted and List-Directed I/O
List-Directed I/O: Let the Compiler Choose the Format
The
* where a format would go means "use a sensible default for whatever these values are". It is the fastest way to print something while debugging, and the reason its output looks slightly different from every compiler.#include <stdio.h>
int main(void) {
int count = 42;
double ratio = 2.5;
const char *label = "answer";
/* Every value needs its own conversion, chosen by you. */
printf("%d %f %s\n", count, ratio, label);
return 0;
}program list_directed
implicit none
integer :: count = 42
real :: ratio = 2.5
character(len=6) :: label = "answer"
integer :: numbers(3) = [1, 2, 3]
print *, count, ratio, label
print *, numbers ! a whole array, no loop
end program list_directedThe leading blank on each line is not an accident — it is the historical carriage-control character, and it is why every other example on this page uses an explicit format. What list-directed output does give you is types you never have to name and arrays that print themselves, which is exactly what you want at three in the morning and never what you want in a file somebody else will parse.
An Internal Write Is sprintf
Formatting into a string rather than to a device is spelled by naming a character variable where the unit number would go. That is
sprintf, with the buffer’s size fixed by its declaration rather than passed.#include <stdio.h>
int main(void) {
char line[64];
int count = 42;
double ratio = 2.5;
snprintf(line, sizeof line, "count=%d ratio=%.2f", count, ratio);
printf("[%s]\n", line);
/* Parsing back: sscanf */
int recovered = 0;
sscanf("count=42", "count=%d", &recovered);
printf("recovered %d\n", recovered);
return 0;
}program internal_io
implicit none
character(len=64) :: line
integer :: count = 42, recovered
real :: ratio = 2.5
write(line, '(a, i0, a, f0.2)') "count=", count, " ratio=", ratio
print '(a, a, a)', "[", trim(line), "]"
! Parsing back: an internal READ. The unit must be a character
! VARIABLE, not a literal, so the text goes into one first.
line = "42"
read(line, '(i2)') recovered
print '(a, i0)', "recovered ", recovered
end program internal_ioNote that the internal read takes a character variable, never a literal —
read("42", …) is rejected, because the unit is where the data comes from and a literal cannot be one. The buffer cannot overflow, because a write that does not fit is an error rather than a silent overrun — which is snprintf’s truncation made into a diagnosis. The trim in the print is the blank-padding tax from the strings section: line is 64 characters no matter how few were written. Internal reads are how you parse a number out of text without sscanf’s format-string hazards.Files: open, write, read, close
The same four operations, with keyword arguments instead of a mode string and a small integer unit number instead of a
FILE *. The error handling is a status variable rather than a null return.#include <stdio.h>
int main(void) {
const char *path = "/tmp/c-fortran-io.txt";
FILE *handle = fopen(path, "w");
if (handle == NULL) return 1;
fprintf(handle, "first line\n");
fprintf(handle, "second line\n");
fclose(handle);
handle = fopen(path, "r");
if (handle == NULL) return 1;
char line[64];
while (fgets(line, sizeof line, handle) != NULL) {
printf("read: %s", line);
}
fclose(handle);
remove(path);
return 0;
}program file_io
implicit none
character(len=*), parameter :: path = "/tmp/c-fortran-io.txt"
character(len=64) :: line
integer :: unit, status
open(newunit=unit, file=path, status="replace", action="write")
write(unit, '(a)') "first line"
write(unit, '(a)') "second line"
close(unit)
open(newunit=unit, file=path, status="old", action="read")
do
read(unit, '(a)', iostat=status) line
if (status /= 0) exit
print '(a, a)', "read: ", trim(line)
end do
close(unit, status="delete")
end program file_ionewunit= asks the runtime for a free unit number, which is what saves you from the older practice of picking one and hoping — units 5 and 6 are standard input and output by convention, and colliding with them is an old and reliable bug. iostat= is the pattern to internalize: it turns what would otherwise abort the program into a status you test, and a negative value specifically means end of file.Where They Meet: iso_c_binding
iso_c_binding: Types That Match Exactly
This is the destination the page has been pointing at. The
iso_c_binding module names a kind for each C type — c_int, c_double, c_size_t — that the compiler guarantees matches the platform’s C compiler, whatever that turns out to be.#include <stdio.h>
#include <stddef.h>
int main(void) {
printf("int %zu\n", sizeof(int));
printf("long %zu\n", sizeof(long));
printf("double %zu\n", sizeof(double));
printf("size_t %zu\n", sizeof(size_t));
printf("pointer %zu\n", sizeof(void *));
return 0;
}program c_types
use, intrinsic :: iso_c_binding
implicit none
integer(c_int) :: an_int
integer(c_long) :: a_long
real(c_double) :: a_double
integer(c_size_t) :: a_size
type(c_ptr) :: a_pointer
print '(a, i0)', "int ", storage_size(an_int) / 8
print '(a, i0)', "long ", storage_size(a_long) / 8
print '(a, i0)', "double ", storage_size(a_double) / 8
print '(a, i0)', "size_t ", storage_size(a_size) / 8
print '(a, i0)', "pointer ", storage_size(a_pointer) / 8
end program c_typesThe two columns print the same five numbers, which is the guarantee doing its job. Two kinds are worth singling out:
c_char for text and c_ptr, which is an opaque handle rather than something you can dereference — turning one into a usable Fortran array takes c_f_pointer, two rows down. A kind that the platform cannot provide comes back negative, so c_long_double < 0 is how you find out it is unavailable.Calling a C Function With bind(c)
You declare the C function’s signature in an
interface block, tag it bind(c) with the symbol name, and call it like any other procedure. No wrapper, no name-mangling guesswork, and the compiler checks every call against the declaration.#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void) {
/* The functions on the other side are these, from the C library
that both languages already link against. */
printf("strlen %zu\n", strlen("hello"));
printf("abs %d\n", abs(-5));
return 0;
}program calling_c
use, intrinsic :: iso_c_binding
implicit none
interface
function c_strlen(text) bind(c, name="strlen") result(length)
import :: c_char, c_size_t
character(kind=c_char), intent(in) :: text(*)
integer(c_size_t) :: length
end function c_strlen
function c_abs(value) bind(c, name="abs") result(magnitude)
import :: c_int
integer(c_int), value :: value
integer(c_int) :: magnitude
end function c_abs
end interface
print '(a, i0)', "strlen ", c_strlen("hello" // c_null_char)
print '(a, i0)', "abs ", c_abs(-5_c_int)
end program calling_cThree details do all the work.
value on the abs argument is essential — Fortran passes by reference by default, and C expects the integer itself, so without it you would be handing abs a pointer. c_null_char is appended by hand because a C string is terminated and a Fortran one is not. And import is needed because an interface block does not inherit the enclosing scope’s use.A Derived Type That Matches a C struct
The standard does not fix how a derived type is laid out — the compiler may reorder or pad as it likes. Declaring it
bind(c) forces the C layout, and restricts the fields to types that C has.#include <stdio.h>
#include <stddef.h>
struct Header {
int kind;
double weight;
int flags;
};
int main(void) {
struct Header header = { 7, 2.5, 3 };
printf("sizeof %zu\n", sizeof(struct Header));
printf("kind %zu\n", offsetof(struct Header, kind));
printf("weight %zu\n", offsetof(struct Header, weight));
printf("flags %zu\n", offsetof(struct Header, flags));
printf("values %d %.1f %d\n", header.kind, header.weight, header.flags);
return 0;
}program c_structs
use, intrinsic :: iso_c_binding
implicit none
type, bind(c) :: header
integer(c_int) :: kind
real(c_double) :: weight
integer(c_int) :: flags
end type header
type(header) :: item
item = header(7, 2.5_c_double, 3)
print '(a, i0)', "sizeof ", storage_size(item) / 8
print '(a, i0, 1x, f0.1, 1x, i0)', "values ", item%kind, &
item%weight, item%flags
end program c_structsBoth columns report 24 bytes: four for
kind, four of padding so weight lands on an eight-byte boundary, eight for weight, four for flags and four more of tail padding. Getting the same number from both sides is the point — without bind(c) the Fortran compiler is free to lay it out differently and nothing would tell you. Only the C column can print the individual offsets: there is no offsetof in Fortran, because a program is not supposed to know where a field sits, and the total size is the whole of what you can check from this side. A bind(c) type may not have allocatable or pointer components, default initializers, or type-bound procedures.Turning a C Pointer Into a Fortran Array
A
c_ptr is opaque; you cannot subscript it. c_f_pointer takes one, plus a shape, and gives back a Fortran pointer array that views the same memory — which is how a buffer allocated on the C side becomes something you can write sum() over.#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t count = 5;
double *block = malloc(count * sizeof *block);
if (block == NULL) return 1;
for (size_t index = 0; index < count; index++) {
block[index] = (double)index * 1.5;
}
double total = 0.0;
for (size_t index = 0; index < count; index++) {
total += block[index];
}
printf("total %.1f\n", total);
free(block);
return 0;
}program c_pointers
use, intrinsic :: iso_c_binding
implicit none
interface
function c_malloc(size) bind(c, name="malloc") result(address)
import :: c_size_t, c_ptr
integer(c_size_t), value :: size
type(c_ptr) :: address
end function c_malloc
subroutine c_free(address) bind(c, name="free")
import :: c_ptr
type(c_ptr), value :: address
end subroutine c_free
end interface
integer, parameter :: count = 5
type(c_ptr) :: address
real(c_double), pointer :: block(:)
integer :: index
address = c_malloc(int(count * c_sizeof(1.0_c_double), c_size_t))
call c_f_pointer(address, block, [count])
do index = 1, count
block(index) = real(index - 1, c_double) * 1.5_c_double
end do
print '(a, f0.1)', "total ", sum(block)
call c_free(address)
end program c_pointersOnce
c_f_pointer has run, block is an ordinary Fortran array: sum works on it, so do sections and whole-array assignment, and it is 1-based like everything else. It does not own the memory — the c_free at the end is still yours to write, and this is the one place on the page where the C ownership discipline comes back in full. c_loc goes the other way, handing a Fortran target’s address to C.The Bug That Catches Everyone: Subscript Order
When the two languages share a two-dimensional array, they are looking at the same bytes with opposite conventions. Fortran’s
a(i,j) and C’s a[j][i] name the same element — and writing a[i][j] instead reads a transposed matrix, silently.#include <stdio.h>
int main(void) {
/* Six doubles as a flat block, exactly as they would arrive
from a Fortran caller holding a(2,3). */
double flat[6] = { 1, 2, 3, 4, 5, 6 };
/* Fortran's a(row, column) with 2 rows: the element at
(row, column) lives at flat[(column-1)*2 + (row-1)]. */
for (int row = 1; row <= 2; row++) {
for (int column = 1; column <= 3; column++) {
printf("%.0f ", flat[(column - 1) * 2 + (row - 1)]);
}
printf("\n");
}
return 0;
}program order_trap
implicit none
real :: a(2, 3)
integer :: row, column
! The same six values in the same six memory slots.
a = reshape([1., 2., 3., 4., 5., 6.], [2, 3])
do row = 1, 2
do column = 1, 3
write(*, '(i0, 1x)', advance="no") nint(a(row, column))
end do
print *
end do
end program order_trapBoth columns print the same two rows, and the C one had to do the index arithmetic by hand to get there — which is exactly what a real interop layer does. The three practical rules: declare the C side as a flat array and index it yourself, or as
double a[3][2] with the subscripts reversed; never pass a non-contiguous Fortran array section to C without letting the compiler make a contiguous copy; and remember that C’s a[j][i] is not a transpose, it is the same matrix read correctly.