4 min read
On this page

Arrays

An array in C is a contiguous block of memory holding a fixed number of elements of the same type. There is no array object, no length property, no bounds checking. An array is just bytes in a row, and C trusts you to manage them correctly. This simplicity makes arrays fast and predictable — and makes mistakes invisible.

Fixed-Size Arrays

Declare an array by specifying the type and count:

#include <stdio.h>

int main(void) {
    int scores[5];           // 5 uninitialized ints (garbage values)
    double temperatures[24]; // 24 uninitialized doubles
    char name[32];           // 32 uninitialized chars

    scores[0] = 95;
    scores[1] = 87;
    scores[2] = 92;
    scores[3] = 78;
    scores[4] = 88;

    for (int i = 0; i < 5; i++) {
        printf("scores[%d] = %d\n", i, scores[i]);
    }

    return 0;
}
scores[0] = 95
scores[1] = 87
scores[2] = 92
scores[3] = 78
scores[4] = 88

The size must be a compile-time constant (in standard C89/C90). C99 added variable-length arrays, but they are controversial (more on that later).

Array Initialization

Full Initialization

int primes[5] = {2, 3, 5, 7, 11};

Partial Initialization

Unspecified elements are zero-initialized:

int arr[10] = {1, 2, 3};
// arr = {1, 2, 3, 0, 0, 0, 0, 0, 0, 0}

Zero Initialization

int zeros[100] = {0};   // all elements set to 0

This is the idiomatic way to zero an array. The {0} explicitly sets the first element to 0, and the remaining elements are implicitly zero-initialized.

Implicit Size

Let the compiler count:

int data[] = {10, 20, 30, 40};   // compiler infers size 4
size_t len = sizeof(data) / sizeof(data[0]);   // 4

Designated Initializers (C99)

Set specific elements by index:

#include <stdio.h>

int main(void) {
    int lookup[10] = {
        [0] = 100,
        [5] = 500,
        [9] = 900
    };
    // All other elements are 0

    for (int i = 0; i < 10; i++) {
        printf("[%d] = %d\n", i, lookup[i]);
    }

    return 0;
}
[0] = 100
[1] = 0
[2] = 0
[3] = 0
[4] = 0
[5] = 500
[6] = 0
[7] = 0
[8] = 0
[9] = 900

Designated initializers are especially useful for sparse arrays, lookup tables, and enum-indexed arrays:

typedef enum { RED, GREEN, BLUE, COLOR_COUNT } Color;

const char *color_names[COLOR_COUNT] = {
    [RED]   = "red",
    [GREEN] = "green",
    [BLUE]  = "blue"
};

Arrays Do Not Know Their Size

This is the most important thing to understand about C arrays. Unlike Java, Python, or Rust, a C array carries no length metadata. When you pass an array to a function, it decays to a pointer, and the size information is lost.

#include <stdio.h>

// WRONG: sizeof gives pointer size, not array size
void print_wrong(int arr[]) {
    // sizeof(arr) is sizeof(int*), not the array size
    printf("sizeof inside function: %zu\n", sizeof(arr));
}

// RIGHT: pass the length separately
void print_right(const int *arr, size_t len) {
    for (size_t i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(void) {
    int data[] = {10, 20, 30, 40, 50};
    size_t len = sizeof(data) / sizeof(data[0]);

    printf("sizeof in main: %zu\n", sizeof(data));   // 20 (5 * 4)
    print_wrong(data);                                 // 8 (pointer size)

    print_right(data, len);

    return 0;
}
sizeof in main: 20
sizeof inside function: 8
10 20 30 40 50

The pattern sizeof(arr) / sizeof(arr[0]) computes the element count, but only works when arr is an actual array, not a pointer. Inside a function that receives an array parameter, this trick gives the wrong answer.

The Standard Pattern: Pointer & Length

Idiomatic C passes arrays as a pointer and a length:

#include <stdio.h>
#include <stddef.h>

// The canonical C array function signature
int find_max(const int *arr, size_t len) {
    if (len == 0) return 0;  // handle empty array

    int max = arr[0];
    for (size_t i = 1; i < len; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}

double average(const int *arr, size_t len) {
    if (len == 0) return 0.0;

    long sum = 0;
    for (size_t i = 0; i < len; i++) {
        sum += arr[i];
    }
    return (double)sum / len;
}

int main(void) {
    int values[] = {23, 45, 12, 67, 34, 89, 56};
    size_t len = sizeof(values) / sizeof(values[0]);

    printf("max: %d\n", find_max(values, len));
    printf("avg: %.2f\n", average(values, len));

    return 0;
}
max: 89
avg: 46.57

Use size_t for the length parameter. It is an unsigned type guaranteed to be large enough to represent the size of any object.

Stack vs Heap Arrays

Stack Arrays

Local arrays are allocated on the stack. They are fast to allocate (the compiler just adjusts the stack pointer) but limited in size (the stack is typically 1-8 MB).

void process(void) {
    int small[100];        // fine: 400 bytes on the stack
    int large[1000000];    // dangerous: 4 MB on the stack
                           // may cause stack overflow
}

Heap Arrays

For large or dynamically-sized arrays, allocate on the heap with malloc:

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

int main(void) {
    size_t n = 1000000;
    int *arr = malloc(sizeof(int) * n);
    if (arr == NULL) {
        fprintf(stderr, "allocation failed\n");
        return 1;
    }

    // Use it like a regular array
    for (size_t i = 0; i < n; i++) {
        arr[i] = (int)i;
    }

    printf("arr[999999] = %d\n", arr[999999]);

    free(arr);
    return 0;
}
arr[999999] = 999999

Heap arrays have no size limit (other than available memory) but must be freed manually.

Variable-Length Arrays (VLAs)

C99 introduced VLAs, which allow the array size to be determined at runtime:

void zero_matrix(int n) {
    int matrix[n][n];   // VLA: size determined at runtime
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            matrix[i][j] = 0;
}

VLAs are allocated on the stack and are controversial:

  • They can cause stack overflow if n is large or user-controlled
  • They cannot be initialized at declaration
  • They were made optional in C11 (compilers may not support them)
  • The Linux kernel bans them entirely

In practice, many codebases avoid VLAs and use malloc for runtime-sized arrays instead. The explicit allocation makes the cost visible and the error handling possible.

Passing Arrays to Functions

There are several ways to write a function that takes an array:

// These three declarations are equivalent — compiler treats all as int*:
void func1(int arr[], size_t len);       // "array" syntax
void func2(int *arr, size_t len);        // pointer syntax (preferred)
void func3(int arr[10], size_t len);     // the 10 is ignored by the compiler

// For fixed-size arrays, pointer-to-array syntax enforces the size:
void func4(int (*arr)[10]);              // pointer to array of exactly 10 ints

The first three are identical. The [10] in func3 is documentation, not enforcement. func4 is different: it takes a pointer to an array of exactly 10 ints, and the compiler will warn if you pass a differently-sized array.

Common Pitfalls

  • Writing past the end of an array — C does not check bounds. arr[10] on a 10-element array accesses memory you do not own. This is the buffer overflow that causes security vulnerabilities.
  • Using sizeof on a pointer — inside a function, sizeof(arr) on an array parameter gives the pointer size (8 bytes), not the array size. Always pass the length separately.
  • Large stack arrays — declaring int big[10000000] on the stack will likely cause a stack overflow. Use malloc for large arrays.
  • Forgetting that VLAs are stack-allocated — a VLA with a user-controlled size can overflow the stack. Always validate the size or use malloc.
  • Assuming arrays are zero-initialized — local arrays contain garbage unless you explicitly initialize them. Only static and global arrays are zero-initialized by default.
  • Off-by-one errors — an array of size n has valid indices 0 through n-1. Accessing arr[n] is out of bounds.

Key Takeaways

  • C arrays are contiguous blocks of memory with no length metadata. You must track and pass the length separately.
  • The idiomatic function signature is func(const int *arr, size_t len). Use const when the function does not modify the array.
  • sizeof(arr) / sizeof(arr[0]) computes the element count but only works for actual arrays, not pointers.
  • Stack arrays are fast but size-limited. Heap arrays (via malloc) support arbitrary sizes but require manual memory management.
  • VLAs are runtime-sized stack arrays. They are controversial, optional in C11, and avoided in many codebases.
  • Designated initializers ([index] = value) are useful for sparse arrays and enum-indexed lookup tables.
  • Array access is unchecked. Out-of-bounds writes are buffer overflows — the most exploited class of bugs in computing history.