What you'll learn
Dynamic allocation creates storage whose size and lifetime are decided at runtime. It powers flexible collections and long-lived objects, but every successful allocation creates a cleanup responsibility.
By the end, you'll be able to:
- Distinguish automatic, static, and allocated lifetimes
- Allocate, initialize, resize, and free checked storage
- State ownership rules and avoid leaks, double-free, and dangling pointers
- Grow a dynamic array without losing the original allocation
Storage durations
Scope controls name visibility; storage duration controls object lifetime. Allocated objects are different from automatic locals: leaving the creating function does not release them.
Static storage
Entire program
globals · static locals
Automatic storage
One block/call
ordinary local objects
Allocated storage
malloc → free
runtime-sized objects
Stack & heap model
Implementations commonly place automatic call frames on a stack and allocated storage in a heap. These are useful engineering models, though the C standard defines storage durations rather than requiring those exact regions.
| Property | Automatic object | Allocated object |
|---|---|---|
| Size decision | Usually fixed at block entry | Chosen at runtime |
| Lifetime ends | Automatically at block exit | Only when released with free |
| Typical access | Direct name | Through a pointer |
| Main risk | Returning its address | Leak, double-free, use-after-free |
malloc & calloc
malloc returns uninitialized storage suitably aligned for any object type.calloc multiplies count and element size and zeroes the resulting bytes. Both return NULL on failure.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t count = 5;
int *values = malloc(count * sizeof *values);
if (values == NULL) {
fprintf(stderr, "Allocation failed\n");
return 1;
}
for (size_t i = 0; i < count; i++) {
values[i] = (int) (i + 1) * 10;
}
for (size_t i = 0; i < count; i++) {
printf("%d ", values[i]);
}
putchar('\n');
free(values);
values = NULL;
return 0;
}#include <stdlib.h>
size_t count = 100;
int *counters = calloc(count, sizeof *counters);
if (counters == NULL) {
/* handle allocation failure */
}
/* every byte in the allocated region initially contains zero */
free(counters);Tip
sizeof *pointer instead of repeating the type. If the pointer type changes, the allocation expression stays correct.Watch out
count <= SIZE_MAX / sizeof *pointer to prevent size overflow.Resize with realloc
realloc may extend the existing region or move it. On success, the old pointer is invalid; on failure, the old allocation remains valid.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t count = 3;
int *values = malloc(count * sizeof *values);
if (values == NULL) return 1;
values[0] = 10;
values[1] = 20;
values[2] = 30;
size_t new_count = 6;
int *resized = realloc(values, new_count * sizeof *values);
if (resized == NULL) {
free(values); // original allocation is still valid
return 1;
}
values = resized;
for (size_t i = count; i < new_count; i++) values[i] = 0;
printf("%d %d\n", values[2], values[5]);
free(values);
return 0;
}Key idea
values = realloc(values, ...) directly would lose the only pointer to the original allocation when resizing fails.Ownership & cleanup
Ownership is a design rule: the owner is responsible for eventually callingfree. APIs should state whether they borrow a pointer, take ownership, or return ownership to the caller.
- Exactly one clear owner for each allocation
- Borrowed pointers never outlive their owner
- Every successful allocation has a reachable cleanup path
- After
free, do not read, write, compare as an array cursor, or free again
#include <stdio.h>
#include <stdlib.h>
int process(size_t count) {
int result = 1;
int *left = NULL;
int *right = NULL;
left = malloc(count * sizeof *left);
if (left == NULL) goto cleanup;
right = malloc(count * sizeof *right);
if (right == NULL) goto cleanup;
/* perform work */
result = 0;
cleanup:
free(right); // free(NULL) is safe
free(left);
return result;
}
int main(void) {
return process(100);
}Note
goto in C. It avoids duplicating release logic across many early-error branches.Failure patterns
| Bug | What happened | Prevention |
|---|---|---|
| Leak | The final pointer to live allocated storage was lost | One owner and cleanup on every exit |
| Double-free | The same live allocation was released twice | Transfer ownership explicitly |
| Use-after-free | Code dereferenced a pointer after lifetime ended | Invalidate borrows; structure cleanup |
| Invalid free | Pointer was not returned by an allocation function | Free only owned allocation bases |
| Size overflow | Byte calculation wrapped before allocation | Check count before multiplication |
Build a dynamic array
A dynamic array tracks three facts: its element pointer, current size, and allocated capacity. Growing geometrically makes repeated append operations efficient.
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
size_t size;
size_t capacity;
} IntVector;
bool push(IntVector *vector, int value) {
if (vector->size == vector->capacity) {
size_t next = vector->capacity == 0 ? 4 : vector->capacity * 2;
if (next > SIZE_MAX / sizeof *vector->data) return false;
int *grown = realloc(vector->data, next * sizeof *vector->data);
if (grown == NULL) return false;
vector->data = grown;
vector->capacity = next;
}
vector->data[vector->size++] = value;
return true;
}
int main(void) {
IntVector values = {0};
for (int value = 10; value <= 50; value += 10) {
if (!push(&values, value)) {
free(values.data);
return 1;
}
}
printf("size=%zu capacity=%zu last=%d\n",
values.size, values.capacity, values.data[values.size - 1]);
free(values.data);
return 0;
}Key idea
size counts constructed values; capacity counts available slots. Code may access only indexes below size, even when more storage has been allocated.Recap & quick check
Key takeaways
- Allocated storage lives from a successful allocation until free, independent of block scope.
- Check allocation failure and multiplication overflow before using storage.
- Use a temporary pointer with realloc so failure cannot lose the original allocation.
- Ownership identifies who must release a resource; borrowed pointers must not outlive it.
- Dynamic arrays separate logical size from allocated capacity and grow geometrically.
Quick check
1. What are malloc's newly allocated bytes initialized to?
2. What remains true when realloc returns NULL?
3. What does free(NULL) do?
4. Who should call free?
Phase 2 complete. Phase 3 begins with Module 11 — Structs, Unions, Enums & typedef, where related values become expressive domain types.