TRT
Sign in

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.

FreeAbout 17 minutes

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

Array decay during function call parameter passstep 1 of 4
int nums[3] = {10, 20, 30};size_t full_sz = sizeof(nums);int *p = nums;int val = nums[1];
  1. slot 0nums[0]: 10(looked at in this step)
  2. slot 1nums[1]: 20(looked at in this step)
  3. slot 2nums[2]: 30(looked at in this step)
  4. slot 3full_sz: unset
  5. 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:

c
#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:

text
Inside main:     40 bytes
Inside function: 8 bytes

Inside 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:

c
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.
text
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:

c
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:

  1. When passed to sizeof: The sizeof operator 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.
  2. When operand to the address-of operator (&): The expression &nums does not yield an int **. It evaluates to the address of the entire array with the type int (*)[3] (pointer to an array of 3 ints).
  3. 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:

c
(*((E1) + (E2)))

When you write nums[1]:

  1. nums decays to a pointer to its first element (0x7ffd20).
  2. The addition nums + 1 scales the integer offset by sizeof(int) (4 bytes), arriving at address 0x7ffd24.
  3. The dereference operator * reads the 4-byte value stored at that address (20).

Because arithmetic addition is commutative (a + b = b + a):

c
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:

c
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:

assembly
movl (%rdi, %rsi, 4), %eax

This 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:

PropertyImplementationImpact
Sequential accessContiguous memory allocationHigh cache hit rate; hardware prefetchers stream data into L1 cache
Parameter passingDecays to 8-byte pointerSingle-cycle register pass; zero data copying
Runtime bounds trackingNon-existent in standard CNo runtime bounds-checking overhead; high risk of buffer overflow
Size preservationLost upon decayRequires 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] pulls arr[1] through arr[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:

c
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:

c
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:

c
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 + 1 advances the address by 1 x sizeof(int) = 4 bytes (pointing to arr[1]).
  • p2 + 1 advances 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:

c
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

Something wrong with this lesson? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save