⚡ Dynamic Memory — The Core Idea
Stack memory is fast but fixed-size and automatically reclaimed. Heap memory lets you request exactly how many bytes you need at runtime and keep them alive as long as you want — you control when they are freed. The four heap functions live in <stdlib.h>:
n bytes. Contents are uninitialised (garbage). Returns NULL on failure. Always check.n × size bytes, zero-filled. Safer default than malloc when you need clean memory.n bytes. May move to a new address. Old data is preserved up to the smaller of old/new size.free, set the pointer to NULL — accessing freed memory is undefined behaviour.📐 sizeof and typeof
sizeof(type) returns the number of bytes a type occupies at compile time — sizeof(int) is typically 4, sizeof(double) is 8. Always use sizeof instead of hard-coded byte counts — it makes your code portable across 32-bit and 64-bit systems and across different struct layouts.
The idiomatic malloc pattern is T *p = malloc(n * sizeof *p) — using sizeof *p (size of what the pointer points to) rather than sizeof(T). This way, if you ever change the type of p, the malloc automatically stays correct without a separate edit.
typeof(expr) is a GCC/Clang extension (standardised in C23) that yields the type of an expression. It is most useful in macros: #define SWAP(a,b) do { typeof(a) _t=(a);(a)=(b);(b)=_t; } while(0) — works for any type without needing a separate type argument.
vector or Java ArrayList. The key ingredients: a capacity counter (slots allocated), a size counter (elements actually used), and a realloc call whenever size == capacity. We use sizeof *arr throughout so the code works even if you change the element type. After every realloc, we verify the return value is not NULL before replacing the old pointer — otherwise a failed realloc would leak the original block.
#include <stdio.h> #include <stdlib.h> /* ── typeof-style SWAP macro — works for any type ── */ #define SWAP(a, b) do { \ __typeof__(a) _t = (a); \ (a) = (b); (b) = _t; \ } while (0) /* ── Dynamic array descriptor ── */ typedef struct { int *data; /* pointer to heap block */ int size; /* elements currently stored */ int capacity; /* slots allocated on heap */ } DynArr; /* ── Initialise with starting capacity ── */ void da_init(DynArr *da, int cap) { da->data = malloc(cap * sizeof *da->data); if (!da->data) { fprintf(stderr, "malloc failed\n"); exit(1); } da->size = 0; da->capacity = cap; printf(" init: capacity=%d heap bytes=%zu\n", cap, cap * sizeof *da->data); } /* ── Append — doubles capacity when full ── */ void da_push(DynArr *da, int val) { if (da->size == da->capacity) { int newCap = da->capacity * 2; int *tmp = realloc(da->data, newCap * sizeof *da->data); if (!tmp) { fprintf(stderr, "realloc failed\n"); exit(1); } da->data = tmp; da->capacity = newCap; printf(" GROW capacity %d -> %d (realloc)\n", da->capacity / 2, newCap); } da->data[da->size++] = val; } /* ── Pop last element ── */ int da_pop(DynArr *da) { if (da->size == 0) { printf(" pop: empty!\n"); return -1; } return da->data[--da->size]; } /* ── Sort using pointer walk + SWAP macro ── */ void da_sort(DynArr *da) { for (int i = 0; i < da->size - 1; i++) for (int j = 0; j < da->size - i - 1; j++) if (da->data[j] > da->data[j+1]) SWAP(da->data[j], da->data[j+1]); } /* ── Print contents ── */ void da_print(const DynArr *da, const char *label) { printf(" %-14s[ ", label); for (int *p = da->data; p < da->data + da->size; p++) printf("%d ", *p); printf("] size=%d cap=%d\n", da->size, da->capacity); } /* ── Free heap memory ── */ void da_free(DynArr *da) { free(da->data); da->data = NULL; da->size = da->capacity = 0; printf(" free: heap block released, pointer nulled\n"); } int main() { DynArr da; printf("=== Dynamic Array (start cap=4) ===\n"); da_init(&da, 4); printf("\n--- Push 10 elements (watch it grow) ---\n"); int vals[] = { 42, 17, 8, 95, 33, 61, 4, 78, 50, 29 }; for (int i = 0; i < 10; i++) da_push(&da, vals[i]); da_print(&da, "After push:"); printf("\n--- sizeof info ---\n"); printf(" sizeof(int) = %zu bytes\n", sizeof(int)); printf(" sizeof(*da.data) = %zu bytes\n", sizeof *da.data); printf(" sizeof(DynArr) = %zu bytes\n", sizeof(DynArr)); printf(" heap used by data = %zu bytes\n", da.capacity * sizeof *da.data); printf("\n--- Sort + pop 2 ---\n"); da_sort(&da); da_print(&da, "Sorted:"); printf(" pop() -> %d\n", da_pop(&da)); printf(" pop() -> %d\n", da_pop(&da)); da_print(&da, "After pop:"); printf("\n--- Cleanup ---\n"); da_free(&da); return 0; }
=== Dynamic Array (start cap=4) === init: capacity=4 heap bytes=16 --- Push 10 elements (watch it grow) --- GROW capacity 4 -> 8 (realloc) GROW capacity 8 -> 16 (realloc) After push: [ 42 17 8 95 33 61 4 78 50 29 ] size=10 cap=16 --- sizeof info --- sizeof(int) = 4 bytes sizeof(*da.data) = 4 bytes sizeof(DynArr) = 16 bytes heap used by data = 64 bytes --- Sort + pop 2 --- Sorted: [ 4 8 17 29 33 42 50 61 78 95 ] size=10 cap=16 pop() -> 95 pop() -> 78 After pop: [ 4 8 17 29 33 42 50 61 ] size=8 cap=16 --- Cleanup --- free: heap block released, pointer nulled
int *tmp = realloc(da->data, ...); if (!tmp) { /* handle */ } da->data = tmp;. If you write da->data = realloc(da->data, ...) and realloc returns NULL, you've lost the original pointer — that's a memory leak with no recovery.sizeof *ptr is safer than sizeof(Type). Writing malloc(n * sizeof *da->data) means if you ever change data from int* to long*, the malloc size updates automatically. With sizeof(int), you'd have to hunt down every malloc call manually.Student struct, then malloc a block large enough for N students: Student *db = malloc(n * sizeof *db). Each element is accessed exactly like a stack array — db[i].name, db[i].marks. When we need more room, realloc resizes the block without changing how we access elements. The program demonstrates adding students, searching by roll number using a pointer walk, sorting by marks with a pointer-based comparison, and computing statistics — then frees everything cleanly.
#include <stdio.h> #include <stdlib.h> #include <string.h> /* ── Student struct ── */ typedef struct { int roll; char name[30]; float marks; char grade; } Student; /* ── Database: array of Students on the heap ── */ typedef struct { Student *data; int count; int cap; } StudentDB; /* assign grade from marks */ char calcGrade(float m) { if (m >= 90) return 'A'; if (m >= 75) return 'B'; if (m >= 60) return 'C'; if (m >= 45) return 'D'; return 'F'; } /* init DB with initial capacity */ void db_init(StudentDB *db, int cap) { db->data = malloc(cap * sizeof *db->data); if (!db->data) { fprintf(stderr,"malloc failed\n"); exit(1); } db->count = 0; db->cap = cap; printf(" DB init: cap=%d sizeof(Student)=%zu " "heap=%zu bytes\n", cap, sizeof(Student), cap * sizeof(Student)); } /* add student — realloc if full */ void db_add(StudentDB *db, int roll, const char *name, float marks) { if (db->count == db->cap) { int nc = db->cap * 2; Student *tmp = realloc(db->data, nc * sizeof *db->data); if (!tmp) { fprintf(stderr,"realloc failed\n"); exit(1); } db->data = tmp; db->cap = nc; printf(" GROW DB cap -> %d\n", nc); } Student *s = &db->data[db->count++]; s->roll = roll; s->marks = marks; s->grade = calcGrade(marks); strncpy(s->name, name, 29); } /* search by roll using pointer walk */ Student* db_find(StudentDB *db, int roll) { Student *end = db->data + db->count; for (Student *p = db->data; p < end; p++) if (p->roll == roll) return p; return NULL; } /* sort by marks descending (bubble via pointer) */ void db_sort(StudentDB *db) { int n = db->count; for (int i = 0; i < n-1; i++) for (Student *p = db->data; p < db->data + n-i-1; p++) if (p->marks < (p+1)->marks) { Student tmp = *p; *p = *(p+1); *(p+1) = tmp; } } /* print full roster */ void db_print(const StudentDB *db) { printf(" %-5s %-14s %6s Grade\n", "Roll", "Name", "Marks"); printf(" %s\n", "-------------------------------"); for (int i = 0; i < db->count; i++) { Student *s = &db->data[i]; printf(" %-5d %-14s %6.1f %c\n", s->roll, s->name, s->marks, s->grade); } } /* stats using pointer walk */ void db_stats(const StudentDB *db) { float sum = 0, best = db->data[0].marks; Student *top = db->data; Student *end = db->data + db->count; for (Student *p = db->data; p < end; p++) { sum += p->marks; if (p->marks > best) { best = p->marks; top = p; } } printf(" Average : %.2f\n", sum / db->count); printf(" Topper : %s (%.1f)\n", top->name, top->marks); } int main() { StudentDB db; printf("=== Student Database (Dynamic Struct Array) ===\n\n"); db_init(&db, 3); printf("\n--- Add 6 students (triggers realloc at 4th) ---\n"); db_add(&db, 101, "Ananya", 88.5f); db_add(&db, 102, "Rohan", 74.0f); db_add(&db, 103, "Priya", 95.5f); db_add(&db, 104, "Karan", 61.0f); db_add(&db, 105, "Sunita", 82.5f); db_add(&db, 106, "Arjun", 47.0f); printf("\n--- Roster (insertion order) ---\n"); db_print(&db); printf("\n--- Search roll=104 ---\n"); Student *found = db_find(&db, 104); if (found) printf(" Found: %s marks=%.1f grade=%c" " addr=%p\n", found->name, found->marks, found->grade, (void*)found); printf("\n--- Sort by marks (descending) ---\n"); db_sort(&db); db_print(&db); printf("\n--- Statistics ---\n"); db_stats(&db); printf("\n--- sizeof breakdown ---\n"); printf(" sizeof(Student) = %zu bytes\n", sizeof(Student)); printf(" heap used = %zu bytes (%d slots)\n", db.cap * sizeof(Student), db.cap); printf(" data actually = %zu bytes (%d students)\n", db.count * sizeof(Student), db.count); free(db.data); db.data = NULL; printf("\n free(db.data) — done.\n"); return 0; }
=== Student Database (Dynamic Struct Array) === DB init: cap=3 sizeof(Student)=40 heap=120 bytes --- Add 6 students (triggers realloc at 4th) --- GROW DB cap -> 6 --- Roster (insertion order) --- Roll Name Marks Grade ------------------------------- 101 Ananya 88.5 B 102 Rohan 74.0 B 103 Priya 95.5 A 104 Karan 61.0 C 105 Sunita 82.5 B 106 Arjun 47.0 D --- Search roll=104 --- Found: Karan marks=61.0 grade=C addr=0x55a3f2c01090 --- Sort by marks (descending) --- Roll Name Marks Grade ------------------------------- 103 Priya 95.5 A 101 Ananya 88.5 B 105 Sunita 82.5 B 102 Rohan 74.0 B 104 Karan 61.0 C 106 Arjun 47.0 D --- Statistics --- Average : 74.75 Topper : Priya (95.5) --- sizeof breakdown --- sizeof(Student) = 40 bytes heap used = 240 bytes (6 slots) data actually = 240 bytes (6 students) free(db.data) — done.
Student tmp = *p; *p = *(p+1); *(p+1) = tmp; copies all 40 bytes of the struct in each swap. For large structs this is slow — a better approach is to sort an array of pointers to structs (Student **) and swap the pointers (8 bytes each) instead of the structs themselves.
free(), always null the pointer. free(db.data); db.data = NULL; — this prevents accidental use-after-free bugs. Calling free(NULL) is safe and does nothing, so nulling the pointer also makes double-free calls harmless.
- malloc / calloc — allocate on the heap. Use
sizeof *ptrnotsizeof(Type). Always check for NULL. calloc zero-fills; malloc leaves garbage. - realloc — always assign to a temp pointer. If realloc returns NULL, the original block is still valid; assigning directly would leak it. Double capacity each time for O(1) amortised append.
- typeof / __typeof__ — yields the type of an expression at compile time. Makes SWAP macros type-generic. Standardised in C23; use
__typeof__for older GCC/Clang. - Struct malloc —
Student *db = malloc(n * sizeof *db). Access exactly like a stack array:db[i].marks. Walk withStudent *p = db; p < db+n; p++— eachp++advances bysizeof(Student)bytes. - free discipline — free every malloc. After free, set pointer to NULL. Never access freed memory. For large structs, sort pointer arrays not struct arrays to avoid copying full structs on each swap.