Dynamic Memory Allocation in C: malloc, calloc, realloc and free with a Worked Example

A runnable marks example shows how C requests, initializes, grows and releases runtime storage, with checked byte counts, ownership and safe realloc handling.

KnowledgeGate Team

Exam prep & CS education

Updated 2 Sep 20265 min read

A fixed array works when its element count is known while writing the program, but real input can grow at runtime. Four similar library calls have different rules. The runnable program allocates four marks, grows the block to six, calculates a checked result and releases it cleanly. At each step, ask: how many bytes were requested, which pointer owns the live block, and which values are initialized?

Dynamic memory allocation in C: what changes at runtime

int fixed[4] = {72, 85, 91, 68}; has a fixed size. With int *marks = malloc(count * sizeof *marks);, count can arrive at runtime. The pointer and allocated object are separate: the block remains allocated until free() releases it or successful realloc() replaces it.

The pointer is typically in main's stack frame and the block in the heap, but portable C follows lifetime and ownership, not physical placement. Under sizeof(int) == 4, four integers need 4 x 4 = 16 bytes. Allocate dynamically when size arrives at runtime, an object must outlive its creating function, or a structure must grow. Coding and Skill Development Courses covers the broader path.

malloc, calloc, realloc and free have distinct jobs

Call

Purpose

Initial contents

Failure or result

Ownership effect

malloc(bytes)

Request bytes

Indeterminate

NULL on failure

Result owns a block

calloc(count, size)

Request an array

Zero-initialized

NULL on failure

Result owns a block

realloc(pointer, new_bytes)

Resize a block

Added bytes indeterminate

May fail, stay or move

Success replaces old block

free(pointer)

End object lifetime

Not applicable

No return

Ownership ends

With four-byte integers, malloc(4 * sizeof *marks) requests 16 bytes, and no element may be read before assignment. calloc(6, sizeof *seen) requests 24 bytes and yields {0, 0, 0, 0, 0, 0} after success; seen[2] = 1 makes {0, 0, 1, 0, 0, 0}. Then call free(seen);. free(NULL) is permitted, but a second free of the same live address is not.

Include <stdint.h> and reject count > SIZE_MAX / sizeof *marks before multiplying. Allocate, check against NULL, then initialize. sizeof *marks follows the pointed-to type, and C needs no malloc() cast.

A malloc block of four marks beside a zero-initialized calloc block, both checked for NULL before use.

Worked example: allocate four marks and grow them to six

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

int main(void) {
    size_t count = 4;
    if (count > SIZE_MAX / sizeof(int)) {
        return EXIT_FAILURE;
    }

    int *marks = malloc(count * sizeof *marks);
    if (marks == NULL) {
        return EXIT_FAILURE;
    }

    int initial[4] = {72, 85, 91, 68};
    for (size_t i = 0; i < count; ++i) {
        marks[i] = initial[i];
    }

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

    int *grown = realloc(marks, new_count * sizeof *marks);
    if (grown == NULL) {
        free(marks);
        return EXIT_FAILURE;
    }

    marks = grown;
    marks[4] = 77;
    marks[5] = 88;
    count = new_count;

    int total = 0;
    for (size_t i = 0; i < count; ++i) {
        total += marks[i];
    }

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

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

First, {72, 85, 91, 68} totals 72 + 85 + 91 + 68 = 316. Four elements request 16 bytes under the teaching assumption, but the portable code does not hard-code 16. After growth, only the added cells receive 77 and 88: 316 + 77 + 88 = 481, and 481 / 6.0 = 80.1666..., printed as:

count=6 total=481 average=80.17

Safe realloc code separates success from failure

On a successful positive resize, realloc() preserves the first min(old_size, new_size) bytes. Added bytes are uninitialized, and the block may stay or move. Assign marks = grown and treat old aliases as invalid.

For this failed non-zero resize, grown == NULL leaves the original block live and owned by marks. The example frees it before exit. Direct assignment is unsafe because failure loses the owner and leaks the block. Handle zero explicitly with free(), and assign added cells before reading.

Three realloc branches for the marks block: growth in place, a moved block, and failure that leaves the original live.

Ownership and cleanup prevent lifetime errors

Ownership is a discipline, not a C keyword. marks owns one allocation; successful realloc() transfers ownership to its result, and free(marks) ends it. Setting marks = NULL cannot repair copied aliases.

After int *alias = marks; free(marks); marks = NULL;, alias is dangling too. alias[0] cannot recover 72. There is no leak, but that access is invalid.

For two resources, start with int *marks = NULL; char *label = NULL;. If label owns 12 bytes and marks owns 24, route all exits through free(marks); free(label);, including partial failure.

Common dynamic-allocation errors and repairs

Faulty code

Why it happens

What goes wrong

Repair

marks = realloc(marks, 24)

Convenience

Leak on failure

Use a temporary

Read fresh malloc memory

Allocation seems initialized

Indeterminate read

Assign first

free(marks); printf("%d", marks[0]);

Address remains

Use after free

Stop access

Call free(marks) twice

Cleanup overlaps

Double free

One exit path

Return without cleanup

Failure path forgotten

Leak

Common cleanup

A six-element block has indices 0 to 5; marks[6] = 99 is out of bounds. With an 8-byte pointer and 4-byte int, malloc(6 * sizeof marks) requests 48 bytes instead of 24. malloc(6 * sizeof(char)) requests only 6. Use 6 * sizeof *marks.

Compile with cc -std=c11 -Wall -Wextra -Wpedantic -g dma.c -o dma. If supported, add -fsanitize=address,undefined and test a faulty bounds version. Classify reports by size, initialization, bounds, lifetime or ownership. One clean run does not prove every path correct.

How assessments test dynamic allocation

Assessments ask you to calculate bytes under a stated sizeof, compare initialization, trace indices and realloc() failure, or find leaks and dangling pointers. A complete trace should state the requested byte count, initialized range, live owner and valid indices after each operation.

After successful calloc(3, sizeof *p); p[1] = 9;, values are {0, 9, 0} and total 9. With sizeof(int) == 4, malloc(5 * sizeof *q) requests 20 bytes, none readable before initialization. Dynamic storage can implement stacks and queues; per-node allocation and recursive cleanup lead into binary trees and binary search trees.

Dynamic memory allocation in C: the short version

Remember six rules: validate count; size with sizeof *pointer; check allocation; initialize before reading; resize through a temporary; release each owned block once. The path is 4 elements -> 16 bytes -> 6 elements -> 24 bytes -> total 481 -> average 80.17.

For practice, grow {14, 28, 42} from capacity 3 to 5 and append 56, 70. Expect 14 + 28 + 42 + 56 + 70 = 210 and 210 / 5.0 = 42.00, with the same guard, temporary pointer and final free().

Use the C Language Course: Concepts, MCQs and Coding for a structured C path, or Coding for Placements: C, C++, Java and Python for broader problem-solving practice. Compile the program, force every manually reachable cleanup path, and explain who owns the allocation after each line.