Dynamic Memory Allocation

Every array and variable used so far has had a size fixed at compile time. malloc, calloc, realloc, and free let a program request and release memory at runtime, exactly when it needs it and exactly how much it needs.

Every array and ordinary variable you have written so far has had its size fixed the moment the compiler saw its declaration — you had to know, in advance, exactly how many elements you would need. Dynamic memory allocation removes that constraint: four functions declared in <stdlib.h> let a running program ask the heap for exactly the memory it needs, exactly when it needs it, resize that memory later if requirements change, and hand it back the moment it is no longer needed. This page walks through the whole lifecycle in order — why dynamic allocation exists, the stack-versus-heap picture behind it, each of the four core functions (malloc, calloc, realloc, free) in turn, the mental model of a pointer variable versus the object it points to, and the classic bugs — leaks, dangling pointers, double frees, use-after-free — that come from managing that lifecycle by hand.

Why Dynamic Memory Allocation Is Needed

Consider an ordinary array declaration such as int numbers[100];. Its size, 100, is baked in the moment the array object is created, and nothing in the program can change that afterward. That is fine as long as you happen to know the right size in advance — but what if the actual amount of data depends on something only known while the program is running, such as a number the user types in?

If the user says they need 500 numbers, a 100-element array is simply too small. You could guess bigger and declare int numbers[10000];, but now the program reserves that much space every single time it runs, even on the runs that only need 3 numbers — wasted memory that scales with however generous your guess was.

Dynamic allocation solves this by moving the size decision from compile time to runtime: the program starts, reads however much space it actually needs, asks the heap for precisely that much memory, uses it for as long as it is needed, and releases it again when it is done.

Nothing about this flow is possible with a plain fixed-size array — the size has to be known before the array is declared. Dynamic allocation defers that decision until the "read required size" step actually runs.

Program starts
     |
     v
Read required size
     |
     v
Request memory from heap
     |
     v
Use allocated memory
     |
     v
Release memory

Stack Memory vs Heap Memory

Dynamic allocation is closely tied to a particular storage duration, usually called allocated storage duration, and the memory region it draws from is commonly called the heap. A simplified conceptual picture of a running program's memory looks roughly like this (this is a mental model to build intuition, not a guarantee of the literal layout every C implementation uses):

An ordinary local variable is created on the stack and disappears automatically the moment the block it was declared in ends:

A dynamically allocated object works differently. Its lifetime is not tied to any block or function call at all — it lasts until the program explicitly releases it:

It is worth separating two things here that are easy to blur together: p itself is an ordinary local variable living on the stack, while the int object p points to lives separately, on the heap, with its own independent lifetime.

Higher addresses
+---------------------------+
|          Stack            |
|  local variables          |
|  function call data       |
+---------------------------+
|                            |
|      Free / other          |
|                            |
+---------------------------+
|           Heap             |
|  dynamically allocated     |
|  objects                   |
+---------------------------+
|  Global / static data      |
+---------------------------+
|  Program code              |
+---------------------------+
Lower addresses
void function(void)
{
    int x = 10;
    /* x has automatic storage duration --
       it ceases to exist when function() returns */
}
int *p = malloc(sizeof(int));
/* the int object malloc() created has allocated storage duration */

The <stdlib.h> Header

All four dynamic-memory functions are declared in one standard header:

The primary functions this header provides are malloc(), calloc(), realloc(), and free() — one to request memory, one to request zero-initialized memory, one to resize an existing allocation, and one to release memory back to the system. Every section below covers one of these four functions, or a consequence of using them, in turn.

#include <stdlib.h>

malloc() Basics and Syntax

malloc, short for memory allocation, requests a block of memory of a specified size, in bytes, and returns a pointer to the start of that block.

malloc() takes a byte count as a size_t — an unsigned integer type guaranteed large enough to represent the size of the largest object the implementation supports — and returns void *, a generic pointer. C implicitly converts that void * to whatever object-pointer type you assign it to, so no explicit cast is required (see the section on casting malloc() below).

For example, this requests exactly enough memory for one int:

p is a small variable that simply stores the address 0x9000. The int object p points to is a completely separate piece of memory, living on the heap, and its value is not yet meaningful.

That last point matters: the memory malloc() hands back is not initialized to any useful value — whatever bytes happen to already sit at that address are what you get, until you write to it yourself.

void *malloc(size_t size);
int *p = malloc(sizeof(int));

Allocating an Array with malloc()

malloc() is just as commonly used to obtain an array-like block of several elements. Suppose the program needs n integers, where n is only known at runtime:

The allocated block can then be accessed exactly like an ordinary array, using square-bracket notation:

This works because indexing is defined in terms of pointer arithmetic: numbers[i] is, by definition, equivalent to *(numbers + i). Since numbers is simply a pointer to the first element of the allocated block, that equivalence holds just as well for dynamically allocated memory as it does for a declared array.

int n = 5;
int *numbers = malloc(n * sizeof *numbers);
numbers
   |
   v
+----+----+----+----+----+
| ?  | ?  | ?  | ?  | ?  |
+----+----+----+----+----+
  0    1    2    3    4
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;

Always Check for Allocation Failure

Memory allocation is a request, not a guarantee — the heap can run out of space, so any call to malloc() (or calloc() or realloc()) can fail. When it does, it returns NULL instead of a usable pointer.

It is tempting to skip this check and go straight to using the pointer. But if malloc() fails and returns NULL, writing to numbers[0] immediately afterward dereferences a null pointer — undefined behavior that typically crashes the program. Always test the returned pointer against NULL before using it for anything.

Every dynamic-memory example on this page follows the same shape: allocate, check for NULL, use, then free. This one reads n from the user, so the array size genuinely is not known until the program is already running.

Read n from standard input.

Ask the heap for n ints with malloc(n * sizeof *numbers).

If numbers is NULL, report the failure and exit before touching the pointer.

Otherwise fill the array, print it, and release it with free() when done.

int *numbers = malloc(n * sizeof *numbers);
if (numbers == NULL) {
    /* allocation failed */
}
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int n;
    printf("Enter number of elements: ");
    scanf("%d", &n);

    int *numbers = malloc(n * sizeof *numbers);
    if (numbers == NULL) {
        printf("Memory allocation failed.\n");
        return 1;
    }

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

    free(numbers);
    return 0;
}

Why sizeof Should Be Used Instead of Hardcoded Byte Counts

Both of the following requests are valid ways to size an array allocation:

sizeof *p asks for the size of whatever type p points to, rather than naming the type by hand. Because of that, this form automatically keeps working if p's declared type ever changes — there is nothing to remember to update on the malloc() line itself.

That second form is why it is often the preferred style — for example, changing the pointer's type is all it takes to correctly resize the allocation:

Avoid hardcoding a byte count such as malloc(n * 4);. That silently assumes int is exactly 4 bytes on every platform the code might ever run on, which C does not guarantee. malloc(10) is an especially sharp version of this mistake: it allocates 10 bytes, not 10 integers — enough space for only 2 or 3 ints depending on the platform, not the 10 the code probably intended. Always express the size in terms of sizeof(int) or, better, sizeof *p.

int *p = malloc(n * sizeof(int));
int *p = malloc(n * sizeof *p);
double *p = malloc(n * sizeof *p);

malloc() Does Not Initialize Memory

A block returned by malloc() is not zeroed, cleared, or set to any particular value — its bytes hold whatever was already there, which is effectively unpredictable.

Reading any of those elements before writing to them first is undefined behavior. The fix is simply to initialize every element before it is ever read:

int *p = malloc(5 * sizeof *p);
/* p[0] through p[4] currently hold indeterminate values */
for (int i = 0; i < 5; i++) {
    p[i] = 0;
}

printf("%d\n", p[0]); /* now well-defined, because p[0] was written first */

calloc() and Its Zero-Initialization Guarantee

calloc() allocates memory for an array of elements and, unlike malloc(), guarantees that every byte of the block is initialized to zero.

calloc() takes two arguments instead of one: how many elements, and the size of each element. It is common — and safer than doing the multiplication by hand — to let calloc() combine the two rather than pre-multiplying yourself.

For ordinary integer and floating-point objects, all-zero bytes and the value 0 happen to coincide, so in practice calloc()'d numeric arrays read back as 0. The precise rule the standard actually makes, though, is only about the bytes being zero — do not generalize that every type's zero-bit representation is automatically the same thing as its value 0 in every possible case.

void *calloc(size_t count, size_t size);
int *numbers = calloc(5, sizeof *numbers);
+----+----+----+----+----+
|  0 |  0 |  0 |  0 |  0 |
+----+----+----+----+----+

malloc() vs calloc(): A Comparison

Feature             malloc()               calloc()
-----------------------------------------------------------------
Arguments           1                      2
Purpose             Allocate bytes         Allocate array storage
Initial contents    Indeterminate          All bits zero
Typical use         General allocation     Zero-initialized storage
Header              <stdlib.h>             <stdlib.h>
int *a = malloc(10 * sizeof *a);
int *b = calloc(10, sizeof *b);

/* a's ten elements are indeterminate; b's ten elements are all 0 */

free() and Releasing Memory

Memory obtained through dynamic allocation should eventually be released with free() once the program has no further use for it.

After free(p) runs, the dynamically allocated object it pointed to no longer exists. The pointer variable p itself is untouched and still exists for as long as it is in scope — but its value is no longer a valid pointer to any live object, which is exactly what makes using it afterward dangerous (see Dangling Pointers below).

Before free:
p
|
v
+---------------------+
| allocated memory    |
+---------------------+

After free:
p -----------> invalid object

Setting Pointers to NULL After Freeing

A widely used defensive habit is to immediately set a pointer to NULL right after freeing what it pointed to:

This does not undo the free() — the object is still gone — but it does make accidental reuse of the stale pointer easy to detect, because NULL is a value you can explicitly check for:

This is particularly worth doing whenever the pointer remains in scope and could plausibly be read or freed again later in the same function.

free(p);
p = NULL;
free(p);
p = NULL;

if (p == NULL) {
    printf("No allocated object.\n");
}

realloc() Syntax and Basic Usage

realloc() changes the size of an existing dynamically allocated block, either growing or shrinking it, while trying to preserve its contents.

ptr must be either NULL or a pointer previously returned by malloc(), calloc(), or realloc() (and not yet freed). size is the new total size, in bytes, the block should have.

Notice that the result of realloc() is captured in a separate temporary pointer, temp, rather than being written directly over numbers. The next section explains exactly why that separation matters.

void *realloc(void *ptr, size_t size);
int *numbers = malloc(5 * sizeof *numbers);

int *temp = realloc(numbers, 10 * sizeof *numbers);
if (temp != NULL) {
    numbers = temp;
}

Why You Must Use a Temporary Pointer with realloc()

It is tempting to write the result of realloc() straight back onto the pointer being resized:

The problem is what happens if realloc() fails. On failure it returns NULL, and — critically — the original allocation is left completely untouched and still valid. If the code above overwrote numbers with that NULL result, the only pointer to the still-valid original allocation is gone.

This is exactly the classic "losing the original pointer with realloc()" mistake: writing p = realloc(p, new_size); looks harmless right up until realloc() fails. At that point p becomes NULL, and the address of the still-allocated original block is gone forever — memory that can never be freed for the rest of the program's run. Always assign the result to a separate temporary pointer first, and only update the original pointer once you have confirmed the temporary pointer is not NULL.

numbers = realloc(numbers, 10 * sizeof *numbers); /* risky */
Original allocation
       ^
       |
    numbers

realloc fails
       |
       v
    numbers = NULL

Original allocation still exists,
but its pointer has been lost.
int *temp = realloc(numbers, 10 * sizeof *numbers);
if (temp != NULL) {
    numbers = temp;
}
/* if temp was NULL, numbers still safely refers to the original allocation */

What realloc() Does Internally: In-Place vs Move-and-Copy

There are two broad ways realloc() can satisfy a resize request. Which one happens is entirely up to the implementation — the caller cannot choose or predict it.

If there happens to be enough free space directly after the existing block, the implementation can simply extend it in place and hand back the same address. Otherwise, it allocates a new, larger block elsewhere, copies the existing contents across, and releases the old block automatically as part of that successful resize.

Because the old allocation is released automatically whenever realloc() succeeds and returns a different address, you must stop using whatever pointer value you had before the call. Only the pointer realloc() actually returned (captured in your temporary pointer) refers to a live object afterward.

Case 1: resized in place

Before:
numbers
   |
   v
+--------------------+
| allocated object   |
+--------------------+

After:
numbers
   |
   v
+--------------------------------+
|      larger allocation         |
+--------------------------------+


Case 2: a new location is required

Old allocation
+----------------+
| existing data  |
+----------------+
       |
       | copy/move as required
       v
New allocation
+------------------------+
| existing data + space  |
+------------------------+

What Happens to Existing Data During realloc()

When realloc() succeeds, the data already in the block is preserved up to the lesser of the old size and the new size. If the new block is larger, the extra bytes beyond the old size are indeterminate, exactly like freshly malloc'd memory. If the new size is smaller, whatever data lived beyond the new size is simply gone.

The first three elements survived the resize untouched. The two new elements at the end are uninitialized and must be written to before they are read, for the same reason freshly malloc'd memory must be.

int *p = malloc(3 * sizeof *p);
p[0] = 10;
p[1] = 20;
p[2] = 30;

int *temp = realloc(p, 5 * sizeof *p);
if (temp != NULL) {
    p = temp;
}
+----+----+----+----+----+
| 10 | 20 | 30 | ?  | ?  |
+----+----+----+----+----+

realloc(NULL, size) Behaves Like malloc()

A useful special case worth knowing: calling realloc() with a NULL pointer behaves exactly like calling malloc() with that same size.

This is genuinely useful when writing generic, growable-buffer logic: a single realloc() call can handle both "there is nothing allocated yet" (ptr is NULL) and "grow the existing block" (ptr is a live allocation), without needing a separate branch for the very first allocation.

realloc(NULL, size);   /* behaves like malloc(size); */

realloc(ptr, 0): A Portability Caveat

Calling realloc() with a size of 0 is a case where relying on one specific, exact behavior is not portable advice: the precise behavior and its practical consequences have differed across C standard versions and across implementations.

For portable code, do not build application logic around any single assumption of what realloc(ptr, 0) will do. Instead, handle the zero-size case explicitly, according to what your program actually needs:

if (new_size == 0) {
    /* handle the zero-element case explicitly rather than
       relying on exactly what realloc(ptr, 0) happens to do */
}

realloc() Can Leave Other Aliasing Pointers Dangling

Suppose a second pointer has been made to alias the same allocation as the one being resized:

If realloc() moves the allocation to a new address, only p gets updated by the code above. q still holds the old address — realloc() has no way to know q even exists, so it certainly cannot update it for you.

q has become a dangling pointer the moment the move happened, even though it looks completely unchanged. This is one concrete reason dynamically allocated objects need carefully managed ownership — see the next section — rather than being casually aliased by several pointers with no clear plan for keeping them in sync.

int *p = malloc(5 * sizeof *p);
int *q = p;

int *temp = realloc(p, 10 * sizeof *p);
if (temp != NULL) {
    p = temp;
}
q ----> (old address; possibly freed and reused by now)

Integer Overflow During Allocation Size Calculations

This is a real-world security and correctness issue, not just a theoretical one. Consider a size computed from a variable count:

The multiplication n * sizeof *p can overflow if n is large enough, wrapping around to a small number. malloc() then happily succeeds — but allocates a far smaller block than the code assumes, and every later write past that (much smaller) real size becomes a buffer overflow.

A safer pattern is to check the multiplication for overflow before performing it, comparing against SIZE_MAX (the largest value a size_t can hold):

sizeof *p only needs p's type, not its value, so this check is valid even though p has not been given a value yet. Dividing SIZE_MAX by the element size first, rather than multiplying n by it, guarantees the comparison itself can never overflow.

This matters especially whenever an allocation's size is derived, directly or indirectly, from something outside the program's own control — user input, a file, or a network message.

size_t n = /* some value, possibly from external input */;
int *p = malloc(n * sizeof *p);
#include <stdint.h> /* for SIZE_MAX, on implementations that declare it there */

int *p;
size_t n = /* some value, possibly from external input */;

if (n > SIZE_MAX / sizeof *p) {
    /* the multiplication would overflow -- refuse the allocation */
    return 1;
}

p = malloc(n * sizeof *p);

Allocating a Single Structure Dynamically

Dynamic allocation is not limited to arrays of scalars — a single structure can just as easily be allocated on the heap. Given:

a single Student can be allocated, filled in through the pointer, and eventually released the same way as any other dynamic allocation:

sizeof *student correctly asks for the size of a whole struct Student, and -> is used instead of . because student is a pointer to the structure rather than the structure itself.

struct Student {
    int id;
    double marks;
};
struct Student *student = malloc(sizeof *student);
if (student != NULL) {
    student->id = 101;
    student->marks = 85.5;

    /* ... use student ... */

    free(student);
}

Dynamically Allocating an Array of Structures

The same principles extend directly to an array of structures — allocate enough space for every element, then index it exactly like a normal array:

struct Student *students = malloc(100 * sizeof *students);
if (students != NULL) {
    students[0].id = 101;
    students[1].id = 102;
}
students
    |
    v
+----------+----------+----------+
| Student  | Student  | Student  |
|    0     |    1     |    2     |
+----------+----------+----------+

Dynamic Memory and Ownership

Every dynamically allocated object needs exactly one clear answer to the question "who is responsible for calling free() on this?" — that responsibility is usually called ownership.

Nearly every category of dynamic-memory bug covered in the rest of this page — leaks, dangling pointers, double frees — ultimately comes down to uncertainty about ownership: two parts of a program each assuming the other one will call free(), or one part calling it while another part is still using the pointer.

Allocation
    |
    v
Ownership
    |
    v
   Use
    |
    v
Release
int *data = malloc(10 * sizeof *data);
/* data now owns/references the allocation */

if (data != NULL) {
    /* use data */
}

free(data);
data = NULL;

Memory Leaks

A memory leak happens when dynamically allocated memory becomes unreachable — no pointer anywhere in the program refers to it any longer — before it has been freed. Once that happens, the memory can never be released; it is simply wasted for the rest of the program's run.

It is easy to allocate and simply forget to free — especially along an error path or an early return that skips the usual cleanup code. Every successful malloc(), calloc(), or realloc() call should eventually be matched by exactly one free() call for that same allocation.

The fix here is to release the first allocation before overwriting its only pointer, or otherwise retain and manage it some other way:

int *p = malloc(100 * sizeof *p);
p = malloc(200 * sizeof *p); /* the first allocation is now unreachable */
First allocation
+----------------+
| 100 elements   |
+----------------+
      ^
      |
      X  pointer lost

p ---------> Second allocation
free(p);
p = malloc(200 * sizeof *p);

Dangling Pointers

A dangling pointer is a pointer whose value still looks like a valid address, but which refers to an object whose lifetime has already ended.

free(p) ends the lifetime of the object at 0x9000, but p itself is left completely untouched — it still holds that same address. Dereferencing p now reads memory that no longer belongs to the program.

The defensive fix pairs neatly with the practice from earlier in this page — set the pointer to NULL immediately after freeing, then check before dereferencing:

int *p = malloc(sizeof *p);
*p = 42;

free(p);

printf("%d\n", *p);   /* WRONG -- p is now a dangling pointer */
free(p);
p = NULL;

if (p != NULL) {
    printf("%d\n", *p);
}

Double Free

Calling free() twice on the same pointer, without anything valid happening to it in between, is also invalid and is undefined behavior:

This is exactly the classic "double free" mistake — the code looks harmless at a glance, but freeing the same live allocation twice corrupts the heap's own bookkeeping and is undefined behavior, even though nothing about the second free(p); call looks different from the first.

The same defensive pattern used against dangling pointers also protects against this, because free(NULL) is explicitly guaranteed to do nothing:

Note the limit of this fix, though: setting one pointer to NULL does not automatically update any other alias that still points at the same freed allocation — exactly the aliasing problem described earlier for realloc().

int *p = malloc(sizeof *p);

free(p);
free(p);   /* WRONG */
free(p);
p = NULL;

/* ... later ... */
free(p);   /* valid: freeing a NULL pointer has no effect */

Use-After-Free

Use-after-free is the general name for reading or writing through a pointer to an object that has already been freed:

This is the same mistake as "using memory after free()" in the classic list of dynamic-memory bugs. It is particularly dangerous precisely because it often appears to work — the freed memory may still visibly hold the old bytes for a while — right up until it does not, sometimes far away from the line that actually caused the problem.

Depending on what else is happening in the program at the time, use-after-free can produce outright crashes, silently corrupted data, unpredictable and hard-to-reproduce behavior, or a genuine security vulnerability that an attacker can exploit.

int *p = malloc(sizeof *p);
*p = 100;

free(p);

*p = 200;   /* WRONG -- the object no longer exists */

Out-of-Bounds Access Is Still Possible

Dynamic allocation does not remove ordinary array bounds checking requirements — C still performs none of it for you. Given:

This is the same shape as the classic "buffer overflow" mistake: a loop written as for (int i = 0; i <= 5; i++) instead of i < 5 writes one element past the end of a 5-element allocation on its last iteration. Dynamic memory still requires exactly the same careful bounds management a fixed array does.

int *p = malloc(5 * sizeof *p);
/* valid indexes: 0 1 2 3 4 */
p[5] = 100;   /* WRONG -- out of bounds */
for (int i = 0; i < 5; i++) {   /* corrected: strictly less than 5 */
    p[i] = i;
}

sizeof with malloc() for Types

Both of these are valid ways to size an allocation:

The second is often preferable specifically because it is self-updating: changing p's declared type from int * to double * automatically changes what malloc(10 * sizeof *p) actually allocates, correctly, without anyone needing to remember to also update the type named inside the malloc() call itself.

int *p = malloc(10 * sizeof(int));
int *p = malloc(10 * sizeof *p);

Why You Should Not Cast malloc()'s Return Value in C

In C, it is conventional — and generally recommended — not to cast the void * that malloc() returns:

C automatically converts a void * to any other object-pointer type in an assignment, so the cast adds nothing functionally in standard C.

The cast is not just unnecessary — it can actively hide a real bug. If <stdlib.h> was accidentally left out, an old-style C compiler could assume malloc() returns int (rather than reporting an error), and a cast such as (int *) would then silently paper over that missing declaration instead of producing a diagnostic. Leaving the cast off lets the compiler tell you when something is actually wrong.

int *p = malloc(sizeof *p);          /* preferred */
int *p = (int *)malloc(sizeof *p);   /* unnecessary in C */

Zero-Size Allocation

A call such as malloc(0); is permitted by the C standard, but it does not give you a genuinely usable object — the standard leaves considerable latitude here, and what exactly comes back (a NULL pointer, or a non-NULL pointer that must not be dereferenced) is implementation-defined.

If your program has a legitimate case where zero elements are needed, it is clearer and safer to detect and handle that case explicitly, rather than relying on whatever a particular implementation happens to do with a zero-size request:

malloc(0); /* permitted, but do not treat the result as ordinary usable storage */
if (n == 0) {
    /* no allocation needed at all */
}

The Dynamic Memory Lifecycle

Pulling the previous sections together, a typical dynamically allocated object moves through the same handful of stages every time:

Every dynamically allocated object you write should be traceable through exactly this shape: one allocation, any number of legitimate resizes, and exactly one eventual free().

malloc()
                |
                v
       +-----------------+
       | Allocated object|
       +-----------------+
                |
                v
             Use it
                |
                v
          realloc()?
          /         \
        yes          no
         |            |
         v            |
   Resize object      |
         |            |
         +-----+------+
               v
             free()
               |
               v
       Object lifetime ends

A Complete Worked Dynamic-Array Example

This example ties together allocation, the NULL check, an overflow-safe size calculation, and a safe realloc() using a temporary pointer, all in one program.

SIZE_MAX comes from <stdint.h> on some implementations and <stdlib.h> on others, so this program includes both headers to be safe. The overflow check runs before the realloc() call itself, and both the original numbers pointer and its NULL-checked replacement temp are kept properly separate throughout.

Allocate an initial 5-int array and check for NULL before using it.

Fill and (implicitly) own the first 5 elements.

Before growing to 10 elements, verify new_capacity * sizeof *numbers cannot overflow.

realloc() into a temporary pointer, check it for NULL, and only then update numbers.

Fill the newly available elements, print everything, then free once and set numbers to NULL.

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

int main(void)
{
    size_t capacity = 5;
    int *numbers = malloc(capacity * sizeof *numbers);
    if (numbers == NULL) {
        printf("Initial allocation failed.\n");
        return 1;
    }

    for (size_t i = 0; i < capacity; i++) {
        numbers[i] = (int)(i + 1);
    }

    size_t new_capacity = 10;
    if (new_capacity > SIZE_MAX / sizeof *numbers) {
        free(numbers);
        return 1;
    }

    int *temp = realloc(numbers, new_capacity * sizeof *numbers);
    if (temp == NULL) {
        free(numbers);
        return 1;
    }
    numbers = temp;

    for (size_t i = capacity; i < new_capacity; i++) {
        numbers[i] = (int)(i + 1);
    }

    for (size_t i = 0; i < new_capacity; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");

    free(numbers);
    numbers = NULL;

    return 0;
}

Best Practices for Dynamic Memory

The individual rules covered across this page collapse into a short, memorable checklist.

Include <stdlib.h> wherever you use malloc(), calloc(), realloc(), or free().

Size every allocation with sizeof rather than a hardcoded byte count.

Check every allocation's result against NULL before using it.

Always resize through a temporary pointer, never directly onto the original.

Free every allocation exactly once, once you are certain nothing still needs it.

Set long-lived pointers to NULL immediately after freeing them.

Finally, track the allocation size separately from the pointer itself. A pointer only ever stores an address — it does not carry along the number of elements you originally asked for:

#include <stdlib.h>
malloc(n * sizeof *p);
if (p == NULL) {
    /* handle failure */
}
void *temp = realloc(p, new_size);
if (temp != NULL) {
    p = temp;
}
free(p);
p = NULL;
int *p = malloc(100 * sizeof *p);
/* p does not, and cannot, remember that 100 elements were requested */

size_t count = 100;   /* keep the size in a variable of its own */
int *q = malloc(count * sizeof *q);

Pointer Variable vs Allocated Object: The Mental Model

Given int *p = malloc(5 * sizeof *p);, it helps to keep two entirely separate objects clearly apart in your head: the pointer variable p itself, and the five-int block it points to.

p is one small box holding a single address; that address merely marks the start of five separate int objects living contiguously on the heap. free(p) ends the lifetime of all five of those int objects, but it does not destroy the pointer variable p, which keeps existing as an ordinary variable in its own right.

Keeping this split clearly in mind is what makes dangling pointers, double frees, and ownership bugs easy to reason about: the pointer and the object it refers to have entirely independent lifetimes, and only one of the two ever gets destroyed by free().

The Four Main Functions at a Glance

This is the same lifecycle from earlier in the page, redrawn around the four functions themselves rather than the object's states: allocate with malloc() or calloc(), use it, optionally resize with realloc() any number of times, and release it exactly once with free().

Function      Purpose
-------------------------------------------
malloc()      Allocate memory
calloc()      Allocate memory and zero all bytes
realloc()     Resize an existing allocation
free()        Release an allocation
malloc / calloc
                    |
                    v
               +---------+
               |   USE   |
               +----+----+
                    |
              realloc()?
                    |
                    v
               +---------+
               |   USE   |
               +----+----+
                    |
                    v
                  free()

Dynamic Memory vs Fixed Arrays

Neither approach is strictly better in every situation — a fixed array is simpler and manages itself automatically whenever its size really is known up front, while dynamic allocation trades that automatic management for the flexibility of a size decided at runtime, resizability through realloc(), and the responsibility of calling free() yourself exactly once.

What is the key difference in storage duration between a fixed-size array declared inside a function and memory obtained from malloc()?

The array has automatic storage duration and its lifetime ends the moment the function returns; malloc'd memory has allocated storage duration and persists until it is explicitly released with free(), regardless of which function happens to be executing at the time.

Why is malloc(n * sizeof *p) generally preferred over malloc(n * 4) when allocating an array of int?

sizeof *p automatically reflects the real size of whatever type p points to on the current implementation, so the code stays correct even though C does not guarantee int is exactly 4 bytes, and it keeps working automatically if p's declared type is ever changed.

Why should realloc()'s result always be assigned to a temporary pointer rather than written straight back onto the original pointer?

If realloc() fails it returns NULL while leaving the original allocation completely untouched; overwriting the original pointer with that NULL result destroys the only reference to the still-valid allocation, causing a memory leak. A temporary pointer lets you check for failure before deciding whether to update the original.

What is the difference between a memory leak and a dangling pointer?

A memory leak is allocated memory that is still valid but no longer reachable by any pointer, so it can never be freed. A dangling pointer is the opposite situation: the pointer variable itself is still reachable, but the object it used to point to has already been freed, so using it is invalid.

Why does malloc() leave memory uninitialized while calloc() guarantees zeroed memory, and why does this distinction matter in practice?

malloc() simply reserves bytes and leaves their previous contents in place, so a freshly malloc'd block holds indeterminate values until something writes to it, while calloc() explicitly zeroes every byte of the block it allocates. This matters because reading a malloc'd element before initializing it produces unpredictable results, whereas calloc()'s guarantee makes a freshly allocated array or counter safe to read immediately.

Write a C program that reads an integer n from the user, dynamically allocates an array of n integers with malloc, checks for allocation failure, fills the array with values, prints it, and then frees it.

The program reads n at runtime, so the array size cannot be known at compile time -- exactly the situation dynamic allocation is for. malloc() requests n ints sized with sizeof *numbers rather than a hardcoded byte count, the result is checked against NULL before anything is written through it, and the single allocation is released with one matching free() call at the end.

Write a C program that allocates an array of n integers using calloc and prints the array to demonstrate that every element starts out as zero.

calloc(n, sizeof *numbers) both allocates space for n ints and guarantees every byte of that space is zeroed, so the array can be printed immediately without any explicit initialization loop -- every element reads back as 0, unlike a malloc'd array whose contents would be indeterminate at this point.

Write a C program that starts with a dynamically allocated array of 5 integers, uses realloc with a temporary pointer to grow it to 10 integers, and prints all ten elements afterward.

The original 5-element block is grown with realloc(), whose result is captured in the temporary pointer temp and checked for NULL before numbers is ever updated -- this protects the original allocation in case realloc() fails. Once the resize succeeds, the first five elements survive unchanged and the newly available five are filled in before the whole array is printed.

Write a C program that correctly frees a dynamically allocated block before reassigning its pointer to a new allocation, avoiding the memory leak that would occur from overwriting the pointer directly.

Writing p = malloc(200 * sizeof *p); directly over a p that already points at a live 100-element block would make that first block unreachable and therefore leaked. Calling free(p) on the first allocation before reassigning p avoids that leak entirely, and the second allocation is freed in turn once the program is done with it.

Write a C program that dynamically allocates a single struct Student (with id and marks members), fills its fields through the pointer, prints them, and frees the structure.

sizeof *student requests exactly enough space for one struct Student regardless of how many members it has, and the -> operator is used to reach the id and marks fields because student is a pointer to the structure rather than the structure itself. The single allocation is matched by exactly one free() call once the structure is no longer needed.

Write a C program that dynamically allocates an array of struct Student records, fills each record in a loop, prints all of them, and frees the array.

malloc(count * sizeof *students) reserves space for count whole structures in one contiguous block, and students[i] indexes into that block exactly like an ordinary declared array of structures would, using the . operator on each indexed element rather than ->. One free() call at the end releases the entire block at once.

Write a C program that frees a dynamically allocated pointer, immediately sets it to NULL, and then checks the pointer before dereferencing it again, so the program never performs a use-after-free.

Setting p to NULL immediately after free(p) turns an otherwise silent dangling pointer into something the program can actually detect: the later if (p != NULL) check reliably skips the dereference, so the object that free() already destroyed is never read through p again.

Write a C program that safely allocates an array of n integers by first checking, using SIZE_MAX, that the size calculation n * sizeof *p cannot overflow before calling malloc.

Dividing SIZE_MAX by sizeof *p and comparing n against that quotient tests whether n * sizeof *p would overflow without ever performing the multiplication itself, so the comparison can never overflow. Only once that check passes does the program call malloc(), which prevents a maliciously or accidentally huge n from silently wrapping around into a small, unsafe allocation.

Write a C program implementing a growable integer buffer that starts with a small capacity and doubles its capacity via realloc, using a temporary pointer, each time it becomes full, while appending the integers 1 through 20.

The buffer starts small and only grows when count reaches capacity, at which point capacity is doubled and realloc() is called through a temporary pointer so a failed resize would leave the original, still-valid buffer intact instead of leaking it. This doubling strategy is the same idea real dynamic-array implementations use to keep the number of expensive realloc() calls small while still supporting an unbounded number of appended elements.

Feature                      Fixed array              Dynamic allocation
---------------------------------------------------------------------------
Size known at declaration    Usually yes              Can be determined at runtime
Storage duration              Depends on where declared  Allocated
Resize                       Cannot resize object itself   realloc() can resize
Management                   Mostly automatic         Programmer-managed
Requires free()               No                       Yes
Can cause memory leaks        No                       Yes
Typical syntax                int a[10]                malloc() / calloc()