C in 100 Seconds: Pass by Reference | Episode 19
Video: C in 100 Seconds: Pass by Reference — Pointers as Parameters | Episode 19 by Taught by Celeste AI - AI Coding Coach
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
swapexample: pass&xand&y; the function does*a = *betc.
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):
- Copies of
xandyare made on the stack asaandb. - The function swaps the copies.
- The function returns;
aandbare gone. xandyin 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
- Modify caller's variables. The swap example.
- Return multiple values.
divmod(a, b, &q, &r). - Avoid copying large structs.
process(&big_struct)is one pointer (8 bytes);process(big_struct)could be hundreds of bytes copied. - 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.