Motivation for Runtime Allocation
Automatic variables and fixed-size arrays live on the stack and their sizes are frozen at compile time. When the required amount of data is not known until the program is running, the heap offers a flexible alternative.
Core Allocation Routines
All prototypes live in <stdlib.h>.
malloc
void *malloc(size_t bytes);
Requests a contiguous block of at least bytes bytes. On success the pointer is suitably aligned for any object type; on failure it returns NULL. The contents are indeterminate.
Example – allocate ten ints, fill and print:
int *values = malloc(10 * sizeof *values);
if (!values) {
perror("malloc");
exit(EXIT_FAILURE);
}
for (size_t i = 0; i < 10; ++i)
values[i] = (int)(i + 1);
for (size_t i = 0; i < 10; ++i)
printf("%d ", values[i]);
free(values);
values = NULL; /* defensive programming */
Passing 0 yields implementation-defined behaviour; do not rely on it.
calloc
void *calloc(size_t count, size_t size);
Allocates space for count objects of size bytes each and zeroes the entire block. Otherwise behaves like malloc.
double *matrix = calloc(4, sizeof *matrix); /* four zeroed doubles */
free(matrix);
realloc
void *realloc(void *ptr, size_t new_size);
- If
ptrisNULL, behaves likemalloc(new_size). - If
new_sizeis zero, behaviour is implementation-defined (acts likefree(ptr)on many systems). - Otherwise it tries to resize the existing block:
- In-place: when the adjacent heap space is free, the same address is returned.
- Move-and-copy: when no room, a new block is allocated, data is copied, the old block is freed, and the new address is returned.
Always capture the return value in a temporary pointer to avoid leaks on failure:
size_t cap = 5;
int *data = malloc(cap * sizeof *data);
...
if (need_more) {
cap *= 2;
int *tmp = realloc(data, cap * sizeof *data);
if (!tmp) {
perror("realloc");
free(data);
exit(EXIT_FAILURE);
}
data = tmp;
}
free
void free(void *ptr);
Releases a block previously obtained from malloc, calloc, or realloc. Passing NULL is harmless; anything else invokes undefined behaviour.
Typical Heap Mistakes
| Fault | Minimal Example | Remedy |
|---|---|---|
Dereferencing NULL |
int *p = malloc(SIZE_MAX); *p = 3; |
Check result before use. |
| Out-of-bounds write | int *p = malloc(5 * sizeof *p); p[5] = 0; |
Allocate enough bytes, use correct indices. |
free on non-heap pointer |
int x; free(&x); |
Only free heap blocks. |
Partial free |
p += 10; free(p); |
Pass the original base address. |
Double free |
free(p); free(p); |
Set pointer to NULL after first free. |
| Leak | void f(){ malloc(100); } |
Ensure every path frees the block. |
Quiz Walk-through
Question 1
void grab(char *p) {
p = malloc(100);
}
void demo(void) {
char *s = NULL;
grab(s); /* s unchanged */
strcpy(s, "hi"); /* UB */
}
Fix: pass the address of the pointer.
void grab(char **p) { *p = malloc(100); }
...
grab(&s);
...
free(s);
Question 2
Returning the address of a local variable:
char *broken(void) {
char buf[] = "hello";
return buf; /* dangling pointer */
}
Options:
const char *ok(void) {
return "hello"; /* string literal lives in static segment */
}
static char buf[100];
char *also_ok(void) {
strcpy(buf, "hello");
return buf; /* static storage */
}
Question 3
Memory leak variant:
void get(char **p, int n) { *p = malloc(n); }
void run(void) {
char *s = NULL;
get(&s, 100);
puts("leaked"); /* forgot free */
}
Question 4
Use-after-free:
char *s = malloc(100);
free(s);
if (s) strcpy(s, "oops"); /* UB */
Nullify the pointer after freeing.
Process Memory Map
| Segment | Storage Duration | Typical Contents |
|---|---|---|
| Stack | automatic | local variables, return addresses |
| Heap | explicit | blocks from malloc family |
| Data / BSS | program lifetime | globals, static locals |
| Code / Text | program lifetime | machine instructions, read-only constants |
int g = 1; /* Data */
static int sg = 2; /* Data */
void f(void) {
static int s = 3; /* Data */
int l = 4; /* Stack */
int *h = malloc(sizeof *h); /* Heap */
free(h);
}
Flexible Array Members (C99)
A structure may end with an array of unknown size:
typedef struct msg {
uint32_t len;
char payload[]; /* flexible */
} msg_t;
sizeof(msg_t)ignores the flexible member.- At least one other member must precede it.
- Allocate extra bytes for the tail:
msg_t *m = malloc(sizeof *m + needed);
if (!m) { perror("malloc"); exit(EXIT_FAILURE); }
m->len = needed;
/* use m->payload[i] ... */
free(m);
Advantages over a separate pointer:
- One
malloc/ onefreesimplifies ownership. - Contiguous layout improves locality and reduces fragmentation.
Alternative with pointer:
typedef struct msg_alt {
uint32_t len;
char *payload;
} msg_alt_t;
msg_alt_t *m = malloc(sizeof *m);
m->payload = malloc(needed);
...
free(m->payload);
free(m);
Requires two allocations and two releases, and the two heap blocks may be far apart.
Always pair every successful allocation with a matching free, guard against NULL, and never access memory after it has been released.