Chapter 7: Memory Management: `malloc`, `calloc`, `realloc`, `free`

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.

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 a void * (generic pointer) to the beginning of the allocated block of memory.
  • If allocation fails (e.g., out of memory), it returns NULL.

Usage:

  1. Call malloc with the desired size in bytes. Use sizeof() to calculate the correct size for your data type.
  2. Type cast the void * returned by malloc to the appropriate pointer type. (While implicitly converted in C, explicit casting is good practice and required in C++).
  3. Check for NULL to 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, or NULL on 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 (or NULL to behave like malloc).
  • new_size: The new desired size for the memory block in bytes.
  • Returns a void * to the new memory block, or NULL on 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).
  • If ptr is NULL, realloc behaves like malloc.
  • If new_size is 0 and ptr is not NULL, realloc behaves like free.

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 by malloc, calloc, or realloc.
  • Passing NULL to free() 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 to NULL.
  • Double Free: Calling free() on the same memory block twice. This can corrupt the heap and cause crashes. Setting pointers to NULL after freeing helps prevent this.
  • Invalid free(): Calling free() on a pointer that was not returned by malloc, calloc, or realloc, 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, or realloc returned NULL. This can lead to dereferencing a NULL pointer (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:

  1. Prompts the user to enter the number of integers they want to store.
  2. Dynamically allocates an array of ints of that size using malloc.
  3. Checks if malloc was successful. If not, print an error and exit.
  4. Prompts the user to enter each integer, storing them in the dynamically allocated array.
  5. Calculates and prints the sum of all elements in the array.
  6. Frees the dynamically allocated memory.
  7. 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:

  1. Initialize a char *buffer = NULL; and size_t current_buffer_size = 0;.
  2. In a while loop:
    • Prompt the user to “Enter a word (or ‘quit’ to exit): “.
    • Read the word into a small temporary char array (e.g., char temp_word[100];).
    • If the word is “quit”, break the loop.
    • Calculate the length of the temp_word using strlen().
    • Calculate the required_size for the buffer: current_buffer_size + strlen(temp_word) + 1 (for null terminator). If it’s the first word, just strlen(temp_word) + 1. Also add space for a separator if adding more words (e.g. 1 byte for a space).
    • Use realloc to expand buffer to required_size. Remember to use a temporary pointer for realloc’s return value.
    • If realloc succeeds, append the temp_word to buffer using strcat (or strncat for safety, ensuring the buffer is null-terminated before appending if realloc provided a clean new block). Add a space separator before appending new words if the buffer isn’t empty.
    • Update current_buffer_size.
  3. After the loop, print the entire concatenated buffer.
  4. free() the buffer and set it to NULL.

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.