Docs
README
Calloc and Realloc in C
Table of Contents
- •Introduction
- •The calloc() Function
- •The realloc() Function
- •Comparing malloc, calloc, and realloc
- •Memory Initialization Patterns
- •Resizing Dynamic Arrays
- •Common Pitfalls and Best Practices
- •Advanced Techniques
- •Performance Considerations
- •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
| Parameter | Description |
|---|---|
num_elements | Number of elements to allocate |
element_size | Size of each element in bytes |
Return Value
| Return | Description |
|---|---|
void * | Pointer to the allocated and zero-initialized memory |
NULL | Allocation 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()
| Situation | Use 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
| Parameter | Description |
|---|---|
ptr | Pointer to previously allocated memory block |
new_size | New size in bytes |
Return Value
| Return | Description |
|---|---|
void * | Pointer to reallocated memory (may be different from original) |
NULL | Reallocation 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
| Feature | malloc() | calloc() | realloc() |
|---|---|---|---|
| Parameters | Total size | Count + element size | Pointer + new size |
| Initialization | Uninitialized | Zero-initialized | Preserves + uninitialized |
| Overflow check | No | Yes | No |
| Use case | General allocation | Zero-init needed | Resizing |
| Previous pointer | N/A | N/A | Required (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
| Scenario | Recommended Function |
|---|---|
| Need zero-initialized memory | calloc() |
| Need to resize existing allocation | realloc() |
| Large arrays where zero-init is free | calloc() (OS optimizes) |
| Small allocations, initialization not needed | malloc() |
| Security-sensitive (avoid info leaks) | calloc() |
| Performance-critical, will overwrite anyway | malloc() |
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
- •
calloc() allocates and zero-initializes memory
- •Takes element count and size separately
- •Provides overflow protection
- •Ideal for arrays that need zero initialization
- •
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
- •
Comparison
- •
malloc(): Fast, uninitialized - •
calloc(): Zero-initialized, overflow-safe - •
realloc(): Resize existing allocations
- •
- •
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
- •Use
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.