Kodewick
Lesson 21 of 30Contents

Memory leaks and dangling pointers

Lesson 21 of 30 · Part 4 · Memory · about 15 minutes

Heap memory is yours to ask for and yours to give back. This lesson is about what happens when the giving back goes wrong: you forget, you keep using it, or you give it back twice. First, forgetting:

main.c
1#include <stdio.h>2#include <stdlib.h>3 4int *make_week(int base) {5    int *steps = malloc(7 * sizeof(int));6    for (int i = 0; i < 7; i++) {7        steps[i] = base + i * 100;8    }9    return steps;10}11 12int main(void) {13    for (int week = 1; week <= 3; week++) {14        int *steps = make_week(week * 1000);15        printf("Week %d, day one: %d steps\n", week, steps[0]);16    }17    return 0;18}
Output
Week 1, day one: 1000 steps
Week 2, day one: 2000 steps
Week 3, day one: 3000 steps

Nothing looks wrong in the output, and the program ends normally. Press Step all the way to the end: the heap holds three "28 bytes · line 5" blocks, one more each time around the loop, and none of them was given back. When the program ends they're still there, each one marked "leaked" in red.

Forgetting to free

Asking for memory and never giving it back is a memory leak. Each time around, steps in main points at a new block, and nothing points at the old one anymore: nobody can use it, and nobody can give it back.

This program runs for a moment and ends, and when it ends the system takes all its memory back, so no harm shows. But a program that keeps running (a server, a browser, a game) and leaks a little on every request takes up more and more memory, until it slows to a crawl or the system shuts it down.

A real C program that leaks says nothing about it; the output is exactly the same as without the leak. Running it here doesn't warn you either. To see it, press Step to the end and look at which blocks are left on the heap.

Losing the pointer

It's not only a missing free. Point the pointer somewhere else and the old block is just as lost:

main.c
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5    int *prices = malloc(3 * sizeof(int));6    prices[0] = 12;7    prices = malloc(5 * sizeof(int));8    prices[0] = 30;9    printf("%d\n", prices[0]);10    free(prices);11    return 0;12}
Output
30

Press Step: past line 7, prices's arrow swings from the "12 bytes" block to a new "20 bytes" block, and no arrow at all points at the old one. The free on line 10 gives back the new block. At the end, the old block is marked "leaked".

Fixing the leak

The rule is simple: give back every block before the last pointer to it is gone. In the first example, free each week once you're done with it:

main.c
1#include <stdio.h>2#include <stdlib.h>3 4int *make_week(int base) {5    int *steps = malloc(7 * sizeof(int));6    for (int i = 0; i < 7; i++) {7        steps[i] = base + i * 100;8    }9    return steps;10}11 12int main(void) {13    for (int week = 1; week <= 3; week++) {14        int *steps = make_week(week * 1000);15        printf("Week %d, day one: %d steps\n", week, steps[0]);16        free(steps);17    }18    return 0;19}
Output
Week 1, day one: 1000 steps
Week 2, day one: 2000 steps
Week 3, day one: 3000 steps

Press Step to the end again: all three blocks are gray and "freed", and none is red.

Tip: As you write a malloc, decide right then who will free it, and on which line. When a function returns heap memory, add a comment saying the caller has to free it.

Using it after free

The opposite mistake is more dangerous: the block has been given back, but a pointer still points there and you keep using it. That pointer is a dangling pointer.

main.c
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5    int *balance = malloc(sizeof(int));6    *balance = 500;7    free(balance);8    printf("Balance: %d\n", *balance);9    return 0;10}
Output
heap-use-after-free

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

After free, the address in balance hasn't changed, but that memory isn't yours anymore, and the system may hand it to someone else at any moment. A real C program might still print 500, might print garbage, or might go wrong somewhere else much later. Our engine stops on line 8. Press "see how it went wrong": the block is already gray, and balance's arrow has turned into a red dashed line.

It often happens with two pointers to the same block: one is used to free it, and the other keeps being used:

main.c
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5    int *scores = malloc(3 * sizeof(int));6    scores[0] = 88;7    int *best = scores;8    free(scores);9    printf("Top score: %d\n", best[0]);10    return 0;11}
Output
heap-use-after-free

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

best = scores copies only the address, so two arrows point at one block. Free it once and both pointers are dangling.

Freeing twice

Giving the same block back twice isn't allowed either. Here are two pointers to one block again, each freed once:

main.c
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5    int *list = malloc(4 * sizeof(int));6    int *copy = list;7    list[0] = 7;8    copy[1] = 9;9    free(list);10    free(copy);11    return 0;12}
Output
attempting double-free

What happened: Stopped at line 10. This memory was freed twice.

copy and list are the same block. Line 9 already gave it back, and line 10 gives it back again. A real C program often crashes here, and it can also scramble the heap so that a later malloc goes wrong. Our engine stops on line 10.

One more: freeing an address that didn't come from malloc. An ordinary variable lives on the stack and goes away by itself when its function ends; it isn't yours to give back:

main.c
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5    int count = 3;6    int *p = &count;7    *p = 4;8    free(p);9    return 0;10}
Output
main.c: In function 'main':
main.c:8:5: warning: 'free' called on unallocated object 'count' [-Wfree-nonheap-object]
    8 |     free(p);
      |     ^~~~~~~
main.c:5:9: note: declared here
    5 |     int count = 3;
      |         ^~~~~
attempting free on address which was not malloc()-ed

What happened: Stopped at line 8. free() was given an address that malloc() didn't return.

This time gcc notices while compiling and warns, but a warning doesn't stop the program. Our engine stops on line 8: count didn't come from malloc.

Set it to NULL after free

A good habit: right after free, set the pointer to NULL. Then it no longer dangles, a quick != NULL check tells you whether it's usable, and freeing it again by accident is harmless, because free(NULL) does nothing.

main.c
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5    int *cart = malloc(2 * sizeof(int));6    cart[0] = 15;7    cart[1] = 40;8    printf("Total: %d\n", cart[0] + cart[1]);9    free(cart);10    cart = NULL;11    if (cart != NULL) {12        printf("The cart is still there\n");13    } else {14        printf("The cart is empty\n");15    }16    free(cart);17    return 0;18}
Output
Total: 55
The cart is empty

Press Step: past line 10, cart's arrow is gone, and it says NULL.

Guess first

What happens with this program?

1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5    int *a = malloc(sizeof(int));6    int *b = a;7    free(a);8    a = NULL;9    free(b);10    return 0;11}

Practice

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

Exercise 1

Freed too soon

This program calls free too early and keeps using the memory. Move that line so the program prints First: 95 and Second: 88, and still gives the memory back before it ends.

It should print
First: 95
Second: 88
exercise1.c
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5    int *top = malloc(2 * sizeof(int));6    top[0] = 95;7    top[1] = 88;8    printf("First: %d\n", top[0]);9    free(top);10    printf("Second: %d\n", top[1]);11    return 0;12}
Output
Press Run or Ctrl+Enter
Exercise 2

Free it once

orders and today point at the same memory, and the program frees it twice. Delete the extra line so the program prints 39 orders today and ends normally.

It should print
39 orders today
exercise2.c
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5    int *orders = malloc(3 * sizeof(int));6    orders[0] = 12;7    orders[1] = 7;8    orders[2] = 20;9    int *today = orders;10    printf("%d orders today\n", today[0] + today[1] + today[2]);11    free(orders);12    free(today);13    return 0;14}
Output
Press Run or Ctrl+Enter
Exercise 3

A safe free

Finish release: free the parameter p, then return NULL. main writes data = release(data);, so calling it twice does no harm. The program should print data is NULL.

It should print
data is NULL
exercise3.c
1#include <stdio.h>2#include <stdlib.h>3 4int *release(int *p) {5 6}7 8int main(void) {9    int *data = malloc(4 * sizeof(int));10    data = release(data);11    data = release(data);12    if (data == NULL) {13        printf("data is NULL\n");14    }15    return 0;16}
Output
Press Run or Ctrl+Enter

What you learned

  • Memory you ask for and never give back is a memory leak: the output doesn't show it, and the program takes more memory the longer it runs. Step to the end, and blocks never freed are marked "leaked".
  • Before you change or lose the last pointer to a block, free it.
  • Using memory after free (a dangling pointer), freeing a block twice, and freeing an address malloc didn't give you are all undefined behavior; our engine stops and explains.
  • When two pointers point at one block, one free leaves both dangling; give it back only once.
  • Set a pointer to NULL after free: you can check it before use, and freeing it again is harmless. Other pointers to the same block don't change with it, though.