[C Series 13] Using Dynamic Memory Allocation Only When You Need It

한국어 버전

After practicing stdio.h and file I/O in lesson 12, it is time to “borrow just enough memory” during execution. Up to now we fixed array sizes at compile time, but user input length or file size is unknown ahead of time. malloc and free let programs request space on demand and release it later.

New Terms in This Post

  1. Heap: The memory region where a program can request arbitrary amounts while it runs
  2. Dynamic memory allocation: The process of reserving space at runtime with functions such as malloc
  3. size_t: The unsigned integer type used for expressing sizes and counts
  4. Memory leak: Memory that was allocated but never freed and can no longer be reached

Key Ideas

Study Notes

  • Time required: 60–90 minutes
  • Prerequisites: arrays, basic pointers, stdio.h experience
  • Goal: use the malloc family to reserve space and terminate its lifetime explicitly with free

Static arrays are hard to resize, so they struggle with unknown or growing inputs. You often cannot predict how many characters a user will type or how many scores live in a file. Dynamic memory solves that “size decided at runtime” problem. Each allocation should follow four steps:

  1. Request the needed number of bytes with malloc/calloc/realloc.
  2. Check the return value for NULL to confirm success.
  3. Use the pointer like an array or hand it to other functions.
  4. Free the space when done, ideally setting the pointer to NULL afterward.

In this post we will:

  • Compare static arrays with heap allocations
  • Explore similarities and differences among malloc, calloc, and realloc
  • Explain why free is mandatory and how to detect leaks
  • Practice NULL checks
  • Grow a buffer on demand based on input length

Code Walkthrough

Comparing Stack Arrays and Heap Memory

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

int main(void) {
    int fixed[3] = {1, 2, 3};
    int *dynamic = malloc(sizeof(int) * 3);

    if (dynamic == NULL) {
        return 1;
    }

    for (int i = 0; i < 3; i++) {
        dynamic[i] = (i + 1) * 10;
    }

    printf("fixed[1] = %d\n", fixed[1]);
    printf("dynamic[1] = %d\n", dynamic[1]);

    free(dynamic);
    return 0;
}

fixed lives in stack memory with a compile-time length. dynamic borrows space from the heap during execution, and free(dynamic); releases it explicitly.

Requesting Space with malloc

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

int main(void) {
    size_t count = 5;
    int *scores = malloc(sizeof(int) * count);

    if (scores == NULL) {
        printf("Failed to allocate memory.\n");
        return 1;
    }

    for (size_t i = 0; i < count; i++) {
        scores[i] = (int)(i * 10);
    }

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

    free(scores);
    scores = NULL;
    return 0;
}
  • Compute bytes as “number of elements × size of one element,” such as sizeof(int) * count.
  • size_t is the standard type for sizes: unsigned and naturally sized per platform.
  • After free, assign NULL so accidental reuse stands out.

Getting Zero-Initialized Space with calloc

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

int main(void) {
    size_t count = 4;
    int *buffer = calloc(count, sizeof(int));

    if (buffer == NULL) {
        return 1;
    }

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

    free(buffer);
    return 0;
}

calloc takes the element count and element size separately and fills the block with zeros. For beginners, the easiest way to think about it is this: malloc gives you uninitialized memory, while calloc gives you zeroed memory. If you want an integer array to start at 0, calloc is often the simpler choice.

Growing a Buffer with realloc

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

int main(void) {
    size_t capacity = 4;
    size_t length = 0;
    char *line = malloc(capacity);

    if (line == NULL) {
        return 1;
    }

    const char *source = "C-memory";

    for (size_t i = 0; source[i] != '\0'; i++) {
        if (length + 1 >= capacity) {
            size_t new_capacity = capacity * 2;
            char *temp = realloc(line, new_capacity);
            if (temp == NULL) {
                free(line);
                return 1;
            }
            line = temp;
            capacity = new_capacity;
        }

        line[length++] = source[i];
    }

    line[length] = '\0';
    printf("line = %s (capacity = %zu)\n", line, capacity);

    free(line);
    return 0;
}
  • realloc resizes an existing block.
  • Capture the return value in a temporary pointer so you don’t lose the original on failure.
  • If it succeeds, use only the new pointer; the old address may no longer be valid.
  • The reallocated memory might move, so never keep old pointers around.

Always Think About Failure

Heap space is finite. When malloc fails it returns NULL, so treat this pattern as mandatory:

int *data = malloc(sizeof(int) * n);
if (data == NULL) {
    // Log, notify the user, and exit safely
}

free is safe to call with NULL, so if you are not sure whether a pointer was freed, checking for NULL first is fine.

Building Habits for Detecting Leaks

Create a quick review checklist to avoid leaks:

  1. Decide whether the function that calls malloc also calls free, or whether it returns the pointer and hands off ownership.
  2. If the function has multiple return statements, ensure each path frees what it owns.
  3. When a struct holds dynamic arrays, include a corresponding cleanup function that frees the inner pointers.

Why It Matters

  • Dynamic memory is essential for inputs whose size is determined at runtime: user text, file contents, network data, and more.
  • Understanding malloc/free sets the stage for pointer arithmetic, structs, and custom containers.
  • Tracking leaks trains you to manage other resources (files, sockets) just as carefully.

Practice in CodeSandbox

The sandbox below uses CodeSandbox's Universal starter. For C, the key learning loop is still compile and run in the terminal, so recreate the lesson code as a source file and repeat that cycle directly.

Live Practice

C Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Universal starterCterminal
  1. Fork the starter and create a practice file such as hello.c
  2. Paste in the lesson code and compile it if clang or gcc is available
  3. Edit the code, rebuild it, and compare the new output

For C, the terminal build loop matters more than a browser preview. Compiler availability can vary by environment, so first confirm that clang or gcc is present in the Universal starter.

Practice

  • Try it: Run each malloc/calloc/realloc example and print capacity and length to see how they change.
  • Extend it: In the realloc example, let the user provide source and exit immediately if the input is empty.
  • Debug it: Comment out free calls and inspect the warnings from valgrind or AddressSanitizer (if installed).
  • Done when: You can describe when to allocate and when to free, and you can write a NULL check pattern from memory.

Wrap-up

Dynamic memory is space you borrow temporarily. With the malloc family and careful free habits under your belt, the next lesson will explore what memory bugs look like in practice and which tools help you track them down.

💬 댓글

이 글에 대한 의견을 남겨주세요