You may recognise malloc() and free() but still wonder where the pointer itself lives, whether fresh bytes contain zeroes, or what remains valid after realloc(). A practical model of C object lifetimes includes allocation, growth, calculation, and cleanup. Numerical byte counts use an illustrative implementation where sizeof(int) == 4; the code stays portable by using sizeof *pointer.
Memory management in C starts with lifetime, not just malloc
Scope controls where a name is visible. Storage duration controls how long its object exists. Common systems use stack, static-storage, and heap regions, but C does not mandate regions by those names.
C defines automatic, static, allocated, and thread storage durations. File-scope int audit_total = 7; has static storage duration. Inside main, size_t count = 4; and pointer variable int *scores have automatic storage duration. malloc(count * sizeof *scores) creates a separate allocated object that exists until released.
On our 4-byte-int system, scores points to a 16-byte block holding {12, 18, 21, 27}. Pointer and pointee are separate objects with separate lifetimes. The OS layer appears in Memory Management in OS: Paging and Segmentation. Dynamic allocation is one part of the C lifetime model. Dynamic Memory Allocation in C: malloc, calloc, realloc and free with a Worked Example gives the focused allocator-function and resizing treatment; the broader check is whether each pointer and pointee still names a live object. malloc() does not choose a page or physical address.

malloc, calloc, realloc and free have different contracts
Function | Contract |
|---|---|
| Reserves an uninitialised block. Its initial values are indeterminate. |
| Reserves a block and zero-initialises its bytes. |
| May resize the block in place or move it. |
| Ends the allocated object's lifetime. Passing |
malloc(4 * sizeof *scores) requests 16 uninitialised bytes here. calloc(5, sizeof *scores) requests 20 bytes for five integers initially equal to zero. Check either result before dereferencing. In C, do not cast malloc(); sizeof *scores automatically follows a changed pointee type.
For a variable-size request, include <stdint.h> for SIZE_MAX and guard the multiplication first:
if (count > SIZE_MAX / sizeof *scores) {
/* reject the request */
}Validate multiplication, allocate, test for NULL, then initialise before reading. free() neither clears the pointer variable nor promises to zero released bytes.
Memory allocation in C: trace three sensor samples as they grow to five
The C11 program allocates three sensor samples, grows the block to five, computes their average, and releases the allocation.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
int audit_total = 7;
int main(void) {
size_t count = 3;
int *samples = NULL;
if (count > SIZE_MAX / sizeof *samples) return EXIT_FAILURE;
samples = malloc(count * sizeof *samples);
if (samples == NULL) return EXIT_FAILURE;
int initial[] = {8, 13, 21};
int sum = 0;
for (size_t i = 0; i < count; ++i) {
samples[i] = initial[i];
sum += samples[i];
}
size_t new_count = 5;
if (new_count > SIZE_MAX / sizeof *samples) {
free(samples);
return EXIT_FAILURE;
}
int *grown = realloc(samples, new_count * sizeof *samples);
if (grown == NULL) {
free(samples);
return EXIT_FAILURE;
}
samples = grown;
count = new_count;
samples[3] = 34;
samples[4] = 55;
sum += samples[3] + samples[4];
double average = (double)sum / count;
printf("count=%zu, sum=%d, average=%.1f\n", count, sum, average);
free(samples);
samples = NULL;
return EXIT_SUCCESS;
}The first request is 3 x 4 = 12 bytes, with 8 + 13 + 21 = 42. Growth requests 5 x 4 = 20 bytes. Values become {8, 13, 21, 34, 55}, so 42 + 34 + 55 = 131 and 131 / 5.0 = 26.2. Output is count=5, sum=131, average=26.2.
Setting samples = NULL after free(samples) is useful local discipline, but it does not repair any other aliases that still hold the released address.
realloc in C preserves values but not addresses
On success, the first min(old_size, new_size) bytes are preserved. New bytes remain indeterminate until initialised. Only use the returned pointer, whether its numeric address is unchanged or different.
For this non-zero 20-byte request, failure returns NULL while samples still owns the valid 12-byte block. Direct assignment with samples = realloc(samples, 20) could overwrite its only reference and leak it. A temporary pointer preserves ownership.
Ownership rules make C cleanup predictable
Ownership is a program rule, not a C keyword. int *load_scores(size_t *count) can return an allocated array and transfer ownership to its caller, which must call free() exactly once.
In int *bad(void) { int local = 9; return &local; }, the address escapes, but automatic object local stops existing on return. Dereferencing the result has undefined behaviour. String literals and global objects must not be passed to free() merely because pointers reach them.
For multiple resources, initialise owners to NULL and use one cleanup path:
char *name = NULL;
int *scores = NULL;
int status = EXIT_FAILURE;
name = malloc(32);
if (name == NULL) goto cleanup;
scores = malloc(6 * sizeof *scores); /* 24 bytes here */
if (scores == NULL) goto cleanup;
status = EXIT_SUCCESS;
cleanup:
free(scores);
free(name);If the second allocation fails, cleanup still releases name. Normal exit releases both blocks once.
Memory leaks, dangling pointers and bounds errors in C
Bug | Why it happens | What goes wrong | Fix |
|---|---|---|---|
Overwrite the only pointer to a live 16-byte block | A new allocation is assigned too early | The old block leaks | Free it first or preserve its owner |
Read | Both pointers referred to one allocation |
| Stop all access after the free |
Free the same non-null pointer twice | Ownership is unclear | The second free has undefined behaviour | Give one owner one cleanup duty |
Read samples[5] from five elements | The index is mistaken for a count | Index 5 is out of bounds | Use only indices 0 through 4 |
With an 8-byte pointer and 4-byte int, malloc(5 * sizeof samples) requests 5 x 8 = 40 bytes because samples is a pointer. Five integers need 5 x 4 = 20 bytes. This over-allocation is still a reasoning bug; a larger pointee could instead cause under-allocation. Use malloc(5 * sizeof *samples).
Compile with strong warnings and, where supported, AddressSanitizer: cc -std=c11 -Wall -Wextra -Wpedantic -fsanitize=address -g memory.c -o memory. Use its reports to trace the violated lifetime or bound. Leak detection support varies by platform.
Memory management questions test tracing, bytes and validity
Trace int *p = malloc(3 * sizeof *p); p[0] = 4; p[1] = 7; p[2] = 9; int *q = p; free(p);. Is q[1] still 7? No. q is dangling, so reading it has undefined behaviour. One allocation and one free means the defect is a lifetime violation, not a leak.
After successful realloc(), use its returned pointer even if its address looks unchanged. After failure for a non-zero request, the original pointer stays valid and must be freed. realloc() neither always moves nor always extends in place.
For any trace, ask four questions:
How many bytes were requested?
Which indices are within bounds?
Who owns each allocation now?
Which pointers still refer to live objects?
Memory management in C: the short version and next step
Keep five rules: calculate sizes with sizeof *pointer; check multiplication and allocation failure; initialise before reading; grow through a temporary pointer; release each owned allocation exactly once. As a self-check, shrinking the worked five-element block to two integers would request 2 x 4 = 8 bytes, preserve {8, 13} after success, and give 8 + 13 = 21.
Beginners can use the C Programming Course for the wider language foundation. For concept, MCQ, and coding-question practice, continue with the C Language Course: Concepts, MCQs and Coding Questions. Once you can allocate, resize, traverse, and free an array reliably, Coding & DSA Courses for Placements is the broader path.
Now implement the worked example, force one allocation-failure path if your environment permits, and verify that every successful allocation has one owner and one cleanup path.




