Topic 72 of 100 Pointers in C Interactive Tutorial

Passing Pointers to Functions in C: Syntax, Benefits, and Examples

12 mins read Last updated: 22 Aug 2026 27,100 views
C Compiler 0.00%

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

Core Objective & Problem Statement

How does passing pointers simulate pass-by-reference, allow functions to mutate caller data, and return multiple results?

Direct Answer Summary

There's a sentence that appears in every C tutorial and, honestly, in just about every C interview: C is pass-by-value. Functions receive copies of their arguments. Change the copy, and the original is untouched.

That's true — and yet C programmers modify variables inside functions constantly. scanf("%d", &x) changes x. fgets(buf, ...) fills your buffer. swap(&a, &b) exchanges them. How?

Because the value being copied is an address. Copy an address and you can still reach the original data through it. That's the whole trick behind "pass by reference" in C — which isn't a separate feature at all, just pass-by-value applied to pointers.

This page covers how to do it, why it matters, when to use it (and when not to), and the risks it introduces. It's the practical counterpart to the pointer arithmetic mechanics from the last tutorial.

(Suggested image: the hero diagram — pass-by-value showing a copied box in the function vs pass-by-pointer showing a copied arrow that leads back to the caller's original box. This contrast is the entire concept.)

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

The Problem: Functions Get Copies

Start with the failure everyone encounters:

main.c
Run Online
#include <stdio.h>
void addTen(int n) {
    n = n + 10;
    printf("Inside function : %d\n", n);
}
int main(void) {
    int score = 50;
    addTen(score);
    printf("Back in main    : %d\n", score);
    // still 50!    return 0;}

Output:

main.c
Run Online
Inside function : 60Back in main : 50

The function computed 60, printed it, and vanished — along with its local n. main's score never changed, because addTen received a copy of the value 50. Modifying a copy does nothing to the original.

This is correct, intentional C behavior. It's also frequently inconvenient — and sometimes fatal, as with swap, where copying makes the task literally impossible.

(Suggested image: two boxes — main's "score = 50" and the function's "n = 60" — with a note that the 60 is thrown away when the function returns)


The Solution: Pass the Address

Hand the function the location instead of the value:

main.c
Run Online
#include <stdio.h>
void addTen(int *n) {
    // parameter is a pointer to int    *n = *n + 10;                // write THROUGH the pointer}
    int main(void) {
        int score = 50;
        addTen(&score);
        // pass the ADDRESS
        printf("Back in main : %d\n", score);
        // 60!    return 0;}

Output:

Two syntax changes make all the difference:

  • At the call:addTen(&score) — pass the address with &
  • In the function:int *n in the parameter, and *n to dereference and write

Now the function receives a copy of the address, but that address points at main's actual score. Writing through it changes the original.

The mental model: passing a value is giving someone a photocopy of a document. Passing a pointer is giving them the document's location. They can still only hold a piece of paper — but that paper leads to your original.

(Suggested image: the arrow version — main's score box, an arrow labeled "copied address" going into the function, then a write arrow going back through it into score)


The Parameter Syntax Forms

Same three-way equivalence we saw with arrays — all identical to the compiler:

main.c
Run Online
void process(int *p);
// ✓ the standard formvoid process(int p[]);
// same thing — array notation is pointer notationvoid process(int p[10]);
// same thing — the 10 is ignored

For a plain single variable, use int *p — it's the honest form. The bracket notations exist for when you're genuinely passing arrays and want to signal that.

Calling convention to remember:

Parameter typeCall with
int nfunc(x) — the value
int *pfunc(&x) — the address
int arr[] (array)func(arr) — already an address, no & needed

That third row is the one that trips people: arrays don't need & because the array name is an address. Single variables do.


The Classic: swap()

This is the program every C course uses to introduce pointer parameters — and it's a good choice, because there is genuinely no other way to write it:

main.c
Run Online
#include <stdio.h>
void swap(int *a, int *b) {
    int temp = *a;
    // save the value at a    *a = *b;                // put b's value into a's location    *b = temp;              // put the saved value into b's location}
    int main(void) {
        int x = 5, y = 10;
        printf("Before : x = %d, y = %d\n", x, y);
        swap(&x, &y);
        // pass addresses
        printf("After  : x = %d, y = %d\n", x, y);
        return 0;
    }

Output:

main.c
Run Online
Before : x = 5, y = 10After : x = 10, y = 5

Why the copy-based version can't work:

main.c
Run Online
void swapBroken(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}
// swaps the copies, then throws them away — caller sees nothing

The pointer version works because *a and *b refer to the caller's storage. temp is a genuine local copy — that's fine and necessary — but the two assignments write to real locations.

(Suggested image: the swap traced in three steps over four memory boxes — main's x, main's y, and the function's a, b pointers — showing the values moving through temp)


Output Parameters: Returning More Than One Thing

A C function returns exactly one value. Pointers are how you return two, three, or ten.

main.c
Run Online
#include <stdio.h>
// Returns success, and writes two results through pointersint divide(int a, int b, int *quotient, int *remainder) {    if (b == 0) {        return 0;                     // failure — outputs untouched    }
*quotient = a / b;
*remainder = a % b;
return 1;
// success}
int main(void) {
    int q, r;
    if (divide(17, 5, &q, &r)) {
        printf("17 / 5 = %d remainder %d\n", q, r);
        // 3 remainder 2    } else {        puts("Division by zero!");    }
        return 0;
    }

Output:

Three design lessons in one small function:

  1. Outputs go through pointers — *quotient and *remainder write into caller-provided storage.
  2. The return value carries status — 1 for success, 0 for failure. This is the standard C convention: use the return value for "did it work?", and pointers for "here are the results." You'll see it everywhere from scanf (!= 1 means bad input) to fopen (NULL means failure).
  3. Outputs are left untouched on failure — a subtle but important design choice. A caller can't accidentally use garbage from a failed call.

The caller's side matters too:main supplies &q and &r — real storage it owns. The function doesn't allocate anything; it just fills what it's given. That ownership discipline (who allocates, who frees) is the same idea from the dynamic memory territory coming later.

(Suggested image: a function with two output arrows going back to the caller's variables, next to a status return arrow — the "one return value + N outputs" pattern)


const Pointer Parameters: Read-Only Promises

When a function takes a pointer but shouldn't modify the target, mark it const:

main.c
Run Online
void printValue(const int *p) {
    printf("%d\n", *p);
    // *p = 99;
    // ✗ compile error — promise enforced
}
int processData(const int *arr, int n) {
    int total = 0;
    for (int i = 0;
    i < n;
    i++) {
        total += arr[i];
        // arr[i] = 0;
        // ✗ compile error
    }
    return total;
}

Why this habit matters more than it looks:

  • Compiler-enforced documentation. Not a comment that can rot — the compiler checks it every build.
  • Prevents whole classes of bugs. An accidental write to the caller's data becomes a compile error instead of silent corruption.
  • Enables optimization. The compiler knows the data won't change and can cache values in registers.
  • Signals intent to readers. Seeing const int * in a prototype tells you instantly: this function reads your data.

The rule: every pointer parameter that doesn't need to modify its target should be const. In well-written C code, most pointer parameters are const.

One detail to be aware of:const int *p means "pointer to const int" — you can't change the value, but you can change what p points at. By contrast int * const p means "const pointer to int" — you can change the value, but not the pointer itself. This subtlety is worth mentioning because the two are easy to conflate; in function parameters, const int * is the common case.


Benefits: Why Bother?

Four real advantages, in rough order of importance:

1. Modifying the caller's data. The only way to do it. Essential for swap, sorting, filling buffers, accumulating results.

2. Multiple return values. As above — the standard C way to return more than one thing.

3. Efficiency with large data. Passing a struct by value copies the entire struct; passing a pointer copies 8 bytes:

main.c
Run Online
void renderBad(struct Scene scene);
// copies the whole scenevoid renderGood(const struct Scene *scene);
// copies an address — much cheaper

For a large struct (say, kilobytes), this is a severe difference. Real C code passes pointers almost everywhere for this reason. const keeps it safe.

4. Enabling generic and callback-based code. Passing function pointers (from the callbacks tutorial) is pointer-passing applied to code itself — the mechanism behind qsort, event handlers, and plug-in architectures.

(Suggested image: a comparison graphic — struct copied "by value" (a big block duplicated) vs "by pointer" (one small arrow) — showing the memory cost difference)


Risks and Responsibilities

Pointers into functions aren't free. They introduce failure modes that plain values don't have.

Risk 1: Dereferencing NULL

main.c
Run Online
void printValue(const int *p) {
    printf("%d\n", *p);
    // if p is NULL → crash
}
printValue(NULL);
// ✗ segfault

Defend with a check:

main.c
Run Online
void printValue(const int *p) {
    if (p == NULL) {
        puts("(null value)");
        return;
    }
    printf("%d\n", *p);
}

Any pointer parameter you didn't create and can't guarantee should be validated. This is why C library functions so often return NULLand accept NULL-checking — defensive programming at the API boundary.

Risk 2: Dangling Pointers

If a caller passes the address of something that then goes out of scope, the pointer becomes dangerous:

main.c
Run Online
int *getLocal(void) {
    int local = 42;
    return &local;
    // ✗ local dies — returned address is invalid
}

This is the same trap from the nested-functions and returning-arrays discussions. The rule: never return or store the address of a local variable.

Risk 3: Aliasing Confusion

Two pointers referring to the same memory is sometimes intentional and sometimes a bug:

main.c
Run Online
void addTo(int *a, int *b) {
    *a += 5;
    *b += 5;
    // if a == b, the value increased by 10!
}
int x = 10;
addTo(&x, &x);
// legal but probably not intended

C doesn't prevent aliasing. Where it could cause surprises, document the assumption ("the two pointers must not alias") or restructure.

Risk 4: Type Confusion

Passing the wrong type through a void * and casting it wrongly produces silent garbage:

main.c
Run Online
void *data = &someInt;
double *d =(double *)data;
// ✗ wrong type — reinterpretation, not conversion

(The void * mechanism needs the cast, but the cast must be correct — C trusts you completely.)

(Suggested image: a checklist graphic of the four risks with a one-line mitigation under each)


A Complete Example: A Small Data-Processing Library

Everything on this page, working together as a realistic module:

main.c
Run Online
#include <stdio.h>
// --- Read-only: const pointer parameters ---void printArray(const int *arr, int n) {    printf("[ ");    for (int i = 0; i < n; i++) {        printf("%d ", arr[i]);    }    printf("]\n");}
// --- Output parameters: two results via pointers, status via return ---int analyze(const int *arr, int n, int *outMin, int *outMax, double *outAvg) {    if (arr == NULL || n <= 0) {        return 0;                            // invalid input — nothing written    }
int min = arr[0], max = arr[0];
long total = 0;
for (int i = 0; i < n; i++) {
    if (arr[i] < min) min = arr[i];
    if (arr[i] > max) max = arr[i];
    total += arr[i];
}
*outMin = min;
*outMax = max;
*outAvg =(double)total / n;
return 1;
}
// --- Modifying: plain pointer parameter ---void scaleValues(int *arr, int n, int factor) {    for (int i = 0; i < n; i++) {        arr[i] *= factor;    }}
// --- Swap: the canonical example ---void swap(int *a, int *b) {    int temp = *a;    *a = *b;    *b = temp;}
// --- Modifying two separate variables through pointers ---void resetBoth(int *a, int *b) {    *a = 0;    *b = 0;}
int main(void) {
    int data[] = {
        45, 62, 78, 34, 90
    };
    int n = 5;
    int min, max;
    double avg;
    puts("=== Original ===");
    printArray(data, n);
    if (analyze(data, n, &min, &max, &avg)) {
        printf("Min : %d\n", min);
        printf("Max : %d\n", max);
        printf("Avg : %.2f\n", avg);
    }
    puts("\n=== After scaling by 2 ===");
    scaleValues(data, n, 2);
    printArray(data, n);
    puts("\n=== Swapping first and last ===");
    swap(&data[0], &data[n - 1]);
    printArray(data, n);
    puts("\n=== Resetting two counters ===");
    int errors = 12, warnings = 7;
    printf("Before: errors=%d, warnings=%d\n", errors, warnings);
    resetBoth(&errors, &warnings);
    printf("After : errors=%d, warnings=%d\n", errors, warnings);
    return 0;
}

Sample Run:

main.c
Run Online
=== Original ===
[ 45 62 78 34 90 ]
Min : 34
Max : 90
Avg : 61.60
=== After scaling by 2 ===
[ 90 124 156 68 180 ]
=== Swapping first and last ===
[ 180 124 156 68 90 ]
=== Resetting two counters ===
Before: errors=12, warnings=7
After : errors=0, warnings=0

Read the prototypes as a map of intention, using the convention from earlier in the series:

PrototypeMeaning
const int *arr, int n"I only read this"
int *arr, int n"I will modify this"
..., int *outMin, int *outMax, double *outAvg"I'll write results here"
int return"This tells you whether it worked"

That's the entire pointer-parameter vocabulary in four rows. Combined with sensible names (outMin names hint that they're outputs), this is how readable C APIs are designed.


Common Mistakes

  1. Forgetting & at the call site — addTen(score) with void addTen(int *n) gives a compiler warning (int vs int*), but if you cast away the warning, you'll write to a bogus address. Always check those warnings.
  2. Forgetting * inside the function — a = b; instead of *a = *b; just reassigns local pointers and changes nothing. Extremely common bug, and it compiles fine.
  3. Adding & for arrays — func(&arr) where func(int arr[]) expects an array decays to a pointer-to-pointer. Compiles with warnings, behaves wrongly.
  4. Dereferencing without a NULL check — crashes on invalid input. Check any pointer you didn't create.
  5. Returning or storing the address of a local — dangling pointer. The address is invalid the moment the function returns.
  6. Forgetting const on read-only parameters — you lose the compiler's protection and mislead readers about the function's behavior.
  7. Accidentally aliasing the same variable twice — addTo(&x, &x) applies the effect twice. Document or guard against it in APIs where it matters.
  8. Modifying the pointer instead of the value — p++ inside the function changes the local copy of the pointer, not the caller's data. (Related to the *p++ precedence traps from the pointer arithmetic tutorial.)
  9. Using a stale pointer after the target moved or was freed — the pointer still holds the old address.

(Suggested image: "9 Mistakes" checklist — with #1, #2, and #5 as the three emphasized items)


Final Thoughts

"C is pass-by-value" is true, and it's the foundation — not a limitation to work around. Functions get copies, always. Passing a pointer is simply choosing what to copy: not your data, but its location. Once that's clear, everything else is mechanics: & at the call, * in the body, const when read-only, NULL checks when the pointer comes from outside.

Notice how much of this series you've now assembled into one idea: scanf("%d", &x) was your first pointer parameter, swap is the same mechanism applied deliberately, array parameters are always pointers, and const came from the string and array tutorials. Pointer parameters aren't a new topic — they're the thread connecting everything you've already learned.

Next in this cluster: pointers to pointers, where functions like malloc-wrapping code and char **argv finally make sense.

Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
// Swap function using pointers
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
int main() {
    int x = 10, y = 20;
    printf("Before swap: x = %d, y = %d\
", x, y);
    swap(&x, &y);
    printf("After swap:  x = %d, y = %d\
", x, y);
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Always check incoming pointer parameters for NULL (if (ptr == NULL) return ERROR;) before dereferencing them inside functions.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: Why does `void badSwap(int a, int b) { int t = a; a = b; b = t; }` fail?

A: C passes arguments by value. `badSwap` modifies copies of variables that exist only within its local stack frame, leaving caller variables unaffected.
Q

Q: How do you return two values from a C function?

A: Pass two pointers as parameters (`int divide(int a, int b, int *quotient, int *remainder)`).

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations