Memory leaks and dangling pointers
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:
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}
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:
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}
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:
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}
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.
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}
heap-use-after-freeWhat 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:
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}
heap-use-after-freeWhat 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:
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}
attempting double-freeWhat 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:
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}
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.
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}
Total: 55 The cart is empty
Press Step: past line 10, cart's arrow is gone, and it says NULL.
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}
a = NULL; changes only a; b still holds the old address. free(b) on line 9 gives back a block that was already given back: a double free. Setting a pointer to NULL is a good habit, but it covers only that one pointer, not the others pointing at the same block.
Practice
Write the code, then press Check. A hint costs nothing; the answer is there when you need it.
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.
First: 95 Second: 88
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}
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.
39 orders today
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}
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.
data is NULL
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}
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,
freeit. - Using memory after
free(a dangling pointer), freeing a block twice, and freeing an addressmallocdidn't give you are all undefined behavior; our engine stops and explains. - When two pointers point at one block, one
freeleaves both dangling; give it back only once. - Set a pointer to
NULLafterfree: you can check it before use, and freeing it again is harmless. Other pointers to the same block don't change with it, though.