Pointers in C — practice with 4 bus programs: swap, walks, the day summary and a search that can return NULL (to lose your fear of them)
In my first practice session with pointers I wrote a function to flip the route round (so the origin became the destination) and tested it ten times: it printed correctly inside the function and, back in main, nothing had changed. I had forgotten that a C function receives copies. The lesson it left me is the lesson of this whole practice: if you want a function to touch your variables, give it their address.
This is the hands-on part of the pointers in C theory. There are four complete, medium-level programs, all using bus route 201: swapping origin and destination, walking the stops in both directions, building the day summary with functions that return several results, and looking up a stop with a function that may come back with NULL. The data look like the real thing (passengers boarding, waiting minutes, stop codes) and every output comes from a real run with gcc 14. Memory addresses will be different on your computer: what matters is the distance between them.
Table of Contents
Before you start
You should be comfortable with:
- What a pointer holds and what
&and*do. - That
p + 1moves one element and thatv[i]is*(v + i). - What
NULLis and why you check it before using*p. - That an array’s name turns into the address of its first element.
If any of these feels shaky, go over the theory before carrying on.
The programs use simple functions to organise the code. Functions are covered in depth in the next topic, Functions in C; for now it’s enough to know that void means it returns nothing and that a function like int *find(...) returns an address. Compile each program with gcc -Wall -std=c11 program.c -o program and run it with ./program. Try them in the site’s C playground too, which compiles and runs them right in the browser.
Program 1: swap, flipping origin and destination
In the morning route 201 goes from San Telmo (stop 101) to Teror (stop 152); in the afternoon it does the reverse trip. We want a function that swaps origin and destination. It looks trivial, and that’s exactly why it’s the classic program for understanding why pointers exist: the “obvious” version doesn’t work.
The idea
We write two versions. swap_wrong receives the two codes (by value) and swaps them; swap receives their addresses and swaps them through those. In both functions we print where the thing they touch lives, so we can compare those addresses with the address of origin in main. The swap itself is the usual one: keep one in a temporary variable, copy the second over the first and put the saved one in the second.
Before you code, think about what the function gets when you call it: the numbers 101 and 152, or the places where they’re stored? Only with the places can it change the originals.
The code
#include <stdio.h>
/* Version WITHOUT pointers: gets copies of both codes */
void swap_wrong(int a, int b) {
printf(" in swap_wrong, a lives at %p\n", (void *)&a);
int temp = a;
a = b;
b = temp; /* the copies get swapped, not the originals */
}
/* Version WITH pointers: gets the addresses of the originals */
void swap(int *a, int *b) {
printf(" in swap, a points to %p\n", (void *)a);
int temp = *a; /* keep what's at the first address */
*a = *b; /* copy the second over the first */
*b = temp; /* and put the saved one in the second */
}
int main(void) {
int origin = 101, dest = 152; /* stop codes: San Telmo and Teror */
printf("Out: origin=%d dest=%d\n", origin, dest);
printf(" in main, origin lives at %p\n", (void *)&origin);
swap_wrong(origin, dest);
printf("After swap_wrong: origin=%d dest=%d\n", origin, dest);
swap(&origin, &dest);
printf("Back: origin=%d dest=%d\n", origin, dest);
return 0;
}
Output
Out: origin=101 dest=152 in main, origin lives at 0x7fff663721a0 in swap_wrong, a lives at 0x7fff6637217c After swap_wrong: origin=101 dest=152 in swap, a points to 0x7fff663721a0 Back: origin=152 dest=101
Check it: after swap_wrong the origin is still 101, and after swap the codes have swapped (152 and 101). The key is the addresses. origin lives at …21a0. Inside swap_wrong, a lives at …217c: another box, holding a copy of 101. Inside swap, however, a points to …21a0, the box of main‘s origin. That’s why *a = *b writes into main.
Questions you might ask
Why does swap_wrong swap inside the function? Because it swaps its own variables a and b, which are copies. If you printed a and b at the end of swap_wrong you’d see 152 and 101. The problem is those copies disappear when the function ends.
Why is the call swap(&origin, &dest)? Because the parameters are int * and expect addresses. If you wrote swap(origin, dest), gcc 14 would give an error: you’d be passing an int where a pointer belongs.
What is temp for? If you did *a = *b; and then *b = *a;, the first value would already be lost and both variables would end up as 152. The temporary keeps the 101 before it’s overwritten.
Why do a and b have the same names in both functions if they’re so different? Because each function has its own local variables. In swap_wrong, a is an int; in swap, a is an int *. What decides what each one is, is the parameter’s declaration, not its name.
Declaring void swap(int *a, int *b) and inside writing int temp = a; a = b; b = temp; with no asterisks. It compiles (gcc will warn you when a pointer goes into an int), but what you swap are the local addresses, not the data: nothing changes in main.
Try it yourself
- Add a
printfat the end ofswap_wrongwith the values ofaandbto see that inside they did swap. - Write
swap_double(double *a, double *b)and swap the kilometres of two stops.
Program 2: walking the stops with a pointer
We have the passengers boarding at each of the six stops of a trip. We want to walk through them in four ways without a single index: outbound (adding up the total), on the way back, only the even stops and, finally, with each box’s address to watch pointer arithmetic in action.
The idea
A single pointer p does all the work. On the way out it starts at boarding (the address of box 0) and moves on while p < boarding + n, that is, until it reaches the address just past the end. The way back has a catch: the natural thing would be to start at the last box and go down while p >= boarding, but that leaves p pointing before the array at the last comparison, and C only guarantees you can point at the box after the end, not the one before the start. So we start at boarding + n and read *(p - 1). For the even stops, the step is p += 2. And the stop number comes from subtracting pointers: p - boarding.
Before you code, draw the six boxes and mark with an arrow where p is on each lap. The limit boarding + n is the imaginary box at the end: you may point at it, but never read it.
The code
#include <stdio.h>
int main(void) {
int boarding[6] = {15, 8, 42, 3, 27, 19}; /* passengers boarding at each stop */
int n = 6;
int *p;
int total = 0;
printf("Out: ");
for (p = boarding; p < boarding + n; p++) { /* from the first to the last */
printf("%d ", *p);
total += *p;
}
printf("(total %d)\n", total);
printf("Back: ");
for (p = boarding + n; p > boarding; p--) /* p starts just past the end */
printf("%d ", *(p - 1)); /* and reads the previous box */
printf("\n");
printf("Even stops (0, 2, 4): ");
for (p = boarding; p < boarding + n; p += 2) /* jumps two at a time */
printf("%d ", *p);
printf("\n\n");
for (p = boarding; p < boarding + n; p++) /* p - boarding = stop number */
printf("stop %td: %p -> %d\n", p - boarding, (void *)p, *p);
return 0;
}
Output
Out: 15 8 42 3 27 19 (total 114) Back: 19 27 3 42 8 15 Even stops (0, 2, 4): 15 42 27 stop 0: 0x7ffc62da6110 -> 15 stop 1: 0x7ffc62da6114 -> 8 stop 2: 0x7ffc62da6118 -> 42 stop 3: 0x7ffc62da611c -> 3 stop 4: 0x7ffc62da6120 -> 27 stop 5: 0x7ffc62da6124 -> 19
Check it by hand: 15 + 8 + 42 + 3 + 27 + 19 = 114, the outbound total. The way back is the same array reversed. The even stops are boxes 0, 2 and 4 (15, 42 and 27). In the last part, the addresses end in …110, …114, …118, …11c, …120, …124: they go up 4 at a time because p is an int * and each p++ moves one int. In hexadecimal, after …11c comes …120 (c = 12, plus 4 makes 16, which is written 10).
This is the trace of the way back:
| Lap | p points to | *(p - 1) | Prints |
|---|---|---|---|
| 1 | boarding + 6 (end) | box 5 | 19 |
| 2 | boarding + 5 | box 4 | 27 |
| 3 | boarding + 4 | box 3 | 3 |
| 4 | boarding + 3 | box 2 | 42 |
| 5 | boarding + 2 | box 1 | 8 |
| 6 | boarding + 1 | box 0 | 15 |
| end | boarding | p > boarding is false | — |
Questions you might ask
Why p < boarding + n and not p <= boarding + n? Because boarding + n is the address past the end. With <= the loop would take one lap too many and read memory that isn’t part of the array: it’s the extra lap you saw with ASan in the theory.
What is %td? The format for ptrdiff_t, the type you get when you subtract two pointers. p - boarding isn’t an int, even if it fits in one: with %td gcc has nothing to warn about.
Would the even-stops loop work with a 7-box array? Yes: with 7, p would visit 0, 2, 4 and 6 and then boarding + 8, which is already greater than boarding + 7, so it would stop. Adding past the end just to compare is tolerable in practice, but the cleanest thing is not to overshoot: with odd-sized arrays, check the limit before jumping.
Why not walk back with for (p = boarding + n - 1; p >= boarding; p--)? Almost every compiler accepts it and it almost always works, but on the last comparison p would be boarding - 1, an address the C standard doesn’t guarantee. Starting at the end and reading *(p - 1) avoids the problem and is the pattern the standard library uses.
Reusing p in a second loop without giving it a value again. After the way out, p is boarding + 6; if the next for doesn’t start with p = boarding, it won’t enter or it will read outside. That’s why every for in this program sets p in its first part.
Try it yourself
- Add a fifth pass that prints only the stops where more than 20 passengers board.
- Change the array to
double(each stop’s kilometre) and check that the addresses go up 8 at a time.
Program 3: the day summary with several results
At the end of the day we want a summary of route 201: the total number of passengers, the mean per stop, the stop with the most and the fewest, the range and, for the return trip, the array reversed. The problem: a C function can only return one value with return, and here nearly every function has to return two.
The idea
Instead of returning the results, each function receives the addresses of the variables in main where it must leave them: sum_and_mean(passengers, n, &sum, &mean) and find_max_min(passengers, n, &maximum, &minimum). Inside, it writes with *sum = …. That’s C’s way of having several output values. To reverse the array, two pointers, start and end, begin at the two ends, swap what they point to and move towards each other until they cross.
Before you code, split the parameters into two groups: those the function only reads (the array and its size) and those it fills in (they carry & in the call). Put the second group at the end of the list so it’s obvious at a glance.
The code
#include <stdio.h>
/* Works out sum and mean: two results, so they go through pointers */
void sum_and_mean(int *v, int n, int *sum, double *mean) {
*sum = 0; /* I write into main's variable */
for (int *p = v; p < v + n; p++)
*sum += *p;
*mean = (double)*sum / n; /* the cast avoids integer division */
}
/* Finds the maximum and the minimum in a single pass */
void find_max_min(int *v, int n, int *maximum, int *minimum) {
*maximum = *minimum = v[0]; /* start with the first stop */
for (int *p = v + 1; p < v + n; p++) {
if (*p > *maximum) *maximum = *p;
if (*p < *minimum) *minimum = *p;
}
}
/* Reverses the array in place, with two pointers that cross */
void reverse(int *v, int n) {
int *start = v; /* first box */
int *end = v + n - 1; /* last box */
while (start < end) {
int temp = *start;
*start = *end;
*end = temp;
start++; /* they move towards each other */
end--;
}
}
void show(const char *title, int *v, int n) {
printf("%s", title);
for (int *p = v; p < v + n; p++)
printf("%d ", *p);
printf("\n");
}
int main(void) {
int passengers[8] = {15, 8, 42, 3, 27, 19, 11, 35}; /* boarding at each stop, route 201 */
int n = 8;
int sum, maximum, minimum;
double mean;
printf("=== ROUTE 201: DAY SUMMARY ===\n\n");
show("Out: ", passengers, n);
sum_and_mean(passengers, n, &sum, &mean); /* & on whatever gets filled in */
find_max_min(passengers, n, &maximum, &minimum);
printf("Sum: %d\n", sum);
printf("Mean: %.2f\n", mean);
printf("Max: %d\n", maximum);
printf("Min: %d\n", minimum);
printf("Range: %d\n\n", maximum - minimum);
reverse(passengers, n); /* the array already is an address */
show("Back: ", passengers, n);
return 0;
}
Output
=== ROUTE 201: DAY SUMMARY === Out: 15 8 42 3 27 19 11 35 Sum: 160 Mean: 20.00 Max: 42 Min: 3 Range: 39 Back: 35 11 19 27 3 42 8 15
Check: 15 + 8 + 42 + 3 + 27 + 19 + 11 + 35 = 160 passengers, and 160 / 8 = 20.00 on average. The maximum is 42 (third stop), the minimum 3 (fourth) and the range 42 − 3 = 39. The way back is the array reversed: 35 first and 15 last.
This is how the array changes inside reverse, swap by swap:
| Lap | start | end | Swaps | Array afterwards |
|---|---|---|---|---|
| 1 | box 0 | box 7 | 15 and 35 | 35 8 42 3 27 19 11 15 |
| 2 | box 1 | box 6 | 8 and 11 | 35 11 42 3 27 19 8 15 |
| 3 | box 2 | box 5 | 42 and 19 | 35 11 19 3 27 42 8 15 |
| 4 | box 3 | box 4 | 3 and 27 | 35 11 19 27 3 42 8 15 |
| end | box 4 | box 3 | start < end is false | — |
Questions you might ask
Why does passengers have no & in the call while sum does? Because the array’s name already turns into its address. sum is a plain int: for the function to write into it, you have to pass &sum.
Why *sum = 0; at the start of the function? Because main‘s sum has no initial value: without that line, *sum += *p would add onto garbage. The function takes care of leaving the full result.
Why (double)*sum / n? If you divide two ints, C does integer division: 160 / 8 gives 20, but 161 / 8 would give 20 instead of 20.125. The cast turns the sum into a double before dividing.
What does *maximum = *minimum = v[0]; do? A chained assignment: first *minimum = v[0] and then *maximum takes that same value. Starting from the first element, and not from 0, is the right thing: if every stop had negative passengers (people getting off), starting at 0 would give a false maximum.
Why while (start < end) and not !=? With an even number of boxes, start and end never meet: they cross. With != the loop would keep going after crossing and reverse the array again, putting it back as it was (and running off the end).
Forgetting the & in the call: sum_and_mean(passengers, n, sum, mean);. gcc 14 turns it into an error (you pass an int where an int * is expected); with older compilers it compiled and the function wrote to a made-up address.
Try it yourself
- Add a function
void count_above(int *v, int n, double mean, int *how_many)that counts the stops with more passengers than the mean. - Make
find_max_minalso return which stop holds the maximum, with a fifth parameterint *pos_max.
Program 4: finding a stop and coming back with NULL
Each stop on the route has a code and an average waiting time. We want to know where the wait is longest and shortest, and then ask the user for a stop code and tell them its position on the route. If the code doesn’t exist, we have to say so without breaking anything.
The idea
The search functions return neither the value nor the position: they return the address of the element (int *). With that address we have both: *r is the value and r - waits is the position, which we also use to look up that stop’s code in the other array. For the “not found” case, the function returns NULL, and main checks it before using *r. The code is read with scanf, checking that a number really was read.
Before you code, think about what you’d return without pointers: the position, and -1 if it isn’t there. With a pointer, NULL plays the role of -1, and the value and position come for free.
The code
#include <stdio.h>
/* Returns the ADDRESS of the longest wait */
int *find_max(int *v, int n) {
int *best = v;
for (int *p = v + 1; p < v + n; p++)
if (*p > *best)
best = p; /* I keep the address, not the value */
return best;
}
/* Returns the address of the shortest wait */
int *find_min(int *v, int n) {
int *best = v;
for (int *p = v + 1; p < v + n; p++)
if (*p < *best)
best = p;
return best;
}
/* Looks for a stop code: its address, or NULL if it's not there */
int *find(int *v, int n, int value) {
for (int *p = v; p < v + n; p++)
if (*p == value)
return p; /* found: leave now */
return NULL; /* went through it all and it wasn't there */
}
int main(void) {
int codes[8] = {101, 104, 112, 118, 125, 131, 140, 152};
int waits[8] = {4, 9, 2, 12, 6, 3, 15, 7}; /* minutes of waiting at each stop */
int n = 8;
int *r;
int target;
r = find_max(waits, n);
int i = (int)(r - waits); /* pointer subtraction = position */
printf("Longest wait: %d min at stop %d (position %d)\n", *r, codes[i], i);
r = find_min(waits, n);
i = (int)(r - waits);
printf("Shortest wait: %d min at stop %d (position %d)\n", *r, codes[i], i);
printf("\nWhich stop are you looking for? ");
if (scanf("%d", &target) != 1) { /* if it's not a number, stop */
printf("That's not a stop code\n");
return 1;
}
r = find(codes, n, target);
if (r != NULL) /* ALWAYS before using *r */
printf("Code %d is stop %d on the route (%p)\n", *r, (int)(r - codes), (void *)r);
else
printf("Code %d is not on route 201\n", target);
return 0;
}
Output
First run, looking for stop 125:
Longest wait: 15 min at stop 140 (position 6) Shortest wait: 2 min at stop 112 (position 2) Which stop are you looking for? 125 Code 125 is stop 4 on the route (0x7ffc904314d0)
And another run with a code that doesn’t exist, 130:
Longest wait: 15 min at stop 140 (position 6) Shortest wait: 2 min at stop 112 (position 2) Which stop are you looking for? 130 Code 130 is not on route 201
The longest wait is 15 minutes, in box 6 of the waiting array; box 6 of codes is stop 140. The shortest, 2 minutes in box 2 (stop 112). 125 is in box 4: r - codes gives 4 because r is 4 ints (16 bytes) past the start of the array. With 130, find goes through all 8 boxes without finding it and returns NULL, and main takes the else branch without touching *r.
Trace of find_max over the waits {4, 9, 2, 12, 6, 3, 15, 7}:
| Lap | p at box | *p | *best before | Does best change? |
|---|---|---|---|---|
| 1 | 1 | 9 | 4 | Yes, to box 1 |
| 2 | 2 | 2 | 9 | No |
| 3 | 3 | 12 | 9 | Yes, to box 3 |
| 4 | 4 | 6 | 12 | No |
| 5 | 5 | 3 | 12 | No |
| 6 | 6 | 15 | 12 | Yes, to box 6 |
| 7 | 7 | 7 | 15 | No |
Questions you might ask
What does int *find_max(...) mean? That the function returns an int *, the address of an integer. The asterisk next to the name is the usual C style, but it belongs to the return type.
Is it safe to return best? Yes, because it points inside the waits array, which lives in main and still exists when the function ends. What you can never return is the address of a local variable of the function: you’ll see it in the exercises, in “What’s wrong here?”.
Why if (scanf("%d", &target) != 1)? scanf returns how many items it read. If the user types letters, it returns 0 and target stays without a value; checking it avoids searching for garbage. &target is the same old &: scanf needs the address to leave the number there.
Why (int)(r - codes) in the printf? The subtraction gives a ptrdiff_t and the format is %d: the cast turns it into an int. You could also print it directly with %td.
Using *r without checking for NULL. With 130, *r would dereference address 0 and the program would end in Segmentation fault. The if (r != NULL) check (or if (r)) always goes before the first *r.
Try it yourself
- Make the program ask for stops in a loop until the user types 0.
- Write
int *find_from(int *from, int *end, int value)and use it to find all the stops with a wait longer than 8 minutes.
Common mistakes in this practice
- Forgetting the
&in the call to a function that fills in variables:swap(origin, dest)instead ofswap(&origin, &dest). - Swapping the pointers instead of the data:
temp = a; a = b; b = temp;insideswapmoves local addresses and changes nothing outside. - Using
*rwithout checking forNULLafter a search. - Walking with
<=up tov + n, reading a box that doesn’t exist. - Not resetting the pointer before a second walk.
- Dividing two
ints for a mean and losing the decimals without noticing.
Watch it step by step in Python Tutor
Python Tutor draws the call stack: each function in its frame and each pointer as an arrow. Swap is the program where it’s clearest why pointers are needed. Choose C and paste:
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main(void) {
int origin = 101, dest = 152;
swap(&origin, &dest);
printf("origin=%d dest=%d\n", origin, dest);
return 0;
}
What to watch step by step:
- Before the call,
main‘s frame hasoriginanddestwith 101 and 152. - On entering
swapa new frame appears withaandb. They don’t hold numbers: they’re two arrows reaching over tomain‘s variables. - After
int temp = *a;,tempis 101 and lives inswap‘s frame. - On
*a = *b;the box that changes isorigininmain, at the other end of the arrow. This is the key moment: the function is writing outside its own frame. - When
swapreturns its whole frame disappears, butmainkeeps the swap:origin=152 dest=101.
Summary
- A function that receives addresses can modify the caller’s variables: it’s the only way to write a swap in C.
- A pointer walks an array with
p++and stops atv + n, the address past the end, which you compare against but never read. - To “return” several results, the function receives pointers to
main‘s variables and writes with*result = …. - Two pointers moving in from the ends reverse an array in place, with no extra array.
- A search can return a pointer to the element:
*rgives the value,r - vthe position andNULLmeans “not there”. - Before using
*r, always check it isn’tNULL.
Checklist: have you practised it?
- Have you run the four programs and checked the outputs?
- Can you explain why
swap_wrongchanges nothing inmain? - Can you walk an array backwards without the pointer leaving through the front?
- Do you know which parameters take
&in the call and why the array doesn’t? - Would you check for
NULLbefore using the result of a search? - Have you tried the “Try it yourself” ideas without looking at the theory?
If you answered yes to almost all of them, you’re ready for the pointers in C exercises, where I don’t hand you the code. See you there!
Next step: exercisesPointers in C — solved exercises with the bus: copying timetables, counting passengers, adjusting delays and an array library (plus the cheat sheet for the exam)Did this article help you? Here is how you can support SergioLearns →




What is an operating system — theory with your laptop on a study afternoon, a write() call and a CPU that never rests (and the three programs that weren’t running at the same time)