Dynamic Memory: malloc, calloc, realloc & free

C · Lesson 20 of 32 7 min read Updated

So far every array has had a size fixed when you wrote the program. Real programs often do not know the size in advance: how many students are in the file, how many orders arrive today, how long the user's name is. Dynamic memory lets a program ask for memory while it runs, use it, and give it back when finished.

In this lesson you will use malloc, calloc, realloc and free from stdlib.h, grow an array as data arrives, return allocated memory from functions, and learn the memory bugs that every C programmer must avoid.

The stack and the heap

Local variables live on the stack. Their memory is created automatically when a function starts and released when it returns. The stack is fast but small (often a few megabytes), and its memory cannot outlive the function.

Allocated memory comes from a much larger area called the heap. You request it with an allocation function and it stays yours until you release it with free, even after the function that requested it has returned. C does not clean up heap memory for you; there is no garbage collector.

FunctionWhat it doesStarting contents
malloc(size)Allocates size bytesGarbage
calloc(count, size)Allocates count elements of size bytes eachAll zero
realloc(p, new_size)Resizes an allocation, moving it if neededOld contents kept
free(p)Gives the memory backNot applicable

malloc and free

malloc returns a pointer to the new memory, or a null pointer if no memory is available. Always check the result before using it.

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

int main(void)
{
    size_t n = 4;   // in a real program this might come from input or a file
    int *marks = malloc(n * sizeof *marks);
    if (marks == nullptr) {
        fprintf(stderr, "Out of memory\n");
        return EXIT_FAILURE;
    }

    int sample[] = {67, 82, 74, 91};
    for (size_t i = 0; i < n; i++) {
        marks[i] = sample[i];
    }

    int total = 0;
    for (size_t i = 0; i < n; i++) {
        total += marks[i];
    }
    printf("Average of %zu students: %.2f\n", n, (double) total / n);

    free(marks);
    marks = nullptr;
    return 0;
}
Output
Average of 4 students: 78.50

Three habits are visible here:

  • Size with sizeof *marks. n * sizeof *marks means "n elements of whatever marks points to". If you later change the type of marks, the size stays correct.
  • Check for nullptr. Allocation can fail. fprintf(stderr, ...) prints the message to the standard error stream, which is meant for error messages.
  • Free once, then forget the pointer. After free, the pointer still holds the old address but must never be used again. Setting it to nullptr makes accidental reuse easier to catch, and free(nullptr) safely does nothing.

Once allocated, marks works exactly like an array: marks[i] is *(marks + i), as you saw in Pointer Arithmetic & Arrays.

calloc: zeroed memory

calloc takes the number of elements and the size of one element, and sets every byte to zero. It is ideal for counters. It also checks that count * size does not overflow, which a hand-written multiplication for malloc does not.

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

int main(void)
{
    size_t candidates = 3;
    int *votes = calloc(candidates, sizeof *votes);   // all start at 0
    if (votes == nullptr) {
        return EXIT_FAILURE;
    }

    int ballots[] = {1, 0, 2, 1, 1, 0, 2, 1};
    size_t count = sizeof ballots / sizeof ballots[0];
    for (size_t i = 0; i < count; i++) {
        votes[ballots[i]]++;
    }

    for (size_t c = 0; c < candidates; c++) {
        printf("Candidate %zu: %d votes\n", c + 1, votes[c]);
    }
    free(votes);
    return 0;
}
Output
Candidate 1: 2 votes
Candidate 2: 4 votes
Candidate 3: 2 votes

realloc: growing an array

When you do not know how many items will arrive, start small and grow. realloc resizes an allocation, copying the old contents if it has to move them. The usual strategy is to double the capacity whenever the array is full, so growth happens rarely.

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

int main(void)
{
    size_t capacity = 2;
    size_t count = 0;
    int *orders = malloc(capacity * sizeof *orders);
    if (orders == nullptr) {
        return EXIT_FAILURE;
    }

    for (int id = 101; id <= 110; id++) {
        if (count == capacity) {
            size_t new_capacity = capacity * 2;
            int *bigger = realloc(orders, new_capacity * sizeof *orders);
            if (bigger == nullptr) {
                free(orders);
                return EXIT_FAILURE;
            }
            orders = bigger;
            capacity = new_capacity;
            printf("Grew to %zu slots\n", capacity);
        }
        orders[count++] = id;
    }

    printf("Stored %zu orders, last one is %d\n", count, orders[count - 1]);
    free(orders);
    return 0;
}
Output
Grew to 4 slots
Grew to 8 slots
Grew to 16 slots
Stored 10 orders, last one is 110

Notice that the result of realloc goes into a temporary pointer, bigger. If realloc fails, it returns a null pointer but leaves the old block untouched. Writing orders = realloc(orders, ...) directly would overwrite your only pointer to that old block with null, and the memory would be lost.

Returning allocated memory from a function

A function cannot return a pointer to its local array, because that memory disappears when it returns. It can return heap memory, as long as the caller knows it must call free. Document this "ownership" in a comment.

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

// Returns a new string that the caller must free.
char *make_label(const char *name, int roll)
{
    int needed = snprintf(nullptr, 0, "%s (Roll %d)", name, roll);
    char *label = malloc((size_t) needed + 1);
    if (label == nullptr) {
        return nullptr;
    }
    snprintf(label, (size_t) needed + 1, "%s (Roll %d)", name, roll);
    return label;
}

int main(void)
{
    char *label = make_label("Ananya Iyer", 27);
    if (label == nullptr) {
        return EXIT_FAILURE;
    }
    printf("%s, %zu characters\n", label, strlen(label));
    free(label);
    return 0;
}
Output
Ananya Iyer (Roll 27), 21 characters

Calling snprintf with a null pointer and size 0 writes nothing but returns the length the text needs. The function then allocates exactly that much plus one byte for '\0'. For a plain copy of a string, C23 also standardised strdup(s), which returns an allocated copy that you must free.

The four classic memory bugs

C
int *p = malloc(10 * sizeof *p);
// 1. Memory leak: the function returns without free(p)
// 2. Heap overflow: p[10] = 7; writes past the end
free(p);
// 3. Use after free: p[0] = 5; uses memory you gave back
// 4. Double free: free(p); releases the same block twice
  • A memory leak wastes memory. In a long-running program such as a server, leaks slowly use up all the memory.
  • A heap overflow corrupts the allocator's own records or other data, often causing a crash much later.
  • Use after free and double free are undefined behaviour and common sources of security holes.

None of these is caught by the compiler. The tools in Debugging with gdb & Sanitizers, especially AddressSanitizer, find them while you test. A simple rule prevents most of them: every malloc has exactly one matching free, and you decide clearly which part of the program owns each block.

Common mistakes

  • Not checking for a null result. Writing through a null pointer crashes the program.
  • Allocating the wrong size. malloc(n) gives n bytes, not n integers. Write malloc(n * sizeof *p).
  • Forgetting the + 1 for strings. A string of length len needs len + 1 bytes.
  • p = realloc(p, size). On failure this loses the old block. Use a temporary pointer.
  • Calling realloc with size 0. In C23 this is undefined behaviour. Call free instead.
  • Freeing memory that was not allocated. Only pass free pointers from malloc, calloc, realloc or strdup.

Key points

  • Heap memory from malloc, calloc or realloc lasts until you free it.
  • Always check the returned pointer for nullptr before using it.
  • Size allocations as count * sizeof *p; calloc also zeroes the memory and checks for overflow.
  • Grow arrays with realloc into a temporary pointer, doubling the capacity when full.
  • After free(p), never use p again; setting it to nullptr helps.
  • Leaks, heap overflows, use-after-free and double free are the classic bugs; test with sanitizers.

Exercise

Given the marks 35, 72, 18, 90, 56, 41 in an ordinary array, write a program that first counts how many are 40 or more, then uses malloc to allocate an array of exactly that size, copies the passing marks into it, prints them, and frees the memory.

Show solution
C
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int marks[] = {35, 72, 18, 90, 56, 41};
    size_t n = sizeof marks / sizeof marks[0];

    size_t passed = 0;
    for (size_t i = 0; i < n; i++) {
        if (marks[i] >= 40) {
            passed++;
        }
    }

    int *pass_list = malloc(passed * sizeof *pass_list);
    if (pass_list == nullptr) {
        return EXIT_FAILURE;
    }

    size_t k = 0;
    for (size_t i = 0; i < n; i++) {
        if (marks[i] >= 40) {
            pass_list[k++] = marks[i];
        }
    }

    printf("%zu passed:", passed);
    for (size_t i = 0; i < passed; i++) {
        printf(" %d", pass_list[i]);
    }
    printf("\n");

    free(pass_list);
    return 0;
}
Output
4 passed: 72 90 56 41

Counting first lets the program allocate exactly the right amount of memory. Every path that allocates also frees, so there is no leak.

Quick Quiz

1. What does malloc return when it cannot allocate memory?

2. Which call allocates space for 50 double values, all set to zero?

3. Why should you write tmp = realloc(p, size) instead of p = realloc(p, size)?

4. What is a memory leak?

Learn C with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call