Advanced Topics
Dynamic Memory
Master C Dynamic Memory — static vs dynamic memory, malloc / calloc / realloc / free, malloc vs calloc, and dynamic arrays — using LEARNCODEGURU’s 6-level learning model.
Level 1 Basics Content
C Dynamic Memory Overview
Memory in a C program is obtained in two ways. Static (compile-time) allocation fixes the size before the program runs. Dynamic (run-time) allocation asks the operating system for memory while the program is running. This level covers static memory, dynamic memory, a static vs dynamic comparison, implementing dynamic memory in C, malloc vs calloc, and dynamic arrays.
Four library functions do all the work, all declared in <stdlib.h>:
malloc, calloc, realloc, and free.
malloc or calloc What is static memory?
Static memory allocation means the amount of memory is decided at compile time and cannot change while the program runs. Ordinary variables, fixed-size arrays, and globals all work this way.
int x; /* one int — size fixed at compile time */
int a[100]; /* always 100 ints, even if you use 3 */
char name[50]; /* always 50 bytes */
- Size known before running — the compiler reserves it
- Automatic cleanup — locals disappear when the block ends
- Lives on the stack (locals) or in the data segment (globals/statics)
- Cannot resize —
a[100]can never becomea[200]
int a[100] and the user needs 500
values, your program fails. If they need 3, you wasted 97 slots. Static sizing forces you to
guess.
What is dynamic memory?
Dynamic memory allocation means requesting memory at run time, after you know how much you actually need. The memory comes from a region called the heap and stays yours until you explicitly release it.
#include <stdlib.h>
int n;
scanf("%d", &n); /* size decided by the user */
int *a = malloc(n * sizeof(*a));
if (a == NULL) return 1; /* allocation can fail */
/* ... use a[0] .. a[n-1] ... */
free(a); /* you must release it */
a = NULL;
- Size decided while running — exactly what you need
- Lives on the heap — survives until
free - Accessed only through a pointer
- Manual cleanup — forgetting
freecauses a memory leak - Can be resized with
realloc
Static vs dynamic memory
| # | Aspect | Static allocation | Dynamic allocation |
|---|---|---|---|
| 1 | When size is decided | Compile time | Run time |
| 2 | Memory region | Stack / data segment | Heap |
| 3 | Declared using | int a[100]; | malloc / calloc / realloc |
| 4 | Can be resized? | No | Yes, with realloc |
| 5 | Deallocation | Automatic (scope end) | Manual — you call free |
| 6 | Lifetime | Block or whole program | Until free is called |
| 7 | Access | Variable name or index | Only through a pointer |
| 8 | Speed | Faster (simple stack move) | Slower (heap bookkeeping) |
| 9 | Memory efficiency | May waste or fall short | Allocate exactly what is needed |
| 10 | Typical risk | Fixed limit / stack overflow | Memory leaks, dangling pointers |
| 11 | Can outlive its function? | No (locals die on return) | Yes — return the pointer safely |
Implementing dynamic memory in C
All four functions live in <stdlib.h>:
| # | Function | Prototype | Purpose | Returns |
|---|---|---|---|---|
| 1 | malloc | void *malloc(size_t size); | Allocate one block of size bytes | Pointer, or NULL on failure |
| 2 | calloc | void *calloc(size_t n, size_t size); | Allocate n items and zero them | Pointer, or NULL on failure |
| 3 | realloc | void *realloc(void *p, size_t size); | Resize an existing block | New pointer, or NULL on failure |
| 4 | free | void free(void *p); | Release a block back to the heap | Nothing |
malloc — allocate raw bytes:
int *p = malloc(5 * sizeof(*p)); /* room for 5 ints */
if (p == NULL) {
printf("allocation failed\n");
return 1;
}
/* contents are garbage — write before reading */
calloc — allocate and zero:
int *q = calloc(5, sizeof(*q)); /* 5 ints, all 0 */
realloc — grow or shrink:
int *tmp = realloc(p, 10 * sizeof(*p));
if (tmp != NULL)
p = tmp; /* never assign realloc directly to p */
free — release it:
free(p);
p = NULL; /* avoid a dangling pointer */
malloc / calloc /
realloc needs exactly one matching free.
calloc() vs malloc()
| # | Aspect | malloc() | calloc() |
|---|---|---|---|
| 1 | Full form | Memory allocation | Contiguous allocation |
| 2 | Number of arguments | One — total bytes | Two — count and item size |
| 3 | Syntax | malloc(n * sizeof(int)) | calloc(n, sizeof(int)) |
| 4 | Initialization | None — garbage values | All bytes set to 0 |
| 5 | Speed | Faster (no zeroing) | Slightly slower (zeroing cost) |
| 6 | Safe to read before writing? | No — undefined behavior | Yes — reads as 0 |
| 7 | Returns on failure | NULL | NULL |
| 8 | Released with | free() | free() |
| 9 | Best used when | You will fill every element yourself | Zero is the correct starting value |
/* These two request the same amount of memory */
int *a = malloc(10 * sizeof(int)); /* values: garbage */
int *b = calloc(10, sizeof(int)); /* values: all 0 */
calloc saves you a loop.
Dynamic arrays — 1D
A dynamic array is just a pointer to a heap block treated as consecutive elements. You index it exactly like a normal array.
int n, i;
int *a;
printf("How many elements? ");
scanf("%d", &n);
a = malloc(n * sizeof(*a));
if (a == NULL) return 1;
for (i = 0; i < n; i++)
scanf("%d", &a[i]); /* a[i] works normally */
free(a);
a = NULL;
Growing it later with realloc:
int *tmp = realloc(a, (n * 2) * sizeof(*a));
if (tmp == NULL) {
/* old block still valid — handle the error */
} else {
a = tmp;
n = n * 2; /* existing values are preserved */
}
sizeof(a) gives the pointer size, not the array
size. You must track n yourself.
Dynamic arrays — 2D
Method A — array of row pointers (int **):
int **m;
int i, rows = 3, cols = 4;
m = malloc(rows * sizeof(*m)); /* array of row pointers */
for (i = 0; i < rows; i++)
m[i] = malloc(cols * sizeof(**m)); /* each row */
m[1][2] = 99; /* normal 2D syntax */
/* free in reverse order: rows first, then the outer array */
for (i = 0; i < rows; i++)
free(m[i]);
free(m);
Method B — one flat contiguous block:
int *m = malloc(rows * cols * sizeof(*m));
m[1 * cols + 2] = 99; /* row 1, column 2 */
free(m); /* one allocation, one free */
| Aspect | Method A (int **) | Method B (flat block) |
|---|---|---|
| Allocations | rows + 1 | 1 |
| Indexing | m[i][j] | m[i * cols + j] |
| Contiguous memory? | No — rows scattered | Yes |
| Freeing | Loop, then outer free | Single free |
| Rows of different lengths? | Possible (jagged) | No |
Sample program — dynamic array with malloc and free
/* Dynamic memory basics: allocate, check, use, free */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *a;
int n, i, sum = 0;
printf("How many integers? ");
scanf("%d", &n);
if (n <= 0) {
printf("n must be positive\n");
return 1;
}
a = malloc(n * sizeof(*a));
if (a == NULL) {
printf("Out of memory\n");
return 1;
}
printf("Enter %d integers:\n", n);
for (i = 0; i < n; i++) {
scanf("%d", &a[i]);
sum += a[i];
}
printf("Array: ");
for (i = 0; i < n; i++)
printf("%d ", a[i]);
printf("\nSum = %d\n", sum);
free(a);
a = NULL;
return 0;
}
Level 2 Advanced Content
Going deeper into dynamic memory
Level 1 covered static vs dynamic, the four functions, malloc vs calloc, and dynamic arrays.
Level 2 explains memory segments, realloc in detail,
memory leaks, dangling and double-free bugs, allocating
strings and structs, returning heap memory from functions, and common mistakes.
Stack vs heap — where memory comes from
| Aspect | Stack | Heap |
|---|---|---|
| Holds | Locals, parameters, return addresses | Blocks from malloc family |
| Managed by | Compiler automatically | You, explicitly |
| Allocation cost | Very cheap (move a pointer) | Higher (search / bookkeeping) |
| Size limit | Small (often 1–8 MB) | Large (most of available RAM) |
| Lifetime | Until the block exits | Until free |
| Failure mode | Stack overflow | malloc returns NULL, leaks |
int a[1000000]; as a local may
overflow the stack, while malloc handles it comfortably.
realloc in detail
- Existing contents are preserved up to the smaller of the old and new sizes
- When growing, the extra bytes are uninitialized (not zeroed)
- The block may be moved — the old pointer becomes invalid
realloc(NULL, n)behaves likemalloc(n)- On failure it returns
NULLand the original block is untouched
/* WRONG — leaks the original block if realloc fails */
p = realloc(p, newSize);
/* RIGHT — keep the old pointer until success is confirmed */
int *tmp = realloc(p, newSize);
if (tmp == NULL) {
/* p is still valid; recover or exit */
} else {
p = tmp;
}
Memory leaks
A leak happens when you lose the last pointer to a block you never freed. The memory stays reserved for the life of the process.
int *p = malloc(100 * sizeof(*p));
p = malloc(200 * sizeof(*p)); /* LEAK: first block is unreachable */
/* Also a leak: returning early without freeing */
if (error)
return 1; /* forgot free(p) */
- Free before overwriting a pointer that owns memory
- Free on every exit path, including error returns
- In a loop, a small leak per iteration becomes a large one quickly
Dangling pointers, double free, and use-after-free
| Bug | What happens | Fix |
|---|---|---|
| Dangling pointer | p still holds a freed address | p = NULL; right after free |
| Use-after-free | Reading/writing freed memory — undefined | Never touch p after freeing |
| Double free | free(p) twice — undefined, often aborts | Set to NULL; free(NULL) is safe |
| Freeing a non-heap pointer | free on a stack array — undefined | Only free what the malloc family returned |
| Freeing a shifted pointer | free(p + 1) — undefined | Free the exact original address |
free(p);
p = NULL; /* now free(p) again is harmless, and if (p) works */
Allocating strings and structs
/* String buffer: remember +1 for '\0' */
char *s = malloc((len + 1) * sizeof(*s));
if (s != NULL) {
strcpy(s, "hello");
free(s);
}
/* One struct */
struct Point { int x, y; };
struct Point *pt = malloc(sizeof(*pt));
pt->x = 3;
pt->y = 4;
free(pt);
/* Array of structs */
struct Point *pts = calloc(n, sizeof(*pts));
strlen(text) + 1 bytes. Forgetting the
terminator byte is the most common heap-string bug.
Returning heap memory from a function
Unlike a local array, heap memory outlives the function — so returning the pointer is safe. The caller then owns it and must free it.
/* SAFE — heap block survives the return */
int *makeArray(int n) {
int *a = calloc(n, sizeof(*a));
return a; /* caller must free it */
}
/* UNSAFE — local dies at return, pointer dangles */
int *bad(void) {
int a[10];
return a; /* undefined behavior */
}
free.
Common mistakes
| Mistake | Why it hurts | Fix |
|---|---|---|
Not checking for NULL | Crash when memory runs out | Test the return value every time |
Forgetting free | Memory leak grows over time | Pair each allocation with a free |
Using memory after free | Undefined behavior, silent corruption | Set the pointer to NULL |
| Double free | Heap corruption or abort | NULL after freeing |
Reading malloc memory before writing | Garbage values / undefined | Use calloc or initialize first |
Wrong size — forgetting * n | Buffer overflow past the block | n * sizeof(*p) |
Missing <stdlib.h> | Implicit declaration bugs | Always include it |
p = realloc(p, n) directly | Leaks the old block on failure | Use a temporary pointer |
Freeing only the outer pointer of int ** | Every row leaks | Free rows first, then the outer array |
Level 3 C Tricky Interview Points
Advanced tricky interview points on malloc vs calloc, NULL checks, realloc semantics, leaks, dangling pointers, and dynamic 2D arrays.
Tricky Interview Point-1:
malloc does not initialize memory; calloc zeroes it
Memory from malloc holds indeterminate bytes — reading it before writing is undefined behavior. calloc sets every byte to 0, which is why it is slightly slower but safer for arrays you intend to accumulate into.
Tricky Interview Point-2:
Always check the return value — malloc can return NULL
On allocation failure malloc/calloc/realloc return NULL. Dereferencing that is a crash. Write if (p == NULL) { /* handle */ } immediately after every allocation.
Tricky Interview Point-3:
sizeof(*p) is safer than repeating the type name
int *p = malloc(n * sizeof(*p)); stays correct even if you later change p to long *. Writing sizeof(int) twice invites a mismatch when the type changes.
Tricky Interview Point-4:
Casting malloc's return is unnecessary in C
malloc returns void *, which converts implicitly to any object pointer type. The cast is required in C++ but in C it can hide a missing <stdlib.h> include, which used to cause real bugs.
Tricky Interview Point-5:
free(NULL) is safe and does nothing
The standard guarantees it, so you do not need if (p != NULL) free(p);. What is NOT safe is freeing the same pointer twice — double free is undefined behavior.
Tricky Interview Point-6:
free does not change your pointer — it becomes dangling
After free(p), p still holds the old address. Using it is use-after-free. Set p = NULL; immediately so later checks and accidental uses fail loudly instead of silently corrupting memory.
Tricky Interview Point-7:
You must free the exact pointer malloc returned
If you advance a pointer (p++ while walking an array), you cannot free the advanced value. Keep the original address in a separate variable to pass to free.
Tricky Interview Point-8:
realloc may move the block and invalidate the old pointer
Assign to a temporary: tmp = realloc(p, newsize); if (tmp) p = tmp;. Writing p = realloc(p, n) leaks the original block when realloc returns NULL.
Tricky Interview Point-9:
realloc(NULL, n) behaves like malloc(n); realloc(p, 0) is implementation-defined
The first form is a documented convenience. The second may free the block and return NULL, or return a unique pointer — do not rely on it; call free explicitly instead.
Tricky Interview Point-10:
Heap memory lives until you free it — not until the function returns
This is why a function can allocate a buffer and return the pointer safely, unlike returning the address of a local array. The trade-off is that someone must remember to free it.
Tricky Interview Point-11:
A memory leak is losing the last pointer to a live block
Overwriting p without freeing the old block leaks it. The memory stays reserved until the process exits. Long-running programs die from accumulated leaks.
Tricky Interview Point-12:
VLAs are stack-allocated, not dynamic memory
int a[n]; (C99) sizes at runtime but lives on the stack and vanishes at block exit — you cannot free it or return it. Large n risks stack overflow, which is why malloc is preferred for big data.
Tricky Interview Point-13:
A dynamic 2D array via int ** is not contiguous
An array of row pointers means rows can sit anywhere in the heap, so a single memcpy across the whole matrix is invalid. A single malloc(rows*cols*sizeof(int)) with manual indexing gives one contiguous block.
Tricky Interview Point-14:
Freeing a 2D int ** array requires a loop, in reverse order
Free each row first, then the array of pointers. Freeing the outer pointer first loses the row addresses and leaks every row.
Level 4 Simple Programs
Short programs covering malloc, calloc comparison, realloc growth, dynamic strings, and dynamic 2D matrices.
Program 1 — malloc vs calloc side by side
/* calloc gives zeros; malloc gives garbage */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 5, i;
int *a = malloc(n * sizeof(*a));
int *b = calloc(n, sizeof(*b));
if (a == NULL || b == NULL) {
printf("allocation failed\n");
free(a);
free(b);
return 1;
}
printf("calloc values (guaranteed 0): ");
for (i = 0; i < n; i++)
printf("%d ", b[i]);
printf("\n");
/* malloc memory must be written before it is read */
for (i = 0; i < n; i++)
a[i] = i * 10;
printf("malloc values (after writing): ");
for (i = 0; i < n; i++)
printf("%d ", a[i]);
printf("\n");
free(a);
free(b);
return 0;
}
Program 2 — Growing an array with realloc
/* Existing values survive the resize */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 3, i;
int *a = malloc(n * sizeof(*a));
int *tmp;
if (a == NULL) return 1;
for (i = 0; i < n; i++)
a[i] = i + 1; /* 1 2 3 */
tmp = realloc(a, 6 * sizeof(*a));
if (tmp == NULL) {
printf("resize failed\n");
free(a);
return 1;
}
a = tmp;
for (i = 3; i < 6; i++)
a[i] = (i + 1) * 10; /* new slots */
printf("After realloc: ");
for (i = 0; i < 6; i++)
printf("%d ", a[i]);
printf("\n");
free(a);
return 0;
}
Program 3 — Dynamic 2D matrix with int **
/* Allocate rows, then each row; free in reverse */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int **m;
int rows, cols, i, j;
printf("Enter rows and cols: ");
scanf("%d %d", &rows, &cols);
m = malloc(rows * sizeof(*m));
if (m == NULL) return 1;
for (i = 0; i < rows; i++) {
m[i] = malloc(cols * sizeof(**m));
if (m[i] == NULL) return 1;
}
printf("Enter %d elements:\n", rows * cols);
for (i = 0; i < rows; i++)
for (j = 0; j < cols; j++)
scanf("%d", &m[i][j]);
printf("Matrix:\n");
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++)
printf("%4d", m[i][j]);
printf("\n");
}
for (i = 0; i < rows; i++)
free(m[i]); /* rows first */
free(m); /* then the outer array */
return 0;
}
Program 4 — Dynamic 2D matrix as one flat block
/* One malloc, one free, contiguous memory */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *m;
int rows = 2, cols = 3, i, j;
m = calloc(rows * cols, sizeof(*m));
if (m == NULL) return 1;
for (i = 0; i < rows; i++)
for (j = 0; j < cols; j++)
m[i * cols + j] = (i + 1) * (j + 1);
printf("Matrix:\n");
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++)
printf("%4d", m[i * cols + j]);
printf("\n");
}
free(m);
return 0;
}
Program 5 — Dynamic string buffer
/* Allocate a line buffer on the heap */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
int cap = 100;
char *line = malloc(cap * sizeof(*line));
size_t len;
if (line == NULL) return 1;
printf("Enter a line: ");
if (fgets(line, cap, stdin) == NULL) {
free(line);
return 1;
}
len = strlen(line);
if (len > 0 && line[len - 1] == '\n')
line[len - 1] = '\0';
printf("You typed: [%s] length = %zu\n", line, strlen(line));
free(line);
line = NULL;
return 0;
}
Level 5 Problem Solving Tricks
How to master dynamic memory while solving problems
Most heap bugs come from unchecked allocations, forgotten frees, using memory after freeing, and wrong size calculations. These habits make dynamic memory routine instead of risky.
Pair every malloc with a free before you write more code
Allocate, then immediately scroll to where the block dies and write the free. Treating them as a matched pair prevents most leaks from ever being created.
Check for NULL on the very next line
p = malloc(...); if (p == NULL) { fprintf(stderr, "out of memory\n"); return 1; } Make it muscle memory so no allocation is ever used unchecked.
Use n * sizeof(*p) as your allocation formula
It reads as "n of whatever p points to" and survives type changes. Combine with a named count variable so the size is obvious to reviewers.
Set the pointer to NULL right after freeing
free(p); p = NULL; converts a silent use-after-free into an immediate, debuggable failure and makes if (p) checks meaningful later.
Keep the original pointer when traversing
Walk with a second pointer (int *q = p;) so p always holds the address free needs. Never increment the pointer you plan to release.
Use a temporary for realloc
tmp = realloc(p, newSize); if (tmp == NULL) { /* keep old p, handle error */ } else p = tmp;. This is the only leak-free way to grow a block.
Prefer calloc when zeros are the correct starting value
For counters, sums, or flags, calloc removes an entire initialization loop and eliminates the risk of reading uninitialized bytes.
For a dynamic matrix, consider one flat block
int *m = malloc(rows * cols * sizeof(*m)); then index m[i * cols + j]. One allocation, one free, contiguous memory, and no pointer-array bookkeeping.
Quick success checklist
- Before run:
<stdlib.h>included? NULL checked? size isn * sizeof(*p)? - Debug order: print
nand the pointer → verify every index < n → confirm one free per allocation - Cleanup habit: free on every exit path →
p = NULLafter free → rows before outer array forint **
Level 6 Practice Programs List
Programming questions only — focused on dynamic memory: malloc, calloc,
realloc, free, dynamic 1D and 2D arrays, and safe cleanup.
- 1. Write a C program that allocates memory for one integer using malloc, stores a value, prints it, and frees the memory.
- 2. Write a C program that reads n from the user, allocates a dynamic array of n integers with malloc, reads the values, and prints their sum.
- 3. Write a C program that does the same task with calloc and prints the array before reading input to show that it starts as all zeros.
- 4. Write a C program that allocates an array of n integers, then uses realloc to grow it to 2n and fills the new positions.
- 5. Write a C program that allocates a dynamic string buffer with malloc, reads a line with fgets, and prints its length.
- 6. Write a C program that creates a dynamic 2D matrix using an array of row pointers (int **), reads it, prints it, and frees it correctly.
- 7. Write a C program that creates a dynamic 2D matrix as a single flat block and accesses elements using m[i * cols + j].
- 8. Write a C program that allocates memory, checks for NULL, and prints an error message if the allocation fails.
- 9. Write a C program that demonstrates a dangling pointer by freeing memory and then setting the pointer to NULL as the fix.
- 10. Write a C program with a function that allocates and returns an array of n integers, with main responsible for freeing it.