- Learn
- Computer Science & IT
- Programming
- Programming in C
- Dynamic Memory Management in C
Dynamic Memory Management in C
IntermediateLesson 5 of 8 in Programming in C Course
What you will learn
- 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 storageDescribe 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 heapChoose between local variables and heap allocation from the size, lifetime and growth of the data, and avoid returning the address of a local variable.
- Compare stack, heap and static storage
- 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 sizeofCompute the number of bytes to request, call malloc, check for failure and use the block as an array.
- Get zero-filled memory with callocUse 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 ownershipFree 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.
- Request memory with malloc and sizeof
- 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 reallocCall 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 arrayKeep a length and a capacity next to the pointer, grow the capacity geometrically, and read input of unknown length into a buffer that expands.
- Grow and shrink a block safely with realloc
- 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 leaksExplain 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 freesRecognize 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 readsKeep every access within the bounds of its block, size string buffers correctly, and set memory before reading it.
- Explain and trace segmentation faultsExplain what a segmentation fault is, name its common causes, and work from the crash back to the faulty line.
- Find and prevent memory leaks
- 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 MemcheckBuild a program for Valgrind, run it with leak checking, and read its error messages, heap summary and leak categories.
- Use AddressSanitizer and LeakSanitizerBuild a program with AddressSanitizer, read its reports, and choose between sanitizers and Valgrind.
- Fix memory bugs from a tool reportFollow 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.
- Check a program with Valgrind Memcheck
Dynamic Memory Management in C study guide
1Explain 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 duration | Typical objects | Lifetime ends |
|---|---|---|
| Automatic | Ordinary local variables and parameters | When their block ends |
| Static | Globals and local variables declared static | When the program ends |
| Allocated | Blocks obtained from allocation functions | When 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
| Need | Suitable starting choice |
|---|---|
| A small fixed array used during one call | Local array |
| A count read from input | Checked heap allocation |
| A result that survives its creating function | Heap allocation or a caller-owned buffer |
| Data that grows as input arrives | Resizable heap buffer |
| A very large array | Heap 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.
2Allocate 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 *pointerfor an array’s byte count. - Check multiplication overflow before calling
malloc. - Check for
NULLbefore using the result. - Initialize each
mallocelement before reading it. - Keep the element count separately;
sizeof pointercannot 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 use | Natural choice |
|---|---|
| Increment integer counters | calloc |
| Set integer flags gradually from an all-zero state | calloc |
| Immediately overwrite every element | malloc |
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
callocan 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
reallocdoes 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 tofree. - 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
freeof the latest block.
3Resize 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
freeto 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.
4Recognize 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
reallocinvalidates 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
nelements are zero throughn - 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.
5Find 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 category | What it means |
|---|---|
| Definitely lost | No pointer to the block remains |
| Indirectly lost | The block is reachable only through another lost block |
| Possibly lost | Only an interior pointer was found |
| Still reachable | A 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=yesto 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
| Need | Useful tool |
|---|---|
| Fast checks during development, including local-array bounds | ASan build |
| Uninitialized-value tracking without sanitizer instrumentation | Memcheck on a supported system |
| Allocation leaks at exit | LeakSanitizer 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
1Follow an object through function calls
9 steps
- 1IntroductionAn 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.
- 2ExplanationName the three lifetimes
An ordinary local has automatic storage duration and ends with its block. A global or
staticlocal 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.
- 3ExampleCreate 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
intis 4 bytes, the block has 20 bytes. Checkpbefore use. The pointer variable belongs to the function call, but the allocated block is not part of that call’s automatic storage. - 4ExampleReturn the address
The helper returns
p, and its caller stores the result inint *nums. The address is copied; the array is not copied. The helper’s local variables then end.On success,
numsstill 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. - 5ChallengeWhat survives the return?
A helper creates an automatic
int countand 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
countended on return. The copied address reaches only the array; keeping an address would not preservecountif the helper had returned&countinstead. - 6ExampleTry returning a local array
Now change the helper to
int values[5];and returnvalues. 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.
- 7ConnectionChoose 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.
- 8ChallengeTwo 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.
- 9RecapSeparate 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.
2Allocate a grid and clean up every path
13 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionPlan both success and failure
- 2ExplanationTurn counts into bytes
- 3TipGuard before multiplying
- 4ChallengeCount or pointer size?
- 5ExplanationGive counters a starting value
- 6ExampleUse the zeroed row
- 7ChallengeWhat about later growth?
- 8ExplanationCount blocks, not pointer names
- 9ExampleLet a partial failure stop construction
- 10ChallengeChoose the cleanup order
- 11Key InsightAn owner can have borrowers
- 12ChallengeOne path stops early
- 13RecapKeep cleanup alongside construction
3Append values without losing data
12 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionGrow before the next store
- 2ExplanationResize can replace the object
- 3ExampleSeparate failure from success
- 4ChallengeWhat can be preserved after shrinking?
- 5ExplanationLength and capacity answer different questions
- 6ExampleCross the first growth boundary
- 7ExampleCross the next boundary
- 8Key InsightSpare space pays for fewer copies
- 9ChallengeA failed append leaves what state?
- 10ConnectionA string reserves a terminator slot
- 11ChallengeA borrowed selection crosses growth
- 12RecapCommit state only after success
4Inspect a block’s whole history
15 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionFour questions for each access
- 2ExampleLose an allocation by replacing its address
- 3ChallengeDoes clearing the pointer clean up?
- 4TipInspect exits as well as loops
- 5ExampleFree once, invalidate all aliases
- 6ChallengeRepair the last use
- 7Key InsightExpected output can come from invalid code
- 8ExampleCount the null terminator
- 9ExampleAllocated does not mean initialized
- 10ChallengeFind two separate bounds
- 11ExplanationA fault is a symptom
- 12ExampleRead the crashing access
- 13ChallengeA print makes the crash disappear
- 14ChallengeCombine ownership and bounds
- 15RecapIdentify the broken rule
5Read a report and verify the repair
14 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionTurn a message into a block history
- 2ExplanationUse an ordinary debug build for Memcheck
- 3ExampleTranslate the address description
- 4ChallengeRead bytes as bytes
- 5TipChoose a report option for the problem
- 6ExplanationBuild an ASan executable separately
- 7ExampleFollow use-after-free through three stacks
- 8ChallengeA clean ASan run misses a value error
- 9ExampleMatch the report to the loop
- 10ExamplePut the valid lifetime around the work
- 11ChallengeWhere should a leak be fixed?
- 12ConnectionCheck exit and platform support
- 13ChallengeFinish the repair cycle
- 14RecapUse evidence, repair, rerun
Dynamic Memory Management in C practice questions
Set 1: Lifetimes, byte counts and first checks10 Practice Questions
Question 1 of 10
Show answerHide answer
Question 2 of 10
Show answerHide answer
Question 3 of 10
sizeof(int) is 4. How many bytes should the allocation request?Show answerHide answer
Question 4 of 10
int *hits = calloc(6, sizeof *hits);, what is hits[4] before any later assignments?Show answerHide answer
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
Show answerHide answer
Question 6 of 10
int *tmp = realloc(data, bytes); and returns NULL. The original block was valid. What can the owner do now?Show answerHide answer
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
Show answerHide answer
Question 8 of 10
int *view = owner; free(owner); owner = NULL;. What happens if it then reads view[0]?Show answerHide answer
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
strcpy into a fresh heap buffer. What is the smallest required allocation in C bytes?Show answerHide answer
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;
Show answerHide answer
Set 2: Follow allocation and cleanup paths10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
p. The caller stores the result in values. What remains alive immediately after the helper returns?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
n, what condition detects that n * sizeof *p cannot fit in size_t before allocation?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
a, checks it, then tries to allocate b. The second allocation fails. It owns a and will return failure immediately. What cleanup is needed?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
static int visits = 0; and executes return ++visits;. What value does its third call return?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
free after a loop that fills a heap array. What should the investigation check before changing the release call?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;
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
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
int *p = malloc(11 * sizeof p);. How many bytes does it request, and what is wrong with the expression?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
calloc of a character buffer, what C string does it initially contain?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
buffer. It executes buffer = NULL; free(buffer); after its last use. What is the result for the original block?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
p, then later calls free(alias), where alias was copied from p before release. What error is this?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
stdout lacks a newline, then the program crashes. Its text never appears. What explains why this cannot reliably locate how far execution reached?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
program-asan with AddressSanitizer and wants an independent Memcheck run. What executable should Memcheck receive?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
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?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
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
calloc and later successfully enlarged the block to seven counters. What must happen before incrementing counter 6 for the first time?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
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
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
double *samples, what does sizeof samples measure?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
p[index]. What should be checked about that access?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
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?Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
A
AddressSanitizer
Alias
NULL after release leaves any saved aliases unusable for later reads.Allocation
Allocation site
Automatic storage
B
Backtrace
Borrowed pointer
Buffer reuse
C
Call stack
Caller-provided buffer
calloc
calloc reserves an array using a count and element size, zeroes its bytes, and returns null if allocation fails.calloc request begin at zero before the first increment.Capacity
Cleanup path
D
Dangling pointer
Definitely lost
Double free
free on what was one shared live allocation.Dynamic array
F
First reported error
free
free releases a live allocated block for reuse when passed its starting address, and does nothing when passed NULL.G
Geometric growth
H
Heap
Heap buffer overflow
Heap summary
I
Indirectly lost
Initialization
Instrumentation
-fsanitize=address during compilation and linking creates an ASan-instrumented executable.Interior pointer
L
LeakSanitizer
Length
Lost allocation
NULL without calling free first loses the allocation’s address.M
malloc
malloc reserves a requested number of bytes and returns a block address or null, without initializing the stored values.malloc needs assignments before its elements are added or compared.Memcheck
Memory leak
MemorySanitizer
N
Nested allocation
Null pointer
Null terminator
O
Object lifetime
realloc ends the old object’s lifetime even when the returned address looks unchanged.Off-by-one error
i <= n to i < n makes a zero-based traversal visit exactly the n intended elements.Origin tracking
--track-origins=yes can explain why a later branch depends on unset heap data.Output buffering
fflush(stdout) after a progress print makes its buffered text available before the next operation.Ownership
P
Possibly lost
R
realloc
realloc replaces an allocated block with a requested-size object, retaining the fitting prefix and possibly changing its address.Reproducing input
S
Segmentation fault
Shared static buffer
Size overflow
size_t and wraps to a smaller representable value.SIZE_MAX / sizeof *p before multiplying it by the element size.size_t
size_t is the unsigned integer type used for object sizes and allocation byte counts, including the result of sizeof.size_t variables for its length and capacity so they can describe object sizes.sizeof
sizeof gives a type or object’s size in C bytes; on a pointer, it measures the pointer instead of its target allocation.n * sizeof *p follows the element type, while sizeof p cannot recover the array’s length.Stack overflow
Static storage
static.Still reachable
Storage duration
String literal
char word[] = "hello"; when a local writable copy is needed instead of a pointer to the literal.T
Temporary pointer
tmp = realloc(p, bytes), a null result leaves p available for continued use or cleanup.U
Undefined behavior
UndefinedBehaviorSanitizer
-fsanitize=address,undefined to enable the two compatible sets of checks.Uninitialized read
Use-after-free
V
Variable-length array
int a[n] does not provide the null-result failure check available from heap allocation.Verification run
Z
Zero-filled memory
calloc starts with a null character and holds an empty string.About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.