Foundations · Module 3 · Lesson 4 of 9
Array Memory Layout and Pointer Decay
Understand contiguous buffer allocation, trace the array-to-pointer decay rule, and inspect the structural difference between arrays and pointers.
By the end you can
- Diagram contiguous memory allocation for flat single-dimensional arrays
- Identify the exact conditions under which an array decays into a pointer
- Differentiate between the three exceptions to decay: sizeof, address-of (&), and string initialization
- Explain why arr[i] is syntactically equivalent to *(arr + i) and i[arr]
Builds on Generic Pointers and Null Pointer Mechanics.
01 · A question first
Why does sizeof(arr) return 40 inside main() for an array of ten integers, but inside a function taking int arr[10] as an argument, sizeof(arr) suddenly returns 8? Understanding array-to-pointer decay reveals how C handles buffers across memory.
Make a guess, then step through the trace below to check it.
02 · Watch it run
int nums[3] = {10, 20, 30};size_t full_sz = sizeof(nums);int *p = nums;int val = nums[1];- slot 0nums[0]: 10(looked at in this step)
- slot 1nums[1]: 20(looked at in this step)
- slot 2nums[2]: 30(looked at in this step)
- slot 3full_sz: unset
- slot 4p: unset
Stack allocates 12 contiguous bytes for nums at 0x7ffd20: slots at 0x7ffd20, 0x7ffd24, and 0x7ffd28.
Machine work: movl 10, -12(%rbp); movl20, -8(%rbp); movl $30, -4(%rbp)
03 · The idea
Start with the problem
One of the most persistent misunderstandings in C programming is the belief that arrays and pointers are identical concepts.
Consider a programmer writing an array utility function:
#include <stdio.h>
void print_buffer_size(int buffer[10]) {
printf("Inside function: %zu bytes\n", sizeof(buffer));
}
int main(void) {
int buffer[10];
printf("Inside main: %zu bytes\n", sizeof(buffer));
print_buffer_size(buffer);
return 0;
}On a 64-bit platform, running this binary outputs:
Inside main: 40 bytes
Inside function: 8 bytesInside main(), the compiler recognized that buffer contains ten 4-byte integers, computing 10 x 4 = 40 bytes. But inside print_buffer_size(), despite writing int buffer[10] in the parameter list, the compiler reports 8 bytes—the size of a single 64-bit pointer.
Now consider another syntax surprise:
int numbers[5] = {10, 20, 30, 40, 50};
printf("%d\n", numbers[2]);
printf("%d\n", 2[numbers]); /* Valid C syntax! */Both lines print 30.
Why did the array lose its size information the instant it crossed a function boundary? And why can an array index swap positions with the array identifier itself?
To resolve these contradictions, you must understand contiguous memory layout, the mechanics of array subscript translation, and the array-to-pointer decay rule.
How the idea works
An array and a pointer are distinct concepts in C:
- An array is a single contiguous block of memory allocated to hold multiple elements of identical type. Its size is baked into its type at declaration.
- A pointer is a variable that stores a memory address.
Array in Memory (int arr[3])
Address: 0x7ffd20 0x7ffd24 0x7ffd28
Value: [ 10 ][ 20 ][ 30 ] (Total: 12 contiguous bytes)
Pointer (int *ptr = arr)
Address: 0x7ffd30
Value: [ 0x7ffd20 ] (Holds 8-byte target address)1. The Contiguous Memory Guarantee
The C standard guarantees that elements of an array are stored in contiguous virtual memory with no padding between elements.
For an array declared as T arr[N]:
- The address of element
arr[0]is the base address of the array. - The address of element
arr[i]is computed as: Address(arr[i]) = Base Address + (i x sizeof(T))
Because elements are contiguous, navigating to an index requires no hash lookups or traversal chains. The CPU computes the address with a single arithmetic instruction.
2. The Array Decay Rule
Under the C standard, an expression of array type implicitly converts ("decays") into a pointer to its first element in almost all expression contexts.
When you reference nums in an expression:
int nums[3] = {10, 20, 30};
int *p = nums; /* nums decays to &nums[0] */The identifier nums evaluates to the address &nums[0]. In step 3 of the trace above, register RAX receives the address 0x7ffd20 and loads it into pointer p.
3. The Three Exceptions to Array Decay
An array expression does not decay into a pointer in only three specific situations:
- When passed to
sizeof: Thesizeofoperator evaluates the full memory footprint of the array object. In step 2 of the trace,sizeof(nums)yields 12 bytes (3 x 4), not 8 bytes. - When operand to the address-of operator (
&): The expression&numsdoes not yield anint **. It evaluates to the address of the entire array with the typeint (*)[3](pointer to an array of 3 ints). - When used as a string literal initializer: In
char str[] = "hello";, the literal does not decay to a pointer; its bytes are copied into the newly allocated array on the stack.
4. Indexing as Pointer Arithmetic
The square bracket operator [] is purely syntactic sugar. Under the C specification, the expression E1[E2] is defined to be identical to:
(*((E1) + (E2)))When you write nums[1]:
numsdecays to a pointer to its first element (0x7ffd20).- The addition
nums + 1scales the integer offset bysizeof(int)(4 bytes), arriving at address0x7ffd24. - The dereference operator
*reads the 4-byte value stored at that address (20).
Because arithmetic addition is commutative (a + b = b + a):
nums[1] == *((nums) + (1)) == *((1) + (nums)) == 1[nums]This explains why 2[numbers] compiles and evaluates identically to numbers[2].
5. Function Parameter Adjustment
In C, an array cannot be passed by value to a function.
When an array type appears inside a function parameter list, the compiler automatically adjusts the declaration into a pointer declaration:
void process(int arr[10]); /* Adjusted to: void process(int *arr); */
void process(int arr[]); /* Adjusted to: void process(int *arr); */
void process(int *arr); /* Identical signature */The dimension 10 is discarded by the compiler. Inside the function body, arr is an ordinary pointer variable. This explains why sizeof(buffer) inside print_buffer_size() reported 8 bytes.
Why it is correct
Understanding why C enforces array decay reveals the architectural constraints of early computer systems.
Preventing Stack Exhaustion
When Dennis Ritchie developed C for the PDP-11 minicomputer, systems operated with mere kilobytes of RAM.
If passing an array of 10,000 integers to a function copied the entire array by value, every function invocation would copy 20,000 to 40,000 bytes onto the limited call stack, causing immediate stack overflow.
By defining arrays to decay into pointers upon evaluation, passing an array to a function copies only an 8-byte pointer into a register, achieving zero-copy performance without explicit address syntax from the caller.
Hardware Addressing Modes
CPUs provide hardware instructions designed around base-plus-offset memory addressing. On x86-64:
movl (%rdi, %rsi, 4), %eaxThis instruction calculates RDI + (RSI x 4) and loads the value into register EAX in a single clock cycle. The equivalence between arr[i] and *(arr + i) directly maps language syntax to native machine capabilities.
What it costs
Array design choices carry concrete performance characteristics:
| Property | Implementation | Impact |
|---|---|---|
| Sequential access | Contiguous memory allocation | High cache hit rate; hardware prefetchers stream data into L1 cache |
| Parameter passing | Decays to 8-byte pointer | Single-cycle register pass; zero data copying |
| Runtime bounds tracking | Non-existent in standard C | No runtime bounds-checking overhead; high risk of buffer overflow |
| Size preservation | Lost upon decay | Requires passing explicit length parameters (size_t len) |
Cache Locality
Because arrays are contiguous, traversing an array linearly maximizes CPU cache efficiency:
- When
arr[0]is accessed, the hardware cache controller loads the entire 64-byte cache line from RAM into the L1 data cache. - For a 4-byte integer array, loading
arr[0]pullsarr[1]througharr[15]into L1 cache simultaneously. - Subsequent accesses read from cache in 1 nanosecond rather than waiting 50 to 100 nanoseconds for main RAM.
Pitfalls and variations
Pitfall 1: sizeof on Function Parameters
Attempting to compute element counts inside a function using sizeof remains one of the most common C bugs:
void bad_print(int data[100]) {
/* BUG: Evaluates to sizeof(int *) / sizeof(int) = 8 / 4 = 2 */
size_t count = sizeof(data) / sizeof(data[0]);
for (size_t i = 0; i < count; i++) {
printf("%d ", data[i]); /* Prints only 2 elements! */
}
}The compiler treats int data[100] as int *data. The element count calculation evaluates to 8 / 4 = 2. Always pass the buffer length explicitly:
void safe_print(const int *data, size_t length) {
for (size_t i = 0; i < length; i++) {
printf("%d ", data[i]);
}
}Pitfall 2: Confusing arr with &arr
While arr and &arr point to the identical numeric byte address in memory, they possess completely different types and pointer arithmetic strides:
int arr[5] = {1, 2, 3, 4, 5};
/* Base address of element 0: type is int * */
int *p1 = arr;
/* Base address of the entire array: type is int (*)[5] */
int (*p2)[5] = &arr;When you perform pointer arithmetic:
p1 + 1advances the address by 1 x sizeof(int) = 4 bytes (pointing toarr[1]).p2 + 1advances the address by 1 x sizeof(int[5]) = 20 bytes (skipping past the entire array).
Pitfall 3: Modifying Array Identifiers
A pointer variable is an lvalue (it has storage that can be reassigned). An array name is not an lvalue:
int nums[5];
int other[5];
int *ptr = nums;
ptr = other; /* VALID: pointer variable reassigned */
nums = other; /* COMPILE ERROR: assignment to expression with array type */
nums++; /* COMPILE ERROR: lvalue required as increment operand */An array identifier represents a fixed memory location allocated on the stack or in static data. It cannot be reassigned or incremented.
Where it is used
The interplay between arrays and pointer decay is central across production systems:
- POSIX System Calls: Low-level I/O functions accept decayed array buffers alongside explicit lengths:
c char buffer[4096]; ssize_t bytes_read = read(fd, buffer, sizeof(buffer)); - High-Performance Audio & Video Codecs: Image and audio buffers are allocated as flat contiguous arrays. Decaying to pointers allows SIMD vector intrinsics (AVX2, NEON) to process 16 or 32 bytes simultaneously.
- Kernel Memory Mapping: Operating systems allocate page directory tables as contiguous arrays of page entries, relying on base-plus-offset calculation to translate virtual addresses.
In the next lesson, we will expand this memory model into two dimensions: understanding how multidimensional arrays are laid out in row-major order and how array pointer syntax scales.
04 · Prove it
Question 1 of 3 · answer from memory
What does evaluating sizeof(arr) return when arr is declared as a function parameter: void print_all(int arr[100])?
05 · Practice
Asked in interviews
Something wrong with this lesson? Report a problem