Dynamic allocation gives a C program storage whose size or lifetime is decided at runtime. malloc and calloc create a block, realloc requests a resized block, and free ends the allocation’s lifetime. The key design question is ownership: exactly one part of the program must know who releases each successful allocation.
After this lesson, you can calculate allocation sizes defensively, handle allocation failure without losing the original pointer, initialize new storage, avoid use-after-free and double-free defects, and design a cleanup path that works after partial failure.
Automatic local objects normally live until execution leaves their block. Dynamically allocated objects live from a successful allocation until free, which allows their size and lifetime to outlast one function call. The C standard does not require a particular stack or heap size; limits come from the implementation and execution environment.
| Feature | Stack | Heap |
|---|---|---|
| Allocation | Automatic (on function call) | Manual (malloc/calloc/realloc) |
| Deallocation | Automatic (on function return) | Manual (free) |
| Limit | Implementation and environment dependent | Available address space and allocator limits |
| Speed | Fast | Slower (OS involvement) |
| Use case | Local variables, function calls | Large/variable-size data, long-lived data |
All dynamic memory functions are in <stdlib.h>:
| Function | Description | Initialization |
|---|---|---|
| malloc(size) | Allocates size bytes | Uninitialized (garbage values) |
| calloc(n, size) | Allocates n x size bytes | Zero-initialized |
| realloc(ptr, size) | Resizes previously allocated block | Preserves existing data |
| free(ptr) | Releases allocated memory | - |
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("Enter number of elements: ");
scanf("%d", &n);
// malloc - allocate n integers (uninitialized)
int *arr = malloc((size_t)n * sizeof *arr);
if (arr == NULL) {
printf("Memory allocation failed!\n");
return 1;
}
// Fill array
for (int i = 0; i < n; i++) {
arr[i] = (i + 1) * 10;
}
// Print array
printf("Array: ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// ALWAYS free when done
free(arr);
arr = NULL; // good practice: avoid dangling pointer
printf("Memory freed.\n");
return 0;
}
/*
Enter number of elements: 5
Array: 10 20 30 40 50
Memory freed.
*/
#include <stdio.h>
#include <stdlib.h>
int main() {
// calloc - allocates and zero-initializes
int *arr = (int*)calloc(5, sizeof(int));
if (!arr) { printf("calloc failed\n"); return 1; }
printf("calloc (all zeros): ");
for (int i = 0; i < 5; i++) printf("%d ", arr[i]); // 0 0 0 0 0
printf("\n");
// Fill with values
for (int i = 0; i < 5; i++) arr[i] = i + 1;
// realloc - resize to 10 elements
int *bigger = realloc(arr, 10 * sizeof *arr);
if (!bigger) {
printf("realloc failed\n");
free(arr);
return 1;
}
arr = bigger; // arr now points to the resized block
// Initialize new elements
for (int i = 5; i < 10; i++) arr[i] = (i + 1) * 10;
printf("After realloc (10 elements): ");
for (int i = 0; i < 10; i++) printf("%d ", arr[i]);
printf("\n");
free(arr);
return 0;
}
/*
calloc (all zeros): 0 0 0 0 0
After realloc (10 elements): 1 2 3 4 5 60 70 80 90 100
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[50];
int score;
} Student;
int main() {
int n;
printf("How many students? ");
scanf("%d", &n);
// Allocate array of structs dynamically
Student *students = malloc((size_t)n * sizeof *students);
if (!students) { printf("Allocation failed\n"); return 1; }
// Input data
for (int i = 0; i < n; i++) {
printf("Enter name and score for student %d: ", i + 1);
scanf("%s %d", students[i].name, &students[i].score);
}
// Find highest scorer
int maxIdx = 0;
for (int i = 1; i < n; i++) {
if (students[i].score > students[maxIdx].score) maxIdx = i;
}
printf("\nAll students:\n");
for (int i = 0; i < n; i++) {
printf(" %-15s %d\n", students[i].name, students[i].score);
}
printf("Top scorer: %s (%d)\n", students[maxIdx].name, students[maxIdx].score);
free(students);
students = NULL;
return 0;
}
Allocate by the pointed type, for example malloc(count * sizeof *items). In C, do not cast the void pointer returned by malloc; a cast can hide a missing <stdlib.h> declaration. Before multiplying count by element size, ensure the multiplication cannot overflow SIZE_MAX. A wrapped size can allocate a smaller block than the later loop expects.
malloc leaves the bytes indeterminate; initialize every element before reading it. calloc performs the size multiplication and initializes all bits to zero, which produces zero for integer types but should not be described as constructing every possible semantic default. A zero-size allocation has implementation-defined usability details, so handle an empty collection explicitly.
realloc may extend the existing block or allocate a new block, copy preserved bytes, and free the old block. If it fails for a nonzero requested size, it returns NULL and the original allocation remains valid. Assign the result to a temporary pointer; assigning directly to the only pointer leaks the original block on failure.
New bytes added by a successful growth are uninitialized. Initialize them before use. Any pointer into the old block becomes invalid if realloc moves the allocation, so store indexes or offsets rather than long-lived interior pointers across a resize.
free accepts NULL, which makes a single cleanup section practical after partial initialization. After free, all pointers to the allocation become dangling; setting one variable to NULL does not repair aliases held elsewhere. The durable solution is a clear ownership rule that prevents those aliases from being used.
Use runtime tools when available: compiler sanitizers can detect many out-of-bounds and use-after-free defects, while leak detectors reveal allocations that lost their owner. Tools support the ownership model; they do not replace one.
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.