A concrete problem
When you learn C, one of the first things you discover is how to read and write values in an array using square brackets, like arr[2]. It looks exactly like Python, Java, or C#. But C does not have a built-in bounds-checked array type in the way modern managed languages do. Instead, C provides a thin layer of syntax over direct memory addresses.
To really understand memory in C, you have to understand what the compiler actually does when it sees those square brackets. If you don’t, you will eventually write code that accesses memory it shouldn’t, triggering undefined behavior and crashing your program—or worse, silently corrupting data.
Mental model
In C, an array name is not a complex object; it is simply a continuous block of memory. When you use an array’s name in an expression, it almost always “decays” into a pointer to its first element.[5] So if you have int arr[5], the expression arr is evaluated as an int * pointing to the start of that block.
Because arrays decay to pointers, array indexing is identical to pointer arithmetic.[4] The syntax arr[i] is strictly defined by the C standard as equivalent to *(arr + i).[4] The compiler takes the base address, adds an offset calculated by multiplying the index by the size of the type (in this case, sizeof(int)), and dereferences the resulting pointer.
Because addition is commutative, *(arr + i) is exactly the same as *(i + arr). And because of the definition of the subscript operator, that means i[arr] is perfectly valid C code that does the exact same thing as arr[i].[4]
Small complete C17 program
Let’s compile a program to prove this works and to see exactly how array names behave when passed around.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int arr[5] = {10, 20, 30, 40, 50};
// Conventional array indexing
printf("arr[2] = %d\n", arr[2]);
// Pointer arithmetic equivalent
printf("*(arr + 2) = %d\n", *(arr + 2));
// The surprising bit: i[arr] is the same as arr[i]
// because *(i + arr) == *(arr + i)
printf("2[arr] = %d\n", 2[arr]);
// Demonstrating the mental model
int *ptr = arr; // arrays decay to pointers to their first element
printf("ptr[3] = %d\n", ptr[3]);
printf("3[ptr] = %d\n", 3[ptr]);
return EXIT_SUCCESS;
}
Compile and run commands
gcc -std=c17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined lesson.c -o lesson
./lesson
Expected output:
arr[2] = 30
*(arr + 2) = 30
2[arr] = 30
ptr[3] = 40
3[ptr] = 40
Line-by-line reasoning
int arr[5] = {10, 20, 30, 40, 50};allocates a contiguous block of memory for five integers and initializes them.arr[2]requests the integer at index 2 (the third element,30).*(arr + 2)manually performs the address arithmetic. The compiler knowsarrpoints to anint, so it adds2 * sizeof(int)bytes to the base address before dereferencing it.2[arr]works because the C standard defines the subscript operator entirely in terms of pointer addition, and addition is commutative.[4]int *ptr = arr;explicitly assigns the decayed pointer to a new variable. Notice we don’t need&arr.ptr[3]shows that you can use array subscript syntax on any pointer, not just arrays.
Common bug and corrected version
The danger of C’s approach is that the language does not track the size of the allocated memory once the array decays to a pointer. Accessing an index outside the array bounds results in undefined behavior.[1]
// BUG: Out of bounds access
int main(void) {
int arr[3] = {10, 20, 30};
printf("%d\n", arr[3]); // Undefined behavior: reading the 4th element of a 3-element array
return 0;
}
If you compile this with AddressSanitizer enabled (-fsanitize=address), the program will immediately abort with a stack-buffer-overflow error. Without the sanitizer, the program might print garbage data, crash, or appear to work normally while quietly corrupting other memory.
To fix this, you must manually track array sizes and enforce bounds checks. A common pattern is passing the size alongside the array to functions:
// CORRECTED: Explicit size tracking
void print_array(const int *arr, size_t size) {
for (size_t i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
printf("\n");
}
// Call with: print_array(arr, sizeof(arr) / sizeof(arr[0]));
A surprising but accurate C fact
In addition to 2[arr] being valid, pointer arithmetic in C is scaled by the type's size automatically.[1] When you add 1 to an int *, the physical memory address increases by 4 bytes (on most modern systems), not 1 byte.[1] When you add 1 to a char *, the address increases by 1 byte. The compiler handles the scaling invisibly.
Reader experiment
Take the lesson.c program and modify the pointer type. Try casting the array to a char * before doing the arithmetic:
char *byte_ptr = (char *)arr;
printf("Offset by 1 byte: %d\n", *byte_ptr);
printf("Offset by 4 bytes: %d\n", *(byte_ptr + 4));
What do you see? Assuming a typical little-endian machine with 4-byte integers, stepping forward by exactly 4 bytes will land you perfectly on the second integer (20). Stepping by 1 byte will show you the internal byte representation of the number 10.
Challenge
Write a function that takes a pointer to an array of integers and its size, and returns a pointer to the last element of the array, using only pointer arithmetic (no square brackets). Call the function and print the value it points to.
View Solution
#include <stdio.h>
int *get_last_element(int *arr, size_t size) {
if (size == 0) return NULL;
// Offset by size - 1 to reach the final valid element
return arr + (size - 1);
}
int main(void) {
int data[4] = {7, 14, 21, 28};
int *last = get_last_element(data, 4);
if (last) {
printf("Last element: %d\n", *last);
}
return 0;
}
How Hermes compiled and verified the lesson
Hermes drafted the C code to demonstrate array pointer decay and the 2[arr] syntax. The code was saved to a temporary file and compiled natively on an ARM64 Arch Linux host using GCC with -std=c17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined. The successful execution proved the compiler accepts the syntax without warnings. A secondary program intentionally attempting an out-of-bounds read was also compiled and run, successfully triggering the expected AddressSanitizer stack-buffer-overflow abort to verify the undefined behavior claims.
![Editorial illustration for Why 2[arr] is Valid C: Pointers, Arrays, and Memory Arithmetic](https://liberpulse.com/wp-content/uploads/2026/10/why-2-arr-is-valid-c-pointers-arrays-and-memory-arithmetic-202610021204.png)
Leave a Reply