Skip to main content
@shmVirus

Arrays

Array declaration, initialization, indexing, traversal, updates, multidimensional arrays, parameters, variable-length arrays, and boundary safety.

An array groups a fixed number of elements of one type into one contiguous object. Arrays are C’s fundamental representation for tables, buffers, and sequences whose storage is known where the array is created. Their power comes with a strict contract: C does not check indices at runtime, so every access must be justified by a bound.

This chapter teaches the C language mechanics of arrays. Dynamic growth and array-based abstract data structures belong to the Data Structures course.

Array Declaration

int temperatures[7];

This declares one object containing seven int elements. Valid indices are 0 through 6.

index:          0     1     2     3     4     5     6
element:      [ ? ] [ ? ] [ ? ] [ ? ] [ ? ] [ ? ] [ ? ]

Because temperatures is an uninitialized automatic array, each element has an indeterminate value. Assign before reading.

An array type includes its element type and length. int[7] and int[8] are different types. The length must be greater than zero in a standard fixed-length array declaration.

Prefer size_t for sizes and indices because it is the unsigned type returned by sizeof and is designed to represent object sizes.

Array Initialization

An initializer supplies element values in index order:

int scores[5] = {84, 91, 77, 88, 95};

When the initializer determines the length, omit the repeated number:

int scores[] = {84, 91, 77, 88, 95};

The compiler counts five elements. This avoids a stale bound when initializers are edited.

Partial Initialization

Missing elements are initialized as if with zero:

int counts[8] = {1, 2};

produces:

[1, 2, 0, 0, 0, 0, 0, 0]

The idiom

int counts[8] = {0};

initializes every element to zero. It is not a special “zero the array” syntax; it explicitly initializes the first element to zero and the remaining elements receive zero initialization.

Designated initializers can target indices:

int status_by_code[8] = {
    [1] = 200,
    [4] = 404,
    [5] = 500
};

Unmentioned elements become zero. Designators are helpful when indices have named meanings, especially with enumeration constants.

Array Indexing

The expression array[index] designates an element:

int values[] = {10, 20, 30};
int middle = values[1]; /* 20 */

Indexing is defined in terms of pointer arithmetic: values[i] means *(values + i). That equivalence explains both contiguous layout and the absence of bound checks; Chapter 9 develops the pointer model.

An index is valid only when it identifies an existing element. Forming or dereferencing an out-of-range element access produces undefined behaviour:

int values[3] = {10, 20, 30};
printf("%d\n", values[3]); /* undefined: last valid index is 2 */

Undefined behaviour may appear to print a nearby value, crash, or corrupt unrelated state. A successful test run does not make the access valid.

Element Count

For an actual array object in the same scope:

int values[] = {10, 20, 30, 40};
size_t count = sizeof values / sizeof values[0];

sizeof values is total array storage, and sizeof values[0] is one element’s storage. The ratio is four.

A common macro is:

#define ARRAY_COUNT(array) (sizeof(array) / sizeof((array)[0]))

It works only when its argument is an actual array, not a pointer. Macros cannot reliably enforce that requirement on every C implementation, so use it only where the argument is visibly an array.

Array Traversal

The standard full traversal uses a half-open range:

for (size_t i = 0; i < count; i++) {
    printf("values[%zu] = %d\n", i, values[i]);
}

The invariant is:

At the beginning of each iteration, elements before index i have been processed, and i is at most count.

i < count ensures the body only sees valid indices. When i == count, every element has been processed and the loop ends.

Accumulation

long sum = 0;
for (size_t i = 0; i < count; i++) {
    sum += values[i];
}

The accumulator type may need a wider range than the element type. Even a wider type can overflow for sufficiently many or large values, so serious code derives a numeric bound or performs checked arithmetic.

Reverse Traversal

Unsigned indices require care. This is wrong:

for (size_t i = count - 1; i >= 0; i--) { /* never stops as intended */
    /* ... */
}

size_t cannot become negative, and count - 1 already wraps if count is zero. A safe idiom is:

for (size_t i = count; i-- > 0;) {
    printf("%d\n", values[i]);
}

The comparison uses the old value; after a successful test, i has been decremented to the next valid index.

Array Updates

An element is a modifiable object when the array is not const:

for (size_t i = 0; i < count; i++) {
    values[i] *= 2;
}

Arrays themselves are not assignable:

int source[3] = {1, 2, 3};
int target[3];

/* target = source; */ /* invalid */

Copy element by element:

for (size_t i = 0; i < 3; i++) {
    target[i] = source[i];
}

or, for compatible arrays of trivially copyable object representations, use memcpy from <string.h> with exact byte counts. Element-wise copying is usually clearer at this stage and naturally supports type-aware transformations.

Assignment cannot resize an array. Its length is fixed for that object’s lifetime.

Array Parameters

In a function parameter declaration, array syntax adjusts to pointer syntax:

long sum_values(const int values[], size_t count)

means the same parameter type as:

long sum_values(const int *values, size_t count)

The function receives a pointer to the first element, not a copy of the whole array. The element count must be passed separately or encoded by another contract.

long sum_values(const int values[], size_t count) {
    long total = 0;

    for (size_t i = 0; i < count; i++) {
        total += values[i];
    }

    return total;
}

const states that the function will not modify elements through this parameter. It does not make the caller’s original array globally immutable.

The Sizeof Trap

Inside this function:

void inspect(int values[10]) {
    printf("%zu\n", sizeof values); /* pointer size, not ten ints */
}

values is a pointer parameter despite the bracket spelling. Use the explicit count supplied by the caller.

Minimum-Size Contracts

C allows a static bound in an array parameter:

double average(size_t count, const double values[static count]);

At a call, the pointer must provide access to at least count elements. This is a precondition, not a runtime check inserted by the language. Because support and diagnostic quality vary, document the contract regardless of syntax.

Mutation

A function can modify caller elements because the copied pointer designates the same array storage:

void fill_zero(int values[], size_t count) {
    for (size_t i = 0; i < count; i++) {
        values[i] = 0;
    }
}

C still passes the pointer by value. Reassigning the parameter itself would not change the caller’s array identity.

Multidimensional Arrays

int board[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

board is an array of three elements, where each element is an array of four int values. Storage is contiguous in row-major order:

board[0][0] board[0][1] board[0][2] board[0][3]
board[1][0] board[1][1] board[1][2] board[1][3]
board[2][0] board[2][1] board[2][2] board[2][3]

Traverse one dimension per loop:

for (size_t row = 0; row < 3; row++) {
    for (size_t column = 0; column < 4; column++) {
        printf("%4d", board[row][column]);
    }
    putchar('\n');
}

Multidimensional Parameters

All dimensions except the first must be known by the function’s type or supplied before use:

enum { COLUMNS = 4 };

void print_board(size_t rows, const int board[][COLUMNS]) {
    for (size_t row = 0; row < rows; row++) {
        for (size_t column = 0; column < COLUMNS; column++) {
            printf("%4d", board[row][column]);
        }
        putchar('\n');
    }
}

The compiler needs the row width to compute where board[row] begins. This is not the same type as int **; a contiguous two-dimensional array should not be passed as though it were a pointer to pointers.

With supported variable-length array parameters, dimensions can be passed:

void print_matrix(size_t rows, size_t columns,
                  const int matrix[rows][columns]);

The dimension parameters must appear before the array parameter so they are in scope.

Variable-Length Arrays

A variable-length array (VLA) has a runtime length:

void process(size_t count) {
    int scratch[count];
    /* ... */
}

VLA support became optional for conforming implementations beginning with C11. A C17 implementation may define __STDC_NO_VLA__ to indicate it does not support them. Even when supported, VLAs have important constraints:

  • the bound must be positive when storage is allocated;
  • storage usually has automatic duration and may exhaust a limited call stack;
  • allocation failure cannot be checked like malloc failure;
  • the size is fixed for that particular array object’s lifetime.

Use VLAs only for small, validated bounds under a known implementation contract. Fixed maximum arrays or dynamic allocation are often more portable and controllable.

VLA parameter notation can describe runtime-shaped data without necessarily allocating a VLA inside the function. Still, code intended for implementations without VLA support needs another interface.

Boundary Safety

For an array of count elements, a valid access must prove:

0 <= index < count

With size_t, non-negativity is inherent, leaving index < count as the critical check.

Validate Before Access

bool read_at(const int values[], size_t count,
             size_t index, int *result) {
    if (values == NULL || result == NULL || index >= count) {
        return false;
    }

    *result = values[index];
    return true;
}

The check occurs before forming an invalid element access. Pointer output is introduced fully in Chapter 9.

Size Arithmetic

Expressions such as count * sizeof values[0] can overflow size_t before allocation or copying. Chapter 11 shows the standard guard:

if (count > SIZE_MAX / sizeof values[0]) {
    /* requested byte size cannot be represented */
}

Fixed local arrays also need reasoned bounds. A user-provided value should never become a VLA length without validation.

Index State Trace

Trace an in-place prefix-total loop:

int values[] = {3, 1, 4, 2};
for (size_t i = 1; i < 4; i++) {
    values[i] += values[i - 1];
}
iarray beforeupdatearray after
1[3, 1, 4, 2]values[1] = 1 + 3[3, 4, 4, 2]
2[3, 4, 4, 2]values[2] = 4 + 4[3, 4, 8, 2]
3[3, 4, 8, 2]values[3] = 2 + 8[3, 4, 8, 10]

The invariant is: after iteration i, values[i] equals the sum of original elements 0 through i. Starting at index one is essential because each update reads its predecessor.

Change the insertion index and value below, then advance the steps to watch which elements must move before the new value can be stored.

Enable JavaScript to use the array-shift experiment.

Scorebook Case

This complete program keeps a fixed collection of marks in one array and passes its element count explicitly to every function. It demonstrates a crucial split: array storage belongs to the caller, while functions borrow access for the duration of each call.

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

static bool marks_valid(const int marks[], size_t count) {
    if (marks == NULL && count != 0) {
        return false;
    }
    for (size_t i = 0; i < count; i++) {
        if (marks[i] < 0 || marks[i] > 100) {
            return false;
        }
    }
    return true;
}

static long marks_sum(const int marks[], size_t count) {
    long total = 0;
    for (size_t i = 0; i < count; i++) {
        total += marks[i];
    }
    return total;
}

static bool marks_extremes(const int marks[], size_t count,
                           int *minimum, int *maximum) {
    if (marks == NULL || count == 0 ||
        minimum == NULL || maximum == NULL) {
        return false;
    }

    int low = marks[0];
    int high = marks[0];
    for (size_t i = 1; i < count; i++) {
        if (marks[i] < low) {
            low = marks[i];
        }
        if (marks[i] > high) {
            high = marks[i];
        }
    }
    *minimum = low;
    *maximum = high;
    return true;
}

int main(void) {
    int marks[] = {72, 91, 65, 88, 79};
    size_t count = sizeof marks / sizeof marks[0];

    if (!marks_valid(marks, count)) {
        fputs("invalid mark in scorebook\n", stderr);
        return EXIT_FAILURE;
    }

    int minimum;
    int maximum;
    if (!marks_extremes(marks, count, &minimum, &maximum)) {
        fputs("scorebook is empty\n", stderr);
        return EXIT_FAILURE;
    }

    long total = marks_sum(marks, count);
    double mean = (double)total / (double)count;
    printf("count=%zu min=%d max=%d mean=%.2f\n",
           count, minimum, maximum, mean);
    return EXIT_SUCCESS;
}

The complete program computes count where marks is still an array object. Inside marks_sum, the parameter spelling const int marks[] is adjusted to a pointer; sizeof marks there would report pointer size, not the caller’s array size. The count is therefore part of every traversal contract.

The const qualifier promises that validation, summation, and extreme finding do not modify elements through their parameters. It does not freeze the caller’s array forever. main still owns a mutable array and could change it between calls.

Empty Input Is a Design Case

The sum of an empty sequence can naturally be zero, so marks_sum(NULL, 0) performs zero iterations and returns zero. A minimum or maximum has no corresponding identity value for arbitrary int data. marks_extremes reports failure instead of inventing a sentinel that might also be a valid element.

Notice the order of checks:

if (marks == NULL || count == 0 || ...) {

No element is read before both pointer and count establish that index zero exists. The function computes candidates in local variables and writes output parameters only after traversal succeeds. If it returns false, caller outputs remain unchanged.

Minimum Trace

For marks [72, 91, 65, 88, 79], the extreme loop begins with a real element rather than arbitrary constants:

i before bodycurrent marklow beforehigh beforestate after
1917272low 72, high 91
2657291low 65, high 91
3886591unchanged
4796591unchanged

Before each test, low and high are the minimum and maximum of the nonempty prefix [0, i). Reading marks[i] extends that prefix by one. At termination i == count, the prefix is the complete array.

Starting with low = 0 would be wrong for an array whose values are all positive when computing the maximum’s analogous negative case, and using INT_MAX/INT_MIN unnecessarily couples the logic to type limits. Initializing from the first element expresses the nonempty precondition directly.

Array Shape Contracts

An array interface needs more than an element pointer:

base address + element type + accessible count + mutation permission

For a matrix, it also needs row count and row stride. In int matrix[rows][columns], a row contains columns adjacent int values, so locating [r][c] conceptually uses:

offset in elements = r * columns + c

Both r < rows and c < columns must hold. Checking only the flattened offset is insufficient if row and column meaning matters, and r * columns itself may overflow for untrusted dimensions before allocation or indexing.

Array parameters do not carry ownership. A function must not retain the adjusted pointer after the caller’s array lifetime ends. It also must not assume the count is truthful merely because a pointer is non-null; the pointer/count pair is a caller-supplied contract that C cannot verify at runtime.

Overlap and Direction

When source and destination regions overlap, copy direction affects correctness. Moving elements one place to the right must proceed from the end toward the beginning:

before: [A B C D _]
copy D -> slot 4
copy C -> slot 3
copy B -> slot 2
copy A -> slot 1
after:  [A A B C D]

Copying forward would overwrite B before its original value was moved. Library memmove handles overlapping byte regions; memcpy requires non-overlap. Even hand-written element loops need an explicit overlap policy.

Address Walk

Contiguity means successive elements occupy successive sizeof(element)-byte regions. If an illustrative int array begins at address 1000 and sizeof(int) is 4, its conceptual layout is:

expression    illustrative address    stored value
values[0]     1000                    72
values[1]     1004                    91
values[2]     1008                    65
values[3]     1012                    88
values[4]     1016                    79
one-past      1020                    no element

Actual addresses depend on the run and implementation. The useful guarantee is the spacing relationship, not the particular numbers. &values[i] is equivalent to values + i while i is within zero through count, but the one-past result cannot be dereferenced.

For int matrix[3][4], each row is itself an array of four int. If an int occupies four C bytes, consecutive row starts are sixteen bytes apart on that illustrative implementation. The compiler needs the column count to perform this stride calculation; this is why a pointer-to-pointer cannot substitute for a contiguous matrix.

Mutation Ledger

Inserting into a fixed-capacity active prefix requires three facts:

0 <= index <= size
size < capacity
move [index, size) one position right before storing

For active values [8, 3, 5, 2], capacity six, and insertion of 9 at index two:

physical start: [8, 3, 5, 2, _, _] size=4
move index 3->4: [8, 3, 5, 2, 2, _]
move index 2->3: [8, 3, 5, 5, 2, _]
store at index 2:[8, 3, 9, 5, 2, _]
publish size=5:  active prefix is [8, 3, 9, 5, 2]

size changes last. Until all writes finish, publishing a larger size would claim an element is meaningful before the invariant is restored. Deletion reverses the idea: shift [index + 1, size) left, then reduce size. The stale physical value beyond the new size is not logically present and need not be erased for an integer array, though sensitive data may require a deliberate clearing policy.

Counts and Sentinels

An explicit count allows every element value. A sentinel reserves one value to mark the end:

int values[] = {4, 7, 2, -1}; /* -1 used as terminator */

This is safe only if -1 is forbidden as data and the sentinel is guaranteed within accessible storage. C strings use a null-character sentinel under exactly such a contract. General integer arrays usually use counts because their entire value range may be meaningful.

Sentinel scanning still needs a physical bound when data may be corrupted or external. Searching indefinitely for a missing sentinel reads beyond the array. A bounded search can stop at either count or sentinel and report which occurred.

Traversal Locality

Row-major matrices are usually fastest to traverse with columns in the inner loop because successive reads are adjacent:

for (size_t row = 0; row < rows; row++) {
    for (size_t column = 0; column < columns; column++) {
        use(matrix[row][column]);
    }
}

Reversing the loops visits elements columns apart. Both orders are correct when bounds hold, but memory caches generally reward contiguous access. This difference matters for large numeric grids and barely matters for a 3-by-3 teaching example. Correct bounds and a clear algorithm come first; locality becomes an engineering choice once measurement shows scale.

A transpose cannot be performed in place by merely swapping every (row,column) with (column,row) on a non-square fixed array because the shape and type differ. For a square matrix, swap only one triangle; swapping both triangles would undo every change. Array shape belongs to the operation contract, not just its loop syntax.

Boundary Failures

  • Using <= count: index count is one past the final element and cannot be dereferenced.
  • Assuming initialization without an initializer: automatic array elements are indeterminate.
  • Dividing sizeof values inside an array-parameter function: the parameter is a pointer there.
  • Treating a 2D array as int **: their representations and types differ.
  • Assigning arrays: copy elements or redesign the interface.
  • Unsigned reverse loops with i >= 0: unsigned values do not become negative.
  • Using unchecked input as a VLA bound: stack exhaustion or invalid bounds can follow.
  • Assuming C checks bounds: every index proof belongs to the program.

Array Reasoning

  • An array is a fixed-length, contiguous collection of elements of one type.
  • Valid indices form the half-open range from zero through one less than the count.
  • Initializer length can determine an array’s bound; omitted initializers become zero.
  • sizeof array / sizeof array[0] works only where the operand is an actual array.
  • Array parameters adjust to pointers, so functions need explicit size contracts.
  • Multidimensional arrays are arrays of arrays stored in row-major order.
  • VLA support is optional in C17 and runtime bounds require careful validation.
  • C provides no automatic bound checks; safe traversal makes the proof visible.

Array Problems

Establish the Bounds

  1. State the valid index range for an array of count elements.
  2. Why does sizeof produce different results for an array object and an array parameter?
  3. Explain why int matrix[3][4] is not compatible with int **.
  4. What portability and safety concerns apply to VLAs?

Move the Index

  1. Trace the prefix-total loop for {2, 5, -1, 3}.
  2. List the row-major storage order of int table[2][3] using element expressions.
  3. Determine every initialized value in int flags[6] = {[1] = 7, [4] = 9};.

Repair the Boundary

  1. Correct a loop that uses i <= count.
  2. Repair a reverse traversal that underflows size_t on an empty array.
  3. Explain why an array_length(int values[]) function cannot compute the caller’s length with sizeof.

Process the Elements

  1. Write functions to fill an array, print it, compute its sum, and compute its mean. Give every function an explicit count.
  2. Read a 3-by-3 integer matrix and print row totals and column totals. Validate every input conversion.
  3. Rotate the elements of a fixed array one position to the right using one saved value and no second array.

Prove the Range

  1. State a loop invariant for copying one array into another.
  2. Design an interface for printing matrices when VLA support is unavailable. Explain how the function learns the row stride.