Skip to main content

Welcome to a Grasp lesson

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

Lesson 16

Passing Arrays to Functions: Pointer Decay Explained

Recommended resources

Hello! Welcome to your next lesson.

In our last session, we drew a clear line between char arr[] = "string" and char *p = "string". We saw that while an array name is not a pointer, it can be used to produce a pointer to its first element. Today, we will formalize this conversion, known as "array decay," and explore its critical implications when passing both 1D and 2D arrays to functions.

This is a fundamental concept in C. A solid grasp of it is often what separates candidates who can simply use arrays from those who truly understand C's memory model and type system—a distinction your interviewers will surely be looking for.

Your learning outcome is to pass 1D and 2D arrays to functions using pointer notation and explain the mechanism of pointer decay.


The Principle of Array Decay

At its heart, "array decay" is a simple rule: except in a few specific cases, whenever a C compiler encounters an expression of an "array type," it implicitly converts that expression into a pointer to the array's first element.

This "decay" signifies a loss of information: the array's type (which includes its size) is lost, and all that remains is a pointer to a single element type.

To understand this rule fully, it's just as important to know the exceptions.

What is array-to-pointer conversion aka. decay?

This highly-rated Stack Overflow answer provides the formal definition of array decay straight from the C standard and lists the key exceptions. This is the level of precision expected from an experienced engineer.

Please read the answer that begins with the quote from the C99 standard ('Except when it is the operand of the sizeof operator...'). Focus on the three exceptions it lists, as they are the cornerstones of understanding this topic.

As you just read, array decay does not happen when the array expression is an operand of:

  1. The sizeof operator.
  2. The unary & (address-of) operator.
  3. A string literal used to initialize a character array.

The sizeof exception is the most direct way to observe decay in action. Let's see it in code.




#include <stdio.h>

// The parameter 'arr' LOOKS like an array, but it has decayed to a pointer.
void process_array(int arr[]) {
    // This will print the size of a pointer, not the original array.
    printf("Inside function, sizeof(arr) = %zu bytes\n", sizeof(arr)); 
}

int main(void) {
    int my_array[10] = {0};

    // Here, 'sizeof' is applied directly to the array type, so no decay occurs.
    printf("In main, sizeof(my_array) = %zu bytes\n", sizeof(my_array)); // Prints 40 (10 * sizeof(int))
    
    process_array(my_array); // 'my_array' decays to a pointer before being passed.
    
    return 0;
}

On a 64-bit system, this code will likely output 40 and then 8, clearly demonstrating the "loss of size information" when the array is passed to the function.



Passing 1D Arrays to Functions

Because of array decay, when you pass a 1D array to a function, what the function actually receives is a pointer to the array's first element. This is why the following three function prototypes are completely equivalent to the compiler:

// Version 1: Explicit size (the 10 is ignored by the compiler and serves only as documentation)
void func(int arr[10]);

// Version 2: Unspecified size (more honest about the lack of size info)
void func(int arr[]);

// Version 3: Pointer notation (the most explicit and accurate representation)
void func(int *arr);

All three declare a function that takes a single parameter of type int*. Inside the function, you can use arr[i] syntax, but remember this is just syntactic sugar for pointer arithmetic: *(arr + i).

Interpretation of Array Indexing in Pointer Notation
This image illustrates how `a[2]` is interpreted by the compiler as `*(a + 2)`. When an array decays to a pointer, this pointer arithmetic is what allows element access within a function.


Passing 2D Arrays: The Critical Details

This is where a deeper understanding is essential. Let's consider a 2D array:
int matrix[3][4];

In memory, this is a single, contiguous block of 3 * 4 = 12 integers, laid out in row-major order.

When the expression matrix decays, it converts to a pointer to its first element. What is the "first element" of matrix? It is the first row, matrix[0], which is itself an array of 4 integers.

Therefore, the decayed type is not int** (a pointer to a pointer to an int), but rather int (*)[4] (a pointer to an array of 4 ints).

This distinction is crucial and is a very common interview topic.

Function Declarations for 2D Arrays

Because the compiler needs to know the size of a row to perform pointer arithmetic (matrix + 1 must advance the pointer by the size of one row), the column dimension must be specified in the function prototype.

The following declarations are equivalent for passing our int matrix[3][4]:

// Version 1: Full dimensions (3 is documentation, 4 is required)
void process_2d(int mat[3][4]);

// Version 2: Omitting the first dimension (most common)
void process_2d(int mat[][4]);

// Version 3: Explicit pointer-to-array notation (most accurate)
void process_2d(int (*mat)[4]);

Why is int **mat wrong for a 2D array?
An int ** is a pointer to a pointer. It expects to dereference once to get a pointer (int*), and a second time to get the int value. This matches the memory layout of a jagged array (an array of pointers), but not a true 2D array which is stored in a single contiguous block.

2D Array Memory Layout Comparison
This image visually contrasts the memory layout of a true 2D array ('Contiguous Memory') with an array of pointers. Passing a contiguous 2D array to a function expecting an `int **` will cause the function to misinterpret memory addresses, leading to undefined behavior.

The following reading provides excellent code examples and reinforces the distinction between a pointer-to-an-array and a pointer-to-a-pointer.

Pointers and 2D Arrays

The article 'Pointers and 2D Arrays' clearly explains the pointer arithmetic for 2D arrays and provides code examples for passing them to functions.

Start by reading the section that begins 'Note that int (*ptr)[10];'. This clearly differentiates a pointer-to-an-array from an array-of-pointers. Next, review the third code example in the article (under the second 'OR'), which starts with void display (int (*)[3]);. This demonstrates exactly how to pass a 2D array using pointer notation. Finally, read the 'Note' at the very end of the article, which explicitly warns against using a double pointer (int **mat) for a 2D array. This is a key takeaway.

Here's a complete example you can walk through, combining these concepts:




#include <stdio.h>

// The parameter 'mat' is a pointer to an array of 4 integers.
void print_matrix(int (*mat)[4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            // mat[i][j] is syntactic sugar for *(*(mat + i) + j)
            // It works because 'mat' is a pointer to an array,
            // so 'mat + i' correctly advances to the i-th row.
            printf("%d ", mat[i][j]);
        }
        printf("\n");
    }
}

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

    // The expression 'matrix' decays to type 'int (*)[4]' and is passed.
    print_matrix(matrix, 3);
    
    return 0;
}


Conclusion

You are now prepared to explain not just how to pass arrays to functions, but why it works the way it does, rooted in the C type system and memory model.

Key Takeaways:

  • Array Decay: An array expression is implicitly converted to a pointer to its first element, except when used with sizeof, &, or as a string literal initializer.
  • Passing 1D Arrays: When passing a 1D array, the function receives a pointer (int*). All size information is lost.
  • Passing 2D Arrays: When passing a 2D array arr[R][C], the function receives a pointer to an array (int (*)[C]). The compiler must know the column dimension C to calculate row offsets correctly.
  • 2D Array vs. Double Pointer: A true 2D array (int mat[R][C]) has a contiguous memory layout and is fundamentally different from an int **, which points to an array of pointers. Using one for the other leads to undefined behavior.

We've just established that a 2D array is not a double pointer. In our next lesson, we will explore what double pointers (int **) actually are, how they are correctly used (e.g., to modify another pointer's value), and how they enable you to build dynamic data structures like an array of strings (char *argv[]).