Lesson 9 of 12
Dynamic Memory
Learn to allocate at runtime with malloc, so data can outlive a and be sized on the fly, and how to free it again when you are done.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
/* malloc: size in BYTES, returns void* (or NULL if it fails) */
int *nums = malloc(5 * sizeof *nums);
if (nums == NULL) { /* ALWAYS check */
perror("malloc");
return 1;
}
for (int i = 0; i < 5; i++) nums[i] = i * i;
/* calloc: count + size, and zeroes the memory */
int *zeros = calloc(5, sizeof *zeros);
/* realloc: grow or shrink. Assign to a TEMP so you do not leak on failure. */
int *bigger = realloc(nums, 10 * sizeof *nums);
if (bigger == NULL) { free(nums); return 1; }
nums = bigger;
for (int i = 5; i < 10; i++) nums[i] = i * i;
printf("%d %d\n", nums[3], nums[9]); /* 9 81 */
free(nums);
free(zeros);
nums = NULL; /* defensive: a freed pointer that is NULL cannot be reused */
return 0;
}#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* A growable array — the data structure C does not give you. */
typedef struct {
int *items;
size_t length;
size_t capacity;
} IntVec;
int vec_init(IntVec *v, size_t capacity) {
v->items = malloc(capacity * sizeof *v->items);
if (!v->items) return -1;
v->length = 0;
v->capacity = capacity;
return 0;
}
int vec_push(IntVec *v, int value) {
if (v->length == v->capacity) {
size_t new_cap = v->capacity * 2;
int *grown = realloc(v->items, new_cap * sizeof *v->items);
if (!grown) return -1; /* old buffer still valid */
v->items = grown;
v->capacity = new_cap;
}
v->items[v->length++] = value;
return 0;
}
void vec_free(IntVec *v) {
free(v->items);
v->items = NULL;
v->length = v->capacity = 0;
}
int main(void) {
IntVec v;
if (vec_init(&v, 2) != 0) return 1;
for (int i = 0; i < 10; i++) vec_push(&v, i);
printf("%zu items, capacity %zu, last=%d\n", v.length, v.capacity, v.items[9]);
vec_free(&v);
return 0;
}#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Strings on the heap: remember the +1 for the terminating '\0'. */
char *duplicate(const char *src) {
size_t len = strlen(src);
char *copy = malloc(len + 1);
if (!copy) return NULL;
memcpy(copy, src, len + 1); /* copies the '\0' too */
return copy;
}
char *join(const char *a, const char *b, char sep) {
size_t la = strlen(a), lb = strlen(b);
char *out = malloc(la + lb + 2);
if (!out) return NULL;
memcpy(out, a, la);
out[la] = sep;
memcpy(out + la + 1, b, lb + 1);
return out;
}
int main(void) {
char *name = duplicate("Ada Lovelace");
char *path = join("usr", "local", '/');
printf("%s | %s\n", name, path);
/* Whoever allocates must document who frees. Here, the caller. */
free(name);
free(path);
return 0;
}Common question
Is forgetting to free memory a problem in a short program?
Local live on the and vanish when their ends. Anything that has to outlive its scope, or whose size is not known at compile time, is allocated on the heap with malloc or calloc and handed back with free. You decide both moments, which is what makes C fast and what makes it dangerous.
Use rules that make the bookkeeping visible: allocate and free in the same layer, document who owns each pointer, check every allocation, free exactly once and the pointer to afterwards. Run your tests under a sanitiser; a tool finds leaks and use-after-free in seconds, where a person might take days.
Source: The C standard says malloc returns "either a null pointer or a pointer to the allocated space", and the space stays allocated until the program calls free.
Common mix-ups
Each of these sounds reasonable. Think about why it is wrong, then open the explanation.
The reclaims everything when the process exits, so leaks in a short program are harmless.
Freeing memory twice is harmless because the memory is already gone.
malloc always succeeds on a modern machine.
Tip: use the ← and → keys to move between lessons.