Passing arrays to functions is one of the most fundamental yet misunderstood topics in C programming. Unlike primitive scalar types (like int or float) which are passed by value, arrays in C cannot be passed directly by value as a complete block of memory. Instead, an array name automatically decays into a pointer to its first element when passed as a function parameter.

Array Parameter Syntax Quick Reference

In function declarations, the three array parameter syntaxes below are identical to the C compiler—all three receive a memory address pointer:

array_parameter_signatures.cc
/* All 3 function prototypes receive a pointer (int *arr) and behave identically */
void process_1(int arr[], size_t size);    /* Unbounded array bracket syntax */
void process_2(int arr[10], size_t size);  /* Size inside brackets is ignored by compiler */
void process_3(int *arr, size_t size);     /* Explicit pointer syntax (preferred in production) */

Understanding Pointer Decay: Memory Layout Mechanics

When an array is declared on the stack, it occupies contiguous memory. Passing it to a function copies only the 8-byte memory address of the first element (&arr[0]), leaving the original data in place:

Stack Memory vs Function Parameter Decaytext
+---------------------------------------------------------------------------------+
| CALLER STACK FRAME (main function)                                              |
| int numbers[4] = { 10, 20, 30, 40 };                                            |
| Memory Addr: 0x7fff1000 [ 10 | 20 | 30 | 40 ] (Occupies 16 bytes)                 |
+---------------------------------------------------------------------------------+
                                |
                                | Calling modify_array(numbers, 4)
                                v
+---------------------------------------------------------------------------------+
| CALLEE STACK FRAME (modify_array function)                                      |
| int *arr = 0x7fff1000;  (Receives 8-byte pointer addressing 0x7fff1000)        |
| Dereferencing arr[0] directly mutates caller's memory at 0x7fff1000!            |
+---------------------------------------------------------------------------------+

1. Passing Entire Arrays vs Individual Elements

You can pass an entire array by passing its base pointer, or pass individual elements by value or by reference:

pass_array_vs_element.cc
#include <stdio.h>
 
// 1. Pass by Value: Receives a copy of a single integer element
void print_single_val(int val) {
    printf("Single value copy: %d\n", val);
}
 
// 2. Pass by Reference: Receives a pointer to modify a single element
void update_single_val(int *val_ptr) {
    *val_ptr += 100;
}
 
// 3. Pass Entire Array: Receives pointer to first element and mutates array
void scale_array(int *arr, size_t size, int factor) {
    for (size_t i = 0; i < size; i++) {
        arr[i] *= factor;
    }
}
 
int main(void) {
    int data[] = { 5, 10, 15 };
    size_t len = sizeof(data) / sizeof(data[0]);
 
    print_single_val(data[1]);       // Pass element by value (10)
    update_single_val(&data[0]);     // Pass element by reference (modifies data[0] to 105)
 
    scale_array(data, len, 2);       // Scale entire array
 
    printf("Updated array: %d, %d, %d\n", data[0], data[1], data[2]); // Output: 210, 20, 30
    return 0;
}

Key Insights from This Implementation:

  • Element Copying: Passing data[1] passes a copy of the integer value 10. Modifying val inside print_single_val() has zero effect on the original data array.

  • Address-Of Operator `&`: Passing &data[0] explicitly passes the memory address of the first slot, allowing update_single_val() to mutate the original caller value.

  • Array Length Parameter: Always pass size_t size as a separate argument alongside the array pointer because sizeof(arr) inside a receiving function returns the pointer size (8 bytes), not the full array byte length.

2. Protecting Array Read-Only Access with const Pointers

When a function needs to inspect or search an array without modifying its contents, qualify the pointer parameter with const int *arr. This instructs the C compiler to reject any accidental assignment or mutation:

const_array_parameter.cc
#include <stdio.h>
 
// Function reads array elements safely without side effects
int sum_elements(const int *arr, size_t size) {
    int total = 0;
    for (size_t i = 0; i < size; i++) {
        // arr[i] = 0; // Compiler Error: assignment of read-only location
        total += arr[i];
    }
    return total;
}
 
int main(void) {
    int scores[] = { 88, 92, 79, 95 };
    size_t count = sizeof(scores) / sizeof(scores[0]);
 
    int result = sum_elements(scores, count);
    printf("Total Sum: %d\n", result); // Output: 354
    return 0;
}

Why Const Matters in Production C:

  • API Contract Security: Marking array parameters const guarantees callers that their data structure will remain untouched.

  • Compiler Verification: Attempting to write to arr[i] triggers an immediate compile-time error (read-only location assignment), preventing subtle runtime bugs.

3. Passing 2D Multidimensional Arrays (Row Stride Math)

When passing 2D arrays (int matrix[3][4]), the compiler requires the column size N in the parameter declaration (int arr[][4]) to perform row stride address arithmetic:

pass_2d_array.cc
#include <stdio.h>
 
#define COLS 3
 
// Fixed-column 2D array parameter
void print_matrix_2d(int arr[][COLS], size_t rows) {
    for (size_t i = 0; i < rows; i++) {
        for (size_t j = 0; j < COLS; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
 
// C99 Variable Length Array (VLA) parameter syntax
void print_dynamic_matrix(size_t rows, size_t cols, int matrix[rows][cols]) {
    for (size_t i = 0; i < rows; i++) {
        for (size_t j = 0; j < cols; j++) {
            printf("%02d ", matrix[i][j]);
        }
        printf("\n");
    }
}
 
int main(void) {
    int grid[2][3] = {
        { 1, 2, 3 },
        { 4, 5, 6 }
    };
 
    printf("Fixed 2D Matrix:\n");
    print_matrix_2d(grid, 2);
 
    printf("C99 VLA Dynamic Matrix:\n");
    print_dynamic_matrix(2, 3, grid);
 
    return 0;
}

Under the Hood: Row Stride Address Arithmetic:

  • Row Stride Formula: To locate arr[i][j] in memory, the compiler calculates: element_addr = base_addr + (i * COLS + j) * sizeof(element). Omitting COLS breaks this calculation.

  • C99 VLA Flexibility: In C99, placing size_t rows, size_t cols *before* int matrix[rows][cols] allows functions to process 2D matrices of arbitrary runtime dimensions.

Troubleshooting & Common Pitfalls Checklist

  • Out-of-Bounds Memory Corruption - C does not perform array boundary checking. Passing an incorrect size argument allows function loops to read/write past the allocated stack buffer into adjacent memory.

  • Returning Local Stack Arrays - Never return a pointer to a local automatic array (int local[10]) from a function. Local stack memory is destroyed upon function return, resulting in a dangling pointer.

  • Incompatible Pointer Types in 2D Arrays - Passing int **matrix to a function expecting int matrix[3][4] is invalid because a pointer-to-pointer (int**) does not match contiguous 2D memory stride.

Mastering array decay, pointer arithmetic, and const qualification empowers C developers to build safe, high-performance systems and embedded applications.