3 min read
On this page

malloc & free

Memory Layout of a C Program

A running C program divides memory into four main regions:

  • Text segment. The compiled machine code. Read-only.
  • Data segment. Global and static variables with initial values, plus constants.
  • Stack. Local variables and function call frames. Grows and shrinks automatically. Fixed size (typically 1-8 MB).
  • Heap. Dynamically allocated memory. You control its lifetime. Can grow as needed (limited by system memory).

The heap is where malloc and friends operate.

malloc: Allocate Raw Bytes

malloc allocates a block of memory on the heap and returns a void* pointer to it. The memory is uninitialized.

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

int main(void) {
    int *nums = malloc(5 * sizeof(int));
    if (nums == NULL) {
        fprintf(stderr, "malloc failed\n");
        return 1;
    }

    for (int i = 0; i < 5; i++) {
        nums[i] = (i + 1) * 10;
    }

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

    free(nums);
    return 0;
}
nums[0] = 10
nums[1] = 20
nums[2] = 30
nums[3] = 40
nums[4] = 50

Always use sizeof with the variable, not the type, so the size stays correct if the type changes:

int *p = malloc(n * sizeof(*p));  /* preferred */
int *p = malloc(n * sizeof(int)); /* works but fragile */

calloc: Allocate & Zero-Initialize

calloc allocates memory for an array of elements and sets all bytes to zero:

int *nums = calloc(100, sizeof(int));
if (!nums) {
    perror("calloc");
    return 1;
}
/* All 100 ints are guaranteed to be 0 */

calloc takes two arguments: the number of elements and the size of each element. It also checks for integer overflow in the multiplication, which malloc(n * size) does not.

Use calloc when you need zero-initialized memory. Use malloc when you plan to fill every byte before reading.

realloc: Resize an Allocation

realloc changes the size of an existing allocation. It may move the memory to a new location.

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

int main(void) {
    int capacity = 4;
    int count = 0;
    int *arr = malloc(capacity * sizeof(*arr));
    if (!arr) return 1;

    for (int i = 0; i < 10; i++) {
        if (count >= capacity) {
            capacity *= 2;
            int *tmp = realloc(arr, capacity * sizeof(*tmp));
            if (!tmp) {
                free(arr);
                fprintf(stderr, "realloc failed\n");
                return 1;
            }
            arr = tmp;
        }
        arr[count++] = i * i;
    }

    for (int i = 0; i < count; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr);
    return 0;
}
0 1 4 9 16 25 36 49 64 81

Never assign realloc directly back to the original pointer:

/* WRONG: if realloc fails, arr is lost and the old memory leaks */
arr = realloc(arr, new_size);

/* CORRECT: use a temporary pointer */
int *tmp = realloc(arr, new_size);
if (!tmp) { /* handle error, arr is still valid */ }
arr = tmp;

When realloc succeeds, the old pointer is invalid (possibly freed). Only use the new pointer.

free: Release Memory

free returns memory to the heap. After free, the pointer is dangling and must not be used.

int *data = malloc(100 * sizeof(int));
/* ... use data ... */
free(data);
data = NULL;  /* good practice: prevent accidental use */

Setting the pointer to NULL after freeing is a defensive habit. Dereferencing NULL crashes immediately with a clear error, while dereferencing a freed pointer produces unpredictable behavior.

free(NULL) is safe and does nothing.

Always Check if malloc Returned NULL

malloc, calloc, and realloc return NULL when they fail (out of memory). Using a NULL pointer is undefined behavior.

struct Node *node = malloc(sizeof(*node));
if (node == NULL) {
    fprintf(stderr, "Out of memory\n");
    exit(EXIT_FAILURE);
}

In small programs, exiting on allocation failure is reasonable. In libraries, return an error code so the caller decides what to do.

The Ownership Rule

Whoever allocates memory is responsible for freeing it. This must be documented clearly.

/* Caller owns the returned string and must free it */
char *format_greeting(const char *name) {
    char *buf = malloc(256);
    if (!buf) return NULL;
    snprintf(buf, 256, "Hello, %s!", name);
    return buf;
}

int main(void) {
    char *msg = format_greeting("Alice");
    if (msg) {
        printf("%s\n", msg);
        free(msg);  /* caller's responsibility */
    }
    return 0;
}
Hello, Alice!

If ownership is ambiguous, memory leaks and double frees are inevitable.

Double Free: Undefined Behavior

Freeing the same pointer twice is undefined behavior. It can crash, corrupt the heap, or create exploitable security vulnerabilities.

int *p = malloc(sizeof(int));
free(p);
free(p);  /* UNDEFINED BEHAVIOR */

Setting pointers to NULL after freeing prevents accidental double frees, since free(NULL) is safe:

free(p);
p = NULL;
free(p);  /* safe: free(NULL) is a no-op */

Real-World Example: A Dynamic String

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

struct DynString {
    char *data;
    size_t length;
    size_t capacity;
};

struct DynString *dynstr_create(void) {
    struct DynString *s = malloc(sizeof(*s));
    if (!s) return NULL;

    s->capacity = 16;
    s->data = malloc(s->capacity);
    if (!s->data) { free(s); return NULL; }

    s->data[0] = '\0';
    s->length = 0;
    return s;
}

int dynstr_append(struct DynString *s, const char *text) {
    size_t text_len = strlen(text);
    size_t needed = s->length + text_len + 1;

    if (needed > s->capacity) {
        size_t new_cap = s->capacity;
        while (new_cap < needed) new_cap *= 2;

        char *tmp = realloc(s->data, new_cap);
        if (!tmp) return -1;

        s->data = tmp;
        s->capacity = new_cap;
    }

    memcpy(s->data + s->length, text, text_len + 1);
    s->length += text_len;
    return 0;
}

void dynstr_destroy(struct DynString *s) {
    if (s) {
        free(s->data);
        free(s);
    }
}

int main(void) {
    struct DynString *s = dynstr_create();
    if (!s) return 1;

    dynstr_append(s, "Hello");
    dynstr_append(s, ", ");
    dynstr_append(s, "world!");

    printf("String: \"%s\" (length=%zu, capacity=%zu)\n",
           s->data, s->length, s->capacity);

    dynstr_destroy(s);
    return 0;
}
String: "Hello, world!" (length=13, capacity=16)

This demonstrates malloc for initial allocation, realloc for growth, and free for cleanup.

Real-World Example: Reading an Entire File

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

char *read_file(const char *path, size_t *out_size) {
    FILE *f = fopen(path, "rb");
    if (!f) return NULL;

    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);

    if (size < 0) { fclose(f); return NULL; }

    char *buf = malloc(size + 1);
    if (!buf) { fclose(f); return NULL; }

    size_t read = fread(buf, 1, size, f);
    fclose(f);

    buf[read] = '\0';
    if (out_size) *out_size = read;
    return buf;  /* caller must free */
}

int main(void) {
    size_t size;
    char *content = read_file("example.txt", &size);
    if (content) {
        printf("Read %zu bytes\n", size);
        free(content);
    }
    return 0;
}

Common Pitfalls

  • Not checking for NULL. malloc can fail. Dereferencing NULL is undefined behavior. Always check.
  • Using memory after free. The pointer is dangling. Set it to NULL after freeing.
  • Memory leaks. Every malloc needs a matching free. If a function has multiple return paths, ensure all of them free allocated memory (or transfer ownership to the caller).
  • Integer overflow in size calculations. malloc(n * sizeof(int)) overflows silently if n is very large. Use calloc for overflow-checked allocation, or check the multiplication manually.
  • Mixing allocation functions. Memory allocated with malloc must be freed with free. Do not mix with C++ new/delete or platform-specific allocators.
  • Forgetting to free realloc's old pointer on failure. If realloc fails, the original pointer is still valid and must still be freed.

Key Takeaways

  • malloc allocates raw bytes. calloc allocates and zeros. realloc resizes. free releases.
  • Always check for NULL return values from allocation functions.
  • Every allocation needs exactly one free. Document ownership clearly.
  • Use sizeof(*ptr) instead of sizeof(Type) for resilient size calculations.
  • Double free is undefined behavior. Set pointers to NULL after freeing.
  • The heap is the only option for memory that must outlive the current scope or whose size is unknown at compile time.