Dynamic Memory Allocation in C: malloc, calloc, realloc and the Bugs Exams Test

Follow one resizable integer array from allocation to release. The worked trace explains byte counts, preserved values, uninitialised elements and the safe temporary-pointer pattern.

KnowledgeGate Team

Exam prep & CS education

Updated 1 Aug 20266 min read

Dynamic memory is where clean-looking C code can hide its worst bugs, and exam questions know it. A program may compile while leaking a block, reading uninitialised bytes or using a pointer after the object is gone.

The safe pattern is small: allocate the right number of bytes, check the result, resize through a temporary pointer, free exactly once and clear pointers you might otherwise reuse. Every classic dynamic-memory bug is one of those five steps done wrong, and the byte counts are where the mistake shows.

Stack vs heap and the four memory functions

An automatic local variable lives for the duration of its block. Its storage is reclaimed automatically when execution leaves that block, which is why implementations keep it on the stack.

Dynamically allocated storage comes from the heap. Its lifetime is not tied to the block that requested it. The allocation remains until the program passes its address to free, so ownership must be explicit.

The four functions to know are declared in <stdlib.h>:

Function

Purpose

Important result

malloc(size)

Requests one block of size bytes

Bytes are uninitialised; returns NULL on failure

calloc(n, size)

Requests space for n objects of size bytes each

All bytes are initialised to zero; returns NULL on failure

realloc(ptr, newsize)

Changes the size of an allocated block

May move the block; returns the new address or NULL on failure

free(ptr)

Releases an allocated block

The old pointer value must not be dereferenced afterwards

Allocation functions return void *, which converts to an object-pointer type in C. Do not hide a missing <stdlib.h> declaration by adding a cast. Include the header and check the returned pointer.

These byte counts assume sizeof(int) = 4 bytes, which is what mainstream 32-bit and 64-bit compilers give. The C standard guarantees only that an int holds at least 16 bits, so a size calculation always multiplies by sizeof rather than a hard-coded 4.

Worked example: a resizable int array

First request space for five integers:

int *a = malloc(5 * sizeof(int));
if (a == NULL) {
    return;
}

The byte calculation is:

  1. Number of elements: 5

  2. Bytes per element: 4

  3. Requested size: 5 * 4 = 20 bytes

The 20 bytes are uninitialised, so reading a[0] before writing it would be undefined behaviour. Fill all five valid elements:

a[0] = 10;
a[1] = 20;
a[2] = 30;
a[3] = 40;
a[4] = 50;

Now grow the array to eight integers. Use a temporary pointer:

int *tmp = realloc(a, 8 * sizeof(int));
if (tmp == NULL) {
    free(a);
    return;
}
a = tmp;

The new request is 8 * 4 = 32 bytes. If it succeeds, realloc preserves the existing data up to the smaller of the old and new sizes. Therefore a[0] through a[4] still hold 10, 20, 30, 40, 50.

The extra 32 - 20 = 12 bytes provide space for three more integers because 12 / 4 = 3. Those new elements, a[5], a[6] and a[7], are uninitialised. Growing with realloc does not zero the added region.

For comparison, allocate five integers with calloc:

int *b = calloc(5, sizeof(int));
if (b == NULL) {
    free(a);
    return;
}

This also requests 5 * 4 = 20 bytes, but all bytes start at zero, so b[0] through b[4] are zero in this integer example.

Release both allocations when they are no longer needed:

free(a);
a = NULL;

free(b);
b = NULL;

Assigning NULL does not free memory. The preceding free does that. Clearing the pointer simply prevents this variable from continuing to hold a stale address.

Keeping a pointer's own value separate from the object it points to is what makes this kind of trace readable. Pointers in C for GATE builds that mental model with address diagrams.

Heap diagram: realloc moves the array to a new 32-byte block, leaving three uninitialised slots beside a separate zeroed calloc block.

The addresses in the figure are illustrative. The important rule is that a successful realloc may return the same address or a different one. Code must work in either case.

The realloc self-assignment trap

This compact line is unsafe:

a = realloc(a, 8 * sizeof(int));

If the request succeeds, it appears to work. If it fails, realloc returns NULL and leaves the original allocation unchanged. The assignment then overwrites the only saved address with NULL. The original 20-byte block still exists, but the program can no longer reach or free it. The block leaks, and its five stored values are lost to the program.

The temporary-pointer form avoids that loss:

int *tmp = realloc(a, 8 * sizeof(int));
if (tmp == NULL) {
    /* a still points to the original block */
    free(a);
    return;
}
a = tmp;

The assignment to a occurs only after success. Until then, a retains ownership of the original block. In a program that can recover from the failed growth, you could keep using a instead of freeing it; the example frees it because the function is returning.

The dynamic-memory bug catalogue

Most exam questions draw from a short set of ownership failures.

Memory leak

A leak occurs when allocated storage remains live but the program has lost every pointer that could free it. Realloc self-assignment is one route. Another is assigning a fresh allocation to a without first freeing the block already owned by a.

Dangling pointer

After free(a), the variable a may still contain the old address, but no live object belongs to the program there. Dereferencing it, reading it or writing through it is undefined behaviour. Set a = NULL when that variable should no longer refer to anything.

Timeline of a dangling pointer: it points to a live block, keeps a stale address after free, then becomes safe once set to NULL.

Double free

Calling free(a) twice on the same block is undefined behaviour, because after the first call the block is no longer yours to release. Clearing the pointer after the first release helps because free(NULL) is safe, but ownership design is the real fix. Each block should have one clear release point.

Uninitialised read

malloc does not initialise its block. Reading an element before storing a value is undefined behaviour. calloc zero-initialises the allocated bytes, but that does not remove the need for a failure check.

Missing NULL check

Allocation can fail. Dereferencing the returned pointer before checking it can therefore dereference NULL. Test the result immediately, before filling the block.

Wrong size calculation

With sizeof(int) = 4, malloc(5) requests only 5 bytes. Five integers need 5 * sizeof(int) = 5 * 4 = 20 bytes. Prefer malloc(5 * sizeof *a) when a has the intended pointer type, because it stays correct if the pointed-to type changes.

Storage duration and allocated lifetime are related but different ideas. Storage Classes in C is the useful next comparison for understanding what the language manages automatically and what your code must release.

How exams test this and the short version

GATE Programming and Data Structures questions, CDAC C-CAT Section B, placement tests and viva rounds commonly ask which line leaks, whether a read is defined, what values survive a resize or which allocation is large enough.

For any snippet, write down four facts: allocated byte count, current owner pointer, initialised range and release point. In the worked example, those checks give 20 original bytes, 32 resized bytes, five preserved integers and three uninitialised new integers. Both calculations agree: the growth is 12 bytes, and 12 / 4 gives the three new slots.

KnowledgeGate's question bank carries over 600 questions on pointers alone, along with sets on heap allocation and memory-management bugs. Work through the C Language Course: Concepts, MCQs & Coding as the primary next step, with GATE Guidance by Sanchit Sir for PDS exam depth. The Coding & DSA Courses for Placements path connects these rules to programs you write yourself.

The short rule is: allocate the correct byte count, check NULL, resize through a temporary pointer, free each owned block once and null out a pointer that should no longer be used.