Memory Management in C: Stack, Heap, malloc, calloc, realloc and free with a Worked Example

Learn how C object lifetimes, allocation functions, ownership, bounds, and cleanup fit together through a complete resizable-array example.

KnowledgeGate Team

Exam prep & CS education

Updated 4 Sep 20266 min read

You may recognise malloc() and free() but still wonder where the pointer itself lives, whether fresh bytes contain zeroes, or what remains valid after realloc(). A practical model of C object lifetimes includes allocation, growth, calculation, and cleanup. Numerical byte counts use an illustrative implementation where sizeof(int) == 4; the code stays portable by using sizeof *pointer.

Memory management in C starts with lifetime, not just malloc

Scope controls where a name is visible. Storage duration controls how long its object exists. Common systems use stack, static-storage, and heap regions, but C does not mandate regions by those names.

C defines automatic, static, allocated, and thread storage durations. File-scope int audit_total = 7; has static storage duration. Inside main, size_t count = 4; and pointer variable int *scores have automatic storage duration. malloc(count * sizeof *scores) creates a separate allocated object that exists until released.

On our 4-byte-int system, scores points to a 16-byte block holding {12, 18, 21, 27}. Pointer and pointee are separate objects with separate lifetimes. The OS layer appears in Memory Management in OS: Paging and Segmentation. Dynamic allocation is one part of the C lifetime model. Dynamic Memory Allocation in C: malloc, calloc, realloc and free with a Worked Example gives the focused allocator-function and resizing treatment; the broader check is whether each pointer and pointee still names a live object. malloc() does not choose a page or physical address.

C memory map: audit_total in static storage, scores and count on the stack, and scores pointing to a 16-byte heap block of 12, 18, 21, 27.

malloc, calloc, realloc and free have different contracts

Function

Contract

malloc(bytes)

Reserves an uninitialised block. Its initial values are indeterminate.

calloc(count, size)

Reserves a block and zero-initialises its bytes.

realloc(pointer, new_bytes)

May resize the block in place or move it.

free(pointer)

Ends the allocated object's lifetime. Passing NULL is allowed.

malloc(4 * sizeof *scores) requests 16 uninitialised bytes here. calloc(5, sizeof *scores) requests 20 bytes for five integers initially equal to zero. Check either result before dereferencing. In C, do not cast malloc(); sizeof *scores automatically follows a changed pointee type.

For a variable-size request, include <stdint.h> for SIZE_MAX and guard the multiplication first:

if (count > SIZE_MAX / sizeof *scores) {
    /* reject the request */
}

Validate multiplication, allocate, test for NULL, then initialise before reading. free() neither clears the pointer variable nor promises to zero released bytes.

Memory allocation in C: trace three sensor samples as they grow to five

The C11 program allocates three sensor samples, grows the block to five, computes their average, and releases the allocation.

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

int audit_total = 7;

int main(void) {
    size_t count = 3;
    int *samples = NULL;
    if (count > SIZE_MAX / sizeof *samples) return EXIT_FAILURE;

    samples = malloc(count * sizeof *samples);
    if (samples == NULL) return EXIT_FAILURE;

    int initial[] = {8, 13, 21};
    int sum = 0;
    for (size_t i = 0; i < count; ++i) {
        samples[i] = initial[i];
        sum += samples[i];
    }

    size_t new_count = 5;
    if (new_count > SIZE_MAX / sizeof *samples) {
        free(samples);
        return EXIT_FAILURE;
    }

    int *grown = realloc(samples, new_count * sizeof *samples);
    if (grown == NULL) {
        free(samples);
        return EXIT_FAILURE;
    }
    samples = grown;
    count = new_count;
    samples[3] = 34;
    samples[4] = 55;
    sum += samples[3] + samples[4];

    double average = (double)sum / count;
    printf("count=%zu, sum=%d, average=%.1f\n", count, sum, average);

    free(samples);
    samples = NULL;
    return EXIT_SUCCESS;
}

The first request is 3 x 4 = 12 bytes, with 8 + 13 + 21 = 42. Growth requests 5 x 4 = 20 bytes. Values become {8, 13, 21, 34, 55}, so 42 + 34 + 55 = 131 and 131 / 5.0 = 26.2. Output is count=5, sum=131, average=26.2.

Setting samples = NULL after free(samples) is useful local discipline, but it does not repair any other aliases that still hold the released address.

realloc in C preserves values but not addresses

On success, the first min(old_size, new_size) bytes are preserved. New bytes remain indeterminate until initialised. Only use the returned pointer, whether its numeric address is unchanged or different.

For this non-zero 20-byte request, failure returns NULL while samples still owns the valid 12-byte block. Direct assignment with samples = realloc(samples, 20) could overwrite its only reference and leak it. A temporary pointer preserves ownership.

Ownership rules make C cleanup predictable

Ownership is a program rule, not a C keyword. int *load_scores(size_t *count) can return an allocated array and transfer ownership to its caller, which must call free() exactly once.

In int *bad(void) { int local = 9; return &local; }, the address escapes, but automatic object local stops existing on return. Dereferencing the result has undefined behaviour. String literals and global objects must not be passed to free() merely because pointers reach them.

For multiple resources, initialise owners to NULL and use one cleanup path:

char *name = NULL;
int *scores = NULL;
int status = EXIT_FAILURE;

name = malloc(32);
if (name == NULL) goto cleanup;
scores = malloc(6 * sizeof *scores); /* 24 bytes here */
if (scores == NULL) goto cleanup;
status = EXIT_SUCCESS;

cleanup:
free(scores);
free(name);

If the second allocation fails, cleanup still releases name. Normal exit releases both blocks once.

Memory leaks, dangling pointers and bounds errors in C

Bug

Why it happens

What goes wrong

Fix

Overwrite the only pointer to a live 16-byte block

A new allocation is assigned too early

The old block leaks

Free it first or preserve its owner

Read alias[1] after alias = scores; free(scores);

Both pointers referred to one allocation

alias is dangling, so the read is use-after-free

Stop all access after the free

Free the same non-null pointer twice

Ownership is unclear

The second free has undefined behaviour

Give one owner one cleanup duty

Read samples[5] from five elements

The index is mistaken for a count

Index 5 is out of bounds

Use only indices 0 through 4

With an 8-byte pointer and 4-byte int, malloc(5 * sizeof samples) requests 5 x 8 = 40 bytes because samples is a pointer. Five integers need 5 x 4 = 20 bytes. This over-allocation is still a reasoning bug; a larger pointee could instead cause under-allocation. Use malloc(5 * sizeof *samples).

Compile with strong warnings and, where supported, AddressSanitizer: cc -std=c11 -Wall -Wextra -Wpedantic -fsanitize=address -g memory.c -o memory. Use its reports to trace the violated lifetime or bound. Leak detection support varies by platform.

Memory management questions test tracing, bytes and validity

Trace int *p = malloc(3 * sizeof *p); p[0] = 4; p[1] = 7; p[2] = 9; int *q = p; free(p);. Is q[1] still 7? No. q is dangling, so reading it has undefined behaviour. One allocation and one free means the defect is a lifetime violation, not a leak.

After successful realloc(), use its returned pointer even if its address looks unchanged. After failure for a non-zero request, the original pointer stays valid and must be freed. realloc() neither always moves nor always extends in place.

For any trace, ask four questions:

  1. How many bytes were requested?

  2. Which indices are within bounds?

  3. Who owns each allocation now?

  4. Which pointers still refer to live objects?

Memory management in C: the short version and next step

Keep five rules: calculate sizes with sizeof *pointer; check multiplication and allocation failure; initialise before reading; grow through a temporary pointer; release each owned allocation exactly once. As a self-check, shrinking the worked five-element block to two integers would request 2 x 4 = 8 bytes, preserve {8, 13} after success, and give 8 + 13 = 21.

Beginners can use the C Programming Course for the wider language foundation. For concept, MCQ, and coding-question practice, continue with the C Language Course: Concepts, MCQs and Coding Questions. Once you can allocate, resize, traverse, and free an array reliably, Coding & DSA Courses for Placements is the broader path.

Now implement the worked example, force one allocation-failure path if your environment permits, and verify that every successful allocation has one owner and one cleanup path.