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.

Before you start

You should be comfortable with:

  • What a pointer holds and what & and * do.
  • That p + 1 moves one element and that v[i] is *(v + i).
  • What NULL is 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.

swap_wrong versus swapOn the left, main with origin and dest. swap_wrong gets copies at another address; swap gets two pointers that point to the variables of main.mainorigin = 101 → 152…21a0dest = 152 → 101next to itswap_wrong(101, 152)a = 152, b = 101copies at …217cswap(&origin, &dest)a = …21a0b = address of destswap_wrong swaps its copies and they vanish; swap writes into main
Arrows only come out of swap: it is the only one that can touch the variables in 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 printf at the end of swap_wrong with the values of a and b to 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:

Lapp points to*(p - 1)Prints
1boarding + 6 (end)box 519
2boarding + 5box 427
3boarding + 4box 33
4boarding + 3box 242
5boarding + 2box 18
6boarding + 1box 015
endboardingp > boarding is false—
Walking with a pointer, there and backSix boxes with the passengers at each stop. On the way out p goes from the first to the sixth; the dotted box is boarding + 6, which can be pointed at but not read. On the way back p starts there and reads the previous box.1584232719stop 0stop 1stop 2stop 3stop 4stop 5Outbound: p starts at boarding and stops at boarding + 6endReturn: p starts at boarding + 6 and reads *(p – 1)p*(p – 1)
The address just past the end is fine for comparing, never for reading with *.

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:

reverse with two pointersFive rows: the initial array and how it looks after each of the four swaps between start and end, highlighted in coral.start15842327191135swap 135842327191115swap 235114232719815swap 335111932742815swap 435111927342815start moves forward and end moves back until they cross: 4 swaps for 8 boxes
In each row, the two coral boxes are the ones start and end point to in that swap.
LapstartendSwapsArray afterwards
1box 0box 715 and 3535 8 42 3 27 19 11 15
2box 1box 68 and 1135 11 42 3 27 19 8 15
3box 2box 542 and 1935 11 19 3 27 42 8 15
4box 3box 43 and 2735 11 19 27 3 42 8 15
endbox 4box 3start < 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_min also return which stop holds the maximum, with a fifth parameter int *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}:

Lapp at box*p*best beforeDoes best change?
1194Yes, to box 1
2229No
33129Yes, to box 3
44612No
55312No
661512Yes, to box 6
77715No

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 of swap(&origin, &dest).
  • Swapping the pointers instead of the data: temp = a; a = b; b = temp; inside swap moves local addresses and changes nothing outside.
  • Using *r without checking for NULL after a search.
  • Walking with <= up to v + 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:

  1. Before the call, main‘s frame has origin and dest with 101 and 152.
  2. On entering swap a new frame appears with a and b. They don’t hold numbers: they’re two arrows reaching over to main‘s variables.
  3. After int temp = *a;, temp is 101 and lives in swap‘s frame.
  4. On *a = *b; the box that changes is origin in main, at the other end of the arrow. This is the key moment: the function is writing outside its own frame.
  5. When swap returns its whole frame disappears, but main keeps 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 at v + 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: *r gives the value, r - v the position and NULL means “not there”.
  • Before using *r, always check it isn’t NULL.

Checklist: have you practised it?

  • Have you run the four programs and checked the outputs?
  • Can you explain why swap_wrong changes nothing in main?
  • 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 NULL before 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 →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *