Memory Management in C

malloc, calloc, realloc, free, structs, function pointers, and avoiding a real use-after-free bug.

Dynamic memory: malloc, calloc, realloc, free

C has no garbage collector — every byte you request from the heap with malloc/calloc/realloc must eventually be released with a matching free, or it leaks for the lifetime of the process.

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

int main(void) {
    // malloc: allocates raw, UNINITIALIZED memory for 5 ints
    int *nums = malloc(5 * sizeof(int));
    if (nums == NULL) {
        fprintf(stderr, "allocation failed\n");
        return 1;
    }

    for (int i = 0; i < 5; i++) {
        nums[i] = i * 10;
    }
    printf("%d\n", nums[2]); // 20

    free(nums);   // release the memory back to the system
    nums = NULL;  // good practice: avoid leaving a dangling pointer around

    return 0;
}

Always check the return value of malloc — it returns NULL if the allocation fails (out of memory), and dereferencing a NULL pointer is undefined behavior.

C
int *zeros = calloc(5, sizeof(int)); // allocates AND zero-initializes 5 ints
// zeros[0..4] are all guaranteed to be 0, unlike malloc's uninitialized memory

int *bigger = realloc(zeros, 10 * sizeof(int)); // grows (or shrinks) an existing allocation
if (bigger == NULL) {
    // realloc failed — the ORIGINAL 'zeros' block is still valid and must still be freed
    free(zeros);
    return 1;
}
// bigger may be the same pointer as zeros, or a brand-new address — always use the return value
free(bigger);
Function Purpose Initializes memory?
malloc(size) Allocate size bytes No — contents are garbage
calloc(count, size) Allocate count * size bytes Yes — zero-filled
realloc(ptr, newSize) Resize an existing allocation Existing bytes preserved; new bytes uninitialized
free(ptr) Release an allocation back to the system N/A

Structs

A struct groups related fields together under one type — C's basic tool for modeling a record or object's data (with no built-in methods; behavior is just ordinary functions that take the struct as a parameter).

C
typedef struct {
    char name[50];
    int age;
    double salary;
} Employee;

int main(void) {
    Employee e = {"Ada", 30, 85000.0};
    printf("%s is %d\n", e.name, e.age); // Ada is 30

    Employee *ePtr = &e;
    printf("%s\n", ePtr->name);   // "->" dereferences a struct pointer and accesses a field
    printf("%s\n", (*ePtr).name); // equivalent, but -> is idiomatic and far more common

    return 0;
}

Structs are frequently allocated on the heap when their lifetime needs to outlive the function that creates them:

C
Employee *createEmployee(const char *name, int age) {
    Employee *e = malloc(sizeof(Employee));
    if (e == NULL) return NULL;

    strncpy(e->name, name, sizeof(e->name) - 1);
    e->name[sizeof(e->name) - 1] = '\0'; // ensure null termination even if name was truncated
    e->age = age;
    e->salary = 0.0;
    return e;
}

int main(void) {
    Employee *e = createEmployee("Grace", 45);
    if (e != NULL) {
        printf("%s\n", e->name); // Grace
        free(e); // the caller is responsible for freeing what createEmployee allocated
    }
    return 0;
}

Function pointers

A function pointer stores the address of a function, letting you pass behavior around as data — the foundation of callbacks in C (and how C simulates what other languages do with lambdas or first-class functions).

C
#include <stdio.h>

int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }

int apply(int (*operation)(int, int), int x, int y) { // takes a function pointer as a parameter
    return operation(x, y);
}

int main(void) {
    int (*opPtr)(int, int) = add; // opPtr points to the add function

    printf("%d\n", opPtr(3, 4));        // 7
    printf("%d\n", apply(add, 3, 4));      // 7
    printf("%d\n", apply(multiply, 3, 4)); // 12

    return 0;
}

The standard library's qsort is a classic real-world use of function pointers — you pass it a comparison function, and it sorts any array using that comparison logic.

A realistic bug: use-after-free

Freeing memory doesn't erase the pointer's value — it just tells the allocator that memory is available for reuse. Using a pointer after freeing it ("use-after-free") is undefined behavior: it might still appear to work, might crash, or might silently read/write memory that's since been reused by something else entirely.

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

int *createValue(int v) {
    int *p = malloc(sizeof(int));
    *p = v;
    return p;
}

int main(void) {
    int *p = createValue(42);
    printf("%d\n", *p); // 42 — fine so far

    free(p);
    // ... p still holds the OLD address, but that memory is no longer ours ...

    printf("%d\n", *p); // BUG: use-after-free — undefined behavior, may print garbage or crash

    return 0;
}

How to avoid it:

  • Set a pointer to NULL immediately after freeing it — dereferencing NULL crashes predictably and loudly, instead of silently corrupting memory.
  • Never return or store a pointer to memory you've already freed, and be especially careful with pointers copied to more than one variable (freeing through one leaves the others dangling).
  • Use a tool like Valgrind or AddressSanitizer (gcc -fsanitize=address) during development — they catch use-after-free and leaks that are otherwise very easy to miss in manual testing.
C
free(p);
p = NULL;              // now dereferencing p crashes immediately and obviously, instead of silently corrupting data
if (p != NULL) {        // defensive checks like this become meaningful once you adopt this habit
    printf("%d\n", *p);
}

Common mistakes

  • Forgetting to free an allocation on every exit path (including early returns and error branches) — a memory leak.
  • Using a pointer after freeing it (use-after-free) instead of nulling it out immediately.
  • Passing the wrong size to malloc — a classic mistake is malloc(sizeof(ptr)) instead of malloc(sizeof(*ptr)) when allocating for a struct through a pointer variable.
  • Forgetting that realloc can return a different address than its input, and continuing to use the old pointer after a successful realloc.

Interview questions

Q: What's the difference between malloc and calloc? Both allocate heap memory, but malloc(size) leaves the memory uninitialized (containing whatever garbage was previously there), while calloc(count, size) additionally zero-initializes every byte of the allocation. calloc is the safer default when you need the initial contents to be predictable (e.g., a fresh array of counters).

Q: What is a use-after-free bug, and how do you defend against it? It happens when a pointer is dereferenced after the memory it points to has already been freed — the memory may have been reused by something else entirely, so the read/write has undefined, unpredictable effects. The standard defense is disciplined ownership (know exactly who is responsible for freeing each allocation), setting a pointer to NULL immediately after freeing it, and running tools like AddressSanitizer or Valgrind during development to catch it before it ships.