When pointers go wrong
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.
| Bug | What the pointer holds |
|---|---|
| wild pointer | a garbage address: it was never given a value |
| NULL pointer | NULL: it points to nothing |
| out of bounds | an address past the end of an array |
| dangling pointer | the 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.
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}
use of uninitialized valueWhat 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:
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}
load of null pointerWhat 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.
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}
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:
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}
stack-buffer-overflowWhat 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.
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}
stack-use-after-returnWhat 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:
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}
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).
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}
a and b point at main's x and y, which still exist after larger returns. So big points at y, and *big = 0 sets y to 0. Only the address of the function's own local variables dangles.
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:
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}
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
NULLbefore using it, whenever it might beNULL. - Walk arrays with
<, never<=, and pass the count with the array. - Never return the address of a local variable.
- Mark pointer parameters
constwhen 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.
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.
Found: 310 Not found
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}
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.
Average: 7.50
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}
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.
12 30 7 18
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}
What you learned
- A wild pointer was never given a value; give every pointer an address or
NULLwhen you declare it. - Following
NULLcrashes a real program; checkp == NULLfirst 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 *pcan read throughpbut 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.