C Pointers and Functions pass by reference guide with swap function example
|

C Pointers and Functions: Pass by Reference Explained (2026)

C pointers and functions are inseparable. Without pointers, C functions can only return one value and can never modify the caller’s variables. Pointers solve both problems. They give functions direct access to the caller’s memory, enabling pass-by-reference behavior, multiple return values, and efficient data handling.

In this lesson from our C Roadmap, you will master how to pass pointers to functions, return pointers safely, and understand why C Functions in C work the way they do.

The Pass by Value Problem in C

C is a strictly pass-by-value language. When you pass a variable to a function, the function receives a copy. Changes to the copy do not affect the original:

#include <stdio.h>

void try_to_double(int x) {
    x = x * 2;
    printf("Inside function: %d\n", x);  // 20
}

int main(void) {
    int num = 10;
    try_to_double(num);
    printf("After function: %d\n", num);  // Still 10!
    return 0;
}

The function try_to_double modifies its local copy. The original num in main is untouched. This is the fundamental limitation that pointers solve.

Pass by Reference with C Pointers

To modify the caller’s variable, pass its address and receive it as a pointer parameter:

#include <stdio.h>

void double_it(int *x) {
    *x = *x * 2;    // Dereference to modify the original
    printf("Inside function: %d\n", *x);
}

int main(void) {
    int num = 10;
    double_it(&num);     // Pass the ADDRESS of num
    printf("After function: %d\n", num);  // 20 — it changed!
    return 0;
}

Here is what happens step by step:

1. main: num = 10, located at address 0x1000
2. Call double_it(&num) → passes 0x1000 (the address)
3. Inside double_it: x = 0x1000 (x is a pointer holding the address)
4. *x = *x * 2 → reads value at 0x1000 (10), multiplies by 2, writes 20 back
5. Return to main: num is now 20

The pointer itself is still passed by value (the function gets a copy of the address), but since both the original and the copy point to the same memory location, changes through either pointer affect the same data.

The Classic Swap Example

The swap function is the textbook example of why pointers are necessary:

#include <stdio.h>

// WRONG: Does not work (swaps copies)
void bad_swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    // Local copies swapped, originals unchanged
}

// RIGHT: Works (swaps through pointers)
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void) {
    int x = 10, y = 20;

    bad_swap(x, y);
    printf("After bad_swap: x=%d, y=%d\n", x, y);   // x=10, y=20

    swap(&x, &y);
    printf("After swap: x=%d, y=%d\n", x, y);         // x=20, y=10

    return 0;
}

This demonstrates the core principle: to modify a variable from another function, you must pass its address and use pointer dereferencing.

Returning Multiple Values with C Pointers

C functions can only return one value. Pointers let you “return” multiple values by writing to pointer parameters. As we learned in Function Parameters & Return, return values have limitations. Pointers bypass them:

#include <stdio.h>

// Returns quotient, writes remainder through pointer
int divide(int dividend, int divisor, int *remainder) {
    *remainder = dividend % divisor;
    return dividend / divisor;
}

// Returns nothing, writes both results through pointers
void min_max(int arr[], int size, int *min, int *max) {
    *min = arr[0];
    *max = arr[0];
    for (int i = 1; i < size; i++) {
        if (arr[i] < *min) *min = arr[i];
        if (arr[i] > *max) *max = arr[i];
    }
}

int main(void) {
    int rem;
    int q = divide(17, 5, &rem);
    printf("17 / 5 = %d remainder %d\n", q, rem);

    int data[] = {34, 12, 89, 3, 67};
    int lo, hi;
    min_max(data, 5, &lo, &hi);
    printf("Min: %d, Max: %d\n", lo, hi);

    return 0;
}

This pattern is everywhere in C. The standard library uses it extensively: scanf(), strtol(), getline() all use pointer parameters for output.

Arrays as Pointer Parameters in C

When you pass an array to a function, it decays to a pointer to its first element. The function receives a pointer, not a copy of the entire array:

#include <stdio.h>

// These three declarations are IDENTICAL:
void func1(int arr[]) { }
void func2(int arr[10]) { }  // The 10 is ignored!
void func3(int *arr) { }     // Most honest declaration

void fill_zeros(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] = 0;  // Modifies the original array!
    }
}

int main(void) {
    int data[] = {1, 2, 3, 4, 5};

    printf("Before: %d %d %d\n", data[0], data[1], data[2]);
    fill_zeros(data, 5);
    printf("After:  %d %d %d\n", data[0], data[1], data[2]);

    // sizeof inside function would give pointer size, not array size
    printf("sizeof(data) in main: %zu\n", sizeof(data));  // 20 (5 ints)

    return 0;
}

This is why you always need to pass the array size separately. Inside the function, sizeof(arr) gives the pointer size (8 bytes on 64-bit), not the array size. We covered this gotcha in C Arrays.

const Pointers in C: Read-Only Access

Use const to tell both the compiler and the reader that a pointer parameter will not modify the data:

#include <stdio.h>

// Promise: this function only READS the array
int sum(const int *arr, int size) {
    int total = 0;
    for (int i = 0; i < size; i++) {
        total += arr[i];
        // arr[i] = 0;  // COMPILER ERROR: cannot modify const
    }
    return total;
}

// Promise: this function will MODIFY the array
void scale(int *arr, int size, int factor) {
    for (int i = 0; i < size; i++) {
        arr[i] *= factor;
    }
}

int main(void) {
    int data[] = {1, 2, 3, 4, 5};
    printf("Sum: %d\n", sum(data, 5));
    scale(data, 5, 10);
    printf("Sum after scale: %d\n", sum(data, 5));
    return 0;
}

There are four combinations of const with pointers:

int *p;             // Mutable pointer to mutable data
const int *p;       // Mutable pointer to READ-ONLY data
int *const p;       // FIXED pointer to mutable data
const int *const p; // FIXED pointer to READ-ONLY data

Read it right-to-left: const int *p means “p is a pointer to a const int.” int *const p means “p is a const pointer to an int.”

Returning Pointers from C Functions

Functions can return pointers, but you must be careful about what the pointer points to. There are safe and dangerous patterns:

#include <stdio.h>
#include <stdlib.h>

// SAFE: Returns pointer to dynamically allocated memory
int *create_array(int size, int value) {
    int *arr = malloc(size * sizeof(int));
    if (arr == NULL) return NULL;
    for (int i = 0; i < size; i++) {
        arr[i] = value;
    }
    return arr;  // Caller must free() this
}

// SAFE: Returns pointer to static variable
int *get_counter(void) {
    static int count = 0;
    count++;
    return &count;
}

// DANGEROUS: Returns pointer to local variable
int *bad_function(void) {
    int local = 42;
    return &local;  // WARNING: local is destroyed after return!
}

int main(void) {
    int *arr = create_array(5, 99);
    if (arr) {
        printf("arr[0] = %d\n", arr[0]);  // 99
        free(arr);
    }

    int *c = get_counter();
    printf("Counter: %d\n", *c);  // 1
    get_counter();
    printf("Counter: %d\n", *c);  // 2

    return 0;
}

The rule is simple: never return the address of a local (stack) variable. As we learned in Variable Scope & Lifetime, local variables are destroyed when the function returns. Return pointers to heap memory (malloc), static variables, or parameters passed in.

Pointer-to-Pointer Parameters

Sometimes a function needs to modify a pointer itself (not just the data it points to). This requires a pointer to a pointer:

#include <stdio.h>
#include <stdlib.h>

// Allocates memory and sets the caller's pointer
void allocate(int **ptr, int size) {
    *ptr = malloc(size * sizeof(int));
}

// Sets the caller's pointer to NULL after freeing
void safe_free(int **ptr) {
    free(*ptr);
    *ptr = NULL;
}

int main(void) {
    int *data = NULL;

    allocate(&data, 10);   // Pass address of the pointer
    if (data) {
        data[0] = 42;
        printf("data[0] = %d\n", data[0]);
        safe_free(&data);
        printf("data is %s\n", data == NULL ? "NULL" : "not NULL");
    }

    return 0;
}

We will explore pointer-to-pointer concepts in depth in our dedicated Pointers to Pointers lesson.

Function Pointers in C: A Brief Introduction

C allows pointers to functions, not just data. A function pointer stores the address of a function and can call it:

#include <stdio.h>

int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }

int main(void) {
    // Declare a function pointer
    int (*operation)(int, int);

    operation = add;
    printf("add: %d\n", operation(3, 4));       // 7

    operation = multiply;
    printf("multiply: %d\n", operation(3, 4));  // 12

    // Array of function pointers
    int (*ops[])(int, int) = {add, multiply};
    for (int i = 0; i < 2; i++) {
        printf("Result: %d\n", ops[i](5, 6));
    }

    return 0;
}

Function pointers are used for callbacks, plugin systems, and the standard library’s qsort() function. They enable a form of polymorphism in C.

Real-World C Pointer Function Patterns

Pattern 1: Error code return + output pointer

typedef enum { OK, ERR_NULL, ERR_OVERFLOW } Status;

Status safe_add(int a, int b, int *result) {
    if (result == NULL) return ERR_NULL;
    long sum = (long)a + b;
    if (sum > INT_MAX || sum < INT_MIN) return ERR_OVERFLOW;
    *result = (int)sum;
    return OK;
}

// Usage:
int answer;
Status s = safe_add(1000000, 2000000, &answer);
if (s == OK) printf("Result: %d\n", answer);

Pattern 2: Callback function

#include <stdio.h>

void for_each(int *arr, int size, void (*callback)(int)) {
    for (int i = 0; i < size; i++) {
        callback(arr[i]);
    }
}

void print_doubled(int x) {
    printf("%d ", x * 2);
}

int main(void) {
    int nums[] = {1, 2, 3, 4, 5};
    for_each(nums, 5, print_doubled);  // 2 4 6 8 10
    printf("\n");
    return 0;
}

Practice Exercises

Exercise 1: Write a function void clamp(int *value, int min, int max) that constrains *value between min and max.

Exercise 2: Write a function void stats(const int *arr, int n, double *mean, int *min, int *max) that calculates all three statistics in one pass.

Exercise 3: Write a function that takes a callback and applies it to each element of an array, storing results in a new array.

Solution to Exercise 1
void clamp(int *value, int min, int max) {
    if (*value < min) *value = min;
    else if (*value > max) *value = max;
}

Summary

C pointers and functions work together to overcome C’s pass-by-value limitation. Pass addresses with &, receive them as pointer parameters, and dereference with * to modify original data. Use const to signal read-only access. Never return addresses of local variables. Function pointers enable callbacks and polymorphic behavior. In the next lesson, we will explore how pointers and arrays are deeply connected. Continue our C Roadmap to master C.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *