Frontend Labs
⋯

Part II — Intermediate: memory, modules, and tooling

Part I's programs lived comfortably inside one function's worth of stack memory. This Part is where C stops being safe by default: dynamic allocation and the ownership rules that go with it, splitting a program across multiple files, file I/O, and the debugging tools (sanitizers, a minimal test harness) you need once a bug is no longer obvious from reading the code. Ends with a vector library, a CSV parser, and a JSON parser — real tools, not toy examples.

10. Dynamic memory: the malloc family, ownership, and the bugs it enables

10.1 The four functions and what they actually do

#include <stdlib.h>

void *malloc(size_t size);              // allocate size bytes, uninitialized
void *calloc(size_t n, size_t size);    // allocate n*size bytes, zeroed, overflow-checked
void *realloc(void *ptr, size_t size);  // resize, may move, contents preserved up to min(old,new)
void  free(void *ptr);                  // return memory to the allocator
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *arr = malloc(10 * sizeof(int));
    if (!arr) { perror("malloc"); return 1; }   // ALWAYS check — allocation can fail

    for (int i = 0; i < 10; i++) arr[i] = i * i;

    int *bigger = realloc(arr, 20 * sizeof(int));
    if (!bigger) {           // realloc failing does NOT free the original
        free(arr);            // so you still must free the original on failure
        return 1;
    }
    arr = bigger;             // only reassign after confirming success

    for (int i = 10; i < 20; i++) arr[i] = i * i;
    for (int i = 0; i < 20; i++) printf("%d ", arr[i]);
    printf("\n");

    free(arr);
    return 0;
}

Why calloc(n, size) over malloc(n * size): calloc checks for multiplication overflow internally before allocating; n * size computed by hand can silently overflow and allocate a tiny buffer that you then write far past the end of. This exact bug class (integer overflow feeding an allocation size) is CWE-190 and shows up in real CVEs regularly.

10.2 The four classic memory bugs, each demonstrated and fixed

// 1. Memory leak — allocated, never freed
void leak(void) {
    char *buf = malloc(100);
    (void)buf;
}   // buf goes out of scope; the 100 bytes are now unreachable and unfreeable

// 2. Use-after-free — reading/writing memory you already returned
void use_after_free(void) {
    char *buf = malloc(100);
    free(buf);
    buf[0] = 'x';        // UB — the allocator may have already reused this memory
}

// 3. Double free — freeing the same pointer twice corrupts the allocator's internal metadata
void double_free(void) {
    char *buf = malloc(100);
    free(buf);
    free(buf);            // UB — often crashes, sometimes exploitable
}

// 4. Buffer overflow — writing past what was allocated
void overflow(void) {
    char *buf = malloc(10);
    buf[10] = 'x';        // UB — index 10 is one past the 10 valid bytes (0..9)
    free(buf);
}

Section 14 shows how AddressSanitizer catches every one of these instantly, with a stack trace pointing at the exact allocation and the exact bad access — this is why no serious C project ships without running its test suite under ASan.

10.3 Ownership: the discipline that replaces a garbage collector

C has no automatic memory management, so every project needs an explicit ownership convention — a rule for who is responsible for freeing what. The three that cover almost all real code:

  1. Caller allocates, caller frees (most common): a function fills a buffer the caller provided.
  2. Callee allocates, caller frees (strdup, malloc-returning functions): document this in the function's name or a comment — create_, _new, _dup prefixes/suffixes are a strong convention.
  3. Ownership transfer: a function takes a pointer and becomes responsible for freeing it later (e.g., adding a node to a linked list that owns its nodes).
// Convention: functions named *_new return heap memory the caller must
// free with the matching *_free. This pairing is the backbone of every
// C library's public API — see Part VI's SQLite/Redis/curl walkthroughs.
typedef struct { char *data; size_t len; } Buffer;

Buffer *buffer_new(size_t capacity) {
    Buffer *b = malloc(sizeof(Buffer));
    if (!b) return NULL;
    b->data = malloc(capacity);
    if (!b->data) { free(b); return NULL; }
    b->len = 0;
    return b;
}

void buffer_free(Buffer *b) {
    if (!b) return;      // free(NULL) is always safe — mirror that here
    free(b->data);
    free(b);
}

Real-world example. This exact _new/_free pairing, with a NULL check at the top of _free so foo_free(NULL) is always safe, is the pattern you'll see in Section 33's SQLite walkthrough (sqlite3_close), Section 34's Redis walkthrough (sdsfree), and Section 35's curl walkthrough (curl_easy_cleanup).


Generated from real-life-c-guide.md · 8 Oct 2026 16:36