ZetCode

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.

array_basics.c
#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.

array_init.c
#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.

array_sum.c
#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.

array_2d.c
#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.

array_vla.c
#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.

array_decay.c
#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.

array_bounds.c
#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

My name is Jan Bodnar, and I'm a dedicated programmer with a deep passion for coding. Since 2007, I've been sharing my expertise through over 1,400 articles and 8 e-books. With more than a decade of teaching experience, I strive to make programming accessible and engaging.

List all C tutorials.