calloc() and realloc()
Master advanced dynamic memory functions: calloc() for zero-initialized allocation and realloc() for resizing existing allocations. Learn when to use each, avoid common pitfalls, and manage growing data structures.
calloc() - Allocate and Zero
calloc() allocates memory and initializes it to zero. Unlike malloc() which leaves memory uninitialized, calloc() ensures all bytes are 0. This prevents bugs from garbage values and is essential for structures with pointers. Slightly slower than malloc() due to initialization.
#include <stdio.h>
#include <stdlib.h>
/* calloc() - allocate and clear */
void* calloc(size_t num_elements, size_t element_size);
/* Returns:
- Pointer to zeroed memory if successful
- NULL if allocation fails
- All bytes set to 0
*/
/* malloc vs calloc comparison */
void compare_malloc_calloc(void) {
/* malloc: uninitialized (garbage) */
int *arr1 = malloc(5 * sizeof(int));
if (arr1 != NULL) {
printf("malloc: ");
for (int i = 0; i < 5; i++) {
printf("%d ", arr1[i]); /* Random values */
}
printf("\n");
free(arr1);
}
/* calloc: zero-initialized */
int *arr2 = calloc(5, sizeof(int));
if (arr2 != NULL) {
printf("calloc: ");
for (int i = 0; i < 5; i++) {
printf("%d ", arr2[i]); /* All zeros */
}
printf("\n");
free(arr2);
}
}
/* calloc usage */
void calloc_example(void) {
/* Allocate array of 10 integers, all zeros */
int *numbers = calloc(10, sizeof(int));
if (numbers == NULL) {
fprintf(stderr, "Allocation failed\n");
return;
}
/* All elements are 0 */
printf("Initial values: ");
for (int i = 0; i < 10; i++) {
printf("%d ", numbers[i]); /* 0 0 0 0 0 0 0 0 0 0 */
}
printf("\n");
free(numbers);
}
/* calloc for structures */
typedef struct {
int id;
char *name;
float salary;
} Employee;
Employee* create_employee_calloc(void) {
/* Allocate and zero */
Employee *emp = calloc(1, sizeof(Employee));
if (emp == NULL) {
return NULL;
}
/* All fields are 0/NULL */
/* id = 0, name = NULL, salary = 0.0 */
return emp;
}
/* When to use calloc */
/*
Use calloc when:
- Want zero-initialized memory
- Working with structures containing pointers
- Need consistent initial state
- Debugging (zeros easier to spot than garbage)
Use malloc when:
- Will immediately initialize all values
- Performance critical (avoid zero overhead)
- Large allocations where zeroing is expensive
*/
/* calloc with overflow checking */
void calloc_overflow_example(void) {
size_t count = 1000000;
size_t size = sizeof(int);
/* calloc checks for overflow automatically */
int *arr = calloc(count, size);
if (arr == NULL) {
/* Could be out of memory OR overflow */
fprintf(stderr, "calloc failed\n");
return;
}
free(arr);
}malloc() vs calloc()
Understanding the differences helps you choose correctly. malloc() is faster but uninitialized. calloc() is safer but slower. For most cases, the safety of zero-initialization is worth the small performance cost.
/* Difference 1: Initialization */
void initialization_difference(void) {
/* malloc: garbage values */
int *m = malloc(sizeof(int));
if (m != NULL) {
printf("malloc: %d (garbage)\n", *m);
free(m);
}
/* calloc: zero */
int *c = calloc(1, sizeof(int));
if (c != NULL) {
printf("calloc: %d (zero)\n", *c); /* 0 */
free(c);
}
}
/* Difference 2: Arguments */
void argument_difference(void) {
/* malloc: total size */
int *m = malloc(10 * sizeof(int)); /* Manual multiplication */
/* calloc: count and size */
int *c = calloc(10, sizeof(int)); /* Separate arguments */
free(m);
free(c);
}
/* Difference 3: Overflow checking */
void overflow_checking(void) {
size_t huge = SIZE_MAX / 2;
/* malloc: Might not detect overflow */
int *m = malloc(huge * sizeof(int)); /* May wrap around */
/* calloc: Better overflow detection */
int *c = calloc(huge, sizeof(int)); /* More likely to fail safely */
if (m != NULL) free(m);
if (c != NULL) free(c);
}
/* Equivalent malloc to calloc */
void* malloc_like_calloc(size_t count, size_t size) {
void *ptr = malloc(count * size);
if (ptr != NULL) {
memset(ptr, 0, count * size); /* Zero manually */
}
return ptr;
}
/* Performance comparison */
void performance_test(void) {
const size_t SIZE = 10000000; /* 10 million */
/* malloc: faster but uninitialized */
int *arr1 = malloc(SIZE * sizeof(int));
if (arr1 != NULL) {
/* Must initialize if needed */
for (size_t i = 0; i < SIZE; i++) {
arr1[i] = 0;
}
free(arr1);
}
/* calloc: slightly slower but pre-zeroed */
int *arr2 = calloc(SIZE, sizeof(int));
if (arr2 != NULL) {
/* Already zeroed */
free(arr2);
}
}
/* When zero-initialization matters */
typedef struct Node {
int data;
struct Node *next;
} Node;
Node* create_node_malloc(int data) {
Node *node = malloc(sizeof(Node));
if (node == NULL) {
return NULL;
}
/* Must initialize explicitly */
node->data = data;
node->next = NULL; /* IMPORTANT! */
return node;
}
Node* create_node_calloc(int data) {
Node *node = calloc(1, sizeof(Node));
if (node == NULL) {
return NULL;
}
/* Already zeroed: data=0, next=NULL */
node->data = data; /* Only need to set non-zero values */
return node;
}realloc() - Resize Allocation
realloc() changes the size of an existing allocation. Essential for dynamic arrays that grow or shrink. May move the memory block, so always use the return value. Old pointer becomes invalid if block moves. Understanding realloc() enables efficient resizable data structures.
/* realloc() - resize allocated memory */
void* realloc(void *ptr, size_t new_size);
/* Behavior:
- ptr == NULL: Acts like malloc(new_size)
- new_size == 0: Acts like free(ptr) (implementation-defined)
- new_size < old_size: Shrinks (may not move)
- new_size > old_size: Grows (may move)
- Returns new pointer (may be same or different)
- Old data preserved up to min(old_size, new_size)
- New space uninitialized if growing
- Returns NULL on failure (old block unchanged)
*/
/* Basic realloc usage */
void realloc_example(void) {
/* Start with small allocation */
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) {
return;
}
/* Initialize */
for (int i = 0; i < 5; i++) {
arr[i] = i;
}
printf("Original: ");
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
printf("\n");
/* Grow to 10 elements */
int *new_arr = realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
/* Realloc failed, arr still valid */
fprintf(stderr, "Realloc failed\n");
free(arr);
return;
}
/* Update pointer */
arr = new_arr;
/* Old data preserved, new space uninitialized */
printf("After realloc: ");
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]); /* First 5 preserved */
}
printf("\n");
free(arr);
}
/* CRITICAL: realloc error handling */
void realloc_error_handling(void) {
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) {
return;
}
/* WRONG: Loses original pointer on failure */
// arr = realloc(arr, 10 * sizeof(int));
// if (arr == NULL) {
// /* Original array lost! Memory leak! */
// }
/* RIGHT: Use temporary pointer */
int *temp = realloc(arr, 10 * sizeof(int));
if (temp == NULL) {
/* Realloc failed, arr still valid */
fprintf(stderr, "Realloc failed\n");
free(arr); /* Can still free original */
return;
}
/* Success: update pointer */
arr = temp;
free(arr);
}
/* Growing array incrementally */
typedef struct {
int *data;
size_t size;
size_t capacity;
} DynamicArray;
DynamicArray* create_array(void) {
DynamicArray *arr = malloc(sizeof(DynamicArray));
if (arr == NULL) {
return NULL;
}
arr->data = malloc(10 * sizeof(int));
if (arr->data == NULL) {
free(arr);
return NULL;
}
arr->size = 0;
arr->capacity = 10;
return arr;
}
int array_push(DynamicArray *arr, int value) {
if (arr->size >= arr->capacity) {
/* Need to grow */
size_t new_capacity = arr->capacity * 2;
int *new_data = realloc(arr->data, new_capacity * sizeof(int));
if (new_data == NULL) {
return -1; /* Failed to grow */
}
arr->data = new_data;
arr->capacity = new_capacity;
}
arr->data[arr->size++] = value;
return 0;
}
void free_array(DynamicArray *arr) {
if (arr != NULL) {
free(arr->data);
free(arr);
}
}
void use_dynamic_array(void) {
DynamicArray *arr = create_array();
if (arr == NULL) {
return;
}
/* Add elements (will grow automatically) */
for (int i = 0; i < 100; i++) {
if (array_push(arr, i) != 0) {
fprintf(stderr, "Failed to add element\n");
break;
}
}
printf("Array size: %zu, capacity: %zu\n", arr->size, arr->capacity);
free_array(arr);
}realloc() Patterns and Best Practices
realloc() enables powerful patterns: growing arrays, shrinking to fit, memory-efficient data structures. Following best practices prevents leaks, crashes, and data loss.
/* Pattern 1: Growing strategy */
void* grow_array(void *ptr, size_t *current_size) {
size_t new_size = (*current_size) * 2; /* Double size */
void *new_ptr = realloc(ptr, new_size);
if (new_ptr != NULL) {
*current_size = new_size;
}
return new_ptr; /* Returns NULL on failure, ptr unchanged */
}
/* Pattern 2: Shrink to fit */
int* shrink_to_fit(int *arr, size_t current_capacity, size_t actual_size) {
if (actual_size == 0) {
free(arr);
return NULL;
}
if (actual_size < current_capacity) {
int *new_arr = realloc(arr, actual_size * sizeof(int));
if (new_arr != NULL) {
return new_arr; /* Shrunk successfully */
}
/* If shrink fails, keep original (unusual but possible) */
}
return arr;
}
/* Pattern 3: realloc with NULL (acts like malloc) */
void realloc_null(void) {
int *arr = realloc(NULL, 10 * sizeof(int)); /* Same as malloc */
if (arr != NULL) {
free(arr);
}
}
/* Pattern 4: Safe realloc wrapper */
void* safe_realloc(void *ptr, size_t new_size) {
if (new_size == 0) {
free(ptr);
return NULL;
}
void *new_ptr = realloc(ptr, new_size);
if (new_ptr == NULL) {
/* Realloc failed, original still valid */
fprintf(stderr, "Realloc failed for size %zu\n", new_size);
}
return new_ptr;
}
/* Pattern 5: Growing with initialization */
int* grow_and_initialize(int *arr, size_t old_size, size_t new_size, int init_value) {
int *new_arr = realloc(arr, new_size * sizeof(int));
if (new_arr == NULL) {
return NULL;
}
/* Initialize new elements */
for (size_t i = old_size; i < new_size; i++) {
new_arr[i] = init_value;
}
return new_arr;
}
/* Pattern 6: Avoid frequent reallocs */
typedef struct {
char *buffer;
size_t length;
size_t capacity;
} String;
int string_append(String *str, const char *text) {
size_t text_len = strlen(text);
size_t new_length = str->length + text_len;
/* Grow only if necessary */
if (new_length >= str->capacity) {
/* Grow by more than needed to avoid frequent reallocs */
size_t new_capacity = (new_length + 1) * 2;
char *new_buffer = realloc(str->buffer, new_capacity);
if (new_buffer == NULL) {
return -1;
}
str->buffer = new_buffer;
str->capacity = new_capacity;
}
memcpy(str->buffer + str->length, text, text_len + 1);
str->length = new_length;
return 0;
}
/* Pattern 7: Array resize with data preservation */
int* resize_array(int *arr, size_t old_size, size_t new_size) {
int *new_arr = realloc(arr, new_size * sizeof(int));
if (new_arr == NULL) {
return NULL;
}
/* If growing, initialize new elements */
if (new_size > old_size) {
for (size_t i = old_size; i < new_size; i++) {
new_arr[i] = 0;
}
}
return new_arr;
}
/* Pattern 8: Move semantics */
void move_allocation(int **dest, int **src, size_t size) {
/* Free destination if allocated */
free(*dest);
/* Transfer ownership */
*dest = *src;
*src = NULL; /* Source no longer owns */
}Common realloc() Pitfalls
realloc() has subtle failure modes. Losing pointers, invalidating references, and assuming realloc never fails are common mistakes. Understanding these pitfalls prevents hard-to-debug errors.
/* Pitfall 1: Losing original pointer */
void pitfall_lost_pointer(void) {
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) return;
/* WRONG */
arr = realloc(arr, 10 * sizeof(int));
if (arr == NULL) {
/* Original pointer lost! Memory leak! */
return;
}
free(arr);
}
/* Fix */
void fixed_lost_pointer(void) {
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) return;
/* RIGHT */
int *temp = realloc(arr, 10 * sizeof(int));
if (temp == NULL) {
free(arr); /* Can still free original */
return;
}
arr = temp;
free(arr);
}
/* Pitfall 2: Assuming pointer doesn't move */
void pitfall_pointer_moves(void) {
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) return;
int *ptr = &arr[2]; /* Pointer to element */
arr = realloc(arr, 100 * sizeof(int)); /* May move! */
/* ptr now INVALID if arr moved */
// *ptr = 42; /* UNDEFINED BEHAVIOR */
free(arr);
}
/* Fix: Recalculate pointers */
void fixed_pointer_moves(void) {
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) return;
size_t index = 2; /* Store index, not pointer */
int *temp = realloc(arr, 100 * sizeof(int));
if (temp == NULL) {
free(arr);
return;
}
arr = temp;
/* Recalculate pointer */
int *ptr = &arr[index]; /* Now valid */
*ptr = 42;
free(arr);
}
/* Pitfall 3: realloc(ptr, 0) behavior */
void pitfall_realloc_zero(void) {
int *arr = malloc(10 * sizeof(int));
if (arr == NULL) return;
/* Implementation-defined behavior */
arr = realloc(arr, 0);
/* May return NULL and free, or return non-NULL */
/* Safer: explicit free */
free(arr);
}
/* Pitfall 4: Not checking return value */
void pitfall_no_check(void) {
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) return;
/* WRONG: Assumes success */
arr = realloc(arr, 10000000000); /* Might fail */
arr[0] = 42; /* CRASH if realloc returned NULL */
free(arr);
}
/* Pitfall 5: Passing already-freed pointer */
void pitfall_freed_pointer(void) {
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) return;
free(arr);
/* WRONG: arr already freed */
arr = realloc(arr, 10 * sizeof(int)); /* UNDEFINED BEHAVIOR */
}
/* Pitfall 6: Multiple references */
void pitfall_multiple_references(void) {
int *arr1 = malloc(5 * sizeof(int));
if (arr1 == NULL) return;
int *arr2 = arr1; /* Both point to same memory */
int *temp = realloc(arr1, 10 * sizeof(int));
if (temp == NULL) {
free(arr1);
return;
}
arr1 = temp;
/* arr2 now INVALID (dangling) */
// arr2[0] = 42; /* UNDEFINED BEHAVIOR */
free(arr1);
}
/* Fix: Update all references */
void fixed_multiple_references(void) {
int *arr1 = malloc(5 * sizeof(int));
if (arr1 == NULL) return;
int **refs[] = {&arr1, NULL}; /* Track all references */
int *temp = realloc(*refs[0], 10 * sizeof(int));
if (temp == NULL) {
free(arr1);
return;
}
/* Update all references */
for (int i = 0; refs[i] != NULL; i++) {
*refs[i] = temp;
}
free(arr1);
}Memory Allocation Strategy
Choosing the right allocation function and growth strategy impacts performance and memory usage. Understand when to use each function and how to balance memory efficiency with allocation overhead.
/* Strategy comparison */
/* malloc: Fast, uninitialized */
int* strategy_malloc(size_t count) {
int *arr = malloc(count * sizeof(int));
if (arr != NULL) {
/* Initialize yourself */
for (size_t i = 0; i < count; i++) {
arr[i] = i;
}
}
return arr;
}
/* calloc: Safe, zero-initialized */
int* strategy_calloc(size_t count) {
int *arr = calloc(count, sizeof(int));
/* Already zeroed */
return arr;
}
/* realloc: Efficient resizing */
int* strategy_realloc(int *arr, size_t old_count, size_t new_count) {
int *new_arr = realloc(arr, new_count * sizeof(int));
if (new_arr != NULL && new_count > old_count) {
/* Initialize new elements */
for (size_t i = old_count; i < new_count; i++) {
new_arr[i] = 0;
}
}
return new_arr;
}
/* Growth strategies */
/* Strategy 1: Double capacity (common) */
size_t grow_double(size_t current) {
return current == 0 ? 1 : current * 2;
}
/* Strategy 2: Fibonacci growth */
size_t grow_fibonacci(size_t current, size_t previous) {
return current + previous;
}
/* Strategy 3: Fixed increment */
size_t grow_fixed(size_t current, size_t increment) {
return current + increment;
}
/* Strategy 4: Percentage growth */
size_t grow_percentage(size_t current, unsigned int percent) {
return current + (current * percent / 100);
}
/* Complete dynamic array implementation */
typedef struct {
void *data;
size_t element_size;
size_t count;
size_t capacity;
} GenericArray;
GenericArray* array_create(size_t element_size) {
GenericArray *arr = malloc(sizeof(GenericArray));
if (arr == NULL) {
return NULL;
}
arr->data = malloc(element_size * 10); /* Initial capacity */
if (arr->data == NULL) {
free(arr);
return NULL;
}
arr->element_size = element_size;
arr->count = 0;
arr->capacity = 10;
return arr;
}
int array_push(GenericArray *arr, const void *element) {
if (arr->count >= arr->capacity) {
size_t new_capacity = arr->capacity * 2;
void *new_data = realloc(arr->data, new_capacity * arr->element_size);
if (new_data == NULL) {
return -1;
}
arr->data = new_data;
arr->capacity = new_capacity;
}
memcpy((char*)arr->data + (arr->count * arr->element_size),
element, arr->element_size);
arr->count++;
return 0;
}
void* array_get(GenericArray *arr, size_t index) {
if (index >= arr->count) {
return NULL;
}
return (char*)arr->data + (index * arr->element_size);
}
void array_free(GenericArray *arr) {
if (arr != NULL) {
free(arr->data);
free(arr);
}
}Summary & What's Next
Key Takeaways:
- ✅ calloc() allocates and zero-initializes memory
- ✅ Use calloc() for pointers and structures needing zeroes
- ✅ realloc() resizes existing allocations efficiently
- ✅ Always use temp pointer with realloc() to avoid leaks
- ✅ realloc() may move memory - update all references
- ✅ Growing by doubling reduces reallocation overhead
- ✅ Zero-initialize new space when growing
- ✅ Choose allocation function based on needs