Part of C in 100s

C in 100 Seconds: Pass by Reference | Episode 19

Celest KimCelest Kim

Video: C in 100 Seconds: Pass by Reference — Pointers as Parameters | Episode 19 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

C Pass by Reference: Pointers as Parameters

C only passes by value. To "pass by reference," you pass a pointer — and the function dereferences. The classic swap example: pass &x and &y; the function does *a = *b etc.

C functions get copies of their arguments. To modify the caller's variables, you pass pointers — and the function writes through them.

The classic swap

#include <stdio.h>

void no_swap(int a, int b) {
  int temp = a;
  a = b;
  b = temp;
}

void swap(int *a, int *b) {
  int temp = *a;
  *a = *b;
  *b = temp;
}

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

  no_swap(x, y);
  printf("After no_swap: x=%d y=%d\n", x, y);   // 10, 20 — unchanged!

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

  return 0;
}

Why no_swap doesn't swap

void no_swap(int a, int b) {
  // a and b are LOCAL COPIES of the caller's x and y
  int temp = a;
  a = b;
  b = temp;
  // when this function returns, a and b are destroyed
  // x and y in main are untouched
}

C always passes by value. When you call no_swap(x, y):

  1. Copies of x and y are made on the stack as a and b.
  2. The function swaps the copies.
  3. The function returns; a and b are gone.
  4. x and y in main are unchanged.

To modify the originals, you need access to their addresses.

How swap works

void swap(int *a, int *b) {
  // a holds the address of caller's x
  // b holds the address of caller's y
  int temp = *a;     // read what x is
  *a = *b;           // write y's value into x's slot
  *b = temp;         // write old x into y's slot
}

swap(&x, &y);   // pass addresses

a and b are still copies — but they're copies of addresses, not values. Through those addresses, the function can read and write the actual x and y in main.

When to pass by pointer

  1. Modify caller's variables. The swap example.
  2. Return multiple values. divmod(a, b, &q, &r).
  3. Avoid copying large structs. process(&big_struct) is one pointer (8 bytes); process(big_struct) could be hundreds of bytes copied.
  4. Optional outputs. Pass NULL when the caller doesn't need the value:
void parse(const char *input, int *result, int *error) {
  *result = ...;
  if (error != NULL) *error = ...;
}

parse(input, &result, NULL);   // ignore error

scanf — the standard "by reference" example

int x;
scanf("%d", &x);    // pass address — scanf writes through it

You've been doing this since episode 12. scanf needs to modify x, so you pass &x.

Returning multiple values via pointers

void minmax(const int *arr, int n, int *min, int *max) {
  *min = arr[0];
  *max = arr[0];
  for (int i = 1; i < n; i++) {
    if (arr[i] < *min) *min = arr[i];
    if (arr[i] > *max) *max = arr[i];
  }
}

int nums[] = {3, 1, 4, 1, 5, 9, 2, 6};
int lo, hi;
minmax(nums, 8, &lo, &hi);
printf("min=%d max=%d\n", lo, hi);   // 1 9

Two outputs (min and max) plus the read-only input (arr, n). Common pattern in C — many stdlib functions look like this.

const for read-only pointers

void print_array(const int *arr, int n) {
  for (int i = 0; i < n; i++) {
    printf("%d ", arr[i]);
  }
}

const int *arr says "I won't modify what arr points to." Two benefits:

  • Documents intent.
  • Compiler enforces it (*arr = 0; errors).

For reading-only access, always mark the pointer const. It catches bugs and lets the caller pass const data without warnings.

Pass struct by pointer for performance

typedef struct {
  char name[100];
  int age;
  float gpa;
  // ... lots of fields ...
} Student;

// Slow — copies the whole struct
void process(Student s) { ... }

// Fast — passes one pointer
void process(const Student *s) { ... }

For structs over a few bytes, prefer passing by pointer (with const if read-only). Modern compilers may optimize, but explicit pointer-passing is clearer.

To access fields, use -> (covered in episode 24):

void birthday(Student *s) {
  s->age++;
}

Passing arrays vs passing pointers

void process1(int arr[], int n) { ... }      // syntactically array
void process2(int *arr, int n) { ... }        // semantically same

Both signatures are identical to the compiler. Arrays decay to pointers when passed. The [] syntax is just documentation.

For multi-dimensional arrays, the second dimension is required:

void grid_sum(int grid[][5], int rows) { ... }   // size of second dim must be known

NULL parameters as "skip this"

void parse(const char *str, int *out_int, char *out_str) {
  if (out_int != NULL) {
    *out_int = atoi(str);
  }
  if (out_str != NULL) {
    strcpy(out_str, str);
  }
}

parse("42", &n, NULL);     // only get the int

Caller passes NULL for outputs they don't need. Function checks before writing. Lightweight optional parameters.

Common mistakes

Forgetting & at call site. swap(x, y) passes values, not addresses. Function gets garbage as "addresses" → segfault.

Forgetting * inside function. void swap(int *a, int *b) { int temp = a; a = b; b = temp; } swaps the pointers (locally). The caller's values stay put.

Passing pointer to local variable, returning the pointer. Local is destroyed when the function returns; pointer dangles.

Modifying a const-marked pointer's target. Compile error — good. (Casting away the const to bypass = undefined behavior.)

Passing huge structs by value when by pointer would do. Slow; sometimes catastrophic stack usage on small embedded systems.

Confusing int *p and int *p[]. int *p[5] is "array of 5 pointers to int." Episode 28 covers arrays of pointers.

What's next

Episode 20: dynamic memory. malloc, sizeof, free. Allocate at runtime; release when done.

Recap

C passes by value. To modify the caller, pass a pointer (&x) and dereference (*p). For multiple return values, take pointer parameters. For large structs, pass const T * to avoid copies. Use const on read-only pointers to document intent and let the compiler enforce it. NULL parameters as optional outputs (caller passes NULL when they don't need it). Forgetting & or * is the most common bug.

Next episode: malloc.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.