Docs

Calloc and Realloc

Calloc and Realloc in C

Table of Contents

  1. •Introduction
  2. •The calloc() Function
  3. •The realloc() Function
  4. •Comparing malloc, calloc, and realloc
  5. •Memory Initialization Patterns
  6. •Resizing Dynamic Arrays
  7. •Common Pitfalls and Best Practices
  8. •Advanced Techniques
  9. •Performance Considerations
  10. •Summary

Introduction

While malloc() is the most commonly used dynamic memory allocation function in C, the standard library provides two additional functions that offer enhanced functionality: calloc() for zero-initialized memory allocation and realloc() for resizing previously allocated memory. Understanding these functions is essential for effective memory management in C programs.

Why We Need calloc() and realloc()

The Limitations of malloc()

// malloc() allocates uninitialized memory
int *arr = malloc(5 * sizeof(int));
// arr contains garbage values - unpredictable content!

// We must manually initialize
for (int i = 0; i < 5; i++) {
    arr[i] = 0;  // Extra step required
}

The Need for Dynamic Resizing

// What if we need to expand our array?
int *arr = malloc(5 * sizeof(int));
// Later we need 10 elements...
// Can't just extend the existing allocation!

calloc() solves the initialization problem, and realloc() solves the resizing problem.


The calloc() Function

Function Signature

#include <stdlib.h>

void *calloc(size_t num_elements, size_t element_size);

Parameters

ParameterDescription
num_elementsNumber of elements to allocate
element_sizeSize of each element in bytes

Return Value

ReturnDescription
void *Pointer to the allocated and zero-initialized memory
NULLAllocation failed (insufficient memory)

How calloc() Works

calloc(5, sizeof(int))

Step 1: Calculate total size = 5 * sizeof(int) = 20 bytes
Step 2: Allocate 20 bytes of memory
Step 3: Initialize all bytes to zero
Step 4: Return pointer to the block

Memory View:
+-----+-----+-----+-----+-----+
|  0  |  0  |  0  |  0  |  0  |  <-- All initialized to 0
+-----+-----+-----+-----+-----+
  [0]   [1]   [2]   [3]   [4]

Basic Usage

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

int main() {
    // Allocate array of 5 integers, all initialized to 0
    int *arr = calloc(5, sizeof(int));

    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }

    // No need to initialize - already zero!
    printf("Values after calloc:\n");
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);  // All will be 0
    }

    free(arr);
    return 0;
}

calloc() vs malloc() with memset()

// Method 1: Using calloc() (preferred)
int *arr1 = calloc(1000, sizeof(int));

// Method 2: Using malloc() + memset() (equivalent)
int *arr2 = malloc(1000 * sizeof(int));
if (arr2 != NULL) {
    memset(arr2, 0, 1000 * sizeof(int));
}

// Both produce the same result, but calloc() is:
// 1. More concise
// 2. Potentially more efficient (OS optimizations)
// 3. Safer against integer overflow

Integer Overflow Protection

// calloc() provides implicit overflow checking
// If num_elements * element_size overflows, calloc returns NULL

size_t huge_count = SIZE_MAX;
size_t huge_size = 100;

// malloc() might allocate wrong amount (overflow wraps around)
void *p1 = malloc(huge_count * huge_size);  // Undefined behavior!

// calloc() will safely return NULL
void *p2 = calloc(huge_count, huge_size);   // Returns NULL

Allocating Different Data Types

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

int main() {
    // Allocating array of doubles
    double *doubles = calloc(10, sizeof(double));

    // Allocating array of structures
    struct Point {
        int x, y;
    };
    struct Point *points = calloc(5, sizeof(struct Point));

    // Allocating 2D array (array of pointers)
    int **matrix = calloc(3, sizeof(int *));
    for (int i = 0; i < 3; i++) {
        matrix[i] = calloc(4, sizeof(int));  // Each row
    }

    // Verify all are zero-initialized
    printf("Double: %.2f\n", doubles[5]);           // 0.00
    printf("Point: (%d, %d)\n", points[2].x, points[2].y);  // (0, 0)
    printf("Matrix: %d\n", matrix[1][2]);           // 0

    // Free memory
    free(doubles);
    free(points);
    for (int i = 0; i < 3; i++) free(matrix[i]);
    free(matrix);

    return 0;
}

When to Use calloc()

SituationUse calloc()
Arrays that need zero initialization✓
Numeric arrays for calculations✓
Structures with many members to initialize✓
Security-sensitive applications✓
Hash tables or lookup tables✓
Graph adjacency matrices✓
Counter/frequency arrays✓

The realloc() Function

Function Signature

#include <stdlib.h>

void *realloc(void *ptr, size_t new_size);

Parameters

ParameterDescription
ptrPointer to previously allocated memory block
new_sizeNew size in bytes

Return Value

ReturnDescription
void *Pointer to reallocated memory (may be different from original)
NULLReallocation failed (original block unchanged)

How realloc() Works

Original: ptr points to 20 bytes

realloc(ptr, 40)  // Expanding

Case 1: Sufficient space after the block
+--------+----------+----------+
|  Data  | Expanded | Free     |
+--------+----------+----------+
    ^
    ptr (unchanged)

Case 2: Need to relocate
Original block:
+--------+----+--------+
|  Data  |Used|  Free  |
+--------+----+--------+

New block:
+--------+----+------------------+
|  Used  |xxxx|   Data   | New  |
+--------+----+------------------+
                   ^
                   new_ptr (different address!)

Basic Usage

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

int main() {
    // Initial allocation
    int *arr = malloc(3 * sizeof(int));
    arr[0] = 10;
    arr[1] = 20;
    arr[2] = 30;

    printf("Original address: %p\n", (void *)arr);

    // Expand to 5 elements
    int *temp = realloc(arr, 5 * sizeof(int));

    if (temp == NULL) {
        printf("Reallocation failed!\n");
        free(arr);  // Original still valid
        return 1;
    }

    arr = temp;  // Update pointer
    printf("New address: %p\n", (void *)arr);

    // Original data preserved
    printf("Original values: %d, %d, %d\n", arr[0], arr[1], arr[2]);

    // Add new elements
    arr[3] = 40;
    arr[4] = 50;

    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }

    free(arr);
    return 0;
}

The Golden Rule: Use a Temporary Pointer

// WRONG - Memory leak if realloc fails!
arr = realloc(arr, new_size);  // If NULL, we lost our pointer!

// CORRECT - Safe pattern
int *temp = realloc(arr, new_size);
if (temp == NULL) {
    // Handle error - arr is still valid!
    free(arr);
    return -1;
}
arr = temp;  // Safe to update now

realloc() Special Cases

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

int main() {
    // Case 1: realloc(NULL, size) == malloc(size)
    int *p1 = realloc(NULL, 10 * sizeof(int));
    printf("Case 1: realloc(NULL, size) works like malloc\n");

    // Case 2: realloc(ptr, 0) - behavior is implementation-defined
    // Some systems free the memory and return NULL
    // Others return a minimal non-NULL pointer
    // AVOID THIS PATTERN - use free() instead!

    // Case 3: Shrinking memory
    int *p2 = malloc(100 * sizeof(int));
    for (int i = 0; i < 100; i++) p2[i] = i;

    p2 = realloc(p2, 10 * sizeof(int));  // Shrink to 10 elements
    printf("Case 3: Shrunk - first 10 values preserved\n");
    for (int i = 0; i < 10; i++) {
        printf("%d ", p2[i]);  // 0 1 2 3 4 5 6 7 8 9
    }
    printf("\n");

    // Case 4: Expanding with uninitialized memory
    int *p3 = calloc(5, sizeof(int));  // [0, 0, 0, 0, 0]
    p3 = realloc(p3, 10 * sizeof(int));
    // Elements 5-9 contain GARBAGE, not zero!
    printf("Case 4: New elements are uninitialized!\n");

    free(p1);
    free(p2);
    free(p3);
    return 0;
}

Shrinking vs Expanding

// Shrinking - always succeeds (in practice)
// Original data preserved up to new size
int *arr = malloc(100 * sizeof(int));
for (int i = 0; i < 100; i++) arr[i] = i * 10;

arr = realloc(arr, 25 * sizeof(int));
// Only first 25 elements remain
// Elements 25-99 are lost

// Expanding - may fail if no memory available
// Original data preserved, new space is UNINITIALIZED
int *arr2 = malloc(25 * sizeof(int));
for (int i = 0; i < 25; i++) arr2[i] = i;

int *temp = realloc(arr2, 100 * sizeof(int));
if (temp) {
    arr2 = temp;
    // Elements 0-24 preserved
    // Elements 25-99 contain garbage!

    // Initialize new elements
    for (int i = 25; i < 100; i++) {
        arr2[i] = 0;  // Manual initialization needed
    }
}

Comparing malloc, calloc, and realloc

Feature Comparison

Featuremalloc()calloc()realloc()
ParametersTotal sizeCount + element sizePointer + new size
InitializationUninitializedZero-initializedPreserves + uninitialized
Overflow checkNoYesNo
Use caseGeneral allocationZero-init neededResizing
Previous pointerN/AN/ARequired (or NULL)

Syntax Comparison

// Allocating 100 integers

// malloc - must calculate size
int *p1 = malloc(100 * sizeof(int));

// calloc - separate count and size
int *p2 = calloc(100, sizeof(int));

// realloc - from NULL (acts like malloc)
int *p3 = realloc(NULL, 100 * sizeof(int));

// Initialization comparison
// p1: garbage values
// p2: all zeros
// p3: garbage values

Performance Comparison

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

#define SIZE 10000000  // 10 million integers

int main() {
    clock_t start, end;

    // malloc - no initialization
    start = clock();
    int *p1 = malloc(SIZE * sizeof(int));
    end = clock();
    printf("malloc: %.6f seconds\n", (double)(end - start) / CLOCKS_PER_SEC);

    // malloc + memset
    start = clock();
    int *p2 = malloc(SIZE * sizeof(int));
    memset(p2, 0, SIZE * sizeof(int));
    end = clock();
    printf("malloc + memset: %.6f seconds\n", (double)(end - start) / CLOCKS_PER_SEC);

    // calloc - zero initialization
    start = clock();
    int *p3 = calloc(SIZE, sizeof(int));
    end = clock();
    printf("calloc: %.6f seconds\n", (double)(end - start) / CLOCKS_PER_SEC);

    // Note: calloc may be faster due to OS optimizations
    // (zero pages, copy-on-write, etc.)

    free(p1);
    free(p2);
    free(p3);
    return 0;
}

Memory Initialization Patterns

Zero Initialization with calloc()

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

// Pattern 1: Counter arrays
void count_frequency(const int *data, int n, int max_val) {
    // calloc ensures all counts start at 0
    int *freq = calloc(max_val + 1, sizeof(int));

    for (int i = 0; i < n; i++) {
        if (data[i] >= 0 && data[i] <= max_val) {
            freq[data[i]]++;
        }
    }

    printf("Frequencies:\n");
    for (int i = 0; i <= max_val; i++) {
        if (freq[i] > 0) {
            printf("%d: %d times\n", i, freq[i]);
        }
    }

    free(freq);
}

// Pattern 2: Boolean flags
void mark_visited(int n) {
    // All positions start as "not visited" (0)
    int *visited = calloc(n, sizeof(int));

    // Visit some positions
    visited[0] = 1;
    visited[5] = 1;

    for (int i = 0; i < n; i++) {
        printf("Position %d: %s\n", i, visited[i] ? "visited" : "not visited");
    }

    free(visited);
}

// Pattern 3: Accumulator arrays
void running_sum(const int *data, int n) {
    // Sum array initialized to 0
    int *prefix_sum = calloc(n + 1, sizeof(int));

    for (int i = 0; i < n; i++) {
        prefix_sum[i + 1] = prefix_sum[i] + data[i];
    }

    printf("Prefix sums: ");
    for (int i = 0; i <= n; i++) {
        printf("%d ", prefix_sum[i]);
    }
    printf("\n");

    free(prefix_sum);
}

int main() {
    int data[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
    int n = sizeof(data) / sizeof(data[0]);

    printf("=== Frequency Counter ===\n");
    count_frequency(data, n, 9);

    printf("\n=== Boolean Flags ===\n");
    mark_visited(10);

    printf("\n=== Running Sum ===\n");
    running_sum(data, n);

    return 0;
}

Initializing New Elements After realloc()

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

// Safe realloc with zero-initialization of new space
int *safe_realloc_zero(int *ptr, size_t old_count, size_t new_count) {
    int *new_ptr = realloc(ptr, new_count * sizeof(int));

    if (new_ptr == NULL) {
        return NULL;
    }

    // Zero-initialize new elements if expanding
    if (new_count > old_count) {
        memset(new_ptr + old_count, 0, (new_count - old_count) * sizeof(int));
    }

    return new_ptr;
}

int main() {
    size_t old_size = 5;
    size_t new_size = 10;

    int *arr = calloc(old_size, sizeof(int));
    for (size_t i = 0; i < old_size; i++) {
        arr[i] = (int)(i + 1) * 10;
    }

    printf("Before realloc:\n");
    for (size_t i = 0; i < old_size; i++) {
        printf("arr[%zu] = %d\n", i, arr[i]);
    }

    arr = safe_realloc_zero(arr, old_size, new_size);

    printf("\nAfter safe realloc:\n");
    for (size_t i = 0; i < new_size; i++) {
        printf("arr[%zu] = %d\n", i, arr[i]);
    }

    free(arr);
    return 0;
}

Resizing Dynamic Arrays

Growing Arrays Strategy

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

typedef struct {
    int *data;
    size_t size;      // Current number of elements
    size_t capacity;  // Allocated capacity
} DynamicArray;

// Initialize with initial capacity
DynamicArray *da_create(size_t initial_capacity) {
    DynamicArray *da = malloc(sizeof(DynamicArray));
    if (!da) return NULL;

    da->data = malloc(initial_capacity * sizeof(int));
    if (!da->data) {
        free(da);
        return NULL;
    }

    da->size = 0;
    da->capacity = initial_capacity;
    return da;
}

// Grow array when needed (doubling strategy)
int da_ensure_capacity(DynamicArray *da, size_t needed) {
    if (needed <= da->capacity) return 1;  // Already enough

    // Double capacity until sufficient
    size_t new_capacity = da->capacity;
    while (new_capacity < needed) {
        new_capacity *= 2;
    }

    int *new_data = realloc(da->data, new_capacity * sizeof(int));
    if (!new_data) return 0;  // Failed

    da->data = new_data;
    da->capacity = new_capacity;
    printf("Grew capacity from %zu to %zu\n", da->capacity / 2, new_capacity);
    return 1;
}

// Add element
int da_push(DynamicArray *da, int value) {
    if (!da_ensure_capacity(da, da->size + 1)) {
        return 0;
    }
    da->data[da->size++] = value;
    return 1;
}

// Shrink to fit
void da_shrink_to_fit(DynamicArray *da) {
    if (da->size == da->capacity) return;

    int *new_data = realloc(da->data, da->size * sizeof(int));
    if (new_data || da->size == 0) {
        da->data = new_data;
        da->capacity = da->size;
    }
}

// Free
void da_destroy(DynamicArray *da) {
    if (da) {
        free(da->data);
        free(da);
    }
}

int main() {
    DynamicArray *arr = da_create(4);

    // Add 20 elements
    for (int i = 1; i <= 20; i++) {
        da_push(arr, i * 10);
    }

    printf("\nArray contents: ");
    for (size_t i = 0; i < arr->size; i++) {
        printf("%d ", arr->data[i]);
    }
    printf("\n");

    printf("Size: %zu, Capacity: %zu\n", arr->size, arr->capacity);

    // Shrink to fit
    da_shrink_to_fit(arr);
    printf("After shrink - Size: %zu, Capacity: %zu\n", arr->size, arr->capacity);

    da_destroy(arr);
    return 0;
}

Growth Strategies Comparison

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

// Strategy 1: Constant growth
// New capacity = old capacity + constant
// Pro: Predictable memory usage
// Con: Many reallocations for large arrays
size_t grow_constant(size_t current, size_t increment) {
    return current + increment;
}

// Strategy 2: Doubling (most common)
// New capacity = old capacity * 2
// Pro: Amortized O(1) insertions
// Con: May waste up to 50% memory
size_t grow_double(size_t current) {
    return current * 2;
}

// Strategy 3: Factor growth (e.g., 1.5x)
// New capacity = old capacity * 1.5
// Pro: Balance between memory and performance
// Con: Slightly more reallocations than doubling
size_t grow_factor(size_t current, double factor) {
    size_t new_cap = (size_t)(current * factor);
    return (new_cap > current) ? new_cap : current + 1;
}

// Strategy 4: Fibonacci growth
// New capacity based on Fibonacci sequence
// Pro: Good memory reuse patterns
// Con: More complex implementation
size_t grow_fibonacci(size_t current, size_t previous) {
    return current + previous;
}

void demonstrate_growth(const char *name, size_t initial, int iterations) {
    printf("\n%s (initial=%zu):\n", name, initial);
    size_t capacity = initial;
    size_t prev = initial;
    size_t total_waste = 0;

    for (int i = 0; i < iterations; i++) {
        size_t old = capacity;

        if (strcmp(name, "Constant (+10)") == 0) {
            capacity = grow_constant(capacity, 10);
        } else if (strcmp(name, "Doubling (x2)") == 0) {
            capacity = grow_double(capacity);
        } else if (strcmp(name, "Factor (x1.5)") == 0) {
            capacity = grow_factor(capacity, 1.5);
        } else {
            size_t new_cap = grow_fibonacci(capacity, prev);
            prev = capacity;
            capacity = new_cap;
        }

        printf("  Realloc %d: %zu -> %zu\n", i + 1, old, capacity);
        total_waste += (capacity - old - 1);  // Wasted slots
    }
}

#include <string.h>

int main() {
    int iterations = 5;

    demonstrate_growth("Constant (+10)", 10, iterations);
    demonstrate_growth("Doubling (x2)", 10, iterations);
    demonstrate_growth("Factor (x1.5)", 10, iterations);
    demonstrate_growth("Fibonacci", 10, iterations);

    return 0;
}

Shrinking Arrays

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

typedef struct {
    int *data;
    size_t size;
    size_t capacity;
} ShrinkableArray;

// Shrink when utilization falls below threshold
void sa_maybe_shrink(ShrinkableArray *sa) {
    // Shrink if using less than 25% of capacity
    // and capacity is larger than minimum
    const size_t MIN_CAPACITY = 8;

    if (sa->capacity > MIN_CAPACITY && sa->size < sa->capacity / 4) {
        size_t new_capacity = sa->capacity / 2;
        if (new_capacity < MIN_CAPACITY) {
            new_capacity = MIN_CAPACITY;
        }

        int *new_data = realloc(sa->data, new_capacity * sizeof(int));
        if (new_data) {
            sa->data = new_data;
            sa->capacity = new_capacity;
            printf("Shrunk capacity to %zu\n", new_capacity);
        }
    }
}

// Remove last element
int sa_pop(ShrinkableArray *sa, int *value) {
    if (sa->size == 0) return 0;

    *value = sa->data[--sa->size];
    sa_maybe_shrink(sa);
    return 1;
}

int main() {
    ShrinkableArray sa;
    sa.capacity = 64;
    sa.size = 60;
    sa.data = malloc(sa.capacity * sizeof(int));

    for (size_t i = 0; i < sa.size; i++) {
        sa.data[i] = (int)i;
    }

    printf("Initial: size=%zu, capacity=%zu\n", sa.size, sa.capacity);

    // Remove elements
    int value;
    while (sa.size > 5) {
        sa_pop(&sa, &value);
    }

    printf("Final: size=%zu, capacity=%zu\n", sa.size, sa.capacity);

    free(sa.data);
    return 0;
}

Common Pitfalls and Best Practices

Pitfall 1: Direct Assignment with realloc()

// DANGEROUS!
int *arr = malloc(10 * sizeof(int));
arr = realloc(arr, 20 * sizeof(int));  // If NULL, memory leaked!

// SAFE
int *arr = malloc(10 * sizeof(int));
int *temp = realloc(arr, 20 * sizeof(int));
if (temp == NULL) {
    // Handle error, arr is still valid
    free(arr);
    exit(1);
}
arr = temp;

Pitfall 2: Using Memory After Shrinking

int *arr = malloc(100 * sizeof(int));
for (int i = 0; i < 100; i++) arr[i] = i;

arr = realloc(arr, 10 * sizeof(int));

// WRONG! Elements beyond index 9 no longer exist
printf("%d\n", arr[50]);  // Undefined behavior!

// CORRECT - Only access valid indices
for (int i = 0; i < 10; i++) {
    printf("%d ", arr[i]);  // Safe
}

Pitfall 3: Forgetting New Memory is Uninitialized

int *arr = calloc(5, sizeof(int));  // [0, 0, 0, 0, 0]

arr = realloc(arr, 10 * sizeof(int));
// arr[5] through arr[9] contain GARBAGE, not zeros!

// SOLUTION: Initialize new elements
int *arr = calloc(5, sizeof(int));
size_t old_size = 5;
size_t new_size = 10;

arr = realloc(arr, new_size * sizeof(int));
memset(arr + old_size, 0, (new_size - old_size) * sizeof(int));

Pitfall 4: Integer Overflow in Size Calculations

// DANGEROUS with malloc
size_t count = 1000000000;
size_t size = sizeof(int);
int *arr = malloc(count * size);  // May overflow!

// SAFER with calloc
int *arr = calloc(count, size);  // Checks for overflow

Pitfall 5: Passing Stack Pointer to realloc()

// WRONG! Cannot realloc stack memory
int stack_arr[10];
int *p = realloc(stack_arr, 20 * sizeof(int));  // Undefined behavior!

// CORRECT - Start with heap memory
int *heap_arr = malloc(10 * sizeof(int));
int *p = realloc(heap_arr, 20 * sizeof(int));  // OK

Best Practices Summary

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

// Best Practice 1: Safe realloc wrapper
void *safe_realloc(void *ptr, size_t new_size, size_t *old_size) {
    void *new_ptr = realloc(ptr, new_size);
    if (new_ptr != NULL && old_size != NULL) {
        *old_size = new_size;
    }
    return new_ptr;
}

// Best Practice 2: calloc for zero-initialized arrays
int *create_zero_array(size_t count) {
    return calloc(count, sizeof(int));
}

// Best Practice 3: Helper for growing arrays
typedef struct {
    void *data;
    size_t elem_size;
    size_t count;
    size_t capacity;
} GenericArray;

int grow_array(GenericArray *arr) {
    size_t new_capacity = arr->capacity == 0 ? 8 : arr->capacity * 2;
    void *new_data = realloc(arr->data, new_capacity * arr->elem_size);

    if (new_data == NULL) {
        return 0;  // Keep original data
    }

    arr->data = new_data;
    arr->capacity = new_capacity;
    return 1;
}

// Best Practice 4: Clear function that zeros memory
void clear_array(GenericArray *arr) {
    if (arr->data && arr->capacity > 0) {
        memset(arr->data, 0, arr->capacity * arr->elem_size);
    }
    arr->count = 0;
}

int main() {
    printf("Demonstrating best practices...\n");

    // Using calloc for counter array
    int *counters = create_zero_array(100);
    counters[50]++;  // Safe, was initialized to 0
    printf("Counter at 50: %d\n", counters[50]);
    free(counters);

    return 0;
}

Advanced Techniques

Implementing reallocarray() (Safe Realloc)

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

// reallocarray - realloc with overflow checking
// (Available in some systems, but here's a portable implementation)
void *my_reallocarray(void *ptr, size_t nmemb, size_t size) {
    // Check for multiplication overflow
    if (nmemb != 0 && size > SIZE_MAX / nmemb) {
        errno = ENOMEM;
        return NULL;
    }
    return realloc(ptr, nmemb * size);
}

int main() {
    // Safe allocation even with large numbers
    int *arr = my_reallocarray(NULL, 1000, sizeof(int));

    if (arr == NULL) {
        perror("Allocation failed");
        return 1;
    }

    // Use array...
    arr[0] = 42;

    // Safely resize
    int *temp = my_reallocarray(arr, 2000, sizeof(int));
    if (temp) {
        arr = temp;
    }

    free(arr);
    return 0;
}

Memory Pool with Chunked Allocation

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

#define CHUNK_SIZE 1024

typedef struct Chunk {
    char data[CHUNK_SIZE];
    size_t used;
    struct Chunk *next;
} Chunk;

typedef struct {
    Chunk *head;
    Chunk *current;
} MemoryPool;

MemoryPool *pool_create(void) {
    MemoryPool *pool = malloc(sizeof(MemoryPool));
    if (!pool) return NULL;

    pool->head = calloc(1, sizeof(Chunk));
    if (!pool->head) {
        free(pool);
        return NULL;
    }

    pool->current = pool->head;
    return pool;
}

void *pool_alloc(MemoryPool *pool, size_t size) {
    if (size > CHUNK_SIZE) return NULL;  // Too large

    // Check if current chunk has space
    if (pool->current->used + size > CHUNK_SIZE) {
        // Allocate new chunk
        Chunk *new_chunk = calloc(1, sizeof(Chunk));
        if (!new_chunk) return NULL;

        pool->current->next = new_chunk;
        pool->current = new_chunk;
    }

    void *ptr = pool->current->data + pool->current->used;
    pool->current->used += size;
    return ptr;
}

void pool_destroy(MemoryPool *pool) {
    Chunk *current = pool->head;
    while (current) {
        Chunk *next = current->next;
        free(current);
        current = next;
    }
    free(pool);
}

int main() {
    MemoryPool *pool = pool_create();

    // Allocate many small objects without individual free
    for (int i = 0; i < 100; i++) {
        int *num = pool_alloc(pool, sizeof(int));
        if (num) *num = i;
    }

    printf("Allocated 100 integers using memory pool\n");

    // Free everything at once
    pool_destroy(pool);
    printf("Pool destroyed\n");

    return 0;
}

Flexible Array Member Allocation

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

// Structure with flexible array member
typedef struct {
    size_t length;
    char data[];  // Flexible array member
} FlexString;

FlexString *flexstring_create(const char *str) {
    size_t len = strlen(str);

    // Allocate struct + space for string + null terminator
    FlexString *fs = malloc(sizeof(FlexString) + len + 1);
    if (!fs) return NULL;

    fs->length = len;
    strcpy(fs->data, str);
    return fs;
}

FlexString *flexstring_append(FlexString *fs, const char *str) {
    size_t add_len = strlen(str);
    size_t new_len = fs->length + add_len;

    // Reallocate with additional space
    FlexString *new_fs = realloc(fs, sizeof(FlexString) + new_len + 1);
    if (!new_fs) return fs;  // Keep original on failure

    strcpy(new_fs->data + new_fs->length, str);
    new_fs->length = new_len;
    return new_fs;
}

int main() {
    FlexString *str = flexstring_create("Hello");
    printf("Initial: '%s' (length=%zu)\n", str->data, str->length);

    str = flexstring_append(str, ", ");
    str = flexstring_append(str, "World!");
    printf("After append: '%s' (length=%zu)\n", str->data, str->length);

    free(str);
    return 0;
}

Performance Considerations

When to Use Each Function

ScenarioRecommended Function
Need zero-initialized memorycalloc()
Need to resize existing allocationrealloc()
Large arrays where zero-init is freecalloc() (OS optimizes)
Small allocations, initialization not neededmalloc()
Security-sensitive (avoid info leaks)calloc()
Performance-critical, will overwrite anywaymalloc()

Benchmarking Example

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

#define ITERATIONS 1000
#define INITIAL_SIZE 1000
#define FINAL_SIZE 100000

void benchmark_incremental_growth(void) {
    clock_t start = clock();

    for (int iter = 0; iter < ITERATIONS; iter++) {
        int *arr = malloc(INITIAL_SIZE * sizeof(int));
        size_t capacity = INITIAL_SIZE;

        // Grow to final size incrementally (10 at a time)
        while (capacity < FINAL_SIZE) {
            int *temp = realloc(arr, (capacity + 10) * sizeof(int));
            if (temp) {
                arr = temp;
                capacity += 10;
            }
        }

        free(arr);
    }

    clock_t end = clock();
    printf("Incremental (+10): %.3f seconds\n",
           (double)(end - start) / CLOCKS_PER_SEC);
}

void benchmark_doubling_growth(void) {
    clock_t start = clock();

    for (int iter = 0; iter < ITERATIONS; iter++) {
        int *arr = malloc(INITIAL_SIZE * sizeof(int));
        size_t capacity = INITIAL_SIZE;

        // Grow to final size by doubling
        while (capacity < FINAL_SIZE) {
            int *temp = realloc(arr, capacity * 2 * sizeof(int));
            if (temp) {
                arr = temp;
                capacity *= 2;
            }
        }

        free(arr);
    }

    clock_t end = clock();
    printf("Doubling (x2): %.3f seconds\n",
           (double)(end - start) / CLOCKS_PER_SEC);
}

void benchmark_single_allocation(void) {
    clock_t start = clock();

    for (int iter = 0; iter < ITERATIONS; iter++) {
        int *arr = malloc(FINAL_SIZE * sizeof(int));
        free(arr);
    }

    clock_t end = clock();
    printf("Single allocation: %.3f seconds\n",
           (double)(end - start) / CLOCKS_PER_SEC);
}

int main() {
    printf("Benchmarking allocation strategies...\n\n");

    benchmark_incremental_growth();
    benchmark_doubling_growth();
    benchmark_single_allocation();

    return 0;
}

Memory Fragmentation

/*
 * Memory Fragmentation Illustration
 *
 * After many alloc/realloc/free cycles:
 *
 * +------+----+--------+----+------+----+--------+
 * | Used |Free|  Used  |Free| Used |Free|  Free  |
 * +------+----+--------+----+------+----+--------+
 *
 * Problem: Total free space might be sufficient,
 * but no single block is large enough!
 *
 * Solutions:
 * 1. Allocate larger blocks upfront
 * 2. Use memory pools
 * 3. Avoid frequent small reallocations
 * 4. Use size classes (like jemalloc)
 */

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

void demonstrate_fragmentation(void) {
    printf("Demonstrating potential fragmentation:\n");

    // Allocate alternating blocks
    void *blocks[100];
    for (int i = 0; i < 100; i++) {
        blocks[i] = malloc((i % 2 == 0) ? 1000 : 100);
    }

    // Free every other block
    for (int i = 0; i < 100; i += 2) {
        free(blocks[i]);
        blocks[i] = NULL;
    }

    // Now we have many small free blocks
    // A large allocation might fail even with enough total free space
    void *large = malloc(50000);
    if (large) {
        printf("Large allocation succeeded\n");
        free(large);
    } else {
        printf("Large allocation failed (fragmentation)\n");
    }

    // Clean up
    for (int i = 1; i < 100; i += 2) {
        free(blocks[i]);
    }
}

int main() {
    demonstrate_fragmentation();
    return 0;
}

Summary

Key Points

  1. •

    calloc() allocates and zero-initializes memory

    • •Takes element count and size separately
    • •Provides overflow protection
    • •Ideal for arrays that need zero initialization
  2. •

    realloc() resizes previously allocated memory

    • •Can expand or shrink allocations
    • •Preserves existing data (up to new size)
    • •New space (when expanding) is uninitialized
    • •Always use a temporary pointer
  3. •

    Comparison

    • •malloc(): Fast, uninitialized
    • •calloc(): Zero-initialized, overflow-safe
    • •realloc(): Resize existing allocations
  4. •

    Best Practices

    • •Use calloc() for counter/flag arrays
    • •Use temporary pointer with realloc()
    • •Initialize new elements after expanding
    • •Use doubling strategy for growing arrays
    • •Consider shrinking when utilization is low

Quick Reference

// calloc - allocate and zero-initialize
int *arr = calloc(n, sizeof(int));

// realloc - resize (use temp pointer!)
int *temp = realloc(arr, new_size);
if (temp != NULL) {
    arr = temp;
}

// realloc as malloc
int *p = realloc(NULL, size);  // Equivalent to malloc(size)

// Common pattern for dynamic growth
if (size >= capacity) {
    capacity *= 2;
    int *temp = realloc(data, capacity * sizeof(int));
    if (temp) data = temp;
}

Common Errors Checklist

  • • Always check return value for NULL
  • • Use temporary pointer with realloc()
  • • Don't realloc() stack memory
  • • Initialize new memory after realloc()
  • • Track both size and capacity
  • • Free memory before program exit
  • • Don't access memory beyond new size after shrinking

Next Steps

After mastering calloc() and realloc(), explore:

  • •Memory debugging tools (Valgrind, AddressSanitizer)
  • •Custom memory allocators
  • •Memory pools and arena allocation
  • •Dynamic data structures (linked lists, trees)

This tutorial is part of the C Programming Learning Series.

Calloc And Realloc - C Programming Tutorial | DeepML