Every array covered so far in this series has had one thing in common: its size was fixed at the moment the program was written, permanently baked into the code before it ever ran. This works fine when you already know exactly how much data your program will need to handle, but real-world programs frequently do not have that luxury. A program might need to store a list of student records where the number of students is only known once the user enters it while the program is already running.
Dynamic memory allocation solves this exact problem, allowing a program to request memory while it is actually running, based on values that are only known at that point in time, rather than being fixed permanently when the code was compiled. This memory is drawn from a region known as the heap, which is separate from the more limited space used for ordinary local variables.
In this tutorial, you will learn how the malloc, calloc, realloc, and free functions work together to manage memory dynamically, how to create arrays whose size is determined at runtime, what memory leaks and dangling pointers are, and how to avoid the most common pitfalls associated with manual memory management in C.
Arrays declared with a fixed size must have that size known at compile time, before the program has even started running. If the actual amount of data needed turns out to be larger than expected, the program simply cannot accommodate it without dynamic memory allocation, since a fixed-size array cannot grow beyond its originally declared capacity.
| Aspect | Fixed-Size Array | Dynamically Allocated Memory |
|---|---|---|
| Size Determined | At compile time, before the program runs. | At runtime, based on values calculated while the program is executing. |
| Memory Location | Typically allocated on the stack. | Allocated on the heap, a separate region of memory. |
| Lifespan | Automatically released once the array goes out of scope. | Remains allocated until explicitly released by the programmer. |
The malloc function requests a block of memory of a specified size from the heap and returns a pointer to the beginning of that block. Unlike memory allocated for ordinary variables, memory obtained through malloc is not automatically initialized, meaning it may contain leftover, unpredictable values.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *scores;
int numberOfStudents = 3;
scores = (int *) malloc(numberOfStudents * sizeof(int));
scores[0] = 85;
scores[1] = 90;
scores[2] = 78;
printf("Second score: %d", scores[1]);
free(scores);
return 0;
}
Second score: 90
Here, the amount of memory requested is calculated by multiplying the number of students by the size of a single integer, ensuring enough space is reserved to hold exactly the required number of values.
The calloc function is similar to malloc, but with one important difference: it automatically initializes all the allocated memory to zero. It also takes its arguments slightly differently, specifying the number of elements and the size of each element as two separate values.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *views;
int days = 5;
views = (int *) calloc(days, sizeof(int));
printf("Initial value at index 2: %d", views[2]);
free(views);
return 0;
}
Initial value at index 2: 0
Because calloc automatically sets every allocated element to zero, the value displayed here is predictable, unlike memory allocated through malloc, which may contain arbitrary leftover data until it is explicitly assigned.
The realloc function changes the size of a previously allocated block of memory, either expanding or shrinking it, while attempting to preserve the existing data already stored within it wherever possible.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *scores;
scores = (int *) malloc(2 * sizeof(int));
scores[0] = 85;
scores[1] = 90;
scores = (int *) realloc(scores, 4 * sizeof(int));
scores[2] = 78;
scores[3] = 92;
printf("Third score: %d\n", scores[2]);
printf("Fourth score: %d", scores[3]);
free(scores);
return 0;
}
Third score: 78 Fourth score: 92
The original two values remain intact after the resize, since realloc preserves existing data while expanding the allocated block to accommodate the two additional integers.
Unlike ordinary local variables, which are automatically released once they go out of scope, dynamically allocated memory remains reserved until it is explicitly released using the free function. Forgetting to do this is one of the most common sources of problems in C programs that use dynamic memory allocation.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *subscriberCount;
subscriberCount = (int *) malloc(sizeof(int));
*subscriberCount = 25000;
printf("Subscribers: %d", *subscriberCount);
free(subscriberCount);
return 0;
}
Subscribers: 25000
Calling free here returns the allocated memory back to the system, making it available for other purposes once the program no longer needs it, which is an essential step for any memory obtained through malloc, calloc, or realloc.
A memory leak occurs when dynamically allocated memory is never released using free, even though the program no longer has any way to access or use it. Over time, especially in long-running programs, memory leaks can accumulate and consume increasingly large amounts of system memory.
#include <stdio.h>
#include <stdlib.h>
void createLeak() {
int *data = (int *) malloc(sizeof(int));
*data = 100;
// Memory is never freed here, and the pointer is lost once the function ends
}
int main() {
createLeak();
printf("Function completed, but memory remains allocated.");
return 0;
}
Function completed, but memory remains allocated.
Once the function finishes, the local pointer variable holding the address of the allocated memory disappears, but the memory itself remains reserved, with no remaining way for the program to reach it and release it properly.
A dangling pointer is a pointer that continues to hold the address of memory that has already been freed. Using a dangling pointer to access or modify that memory leads to undefined behavior, since the memory it points to may have already been reassigned for a completely different purpose.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *data = (int *) malloc(sizeof(int));
*data = 50;
free(data);
// data is now a dangling pointer, since the memory has already been released
data = NULL;
return 0;
}
Setting the pointer to NULL immediately after freeing it, as shown here, is a widely recommended practice, since it prevents the pointer from accidentally being dereferenced later while still holding the address of memory that no longer belongs to the program.
One of the most practical applications of dynamic memory allocation is creating an array whose size is determined by user input, something that is simply not possible with an ordinary fixed-size array declared directly in the code.
#include <stdio.h>
#include <stdlib.h>
int main() {
int numberOfVideos;
printf("Enter number of videos to track: ");
scanf("%d", &numberOfVideos);
int *viewCounts = (int *) malloc(numberOfVideos * sizeof(int));
int i;
for (i = 0; i < numberOfVideos; i++) {
viewCounts[i] = (i + 1) * 500;
}
for (i = 0; i < numberOfVideos; i++) {
printf("Video %d: %d views\n", i + 1, viewCounts[i]);
}
free(viewCounts);
return 0;
}
Enter number of videos to track: 3 Video 1: 500 views Video 2: 1000 views Video 3: 1500 views
Here, the exact size of the array is only decided once the user enters a value while the program is already running, something a fixed-size array declared at compile time could never achieve on its own.
| Function | Purpose | Initializes Memory |
|---|---|---|
| malloc | Allocates a single block of memory of a specified total size. | No, memory may contain leftover, unpredictable values. |
| calloc | Allocates memory for a specified number of elements of a given size. | Yes, all allocated memory is automatically set to zero. |
| realloc | Resizes a previously allocated block of memory, preserving existing data. | Existing data is preserved; any newly added space is not guaranteed to be initialized. |
| Mistake | Correct Practice |
|---|---|
| Forgetting to call free after using dynamically allocated memory. | Always release memory using free once it is no longer needed by the program. |
| Using a pointer after the memory it references has already been freed. | Set the pointer to NULL immediately after freeing it, and avoid using it afterward. |
| Assuming malloc automatically initializes memory to zero. | Use calloc instead of malloc whenever zero-initialized memory is specifically required. |
| Calling free more than once on the same pointer. | Free a given block of memory only once, and set the pointer to NULL immediately afterward to help prevent repeated calls. |
Dynamic memory allocation gives C programs the flexibility to work with data whose size is only known while the program is actually running, something fixed-size arrays simply cannot accommodate on their own. The malloc, calloc, and realloc functions each play a distinct role in requesting and adjusting memory from the heap, while free ensures that memory is properly released once it is no longer needed.
In this tutorial, you learned why dynamic memory allocation is necessary, how each of the core memory management functions works, how to create arrays sized according to runtime input, and how to recognize and avoid common pitfalls such as memory leaks and dangling pointers. With this understanding in place, you are ready to explore file handling, the final major topic in this series, which allows a C program to read from and write to files stored on disk.