Lesson 6 Β· ~20 minutes Β· ties to mission: reason about where every byte lives

malloc, free, and the Heap

Until now, every variable you've declared lived on the stack β€” its size fixed at compile time, its lifetime tied to a function scope. Now you get to ask the OS for memory at runtime. This is how real programs work.

The three memory regions β€” and why the heap matters

Stack Automatic. Local variables, function parameters. Created on entry, destroyed on return. Fast. Size known at compile time.
Lifetime: the function call
Static / Global static locals, globals, string literals. Exist for the entire program. String literals are read-only.
Lifetime: the whole program
Heap Manual. You ask for it with malloc, you release it with free. You choose the size at runtime. You own the lifetime.
Lifetime: from malloc to free

You've been using the stack this whole time. char buf[100]; β€” 100 bytes on the stack. int n = 5; β€” 4 bytes on the stack. Clean, automatic, invisible.

But what if you don't know the size at compile time? What if the user types a string of unknown length and you need to store it? That's when you need the heap.

malloc β€” ask the OS for memory

#include <stdlib.h>     // always include this for malloc/free

char *p = malloc(100);

One line. Here's what it does:

  1. Asks the operating system for 100 contiguous bytes of memory on the heap.
  2. Returns a void * β€” a generic pointer to the first byte.
  3. If the OS can't find 100 bytes, returns NULL.
The heap contract malloc(n) returns either a pointer to n bytes you own, or NULL if it failed. Those are the only two outcomes. The memory is uninitialized β€” whatever bytes were there before are still there. If you want zeros, use calloc.

The pattern β€” always check for NULL

char *buf = malloc(100 * sizeof(char));
if (buf == NULL) {
    printf("Out of memory!\n");
    return 1;            // or handle the error
}
// now buf is 100 bytes of heap memory, all yours

Why check? Because malloc can fail β€” especially with very large allocations, or on memory-constrained systems. Dereferencing NULL is undefined behavior β€” the same class of bug as your bare-pointer scanf from lesson 1.

Why malloc(100 * sizeof(char)) instead of just malloc(100)? sizeof(char) is always 1, so for char arrays the multiplication is redundant. But the habit malloc(n * sizeof(type)) is load-bearing for other types. sizeof(int) is 4 on most machines, so malloc(100 * sizeof(int)) gets you space for 100 ints β€” 400 bytes. Write the sizeof every time and you'll never miscalculate on int, struct, or any multi-byte type.

free β€” give it back

char *buf = malloc(100 * sizeof(char));
if (!buf) { return 1; }

/* use buf */

free(buf);      // return the memory to the OS
buf = NULL;    // optional but smart: prevents use-after-free

free(buf) tells the OS "I'm done with this memory." The bytes are returned to the heap for reuse. After free, buf is a dangling pointer β€” it holds an address that no longer belongs to you.

Three sins of heap memory (1) Use-after-free. Read or write through a pointer after free. UB. The memory may have been given to someone else.
(2) Double-free. Call free twice on the same pointer. UB. The heap's bookkeeping is corrupted.
(3) Memory leak. malloc without a matching free. The memory stays claimed until the program exits. Small leaks are tolerable in short-lived programs; in long-running ones they accumulate and crash.

malloc + strings β€” the real "dynamic string"

Here's a concrete example. You have two strings and you want to concatenate them into a new string β€” on the heap, sized exactly right:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *join(const char *a, const char *b) {
    // Count the exact size we need
    size_t len = strlen(a) + strlen(b) + 1;

    // Ask the heap for exactly that many bytes
    char *result = malloc(len * sizeof(char));
    if (result == NULL) {
        return NULL;
    }

    // Copy both parts in β€” using your lesson 2/5 skills
    char *p = result;
    while (*a != '\0') { *p++ = *a++; }
    while (*b != '\0') { *p++ = *b++; }
    *p = '\0';

    return result;       // caller now owns this memory
}

int main(void) {
    char *s = join("Hello", "World");
    if (s) {
        printf("%s\n", s);   // β†’ HelloWorld
        free(s);            // must free what we malloc'd
    }
    return 0;
}

Notice the discipline: join mallocs, the caller frees. Whoever allocates is responsible for documenting who frees. In library code, this is the convention: the function that returns heap memory transfers ownership to the caller.

Stack vs heap β€” when to use which

Stack (char buf[100])Heap (malloc(100))
SizeFixed at compile timeChosen at runtime
LifetimeAutomatic β€” gone when function returnsManual β€” lives until you free
SpeedVery fast (just move a stack pointer)Slower (OS call, bookkeeping)
Size limitSmall (typically 1–8 MB)Large (limited by available RAM)
ErrorsStack overflow if too deepLeaks, use-after-free, double-free
Best forSmall, short-lived buffersLarge or unknown-size data
Rule of thumb If you know the size at compile time and it's small β€” use the stack. If the size depends on user input, file contents, or could be large β€” use the heap. Default to stack; escalate to heap when you need it.

calloc β€” malloc that zeroes

// These two are equivalent:
int *a = malloc(100 * sizeof(int));     // 100 ints, uninitialized garbage
int *b = calloc(100, sizeof(int));    // 100 ints, all set to 0

calloc(count, size) takes two arguments: how many elements, and how big each one is. It zeroes the memory. Slightly slower, but safer β€” uninitialized heap memory is a common source of unpredictable bugs.

realloc β€” resize a heap allocation

char *buf = malloc(50);
/* ... use buf ... */

/* Need more space? */
char *bigger = realloc(buf, 200);
if (bigger == NULL) {
    // realloc failed β€” original buf is still valid!
    free(buf);
    return 1;
}
buf = bigger;   // now buf points at 200 bytes

realloc tries to grow (or shrink) the allocation in place. If it can't, it allocates new memory, copies the old data over, frees the old block, and returns the new pointer. If it fails, it returns NULL β€” and the old pointer is still valid. This is the one trap: don't do buf = realloc(buf, 200); without checking β€” if it returns NULL, you've lost the original pointer (leak + can't free).

The realloc trap
buf = realloc(buf, 200);  // if NULL β†’ you leaked the original buf!
Always use a temporary variable, check for NULL, then reassign. As shown above.

The malloc family β€” quick reference

FunctionDoesNotes
malloc(n)Allocates n bytes. Uninitialized.Returns void * or NULL.
calloc(count, size)Allocates count Γ— size bytes. Zeroed.Safer default for arrays/structs.
realloc(ptr, n)Resizes allocation to n bytes. Preserves data.Check for NULL β€” old pointer stays valid on failure.
free(ptr)Returns memory to the heap.Only free what you malloc'd. One time only.

Check yourself

β‘  What's wrong with this?

char *s = malloc(6);
strcpy(s, "Hello");
printf("%s\n", s);
  
Show answer Two things. First: no #include <stdlib.h> β€” the compiler may not see malloc's declaration, defaulting to assuming it returns int, which will corrupt the pointer on 64-bit systems. Second: no NULL check β€” if malloc fails, strcpy writes to address 0. Both are real bugs, even if they don't crash on your machine today.

β‘‘ After free(p), is the memory zeroed?

Show answer No. free returns the memory to the heap's internal bookkeeping. The bytes are still there, unchanged. The data may or may not persist β€” it's undefined. Some implementations overwrite it with debug patterns in debug mode, but you cannot rely on that. The only guarantee is that you no longer own those bytes.

β‘’ What's a memory leak?

Show answer Memory that was malloc'd but never free'd. The pointer was lost (went out of scope, was overwritten) before free was called. The OS can't reclaim it until the program exits. Example: char *s = malloc(100); s = "hello"; β€” the malloc'd 100 bytes are now unreachable (the pointer was overwritten by a pointer to a string literal). The original 100 bytes are leaked.

β‘£ malloc(10 * sizeof(int)) β€” how many bytes, and how many ints?

Show answer On a typical 64-bit system where sizeof(int) = 4: 40 bytes, which is room for 10 ints. The sizeof multiplication is what converts "I want 10 ints" into "I need 40 bytes." If you wrote malloc(10) you'd only get 10 bytes β€” room for 2 ints and 2 bytes left over. sizeof is the conversion factor.

πŸ› οΈ Hands-on β€” build a dynamic string duplicator

Write a function mystrdup(const char *src) that returns a heap-allocated copy of src:

  1. Use strlen to find the length. Allocate len + 1 bytes with malloc.
  2. Check for NULL. If allocation fails, return NULL.
  3. Copy the string into the new buffer (use strcpy, or your mystrcpy from lesson 5).
  4. Return the new pointer. The caller owns it and must free it.
  5. In main: call mystrdup("Hello, world!"), print the copy, then free it.
  6. Verify: modify the original string literal… wait, you can't (it's const). But confirm the copy is independent by printing its address and the literal's address β€” they'll differ.
  7. Now test failure: try mystrdup(NULL) β€” does it crash? Add a guard: if src == NULL, return NULL early.
Compare with a model solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *mystrdup(const char *src) {
    if (src == NULL) return NULL;

    size_t len = strlen(src);
    char *copy = malloc(len + 1);
    if (copy == NULL) return NULL;

    // Copy using lesson 5's compact form
    char *dst = copy;
    while (*src != '\0') {
        *dst++ = *src++;
    }
    *dst = '\0';

    return copy;
}

int main(void) {
    char *s = mystrdup("Hello, world!");
    if (s) {
        printf("Copy: %s\n", s);
        printf("Address of copy: %p\n", (void *)s);
        free(s);
    }
    return 0;
}
What you can now do Allocate memory on the heap with malloc/calloc, resize it with realloc, and release it with free. You know the three heap sins (use-after-free, double-free, leak) and how to avoid them. You can write a function that returns heap memory and document the ownership contract. And you understand when to use the stack vs the heap β€” the decision is driven by whether size is known at compile time.

Coming soon β€” realloc in practice

Lesson 6 covers the basics. A future lesson will revisit realloc in depth β€” building a dynamically-growing string or array that starts small and grows as needed. That's where realloc becomes essential. For now, the pattern to remember is: malloc β†’ use β†’ free, always in pairs, always check for NULL.

Questions? Ask your agent. Anything murky β€” "what's the difference between malloc and new? (C++ question β€” skip for now)", "what happens if I free a stack pointer?", "how do I see memory leaks with tools?" β€” type it and we'll dig in. You now have the full picture of C's three memory regions.
Sources & further reading
Β· cppreference: malloc β€” exact semantics, return type, and the full family.
Β· Beej's Guide to C Programming β€” has a thorough section on heap memory.
Β· comp.lang.c FAQ β€” search for "malloc" for classic gotchas.
Β· cppreference: realloc β€” the return-value trap is documented here.
Β· Companion cheat sheet Β· Lesson 5: mystrcpy