Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 9

Arrays and Pointers in C: Understanding Decay

Recommended resources

Hello! Welcome back to our second module on pointers.

In the previous lesson, we mastered the mechanics of pointer arithmetic—incrementing, decrementing, and calculating offsets. We saw how adding 1 to a pointer doesn't mean adding one byte, but rather advancing the pointer by the size of the data type it points to. This concept is the key that unlocks the deep relationship between pointers and arrays in C.

Today's lesson addresses a foundational concept that is often a source of confusion for even experienced C programmers: the relationship between arrays and pointers, including the concept of "array decay." Your goal is to be able to explain this relationship precisely. Understanding this is not just an academic exercise; it's essential for correctly interpreting memory layouts, writing efficient data-processing code, and passing technical interviews.

The Myth: "Arrays Are Just Pointers"

You've probably heard the C mantra: "arrays are pointers." While this is a useful shorthand, it's not strictly true. Believing it literally can lead to confusing paradoxes.

Consider this simple array on a 64-bit system where sizeof(int) is 4:

int arr[5] = {10, 20, 30, 40, 50};

If arr were truly just a pointer, we would expect a few things:

  1. sizeof(arr) should return the size of a pointer, which is 8 bytes. But it returns 20.
  2. We should be able to increment arr to point to the next element, like arr++. But the compiler throws an error.

Why does this happen? The truth is more nuanced and revolves around a mechanism called array decay.

To get a clear, step-by-step explanation of these paradoxes and the underlying rules, let's watch a short video that tackles this question directly.

Are arrays just pointers in C?

This video from Oceano does an excellent job of setting up the 'array vs. pointer' problem and then methodically explaining the solution by referring to the C standard.

Watch the entire video (about 10 minutes). Pay close attention to: The surprising results of sizeof(array) and array++. The formal C standard definition of 'array decay'. The critical difference in type between an array name (arr) and the address of the whole array (&arr). The visual demonstration of how this type difference impacts pointer arithmetic.


Unpacking Array Decay

As the video explained, the key to understanding the array-pointer relationship is array decay. The C standard specifies that an expression with an "array of type" is, in most contexts, converted to an expression with a "pointer to type" that points to the first element of the array.

Let's look at the formal rule (from C99, 6.3.2.1/3):

Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type “array of type” is converted to an expression with type “pointer to type” that points to the initial element of the array object and is not an lvalue.

This is why you can do this:

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // This works!

In the assignment p = arr;, the array name arr is not an operand of sizeof or &. Therefore, it "decays" into a pointer to its first element. The type of the expression arr effectively becomes int *, and its value is the memory address of arr[0].

This decay is also the reason why array subscripting arr[i] is defined to be equivalent to *(arr + i). In the second expression, arr decays to a pointer, pointer arithmetic is performed, and the result is dereferenced.


The Exceptions to the Rule

The most important part of the rule is the list of exceptions. In these specific cases, an array does not decay and remains an object of type "array of type".

1. The sizeof Operator

When an array name is the operand of sizeof, it does not decay. The operator acts on the array object itself.

int arr[5];
int *p = arr;

// sizeof(arr) returns the total size of the array in bytes.
// 5 elements * 4 bytes/int = 20 bytes.
size_t array_size = sizeof(arr); // array_size is 20

// sizeof(p) returns the size of a pointer variable.
// On a 64-bit system, this is 8 bytes.
size_t pointer_size = sizeof(p); // pointer_size is 8

This is the clearest proof that an array and a pointer are fundamentally different types.

2. The Unary & Operator

When you use the address-of operator on an array name, it also does not decay. Instead, &arr gives you a pointer to the entire array object.

This creates a different type of pointer.

Memory Representation of an Array
An array `array` with 5 integer elements in contiguous memory. The base address, `0x1000`, is the value for three different expressions: the decayed array name `array`, the address of the first element `&array[0]`, and the address of the entire array `&array`.

Let's look at the types:

int arr[5] = {10, 20, 30, 40, 50};

// 'p_elem' is a 'pointer to int'.
// This works because 'arr' decays.
int *p_elem = arr; 

// 'p_arr' is a 'pointer to an array of 5 ints'.
// The parentheses are crucial.
int (*p_arr)[5] = &arr; 

Both p_elem and p_arr will hold the exact same memory address. So what's the difference? The type. This distinction becomes critical when you perform pointer arithmetic.

  • p_elem + 1 (or arr + 1): Moves forward by sizeof(int). It points to the second element, arr[1].
  • p_arr + 1 (or &arr + 1): Moves forward by sizeof(int[5]). It points to the memory location immediately after the entire array.

To solidify this, let's see how a debugger confirms this type difference.

Array and pointers equivalence myth

This blog post provides a concise C program and shows the output from the GDB debugger, which explicitly states the different types for variables that all hold the same memory address.

Read the short article, focusing on the C code that declares p_to_first_element and p_to_array. Then, examine the GDB output section which shows their distinct types (char * vs. char (*)[]) despite having the same address value.

Test your understanding!

Given the following code on a system where sizeof(int) is 4:

#include <stdio.h>

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

    // Assume the base address of arr is 0x7ffc1234abcd0
    printf("Address 1: %p\n", (void*)arr);
    printf("Address 2: %p\n", (void*)&arr[0]);
    printf("Address 3: %p\n", (void*)&arr);

    printf("Address 4: %p\n", (void*)(arr + 1));
    printf("Address 5: %p\n", (void*)(&arr + 1));

    return 0;
}
  1. What will be printed for "Address 1", "Address 2", and "Address 3"? Why?
  2. What will be printed for "Address 4"?
  3. What will be printed for "Address 5"?
Show answer
  1. All three will print the same address: 0x7ffc1234abcd0.
    • arr (Address 1) decays to a pointer to the first element.
    • &arr[0] (Address 2) is explicitly the address of the first element.
    • &arr (Address 3) is the address of the array object itself, which by definition starts at its first element.
  2. "Address 4" will be 0x7ffc1234abcd4. This is because arr decays to int*, so adding 1 moves the pointer by sizeof(int), which is 4 bytes.
  3. "Address 5" will be 0x7ffc1234abce4. This is because &arr is of type int (*)[5] (a pointer to an array of 5 ints). Adding 1 moves the pointer by the size of the entire object it points to, which is sizeof(int[5]) or 5 * 4 = 20 bytes. In hexadecimal, 0xd0 + 20 (decimal) is 0xd0 + 0x14, which equals 0xe4.

Conclusion

Today we've dismantled the "arrays are pointers" myth and replaced it with a more precise understanding of their relationship.

Key Takeaways:

  • Arrays are not pointers. They are distinct types. An array is a contiguous block of memory that holds elements, while a pointer is a variable that stores a memory address.
  • In most expressions, an array name decays into a pointer to its first element. This is why pointer arithmetic and array indexing are often interchangeable (*(arr+i) vs arr[i]).
  • The decay rule has important exceptions: when an array is an operand of sizeof or the unary & operator.
  • Using & on an array (&arr) creates a pointer to the entire array, which has a different type (T (*)[N]) from a pointer to the first element (T *). This type difference changes the behavior of pointer arithmetic.

In our next lesson, we will put this knowledge into practice. We'll focus on how to use both array subscripting and pointer arithmetic interchangeably to access and manipulate array elements, a skill that is fundamental to writing clean and efficient C code.