Kodewick
Lesson 18 of 30Contents

When pointers go wrong

Lesson 18 of 30 · Part 3 · Pointers · about 15 minutes

A pointer is only as good as the address in it. When that address is wrong, C doesn't stop you: the program reads or writes the wrong place, and it may crash at once, much later, or never. These are some of the hardest bugs to find in C. This lesson goes through the four most common ones. Each wrong example stops on the line where it goes wrong: press "see how it went wrong" to replay what led there.

BugWhat the pointer holds
wild pointera garbage address: it was never given a value
NULL pointerNULL: it points to nothing
out of boundsan address past the end of an array
dangling pointerthe address of a variable that no longer exists

Wild pointers

Lesson 15 showed a pointer that was never given a value. In real programs, it usually happens like this: the pointer is set only in some cases, and the case never comes up.

main.c
1#include <stdio.h>2 3int main(void) {4    int prices[4] = {12, 30, 7, 18};5    int *cheap;6    for (int i = 0; i < 4; i++) {7        if (prices[i] < 5) {8            cheap = &prices[i];9        }10    }11    printf("Under 5: %d\n", *cheap);12    return 0;13}
Output
use of uninitialized value

What happened: Stopped at line 11. A variable was used before it was given a value. In real C it would hold garbage, different on every run.

No price is under 5, so line 8 never runs, and cheap still holds garbage on line 11. Our engine stops there. Press Step and watch cheap through the loop: it never gets an arrow.

The cure is to give every pointer a value when you declare it. Here that's int *cheap = NULL;, and then a check before using it, as in the next section.

NULL pointers

NULL is the honest way to say "no address". Functions that look for something often return NULL when they don't find it. This one returns the address of the matching element:

main.c
1#include <stdio.h>2 3int *find(int *codes, int count, int target) {4    for (int i = 0; i < count; i++) {5        if (codes[i] == target) {6            return &codes[i];7        }8    }9    return NULL;10}11 12int main(void) {13    int codes[4] = {101, 205, 310, 422};14    int *p = find(codes, 4, 999);15    printf("Found: %d\n", *p);16    return 0;17}
Output
load of null pointer

What happened: Stopped at line 15. This pointer is NULL: nothing can be read or written through it.

There is no 999, so p is NULL, and *p on line 15 follows it to nowhere. On a real computer this usually crashes the program at once ("segmentation fault"). A NULL pointer is still much better than a wild one: you can check for it.

main.c
1#include <stdio.h>2 3int *find(int *codes, int count, int target) {4    for (int i = 0; i < count; i++) {5        if (codes[i] == target) {6            return &codes[i];7        }8    }9    return NULL;10}11 12int main(void) {13    int codes[4] = {101, 205, 310, 422};14    int *p = find(codes, 4, 999);15    if (p == NULL) {16        printf("Not found\n");17    } else {18        printf("Found: %d\n", *p);19    }20    return 0;21}
Output
Not found

Off the end of an array

A pointer walking an array can walk right off the end, the same way an index can (lesson 11). The usual cause is <= where < belongs:

main.c
1#include <stdio.h>2 3int main(void) {4    int temps[5] = {21, 23, 19, 25, 22};5    int total = 0;6    for (int *p = temps; p <= temps + 5; p++) {7        total += *p;8    }9    printf("Total: %d\n", total);10    return 0;11}
Output
stack-buffer-overflow

What happened: Stopped at line 7. This goes past the end of an array (or a string). Real C would read or overwrite whatever is next in memory.

temps + 5 is the spot just past the last element. Having a pointer there is allowed, so loops can compare against it, but reading *p there is not. Press Step and watch the arrow move along the cells, then point past the last one; that's where the engine stops. Write p < temps + 5.

Pointing to a variable that's gone

A function's local variables live in its stack frame, and the frame goes away when the function returns (lesson 8). A pointer to one of them is left pointing at nothing: it dangles.

main.c
1#include <stdio.h>2 3int *make_total(int a, int b) {4    int total = a + b;5    int *q = &total;6    return q;7}8 9int main(void) {10    int *p = make_total(3, 4);11    printf("%d\n", *p);12    return 0;13}
Output
stack-use-after-return

What happened: Stopped at line 11. This memory is no longer yours: it was freed, or it belonged to a function that has returned.

The address goes out through q, so gcc doesn't even warn. (Write return &total; directly and it does.) Press Step: p gets an arrow into make_total's frame, then the frame disappears, and the arrow has nothing left at its end. The engine stops on line 11, where *p follows it. Press "see how it went wrong" to replay it.

Here the function only needs to hand back one number, so return the value itself:

main.c
1#include <stdio.h>2 3int make_total(int a, int b) {4    int total = a + b;5    return total;6}7 8int main(void) {9    int t = make_total(3, 4);10    printf("%d\n", t);11    return 0;12}
Output
7

The value is copied out before the frame goes away. When a function really has to hand back an address, it must be the address of something that outlives the call: one of the caller's variables, a static variable (lesson 9), or memory on the heap (lesson 20).

Guess first

larger returns a pointer. What happens?

1#include <stdio.h>2 3int *larger(int *a, int *b) {4    if (*a > *b) {5        return a;6    }7    return b;8}9 10int main(void) {11    int x = 3;12    int y = 9;13    int *big = larger(&x, &y);14    *big = 0;15    printf("%d %d\n", x, y);16    return 0;17}

const: read but don't change

Put const before the type and the pointer can only be used to read: const int *a means "through a, the int can't be changed". It suits functions that only read an array. If one tries to write by mistake, the compiler catches it:

main.c
1#include <stdio.h>2 3double average(const int *a, int count) {4    int total = 0;5    for (int i = 0; i < count; i++) {6        total += a[i];7    }8    a[0] = 0;9    return (double)total / count;10}11 12int main(void) {13    int scores[4] = {80, 90, 75, 85};14    printf("%.2f\n", average(scores, 4));15    return 0;16}
Output
main.c: In function 'average':
main.c:8:10: error: assignment of read-only location '*a'
    8 |     a[0] = 0;
      |          ^

assignment of read-only location means writing through a const pointer. Delete line 8 and it runs. The const also tells anyone calling average that their array won't be touched.

Good habits

  • Give every pointer a value when you declare it: an address or NULL.
  • Check a pointer for NULL before using it, whenever it might be NULL.
  • Walk arrays with <, never <=, and pass the count with the array.
  • Never return the address of a local variable.
  • Mark pointer parameters const when the function only reads through them.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

Not found

The second search finds nothing, and the program stops on a NULL pointer. Add a check so it prints Not found in that case. The program should print Found: 310, then Not found.

It should print
Found: 310
Not found
exercise1.c
1#include <stdio.h>2 3int *find(int *codes, int count, int target) {4    for (int i = 0; i < count; i++) {5        if (codes[i] == target) {6            return &codes[i];7        }8    }9    return NULL;10}11 12void report(int *p) {13    printf("Found: %d\n", *p);14}15 16int main(void) {17    int codes[4] = {101, 205, 310, 422};18    report(find(codes, 4, 310));19    report(find(codes, 4, 999));20    return 0;21}
Output
Press Run or Ctrl+Enter
Exercise 2

Return the value

average returns the address of its local variable, which dangles. Make it return the double itself, and fix main to match. The program should print Average: 7.50.

It should print
Average: 7.50
exercise2.c
1#include <stdio.h>2 3double *average(int a, int b) {4    double result = (a + b) / 2.0;5    double *p = &result;6    return p;7}8 9int main(void) {10    double *avg = average(6, 9);11    printf("Average: %.2f\n", *avg);12    return 0;13}
Output
Press Run or Ctrl+Enter
Exercise 3

One step too far

This loop reads past the end of prices and the engine stops. Fix it so it prints the four prices, one per line.

It should print
12
30
7
18
exercise3.c
1#include <stdio.h>2 3int main(void) {4    int prices[4] = {12, 30, 7, 18};5    for (int *p = prices; p <= prices + 4; p++) {6        printf("%d\n", *p);7    }8    return 0;9}
Output
Press Run or Ctrl+Enter

What you learned

  • A wild pointer was never given a value; give every pointer an address or NULL when you declare it.
  • Following NULL crashes a real program; check p == NULL first whenever it might be.
  • A pointer can sit just past the end of an array, but reading there is out of bounds: loop with <.
  • Returning the address of a local variable leaves a dangling pointer: the frame is gone. Return the value instead.
  • const int *p can read through p but not write; use it for parameters a function only reads.
  • In our engine, each of these stops on the exact line, and "see how it went wrong" replays how it got there.