4 min read
On this page

Pointer Arithmetic & Arrays

Arrays and pointers in C are deeply intertwined. An array name decays to a pointer in most expressions. Array indexing is syntactic sugar for pointer arithmetic. Understanding this relationship — and understanding where it breaks — is essential for writing correct C and reading systems code.

Arrays Decay to Pointers

When you use an array name in an expression, it automatically converts (decays) to a pointer to its first element. This happens in almost every context.

#include <stdio.h>

void print_first(int *p) {
    printf("first element: %d\n", *p);
}

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};

    // arr decays to &arr[0]
    int *p = arr;              // no & needed — arr IS a pointer here
    print_first(arr);          // arr decays to pointer when passed

    printf("arr:    %p\n", (void *)arr);
    printf("&arr[0]: %p\n", (void *)&arr[0]);
    printf("Same? %s\n", arr == &arr[0] ? "yes" : "no");

    return 0;
}
first element: 10
arr:    0x7ffd3c4a1b40
&arr[0]: 0x7ffd3c4a1b40
Same? yes

When you pass an array to a function, the function receives a pointer. This is why C functions cannot know the size of an array argument — the size information is lost in the decay.

arr[i] Is *(arr + i)

Array indexing is defined as pointer arithmetic plus dereference. The expression arr[i] is exactly equivalent to *(arr + i).

#include <stdio.h>

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};

    // These are all equivalent
    printf("arr[2]:       %d\n", arr[2]);
    printf("*(arr + 2):   %d\n", *(arr + 2));
    printf("*(2 + arr):   %d\n", *(2 + arr));
    printf("2[arr]:       %d\n", 2[arr]);      // legal but terrible

    return 0;
}
arr[2]:       30
*(arr + 2):   30
*(2 + arr):   30
2[arr]:       30

Because addition is commutative, arr[2] and 2[arr] are both valid C. Nobody writes 2[arr] in real code, but understanding why it works proves you understand the pointer arithmetic model.

Pointer Arithmetic Respects Types

When you add an integer to a pointer, the compiler scales the addition by the size of the pointed-to type.

#include <stdio.h>

int main(void) {
    int iarr[3] = {100, 200, 300};
    double darr[3] = {1.1, 2.2, 3.3};
    char carr[3] = {'A', 'B', 'C'};

    int *ip = iarr;
    double *dp = darr;
    char *cp = carr;

    printf("int pointer:\n");
    printf("  ip     = %p\n", (void *)ip);
    printf("  ip + 1 = %p (moved %td bytes)\n",
           (void *)(ip + 1), (char *)(ip + 1) - (char *)ip);

    printf("double pointer:\n");
    printf("  dp     = %p\n", (void *)dp);
    printf("  dp + 1 = %p (moved %td bytes)\n",
           (void *)(dp + 1), (char *)(dp + 1) - (char *)dp);

    printf("char pointer:\n");
    printf("  cp     = %p\n", (void *)cp);
    printf("  cp + 1 = %p (moved %td bytes)\n",
           (void *)(cp + 1), (char *)(cp + 1) - (char *)cp);

    return 0;
}
int pointer:
  ip     = 0x7ffd3c4a1b40
  ip + 1 = 0x7ffd3c4a1b44 (moved 4 bytes)
double pointer:
  dp     = 0x7ffd3c4a1b50
  dp + 1 = 0x7ffd3c4a1b58 (moved 8 bytes)
char pointer:
  cp     = 0x7ffd3c4a1b60
  cp + 1 = 0x7ffd3c4a1b61 (moved 1 byte)

This is the key insight: pointer arithmetic is element-based, not byte-based. The compiler handles the byte-level math for you.

Iterating with Pointers

Pointer-based iteration is idiomatic C and is often clearer than index-based iteration for sequential access:

#include <stdio.h>

void print_array(const int *arr, size_t len) {
    const int *end = arr + len;
    for (const int *p = arr; p < end; p++) {
        printf("%d ", *p);
    }
    printf("\n");
}

// Summing with pointer iteration
long sum_array(const int *arr, size_t len) {
    long total = 0;
    const int *end = arr + len;
    for (const int *p = arr; p < end; p++) {
        total += *p;
    }
    return total;
}

int main(void) {
    int data[] = {5, 10, 15, 20, 25};
    size_t len = sizeof(data) / sizeof(data[0]);

    print_array(data, len);
    printf("sum: %ld\n", sum_array(data, len));

    return 0;
}
5 10 15 20 25
sum: 75

The pattern const int *end = arr + len creates a one-past-the-end pointer. Comparing p < end is valid — you can point one past the last element of an array. Dereferencing that pointer is undefined behavior, but holding and comparing it is fine.

Multidimensional Arrays

A 2D array in C is an array of arrays. The elements are stored in row-major order — all elements of row 0, then all elements of row 1, and so on.

#include <stdio.h>

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

    // Access with indices
    printf("matrix[1][2] = %d\n", matrix[1][2]);  // 7

    // Memory layout is contiguous
    int *flat = &matrix[0][0];
    for (int i = 0; i < 12; i++) {
        printf("%2d ", flat[i]);
    }
    printf("\n");

    // Row addresses
    for (int r = 0; r < 3; r++) {
        printf("row %d starts at %p\n", r, (void *)matrix[r]);
    }

    return 0;
}
matrix[1][2] = 7
 1  2  3  4  5  6  7  8  9 10 11 12
row 0 starts at 0x7ffd3c4a1b00
row 1 starts at 0x7ffd3c4a1b10
row 2 starts at 0x7ffd3c4a1b20

Each row starts 16 bytes (4 ints * 4 bytes) after the previous one. matrix[1][2] is the element at row 1, column 2, which is byte offset (1 * 4 + 2) * sizeof(int) from the start.

Passing 2D Arrays to Functions

You must specify all dimensions except the first. The compiler needs the column count to compute row offsets:

// Must specify the column count
void print_matrix(int rows, int cols, int mat[][4]) {
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            printf("%3d ", mat[r][c]);
        }
        printf("\n");
    }
}

C99 also supports VLA syntax: int mat[rows][cols], where both dimensions are parameters. Without the column count, mat[1] does not know how many bytes to skip.

Pointers to Pointers: int**

A pointer to a pointer holds the address of a pointer variable. The most common use is arrays of strings (arrays of char *):

#include <stdio.h>

void print_strings(char **strings, int count) {
    for (int i = 0; i < count; i++) {
        printf("[%d]: %s\n", i, strings[i]);
    }
}

int main(void) {
    // Array of string pointers
    char *languages[] = {"C", "Python", "Rust", "Go"};
    int count = sizeof(languages) / sizeof(languages[0]);

    print_strings(languages, count);

    // This is what main receives:
    // int main(int argc, char **argv)
    // argv is a pointer to an array of string pointers

    return 0;
}
[0]: C
[1]: Python
[2]: Rust
[3]: Go

char ** is "pointer to pointer to char." languages[i] dereferences the first pointer (getting a char *), and the string functions dereference the second pointer (getting individual char values).

Where Array/Pointer Equivalence Breaks

Arrays and pointers are NOT the same thing. They behave identically in expressions (because of decay), but they differ in two important ways.

sizeof

#include <stdio.h>

int main(void) {
    int arr[10];
    int *p = arr;

    printf("sizeof(arr): %zu\n", sizeof(arr));   // 40 (10 * 4 bytes)
    printf("sizeof(p):   %zu\n", sizeof(p));      // 8  (pointer size)

    // This is how you compute array length
    size_t len = sizeof(arr) / sizeof(arr[0]);
    printf("length: %zu\n", len);                 // 10

    return 0;
}
sizeof(arr): 40
sizeof(p):   8
length: 10

sizeof(arr) gives the total byte size of the array. sizeof(p) gives the size of the pointer itself (8 bytes on 64-bit). After decay, the size information is gone.

& (Address-of)

#include <stdio.h>

int main(void) {
    int arr[5];

    // arr and &arr have the same address but different types
    printf("arr:    %p\n", (void *)arr);       // pointer to int
    printf("&arr:   %p\n", (void *)&arr);      // pointer to int[5]
    printf("&arr+1: %p\n", (void *)(&arr + 1));// moves by 20 bytes (5 * 4)
    printf("arr+1:  %p\n", (void *)(arr + 1)); // moves by 4 bytes

    return 0;
}
arr:    0x7ffd3c4a1b40
&arr:   0x7ffd3c4a1b40
&arr+1: 0x7ffd3c4a1b54
arr+1:  0x7ffd3c4a1b44

arr decays to int * — adding 1 moves 4 bytes. &arr is int (*)[5] (pointer to array of 5 ints) — adding 1 moves 20 bytes (the entire array). Same address, different type, different arithmetic.

Pointer Subtraction

Subtracting two pointers of the same type gives the number of elements between them. &arr[7] - &arr[2] is 5 (elements, not bytes). Use ptrdiff_t (from <stddef.h>) for the result type. Subtracting pointers that do not point into the same array is undefined behavior.

Common Pitfalls

  • Using sizeof on a decayed array — inside a function that receives int *arr, sizeof(arr) returns the pointer size (8), not the array size. Always pass the length separately.
  • Pointer arithmetic on void* — you cannot do arithmetic on void * because the compiler does not know the element size. Cast to char * first for byte-level arithmetic.
  • Out-of-bounds pointer arithmetic — you may point one past the end of an array, but no further. Going two past the end or before the beginning is undefined behavior, even without dereferencing.
  • Confusing int with int[][N]** — int ** is a pointer to a pointer; int [][N] is a 2D array. They have different memory layouts and are not interchangeable.
  • Forgetting that string literals are arrays"hello" is const char[6] (including the null terminator). It decays to const char * in expressions.

Key Takeaways

  • Arrays decay to pointers to their first element in most expressions. This is why you can use array names where pointers are expected.
  • arr[i] is exactly *(arr + i). Array indexing is pointer arithmetic with syntactic sugar.
  • Pointer arithmetic scales by the pointed-to type size. int *p + 1 moves sizeof(int) bytes.
  • Multidimensional arrays are arrays of arrays, stored contiguously in row-major order.
  • int ** (pointer to pointer) is used for arrays of strings and dynamically allocated 2D data.
  • Array/pointer equivalence breaks for sizeof (arrays know their size, pointers do not) and & (different types, different arithmetic).
  • Always pass array length as a separate parameter. C arrays do not carry their size.