C arrays
last modified November 14, 2026
An array is a fixed-size sequence of elements of one type. The elements are stored next to each other, so the address of any element follows from the address of the first one and its index. C neither stores the length with the array nor checks an index before access, which keeps the language small and makes bounds discipline the programmer's job. This tutorial covers declaration, initialisation, iteration, two-dimensional arrays, variable-length arrays and the point where an array becomes a pointer.
Declaring and initialising an array
An array declaration gives a type, a name and a size in brackets, as in
int scores[5];. The size is part of the type and must be a constant
expression when the array has static storage or when the program is compiled as
C89. An automatic array may be left uninitialised, but its elements then hold
indeterminate values, so the first example uses an initialiser list. The list
supplies the elements in order, and the compiler counts them for the length.
#include <stdio.h>
int main(void) {
int scores[5] = { 72, 85, 91, 64, 78 };
size_t len = sizeof scores / sizeof scores[0];
printf("The array holds %zu ints (%zu bytes)\n", len, sizeof scores);
for (size_t i = 0; i < len; i++) {
printf("scores[%zu] = %d\n", i, scores[i]);
}
return 0;
}
The valid indices of scores are 0 through 4, not 1 through 5. The
expression sizeof scores gives the total number of bytes, 20 on this
platform, and dividing it by sizeof scores[0] gives the number of
elements. That division is the usual way to obtain the length of a local array,
and size_t with the %zu conversion is the matching
type for the result.
$ gcc -std=c17 -Wall -Wextra -o array_basics array_basics.c $ ./array_basics The array holds 5 ints (20 bytes) scores[0] = 72 scores[1] = 85 scores[2] = 91 scores[3] = 64 scores[4] = 78
Initialisation forms
C has several initialiser forms and they differ in what happens to the elements
that are not mentioned. An array initialiser is a brace-enclosed list, and a
list with fewer values than the array has elements fills the rest with zeros. An
empty pair of brackets lets the compiler infer the length from the initialiser.
A designated initialiser names an index with [index] = value, a C99
feature that is useful when only a few positions are non-zero.
#include <stdio.h>
int main(void) {
int zeros[4] = { 0 };
int partial[5] = { 1, 2 };
int inferred[] = { 3, 4, 5, 6 };
int designated[6] = { [1] = 10, [4] = 20 };
printf("zeros: ");
for (size_t i = 0; i < 4; i++) {
printf("%d ", zeros[i]);
}
printf("\npartial: ");
for (size_t i = 0; i < 5; i++) {
printf("%d ", partial[i]);
}
printf("\ninferred: ");
for (size_t i = 0; i < sizeof inferred / sizeof inferred[0]; i++) {
printf("%d ", inferred[i]);
}
printf("\ndesignated: ");
for (size_t i = 0; i < 6; i++) {
printf("%d ", designated[i]);
}
printf("\n");
return 0;
}
The first line prints four zeros because { 0 } initialises the
first element and zero-fills the rest. The partial array keeps 1 and 2 and adds
three zeros, while the inferred array gets exactly four elements. In the
designated array, positions 1 and 4 hold 10 and 20 and every other position is
zero. Without an initialiser, only objects with static storage duration are
guaranteed to start at zero; an automatic array such as these would contain
whatever the previous stack contents happened to be.
$ gcc -std=c17 -Wall -Wextra -o array_init array_init.c $ ./array_init zeros: 0 0 0 0 partial: 1 2 0 0 0 inferred: 3 4 5 6 designated: 0 10 0 0 20 0
Iterating and aggregate values
A loop visits the elements by index, starting at 0 and stopping before the
length. The same pattern computes a sum, a count or an average; the only change
is the operation inside the body. The next program works with doubles, prints
each reading and then reports the count, the sum and the mean. Floating-point
output uses %.2f to keep the result readable.
#include <stdio.h>
int main(void) {
double readings[] = { 12.5, 9.0, 15.25, 7.75, 10.0 };
size_t len = sizeof readings / sizeof readings[0];
double sum = 0.0;
for (size_t i = 0; i < len; i++) {
printf("reading %zu: %.2f\n", i + 1, readings[i]);
sum += readings[i];
}
printf("count: %zu\n", len);
printf("sum: %.2f\n", sum);
printf("mean: %.2f\n", sum / (double) len);
return 0;
}
The sum grows by one element per iteration, and the mean divides it by the
number of readings. The cast (double) len is not strictly needed
here because sum is already a double, but it documents that the
division is floating-point rather than integer division. The loop bound
i < len is the important part: using i <= len
would read one element past the end.
$ gcc -std=c17 -Wall -Wextra -o array_sum array_sum.c $ ./array_sum reading 1: 12.50 reading 2: 9.00 reading 3: 15.25 reading 4: 7.75 reading 5: 10.00 count: 5 sum: 54.50 mean: 10.90
Two-dimensional arrays
A two-dimensional array is an array whose elements are themselves arrays.
int grid[3][4] therefore has three rows, each of which is an array
of four ints. The rows are stored one after another, so all four elements of row
0 come first, then all four of row 1, and so on; this layout is called
row-major order. Two nested loops are the natural way to visit the elements,
with the row index choosing a row and the column index choosing an element
inside it.
#include <stdio.h>
int main(void) {
int grid[3][4] = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
int total = 0;
for (size_t row = 0; row < 3; row++) {
int row_sum = 0;
for (size_t col = 0; col < 4; col++) {
printf("%3d", grid[row][col]);
row_sum += grid[row][col];
}
printf(" | %d\n", row_sum);
total += row_sum;
}
printf("total: %d\n", total);
printf("grid[1][2] = %d\n", grid[1][2]);
return 0;
}
The inner loop prints one row and accumulates its sum, and the outer loop adds
that sum to the total. The first row sums to 10, the second to 26 and the third
to 42, for a total of 78. grid[1][2] selects row 1 and then column
2, which holds 7; there is no separate two-dimensional storage, only the
row-major arrangement of the twelve ints.
$ gcc -std=c17 -Wall -Wextra -o array_2d array_2d.c $ ./array_2d 1 2 3 4 | 10 5 6 7 8 | 26 9 10 11 12 | 42 total: 78 grid[1][2] = 7
Variable-length arrays
C99 added variable-length arrays, whose size may be computed at run time. The
array still has automatic storage duration and is usually placed on the stack
when its declaration is reached, but sizeof is evaluated at run
time and the compiler inserts the necessary bookkeeping. C11 made variable-length
arrays optional, and C17 kept them optional; GCC supports them in the modes used
here. The size is not checked, so a very large value can overflow the stack.
#include <stdio.h>
int main(void) {
int n = 6;
int squares[n];
for (int i = 0; i < n; i++) {
squares[i] = i * i;
}
printf("n = %d, sizeof squares = %zu bytes\n", n, sizeof squares);
printf("last square = %d\n", squares[n - 1]);
return 0;
}
The value n is an ordinary automatic variable, not a constant
expression, so squares is a variable-length array. With six
elements sizeof squares is 24 bytes on this platform, and the last
valid index is n - 1, which holds 25. A fixed array would have to
name a constant size and would waste space when the needed length is smaller.
$ gcc -std=c17 -Wall -Wextra -o array_vla array_vla.c $ ./array_vla n = 6, sizeof squares = 24 bytes last square = 25
Arrays and pointers
In most expressions an array name is converted to a pointer to its first
element. The main exceptions are sizeof and the address operator
&, which see the whole array. The conversion is
why a function parameter written as int values[] is adjusted to
int *values: the function receives an address, not a copy of the
elements, and it cannot recover the length with sizeof. The caller
must pass the length separately.
#include <stdio.h>
int sum(const int *values, size_t len) {
int total = 0;
printf("sizeof values in sum(): %zu\n", sizeof values);
for (size_t i = 0; i < len; i++) {
total += values[i];
}
return total;
}
int main(void) {
int values[] = { 2, 4, 6, 8, 10 };
size_t len = sizeof values / sizeof values[0];
printf("sizeof values in main(): %zu\n", sizeof values);
printf("sum: %d\n", sum(values, len));
return 0;
}
In main, sizeof values is 20 because the compiler
still sees the complete array. Inside sum, the same expression is
the size of a pointer, 8 bytes on this 64-bit platform. The function still adds
the five values correctly because it receives the address of the first element
and the explicit length. Marking the parameter const int * promises
that the function will not modify the caller's data.
$ gcc -std=c17 -Wall -Wextra -o array_decay array_decay.c $ ./array_decay sizeof values in main(): 20 sizeof values in sum(): 8 sum: 30
Out-of-bounds access
The valid indices of an array with n elements are 0 through
n - 1. C performs no bounds check on an ordinary subscript, so an
index outside that range reads or writes memory that belongs to some other
object. The result is undefined behavior: it may print a plausible value, crash,
or corrupt an unrelated variable. The next program contains the classic
off-by-one error i <= 3 for an array of three elements.
#include <stdio.h>
int main(void) {
int values[3] = { 10, 20, 30 };
for (size_t i = 0; i <= 3; i++) {
printf("values[%zu] = %d\n", i, values[i]);
}
return 0;
}
The first three iterations are valid. At i == 3 the subscript is
outside the array, and the bounds sanitizer stops the program with the message
shown below. The option -fsanitize=bounds adds checks for arrays
whose bounds are known, and -fno-sanitize-recover=all turns the
first violation into an abort instead of continuing. AddressSanitizer
(-fsanitize=address) is the broader tool for stack and heap
overflows and is covered with dynamic memory.
$ gcc -std=c17 -Wall -Wextra -fsanitize=bounds -fno-sanitize-recover=all -o array_bounds array_bounds.c $ ./array_bounds array_bounds.c:8:47: runtime error: index 3 out of bounds for type 'int [3]'
Source
cppreference: array declaration
cppreference: array initialisation
cppreference: sizeof operator
GCC: instrumentation options
This tutorial covered arrays in C: declaration and initialisation, the length
computed with sizeof, iteration and aggregation, row-major
two-dimensional arrays, variable-length arrays, the conversion of an array to a
pointer when it is passed to a function, and the undefined behavior of an
out-of-bounds access. The C functions tutorial
shows how to combine arrays with parameters and return values, and
C malloc and free explains how to allocate a
sequence whose size is not known until run time.
Author
List all C tutorials.