Dynamic Memory Management in C

Learn how a C program gets memory while it runs and gives it back. Compare the stack and the heap, allocate blocks with malloc and calloc, resize them with realloc and release them with free. Recognize leaks, dangling pointers, double frees and out-of-bounds accesses, understand why they cause crashes such as segmentation faults, and find and fix them with Valgrind and AddressSanitizer.

IntermediateLesson 5 of 8 in Programming in C Course

What you will learn

  1. Describe the stack, the heap and static memory, how long data lives in each, and when a program needs heap memory.
    • Compare stack, heap and static storage
      Describe how automatic, static and allocated storage differ in lifetime and in who manages them, and what happens to each during function calls.
    • Decide when data belongs on the heap
      Choose between local variables and heap allocation from the size, lifetime and growth of the data, and avoid returning the address of a local variable.
  2. Reserve blocks of the right size with malloc and calloc, check that each request succeeded, and free every block exactly once under a clear owner.
    • Request memory with malloc and sizeof
      Compute the number of bytes to request, call malloc, check for failure and use the block as an array.
    • Get zero-filled memory with calloc
      Use calloc to allocate arrays whose elements start at zero, and know when it is the better choice than malloc.
    • Release memory with free and keep track of ownership
      Free every block exactly once at the right moment, decide which part of the program owns each block, and clean up correctly when an allocation fails.
  3. Grow and shrink heap blocks without losing data or memory, and build arrays that expand as data arrives.
    • Grow and shrink a block safely with realloc
      Call realloc through a temporary pointer, keep the data and the original block safe when it fails, and update every pointer after the block moves.
    • Build a growable array
      Keep a length and a capacity next to the pointer, grow the capacity geometrically, and read input of unknown length into a buffer that expands.
  4. Identify leaks, dangling pointers, double frees, out-of-bounds accesses and uninitialized reads in C code, explain why they are dangerous, and trace a segmentation fault to its cause.
    • Find and prevent memory leaks
      Explain what a leak is, spot the code patterns that cause leaks and apply habits that prevent them.
    • Avoid dangling pointers, use-after-free and double frees
      Recognize pointers to memory whose lifetime has ended, explain why using or freeing them again is undefined behavior, and apply the habits that prevent it.
    • Stay inside the block: overflows and uninitialized reads
      Keep every access within the bounds of its block, size string buffers correctly, and set memory before reading it.
    • Explain and trace segmentation faults
      Explain what a segmentation fault is, name its common causes, and work from the crash back to the faulty line.
  5. Run programs under Valgrind's Memcheck and AddressSanitizer, read their reports, and use them to fix leaks and invalid accesses until a program runs clean.
    • Check a program with Valgrind Memcheck
      Build a program for Valgrind, run it with leak checking, and read its error messages, heap summary and leak categories.
    • Use AddressSanitizer and LeakSanitizer
      Build a program with AddressSanitizer, read its reports, and choose between sanitizers and Valgrind.
    • Fix memory bugs from a tool report
      Follow a repeatable workflow from a tool report to the faulty line, fix the cause rather than the symptom, and confirm the fix with a clean run.

Dynamic Memory Management in C study guide

5 chapters

1

Explain where a C program keeps its data

Every object has a lifetime, and a pointer is useful only while its target exists. Local variables, static data and allocated blocks follow different lifetime rules. Connecting those rules to the stack and heap explains why a function can return an allocated array but cannot return a usable address of its own local array. Size, lifetime and later growth guide the choice of storage for each job.

1.1 Storage and lifetime

A variable's address tells you where its data is. Its storage duration tells you how long that data exists. Keeping an address does not keep the object alive.

Three lifetimes
Storage durationTypical objectsLifetime ends
AutomaticOrdinary local variables and parametersWhen their block ends
StaticGlobals and local variables declared staticWhen the program ends
AllocatedBlocks obtained from allocation functionsWhen the block is deallocated

On mainstream systems, automatic objects use the call stack, and allocated blocks use the heap. C defines the lifetimes; it does not require these physical regions. Machine code and static data occupy other regions, whose arrangement depends on the system.

A function call normally creates a stack frame containing its parameters, local variables and return information. Calls nest: the most recent frame is removed first. Returning releases that call's automatic storage, so later calls can reuse the space. The heap's allocator manages a separate pool. It reserves a contiguous block on request and supplies its starting address. Returning from a function does not release that block.

The pointer and its block are separate objects

Consider this function, declared with <stdlib.h> available:

int *make_array(size_t n) {
    if (n == 0 || n > SIZE_MAX / sizeof(int)) return NULL;
    int *p = malloc(n * sizeof *p);
    if (p == NULL) return NULL;
    return p;
}

SIZE_MAX comes from <stdint.h>. When main calls int *nums = make_array(5);, the parameter n and pointer variable p belong to the call. The allocated array does not. Where an int is 4 bytes, the block holds 20 bytes. The return copies its address into nums; it does not copy the array. The caller checks nums, uses the elements after setting them, and eventually frees the block.

The local variable p ends when the function returns. The block survives because its lifetime is allocated, not automatic. This distinction also works in reverse: a local pointer can point to a global object, and a global pointer can point to a heap block.

Static data survives calls
int next_id(void) {
    static int id = 0;
    return ++id;
}

Successive calls return 1, 2 and 3. The initializer runs once, and id lasts for the program's whole run. Removing static makes a new automatic id, initialized to zero, on each call; every call then returns 1.

Stack space is limited

A large local array and deeply nested calls both consume stack space. Exceeding the limit is a stack overflow, which usually ends the program. Many Linux setups allow an 8 MiB stack; Microsoft's default main-thread stack reserve is commonly 1 MiB. These are system settings, not C rules. A local array of 4,000,000 ints needs 16,000,000 bytes where an int is 4 bytes, about 15.3 MiB.

Heap allocation has bookkeeping costs when reserving and releasing blocks. Reading an element is not inherently slower merely because its array is on the heap. Also, the heap memory pool is unrelated to the heap data structure used in a priority queue.

Key Takeaways

  • An object’s storage duration determines its lifetime.
  • Automatic locals end with their block; static objects last for the program’s run.
  • A local pointer and the heap block it points to have separate lifetimes.
  • Large local arrays and deep calls can exhaust the stack.
  • C specifies lifetimes, while stack and heap describe common implementations.

1.2 Choosing storage for the job

Choose storage by asking how much space is needed, when that size becomes known, and how long the data must last. The simplest suitable design is often an ordinary local array.

Match storage to the data
NeedSuitable starting choice
A small fixed array used during one callLocal array
A count read from inputChecked heap allocation
A result that survives its creating functionHeap allocation or a caller-owned buffer
Data that grows as input arrivesResizable heap buffer
A very large arrayHeap allocation with a failure check

A week's daily temperatures need seven values, so double temps[7]; is a natural local array. A grade per student needs a size obtained at run time. With n students, use double *grades = malloc(n * sizeof *grades); after checking the size. For 250 students and 8-byte doubles, that requests 2,000 bytes. Then check the result before filling the array.

Why returning a local buffer fails
char *greeting(void) {
    char buf[16] = "hello";
    return buf;  /* error: buf ends when this call returns */
}

The returned address cannot extend buf's lifetime. The caller gets a dangling pointer even if the old bytes appear unchanged. GCC and Clang can warn about this pattern.

One repair is to let the caller provide storage:

#include <string.h>

void greeting(char out[16]) {
    strcpy(out, "hello");
}

/* In the caller: */
char message[16];
greeting(message);

Here the caller's array remains alive throughout the call and afterward. The parameter still behaves as a pointer; the caller must supply enough space. A second design allocates a result on the heap and makes the caller responsible for freeing it. A third returns a static buffer. That buffer lasts, but all callers share it, and a later call can overwrite its contents. Choose based on whether callers need independent results.

Run-time size does not always mean heap

C99 introduced variable-length arrays, such as int a[n];. They have automatic storage, commonly on the stack. C11 made support optional, and Microsoft's compiler does not support them. A large input can exhaust the stack, and declaring the array gives no null result to check. Heap allocation gives a checkable failure path and supports later resizing.

A pointer is not an ownership label
int value = 3;
int *p = &value;

p points to an automatic object. Passing it to free is invalid. The same rule excludes freeing a static array or a string literal. The address must belong to a live allocation that the program owns.

Likewise, char *s = "hello"; points at a string literal; it does not make a writable copy. Modifying the literal has undefined behavior. Use char s[] = "hello"; for a writable local copy, or allocate space for a separate heap copy.

Key Takeaways

  • Choose storage from the data’s size, required lifetime and need to grow.
  • An address returned from a function is usable only if its target is still alive.
  • A caller-provided buffer keeps ownership with the caller.
  • A static result buffer is shared across calls.
  • Pointers can target local, static or allocated objects; only live allocated blocks can be freed.
  • String literals are not writable copies.
2

Allocate and release heap memory

An allocation request needs a byte count, a failure path and an owner. Work from the number of elements to the required storage, decide whether their initial values should be zero, and use each block only after a successful request. Ownership then connects the final use to cleanup, including nested allocations and functions that succeed only partway through their work.

2.1 Requesting the right number of bytes

malloc reserves a contiguous block of bytes and returns its starting address. Include <stdlib.h> for its declaration. A successful allocation supplies storage, but it does not fill that storage with useful values.

Count elements, then size each element
int *nums = malloc(n * sizeof *nums);

n is an element count; sizeof *nums is the number of bytes per element. Multiplying gives the byte count. Where an int is 4 bytes, ten ints need 40 bytes. Ten 8-byte doubles need 80 bytes. Ten pointers need 80 bytes where each pointer is 8 bytes. A char is always one C byte.

The form sizeof *nums follows the pointed-to type if the declaration changes. For this ordinary non-variable-length type, sizeof does not evaluate *nums, so it does not dereference an uninitialized pointer. In C, the returned void * converts to the object pointer type without a cast.

malloc(n) allocates n bytes, not n ints. sizeof nums measures the pointer variable, not an element or the whole block. On a system with 8-byte pointers and 4-byte ints, malloc(n * sizeof nums) asks for twice the required space, masking the mistaken expression. With other types it can allocate too little.

Check size and failure before use

This complete program rejects an empty request, guards the multiplication, checks allocation and initializes every element:

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

int main(void) {
    size_t n = 5;
    int *nums;
    if (n == 0 || n > SIZE_MAX / sizeof *nums) {
        return EXIT_FAILURE;
    }
    nums = malloc(n * sizeof *nums);
    if (nums == NULL) return EXIT_FAILURE;

    for (size_t i = 0; i < n; ++i) {
        nums[i] = (int)(i * i);
    }
    free(nums);
    return EXIT_SUCCESS;
}

SIZE_MAX, from <stdint.h>, is the largest size_t value. Check with division before multiplication. If the product wraps around first, checking the smaller wrapped result cannot recover the intended size. A nonzero request can fail even when its arithmetic fits, so the null check is a separate step.

The initialized values here are 0, 1, 4, 9 and 16. nums[i] reaches the same element as *(nums + i). Pointer arithmetic advances in elements, so the code does not multiply i by the byte size.

Keep the count with the pointer

There is no standard C operation that recovers an allocation's element count from its pointer. Store n separately and pass both pointer and count to functions. The count controls loops and index checks.

A copied string needs its characters and the terminating '\0'. The string "Amsterdam" has nine characters and needs ten bytes, so request strlen(s) + 1 for its copy. <string.h> supplies strlen.

A zero-size malloc request may return null or a pointer that can be freed but cannot be dereferenced. Handle zero counts as an explicit empty case instead of treating such a pointer as usable array storage.

Key Takeaways

  • Use an element count multiplied by sizeof *pointer for an array’s byte count.
  • Check multiplication overflow before calling malloc.
  • Check for NULL before using the result.
  • Initialize each malloc element before reading it.
  • Keep the element count separately; sizeof pointer cannot recover it.
  • Include the terminating null character when allocating a string copy.
  • Handle zero counts explicitly.

2.2 Starting an array at zero

A counter needs a defined starting value before the first increment. calloc combines allocation with zeroing, so an integer counter array is ready to count after the failure check.

Two arguments describe the array

Include <stdlib.h> and write:

int *tally = calloc(26, sizeof *tally);
if (tally == NULL) return EXIT_FAILURE;

The first argument is the element count and the second is the size of one element. Where an int is 4 bytes, this reserves 104 bytes. Every byte starts at zero, so each integer counter starts at zero. Calling calloc with one combined argument does not compile; its interface requires two arguments.

For the word "banana", increment the counter corresponding to each letter. After processing it, the counters for a, b and n contain 3, 1 and 2. All others remain zero. On a character encoding with consecutive lowercase letters, the index expression is c - 'a'. The input must be a lowercase letter before that expression is used as an array index.

#include <stdlib.h>

int main(void) {
    int *counts = calloc(3, sizeof *counts);
    if (counts == NULL) return EXIT_FAILURE;
    counts[1]++;
    counts[1]++;
    /* The counters now hold 0, 2, 0. */
    free(counts);
    return EXIT_SUCCESS;
}

Only the middle counter was written by the counting code. Its two neighbors still have useful values because allocation zeroed them.

What zero-filled means

calloc sets every bit to zero. This gives zero values for integer and char elements. A positive-size zeroed character buffer therefore begins as an empty string: its first character is '\0'.

On mainstream platforms, all-zero bits also represent floating-point zero and null pointers. Portable C does not guarantee those representations for floating-point and pointer types. If code needs such values across all C implementations, assign 0.0 or NULL to the elements explicitly.

Choosing between the allocation functions
Starting useNatural choice
Increment integer counterscalloc
Set integer flags gradually from an all-zero statecalloc
Immediately overwrite every elementmalloc

A successful malloc followed by memset can zero a block, but the size multiplication in that code still needs an overflow guard. Mainstream calloc implementations reject a product too large for size_t; C23 explicitly requires this. The result still needs a null check because a correctly sized request can fail.

The zeroing applies to this allocation. If realloc later enlarges the block, the added bytes are uninitialized. Set those elements before counting into or reading them. Finally, release the block with free, just as for a block obtained with malloc.

Key Takeaways

  • Pass calloc an element count and an element size.
  • Zero-filled integer counters can be incremented immediately after the allocation check.
  • All-zero bytes are not a portable substitute for assigning floating-point zero or null pointers.
  • Growth with realloc does not zero the added bytes.

2.3 One owner and one final release

An allocated block needs a clear answer to "Who frees this?" That responsibility is ownership. Other code may borrow the address without gaining permission to release it.

Release after the last use

free, declared in <stdlib.h>, makes a live allocated block available for reuse. Pass its starting address exactly once. free(NULL) does nothing. Passing a local address, a string literal, an interior address such as p + 1, or an already freed block has undefined behavior.

free(p) does not assign to the variable p and does not promise to erase the bytes. After release, the old pointer value is invalid. If the variable remains in use, assign p = NULL; without inspecting its old value. This changes only p; aliases still need to stop using the released block.

A function returning newly allocated memory transfers ownership to its caller:

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

char *copy_string(const char *s) {
    char *copy = malloc(strlen(s) + 1);
    if (copy == NULL) return NULL;
    strcpy(copy, s);
    return copy;
}

The caller checks the result, uses the copy and frees it. The input string is borrowed; this function does not free it. POSIX and C23 offer strdup for the same ownership pattern; the function above also works in C17.

Release nested allocations from the inside out

A grid made from an array of row pointers has separate blocks. Three row pointers and three rows of four ints require four allocations. With 8-byte pointers and 4-byte ints, the outer block uses 24 bytes and the rows use 48 bytes, totaling 72 bytes.

Free each row while its address is still available, then free the pointer array. Freeing the outer block does not recursively free its rows. Accessing grid[i] after freeing grid uses released storage.

Clean up partial success

This complete example keeps track of how many rows actually exist:

#include <stdlib.h>

int main(void) {
    size_t rows = 3, made = 0;
    int **grid = malloc(rows * sizeof *grid);
    if (grid == NULL) return EXIT_FAILURE;

    for (; made < rows; ++made) {
        grid[made] = malloc(4 * sizeof *grid[made]);
        if (grid[made] == NULL) break;
    }
    int status = made == rows ? EXIT_SUCCESS : EXIT_FAILURE;
    /* Use the grid here only if every row was allocated. */
    for (size_t i = 0; i < made; ++i) free(grid[i]);
    free(grid);
    return status;
}

If the second row fails, made is 1, so only row zero is released. No uninitialized row pointer is read. Another useful design starts independent pointers at NULL and sends every failure to one cleanup section; free(NULL) makes that section safe after partial success.

Count ownership lifetimes, not allocation calls. A buffer created by malloc and resized successfully twice needs one final free applied to the latest result; successful realloc already ends the previous block's lifetime. The allocator may keep released space for later allocations, so a system monitor need not show an immediate drop in process memory.

Key Takeaways

  • The owner releases each block after its last use.
  • Only a live allocation’s starting address, or NULL, is a valid argument to free.
  • Resetting one pointer does not repair aliases.
  • Free inner blocks before the outer block holding their addresses.
  • Failure paths must release all blocks acquired earlier.
  • Successful resizing needs one eventual free of the latest block.
3

Resize buffers with realloc

Input does not always arrive with a known final size. A resizable buffer lets a program reserve more space as it learns how much it needs. Safe growth preserves both existing values and the ability to recover from failure. Keeping a pointer, a length and a capacity together makes those rules concrete, while geometric growth limits the amount of repeated copying.

3.1 Resizing without losing the old block

realloc changes an allocated block's size. It can grow or shrink the storage, and the returned address may change in either case. Include <stdlib.h> and treat success and failure as different lifetime events.

What stays and what changes

When a block grows, the existing contents survive up to the old size. Added bytes are uninitialized. When it shrinks, only the prefix fitting the new size survives. An array containing 3, 1, 4 and 1 keeps those four values when enlarged to six ints; the last two elements need assignments before use. With 4-byte ints, that changes the request from 16 to 24 bytes.

The allocator may extend the block in place, or allocate elsewhere, copy the retained bytes and release the old block. Your code must work with either result.

Receive the result in a temporary pointer
int *tmp = realloc(nums, 6 * sizeof *nums);
if (tmp == NULL) {
    /* nums still owns its original block. */
} else {
    nums = tmp;
    nums[4] = 5;
    nums[5] = 9;
}

For this positive-size request, failure leaves the original block allocated and unchanged. The program can keep using it or free it before reporting failure. Success replaces ownership with the returned block. Assign the new result before accessing the resized array.

The direct assignment nums = realloc(nums, bytes); loses this protection. If the call fails, it overwrites the only stored address with NULL. The old allocation remains reserved, but the program can no longer release it.

Saved element pointers must be rebuilt

Suppose code saves int *selected = &nums[2];. A successful resize ends the old object's lifetime, even when the returned address happens to match. selected is invalid after that success. Store the index 2 across the resize, then compute selected = &nums[2]; from the new pointer if that index still exists.

Do not compare the old pointer with the result to decide whether aliases need updating. Work from the new result on every success. If shrinking removed the selected element, there is no element pointer to rebuild.

A complete grow-and-release example
#include <stdlib.h>

int main(void) {
    int *nums = malloc(4 * sizeof *nums);
    if (nums == NULL) return EXIT_FAILURE;
    for (size_t i = 0; i < 4; ++i) nums[i] = (int)i;

    int *tmp = realloc(nums, 6 * sizeof *nums);
    if (tmp == NULL) {
        free(nums);
        return EXIT_FAILURE;
    }
    nums = tmp;
    nums[4] = 4;
    nums[5] = 5;
    free(nums);
    return EXIT_SUCCESS;
}

For a variable count, check its byte multiplication before this call, just as for malloc. Request bytes, not elements: realloc(nums, 6) requests six bytes.

realloc(NULL, positive_size) behaves like malloc, which lets an empty buffer start with a null pointer. To release a buffer, call free explicitly. Avoid zero-size resizing: historical library behavior differs, and resizing an existing block to zero has undefined behavior in C23. Neither local arrays nor previously freed blocks are valid resize targets.

Key Takeaways

  • Use a temporary pointer so failed positive-size resizing preserves the owner’s address.
  • Growth preserves the old contents but leaves added bytes uninitialized.
  • Shrinking preserves only the part that still fits.
  • After success, use the returned pointer and rebuild saved element pointers.
  • Use free to release storage; handle zero sizes explicitly.

3.2 An array that grows with its input

A growable array needs three pieces of state: its pointer, its length and its capacity. Length counts initialized elements in use. Capacity counts element slots allocated. Keep length at or below capacity.

Append in a safe order

Start with int *data = NULL; size_t len = 0, cap = 0;. Before an append, check whether len == cap. A full array must grow before writing to data[len]. Only after resizing succeeds may the pointer and capacity change. Store the new value, then increase length.

The following complete example appends ten values with geometric growth:

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

int main(void) {
    int *data = NULL;
    size_t len = 0, cap = 0;
    for (int value = 0; value < 10; ++value) {
        if (len == cap) {
            size_t next;
            if (cap == 0) {
                next = 4;
            } else {
                if (cap > SIZE_MAX / 2 / sizeof *data) {
                    free(data);
                    return EXIT_FAILURE;
                }
                next = cap * 2;
            }
            int *tmp = realloc(data, next * sizeof *data);
            if (tmp == NULL) {
                free(data);
                return EXIT_FAILURE;
            }
            data = tmp;
            cap = next;
        }
        data[len++] = value;
    }
    free(data);
    return EXIT_SUCCESS;
}

The overflow guard checks both doubling and conversion to bytes before either product can wrap. The initial capacity of four also fits this element type. Failure releases the still-owned old allocation and stops; it does not falsely update capacity.

Trace length and capacity separately

Starting with capacity 4, the first four appends use existing space. Before the fifth, capacity doubles to 8. Before the ninth, it doubles to 16. Ten appends finish at length 10 and capacity 16. If both growth operations move, they copy at most four and eight elements, totaling twelve.

Spare capacity is deliberate. Growing by one element for each append causes repeated copying. From initial capacity 1, reaching 1,000 elements can require 999 resizes and 499,500 copied elements. Doubling reaches capacity 1,024 after ten resizes and at most 1,023 copies. A particular resize may be expensive, but the average work per append stays constant with geometric growth.

A factor such as 1.5 leaves less spare space but grows more often. If input has ended, the program can shrink to the used length, handling failure with the same temporary-pointer pattern. An empty array is released with free, not a zero-size resize.

Character buffers need one extra slot

For a growing string, reserve space for '\0'. Before adding a character, grow if len + 1 >= cap. With capacity 8, seven characters fit while leaving the terminator slot; "hello world" triggers growth to 16 and needs twelve bytes including its terminator.

A saved element pointer becomes invalid after successful growth. Keep an index and look up the element again using the new base. When resetting an array for further use, free its block and set data = NULL; len = cap = 0;.

Key Takeaways

  • Length counts used elements; capacity counts available slots.
  • Grow before an append when length equals capacity.
  • Update pointer and capacity only after successful resizing.
  • Geometric growth limits repeated copying at the cost of spare space.
  • Guard growth arithmetic before multiplication.
  • Reserve an extra byte for a string’s terminating null character.
  • Keep indexes across growth and rebuild element pointers afterward.
4

Recognize and prevent memory errors

Memory errors come from broken rules about ownership, lifetime, bounds or initialization. Each leaves a different trail: a lost address, an access after release, an index outside the block or a value read too soon. Some errors crash immediately, while others corrupt data and surface later. Reading the code in terms of blocks and their histories helps identify the cause behind the symptom.

4.1 Finding the path that loses memory

A memory leak is allocated storage that a program no longer needs but never releases. Often the last pointer disappears, making a later free impossible. A leak can run silently while memory use grows.

Trace addresses, not variable names
char *p = malloc(100);
if (p == NULL) return EXIT_FAILURE;
p = malloc(200);  /* loses the only address of the first block */
if (p == NULL) return EXIT_FAILURE;
free(p);

The final free releases only the second block. The name p was reused; the first allocation was not. If the second request fails, the 100-byte block is still lost. Either free the first block before replacing it, when it is no longer needed, or keep its address in another owner variable.

Setting an owning pointer to NULL before freeing it causes the same loss. The assignment changes a pointer variable; it does not tell the allocator to release anything. Automatic locals disappear at the end of a call, but the allocated blocks they pointed to remain.

Inspect every way out of a function

An early return can skip cleanup:

char *line = malloc(100);
if (line == NULL) return EXIT_FAILURE;
if (input_is_bad) {
    free(line);
    return EXIT_FAILURE;
}
/* Use line on the successful path. */
free(line);

Both paths release the block. In a function with more resources, one cleanup section reduces the number of paths to inspect. A function that returns an allocated result transfers its owner responsibility; a function that merely abandons a local owning pointer leaks.

Direct assignment of a failed realloc result is another loss path. Nested allocations leak when their outer pointer array is freed before the inner addresses have been used for cleanup.

Repetition makes small leaks expensive

Suppose a loop allocates 100 bytes per pass and never frees them. After 1,000 passes it has abandoned 100,000 bytes. Free each buffer at the end of its pass, or allocate once before the loop, reuse the block and free it afterward. Reuse also avoids repeated allocator work when the same capacity suffices.

A service leaking 64 bytes per request loses 640,000 bytes after 10,000 requests, exactly 625 KiB. After a million requests it has lost 64,000,000 bytes, about 61 MiB. Long-running programs can eventually fail to allocate or slow the system down. The operating system normally reclaims process memory at exit, but that does not help the service while it runs.

Lost and still reachable are different reports

If a global pointer still holds an allocation's starting address at exit, Valgrind calls the block "still reachable". The address has not been lost; the program simply did not release the block. "Definitely lost" means no usable pointer remains. Keep cleanup deliberate so that these reports show unexplained allocations clearly.

Key Takeaways

  • Overwriting or clearing the last owning pointer can leak a block.
  • Normal, early-return and failure paths all need cleanup.
  • Repeated small leaks add up while the program runs.
  • Reusing a suitable buffer can avoid both repeated allocation work and per-iteration leaks.

4.2 When a pointer outlives its target

A pointer becomes dangling when its target's lifetime ends. The variable may still exist, but reading or writing the old object through that pointer is invalid. Returning a local address and releasing an allocated block can both create this problem.

Find the last use before the release
int *scores = malloc(3 * sizeof *scores);
if (scores == NULL) return EXIT_FAILURE;
scores[0] = 90;
scores[1] = 75;
scores[2] = 82;
free(scores);
printf("%d\n", scores[0]);  /* use-after-free */

The assignment to scores[0] happened during the block's lifetime. The final read did not. Move free(scores) after the print, once every use is complete. Merely assigning scores = NULL before the print changes the invalid access into a null dereference; it does not provide the missing data.

This is undefined behavior: C places no requirements on the result. The program may display an old value, crash or silently damage data. Freed bytes often remain in place until the allocator reuses them. A plausible result therefore cannot validate a read after release.

Aliases share the lifetime problem
int *a = malloc(3 * sizeof *a);
if (a == NULL) return EXIT_FAILURE;
int *b = a;
free(a);
a = NULL;
b[0] = 1;  /* b also lost its live target */

There was one block, with two pointer variables holding its address. Releasing it through a ends its lifetime for both. Nulling a assigns only to a; it cannot update b. Code using borrowed aliases must finish before the owner frees the block, and those aliases must not be used afterward.

Double free is another use of an ended lifetime

Calling free twice for one block is a double free, including a second call through an alias. It can corrupt allocator bookkeeping or make a later allocation fail. Some libraries detect it immediately and stop the program, but immediate detection is not guaranteed.

Assigning an owner variable to NULL after release helps a repeated cleanup call because free(NULL) is valid. It does not make free(b) safe when b still refers to the former block. Exactly one owner should decide when the block is released.

Resizing also ends the old lifetime

A successful realloc replaces the old object even if the returned numeric address is unchanged. Saved pointers into the old block must be recomputed from the returned pointer. Store positions as indexes while growth can happen. A failed positive-size realloc, in contrast, leaves the old allocation alive.

A function returning an automatic local's address creates a similar lifetime error without any call to free: the target ends when the function returns. Use caller-owned storage or a clearly owned heap result. Use-after-free and double free can also become security defects when reused storage can be influenced by an attacker.

Key Takeaways

  • A dangling pointer targets an object whose lifetime has ended.
  • Finish every read and write before freeing the block.
  • One release invalidates every alias to the old object.
  • Nulling an owner helps repeated cleanup but does not repair borrowed aliases.
  • A double free can damage allocator bookkeeping.
  • Successful realloc invalidates saved pointers into the old block.

4.3 Valid bounds and initialized values

A valid element access needs both enough storage and a value that has been set before it is read. A block can have the right size yet contain uninitialized data, or contain useful values yet be accessed outside its bounds.

Check the element range

For n elements, valid indexes run from zero through n - 1. A loop with i <= n performs one extra access. With ten 4-byte ints, the allocation is 40 bytes; p[10] begins immediately after those bytes. Use i < n to visit the ten elements.

An index from input also needs a range check before use. A signed input must be nonnegative and below the element count. Keep the count with the pointer and pass both to functions. A pointer alone cannot describe the permitted range.

Size strings for their terminator
char *copy = malloc(strlen(s));
strcpy(copy, s);  /* writes the terminator beyond the allocation */

This incorrect fragment also needs a failure check. The sizing repair is malloc(strlen(s) + 1), followed by the check and then the copy. "Amsterdam" has nine characters but needs ten bytes with '\0'. The terminator is an actual stored byte, even though it is not printed as part of the text.

A pointer array is not string storage
char **names = malloc(n * sizeof *names);

This reserves n pointer slots. It creates no character buffers for those pointers to address. Before using names[i] as a string, assign it a valid string address. It may borrow an existing array or a literal with a sufficient lifetime, or own a separately allocated string copy. Only the owned strings are freed during nested cleanup.

The same size reasoning applies to numeric arrays: malloc(n) counts bytes, and sizeof p measures a pointer. Use n * sizeof *p, with the overflow guard and null check, to express an element count correctly.

Write before reading
int *t = malloc(3 * sizeof *t);
if (t == NULL) return EXIT_FAILURE;
t[0] = 5;
t[1] = 7;
printf("%d\n", t[0] + t[1] + t[2]);  /* t[2] was never set */
free(t);

t[2] is inside the allocated block, but its value is uninitialized. Assign it before the sum, or use calloc when zero is the intended initial value. The bytes added during growth with realloc need the same attention. Being within bounds does not initialize memory.

Why the crash can happen later

An out-of-bounds write can damage nearby data or the allocator's bookkeeping. An out-of-bounds read can expose unrelated data. Both have undefined behavior. A short overflow may touch mapped memory and escape immediate detection; a later free may be the first visible failure.

Check the allocation size, index range and initialization history independently. A larger allocation cannot supply a meaningful value to an uninitialized element, and zeroing cannot make an out-of-range index valid.

Key Takeaways

  • Valid indexes for n elements are zero through n - 1.
  • String storage includes one byte for the terminating null character.
  • An array of string pointers does not allocate the strings themselves.
  • Initialize all values that a calculation reads, including newly grown elements.
  • An invalid write can corrupt data long before a visible crash.

4.4 Tracing a crash back to its cause

A segmentation fault is an operating-system response to a forbidden memory access. Common causes include dereferencing a null or uninitialized pointer, writing into a string literal, accessing far outside an array, using freed storage and exhausting the stack.

What the message tells you

On Linux, SIGSEGV usually stops the process, and a shell may print "Segmentation fault (core dumped)". A common shell exit status is 139, from the signal convention of 128 plus signal 11. macOS can report SIGSEGV or SIGBUS for invalid accesses. Windows reports the comparable failure as an access violation, often exception code 0xC0000005.

These messages identify a failed access, not its complete history. A small overflow may corrupt a pointer early and cause a crash much later. Many invalid accesses stay within memory the process can address, so they silently corrupt data instead of generating a fault.

Start with line information

Build a debug executable with warnings:

cc -std=c17 -g -O0 -Wall -Wextra program.c -o program

Then run it under a debugger or memory checker. A backtrace lists the function calls active at the crash. Find the topmost frame belonging to your own code. Library functions such as free may be where bad state becomes visible, while an earlier call in your program created it.

At the reported line, identify every pointer being dereferenced. For each, trace its value back to its source. Was allocation checked? Is its target still alive? Does the index fit the block? Was the pointer assigned a valid address at all?

Work through a null result
int *p = NULL;
printf("%d\n", *p);  /* invalid dereference */

The print requires a read through p, but no integer object exists at that address. If p came from an allocation function, check the result before this line and take an error path on failure. If it was cleared after release, move the required use before release or redesign where the data is kept.

An uninitialized pointer, such as int *p; *p = 5;, has no valid target assigned. Allocation, a live object's address, or a checked borrowed pointer must establish that target before dereferencing.

Follow the cause when symptoms shift

An endlessly recursive function can exhaust stack frames in an unoptimized build. A stopping condition fixes the logic. An optimizing compiler may instead turn some recursive calls into a loop, so the absence of a stack crash does not make the unbounded computation useful.

Printed progress messages are also imperfect evidence. stdout is buffered: output can remain unwritten when a process crashes, especially with redirected output. For a diagnostic message, use stderr, or call fflush(stdout) after a progress print. Adding a print can change memory layout and hide an error; it has not repaired the invalid access. Rerun with a checker to find the faulty operation itself.

Key Takeaways

  • A segmentation fault reports a forbidden access; the original corruption may be earlier.
  • Build with debug information and locate the first relevant frame in your own code.
  • Trace each pointer’s origin, target lifetime and index range.
  • Buffered output can hide a print that ran before a crash.
  • A changed symptom after adding a print still needs investigation.
5

Find memory errors with Valgrind and sanitizers

Memory checkers make hidden errors visible by reporting the access and the block's history. Valgrind observes a debug executable; AddressSanitizer adds checks when it is built. Learn to connect their messages to element bounds, allocation sites and release sites, then make a repair that preserves correct ownership. A clean rerun is followed by new inputs that exercise other paths.

5.1 Reading a Valgrind report

Valgrind's default tool, Memcheck, observes a compiled program while it runs. It tracks heap bounds, allocation lifetimes and whether values have been initialized. It needs no sanitizer build, but debug information makes its reports much easier to use.

Build and run
cc -std=c17 -g -O0 -Wall -Wextra program.c -o program
valgrind --leak-check=full ./program

Put the program's own arguments after ./program. Add --show-leak-kinds=all to list all leak categories, and --track-origins=yes when an uninitialized value's origin is unclear.

Valgrind runs on Linux and some other Unix-like systems, not natively on Windows or Apple-silicon Macs. Memcheck often slows execution by roughly 20–30 times. Use a small reproducing input first, then run further tests. A clean test covers only the paths it actually executes.

Read access size and block size separately

A report headed "Invalid write of size 4" describes a four-byte write. An address "0 bytes after a block of size 40 alloc'd" begins immediately after a 40-byte allocation. If that block holds ten 4-byte ints, the bad access starts at element 10; the last valid element is 9.

The report also names the access line and allocation call stack. Use them together: the allocation tells you the intended bounds, and the access tells you which index violated them. "Size 4" does not mean the program overran by four elements.

"Invalid read of size N" similarly describes an invalid read. An address inside a block described as "free'd" signals a lifetime problem; compare its allocation, release and access locations. "Invalid free() / delete / delete[] / realloc()" can indicate a duplicate release or an address that was never a valid allocation start.

For a condition using unset data, Memcheck may say "Conditional jump or move depends on uninitialised value(s)". That spelling belongs to the tool's message. The reported condition consumed the value; --track-origins=yes helps locate where its uninitialized storage originated.

Read the leak category
Report categoryWhat it means
Definitely lostNo pointer to the block remains
Indirectly lostThe block is reachable only through another lost block
Possibly lostOnly an interior pointer was found
Still reachableA pointer remains at exit, but the block was not freed

Suppose a helper allocates ten 4-byte ints into a local pointer and returns without freeing or returning the address. Its 40-byte block is definitely lost. A global pointer retaining the starting address would instead make an unfreed block still reachable. The heap summary's allocation and free totals describe events, including resizing activity; they do not replace ownership reasoning.

A clean exit can report "All heap blocks were freed -- no leaks are possible" and "ERROR SUMMARY: 0 errors from 0 contexts". Memcheck does not generally catch every local or global array overrun that remains inside otherwise valid memory. Fix the first reported error, rerun, and test further inputs, including empty input and paths that return early.

Key Takeaways

  • Use debug information so Memcheck can identify your source lines.
  • Read access width, block bounds and allocation history together.
  • Use --track-origins=yes to trace uninitialized values.
  • Distinguish lost blocks from blocks still reachable at exit.
  • Fix the first error before interpreting later consequences.
  • Run several inputs because a checker observes only executed paths.

5.2 Building checks into the program

AddressSanitizer, or ASan, adds memory-access checks during compilation. The resulting executable checks itself while it runs. It is useful for routine development tests because its typical slowdown is about two times, much less than Memcheck's.

Compile and link with the sanitizer
cc -std=c17 -g -fsanitize=address -fno-omit-frame-pointer \
    program.c -o program-asan
./program-asan

Use the sanitizer flag at both compilation and final linking when building in separate steps. -g supplies line information, and -fno-omit-frame-pointer helps preserve useful call stacks. Microsoft's compiler uses /fsanitize=address.

ASan detects heap, stack and global array overflows, use-after-free, invalid frees and double frees. Scope-lifetime and optional use-after-return checks can also catch pointers to former locals. It requires a rebuild and extra memory. Keep this instrumentation in development and test builds.

Read the three histories

A use-after-free report can begin "ERROR: AddressSanitizer: heap-use-after-free". A following "READ of size 4" describes the invalid operation. The report then shows where the memory was "freed by thread T0 here:" and "previously allocated by thread T0 here:".

Start with the first relevant line in your code in each call stack. The allocation establishes the block; the free ends its lifetime; the reported read occurs after that end. Decide whether to move release after the read or stop accessing the block after release. The report supplies the history, while the intended ownership determines the repair.

For "heap-buffer-overflow", an address "located 0 bytes after 20-byte region" starts just past the region. Convert bytes to elements only after checking the type size used by the program. ASan normally stops at the first error, so fixing it can expose another error on the next run.

Leak checks and uninitialized values are different

LeakSanitizer checks for unreleased allocations at exit. It is normally enabled with ASan on Linux. On supported macOS toolchains it can be enabled with ASAN_OPTIONS=detect_leaks=1; some Mac toolchains do not provide it. Apple's separate leaks tool is another leak-checking option there.

ASan does not track whether an in-bounds value was initialized. For that problem, use Memcheck or Clang's MemorySanitizer on a supported Linux setup. UndefinedBehaviorSanitizer checks other kinds of undefined behavior, such as signed integer overflow, and can be combined with ASan using -fsanitize=address,undefined.

Choose a separate run for each checker
NeedUseful tool
Fast checks during development, including local-array boundsASan build
Uninitialized-value tracking without sanitizer instrumentationMemcheck on a supported system
Allocation leaks at exitLeakSanitizer or Memcheck

Do not run an ASan executable under Valgrind. Build a separate ordinary debug executable for Memcheck, and do not combine ASan and MemorySanitizer in one build. After a clean run, vary the input so other allocation, growth and cleanup paths execute.

Key Takeaways

  • ASan requires compilation and linking with its instrumentation flag.
  • Read the bad access, release and allocation stacks to reconstruct a lifetime error.
  • LeakSanitizer checks leaks at exit where the platform supports it.
  • ASan does not detect ordinary uninitialized reads.
  • Use separate executions and suitable builds for ASan and Memcheck.

5.3 Turning a report into a verified repair

A memory report identifies an observed error and parts of its history. A useful repair restores correct size, bounds, initialization and ownership, then checks the corrected program again.

Reduce and reproduce

Find the smallest input that still produces the error. Build with -g, warnings and little optimization, then use either Memcheck or an ASan build. Read the first report before investigating later ones; corrupted state can generate a chain of misleading failures.

Suppose a program contains:

size_t n = 5;
int *v = malloc(n * sizeof *v);
for (size_t i = 0; i <= n; ++i) v[i] = (int)(i * i);
printf("%d\n", v[2]);
return 0;

On a system with 4-byte ints, ASan reports a four-byte write at the first byte after a 20-byte allocation. The write is in the loop. Memcheck can also report the overflow and later an unreleased allocation; whether the block is lost or still reachable depends on which pointers remain at exit.

Match the report to the code

Five ints occupy indexes zero through four. The condition i <= n reaches index five. The allocation size is correct; the loop bound is not. Making the block larger would hide the symptom while leaving the traversal wrong.

There is also no allocation check, and the owned block is not freed. These require their own repairs, even if the first report stopped the program before it reached them.

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

int main(void) {
    size_t n = 5;
    int *v = malloc(n * sizeof *v);
    if (v == NULL) return EXIT_FAILURE;
    for (size_t i = 0; i < n; ++i) v[i] = (int)(i * i);
    printf("%d\n", v[2]);
    free(v);
    return EXIT_SUCCESS;
}

The repaired program initializes five valid elements, prints 4 while the block is alive and releases it. The fixed small count fits; a count obtained from input also needs the size-overflow check before allocation.

Use the report's history for the right kind of repair

For use-after-free, compare the release and access stacks to find the last legitimate use. Move release after it or remove the stale access. Setting a pointer to NULL cannot make its former data available again.

For a leak, the allocation stack says where the block began. It does not identify the right release line. Trace ownership through successful returns and failures to find where the last use ends. For a double free, remove the duplicate release or correct conflicting owners. Removing the only valid release creates a leak.

For uninitialized data, add Memcheck's --track-origins=yes. The consuming line can be far from the allocation that supplied unset bytes. Initialize every value that the computation reads, or use calloc when zero is the correct initial state.

Verify the repair on more than one path

Rebuild and rerun the original reproducing input. Continue until its report is clean, then try empty input, growth boundaries, ordinary input and error returns. Compiler warnings catch some problems before execution; runtime tools check the paths reached by these tests.

Key Takeaways

  • Reproduce with a small input and read the first error.
  • Use block size and access position to identify the actual bound error.
  • Check allocation failure and cleanup even when the first access error stops execution.
  • Leak reports show allocation sites; ownership determines release sites.
  • Rebuild, rerun the reproducer and exercise additional paths after repair.

Dynamic Memory Management in C examples

5 Walkthroughs

1

Follow an object through function calls

9 steps
Trace local, static and allocated data, then choose storage for a returned result.
  1. 1
    IntroductionAn address needs a live object

    A pointer lets one part of a program find data created elsewhere. Its usefulness depends on whether that data still exists when the pointer is used.

    Trace a helper that creates an integer array, returns its address, and lets its caller use the result. Keep the local pointer and the array separate as you follow the call: they need not have the same lifetime.

  2. 2
    ExplanationName the three lifetimes

    An ordinary local has automatic storage duration and ends with its block. A global or static local lasts for the program’s run. An allocated block lasts until deallocation.

    On mainstream systems, automatic objects occupy the call stack and allocations use the heap. These names describe common implementations; C’s rules define the lifetimes regardless of the memory layout.

  3. 3
    ExampleCreate the local pointer and the block

    Inside make_array, int *p = malloc(5 * sizeof *p); creates two separate objects: the local pointer and, on success, a five-int allocation.

    Where an int is 4 bytes, the block has 20 bytes. Check p before use. The pointer variable belongs to the function call, but the allocated block is not part of that call’s automatic storage.

  4. 4
    ExampleReturn the address

    The helper returns p, and its caller stores the result in int *nums. The address is copied; the array is not copied. The helper’s local variables then end.

    On success, nums still identifies the allocated array. The caller checks the result, initializes and uses its elements, and eventually frees it. Returning did not release the block because its lifetime is allocated.

  5. 5
    ChallengeWhat survives the return?

    A helper creates an automatic int count and a separate heap array. It returns the array’s address. Which of these two objects can the caller still use?

    ---reveal---

    The heap array remains alive until it is released. The helper’s automatic count ended on return. The copied address reaches only the array; keeping an address would not preserve count if the helper had returned &count instead.

  6. 6
    ExampleTry returning a local array

    Now change the helper to int values[5]; and return values. That array is automatic. It ends when the helper returns, leaving the caller without a live target.

    The size is still five ints, but the lifetime is different. A usable result can instead be placed in a caller-provided buffer or allocated and transferred to the caller with a clear cleanup responsibility.

  7. 7
    ConnectionChoose a buffer contract

    A static result buffer survives return, but every call shares it. A later call may overwrite an earlier result. A heap result can give each call its own independent storage, with the caller responsible for release.

    For temporary work, a small fixed local array is simpler. Input-sized, very large or growable arrays call for checked allocation, so the program can handle failure and control lifetime.

  8. 8
    ChallengeTwo results must stay independent

    A caller needs two writable results from successive calls and must keep both. It supplies no buffer. Would one static result array meet that need? What design fits?

    ---reveal---

    One static array would be shared and overwritten. Return a separate allocated result from each call, check each result, and let the caller free both after their final uses. The heap lifetime solves survival; separate blocks solve independence.

  9. 9
    RecapSeparate the address from the lifetime

    Trace the object that a pointer targets: automatic locals end with their block, static objects last for the run, and allocated blocks need deliberate release.

    For a returned result, ask whether storage survives and whether callers need independent copies. For temporary work, ask whether a small fixed local array already fits. The required size and lifetime determine the design before any allocation call is written.

2

Allocate a grid and clean up every path

13 steps
Work from element sizes to checked requests, initialized values and cleanup after partial success.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionPlan both success and failure
  2. 2
    ExplanationTurn counts into bytes
  3. 3
    TipGuard before multiplying
  4. 4
    ChallengeCount or pointer size?
  5. 5
    ExplanationGive counters a starting value
  6. 6
    ExampleUse the zeroed row
  7. 7
    ChallengeWhat about later growth?
  8. 8
    ExplanationCount blocks, not pointer names
  9. 9
    ExampleLet a partial failure stop construction
  10. 10
    ChallengeChoose the cleanup order
  11. 11
    Key InsightAn owner can have borrowers
  12. 12
    ChallengeOne path stops early
  13. 13
    RecapKeep cleanup alongside construction
3

Append values without losing data

12 steps
Trace a dynamic array’s length, capacity and resize failures, including the string terminator case.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionGrow before the next store
  2. 2
    ExplanationResize can replace the object
  3. 3
    ExampleSeparate failure from success
  4. 4
    ChallengeWhat can be preserved after shrinking?
  5. 5
    ExplanationLength and capacity answer different questions
  6. 6
    ExampleCross the first growth boundary
  7. 7
    ExampleCross the next boundary
  8. 8
    Key InsightSpare space pays for fewer copies
  9. 9
    ChallengeA failed append leaves what state?
  10. 10
    ConnectionA string reserves a terminator slot
  11. 11
    ChallengeA borrowed selection crosses growth
  12. 12
    RecapCommit state only after success
4

Inspect a block’s whole history

15 steps
Distinguish leaks, stale aliases, bounds errors and unset values, then trace a crash to its cause.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionFour questions for each access
  2. 2
    ExampleLose an allocation by replacing its address
  3. 3
    ChallengeDoes clearing the pointer clean up?
  4. 4
    TipInspect exits as well as loops
  5. 5
    ExampleFree once, invalidate all aliases
  6. 6
    ChallengeRepair the last use
  7. 7
    Key InsightExpected output can come from invalid code
  8. 8
    ExampleCount the null terminator
  9. 9
    ExampleAllocated does not mean initialized
  10. 10
    ChallengeFind two separate bounds
  11. 11
    ExplanationA fault is a symptom
  12. 12
    ExampleRead the crashing access
  13. 13
    ChallengeA print makes the crash disappear
  14. 14
    ChallengeCombine ownership and bounds
  15. 15
    RecapIdentify the broken rule
5

Read a report and verify the repair

14 steps
Choose a checker, interpret bytes and call stacks, and repair an overflow plus missing cleanup.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionTurn a message into a block history
  2. 2
    ExplanationUse an ordinary debug build for Memcheck
  3. 3
    ExampleTranslate the address description
  4. 4
    ChallengeRead bytes as bytes
  5. 5
    TipChoose a report option for the problem
  6. 6
    ExplanationBuild an ASan executable separately
  7. 7
    ExampleFollow use-after-free through three stacks
  8. 8
    ChallengeA clean ASan run misses a value error
  9. 9
    ExampleMatch the report to the loop
  10. 10
    ExamplePut the valid lifetime around the work
  11. 11
    ChallengeWhere should a leak be fixed?
  12. 12
    ConnectionCheck exit and platform support
  13. 13
    ChallengeFinish the repair cycle
  14. 14
    RecapUse evidence, repair, rerun

Dynamic Memory Management in C practice questions

50 Practice Questions in 5 sets

Set 1: Lifetimes, byte counts and first checks10 Practice Questions

Question 1 of 10

A buffer contains six initialized ints. Code saves the index 4, successfully shrinks the block to three ints, then wants the former selected element. What is a valid next step?
  1. Rebuild the selected pointer as &data[4].
  2. Reject the selection because index 4 was removed.
  3. Read the old selected pointer before another allocation.
  4. Read index 4 if the resized address stayed the same.
Show answerHide answer
Answer: B
Shrinking preserves only the first three elements, at indexes zero through two. Index 4 no longer names an element in the resized block, so it cannot be recovered by recomputing a pointer. The old object also ended on success, even if the address stayed the same; saved pointers cannot rescue discarded data.

Question 2 of 10

A function must produce a writable text result that survives its return. The caller provides no buffer, and separate calls must keep independent results. What storage design fits?
  1. Return a local array; the caller keeps its address.
  2. Return one static buffer; the caller frees it.
  3. Return the literal; the caller changes its letters.
  4. Return an allocated copy; the caller frees it.
Show answerHide answer
Answer: D
An allocated copy has a lifetime beyond the creating call and gives each result independent storage. A static buffer also survives, but it shares one result across calls and cannot be freed by the caller. A local array ends on return, and a string literal cannot supply writable storage.

Question 3 of 10

An array needs 13 ints. On this system sizeof(int) is 4. How many bytes should the allocation request?
  1. 52 bytes
  2. 13 bytes
  3. 17 bytes
  4. 104 bytes
Show answerHide answer
Answer: A
The request needs 13 elements of 4 bytes each, giving 52 bytes. A request for 13 bytes confuses the element count with the byte count; it cannot hold thirteen ints on this system.

Question 4 of 10

After a successful int *hits = calloc(6, sizeof *hits);, what is hits[4] before any later assignments?
  1. An uninitialized value
  2. The integer 6
  3. 0
  4. The integer 4
Show answerHide answer
Answer: C
calloc zeroes every byte, which gives integer elements the value zero. Index 4 lies within the six-element array, so it can be read as zero after the allocation check. Fresh malloc storage would be uninitialized; that rule does not apply to this successful calloc allocation.

Question 5 of 10

A function allocates a pointer array and four separate row blocks. All five allocations succeed, and it owns them all. How should it release the grid?
  1. Free the pointer array, then read its row pointers.
  2. Free just the pointer array; it also owns the rows.
  3. Free all four rows, then the pointer array.
  4. Free each row twice, then free the pointer array.
Show answerHide answer
Answer: C
The rows are four separate allocations, and their addresses are stored in the outer array. Release them while those addresses are still available, then release the outer block. Freeing only the pointer array leaves the row allocations unreleased; freeing it first also makes later reads of its entries invalid.

Question 6 of 10

A positive-size resize uses int *tmp = realloc(data, bytes); and returns NULL. The original block was valid. What can the owner do now?
  1. Keep using or free the unchanged original block.
  2. Use the new bytes through tmp after clearing it.
  3. Free tmp to release the original block at once.
  4. Use data only after allocating it again first.
Show answerHide answer
Answer: A
A failed positive-size realloc leaves the original allocation and its contents unchanged. Because the result was received in tmp, data still holds its valid address. Freeing the null temporary does nothing; it cannot release the still-owned block that data identifies.

Question 7 of 10

A dynamic array starts at length 0 and capacity 3. It doubles capacity only when full. After eight successful appends, what are its length and capacity?
  1. Length 8, capacity 8
  2. Length 12, capacity 12
  3. Length 8, capacity 9
  4. Length 8, capacity 12
Show answerHide answer
Answer: D
The fourth append grows capacity from 3 to 6, and the seventh grows it from 6 to 12. Eight values are now used, so length is 8. Capacity 8 would describe exact-fit growth, not the stated doubling rule; spare capacity does not increase the number of values stored.

Question 8 of 10

After a successful allocation, code runs int *view = owner; free(owner); owner = NULL;. What happens if it then reads view[0]?
  1. It reads valid storage because view is nonnull.
  2. It accesses an object whose lifetime has ended.
  3. It reads zero because owner was reset to null.
  4. It creates a new allocation at the previous address.
Show answerHide answer
Answer: B
view was an alias to the same allocation, whose lifetime ended at free(owner). Assigning NULL to owner updates only that pointer variable. It neither changes view nor restores the old block, so the later read is use-after-free with undefined behavior.

Question 9 of 10

A program copies the string "network" with strcpy into a fresh heap buffer. What is the smallest required allocation in C bytes?
  1. 7 bytes
  2. 9 bytes
  3. 16 bytes
  4. 8 bytes
Show answerHide answer
Answer: D
The seven letters require seven bytes, and strcpy also stores the terminating null character. The smallest buffer therefore has eight bytes. Seven bytes would hold the visible letters but leave the terminator outside the allocation, causing an out-of-bounds write even if printing seemed to work.

Question 10 of 10

A global int *saved initially equals NULL. A helper runs this code and returns. Ints are 4 bytes. Memcheck reports an invalid write immediately after the allocation. What traversal error and exit leak category follow?

int *p = malloc(7 * sizeof *p);
if (p == NULL) return;
for (size_t i = 0; i <= 7; ++i) p[i] = (int)i;
saved = p;
  1. One extra int is written; the block is still reachable.
  2. Four extra ints are written; the block is definitely lost.
  3. One extra int is written; the block is definitely lost.
  4. Four extra ints are written; the block is still reachable.
Show answerHide answer
Answer: A
The seven-int block permits indexes zero through six, but the loop also writes index seven. The access width is one four-byte int. The global retains the starting address after the helper returns, so the unfreed block is still reachable, not definitely lost.
Set 2: Follow allocation and cleanup paths10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

An unfreed allocation still has its starting address stored in a global pointer at exit. What Valgrind leak category describes it?
  1. Definitely lost
  2. Indirectly lost
  3. Still reachable
  4. Possibly lost
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A helper returns a successfully allocated array through a local pointer p. The caller stores the result in values. What remains alive immediately after the helper returns?
  1. The helper’s p variable and the allocated array.
  2. The helper’s p variable and all its parameters.
  3. The allocated array and the caller’s values variable.
  4. The caller’s values variable, but no allocated array.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

For a nonzero count n, what condition detects that n * sizeof *p cannot fit in size_t before allocation?
  1. n > SIZE_MAX / sizeof *p
  2. n * sizeof *p > SIZE_MAX
  3. sizeof p > n / SIZE_MAX
  4. n > SIZE_MAX * sizeof *p
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A function allocates buffer a, checks it, then tries to allocate b. The second allocation fails. It owns a and will return failure immediately. What cleanup is needed?
  1. Free b and return without touching a.
  2. Clear a to null and then return failure.
  3. Return failure; local pointer cleanup frees a.
  4. Free a before returning failure.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A function declares static int visits = 0; and executes return ++visits;. What value does its third call return?
  1. 1
  2. 3
  3. 0
  4. 2
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A string buffer has capacity 12 bytes and contains 11 characters plus its terminator. Before appending another character, what must happen?
  1. Overwrite the terminator and leave the size unchanged.
  2. Increase length twice, then grow after the next append.
  3. Write at index 12 and keep the old terminator in place.
  4. Grow the buffer, then write the character and terminator.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A loop successfully allocates 48 bytes on each of 750 iterations and loses each block’s only pointer without freeing it. How much storage is leaked?
  1. 36,000 bytes
  2. 750 bytes
  3. 48,000 bytes
  4. 36,048 bytes
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A successfully allocated array holds nine elements. What is its largest valid element index?
  1. 9
  2. 8
  3. 10
  4. 7
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A program starts crashing inside free after a loop that fills a heap array. What should the investigation check before changing the release call?
  1. Earlier writes that may have crossed the array’s bounds.
  2. Whether removing all cleanup makes the program finish.
  3. Whether a print changes the reported release address.
  4. Whether allocating the same array locally hides the crash.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A program begins with this checked block and later branches on p[5]. A positive-size resize succeeded. ASan reports nothing, while Memcheck reports an unset value at the branch. What repair fits the complete history?

int *p = calloc(4, sizeof *p);
if (p == NULL) return EXIT_FAILURE;
int *tmp = realloc(p, 6 * sizeof *p);
if (tmp == NULL) { free(p); return EXIT_FAILURE; }
p = tmp;
  1. Reinitialize elements 0 through 3 before the branch.
  2. Initialize element 4 but leave element 5 unchanged.
  3. Initialize elements 4 and 6 before the branch.
  4. Initialize elements 4 and 5 before using their values.
Show answerHide answer

Save this lesson to see this answer.

Set 3: Growth, lifetimes and report evidence10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A helper has a fixed local workspace of 20 ints, used only during the call, with no resize requirement. What storage choice avoids an unnecessary owner and cleanup path?
  1. A heap block whose address is lost.
  2. A returned pointer to a local array.
  3. A heap block behind an unowned global.
  4. An ordinary local array of 20 ints.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

On a system with 8-byte pointers and 4-byte ints, code uses int *p = malloc(11 * sizeof p);. How many bytes does it request, and what is wrong with the expression?
  1. 44 bytes; it sizes an element instead of the pointer.
  2. 88 bytes; it sizes the pointer instead of an element.
  3. 11 bytes; sizeof removes the multiplication entirely.
  4. 88 bytes; it reserves eleven independently owned blocks.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

After a successful positive-size calloc of a character buffer, what C string does it initially contain?
  1. An empty string
  2. A string of spaces
  3. A string of "0"s
  4. An unknown string
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A six-int block is owned through p. Code saves index 4 and a pointer to that element. An attempted growth to nine ints fails through a temporary. A later shrink to three succeeds, and the owner is updated. Can the selection be used afterward?
  1. Yes; the failed growth preserved both selection and lifetime.
  2. Yes; rebuild index 4 from the final three-int owner.
  3. No; the old pointer is invalid and index 4 no longer fits.
  4. No; the failed growth had already freed the six-int block.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

An array has length 6 and capacity 8. Two values are appended successfully. Code then saves its owner in old, sets cap = 16, and assigns data = realloc(data, cap * sizeof *data). That positive-size request fails before another value is stored. What state restores the unchanged array?
  1. Restore data = old, length 9 and capacity 16.
  2. Restore data = old, length 8 and capacity 8.
  3. Keep data = NULL, length 8 and capacity 8.
  4. Restore data = old, length 8 and capacity 16.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A function holds its only allocation in buffer. It executes buffer = NULL; free(buffer); after its last use. What is the result for the original block?
  1. It is freed because the variable once held its address.
  2. It is freed and filled with null bytes by the assignment.
  3. It stays allocated and its address is lost.
  4. It stays allocated but can still be reached through buffer.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

After a checked allocation, a function frees its block through p, then later calls free(alias), where alias was copied from p before release. What error is this?
  1. A double free through two aliases
  2. A leak of two separate live blocks
  3. A valid cleanup of two pointers
  4. An uninitialized read of new storage
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A diagnostic print to stdout lacks a newline, then the program crashes. Its text never appears. What explains why this cannot reliably locate how far execution reached?
  1. The missing text proves the print call never executed.
  2. The print necessarily prevented the memory error itself.
  3. The missing newline necessarily caused the memory fault.
  4. The text may still have been in the output buffer.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A developer compiled program-asan with AddressSanitizer and wants an independent Memcheck run. What executable should Memcheck receive?
  1. The same ASan build with leak detection disabled.
  2. A separate debug build without AddressSanitizer.
  3. The same ASan build with frame pointers omitted.
  4. A source file supplied directly instead of an executable.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

ASan stops on items[n] = 0; just after an n-int allocation. The loop uses i <= n, the result is unchecked, and cleanup is absent. What repair addresses the cause and the remaining ownership defects?
  1. Check allocation, loop with i < n, then free after use.
  2. Allocate n + 1 ints, keep the bound, and omit cleanup.
  3. Check allocation, keep i <= n, then free before the loop.
  4. Keep the size and bound, zero the block, then free after use.
Show answerHide answer

Save this lesson to see this answer.

Set 4: Repair ownership under changing conditions10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

What storage duration does an ordinary file-scope global array have?
  1. Static storage duration
  2. Automatic storage duration
  3. Allocated storage duration
  4. A fresh duration per call
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A library allocated four integer counters with calloc and later successfully enlarged the block to seven counters. What must happen before incrementing counter 6 for the first time?
  1. Increment it directly because all growth preserves zeroing.
  2. Free and recreate all seven counters to preserve old values.
  3. Assign its intended starting value before incrementing it.
  4. Set its address to NULL before using the larger allocation.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A grid’s outer array holds pointers to separately owned rows. Its creator has made rows 0 and 1. A reader borrows row 0 until it returns. Creating row 2 fails while that reader is active. Later entries were never set. What failure cleanup preserves every lifetime?
  1. Free rows 0 and 1, let the reader finish, then free the outer array.
  2. Let the reader finish, free the outer array, then read and free its rows.
  3. Let the reader finish, free every planned row, then free the outer array.
  4. Let the reader finish, free rows 0 and 1, then free the outer array.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A four-int allocation is owned by p. An attempted resize to seven ints fails through tmp, leaving p unchanged. Code then executes p = realloc(p, 9 * sizeof *p), and that request also fails. No aliases exist. Which allocation is now lost?
  1. The first requested seven-int block remains allocated but is lost.
  2. The original four-int block remains allocated but is lost.
  3. The second requested nine-int block remains allocated but is lost.
  4. No block remains allocated because both resize requests failed.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A dynamic array’s capacity doubles from 5 as it fills. It begins empty with that capacity and receives 18 values. If every growth moves the block, what capacity and maximum total element copies result from growth?
  1. Capacity 20; at most 15 copied elements.
  2. Capacity 20; at most 18 copied elements.
  3. Capacity 18; at most 23 copied elements.
  4. Capacity 40; at most 35 copied elements.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A function allocates a result, fills it, frees it, then returns the pointer to its caller. The caller needs to read that result. What ownership repair fits?
  1. Return the freed result and have the caller check nonnull.
  2. Clear the pointer before return and keep the same interface.
  3. Free the result twice so no stale allocation remains.
  4. Return the live result and let the caller free it later.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

Code allocates char **labels = malloc(4 * sizeof *labels);, checks it, then immediately runs strcpy(labels[0], "east");. What design repairs the first entry while making its ownership clear?
  1. Allocate five pointer slots, then copy through entry 0 as written.
  2. Set entry 0 to the literal "east", then overwrite that literal with strcpy.
  3. Allocate five chars for entry 0, check it, then copy and later free it.
  4. Set entry 0 to NULL, then copy five chars through that entry.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A successful resize reduces a block from nine initialized ints to four. What contents are guaranteed to survive?
  1. The original values at indexes 5 through 8.
  2. The original values at indexes 0 through 3.
  3. All nine values, as long as no new allocation occurs.
  4. Four new zero values, replacing the original prefix.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A test has a one-element overflow of a local array, staying within otherwise valid stack memory. A Memcheck run misses it. What next check best fits this evidence?
  1. Repeat Memcheck with leak categories shown.
  2. Rebuild with ASan and rerun the same input.
  3. Treat the access as valid after a clean run.
  4. Replace it with a larger unchecked heap block.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A checker reports a double free. Removing both release calls makes the run stop crashing but leaves an allocation unreleased. What repair should replace that change?
  1. Keep no release and rely on the operating system at exit.
  2. Keep both releases but set a borrowed alias to null.
  3. Move both releases before the last use to shorten the lifetime.
  4. Keep one release after the last use under one owner.
Show answerHide answer

Save this lesson to see this answer.

Set 5: Trace complete memory histories10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A function returns the address of a local character array. The caller sometimes prints the expected text and sometimes crashes. What explains both outcomes?
  1. The local array became static because its address escaped.
  2. The return transferred ownership of a valid heap allocation.
  3. The expected text proves the array was alive on that run.
  4. The array ended on return; the caller’s access is invalid.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

For an ordinary double *samples, what does sizeof samples measure?
  1. The size of one pointed-to double.
  2. The byte count of the whole allocation.
  3. The size of the pointer variable.
  4. The number of initialized array elements.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A function returns a newly allocated string. The caller passes it to a printer that only borrows it; the printer mistakenly frees it. The caller then reads it and frees it. What ownership change repairs the complete path?
  1. Remove the caller’s free; keep its read after the printer.
  2. Remove the printer’s free; keep the caller’s final free.
  3. Keep both frees; set only the printer’s local pointer to null.
  4. Move the caller’s free before the printer to end ownership sooner.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

Memcheck reports that a conditional jump depends on an uninitialized value. What option helps locate where that value originated?
  1. --track-origins=yes
  2. --leak-check=no
  3. --show-leak-kinds=all
  4. --error-limit=no
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A text buffer starts with capacity 6, length 4 and a terminator at index 4. It appends X successfully without growth, updating its terminator. The next character Y triggers growth through a temporary, but the request fails. The API promises to preserve the string on failure. What state remains?
  1. Length 4, capacity 6; both X and Y are discarded.
  2. Length 5, capacity 6; X is retained and the terminator is at 5.
  3. Length 6, capacity 6; Y replaces the terminator at 5.
  4. Length 5, capacity 12; X is retained and the terminator is at 5.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A service processes three jobs. Each job successfully allocates 24 bytes into local a and 40 bytes into local b. On the error path it frees a, sets b = NULL and returns. No aliases remain. What total lost allocation size follows, and what change prevents it?
  1. 192 bytes; free both pointer variables after they return.
  2. 72 bytes; stop freeing a on the error path.
  3. 40 bytes; free only the last job’s b after the loop.
  4. 120 bytes; free each b block before clearing its pointer.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

An array has a borrowed pointer to element 2. Code successfully grows the array through a temporary and updates the owner, then writes through the old borrowed pointer. The address did not change. What rule still makes this access invalid?
  1. Every resize discards the original prefix contents.
  2. The owner and borrowed pointer must differ in address.
  3. Successful resizing ended the old object’s lifetime.
  4. Growing keeps only values reached through index zero.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

Ints are 4 bytes and pointers are 8 bytes. Code uses int *p = malloc(6 * sizeof p);, checks success, initializes indexes 0 through 5, then sums indexes 0 through 11. What diagnosis fits the entire sum?
  1. Its last six reads are in bounds but uninitialized.
  2. Its last six reads are outside a 24-byte allocation.
  3. All twelve reads are valid because allocation initializes them.
  4. Only its final read crosses the oversized allocation’s end.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A program stops with a segmentation fault on a line that reads p[index]. What should be checked about that access?
  1. The pointer’s origin, target lifetime and index range.
  2. Only the index, because any nonnull pointer is live.
  3. Only the release line, because all faults are double frees.
  4. Only output buffering, because an array read cannot fault.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A tool report shows a read in display, a prior free in reset, and allocation in load. The caller invokes load, reset, then display on the same result. It needs that display. What repair follows from the block history?
  1. Increase the size in load, leaving the call order unchanged.
  2. Clear only reset’s local pointer and keep the later read.
  3. Keep the result alive through display, then release it.
  4. Ignore reset because the reported error occurred in display.
Show answerHide answer

Save this lesson to see this answer.

Stuck on your own question? Homework Help

Key Terms

63 Key Terms, each with its definition

A

AddressSanitizer
AddressSanitizer is compiler-added instrumentation for detecting invalid memory accesses and releases while an instrumented executable runs.
An ASan build can identify a local-array overrun that remains inside otherwise valid stack memory.
Alias
An alias is another pointer to the same object, sharing that object’s lifetime without being updated when a different pointer variable changes.
Setting the owner to NULL after release leaves any saved aliases unusable for later reads.
Allocation
An allocation is a request for a reserved block of memory; a successful request supplies its starting address for later use and release.
A program checks whether its allocation succeeded before storing the first element of the requested array.
Allocation site
An allocation site is the program location that requests a block, shown in tool reports to establish where that storage began.
A leak report’s allocation site may be inside a helper, while correct release belongs in its caller.
Automatic storage
Automatic storage belongs to ordinary local objects and parameters whose lifetimes end when execution leaves their block.
A helper’s local array ends when the helper returns, even if the caller saved the array’s address.

B

Backtrace
A backtrace lists the function calls active at a crash or error, helping locate the relevant access and the path that reached it.
Start at the first relevant frame in your own code, then inspect how its pointer obtained its value.
Borrowed pointer
A borrowed pointer gives access to a live object while another part of the program remains responsible for deciding when to release it.
A printer can borrow a string during its call without acquiring permission to free the caller’s block.
Buffer reuse
Buffer reuse keeps one suitable allocation across repeated operations, replacing its contents before eventually releasing the block.
A loop can reuse one 100-byte workspace instead of allocating and abandoning a block on every pass.

C

Call stack
The call stack is the region commonly used to track active function calls, their local objects, parameters and return information.
An unoptimized recursive function can keep adding frames until it exhausts the available stack space.
Caller-provided buffer
A caller-provided buffer is storage supplied to a function for its output, letting the caller retain control of its size and lifetime.
A caller can pass its own 16-byte array to a helper that fills it with a short greeting.
calloc
The function calloc reserves an array using a count and element size, zeroes its bytes, and returns null if allocation fails.
Integer counters from a checked calloc request begin at zero before the first increment.
Capacity
An array’s capacity is the number of element slots reserved in its block, including unused slots that do not yet belong to its logical contents.
A failed growth request must leave capacity unchanged because no additional slots were acquired.
Cleanup path
A cleanup path is code that releases acquired resources before a function exits, including when work stops after partial success.
If a second allocation fails, the error path frees the first block before returning failure.

D

Dangling pointer
A dangling pointer refers to an object whose lifetime has ended, leaving no valid old target for later access through that pointer.
A pointer to a helper’s former local array is dangling immediately after that helper returns.
Definitely lost
An allocation is definitely lost in Memcheck when no pointer remains, so its reserved storage cannot be released through any stored address.
A helper that abandons its sole owning pointer can leave a definitely-lost block in the exit report.
Double free
A double free attempts to release a block whose lifetime has already ended through an earlier release, including through another alias.
An owner and a borrower cannot each call free on what was one shared live allocation.
Dynamic array
A dynamic array is allocated element storage tracked with a length and capacity so it can expand when more values arrive.
A dynamic array may hold ten values in sixteen slots, leaving spare capacity for later appends.

F

First reported error
The first reported error is the earliest checker finding in a run, often the best starting point because later failures can follow from its damage.
Repair the initial overflow before investigating every later allocator failure in the same report.
free
The function free releases a live allocated block for reuse when passed its starting address, and does nothing when passed NULL.
The caller frees a returned string copy only after every borrower has finished reading it.

G

Geometric growth
Geometric growth increases capacity by a multiplicative factor, such as doubling, to limit repeated copying across many appends.
A full capacity-four array grows to eight, then to sixteen when its next growth boundary is reached.

H

Heap
The heap is the memory pool from which the allocator supplies blocks whose lifetimes are controlled through allocation and deallocation.
Returning an allocated array’s address leaves the block on the heap for its caller to use and release.
Heap buffer overflow
A heap buffer overflow reads or writes before or beyond an allocated block’s bounds, even if the surrounding address happens to be accessible.
A loop reaching index ten in a ten-element allocation crosses the block’s valid element range.
Heap summary
A heap summary reports allocation activity and remaining heap storage at exit, supporting investigation of blocks that were not released.
Read the remaining-block totals together with leak categories instead of treating every block as lost.

I

Indirectly lost
An allocation is indirectly lost when it can be reached only through another block that has itself been lost, as reported by Memcheck.
Losing an outer array of owned row addresses can also leave the row allocations indirectly lost.
Initialization
Initialization establishes a value before it is read, through an initial assignment or suitable zero-filled allocation.
A counter must have a defined starting value because incrementing it first reads its previous value.
Instrumentation
Instrumentation adds checking code during a build so the resulting program can detect selected errors while its tested paths execute.
Adding -fsanitize=address during compilation and linking creates an ASan-instrumented executable.
Interior pointer
An interior pointer addresses a position inside an object, such as an array element, instead of its allocation’s starting address.
Save an element index across successful resizing and rebuild the interior pointer from the result.

L

LeakSanitizer
LeakSanitizer checks for leaked allocations at exit and can accompany ASan, with availability and default settings depending on the platform.
Linux ASan builds normally enable leak checks; some macOS toolchains cannot provide them.
Length
An array’s length is its number of initialized elements currently in use, which determines the range for calculations over its stored values.
After appending a fifth value into available storage, length becomes five even if capacity remains eight.
Lost allocation
A lost allocation is a still-reserved block whose usable address is no longer held, preventing the program from releasing it normally.
Clearing the only owner to NULL without calling free first loses the allocation’s address.

M

malloc
The function malloc reserves a requested number of bytes and returns a block address or null, without initializing the stored values.
An integer array obtained with malloc needs assignments before its elements are added or compared.
Memcheck
Memcheck is Valgrind’s default memory checker, tracking invalid accesses, allocation lifetimes, unset values and unreleased blocks during a run.
Run an ordinary debug executable under Memcheck to obtain source lines alongside its memory histories.
Memory leak
A memory leak is an allocation that is no longer needed but remains unreleased, often because the program lost its last pointer to it.
Overwriting the only owner pointer with a new allocation can abandon the old block permanently.
MemorySanitizer
MemorySanitizer is Clang’s checker for uninitialized-value use on supported systems, addressing a kind of error ordinary ASan does not track.
Use it in a separate supported build instead of combining its instrumentation with AddressSanitizer.

N

Nested allocation
A nested allocation is a design with separately allocated inner blocks whose addresses are kept in an outer allocated container.
A grid with separate rows needs row cleanup before the outer pointer array is released.
Null pointer
A null pointer is a pointer value that designates no object, returned by allocation functions when a requested nonzero block cannot be supplied.
A null check sends execution to an error path before code tries to access a failed allocation.
Null terminator
A null terminator is the zero-valued character that marks the end of a C string and occupies one byte beyond its visible characters.
The four-character string "west" needs five bytes when copied together with its terminator.

O

Object lifetime
An object’s lifetime is the period during which it exists in reserved storage and can be accessed through valid pointers to it.
A successful realloc ends the old object’s lifetime even when the returned address looks unchanged.
Off-by-one error
An off-by-one error makes a bound one position too large or small, often including the element count as an invalid final index.
Changing i <= n to i < n makes a zero-based traversal visit exactly the n intended elements.
Origin tracking
Origin tracking follows an uninitialized value back toward the storage that supplied it, helping locate the missing value assignment.
Memcheck’s --track-origins=yes can explain why a later branch depends on unset heap data.
Output buffering
Output buffering temporarily keeps printed text in memory before writing it, so a crash can prevent recent progress messages from appearing.
Calling fflush(stdout) after a progress print makes its buffered text available before the next operation.
Ownership
Ownership is responsibility for releasing an allocated block after its final use, or deliberately transferring that duty to another part of the program.
A copy-producing function transfers ownership to its caller when it returns a newly allocated result.

P

Possibly lost
An allocation is possibly lost in Memcheck when only an interior pointer was found, leaving uncertainty about the allocation-start address.
Inspect whether a moving cursor replaced the owner instead of preserving a separate starting pointer.

R

realloc
The function realloc replaces an allocated block with a requested-size object, retaining the fitting prefix and possibly changing its address.
Growing a four-int block to six preserves the first four values but leaves two new elements unset.
Reproducing input
A reproducing input is a test case that reliably reaches the observed problem, giving a concrete starting point for debugging and verification.
Reduce a failing input while preserving its error so the first relevant report is easier to interpret.

S

Segmentation fault
A segmentation fault is an operating-system fault caused by a forbidden memory access, such as a common null dereference or read-only write.
A bad write can corrupt data first and cause a segmentation fault later in another function.
Shared static buffer
A shared static buffer is a function’s persistent result storage reused by calls, so a later call can overwrite an earlier result.
Two pointers returned by successive calls may refer to the same static array instead of independent text.
Size overflow
A size overflow occurs when a count-to-bytes calculation exceeds the range of size_t and wraps to a smaller representable value.
Check the element count against SIZE_MAX / sizeof *p before multiplying it by the element size.
size_t
The type size_t is the unsigned integer type used for object sizes and allocation byte counts, including the result of sizeof.
A dynamic array uses size_t variables for its length and capacity so they can describe object sizes.
sizeof
The operator sizeof gives a type or object’s size in C bytes; on a pointer, it measures the pointer instead of its target allocation.
The request n * sizeof *p follows the element type, while sizeof p cannot recover the array’s length.
Stack overflow
A stack overflow exhausts the space available for active calls and automatic objects, often through deep recursion or a huge local array.
An unoptimized recursive function without a stopping condition can keep adding frames until it fails.
Static storage
Static storage is storage that lasts for the program’s run, used by global objects and local variables declared with static.
A static counter inside a function keeps its updated value when the next call begins.
Still reachable
An allocation is still reachable in Valgrind when a pointer remains at exit, although the program has not freed the block.
A global pointer retaining an unfreed allocation’s starting address makes it reachable to the exit check.
Storage duration
An object’s storage duration determines how long its storage is reserved, including whether it ends with a block, deallocation or program exit.
A local pointer and its allocated array can have different storage durations even while they share one address.
String literal
A string literal is text written in quotes in C source, with static storage that must not be modified through a pointer.
Use char word[] = "hello"; when a local writable copy is needed instead of a pointer to the literal.

T

Temporary pointer
A temporary pointer receives a resize result before the owner is updated, preserving the old allocation’s address if the request fails.
With tmp = realloc(p, bytes), a null result leaves p available for continued use or cleanup.

U

Undefined behavior
Undefined behavior occurs when C places no requirements on the program’s result after a rule violation, allowing crashes or silent corruption.
An out-of-bounds write can seem harmless in one test and corrupt another value with different input.
UndefinedBehaviorSanitizer
UndefinedBehaviorSanitizer adds checks for selected non-address undefined behavior, such as signed integer overflow, and can accompany ASan.
A development build can use -fsanitize=address,undefined to enable the two compatible sets of checks.
Uninitialized read
An uninitialized read consumes a value that was never set, including an in-bounds element of fresh or newly grown storage.
Summing three allocated ints after assigning only two still reads an unset third value.
Use-after-free
A use-after-free accesses an allocated object after deallocation, which can return old-looking data, corrupt memory or cause a crash.
Printing a freed string through a saved alias is invalid even when the expected words appear.

V

Variable-length array
A variable-length array is an array with a size set during execution and automatic lifetime, commonly occupying stack storage.
Declaring int a[n] does not provide the null-result failure check available from heap allocation.
Verification run
A verification run executes the repaired program with a checker to confirm the tested path, followed by inputs that exercise other paths.
After a clean reproducer, test empty input, growth boundaries and early exits for missing cleanup.

Z

Zero-filled memory
Zero-filled memory has every bit set to zero, giving integer zero but not portably guaranteeing floating-point zero or null pointers.
A positive-size character buffer from calloc starts with a null character and holds an empty string.

About this lesson

By StudeliReport a mistake

Make your own Study Materials

Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.

Get Started