The Heap: malloc and free
Allocate memory whose size is only known at run time: malloc with sizeof, checking for NULL before use, working with the block, and giving it back with free exactly once.
The Size the Compiler Cannot Know
Lesson 1's int scores[5]; reserved room for five ints, and that 5 was a literal gcc read while compiling. Every array in this chapter has had its size settled before the program started, and that is the wall this chapter has been walking towards, because a program does not meet its data until it runs. How many numbers are about to arrive on standard input is not a fact any compiler can be told. Chapter 1's memory diagram set a region aside for exactly this problem and labelled it as memory you ask for by hand, and chapter 3 said that region gives an object a lifetime tied to nothing but your own decision to end it. That region is the heap, and this lesson is those promises being kept.
Asking for Bytes
malloc is declared in <stdlib.h> and its bargain is short. You tell it how many bytes you want, and it either finds that many consecutive bytes on the heap and returns a pointer to the first of them, or it returns NULL because it could not. Nothing else about it is negotiable, which is why the lines around the call matter as much as the call.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int count = 0;
if (scanf("%d", &count) != 1 || count <= 0)
{
printf("expected a positive count\n");
return 0;
}
int *values = malloc(count * sizeof *values);
if (values == NULL)
{
printf("could not allocate room for %d numbers\n", count);
return 0;
}
int total = 0;
for (int i = 0; i < count; ++i)
{
if (scanf("%d", &values[i]) != 1)
{
values[i] = 0;
}
total += values[i];
}
printf("read %d numbers, total %d\n", count, total);
free(values);
return 0;
}
Given 4 on one line and 10 20 30 40 on the next, that prints read 4 numbers, total 100.
Start with the request, count * sizeof *values. Read sizeof *values as "the size of one of the things values points at", which is 4 here, and note that nothing is dereferenced at run time to work it out, because sizeof measures the type of its operand without evaluating it. Multiply by how many you want and you have your byte count. Size the request from the object, not from the type. count * sizeof(int) computes the same number today and becomes a bug tomorrow: change the declaration to long *values and the first form follows along untouched, while the second carries on asking for 4 bytes an element and hands you a block half the size you think you have. The line as written names the type exactly once, in the declaration, and one is the smallest number of places a fact can live in.
Now the thing that line does not have: a cast. malloc returns void *, a pointer to memory of unstated type, and C converts that to int * implicitly on assignment. Do not write (int *) malloc(...). It buys nothing, and it puts the type back in a second place that has to be changed alongside the declaration, undoing what sizeof *values just achieved. It has a famous second cost too, that in older C a cast could hide a forgotten #include <stdlib.h>, although on this platform gcc rejects the missing declaration as an error with or without one. Keep the rule regardless. And if you have met C++ and remember the cast being compulsory there, do not carry that over: C++ has no implicit conversion from void *, so the same cast is required in one language and unwanted in the other.
The last piece is the if. Check every allocation before you use the block. This is not a formality to nod at, it is a branch that has to do something deliberate, and here it prints a message and returns, which is chapter 3's shape for a function that cannot do its job. Skip it and a failed malloc leaves values holding NULL for the first values[i] to dereference, which is chapter 3's SEGV on unknown address 0x000000000000 arriving on the day your program is unlucky rather than on the day you tested it.
The Block Arrives Uninitialized
malloc finds you bytes. It does not put anything in them. The contents are indeterminate, exactly as chapter 1's uninitialized int count; was, and reading an element before you have written it is undefined behaviour. That is the entire reason the fill loop above bothers with values[i] = 0; when scanf converts nothing: leaving the element unwritten and then adding it to total would be that bug. AddressSanitizer does not catch this one, which is worth knowing given that it has caught everything else this chapter has shown you. A program that prints a fresh element draws a -Wmaybe-uninitialized warning if gcc can see the mistake, then runs to completion, and one real run of that here printed -1094795586, which is the byte 0xbe four times over, the sanitizer's fill pattern rather than any kind of answer. There is a second allocator for when zero is the value you actually want: calloc(count, sizeof *values) allocates and zeroes in one step, taking the count and the element size as two arguments rather than pre-multiplied. Use it when zero is a meaningful starting value, and not as a way to avoid thinking about initialization, because a block you are about to fill from input gains nothing from being zeroed first. This lesson stays with malloc so that the rule stays in front of you: write before you read.
What the block is not is a new kind of thing. Those bytes are contiguous, so the elements sit back to back in index order exactly as lesson 1's array did, and every rule from lesson 2 applies unchanged. values[i] still means *(values + i), values + count is still the one-past-the-end address you may form and must not dereference, and a function that works on the block still takes a pointer and a count, which is why int sum_values(const int *values, int count) would accept this block without knowing where it came from. Lesson 2 closed by promising arrays whose size is decided while the program runs, carried around as a pointer and a count. int *values with int count beside it is that pair. The only new part is where the memory came from.
Giving It Back
Handing the bytes back is free(values), whose argument is a pointer some allocation returned. Every allocation gets exactly one free. What makes the heap unlike everything before it is that nothing in the program decides when: no closing brace, no return, no popped frame. The block exists from the malloc until the free you chose to write, which is precisely the lifetime chapter 3 said would be tied to your own decision. One small mercy for later: free(NULL) is defined and does nothing at all, so a cleanup path never has to check first. That also reopens a door chapter 3 closed, because make_answer returning &answer was wrong only in that the object died with the frame, and a heap block has no frame to die with.
#include <stdio.h>
#include <stdlib.h>
int *make_squares(int count)
{
int *values = malloc(count * sizeof *values);
if (values == NULL)
{
return NULL;
}
for (int i = 0; i < count; ++i)
{
values[i] = i * i;
}
return values;
}
int main(void)
{
int *squares = make_squares(5);
if (squares == NULL)
{
printf("could not allocate\n");
return 0;
}
printf("squares[3] is %d and squares[4] is %d\n", squares[3], squares[4]);
free(squares);
return 0;
}
That prints squares[3] is 9 and squares[4] is 16. make_squares allocates, fills and returns; its frame is gone by the time main reads squares[3]; the block is untouched, because it never lived in that frame. So int *make_squares(int count) is a legitimate function in a way that int *make_answer(void) never was, and gcc has nothing to warn about. Notice what came back alongside the pointer, though. main now holds the only address of a live block, and if main does not free it, nobody will. That obligation has a name, ownership: at any moment exactly one piece of code is responsible for freeing a given block, and make_squares handed that responsibility to its caller together with the pointer. C has nothing in its type system to express this, so it lives in your head, in the function's name, and in a comment. What happens when it gets lost is the next lesson.
Using a Block You Already Gave Back
After free(values) the block is gone, but values is not. It is still a variable in your frame still holding the same address, which is chapter 3's dangling pointer moved to the heap. The next program is wrong on purpose.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *values = malloc(4 * sizeof *values);
if (values == NULL)
{
printf("allocation failed\n");
return 0;
}
values[0] = 42;
free(values);
printf("values[0] is %d\n", values[0]);
return 0;
}
gcc happens to see this one, warning pointer 'values' used after 'free' [-Wuse-after-free] at the printf with a note pointing at the free. Run it anyway and the platform stops it, trimmed here:
==12==ERROR: AddressSanitizer: heap-use-after-free on address 0xfc1f80be0010
READ of size 4 at 0xfc1f80be0010 thread T0
#0 0x000000400904 in main /tmp/uaf.c:16
0xfc1f80be0010 is located 0 bytes inside of 16-byte region [0xfc1f80be0010,0xfc1f80be0020)
freed by thread T0 here:
#1 0x0000004008bc in main /tmp/uaf.c:15
previously allocated by thread T0 here:
#1 0x0000004008ac in main /tmp/uaf.c:6
heap-use-after-free joins your collection, and this report is the most generous one the sanitizer produces, because it tells you three things about a single address and gives each one a line number in your own file: line 16 read it, line 15 freed it, line 6 allocated it, and the block was 16 bytes long. Using memory after free is undefined behaviour, and the danger is the usual one. Freeing does not erase anything, so values[0] may well still hold 42, and without the sanitizer this program prints the right answer while being entirely wrong. The other half of the pair is freeing the same block twice. Adding a second free(values); beneath the first in this lesson's opening program is wrong in the same way, and gcc gives the same -Wuse-after-free warning, this time pointing at the second free:
==13==ERROR: AddressSanitizer: attempting double-free on 0xfc1faf1e0010 in thread T0:
#1 0x000000400b9c in main /tmp/heap.c:37
0xfc1faf1e0010 is located 0 bytes inside of 16-byte region [0xfc1faf1e0010,0xfc1faf1e0020)
freed by thread T0 here:
#1 0x000000400b94 in main /tmp/heap.c:36
previously allocated by thread T0 here:
#1 0x000000400aac in main /tmp/heap.c:14
Freeing the same block twice is undefined behaviour as well, and the report reads the same way: line 37 tried to free what line 36 had already freed. One habit disarms it, which is to write values = NULL; immediately after the free, since free(NULL) does nothing and the second call then becomes harmless. Be exact about what that buys, though, because it is easy to over-trust. It protects the one pointer you nulled and no other. If the address was copied into another variable, or returned to a caller, or passed to a function that kept it, every one of those copies still holds the old address, and both of this section's mistakes are still available through them. Nulling is a good reflex; the actual fix is knowing which single piece of code owns the block.
Key Takeaways
- The heap is memory you request while the program runs, so it is where an array whose size arrives at run time has to live.
malloc(bytes)from<stdlib.h>returns a pointer to that many uninitialized bytes, orNULL. The block is contiguous, so lesson 2 applies unmodified: index it, do arithmetic on it, and carry it to functions as a pointer plus a count. - Size the request from the object:
malloc(count * sizeof *p), neversizeof(int). It stays correct when the pointer's type changes. Do not cast the result;void *converts implicitly in C, and the cast just duplicates the type (C++ is the opposite, and requires it). - Check every allocation against
NULLbefore using the block, and make the null branch do something deliberate. An unchecked failure turns the firstp[i]into a null dereference. mallocdoes not initialize. Reading an element before writing it is undefined behaviour exactly as with an uninitialized local, and AddressSanitizer does not catch it.calloc(count, size)zeroes, when zero is genuinely the value you want.- Exactly one
freeper allocation, at the moment you choose, andfree(NULL)is a harmless no-op. Reading after the free isheap-use-after-freeand freeing twice isattempting double-free, both undefined behaviour and both caught here by AddressSanitizer. Setting the pointer toNULLafter freeing protects that one pointer only, not any copy of it. - A heap block outlives the function that allocated it, so a function may return a pointer to memory it allocated, unlike the dangling
&answerof chapter 3. The caller then owns the obligation to free it, which is ownership, and the subject of the next lesson.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
The Heap: malloc and free - Quiz
Test your understanding of the lesson.
Practice Exercises
An Array Whose Size Arrives at Run Time
Write the program lesson 1 could not: the first number on standard input says how many numbers follow, and there is no capacity written anywhere in the source. Read that count, allocate exactly that many ints on the heap, read the numbers into the block, then print three lines: sum: followed by the total, average: followed by the mean to one decimal place with %.1f, and reversed: followed by the numbers from last to first, each preceded by a single space so the line has no trailing space. Then free the block. Three TODOs mark the work and the surrounding program is already written. TODO 1 is the allocation, and both of the lesson's rules are being checked: size the request from the object with count * sizeof *values rather than from the type, and do not cast the result, because malloc returns void * and C converts it for you. The NULL check below it is already in place and must stay, since checking every allocation before using the block is not optional in this course; the graders cannot make an allocation fail, so the branch is there to be correct rather than to be exercised. TODO 2 is the fill, one scanf per element into values[i] with its return value tested, adding each number to total as it arrives. Remember that malloc does not initialize, so every element you are going to read must first be written; that is why the failure branch matters. If scanf converts nothing, print expected followed by the count and the word numbers, free the block, and return 0, because a path that returns without freeing is a leak and this exercise fails on leaks. TODO 3 is the free on the success path, and the whole exercise turns on there being exactly one free per allocation on every route through the program: free it twice and AddressSanitizer reports attempting double-free, print anything after freeing it and you have a use-after-free. The count guard at the top is written for you and handles a count of zero, a negative count, and input that is not a number at all, each of which prints expected a positive count and returns before anything is allocated. Every path returns 0, since a nonzero exit status is a failure however correct the printed output looks.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!