Chapter 7: Memory Management: malloc, calloc, realloc, free
Up until now, all the variables we’ve used have been allocated automatically by the compiler in either static memory (for global variables) or the stack (for local variables and function parameters). This is sufficient for many tasks, but it has limitations:
- Fixed Size: Stack-allocated arrays (like
int arr[10];) must have their size known at compile time. - Limited Lifetime: Stack variables are automatically destroyed when their function exits.
Dynamic Memory Allocation allows your program to request memory from the operating system during runtime (when the program is executing) from an area called the heap. This memory persists until explicitly deallocated or the program ends. This is crucial for:
- Creating data structures of variable sizes (e.g., linked lists, trees, dynamic arrays).
- Handling data whose size is unknown until runtime.
- Creating data that needs to outlive the function that created it.
In this chapter, we will learn how to use the standard C library functions for dynamic memory management: malloc, calloc, realloc, and free. These functions are declared in <stdlib.h>.
7.1 Memory Areas: Stack vs. Heap
It’s important to understand the two primary memory regions involved in variable storage:
Stack:
- Automatic storage: Variables declared inside functions (local variables) are stored here.
- LIFO (Last-In, First-Out): Memory is allocated and deallocated in a highly organized, linear fashion.
- Fast access: Allocation/deallocation is very quick.
- Limited size: The stack usually has a relatively small, fixed size. Too many function calls or large local variables can lead to a “stack overflow.”
- Lifetime: Variables on the stack are destroyed automatically when the function they belong to returns.
Heap:
- Dynamic storage: Memory explicitly requested by the programmer at runtime using functions like
malloc. - Flexible: Memory can be allocated and deallocated in any order.
- Slower access: Allocation/deallocation is generally slower than the stack.
- Larger size: The heap is typically much larger than the stack.
- Lifetime: Memory on the heap persists until explicitly deallocated by
free()or the program terminates. It’s the programmer’s responsibility to manage this memory.
- Dynamic storage: Memory explicitly requested by the programmer at runtime using functions like
7.2 malloc(): Allocate Memory
The malloc() (memory allocate) function allocates a block of memory of a specified size in bytes from the heap.
Syntax:
void *malloc(size_t size);
size: The number of bytes to allocate.malloc()returns avoid *(generic pointer) to the beginning of the allocated block of memory.- If allocation fails (e.g., out of memory), it returns
NULL.
Usage:
- Call
mallocwith the desired size in bytes. Usesizeof()to calculate the correct size for your data type. - Type cast the
void *returned bymallocto the appropriate pointer type. (While implicitly converted in C, explicit casting is good practice and required in C++). - Check for
NULLto handle potential allocation failures.
Example: Allocating space for a single integer.
#include <stdio.h>
#include <stdlib.h> // For malloc, free
int main() {
int *ptr_int; // Declare an integer pointer
// Allocate memory for one integer
ptr_int = (int *) malloc(sizeof(int));
// Check if allocation was successful
if (ptr_int == NULL) {
printf("Memory allocation failed!\n");
return 1; // Indicate an error
}
// Use the allocated memory
*ptr_int = 100;
printf("Value stored in dynamically allocated memory: %d\n", *ptr_int);
printf("Address of dynamically allocated memory: %p\n", (void *)ptr_int); // Cast to void* for %p
// Release the allocated memory
free(ptr_int);
ptr_int = NULL; // Good practice to set freed pointers to NULL
return 0;
}
Example: Allocating an array of integers (dynamic array).
#include <stdio.h>
#include <stdlib.h>
int main() {
int *dynamic_array;
int n, i;
printf("Enter the number of elements: ");
scanf("%d", &n);
// Allocate memory for 'n' integers
dynamic_array = (int *) malloc(n * sizeof(int));
if (dynamic_array == NULL) {
printf("Memory allocation failed!\n");
return 1;
}
printf("Enter %d integers:\n", n);
for (i = 0; i < n; i++) {
printf("Element %d: ", i + 1);
scanf("%d", &dynamic_array[i]); // Use array-style access
}
printf("Dynamically allocated array elements:\n");
for (i = 0; i < n; i++) {
printf("%d ", dynamic_array[i]);
}
printf("\n");
free(dynamic_array);
dynamic_array = NULL;
return 0;
}
7.3 calloc(): Allocate and Initialize Memory
The calloc() (contiguous allocate) function allocates a block of memory for an array of elements and initializes all bytes in the allocated block to zero.
Syntax:
void *calloc(size_t num, size_t size);
num: The number of elements to allocate.size: The size of each element in bytes.- Returns a
void *to the allocated memory, orNULLon failure.
Usage: calloc is useful when you want the allocated memory to be zero-initialized, which malloc does not guarantee (it contains garbage values).
Example:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *zero_array;
int n = 5;
// Allocate memory for 5 integers and initialize to zero
zero_array = (int *) calloc(n, sizeof(int));
if (zero_array == NULL) {
printf("Memory allocation failed!\n");
return 1;
}
printf("Elements allocated by calloc (should be zero):\n");
for (int i = 0; i < n; i++) {
printf("%d ", zero_array[i]);
}
printf("\n");
free(zero_array);
zero_array = NULL;
return 0;
}
7.4 realloc(): Reallocate Memory
The realloc() (reallocate) function changes the size of a previously allocated block of memory. It can expand or shrink the memory block.
Syntax:
void *realloc(void *ptr, size_t new_size);
ptr: A pointer to the previously allocated memory block (orNULLto behave likemalloc).new_size: The new desired size for the memory block in bytes.- Returns a
void *to the new memory block, orNULLon failure.- The returned pointer might be the same as
ptr(if there’s space to expand in place). - Or, it might be a different address (if a new, larger block was allocated elsewhere, and the old content copied).
- The returned pointer might be the same as
- If
ptrisNULL,reallocbehaves likemalloc. - If
new_sizeis0andptris notNULL,reallocbehaves likefree.
Usage:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr;
int initial_size = 3;
int new_size = 5;
// 1. Allocate initial memory for 3 integers
arr = (int *) malloc(initial_size * sizeof(int));
if (arr == NULL) { /* handle error */ return 1; }
for (int i = 0; i < initial_size; i++) {
arr[i] = (i + 1) * 10;
}
printf("Original array (size %d): ", initial_size);
for (int i = 0; i < initial_size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 2. Reallocate memory to new_size for 5 integers
int *temp_arr = (int *) realloc(arr, new_size * sizeof(int));
if (temp_arr == NULL) {
printf("Reallocation failed. Original array (arr) is still valid.\n");
free(arr); // Clean up original array
return 1;
}
arr = temp_arr; // Update 'arr' to point to the new (potentially different) block
// Initialize new elements (if reallocated to larger size)
for (int i = initial_size; i < new_size; i++) {
arr[i] = (i + 1) * 10; // New elements get values
}
printf("Reallocated array (size %d): ", new_size);
for (int i = 0; i < new_size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 3. Shrink the array
int shrink_size = 2;
temp_arr = (int *) realloc(arr, shrink_size * sizeof(int));
if (temp_arr == NULL) { /* handle error */ free(arr); return 1; }
arr = temp_arr;
printf("Shrunken array (size %d): ", shrink_size);
for (int i = 0; i < shrink_size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
free(arr);
arr = NULL;
return 0;
}
Important realloc Tip: Always assign the result of realloc to a temporary pointer first (e.g., temp_arr) before assigning it back to your original pointer (arr). If realloc fails and returns NULL, assigning NULL directly to arr would cause you to lose the reference to your original, still-valid memory block, leading to a memory leak.
7.5 free(): Deallocate Memory
The free() function deallocates the memory block pointed to by ptr, making it available for reuse.
Syntax:
void free(void *ptr);
ptr: A pointer to a memory block previously allocated bymalloc,calloc, orrealloc.- Passing
NULLtofree()is safe and does nothing. - DO NOT free memory that was not dynamically allocated.
- DO NOT free the same memory block twice (double free), which leads to undefined behavior.
Best Practice: After calling free(ptr), always set ptr = NULL;. This prevents dangling pointers (pointers that point to freed memory) and helps catch double-free attempts if you accidentally try to free NULL again.
Code Example: Full Dynamic Memory Workflow
#include <stdio.h>
#include <stdlib.h> // Required for malloc, calloc, realloc, free
int main() {
int *data = NULL; // Always initialize pointers to NULL
int num_elements;
printf("Enter number of initial elements for integer array: ");
if (scanf("%d", &num_elements) != 1 || num_elements <= 0) {
printf("Invalid input for initial elements.\n");
return 1;
}
// Allocate memory using malloc
data = (int *) malloc(num_elements * sizeof(int));
if (data == NULL) {
perror("Failed to allocate initial memory"); // perror prints system error message
return 1;
}
printf("Initial array elements (garbage values):\n");
for (int i = 0; i < num_elements; i++) {
data[i] = i + 1; // Assign some values
printf("%d ", data[i]);
}
printf("\n");
// Reallocate to a larger size
int new_size;
printf("\nEnter new larger size for the array: ");
if (scanf("%d", &new_size) != 1 || new_size <= num_elements) {
printf("Invalid input for new size or not larger than current.\n");
free(data); data = NULL;
return 1;
}
int *temp = (int *) realloc(data, new_size * sizeof(int));
if (temp == NULL) {
perror("Failed to reallocate memory");
free(data); // Original 'data' is still valid
data = NULL;
return 1;
}
data = temp; // Update data to point to the new block
// Initialize new elements
for (int i = num_elements; i < new_size; i++) {
data[i] = (i + 1) * 10;
}
printf("Array after reallocation (new size %d):\n", new_size);
for (int i = 0; i < new_size; i++) {
printf("%d ", data[i]);
}
printf("\n");
// Reallocate to a smaller size
int smaller_size;
printf("\nEnter new smaller size for the array: ");
if (scanf("%d", &smaller_size) != 1 || smaller_size >= new_size || smaller_size <= 0) {
printf("Invalid input for smaller size or not smaller than current.\n");
free(data); data = NULL;
return 1;
}
temp = (int *) realloc(data, smaller_size * sizeof(int));
if (temp == NULL) { // Reallocating to smaller size should rarely fail unless 0
perror("Failed to shrink memory");
free(data);
data = NULL;
return 1;
}
data = temp;
printf("Array after shrinking (size %d):\n", smaller_size);
for (int i = 0; i < smaller_size; i++) {
printf("%d ", data[i]);
}
printf("\n");
// Free the allocated memory
free(data);
data = NULL; // Prevent dangling pointer
printf("\nMemory freed successfully.\n");
// Attempting to access 'data' now would be undefined behavior (dangling pointer)
// Attempting to free 'data' again (which is NULL) is safe.
free(data);
// Demonstration of calloc
printf("\n--- calloc demonstration ---\n");
int *calloc_array = (int *) calloc(3, sizeof(int)); // Allocate 3 ints, all 0
if (calloc_array == NULL) { perror("calloc failed"); return 1; }
printf("Calloc-allocated array elements (should be zeros): ");
for(int i = 0; i < 3; i++) {
printf("%d ", calloc_array[i]);
}
printf("\n");
free(calloc_array);
calloc_array = NULL;
return 0;
}
Compile and Run:
gcc dynamic_memory.c -o dynamic_memory
./dynamic_memory
7.6 Common Dynamic Memory Pitfalls
Dynamic memory management is powerful but comes with responsibility. Mistakes can lead to serious bugs:
- Memory Leaks: Forgetting to
free()allocated memory. The memory remains reserved by your program until it terminates, even if you no longer need it. In long-running applications, this can exhaust available memory. - Dangling Pointers: Accessing memory after it has been
free()d. The memory might have been reallocated to something else, or accessing it could lead to crashes. Always set freed pointers toNULL. - Double Free: Calling
free()on the same memory block twice. This can corrupt the heap and cause crashes. Setting pointers toNULLafter freeing helps prevent this. - Invalid
free(): Callingfree()on a pointer that was not returned bymalloc,calloc, orrealloc, or on memory that was allocated on the stack. - Buffer Overflows/Underflows: Writing past the allocated bounds of a dynamically allocated array. Similar to static arrays, C does not check bounds.
- Allocation Failure: Not checking if
malloc,calloc, orreallocreturnedNULL. This can lead to dereferencing aNULLpointer (segmentation fault).
Using tools like Valgrind (on Linux) can help detect many of these memory-related errors.
Exercise 7.1: Dynamic Array Sum
Write a C program that:
- Prompts the user to enter the number of integers they want to store.
- Dynamically allocates an array of
ints of that size usingmalloc. - Checks if
mallocwas successful. If not, print an error and exit. - Prompts the user to enter each integer, storing them in the dynamically allocated array.
- Calculates and prints the sum of all elements in the array.
- Frees the dynamically allocated memory.
- Sets the pointer to
NULL.
Exercise 7.2: Resizable String Buffer (Mini-Challenge)
Create a C program that repeatedly reads words from the user until they enter “quit”. Store all the words in a single dynamically allocated buffer, growing the buffer using realloc as needed.
Instructions:
- Initialize a
char *buffer = NULL;andsize_t current_buffer_size = 0;. - In a
whileloop:- Prompt the user to “Enter a word (or ‘quit’ to exit): “.
- Read the word into a small temporary
chararray (e.g.,char temp_word[100];). - If the word is “quit”, break the loop.
- Calculate the length of the
temp_wordusingstrlen(). - Calculate the
required_sizefor the buffer:current_buffer_size + strlen(temp_word) + 1(for null terminator). If it’s the first word, juststrlen(temp_word) + 1. Also add space for a separator if adding more words (e.g. 1 byte for a space). - Use
reallocto expandbuffertorequired_size. Remember to use a temporary pointer forrealloc’s return value. - If
reallocsucceeds, append thetemp_wordtobufferusingstrcat(orstrncatfor safety, ensuring the buffer is null-terminated before appending ifreallocprovided a clean new block). Add a space separator before appending new words if the buffer isn’t empty. - Update
current_buffer_size.
- After the loop, print the entire concatenated buffer.
free()thebufferand set it toNULL.
Example:
Enter a word (or 'quit' to exit): Hello
Enter a word (or 'quit' to exit): World
Enter a word (or 'quit' to exit): C
Enter a word (or 'quit' to exit): Programming
Enter a word (or 'quit' to exit): quit
All words: Hello World C Programming
This chapter completes your understanding of fundamental data storage and manipulation in C, extending from static/stack allocation to dynamic memory on the heap. Mastering malloc, calloc, realloc, and free is essential for writing robust and flexible low-level C programs. Be mindful of the common pitfalls, and always practice safe memory management!
In the next chapter, we’ll delve into structures, unions, and enums to define custom data types.