Help improve SovranCode?

Google Analytics can measure anonymous usage after you choose Allow. Essential account and progress features work either way.

SovranCode
C: Foundations for Systems Programming Dynamic Memory Allocation
This device
Course contentsDynamic Memory Allocation · 15 titles

C foundations

What C Is and How C Programs RunYour First C ProgramVariables, Types, and OperatorsInput and Output with printf and scanfConditions and Loops

Program structure and data

Functions, Headers, and ScopeArrays and StringsPointers and Memory AddressesStructures, Enumerations, and UnionsFiles and Command-Line Arguments

Memory, tooling, and practice

Dynamic Memory AllocationPreprocessor Directives and Separate CompilationErrors, Debugging, and Undefined BehaviorBuild Tools and MakefilesProject: Command-Line Inventory Manager
C: Foundations for Systems Programming15 complete lessons

C foundations

What C Is and How C Programs RunYour First C ProgramVariables, Types, and OperatorsInput and Output with printf and scanfConditions and Loops

Program structure and data

Functions, Headers, and ScopeArrays and StringsPointers and Memory AddressesStructures, Enumerations, and UnionsFiles and Command-Line Arguments

Memory, tooling, and practice

Dynamic Memory AllocationPreprocessor Directives and Separate CompilationErrors, Debugging, and Undefined BehaviorBuild Tools and MakefilesProject: Command-Line Inventory Manager
PREVIOUS LESSONFiles and Command-Line Arguments
NEXT LESSONPreprocessor Directives and Separate Compilation
Memory, tooling, and practice 240 min

Dynamic Memory Allocation

Heap allocation lets a C program create storage whose size and lifetime are decided at runtime. That flexibility is powerful precisely because the language does not track the owner, capacity, or lifetime for you. Treat allocation as a contract: acquire one object, establish its valid state, transfer or retain ownership deliberately, then release it exactly once.

What you will leave with

You will distinguish automatic, static, and dynamic storage; allocate arrays with overflow-safe byte counts; initialize with calloc; grow buffers transactionally with realloc; define ownership at API boundaries; recognize leaks, double frees, and use-after-free; and build a capacity-aware dynamic integer vector.

Storage duration explains lifetime before allocation syntax

AUTOMATICFunction-local lifetimeint count; exists for one block invocation and is released when that block ends.
STATICProgram lifetimeA file-scope object and a static local exist for the whole program run.
DYNAMICExplicit lifetimemalloc reserves storage until its owner calls free.
ALLOCATORRuntime-size storageUse it only when an honest fixed capacity is not enough for the problem.

A pointer variable is not the allocation. int *scores might live automatically on the stack, while the block it points to lives dynamically on the heap. When the pointer's scope ends, the heap block does not disappear; losing the only owning pointer leaks the allocation.

int *make_default_scores(void) {
  int *scores = malloc(4 * sizeof *scores);
  if (scores == NULL) return NULL;

  for (int index = 0; index < 4; index++) {
    scores[index] = 0;
  }
  return scores; /* Caller now owns this allocation. */
}

/* int *scores = make_default_scores(); ... free(scores); */

malloc reserves bytes; it does not create a valid value

malloc(bytes) returns suitably aligned uninitialized storage, or NULL when it cannot satisfy the request. The bytes have indeterminate values: reading an int from them before writing a valid int is not initialization.

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

int *allocate_scores(size_t count) {
  if (count == 0) return NULL; /* This API chooses no buffer for empty. */
  if (count > SIZE_MAX / sizeof(int)) return NULL;

  int *scores = malloc(count * sizeof *scores);
  if (scores == NULL) return NULL;
  return scores;
}

Prefer sizeof *scores over spelling a separate type in the byte count. If scores later changes to point to double or a structure, the calculation stays coherent. In C, do not cast malloc's return value: a missing <stdlib.h> declaration should remain a compiler diagnostic instead of being hidden by a cast.

Check overflow before multiplication

count * sizeof *scores is computed before malloc sees it. For an unchecked large count, the calculation can wrap to a smaller byte request and later writes can overrun it. Check count > SIZE_MAX / sizeof *scores first; include <stdint.h> or <stdint.h>'s SIZE_MAX provider as appropriate for your C environment.

calloc gives zeroed bytes, not a universal default constructor

calloc(count, size) reserves space for an array and initializes all its bytes to zero. It is useful for counters, flags, and byte buffers, and it can detect a count-by-size overflow. Still check for NULL, and do not assume all-bits-zero is a portable representation for every possible C object type or semantic default.

size_t count = 32;
unsigned char *seen = calloc(count, sizeof *seen);
if (seen == NULL) {
  return 1;
}

/* seen[index] is zero until this program marks it. */
seen[5] = 1;
free(seen);
seen = NULL;

Write ownership into the API contract

Every allocation needs one clear owner at a time. A function can borrow a pointer for the duration of a call, retain a pointer whose lifetime the caller must guarantee, or transfer ownership. Name this in documentation and reflect it in types and behavior.

BORROW

print_scores(const int *values, size_t count)

Reads caller-owned memory and never frees or stores the pointer.

TRANSFER OUT

int *read_scores(...)

Returns a fresh allocation. A non-NULL return gives the caller one future free duty.

TRANSFER IN

void vector_destroy(Vector *vector)

Releases storage owned by the vector and resets it to a harmless empty state.

“This function frees the pointer” and “this function might retain the pointer” are not implementation trivia; they determine whether every caller is correct. Prefer an API where a single object such as a vector owns its internal buffer rather than exposing a raw pointer plus a separate undocumented cleanup rule.

free ends the allocation's lifetime, not every copy of its address

int *scores = malloc(8 * sizeof *scores);
if (scores == NULL) return 1;

int *alias = scores;
scores[0] = 91;
free(scores);
scores = NULL;

/* alias is now dangling too. Do not read, write, or free(alias). */

Calling free(NULL) is safe and does nothing, which makes cleanup code simpler. Calling free twice on the same allocation, reading through a dangling pointer, or passing a non-allocator pointer to free is undefined behavior. Clearing the one owning field after free is helpful, but it does not repair aliases held elsewhere.

realloc must be a transaction, not a gamble

realloc(pointer, bytes) may extend the existing allocation or move its contents to a new location. On failure it returns NULL and leaves the original allocation valid. Therefore never overwrite the only owning pointer until the call succeeds.

int *grow_scores(int *scores, size_t old_count, size_t new_count) {
  if (new_count > SIZE_MAX / sizeof *scores) return NULL;

  int *grown = realloc(scores, new_count * sizeof *scores);
  if (grown == NULL && new_count != 0) {
    return NULL; /* scores still belongs to the caller. */
  }

  for (size_t index = old_count; index < new_count; index++) {
    grown[index] = 0;
  }
  return grown;
}

Choose and document an empty-buffer policy. realloc(pointer, 0) has implementation-defined behavior across C versions and libraries; a beginner-friendly vector can instead call free explicitly when capacity becomes zero. After a successful moving reallocation, every old alias is dangling even though the newly returned pointer is valid.

Worked implementation: a capacity-aware integer vector

A dynamic vector makes the lifecycle concrete. Its invariants are: 0 <= length <= capacity; items == NULL when capacity is zero; and only elements below length hold initialized values.

struct IntVector {
  int *items;
  size_t length;
  size_t capacity;
};

int vector_push(struct IntVector *vector, int value) {
  if (vector == NULL) return 0;

  if (vector->length == vector->capacity) {
    size_t next = vector->capacity == 0 ? 8 : vector->capacity * 2;
    if (next < vector->capacity || next > SIZE_MAX / sizeof *vector->items) {
      return 0;
    }
    int *grown = realloc(vector->items, next * sizeof *grown);
    if (grown == NULL) return 0;
    vector->items = grown;
    vector->capacity = next;
  }

  vector->items[vector->length] = value;
  vector->length += 1;
  return 1;
}

void vector_destroy(struct IntVector *vector) {
  if (vector == NULL) return;
  free(vector->items);
  vector->items = NULL;
  vector->length = 0;
  vector->capacity = 0;
}

vector_push changes the structure only after its allocation succeeds. That preserves the old vector when memory is exhausted. vector_destroy is idempotent for a correctly initialized vector because free(NULL) is safe and all fields return to the empty invariant.

Design failure paths before the happy path

  • 01
    Validate size and input

    Reject impossible counts, multiplication overflow, and invalid pointers before allocating.

  • 02
    Allocate into a temporary owner

    Do not modify a live object until a requested allocation succeeds.

  • 03
    Initialize the new state

    Write valid values before exposing the pointer through the public object.

  • 04
    Commit and release once

    Transfer ownership deliberately; every acquired allocation has one cleanup route.

  • struct IntVector values = {0};
    int status = 1;
    
    for (int score = 0; score < 5; score++) {
      if (!vector_push(&values, score * 10)) {
        fprintf(stderr, "out of memory\n");
        goto cleanup;
      }
    }
    status = 0;
    
    cleanup:
    vector_destroy(&values);
    return status;

    Make memory bugs observable with tools

    Warnings and tests are the first line of defense, but many memory faults depend on a particular execution path. Build with strong warnings and an address/undefined-behavior sanitizer when your compiler supports it, then exercise error and boundary cases.

    # GCC or Clang, when available:
    cc -std=c17 -Wall -Wextra -Wpedantic -g \
      -fsanitize=address,undefined vector.c -o vector
    ./vector
    
    # Run memory-oriented tests under your platform's dynamic analysis tool
    # when available, and treat a reported leak or invalid access as a failing test.
    Tools confirm behavior; they do not invent ownership

    A sanitizer can show the allocation stack for a leak or use-after-free, but it cannot decide which function should own a value. State ownership and invariants in your API first, then use tools to check whether the execution respects them.

    Independent lab: parse an unknown number of scores

    Extend the previous file lesson. Build score_list.c that reads one integer score per line from a filename, appends each valid score to an IntVector, and prints count, average, minimum, and maximum. Do not guess a maximum row count.

    01Start emptyInitialize struct IntVector scores = 0; never call realloc through an uninitialized pointer.
    02Append transactionallyMake vector_push preserve the old vector on allocation failure.
    03Clean up every pathClose the file and call vector_destroy after parse failure, I/O failure, and success.
    04Test memory pressureTemporarily make capacity growth fail in a test seam and verify that no partial state leaks through.

    Lesson review

    • Dynamic allocation is for runtime-sized or independently-lived storage; a pointer variable and the allocation it references have different lifetimes.
    • Check NULL from malloc/calloc before access and check byte-count overflow before multiplying.
    • Use sizeof *pointer to couple allocation size to the pointed-to type.
    • Every allocation has one owner; document whether APIs borrow, transfer out, or transfer in ownership.
    • Call free exactly once, never use a pointer after free, and reset owning fields to their empty invariant.
    • Assign realloc to a temporary pointer first; commit only when the new allocation succeeds.
    • Use sanitizers and dynamic analysis with tests to find leaks, invalid accesses, and lifecycle errors.
    KNOWLEDGE CHECK

    Make ownership and failure states visible

    Check the allocation contracts before moving on to separate compilation and larger C programs.

    01What does malloc return when it cannot reserve the requested storage?
    02Which expression keeps an allocation correct when the element type changes?
    03What is unsafe about assigning realloc directly to the only owning pointer?
    04After free(buffer), what is a correct defensive follow-up for an owning pointer?
    05Which operation can overflow before malloc is called?
    06What does calloc add beyond malloc?
    07Who should call free for an allocated result returned from a function?
    08Which tool class is designed to find a use-after-free or leaked heap block?
    PREVIOUS LESSONFiles and Command-Line Arguments
    NEXT LESSONPreprocessor Directives and Separate Compilation
    ON THIS PAGEDynamic Memory AllocationStorage durationmalloccallocOwnershipfree and aliasesreallocGrowable vectorFailure pathsDiagnosticsIndependent labLesson reviewKnowledge check
    Course contents