Memory That Waits for the User

Every array so far had its size fixed before the program ran: #define MAX_VALUES 100, and hope nobody ever needs 101. C89 has no arrays sized by a variable, so a program that reads a count n and wants exactly n ints has been out of reach since chapter 7. The missing mechanism is dynamic allocation: asking for memory while the program runs, from a region called the heap, through four functions in <stdlib.h>: malloc, calloc, realloc, and free. The functions are small; what this lesson really teaches is the discipline that comes with them, because heap memory is the first resource in the course that the compiler will not clean up for you.

malloc, the Mandatory Check, and free

The full shape, read a count, allocate exactly that much, use it, give it back:

Given the input 3 10 20 12 this prints sum of 3 values = 42, and the array was sized by the 3, something no #define could do. malloc(n * sizeof(int)) requests n ints' worth of bytes and returns the block's address; last chapter's decay rules mean values[i] indexes it exactly like an array. Three facts carry all the weight. First, malloc can fail: when memory cannot be provided it returns NULL, and using the pointer without checking is the NULL dereference from chapter 11, undefined behaviour, so every allocation is checked before use, no exceptions. Second, the block starts indeterminate: like an uninitialized local, reading it before writing it is undefined behaviour, hence the fill loop before the sum loop. Third, free(values) ends the block's life: the memory returns to the heap and the pointer's value becomes unusable, though the variable itself may be re-aimed. Notice the early-exit path frees too; the scanf-failure return owns a block and must release it, which is where most real leaks hide.

Seasoned C sizes the request from the object instead of the type, values = malloc(n * sizeof *values), which stays correct if values ever changes type; you will meet that form on the linked-list nodes next lesson, but sizeof(int) reads plainest while the ideas are new.

Why There Is No Cast

Older textbooks write values = (int *)malloc(...), and pre-ANSI compilers required something like it because malloc then returned char *. ANSI C changed the type to void *, the generic object pointer, which converts implicitly to and from any object pointer type, so the assignment needs no cast at all. The cast is not merely noise; it used to hide a real bug. Forget #include <stdlib.h> and C89 quietly assumes the undeclared malloc returns int. Assigning that int to a pointer is a constraint violation the compiler must diagnose, on this platform -pedantic-errors refuses to compile it, but writing the cast tells the compiler you meant it, the diagnostic disappears, and on machines where int and pointers differ in size the address arrives truncated. Leaving the cast off keeps the compiler's safety net switched on, which is why this course, and modern C style generally, never casts an allocation result.

calloc, and Growing a Block with realloc

calloc(n, size) takes the element count and element size as two separate arguments, allocates like malloc, and additionally sets every byte of the block to zero, so counters and accumulator arrays start at a known value with no fill loop. It fails the same way, so it gets the same NULL check.

Growing a full block is realloc's job, and it has a trap worth labeling before the correct form. This is wrong:

values = realloc(values, 2 * n * sizeof(int));  /* WRONG: leaks on failure */

When realloc fails it returns NULL and leaves the old block allocated. The wrong line overwrites the only pointer to that block with NULL: the data is unreachable, the block can never be freed, and the program has both lost its contents and leaked the memory. The idiom is to catch the result in a temporary, check it, and only then reassign:

On success realloc preserves the old contents up to the smaller of the two sizes, but it may move the block to do so; the old pointer is then invalid, which is why the last line assigns scores = bigger and why the new elements still need initializing, realloc zeroes nothing. On failure the temporary catches the NULL, the original scores still owns its block, and the failure path frees it. One free per allocation, on every path: realloc's success counts as the free of the old block and the malloc of the new one, so the final free(scores) settles the whole account.

Ownership, Leaks, and What the Sanitizer Shows

The rule that scales from this lesson to every real C program: every allocation has exactly one owner, and the owner frees it exactly once. C's type system does not track this; you do, in the code's structure and, in real projects, in comments saying "caller frees". A leak is what happens when ownership lapses, when no pointer to a block survives, as in this loop that re-aims its only pointer each round:

for (i = 0; i < 3; i++) {
    values = malloc(5 * sizeof(int));   /* previous block's last pointer overwritten */
    ...
}
free(values);                           /* frees only the final round's block */

Nothing goes wrong while the program runs; it prints the right answers and exits 0. Then LeakSanitizer, part of the AddressSanitizer runtime this platform links into every run, walks the heap and files its report on standard error:

==12==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 40 byte(s) in 2 object(s) allocated from:
    #0 0xffff81164488 in malloc (/usr/local/lib64/libasan.so.8+0xd4488)
    #1 0x00000040088c in main /tmp/s.c:10

SUMMARY: AddressSanitizer: 40 byte(s) leaked in 2 allocation(s).

Read it the way it is built: there is no "leak line" to point at, because a leak is an absence, so the stack shows where the lost blocks were allocated, line 10, the malloc in the loop, and the byte count, two 20-byte blocks, tells you which allocation to hunt. Exercises in this chapter set their leak check accordingly: byte-perfect output with a leak report still fails.

The other two memory crimes are sharper. Reading or writing a block after free, and calling free on the same pointer twice, are both undefined behaviour, not "it crashes" or "it prints garbage", the standard promises nothing at all. On this platform AddressSanitizer converts them into immediate aborts whose first line names the crime, from a real run:

==13==ERROR: AddressSanitizer: heap-use-after-free on address 0xfc2f9f3e0040 at pc 0x0000004008c4 bp 0xffffe76290c0 sp 0xffffe76290d8
READ of size 4 at 0xfc2f9f3e0040 thread T0
    #0 0x0000004008c0 in main /tmp/s.c:14

with matching freed by and previously allocated by stacks below, the same anatomy as the leak report. One deliberate kindness closes the lesson: free(NULL) is defined to do nothing. That is why a cleanup path can free every pointer it declared, aimed or not, without checking each one, and it is one more argument for chapter 11's rule that pointers start at NULL.

Key Takeaways

  • malloc(n * sizeof(int)) allocates at run time what #define-sized arrays never could; the block is indeterminate until written, and free returns it.
  • Every malloc, calloc, and realloc result is checked against NULL before use; failure is a return value, not an exception.
  • No cast on allocation results: void * converts implicitly, and the cast historically silenced the missing-<stdlib.h> bug ANSI's rules would otherwise expose.
  • calloc(count, size) takes two arguments and zeroes the block; malloc and realloc zero nothing.
  • Grow with the temporary-pointer idiom: bigger = realloc(p, size); if (bigger == NULL) { ... } p = bigger; — assigning straight to p leaks the old block on failure.
  • One allocation, one owner, one free, on every path including early returns; LeakSanitizer reports a leak by its allocation site, since a leak has no line of its own.
  • Use-after-free and double-free are undefined behaviour, caught by ASan here; free(NULL) is a defined no-op that simplifies cleanup.