Pass-by-Pointer
Fixing the broken swap, output parameters, and why scanf needs the ampersand
Based on content from Dr. Stu Steiner, Eastern Washington University.
In a nutshell
C is pass-by-value. Always. That is the whole game. A function receives copies of its arguments, and changes to those copies die with the stack frame. That single rule is what you came for: it is why scanf needs &x, why swap(int, int) is broken, why every C function that needs to “return more than one thing” takes pointers as output slots, and ultimately why pointers exist in the language at all. The fix is not to change the rule. The fix is to pass the address and let the callee write through it. The caller writes &x, the parameter is declared int *p, and the body says *p = newValue;. Still pass-by-value under the hood; the value being copied is just the address. Once you see this pattern, half of C’s library (scanf, memcpy, fgets, qsort, every resizable-array API) stops being mysterious.
Practice this topic: Pointers drill, or browse the practice gallery.
After this lesson, you will be able to:
- Explain why C is always pass-by-value, even when you are “passing a pointer”
- Write a correct
swapand explain why the temporary variable is necessary - Use the output-parameter pattern to return multiple values from one function
- Say why
scanfneeds&xbutprintfdoes not - Use
conston a pointer parameter to mark input-only data
Quick reference
| Pattern | Caller writes | Callee declares | Callee body uses |
|---|---|---|---|
| Read input | f(x) |
void f(int x) |
x directly |
Modify caller’s int |
f(&x) |
void f(int *p) |
*p = ...; |
| Return two values | f(in, &out1, &out2) |
void f(int in, int *o1, int *o2) |
*o1 = ...; *o2 = ...; |
| Read-only pointer | f(&x) |
void f(const int *p) |
*p in reads only |
Coming from CSCD 210
Java has “pass the object reference by value” for objects and “pass the primitive by value” for primitives. You could never write a Java method that reassigned a caller’s int; the workaround was “return a new value” or “wrap the int in an object.” C removes the restriction by exposing addresses directly. You pass the address of the int, the callee writes through it, the caller sees the new value. The mechanism is the same pass-by-value copying Java does for primitives; the address is the extra level of indirection that lets the two sides share storage. & and * are the operators that add and strip that level. The full type-level picture is in the pointer type algebra deep dive.
Why increment was broken
#include <stdio.h>
void increment(int x) { x++; }
int main(void)
{
int n = 5;
increment(n);
printf("%d\n", n);
return 0;
}
This prints 5, not 6. The call increment(n) copies the value 5 into the parameter x. The function increments its local copy to 6. The function returns. The copy dies. The caller’s n is untouched. K&R §5.2 (p. 95): “In C, all function arguments are passed by value. This means that the called function is given the values of its arguments in temporary variables rather than the originals.”
The fix: pass the address, write through it
void increment(int *p) { (*p)++; }
int main(void)
{
int n = 5;
increment(&n);
printf("%d\n", n); /* 6 */
return 0;
}
Three things changed.
- The caller synthesized an address with
&nand passed it. - The parameter type changed from
inttoint *. - The function body dereferences
pbefore modifying.
The parentheses in (*p)++ matter. Postfix ++ binds tighter than unary *, so *p++ parses as *(p++), which would increment the pointer itself (pointer arithmetic, next lesson). You want to increment the pointed-to object, so write (*p)++.
C is still pass-by-value
Even with pointers, C is pass-by-value. What you are copying is the address. The callee’s parameter p is a fresh local variable holding that copy. Both the caller’s &n and the callee’s p hold the same address, which is why writing through either one reaches the same n.
main's stack frame increment's stack frame
+----------------+ +----------------+
| n = 5 -> 6 | <------- | p = &n |
+----------------+ +----------------+
The arrow is shared storage. The boxes are separate stack slots.
Check your understanding (predict the output)
#include <stdio.h>
void mystery(int *p)
{
p = NULL;
}
int main(void)
{
int x = 10;
int *q = &x;
mystery(q);
printf("%d\n", *q);
return 0;
}
Reveal answer
Prints 10.
mystery(q) copies the address in q into the parameter p. Inside mystery, p = NULL; reassigns the local copy p, not the caller’s q. When mystery returns, p dies and q is unchanged. The dereference *q is still valid and prints 10.
The lesson: to modify a caller’s pointer (not just the object the pointer points to), you need one more level of indirection, int **. That is lesson 6d.
The canonical swap
void swap(int *a, int *b)
{
int tmp = *a; /* save the object a points at */
*a = *b; /* overwrite it with the object b at */
*b = tmp; /* restore the saved value into *b */
}
int x = 3, y = 5;
swap(&x, &y);
/* x is now 5, y is now 3 */
The temporary is essential. Without it:
*a = *b; /* *a is now whatever *b was */
*b = *a; /* but *a is the new value, so *b gets it too. Both equal. */
You destroyed *a before saving it, and now both locations hold the same value. tmp is the saved copy.
K&R §5.2 pages 95 and 96 open the entire pointer chapter with this example. Draw the three assignments on paper and watch the values move; the sequence becomes automatic after a few tries.
The output-parameter pattern
A function can only return one value through the return statement. When you need to produce more than one, use pointer parameters as “output slots”:
#include <stdio.h>
void divmod(int dividend, int divisor, int *quot, int *rem)
{
*quot = dividend / divisor;
*rem = dividend % divisor;
}
int main(void)
{
int q, r;
divmod(17, 5, &q, &r);
printf("%d / %d = %d remainder %d\n", 17, 5, q, r);
/* 17 / 5 = 3 remainder 2 */
return 0;
}
Naming convention: prefix the output-slot parameters (*quot, *rem, *out, *result) so a reader of the signature can tell inputs from outputs without jumping to the body. Lab 6’s addItem(int *length, int *array) uses length as an output slot for the updated length.
Why scanf needs &
scanf must write into your variables. C is pass-by-value. The only way a callee can affect a caller’s storage is to receive its address:
int age;
scanf("%d", &age); /* pass the address so scanf can write there */
scanf’s declaration in <stdio.h> is int scanf(const char *format, ...);. The ellipsis means the argument types are not checked by the compiler, so forgetting the & compiles cleanly and produces undefined behavior at runtime (usually a segfault on the first input attempt).
Two places you do not write &:
printfwith%dwants the value, not the address:printf("%d\n", x);.printfis reading, not writing.scanfwith%swants achar *. Array-name decay (next lesson) makes achar buf[100]produce&buf[0]automatically, so the idiom isscanf("%s", buf);. Either way, you end up with an address.
const on pointer parameters
When a function only reads through a pointer, mark the pointer const. This communicates intent to the caller (“I will not modify your data”), and the compiler will reject any accidental write.
int sum(const int *arr, int n)
{
int total = 0;
for (int i = 0; i < n; i++) {
total += arr[i];
}
/* arr[0] = 0; would be a compile error */
return total;
}
Two positions of const, two different meanings. Read the declaration right-to-left.
| Declaration | English | What cannot change |
|---|---|---|
const int *p |
p is a pointer to const int |
the int that p points to (no write through *p) |
int *const p |
p is a const pointer to int |
p itself (cannot reassign the pointer) |
const int *const p |
a const pointer to const int |
both |
The first form is the one you want on input parameters. The second form is rarely useful at function-parameter scope. The third shows up on global constants and lookup tables.
Forgotten-* and forgotten-& bugs
These are the two most common mistakes when learning pass-by-pointer. Both compile without warning under -Wall.
/* Forgotten * in the callee */
void set_to_zero(int *p)
{
p = 0; /* wrong: sets the local pointer to NULL */
/* *p = 0; right: writes 0 through p to the caller */
}
/* Forgotten & in the caller */
int n = 5;
increment(n); /* wrong: passes value 5, type mismatch for int * */
increment(&n); /* right: passes address of n */
The forgotten & usually fails to compile (int * vs int). The forgotten * compiles cleanly and silently does nothing. When your function “runs but does not change anything,” check every write through a pointer parameter.
Check your understanding (multi-select)
Which of the following correctly describe C’s calling convention?
Reveal answer
True:
- C passes every argument by value; pointers are not an exception.
scanfrequires&xbecause it needs to write intox, and the only way to reach a caller’s storage is through its address.- Inside a function, writing
p = somethingreassigns the local pointer; writing*p = somethingwrites through it into the caller’s storage.
False:
- “C supports pass-by-reference when you use
int *.” C has no pass-by-reference. What it has is pass-by-value of an address, which achieves the same effect. - “A function can return multiple values through
return.” It can onlyreturnone value; the output-parameter pattern covers the rest.
What comes next
A student writes this function and reports that x is unchanged after the call:
void zero_out(int *p) { p = 0; }
int main(void)
{
int x = 7;
zero_out(&x);
printf("%d\n", x);
return 0;
}
&x. The function receives a copy of that address in p, then p = 0; overwrites the local copy with NULL and returns. The caller's x is never touched. The fix is to dereference: *p = 0;. This is the forgotten-* bug; it compiles without warning and silently does nothing.
Next, Pointer Arithmetic & Arrays connects pointers to arrays: p + 1 advances by sizeof(*p) bytes, and arr[i] is literally *(arr + i). Drill this page: Pointers or the practice gallery.