Dynamic Memory Allocation

Allocating memory dynamically using `malloc`, `calloc`, and `realloc`. Freeing allocated memory using `free` to avoid memory leaks.


Resizing Memory with realloc in C

In C programming, the realloc function is a powerful tool for dynamically resizing memory blocks that have been previously allocated using malloc, calloc, or realloc itself. This allows you to adjust the amount of memory your program uses during runtime, which is particularly useful when dealing with data structures whose size is not known in advance.

Understanding realloc

The realloc function has the following prototype (defined in stdlib.h):

void *realloc(void *ptr, size_t size);
  • ptr: A pointer to the memory block that you want to resize. This pointer must have been previously returned by a memory allocation function (malloc, calloc, or realloc). If ptr is NULL, realloc behaves the same as malloc(size), allocating a new memory block of the specified size.
  • size: The new desired size of the memory block, in bytes.

Return Value:

  • If successful, realloc returns a pointer to the (potentially moved) memory block. The contents of the memory block up to the minimum of the old and new sizes are guaranteed to be preserved.
  • If size is zero, realloc returns NULL and frees the original memory block pointed to by ptr. It's equivalent to free(ptr).
  • If realloc cannot allocate the requested memory (e.g., due to insufficient memory), it returns NULL. Crucially, in this case, the original memory block pointed to by ptr remains valid and untouched. This is why handling the potential NULL return is so important.

How realloc Works

realloc attempts to resize the existing memory block in place. If there is enough contiguous memory available after the current block, it will simply extend the block to the new size and return the original pointer. However, if there is not enough contiguous memory, realloc will:

  1. Allocate a new memory block of the requested size.
  2. Copy the contents of the original memory block (up to the minimum of the old and new sizes) to the new block.
  3. Free the original memory block.
  4. Return a pointer to the new memory block.

Because realloc might move the memory block, you should always update your pointer to the block with the value returned by realloc.

Examples

Example 1: Initial Allocation and Resizing

 #include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr = NULL;
    size_t size = 5;

    // Initial allocation using malloc
    arr = (int *)malloc(size * sizeof(int));
    if (arr == NULL) {
        perror("malloc failed");
        return 1;
    }

    printf("Initial allocation: %zu elements\n", size);

    // Fill the array
    for (size_t i = 0; i < size; i++) {
        arr[i] = (int)i;
    }

    // Resize the array to hold 10 elements
    size = 10;
    int *temp = (int *)realloc(arr, size * sizeof(int)); // Use a temporary pointer!
    if (temp == NULL) {
        perror("realloc failed");
        free(arr); // Important: Free the original memory if realloc fails
        return 1;
    }
    arr = temp; // Only assign the new pointer if realloc succeeded

    printf("Resized allocation: %zu elements\n", size);

    // Fill the new elements
    for (size_t i = 5; i < size; i++) {
        arr[i] = (int)i;
    }

    // Print the array
    for (size_t i = 0; i < size; i++) {
        printf("arr[%zu] = %d\n", i, arr[i]);
    }

    free(arr); // Free the allocated memory
    return 0;
} 

Example 2: Handling realloc Failure

 #include <stdio.h>
#include <stdlib.h>

int main() {
    int *data = malloc(100 * sizeof(int));
    if (data == NULL) {
        perror("malloc failed");
        return 1;
    }

    // Attempt to allocate a very large amount of memory (likely to fail)
    int *temp = realloc(data, SIZE_MAX); //Using SIZE_MAX to trigger failure

    if (temp == NULL) {
        perror("realloc failed");
        //IMPORTANT - Do not use 'data', it remains valid.
        printf("realloc failed, original pointer is still valid\n");
        free(data); // Free the original allocated memory.
        return 1;
    } else{
        data = temp;
        printf("Reallocation sucessful with pointer: %p", (void*)data);
    }

    free(data);
    return 0;
} 

The Importance of Using a Temporary Pointer

Always use a temporary pointer when calling realloc. If realloc fails and returns NULL, the original memory block pointed to by ptr remains valid. If you directly assign the return value of realloc to ptr without checking for NULL and realloc fails, you'll lose the original pointer and create a memory leak. Using a temporary pointer allows you to check the return value and, if realloc fails, you can still access and free the original memory block.

This is the correct and safe pattern:

 void *temp = realloc(ptr, new_size);
if (temp == NULL) {
    // realloc failed.  ptr is still valid.
    // Handle the error (e.g., print an error message, free ptr, exit)
    free(ptr);
    ptr = NULL;
    // ...
} else {
    // realloc succeeded
    ptr = temp;
} 

Important Considerations

  • Memory Leaks: If you repeatedly call realloc without properly freeing the old memory block (in the case of failure), you'll create a memory leak.
  • Error Handling: Always check the return value of realloc for NULL to handle potential allocation failures.
  • Efficiency: While realloc is a powerful tool, frequent resizing can be inefficient, especially if it involves copying large amounts of data. Consider using a more suitable data structure if you anticipate frequent resizing.
  • Zero Size: If you call realloc(ptr, 0), it is equivalent to free(ptr). The return value will be NULL.