Programming in C

Chapter 8
Dynamic Memory and Resource Management

Bachelors Program in Electronics and Communication
Programming in C   •   Post Graduate

Dynamic Memory and Resource Management

Introduction

A C program can use memory whose size and lifetime are known at compile time, or it can acquire and release memory during execution. Dynamic memory makes it possible to construct variable-sized data structures, process input of unknown length, implement abstract data types, and manage buffers whose requirements change at run time. The same flexibility introduces responsibilities that are largely automatic in managed languages: the programmer must establish ownership, preserve object lifetime, avoid invalid accesses, and release resources exactly when they are no longer needed.

Dynamic memory management is therefore not merely a collection of library calls. It is a discipline involving storage duration, pointer validity, allocation policies, failure handling, interface design, security, and testing. A program may compile successfully and still contain a memory leak, double deallocation, use-after-free, buffer overflow, or undefined behavior caused by an invalid pointer computation.

This chapter develops a rigorous model of dynamic storage in C, examines allocation functions and their contracts, presents robust implementation patterns, and applies them to a practical variable-length data-processing system.

Learning Objectives

After studying this chapter, the learner should be able to:

  1. distinguish C storage durations and explain the lifetime of dynamically allocated objects;
  2. use malloc, calloc, realloc, and free correctly;
  3. design ownership and cleanup protocols for functions and data structures;
  4. analyze common memory-management failures and their security consequences;
  5. implement exception-like cleanup paths in ordinary C code;
  6. select appropriate allocation strategies for performance, safety, and maintainability;
  7. use diagnostic tools and review methods to validate resource management.

1. Storage, Lifetime, and Ownership

1.1 Storage durations

The C standard recognizes four storage durations:

Storage durationTypical declarationLifetimeTypical use
Staticglobal object, static objectEntire program executionConfiguration and persistent state
Automaticlocal non-static variableEntry to exit of a blockTemporary computation
Threadthread-local objectThread lifetimePer-thread state
Allocatedobject returned by an allocation functionFrom successful allocation until deallocationVariable-sized and dynamically structured data

An allocated object has no declared identifier of its own. A pointer stores an address or, more precisely, a value used to designate an object. The pointer itself may have automatic or static storage duration, while the object to which it points has allocated storage duration. These are separate concepts.

For example:

int *p = malloc(sizeof *p);
if (p != NULL) {
    *p = 42;
}

The local pointer p is automatic. The integer object is allocated. When the block ends, p disappears, but the allocated integer remains until free(p) is executed. If the pointer is lost first, the object becomes unreachable: this is a memory leak.

1.2 Ownership as an engineering contract

Ownership answers four questions:

  • Who is responsible for releasing an object?
  • When does that responsibility begin?
  • May another component retain or modify the object?
  • What happens when an operation fails?

C has no built-in ownership checker. Ownership must therefore be expressed through documentation, naming, interfaces, and convention. A useful convention is:

  • a function named create, alloc, or duplicate returns ownership to its caller;
  • a function named view, get, or data returns a non-owning pointer unless documented otherwise;
  • a function named destroy or free consumes ownership;
  • borrowed pointers must not outlive the object that owns them.

Ownership is different from mere access. A function can borrow a pointer temporarily without being allowed to free it. Confusing borrowed and owned pointers is a major source of double-free and use-after-free defects.

1.3 Pointer validity and lifetime

A pointer is usable only while it designates a live object and while the access satisfies the object’s type and bounds requirements. After free(p), the value of p is indeterminate for purposes of dereferencing; using it as though it still designates the former object is a use-after-free. Assigning NULL after deallocation can reduce accidental reuse:

free(p);
p = NULL;

This does not repair copies of the pointer held elsewhere. Lifetime design must therefore account for aliases, not only the original owner.

2. The Allocation Interface

2.1 malloc

malloc(n) requests n bytes of uninitialized storage. If successful, it returns a pointer suitably aligned for any object type; if it fails, it returns NULL. The contents are indeterminate, so they must be initialized before being read.

size_t count = 100;
int *a = malloc(count * sizeof *a);
if (a == NULL) {
    /* handle allocation failure */
}

The expression sizeof *a is preferable to sizeof(int) because it remains correct if the pointer type changes. In production code, the multiplication must also be checked for overflow before it is passed to malloc.

2.2 calloc

calloc(count, size) allocates space for an array of count elements, each of size bytes, and initializes all bits to zero. It is useful for arrays requiring zero-initialized storage, but “all bits zero” should not be confused with the abstract value zero for every possible C type. In ordinary integer and floating-point implementations it has the expected effect, but portable code should still distinguish representation guarantees from value initialization requirements.

The two arguments also allow an implementation to detect multiplication overflow in some circumstances, but portable application code should not depend on undocumented behavior. Validate the requested product explicitly when the values originate from external input.

2.3 realloc

realloc(p, new_size) changes the size of an allocated object. It may extend the existing block in place or allocate a new block, copy the preserved portion, and release the old block. The original pointer must not be used after a successful relocation.

The safe idiom is:

void *tmp = realloc(p, new_size);
if (tmp != NULL) {
    p = tmp;
} else {
    /* p is still valid and unchanged */
}

Assigning directly to p can leak the original block if reallocation fails. The standard preserves the old object when a nonzero-size reallocation fails. A request of size zero has implementation-defined or deallocation-related behavior depending on the C version and implementation; portable code should avoid using zero as a resizing request and handle zero-sized logical collections separately.

2.4 free

free(p) releases an object previously returned by an allocation function, or accepts NULL without effect. The pointer must refer to the beginning of an allocated object and must not already have been freed. It is invalid to free a pointer to a stack object, a global object, the middle of an allocated block, or memory returned by a different allocation mechanism.

A useful invariant is:

Every successful allocation has exactly one eventual release, and every release applies to exactly one currently live allocation.

3. Correct Sizing and Overflow Safety

Dynamic allocation frequently converts an element count into a byte count. For an array of n elements of type T, the required size is:

B = n × sizeof(T)

Here, B is the number of bytes requested, n is a nonnegative element count, and sizeof(T) is the implementation-defined size of type T in bytes. The relation applies when the object is represented as a contiguous array and the product is representable in sizet.

If n × sizeof(T) exceeds the maximum representable sizet, the arithmetic wraps modulo the unsigned range. The allocator may then receive a smaller size than intended, while subsequent writes assume the larger logical size. This is a classic heap-buffer-overflow vulnerability.

A reusable check is:

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

bool size_mul_overflow(size_t n, size_t size, size_t *result) {
    if (size != 0 && n > SIZE_MAX / size) {
        return true;
    }
    *result = n * size;
    return false;
}

The condition is mathematically equivalent to testing whether n > floor(SIZEMAX / size). It assumes unsigned sizet arithmetic and a valid non-NULL result pointer. Similar checks are required when adding header space, alignment padding, or multiple dimensions.

For a two-dimensional logical array with dimensions r and c and element size s, the required contiguous storage is:

B = r × c × s

The multiplication should be checked step by step, not calculated in an already-overflowed expression.

4. Dynamic Arrays and Growth Policies

A dynamic array normally stores a pointer, a logical length, and a capacity:

typedef struct {
    int *data;
    size_t length;
    size_t capacity;
} IntVector;

The invariant is 0 ≤ length ≤ capacity. Elements in the range [0, length) are initialized and logically present; positions from length to capacity are reserved but not necessarily initialized as meaningful elements.

When the array is full, the implementation chooses a larger capacity. A geometric policy commonly uses:

Ck+1 = ⌈g × Ck⌉

where Ck is the current capacity, g is a growth factor greater than 1, and the ceiling ensures an integer capacity. A factor near 2 gives simple behavior but may use more peak memory; a factor near 1.5 reduces unused space but causes more reallocations. The choice is an engineering trade-off, not a universal constant.

If capacities grow geometrically and reallocation copies the existing elements, the total number of copied elements over many append operations is proportional to the final size rather than proportional to the square of that size. This is the basis for the usual amortized constant-time append claim, although individual appends can still be expensive and allocation latency can be nondeterministic.

A robust reserve operation checks both growth arithmetic and byte multiplication:

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

bool vector_reserve(IntVector *v, size_t needed) {
    if (needed <= v->capacity) return true;

    size_t new_cap = v->capacity ? v->capacity : 8;
    while (new_cap < needed) {
        if (new_cap > SIZE_MAX / 2) {
            new_cap = needed;
            break;
        }
        new_cap *= 2;
    }

    if (new_cap > SIZE_MAX / sizeof *v->data) return false;
    int *tmp = realloc(v->data, new_cap * sizeof *v->data);
    if (tmp == NULL) return false;
    v->data = tmp;
    v->capacity = new_cap;
    return true;
}

The caller must also check overflow when computing needed, for example when appending a batch of m elements: the operation is valid only if m ≤ SIZEMAX - length.

5. Cleanup, Failure Paths, and Resource Composition

C has no automatic destructor and no exception mechanism. A function that acquires several resources must release all resources acquired before the point of failure. The canonical pattern uses a single cleanup path:

int process(void) {
    int status = -1;
    char *buffer = NULL;
    FILE *file = NULL;

    buffer = malloc(4096);
    if (buffer == NULL) goto cleanup;

    file = fopen("input.dat", "r");
    if (file == NULL) goto cleanup;

    /* work; set status = 0 on success */
    status = 0;

cleanup:
    if (file != NULL) fclose(file);
    free(buffer);
    return status;
}

The goto here is structured error handling, not an unbounded transfer of control. It makes the reverse order of cleanup visible and avoids duplicated release code. Each resource is initialized to a neutral value so cleanup is safe on every path.

Resource management extends beyond heap memory. Examples include files, sockets, mutexes, shared-memory mappings, device handles, and transaction locks. Each resource should have a clear acquisition function, release function, owner, and failure policy. A cleanup function should tolerate partially initialized objects when practical.

For composite objects, destruction proceeds from the inner resources outward:

typedef struct {
    char *name;
    int *samples;
    size_t count;
} Record;

void record_destroy(Record *r) {
    if (r == NULL) return;
    free(r->samples);
    free(r->name);
    free(r);
    /* The caller must not use r after this point. */
}

A safer interface may accept Record ** and set the caller’s pointer to NULL after destruction. This reduces stale-owner errors, although aliases remain the caller’s responsibility.

6. Common Failure Modes

Memory leak

A leak occurs when allocated storage remains live but no usable pointer can reach it. Leaks gradually increase process memory and may exhaust system resources. They commonly arise from overwriting the only pointer, returning early on an error path, or forgetting to destroy nested fields.

Double free

A double free releases the same allocation twice. It can corrupt allocator metadata and may be exploitable. Setting one owner pointer to NULL helps only if all possible ownership paths are controlled.

Use-after-free

This occurs when a pointer is dereferenced after its object’s lifetime ends. It is especially difficult to diagnose because the memory may still contain apparently valid bytes.

Buffer overflow and underflow

An access outside an allocated object violates bounds. Index validation, checked size arithmetic, and APIs carrying explicit lengths are essential. NUL-terminated strings are particularly risky because their storage requirement includes one additional byte for the terminator.

Invalid allocation size

Negative signed values converted to sizet can become very large positive values. External counts must be validated before conversion. Multiplication and addition overflow must be checked before allocation.

Uninitialized reads

malloc does not initialize storage. Reading an uninitialized value can produce an indeterminate value and, for some types, undefined behavior. Initialize objects explicitly or use a suitable initialization strategy.

Strict aliasing and effective type errors

Allocated storage has no declared type until used in a permitted manner. Accessing an object through an incompatible pointer type can violate the language rules and invalidate compiler assumptions. Byte-wise inspection through character types is specially permitted, but arbitrary type punning is not automatically safe.

7. Case Study: A Robust Sensor-Record Loader

The following is an illustrative case, not a report of a particular company.

Background

An embedded communications gateway receives records containing a timestamp and a variable number of sensor readings. The original implementation allocated a fixed 64-reading array. Exceptional packets were truncated, while a later patch multiplied a packet-supplied count by sizeof(double) without checking overflow.

Problem

The loader needed to accept variable-length records, reject malformed or excessive packets, preserve data when growth failed, and release all resources on partial failure. Because the gateway runs continuously, leaks and fragmentation were operational concerns, while malformed network input was a security concern.

Analysis

The design identified the following invariants:

  1. the reading count never exceeds a configured maximum;
  2. length never exceeds capacity;
  3. the allocation byte count is representable in size_t;
  4. the record owns its reading buffer;
  5. parsing failure leaves the caller with either a valid complete record or no record;
  6. temporary parsing buffers are released on every exit path.

Approach

The loader first validates the encoded count against the protocol maximum. It then checks the count-to-byte multiplication, reserves capacity with a temporary pointer, parses into the reserved region, and commits length only after all readings are validated. On failure, it destroys the partially constructed record. This “validate, reserve, parse, commit” sequence prevents partially trusted input from becoming visible as a complete object.

A bounded input interface should pass both a pointer and a byte length; it should not assume that the input is NUL-terminated. Allocation policy should also impose a total memory budget, not merely rely on the allocator to fail.

Outcome

With these changes, malformed counts are rejected before allocation, reallocation failure preserves the previous buffer, and all error paths converge on one destructor. Testing with sanitizers found one stale pointer in a diagnostic callback, which was corrected by passing a borrowed view only during the record’s lifetime.

Learning points

The important improvement was not a particular allocator call. It was the explicit resource contract: bounded input, checked arithmetic, single ownership, transactional construction, and deterministic cleanup. These principles transfer directly to packet parsers, file loaders, protocol stacks, and dynamic data structures.

8. Performance, Fragmentation, and Allocation Strategy

General-purpose allocators balance speed, space efficiency, alignment, and concurrency. Repeated allocation and release of differently sized blocks can fragment the heap. External fragmentation means free space exists but is divided into blocks unsuitable for a particular request; internal fragmentation is space reserved inside an allocated block but not used by the application.

For latency-sensitive systems, alternatives include:

  • object pools for many same-sized objects;
  • arenas or regions for objects sharing a lifetime;
  • stack allocation for small, bounded temporary data;
  • ring buffers for producer–consumer streams;
  • slabs or segregated free lists for predictable classes of objects.

An arena can make destruction inexpensive: releasing the arena releases all contained objects together. Its limitation is coarse lifetime control; one long-lived object can keep the entire region alive. A pool improves predictability but requires careful handling of object reuse and stale references.

Custom allocators must document alignment, thread safety, ownership, maximum size, and failure behavior. They should not obscure the standard allocator contract or make it unclear which release function matches an allocation function.

9. Concurrency and Memory Reclamation

In multithreaded programs, protecting the pointer variable is not sufficient if another thread can access the object after its owner releases it. A lock may protect both access and destruction, or a more advanced reclamation scheme may be required. Reference counting can express shared ownership, but it has overhead and cannot by itself resolve cycles. Atomic reference-count updates must use suitable memory-ordering rules, and the final release must occur exactly once.

Hazard pointers, epoch-based reclamation, and read-copy-update techniques address lock-free or read-intensive structures by delaying reclamation until no reader can hold a protected reference. These methods are advanced and should be selected only after profiling and a precise concurrency design. “Thread-safe allocator” does not mean that arbitrary object lifetime protocols are thread-safe.

10. Security, Quality, and Governance

Memory defects are both reliability and security issues. Robust development practices include:

  • validating all externally supplied lengths and counts;
  • checking integer overflow before allocation and indexing;
  • using compiler warnings and static analysis;
  • compiling with AddressSanitizer and UndefinedBehaviorSanitizer during testing;
  • using leak detectors or allocation tracing;
  • testing failure injection, including simulated allocation failure;
  • reviewing ownership at API boundaries;
  • minimizing privileged code and applying defense in depth;
  • documenting assumptions about alignment, lifetime, and thread safety.

A sanitizer is a diagnostic aid, not proof of correctness. It may not detect every leak, race, integer error, or platform-specific behavior. Formal contracts, code review, fuzzing, and boundary-value testing remain necessary.

Modern C standards and coding guidelines increasingly emphasize bounds-aware interfaces, explicit sizes, and analyzable control flow. Some environments prohibit raw dynamic allocation in interrupt handlers or safety-critical components because allocation latency and failure are difficult to bound. In such systems, static allocation or preallocated pools may be mandated by the system safety case.

Artificial intelligence can assist with allocation-site discovery, leak-pattern detection, fuzz-test generation, and code review. However, generated code must be checked against the C standard and the project’s ownership model. AI tools can reproduce unsafe idioms such as direct assignment from realloc, unchecked multiplication, or freeing borrowed memory; they are aids to review, not substitutes for it.

Best-Practice Checklist

  1. Define ownership for every allocated object.
  2. Pair each successful acquisition with one release.
  3. Check allocation results before dereferencing.
  4. Check multiplication and addition for size_t overflow.
  5. Use a temporary pointer for realloc.
  6. Track length separately from capacity.
  7. Keep cleanup paths complete and preferably centralized.
  8. Never use a pointer after its object’s lifetime ends.
  9. Pass explicit lengths for binary data and bounded text.
  10. Test allocation failure and malformed external input.
  11. Use tools, but combine them with contracts and review.
  12. Prefer the simplest allocation strategy that meets performance and lifetime requirements.

Chapter Summary

Dynamic memory provides run-time flexibility but transfers lifetime and resource responsibilities to the programmer. C distinguishes the lifetime of a pointer from the lifetime of the object it designates, and reliable programs make ownership explicit. malloc, calloc, realloc, and free each have precise contracts; misuse causes leaks, invalid access, corruption, or undefined behavior.

Correct allocation requires checked size arithmetic, especially when converting element counts into bytes. Dynamic arrays rely on length–capacity invariants and growth policies. Robust functions preserve the original allocation when realloc fails and use structured cleanup for partial construction. Resource management also covers files, sockets, locks, mappings, and other non-memory resources.

At advanced levels, allocation strategy must be evaluated against fragmentation, latency, concurrency, security, and system constraints. Pools, arenas, and region-based approaches can improve predictability but impose lifetime trade-offs. Sanitizers, static analysis, fuzzing, failure injection, and explicit ownership contracts form a complementary assurance strategy.

Key Terms

Storage duration: The period during which an object’s storage exists.

Allocated storage: Memory obtained dynamically and released explicitly.

Ownership: Responsibility for controlling an object’s lifetime and release.

Borrowed pointer: A non-owning pointer valid only under a documented lifetime.

Memory leak: Allocated storage that is no longer reachable for release.

Use-after-free: Access to an object after its lifetime has ended.

Double free: Releasing the same allocation more than once.

Buffer overflow: An access beyond the upper bound of an object.

malloc: Allocates uninitialized storage.

calloc: Allocates storage for an array and initializes its bytes to zero.

realloc: Resizes an existing allocation, possibly relocating it.

free: Releases dynamically allocated storage.

Capacity: Amount of storage reserved by a dynamic collection.

Length: Number of logically valid elements in a collection.

Fragmentation: Inefficient division or use of free and allocated memory.

Arena: An allocator that groups objects for collective release.

Object pool: A reusable collection of similarly sized objects.

Sanitizer: A run-time instrumentation tool for detecting classes of defects.

Resource invariant: A condition that must remain true throughout resource management.

Review Questions

Short Answer Questions

  1. Distinguish the storage duration of a local pointer from that of the object returned by malloc.
  2. Why is direct assignment from realloc considered unsafe?
  3. Define ownership and borrowing in a C interface.
  4. What is the difference between a dynamic array’s length and capacity?
  5. Why must allocation-size arithmetic be checked for overflow?

Long Answer Questions

  1. Explain the contracts of malloc, calloc, realloc, and free, including important failure cases.
  2. Discuss the causes, consequences, and prevention of leaks, double frees, use-after-free, and buffer overflows.
  3. Design a cleanup strategy for a function that opens a file, allocates two buffers, and acquires a mutex.
  4. Compare geometric dynamic-array growth, arenas, and object pools in terms of performance, fragmentation, and lifetime control.
  5. Explain how ownership documentation, static analysis, sanitizers, fuzzing, and failure injection contribute to assurance.

Analytical / Application Questions

  1. A parser receives an untrusted element count and element size. Develop a sequence of checks that safely determines whether allocation is possible, and justify each check.
  2. An append operation uses v->data = realloc(v->data, n sizeof v->data). Identify every potential defect and rewrite the operation safely.
  3. A multithreaded cache returns pointers to entries while another thread may evict them. Analyze why a thread-safe allocator is insufficient and propose two reclamation strategies.

Multiple Choice Questions with Answers

Question 1

Which statement correctly describes storage in int p = malloc(sizeof p);?

A. Both p and the integer object have automatic storage duration.

B. p is an automatic pointer and the integer object has allocated storage duration.

C. p has allocated storage duration and the integer object has static storage duration.

D. Both p and the integer object have static storage duration.

Correct Answer: B. p is an automatic pointer and the integer object has allocated storage duration.

Question 2

What is the primary reason for using a temporary pointer with realloc?

A. It forces reallocation to occur in place.

B. It initializes the newly allocated bytes.

C. It preserves the original pointer if reallocation fails.

D. It prevents the allocation from being moved.

Correct Answer: C. It preserves the original pointer if reallocation fails.

Question 3

Which operation is required before allocating an array of n objects of type T?

A. Ensure that n * sizeof(T) is representable as size_t.

B. Call free on every element before allocation.

C. Convert n to a signed integer.

D. Use calloc regardless of initialization requirements.

Correct Answer: A. Ensure that n * sizeof(T) is representable as size_t.

Question 4

What does malloc guarantee about the contents of successfully allocated storage?

A. All bytes are zero.

B. All objects are initialized to their language-level zero value.

C. The contents are initialized to one.

D. The contents are uninitialized.

Correct Answer: D. The contents are uninitialized.

Question 5

Which invariant is appropriate for a dynamic array with length and capacity?

A. length >= capacity

B. length == capacity + 1

C. 0 <= length <= capacity

D. capacity == 0 whenever length is nonzero

Correct Answer: C. 0 <= length <= capacity

Question 6

Which pointer may validly be passed to free?

A. A pointer to the middle of an allocated block.

B. A pointer to a local automatic variable.

C. The original pointer returned by an allocation function, if not already freed.

D. A pointer produced by adding one byte to an allocated pointer.

Correct Answer: C. The original pointer returned by an allocation function, if not already freed.

Question 7

A program needs storage for n elements of size s. Which condition detects multiplication overflow for unsigned size_t values when s is nonzero?

A. n < SIZE_MAX * s

B. n > SIZE_MAX / s

C. n == SIZE_MAX + s

D. n <= SIZE_MAX / s

Correct Answer: B. n > SIZE_MAX / s

Question 8

Why can assigning p = realloc(p, new_size) cause a leak?

A. realloc always duplicates the allocation.

B. The old block is never released on success.

C. A failed call can return NULL and overwrite the only pointer to the old block.

D. The assignment converts the pointer to an integer.

Correct Answer: C. A failed call can return NULL and overwrite the only pointer to the old block.

Question 9

Which strategy commonly improves predictability when many objects have the same size and similar lifetimes?

A. An object pool

B. Unbounded recursive allocation

C. Ignoring allocation failures

D. Repeatedly allocating random block sizes

Correct Answer: A. An object pool

Question 10

What is the main purpose of a centralized cleanup path in a C function?

A. To prevent all functions from returning values.

B. To ensure resources acquired before a failure are released consistently.

C. To make every allocation permanent.

D. To eliminate the need for ownership documentation.

Correct Answer: B. To ensure resources acquired before a failure are released consistently.

Programming in C
Chapter 8 of 8