Why this matters
C passes every argument by value: a function gets copies. That is safe and simple, but it means a function cannot change its caller's variables, and it can only return one value. Pointers solve both problems. Passing an address lets the function reach back into the caller's memory. This is how scanf stores what it reads, how library functions report several results at once, and how large data is shared without copying. It is the most common everyday use of pointers.
The classic: swap
#include <stdio.h>
void swap_broken(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void)
{
int x = 1, y = 2;
swap_broken(x, y);
printf("after swap_broken: x = %d, y = %d\n", x, y);
swap(&x, &y);
printf("after swap: x = %d, y = %d\n", x, y);
return 0;
}
after swap_broken: x = 1, y = 2
after swap: x = 2, y = 1
Look at the call stack during each call (the stack was introduced in the functions module):
swap_broken(x, y): swap(&x, &y):
main's frame swap_broken's frame main's frame swap's frame
+-----------+ +-----------+ +-----------+ +-----------+
| x | 1 | | a | 1->2 | | x | 1->2 |<-----+-* a |
| y | 2 | | b | 2->1 | | y | 2->1 |<-----+-* b |
+-----------+ | tmp | 1 | +-----------+ | tmp | 1 |
+-----------+ +-----------+
copies swapped, discarded on return writes land in main's variables
In swap, the parameters a and b are still copies, but copies of addresses. Both copies point at main's x and y, so *a = *b writes into main's frame. The function did not need to return anything.
The caller's side matters as much: swap(&x, &y) passes addresses. Writing swap(x, y) is an error in GCC 14 (passing argument 1 of 'swap' makes pointer from integer without a cast).
You have been doing this all along
scanf("%d", &n) is exactly this pattern. scanf returns how many items it converted (its status), and stores the values themselves through the addresses you give it. Now you know why forgetting the & is so bad: scanf would treat the value of n as an address and write the number to some random place.
Arrays, too: void fill(int a[], int n) is really void fill(int *a, int n), which is why such functions modify the caller's array. The next lesson connects arrays and pointers properly.
Output parameters: returning more than one value
A common C style: the return value reports success or failure, and the actual results come back through pointer parameters.
#include <stdio.h>
/* Returns 1 on success, 0 if b is zero (and then leaves *q and *r alone). */
int divide(int a, int b, int *q, int *r)
{
if (b == 0) {
return 0;
}
*q = a / b;
*r = a % b;
return 1;
}
int main(void)
{
int quot, rem;
if (divide(17, 5, ", &rem)) {
printf("17 / 5 = %d remainder %d\n", quot, rem);
}
if (!divide(1, 0, ", &rem)) {
printf("cannot divide by zero\n");
}
return 0;
}
17 / 5 = 3 remainder 2
cannot divide by zero
Parameters used this way are called output parameters (or "out parameters"). Good habits:
- Document which parameters are outputs and what happens to them on failure.
- Check the status before using the outputs. If
dividefailed,quotandremwere never set and still hold garbage. - Put outputs last in the parameter list; this is the most common convention.
Pointers to pointers to... just pass them on
Inside a function whose parameter is int *a, a is already an address. To hand the same int to another function that wants an int *, pass a itself, not &a:
void sort2(int *a, int *b)
{
if (*a > *b) {
swap(a, b); /* correct: a and b are already pointers */
}
}
swap(&a, &b) would pass the addresses of the pointer variables (type int **), which gcc rejects as an incompatible pointer type. Pointers to pointers are a later lesson.
const: promising not to write
A pointer parameter can also be used just to avoid copying, with no intention of modifying. Mark it const:
double average(const int *values, int n);
const int *values means "a pointer to ints that I will not modify through this pointer". The compiler rejects any *values = ... or values[i] = ... in the function, and readers know the function is read-only. This is the same const you used on array parameters, now written in pointer form. The pointers-to-pointers lesson covers the other places const can go.
Never return the address of a local variable
A function's local variables live in its stack frame, which is released when the function returns. A pointer to one of them outlives the thing it points to; it is called a dangling pointer.
#include <stdio.h>
int *make_number(void)
{
int n = 42;
return &n;
}
int main(void)
{
int *p = make_number();
printf("%d\n", *p);
return 0;
}
main.c: In function 'make_number':
main.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr]
6 | return &n;
| ^~
$ ./prog
Segmentation fault (core dumped)
gcc knows this is always wrong, so it actually compiles return &n; as return NULL; to make the bug crash loudly rather than "work" by accident. In less obvious cases (storing &n in a global, for example) there is no warning, and the stale memory gets reused by the next function call.
while make_number runs: after it returns:
+-----------------+ +-----------------+
| make_number | | (free stack, | <- p still points here
| n = 42 | | reused by the |
+-----------------+ | next call) |
| main p = ? | +-----------------+
+-----------------+ | main p = *-----+--> ???
+-----------------+
The fixes: return the value itself (int make_number(void)), let the caller supply the storage (an output parameter), or allocate memory that outlives the function with malloc, which is the next module.
Common mistakes
- Forgetting
&at the call:swap(x, y). GCC 14 rejects it; older compilers warned and produced a crashing program. - Forgetting
*in the function:a = b;inside swap swaps the local pointer copies and changes nothing visible. gcc says nothing; it is legal code. - Comparing pointers instead of values:
if (a > b)compares the addresses. You meantif (*a > *b). - Using outputs after a failed call: check the status first.
- Returning a pointer to a local variable: heed
-Wreturn-local-addr.
Recap
- Arguments are copied; to let a function change a caller's variable, pass its address and write through it with
*. swap(int *a, int *b)withswap(&x, &y)is the canonical example;scanfworks the same way.- Output parameters let a function return several results; the return value often reports success.
- A pointer parameter is already an address: pass it on without
&. - Mark read-only pointer parameters
const. - Never return the address of a local variable: it dangles as soon as the function returns.
Going further: look at man 3 strtol. Its second parameter, char **endptr, is an output parameter through which strtol tells you where it stopped reading. You will understand the double star after the pointers-to-pointers lesson.