Skip to main content
C beginner Lesson 8 of 23

Arrays in C

Learn 1D and 2D arrays, array decay to pointers, variable-length arrays, and string arrays in C.

Declaring and Initializing Arrays

An array is a contiguous block of memory holding multiple values of the same type. Arrays are the simplest and most cache-friendly data structure in C — all elements sit adjacent in memory, so traversing them is very fast. The trade-off is that the size must be known at compile time (for stack arrays) and cannot change once allocated.

#include <stdio.h>

int main(void) {
    /* Declaration — elements are uninitialized (contain garbage) */
    int scores[5];

    /* Declaration with initialization */
    int primes[6] = {2, 3, 5, 7, 11, 13};

    /* Partial initialization — remaining elements are set to zero */
    int data[10] = {1, 2, 3};   /* data[3..9] == 0 */

    /* Zero-initialize the entire array */
    int zeroes[100] = {0};

    /* Let the compiler count the elements from the initializer */
    double temps[] = {36.5, 37.1, 38.2, 36.9};
    int n = sizeof(temps) / sizeof(temps[0]);   /* 4 */

    /* Designated initializers (C99) — initialize specific indices by name */
    int days_in_month[13] = {
        [1]  = 31, [2]  = 28, [3]  = 31,
        [4]  = 30, [5]  = 31, [6]  = 30,
        [7]  = 31, [8]  = 31, [9]  = 30,
        [10] = 31, [11] = 30, [12] = 31
    };

    for (int i = 0; i < n; i++) {
        printf("temps[%d] = %.1f\n", i, temps[i]);
    }

    return 0;
}

Iterating Over Arrays

The most common pattern is a for loop with an index from 0 to n-1. Always pass the length separately when passing arrays to functions — the array itself carries no size information.

#include <stdio.h>

void print_array(const int *arr, int n) {
    for (int i = 0; i < n; i++) {
        printf("%d", arr[i]);
        if (i < n - 1) printf(", ");
    }
    printf("\n");
}

int find_max(const int *arr, int n) {
    int max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] > max) max = arr[i];
    }
    return max;
}

int main(void) {
    int nums[] = {42, 17, 93, 5, 68, 31};
    int len = sizeof(nums) / sizeof(nums[0]);

    print_array(nums, len);
    printf("Max: %d\n", find_max(nums, len));
    return 0;
}

Array Decay to Pointer

When an array is used in most expressions — especially when passed to a function — it automatically converts to a pointer to its first element. This is called “decay.” The consequence is that the function loses the array’s size information, which is why you must always pass the length as a separate parameter.

#include <stdio.h>

void size_demo(int arr[], int n) {
    /* sizeof(arr) is sizeof(int*) here — the array has decayed to a pointer */
    printf("Inside function: sizeof(arr) = %zu\n", sizeof(arr));
    printf("n = %d\n", n);
}

int main(void) {
    int data[10] = {0};
    printf("In main: sizeof(data) = %zu\n", sizeof(data));  /* 40 bytes */
    size_demo(data, 10);   /* sizeof(arr) = 8 (pointer size) */
    return 0;
}

All three of these function signatures are identical — the array syntax is just syntactic sugar for a pointer:

void f(int arr[], int n);       /* array syntax — decays to pointer */
void f(int *arr, int n);        /* explicit pointer — identical */
void f(int arr[10], int n);     /* the 10 is ignored by the compiler */

2D Arrays

A 2D array in C is stored in row-major order — all elements of row 0, then all of row 1, and so on. This means iterating row by row (outer loop over rows, inner loop over columns) is cache-friendly, while column-major access causes cache misses.

#include <stdio.h>

#define ROWS 3
#define COLS 4

void print_matrix(int mat[ROWS][COLS]) {
    for (int r = 0; r < ROWS; r++) {
        for (int c = 0; c < COLS; c++) {
            printf("%4d", mat[r][c]);
        }
        printf("\n");
    }
}

void multiply(int a[2][3], int b[3][2], int result[2][2]) {
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 2; j++) {
            result[i][j] = 0;
            for (int k = 0; k < 3; k++) {
                result[i][j] += a[i][k] * b[k][j];
            }
        }
    }
}

int main(void) {
    int matrix[ROWS][COLS] = {
        {1,  2,  3,  4},
        {5,  6,  7,  8},
        {9, 10, 11, 12}
    };

    print_matrix(matrix);

    /* 2D arrays are stored contiguously in row-major order */
    /* matrix[r][c] == *(&matrix[0][0] + r * COLS + c) */
    return 0;
}

For 2D arrays passed to functions, all dimensions except the first must be specified so the compiler can compute the row stride:

/* The number of columns must be a compile-time constant */
void process(int mat[][4], int rows) { /* ... */ }

For dynamic 2D arrays where dimensions are not known at compile time, use a flat 1D array with manual index calculation:

#include <stdlib.h>

/* Allocate a rows×cols matrix as a single flat array */
int *make_matrix(int rows, int cols) {
    return malloc((size_t)rows * cols * sizeof(int));
}

/* Access element at (r, c) with the flat-index formula */
#define MAT(m, cols, r, c) m[(r)*(cols) + (c)]

int main(void) {
    int rows = 4, cols = 5;
    int *m = make_matrix(rows, cols);
    MAT(m, cols, 2, 3) = 42;   /* equivalent to m[2][3] = 42 */
    free(m);
    return 0;
}

Variable-Length Arrays (VLAs)

C99 introduced variable-length arrays, whose size is determined at runtime rather than compile time. They are convenient for small, temporary arrays whose size depends on user input or function parameters — but they allocate on the stack, so they are unsafe for large sizes.

#include <stdio.h>

void print_vla(int n) {
    int arr[n];   /* size known only at runtime — allocated on the stack */

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

int main(void) {
    print_vla(5);   /* 0 1 4 9 16 */
    print_vla(8);   /* 0 1 4 9 16 25 36 49 */
    return 0;
}

Caution: VLAs are stack-allocated. Never use them for large or unbounded sizes — a stack overflow has no safe recovery. VLAs were made optional in C11 (__STDC_NO_VLA__ is defined if not supported). For large dynamic arrays, always use malloc.

Arrays of Strings

An array of strings in C is typically an array of const char * pointers, each pointing to a string literal. This is memory-efficient (no copying) but the strings are read-only.

#include <stdio.h>

int main(void) {
    /* Array of pointers to string literals — read-only, variable-length strings */
    const char *days[] = {
        "Monday", "Tuesday", "Wednesday",
        "Thursday", "Friday", "Saturday", "Sunday"
    };
    int ndays = sizeof(days) / sizeof(days[0]);

    for (int i = 0; i < ndays; i++) {
        printf("Day %d: %s\n", i + 1, days[i]);
    }

    /* 2D char array — fixed-size buffers that you can modify */
    char names[3][20] = {"Alice", "Bob", "Charlie"};
    for (int i = 0; i < 3; i++) {
        printf("%s\n", names[i]);
    }

    return 0;
}

The difference matters: const char *days[] is an array of pointers to string literals (read-only, variable-length strings). char names[3][20] is a true 2D array of characters (writable, fixed-size buffers).

Sorting Arrays

The standard library provides qsort for sorting any array. It uses a comparison function pointer, making it generic — you supply the logic for comparing two elements.

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

/* Comparison function: returns negative if a < b, 0 if equal, positive if a > b */
int compare_int(const void *a, const void *b) {
    int ia = *(const int *)a;
    int ib = *(const int *)b;
    return (ia > ib) - (ia < ib);   /* safe: returns -1, 0, or 1 without overflow */
}

int compare_desc(const void *a, const void *b) {
    return compare_int(b, a);   /* reverse the arguments for descending order */
}

int main(void) {
    int arr[] = {64, 25, 12, 22, 11};
    int n = sizeof(arr) / sizeof(arr[0]);

    qsort(arr, n, sizeof(int), compare_int);

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);   /* 11 12 22 25 64 */
    }
    printf("\n");

    return 0;
}

Frequently Asked Questions

Why does C not check array bounds?
C prioritizes performance and trusts the programmer. Bounds checking adds overhead on every array access. It's your responsibility to ensure indices are valid — an out-of-bounds access is undefined behavior and a common source of security vulnerabilities.
What does it mean for an array to 'decay' to a pointer?
When you pass an array to a function or use it in most expressions, it automatically converts to a pointer to its first element. This means sizeof(array) inside a function gives you the pointer size, not the array size.
Are variable-length arrays (VLAs) safe?
VLAs are optional in C11 and later. They allocate on the stack, so large VLAs can cause stack overflow. For large or potentially large arrays, use malloc instead.