Dynamic Memory: malloc, calloc, realloc & free
So far every array has had a size fixed when you wrote the program. Real programs often do not know the size in advance: how many students are in the file, how many orders arrive today, how long the user's name is. Dynamic memory lets a program ask for memory while it runs, use it, and give it back when finished.
In this lesson you will use malloc, calloc, realloc and free from stdlib.h, grow an array as data arrives, return allocated memory from functions, and learn the memory bugs that every C programmer must avoid.
The stack and the heap
Local variables live on the stack. Their memory is created automatically when a function starts and released when it returns. The stack is fast but small (often a few megabytes), and its memory cannot outlive the function.
Allocated memory comes from a much larger area called the heap. You request it with an allocation function and it stays yours until you release it with free, even after the function that requested it has returned. C does not clean up heap memory for you; there is no garbage collector.
| Function | What it does | Starting contents |
|---|---|---|
malloc(size) | Allocates size bytes | Garbage |
calloc(count, size) | Allocates count elements of size bytes each | All zero |
realloc(p, new_size) | Resizes an allocation, moving it if needed | Old contents kept |
free(p) | Gives the memory back | Not applicable |
malloc and free
malloc returns a pointer to the new memory, or a null pointer if no memory is available. Always check the result before using it.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
size_t n = 4; // in a real program this might come from input or a file
int *marks = malloc(n * sizeof *marks);
if (marks == nullptr) {
fprintf(stderr, "Out of memory\n");
return EXIT_FAILURE;
}
int sample[] = {67, 82, 74, 91};
for (size_t i = 0; i < n; i++) {
marks[i] = sample[i];
}
int total = 0;
for (size_t i = 0; i < n; i++) {
total += marks[i];
}
printf("Average of %zu students: %.2f\n", n, (double) total / n);
free(marks);
marks = nullptr;
return 0;
}Average of 4 students: 78.50
Three habits are visible here:
- Size with
sizeof *marks.n * sizeof *marksmeans "n elements of whatevermarkspoints to". If you later change the type ofmarks, the size stays correct. - Check for
nullptr. Allocation can fail.fprintf(stderr, ...)prints the message to the standard error stream, which is meant for error messages. - Free once, then forget the pointer. After
free, the pointer still holds the old address but must never be used again. Setting it tonullptrmakes accidental reuse easier to catch, andfree(nullptr)safely does nothing.
Once allocated, marks works exactly like an array: marks[i] is *(marks + i), as you saw in Pointer Arithmetic & Arrays.
calloc: zeroed memory
calloc takes the number of elements and the size of one element, and sets every byte to zero. It is ideal for counters. It also checks that count * size does not overflow, which a hand-written multiplication for malloc does not.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
size_t candidates = 3;
int *votes = calloc(candidates, sizeof *votes); // all start at 0
if (votes == nullptr) {
return EXIT_FAILURE;
}
int ballots[] = {1, 0, 2, 1, 1, 0, 2, 1};
size_t count = sizeof ballots / sizeof ballots[0];
for (size_t i = 0; i < count; i++) {
votes[ballots[i]]++;
}
for (size_t c = 0; c < candidates; c++) {
printf("Candidate %zu: %d votes\n", c + 1, votes[c]);
}
free(votes);
return 0;
}Candidate 1: 2 votes Candidate 2: 4 votes Candidate 3: 2 votes
realloc: growing an array
When you do not know how many items will arrive, start small and grow. realloc resizes an allocation, copying the old contents if it has to move them. The usual strategy is to double the capacity whenever the array is full, so growth happens rarely.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
size_t capacity = 2;
size_t count = 0;
int *orders = malloc(capacity * sizeof *orders);
if (orders == nullptr) {
return EXIT_FAILURE;
}
for (int id = 101; id <= 110; id++) {
if (count == capacity) {
size_t new_capacity = capacity * 2;
int *bigger = realloc(orders, new_capacity * sizeof *orders);
if (bigger == nullptr) {
free(orders);
return EXIT_FAILURE;
}
orders = bigger;
capacity = new_capacity;
printf("Grew to %zu slots\n", capacity);
}
orders[count++] = id;
}
printf("Stored %zu orders, last one is %d\n", count, orders[count - 1]);
free(orders);
return 0;
}Grew to 4 slots Grew to 8 slots Grew to 16 slots Stored 10 orders, last one is 110
Notice that the result of realloc goes into a temporary pointer, bigger. If realloc fails, it returns a null pointer but leaves the old block untouched. Writing orders = realloc(orders, ...) directly would overwrite your only pointer to that old block with null, and the memory would be lost.
Returning allocated memory from a function
A function cannot return a pointer to its local array, because that memory disappears when it returns. It can return heap memory, as long as the caller knows it must call free. Document this "ownership" in a comment.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Returns a new string that the caller must free.
char *make_label(const char *name, int roll)
{
int needed = snprintf(nullptr, 0, "%s (Roll %d)", name, roll);
char *label = malloc((size_t) needed + 1);
if (label == nullptr) {
return nullptr;
}
snprintf(label, (size_t) needed + 1, "%s (Roll %d)", name, roll);
return label;
}
int main(void)
{
char *label = make_label("Ananya Iyer", 27);
if (label == nullptr) {
return EXIT_FAILURE;
}
printf("%s, %zu characters\n", label, strlen(label));
free(label);
return 0;
}Ananya Iyer (Roll 27), 21 characters
Calling snprintf with a null pointer and size 0 writes nothing but returns the length the text needs. The function then allocates exactly that much plus one byte for '\0'. For a plain copy of a string, C23 also standardised strdup(s), which returns an allocated copy that you must free.
The four classic memory bugs
int *p = malloc(10 * sizeof *p);
// 1. Memory leak: the function returns without free(p)
// 2. Heap overflow: p[10] = 7; writes past the end
free(p);
// 3. Use after free: p[0] = 5; uses memory you gave back
// 4. Double free: free(p); releases the same block twice- A memory leak wastes memory. In a long-running program such as a server, leaks slowly use up all the memory.
- A heap overflow corrupts the allocator's own records or other data, often causing a crash much later.
- Use after free and double free are undefined behaviour and common sources of security holes.
None of these is caught by the compiler. The tools in Debugging with gdb & Sanitizers, especially AddressSanitizer, find them while you test. A simple rule prevents most of them: every malloc has exactly one matching free, and you decide clearly which part of the program owns each block.
Common mistakes
- Not checking for a null result. Writing through a null pointer crashes the program.
- Allocating the wrong size.
malloc(n)givesnbytes, notnintegers. Writemalloc(n * sizeof *p). - Forgetting the
+ 1for strings. A string of lengthlenneedslen + 1bytes. p = realloc(p, size). On failure this loses the old block. Use a temporary pointer.- Calling
reallocwith size 0. In C23 this is undefined behaviour. Callfreeinstead. - Freeing memory that was not allocated. Only pass
freepointers frommalloc,calloc,reallocorstrdup.
Key points
- Heap memory from
malloc,callocorrealloclasts until youfreeit. - Always check the returned pointer for
nullptrbefore using it. - Size allocations as
count * sizeof *p;callocalso zeroes the memory and checks for overflow. - Grow arrays with
reallocinto a temporary pointer, doubling the capacity when full. - After
free(p), never usepagain; setting it tonullptrhelps. - Leaks, heap overflows, use-after-free and double free are the classic bugs; test with sanitizers.
Exercise
Given the marks 35, 72, 18, 90, 56, 41 in an ordinary array, write a program that first counts how many are 40 or more, then uses malloc to allocate an array of exactly that size, copies the passing marks into it, prints them, and frees the memory.
Show solution
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int marks[] = {35, 72, 18, 90, 56, 41};
size_t n = sizeof marks / sizeof marks[0];
size_t passed = 0;
for (size_t i = 0; i < n; i++) {
if (marks[i] >= 40) {
passed++;
}
}
int *pass_list = malloc(passed * sizeof *pass_list);
if (pass_list == nullptr) {
return EXIT_FAILURE;
}
size_t k = 0;
for (size_t i = 0; i < n; i++) {
if (marks[i] >= 40) {
pass_list[k++] = marks[i];
}
}
printf("%zu passed:", passed);
for (size_t i = 0; i < passed; i++) {
printf(" %d", pass_list[i]);
}
printf("\n");
free(pass_list);
return 0;
}4 passed: 72 90 56 41
Counting first lets the program allocate exactly the right amount of memory. Every path that allocates also frees, so there is no leak.
Quick Quiz
1. What does malloc return when it cannot allocate memory?
malloc, calloc and realloc return a null pointer on failure, so always check the result.
2. Which call allocates space for 50 double values, all set to zero?
calloc allocates count elements of the given size and zeroes them; malloc leaves garbage.
3. Why should you write tmp = realloc(p, size) instead of p = realloc(p, size)?
On failure realloc returns null but keeps the old block, so you must not overwrite your only pointer to it.
4. What is a memory leak?
Leaked memory stays reserved until the program ends, wasting resources.
Learn C with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
