Topic 66 of 100 Arrays and Strings in C Interactive Tutorial

Passing Arrays to C Functions: Methods and Example Programs

12 mins read Last updated: 22 Aug 2026 21,600 views
C Compiler 0.00%

What is Passing Arrays to C Functions? Core Concept & Direct Summary

Core Objective & Problem Statement

How are arrays passed to functions via pointer decay, and how can array modifications in functions persist?

Direct Answer Summary

By this point in the Arrays cluster, three separate tutorials have bumped into the same fact and promised to explain it later: when you pass an array to a function, you aren't really passing the array. You're passing a pointer to its first element. That single design decision explains why sizeof fails inside functions, why array parameters need a separate size argument, and why a function can modify your array even though C is pass-by-value.

This page is the payoff. We'll cover the parameter syntaxes, what actually gets passed, how to modify arrays through functions, const protection, the 2D array case, the "how do I return an array?" question, and a couple of complete programs that pull it all together.

(Suggested image: the hero diagram — main() holding a 5-element array with a single arrow labeled "address of element 0" pointing to a function box, and a return arrow labeled "nothing to give back")


How Does Passing Arrays to C Functions Work? In-Depth Mechanics & Architecture

The Syntax: Three Ways to Write It

All three of these declarations mean exactly the same thing to the compiler:

main.c
Run Online
void process(int arr[], int n);
// bracket formvoid process(int arr[10], int n);
// bracket with a sizevoid process(int *arr, int n);
// pointer form
  • int arr[] — the friendliest, most common way to signal "this is an array of ints"
  • int arr[10] — the 10 is completely ignored by the compiler. It's pure documentation, and arguably misleading documentation, since the function will happily accept arrays of any length
  • int *arr — the honest, mechanical truth: it's a pointer

Pick the bracket form for readability in most code; reach for the pointer form when the function is genuinely pointer-oriented (doing arithmetic rather than indexing). Just know that they're interchangeable, and the compiler doesn't care which you choose.

And in every case, remember the size parameter. As we worked through in the array-size tutorial, the function has no way to know how long the array is — sizeof won't help — so the count travels separately:

main.c
Run Online
void printArray(int arr[], int n);
// ✓ the standard C convention

(Suggested image: the three parameter forms shown as three code cards with an equals sign between them and a note "compiler sees these as identical")


What Actually Gets Passed: The Pointer

Recall the memory model from the arrays pillar page: array elements sit contiguously, and the array name acts as the address of element 0. So when you call printArray(marks, 5), what travels into the function is a single memory address — not 20 bytes of copied data.

main.c
Run Online
#include <stdio.h>
void showAddress(int arr[]) {
    printf("Address received inside function : %p\n",(void *)arr);
}
int main(void) {
    int marks[5] = {
        45, 62, 78, 34, 90
    };
    printf("Address of marks in main         : %p\n",(void *)marks);
    printf("Address of marks[0] in main      : %p\n",(void *)&marks[0]);
    showAddress(marks);
    return 0;
}

Typical output:

main.c
Run Online
Address of marks in main : 0x7ffd3c1a4b20Address of marks[0] in main : 0x7ffd3c1a4b20showAddress received inside function : 0x7ffd3c1a4b20

All three addresses are identical. The array name is the address of its first element, and that's precisely what the function receives. No copy of the data was made.

Two big consequences:

  1. Efficiency. Passing a 10,000-element array to a function costs one pointer's worth of copying — not 40,000 bytes. Arrays are effectively passed "by reference," cheaply, whether you intended that or not.
  2. The function can modify your data. Since it holds the address, writing through it changes the original array — despite C being pass-by-value. This is the whole reason arrays behave differently from plain variables.

Modifying Arrays Inside Functions

A regular variable passed to a function gets copied, so the original is safe:

main.c
Run Online
void tryToChange(int x) {
    x = 999;
    // changes the copy only
}

But an array element accessed through a passed pointer writes to the original memory:

main.c
Run Online
#include <stdio.h>
void doubleThem(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        arr[i] *= 2;
        // modifies the CALLER's array!    }}
        int main(void) {
            int values[4] = {
                10, 20, 30, 40
            };
            doubleThem(values, 4);
            for (int i = 0; i < 4; i++) {
                printf("%d ", values[i]);
                // 20 40 60 80 — changed in place!    }    putchar('\n');
                return 0;
            }

Output:

The function never returned anything — yet main's array came back doubled. That's the pointer in action.

This is enormously useful: in-place sorting, filling arrays from input, normalizing data, swapping elements. Any function whose job is "change this collection" uses this mechanism.

(Suggested image: the before/after diagram — main's array boxes, an arrow into the function, the function writing new values into those same boxes, then main seeing the new values. Emphasize "same memory, not a copy.")


Protecting Arrays With const

Because functions can modify arrays, a careless function can wreck your data — or worse, you assume it won't and get surprised. C's answer is const on the parameter:

main.c
Run Online
void printArray(const int arr[], int n) {
    for (int i = 0;
    i < n;
    i++) {
        printf("%d ", arr[i]);
    }
    putchar('\n');
}

Now the compiler enforces the promise:

main.c
Run Online
arr[0] = 99;
// ✗ compile error — cannot modify a const array

Why this deserves to be a habit, not an afterthought:

  • It documents intent. Reading the prototype tells you instantly whether the function reads or writes your data.
  • It prevents bugs. Accidental writes become compile errors instead of silent corruption.
  • It enables optimization. The compiler knows the data won't change and can optimize accordingly.
  • It's self-enforcing documentation — unlike a comment saying "don't modify this," which nothing checks.

The professional rule: if a function doesn't need to modify your array, mark it const — and if a function does need to modify it, make that obvious in the name (doubleThem, sortArray, fillWith) so callers know what they're signing up for.

(Suggested image: a "read-only" lock icon on a const array parameter vs an "editable" pencil icon on a non-const one)


The "How Do I Return an Array?" Question

Short answer: you can't — not directly.

main.c
Run Online
int[] getArray() {
    // ✗ ILLEGAL — no array return types in C    int arr[5] = {
    1,2,3,4,5
}
;
return arr;
// (and this would be doubly wrong)
}
int *getArray() {
    // ⚠️ compiles... but DANGEROUS    int arr[5] = {
    1,2,3,4,5
}
;
return arr;
// ✗ returns a pointer to DEAD memory!
}

That second version is one of the most infamous beginner traps in C. It compiles (maybe with a warning). It might even appear to work in a small test — and then fails mysteriously later. Here's why: arr is a local variable that lives on the stack; when the function returns, that memory is reclaimed. The pointer you returned now points at a stack frame that no longer belongs to you. Anything can overwrite it.

The three correct approaches:

Approach 1: The caller supplies the array (output parameter)

The standard C idiom. The caller owns the memory; the function fills it:

main.c
Run Online
#include <stdio.h>
void fillSquares(int arr[], int n) {
    // caller provides the array    for (int i = 0; i < n; i++) {        arr[i] = (i + 1) * (i + 1);    }}
    int main(void) {
        int squares[5];
        // memory lives in main
        fillSquares(squares, 5);
        for (int i = 0; i < 5; i++) {
            printf("%d ", squares[i]);
            // 1 4 9 16 25    }    putchar('\n');    return 0;}

Output:

This is how most C library functions work — fgets fills your buffer, qsort rearranges your array in place. The caller owns storage; the function does the work.

Approach 2: static local array

Works, with a serious caveat:

main.c
Run Online
int *getPrimes(void) {
    static int primes[5] = {
        2, 3, 5, 7, 11
    }
;
    // static — survives the return    return primes;                              // ✓ safe
}

The static keyword (from our storage classes tutorial) gives the array program-long lifetime, so returning its address is legitimate. The catch: there's only one copy, shared across all calls. Call getPrimes() twice and both pointers refer to the same memory. It also isn't thread-safe. Fine for a constant lookup table; wrong for anything that needs fresh data per call.

Approach 3: Dynamic allocation with malloc

The fully flexible answer, and the only one that lets each call return an independent array:

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
int *createSquares(int n) {
    int *arr = malloc(n * sizeof(int));
    // heap memory — outlives the function    if (arr == NULL) {        return NULL;                            // allocation failed — always check!    }
    for (int i = 0; i < n; i++) {
        arr[i] =(i + 1) *(i + 1);
    }
    return arr;
    // ✓ safe — caller must free() it}
    int main(void) {
        int *sq = createSquares(5);
        if (sq != NULL) {
            for (int i = 0; i < 5; i++) {
                printf("%d ", sq[i]);
            }
            putchar('\n');
            free(sq);
            // ← YOUR responsibility    }
            return 0;
        }

Output:

This is the real answer for "return an array from a function" — but it comes with the ownership rule: whoever allocates, frees; or whoever receives, frees. Forget the free() and you have a memory leak. (Dynamic allocation gets its own full tutorial in the Pointers cluster; this is the preview.)

(Suggested image: a memory diagram comparing the three approaches — stack array dying on return, static array persisting, heap array persisting until free() — with a "danger" marker on the returning-stack-pointer case)


Passing 2D Arrays to Functions

Two-dimensional arrays have a stricter rule (which you saw in the 2D and multidimensional tutorials): the column count is mandatory in the parameter:

main.c
Run Online
void printMatrix(int mat[][4], int rows) {
    // ✓ columns REQUIRED    for (int r = 0;
    r < rows;
    r++) {
        for (int c = 0;
        c < 4;
        c++) {
            printf("%4d", mat[r][c]);
        }
        putchar('\n');
    }
}
void bad(int mat[][], int rows);
// ✗ ERRORvoid alsoBad(int mat[3][], int rows);
// ✗ ERROR

Why? To compute mat[r][c], the compiler must know how many integers are in each row — element [r][c] lives at offset (r × COLS + c). Without COLS, it can't do the arithmetic. It's the same underlying reason int bad[3][] can't be declared.

The #define/constant habit keeps this clean and self-updating:

main.c
Run Online
#define COLS 4
void printMatrix(int mat[][COLS], int rows) {
    ...
}

One more option for true flexibility: passing a pointer to the first element and doing the index math yourself, which lets the dimensions be fully runtime values:

main.c
Run Online
void printMatrixFlat(int *mat, int rows, int cols) {
    for (int r = 0;
    r < rows;
    r++) {
        for (int c = 0;
        c < cols;
        c++) {
            printf("%4d", mat[r * cols + c]);
            // our own row-major math
        }
        putchar('\n');
    }
}
int main(void) {
    int m[2][3] = {
{
            1,2,3
        }
        , {
            4,5,6
        }
    }
;
    printMatrixFlat(&m[0][0], 2, 3);
    // ✓ works with any size    return 0;
}

That r * cols + c formula is the row-major order formula from the 2D page — and this flattening approach is how a lot of real matrix and image code handles variable dimensions.


A Complete Example: A Small Statistics Library

Everything from this page, doing real work:

main.c
Run Online
#include <stdio.h>
// --- Functions taking const arrays: they only READ ---
void printArray(const int arr[], int n) {
    printf("[ ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("]\n");
}
int findMax(const int arr[], int n) {
    int max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] > max) max = arr[i];
    }
    return max;
}
int findMin(const int arr[], int n) {
    int min = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] < min) min = arr[i];
    }
    return min;
}
double average(const int arr[], int n) {
    int total = 0;
    for (int i = 0; i < n; i++) total += arr[i];
    return(double)total / n;
}
// --- Functions that WRITE to the array: no const ---
void scaleArray(int arr[], int n, int factor) {
    for (int i = 0; i < n; i++) {
        arr[i] *= factor;
    }
}
void reverseArray(int arr[], int n) {
    for (int i = 0, j = n - 1; i < j; i++, j--) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}
// --- Caller-supplied output array: the "return an array" pattern ---
void copyArray(const int src[], int dest[], int n) {
    for (int i = 0; i < n; i++) {
        dest[i] = src[i];
    }
}
int main(void) {
    int data[] = {
        45, 62, 78, 34, 90, 55
    };
    int n = sizeof(data) / sizeof(data[0]);
    // sizeof works HERE (real array)
    int backup[6];
    puts("=== Original ===");
    printArray(data, n);
    printf("Max     : %d\n", findMax(data, n));
    printf("Min     : %d\n", findMin(data, n));
    printf("Average : %.2f\n", average(data, n));
    puts("\n=== After scaling by 2 ===");
    scaleArray(data, n, 2);
    printArray(data, n);
    puts("\n=== After reversing ===");
    reverseArray(data, n);
    printArray(data, n);
    puts("\n=== Backup copy (made before scaling) ===");
    // Note: we'd need to copy BEFORE modifying; shown here to demonstrate the pattern
    copyArray(data, backup, n);
    printArray(backup, n);
    return 0;
}

Sample Run:

main.c
Run Online
=== Original ===
[ 45 62 78 34 90 55 ]
Max : 90
Min : 34
Average : 60.67
=== After scaling by 2 ===
[ 90 124 156 68 180 110 ]
=== After reversing ===
[ 110 180 68 156 124 90 ]
=== Backup copy(made before scaling) ===
[ 110 180 68 156 124 90 ]

Read the function signatures as a map of intentions:

  • const int arr[], int n → "I only read your data"
  • int arr[], int n → "I will change your data"
  • int dest[], const int src[], int n → "I'll copy src into dest"

And notice sizeof is used only in main, where data is a genuine array — never inside a function. That's the discipline from the array-size tutorial, applied.

(One small note on the demo: the "backup" copy happens after the modifications, so it copies the modified data. If you wanted a true pre-modification backup, you'd call copyArray earlier. I've kept the order as written because it demonstrates the pattern clearly, but a careful instructor would move it up — worth adjusting if you publish the code.)


Common Array-Passing Mistakes

  1. Using sizeof inside the function to find the length — arr is a pointer there; you get 8 (or 4), not the element count. Pass the size in. This is the mistake.
  2. Forgetting the size parameter entirely — the function compiles, then loops past the end of the array because it has no idea where "end" is.
  3. Returning a pointer to a local (non-static) array — the classic dangling-memory bug. Use output parameters, static, or malloc.
  4. Modifying an array you didn't mean to modify — since passing is by address, any function can change your data. Use const on read-only parameters and check the signatures you call.
  5. Omitting the column size for a 2D array parameter — void f(int m[][]) doesn't compile. Column count is always required.
  6. Confusing arr[i] and *arr semantics in pointer-form parameters — arr[i] is element i; *arr is just element 0. Related, but not the same thing.
  7. Forgetting to free memory allocated in a creating function — if a function returns a malloc'd array, the caller owns it and must free it.
  8. Assuming a function that takes const int arr[] accepts a modified pointer elsewhere — const is a promise the compiler enforces at that boundary; it doesn't make the underlying data immutable for everyone.

(Suggested image: "8 Array-Passing Mistakes" checklist graphic — consistent with the series)


Final Thoughts

Passing arrays to functions comes down to one realization: arrays travel as addresses, not as copies. Everything else — needing a size parameter, functions being able to modify your data, sizeof failing inside the function, const mattering — is a direct consequence of that single fact.

The habits worth adopting permanently: every array parameter comes with an int n, every read-only array parameter gets const, the caller owns the memory (so return arrays via output parameters or malloc, never by returning a local's address), and 2D array parameters always name their column count.

Get those four right and your array functions will be correct and readable by default — which puts you well ahead of most people writing C.

Next in this cluster: working with arrays of strings, where arrays, characters, and pointer-to-pointer syntax all meet.

Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
// Modifies elements in place
void doubleElements(int *arr, size_t n) {
    for (size_t i = 0; i < n; i++) {
        arr[i] *= 2;
    }
}
int main() {
    int nums[] = {
        5, 10, 15, 20
    };
    size_t len = sizeof(nums) / sizeof(nums[0]);
    doubleElements(nums, len);
    printf("Doubled array:\
");
    for (size_t i = 0; i < len; i++) {
        printf("%d ", nums[i]);
    }
    printf("\
");
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Always mark input-only array parameters with const (const int *arr) to enforce read-only immutability.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: Can you pass an array by value (full copy) in C?

A: Not directly as a bare array. However, wrapping an array inside a `struct` allows it to be passed and returned completely by value.
Q

Q: Why should read-only array parameters be marked `const`?

A: It informs both the compiler and the programmer that the function will not alter caller data and allows passing constant arrays.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations