Skip to main content

More malloc & free - Advanced Memory Allocation

This section covers advanced memory allocation techniques including error-handling wrappers, zero-initialized allocation, memory reallocation, and complex allocation patterns.

Function Prototypes

void *malloc_checked(unsigned int b);
char *string_nconcat(char *s1, char *s2, unsigned int n);
void *_calloc(unsigned int nmemb, unsigned int size);
int *array_range(int min, int max);
void *_realloc(void *ptr, unsigned int old_size, unsigned int new_size);

Error-Handling Allocation

malloc_checked() - Guaranteed Allocation

#include <stdlib.h>

/**
 * malloc_checked - allocates memory.
 * @b: amount of bytes.
 * Return: pointer to the allocated memory.
 * if fail, status value is equal to 98.
 */
void *malloc_checked(unsigned int b)
{
    char *g;
    
    g = malloc(b);
    if (g == NULL)
        exit(98);
    return (g);
}
Purpose: Simplifies error handling by terminating the program if allocation fails. When to use:
  • Critical allocations where failure is unrecoverable
  • Simplifying code when NULL checks are verbose
  • Programs that must allocate or die
Example:
// Without malloc_checked - verbose
int *arr = malloc(100 * sizeof(int));
if (arr == NULL)
{
    fprintf(stderr, "Fatal: out of memory\n");
    exit(98);
}

// With malloc_checked - concise
int *arr = malloc_checked(100 * sizeof(int));
// No need to check - either succeeds or program exits
Use malloc_checked carefully:
  • Good for: Critical resources in small programs
  • Bad for: Libraries, servers, long-running applications
  • Alternative: Graceful degradation (free resources, return error)
// Library function - DON'T exit
void *safe_malloc(size_t size)
{
    void *ptr = malloc(size);
    if (ptr == NULL)
        return (NULL);  // Let caller decide
    return (ptr);
}

Advanced String Operations

string_nconcat() - Concatenate with Limit

#include <stdlib.h>

/**
 * string_nconcat - concatenates two strings.
 * @s1: first string.
 * @s2: second string.
 * @n: amount of bytes.
 *
 * Return: pointer to the allocated memory.
 * if fail, status value is equal to 98.
 */
char *string_nconcat(char *s1, char *s2, unsigned int n)
{
    char *sout;
    unsigned int ls1, ls2, lsout, i;
    
    if (s1 == NULL)
        s1 = "";
    
    if (s2 == NULL)
        s2 = "";
    
    for (ls1 = 0; s1[ls1] != '\0'; ls1++)
        ;
    
    for (ls2 = 0; s2[ls2] != '\0'; ls2++)
        ;
    
    if (n > ls2)
        n = ls2;
    
    lsout = ls1 + n;
    
    sout = malloc(lsout + 1);
    
    if (sout == NULL)
        return (NULL);
    
    for (i = 0; i < lsout; i++)
        if (i < ls1)
            sout[i] = s1[i];
        else
            sout[i] = s2[i - ls1];
    
    sout[i] = '\0';
    
    return (sout);
}
Key features:
  1. NULL safety: Treats NULL as empty string
  2. Limit handling: Uses at most n bytes from s2
  3. Smart sizing: If n >= len(s2), uses entire s2
  4. Proper allocation: len(s1) + min(n, len(s2)) + 1
Examples:
char *s1 = "Hello";
char *s2 = " World";

// Concatenate first 3 chars of s2
char *result = string_nconcat(s1, s2, 3);
if (result != NULL)
{
    printf("%s\n", result);  // "Hello Wo"
    free(result);
}

// n larger than s2 - uses all of s2
result = string_nconcat(s1, s2, 100);
// result = "Hello World" (not "Hello World" + garbage)

// Handle NULL
result = string_nconcat(NULL, "Test", 4);
// result = "Test"

result = string_nconcat("Test", NULL, 4);
// result = "Test"
Memory calculation:
s1 = "Hello" (length 5)
s2 = " World" (length 6)
n = 3

Allocation size = 5 + min(3, 6) + 1 = 5 + 3 + 1 = 9 bytes
Result: "Hello Wo\0"
This function is safer than strcat because:
  1. Creates new string (doesn’t modify inputs)
  2. Limits bytes from s2 (prevents overflow)
  3. Handles NULL inputs gracefully
  4. Always null-terminates result

Zero-Initialized Allocation

_calloc() - Clear Allocation

#include <stdlib.h>

/**
 * _calloc - allocates memory for an array.
 * @nmemb: number of elements.
 * @size: size of bytes.
 *
 * Return: pointer to the allocated memory.
 * if nmemb or size is 0, returns NULL.
 * if malloc fails, returns NULL.
 */
void *_calloc(unsigned int nmemb, unsigned int size)
{
    char *p;
    unsigned int i;
    
    if (nmemb == 0 || size == 0)
        return (NULL);
    
    p = malloc(nmemb * size);
    
    if (p == NULL)
        return (NULL);
    
    for (i = 0; i < (nmemb * size); i++)
        p[i] = 0;
    
    return (p);
}
calloc vs malloc:
Featuremalloccalloc
ParametersSize in bytesNumber of elements + size each
InitializationUninitialized (garbage)Zero-initialized
Use caseAny allocationArrays, structures
SpeedFaster (no initialization)Slower (zeros memory)
Examples:
// malloc - uninitialized
int *arr1 = malloc(5 * sizeof(int));
// arr1 = {?, ?, ?, ?, ?} (garbage values)

// calloc - zero-initialized
int *arr2 = _calloc(5, sizeof(int));
// arr2 = {0, 0, 0, 0, 0}

// For structures
struct Person {
    char name[50];
    int age;
    double salary;
};

// All fields zeroed
struct Person *people = _calloc(10, sizeof(struct Person));
// people[0].age = 0, people[0].salary = 0.0, etc.
When to use calloc:
  1. Arrays that need zero initialization
    int *scores = _calloc(100, sizeof(int));  // All scores start at 0
    
  2. Bit flags and boolean arrays
    char *visited = _calloc(1000, sizeof(char));  // All false (0)
    
  3. Preventing uninitialized memory bugs
    char *buffer = _calloc(256, sizeof(char));  // Safe empty string
    
  4. Security-sensitive data
    char *password = _calloc(64, sizeof(char));  // No data leaks
    
Overflow risk with calloc:
// If nmemb * size overflows unsigned int
unsigned int nmemb = 0xFFFFFFFF;
unsigned int size = 2;
// nmemb * size wraps around to small number!

// Safer implementation (check overflow)
void *safe_calloc(unsigned int nmemb, unsigned int size)
{
    if (nmemb != 0 && size > UINT_MAX / nmemb)
        return (NULL);  // Overflow would occur
    return (_calloc(nmemb, size));
}

Array Generation

array_range() - Create Integer Range

#include <stdlib.h>

/**
 * array_range - creates an array of integers.
 * @min: minimum value.
 * @max: maximum value.
 *
 * Return: pointer to the newly created array.
 * if man > mix, returns NULL.
 * if malloc fails, returns NULL.
 */
int *array_range(int min, int max)
{
    int *ar;
    int i;
    
    if (min > max)
        return (NULL);
    
    ar = malloc(sizeof(*ar) * ((max - min) + 1));
    
    if (ar == NULL)
        return (NULL);
    
    for (i = 0; min <= max; i++, min++)
        ar[i] = min;
    
    return (ar);
}
Features:
  • Creates array containing integers from min to max (inclusive)
  • Automatically calculates array size: max - min + 1
  • Uses sizeof(*ar) for type-safe allocation
Examples:
// Simple range
int *arr = array_range(1, 5);
if (arr != NULL)
{
    // arr = {1, 2, 3, 4, 5}
    for (int i = 0; i < 5; i++)
        printf("%d ", arr[i]);
    printf("\n");
    free(arr);
}

// Negative range
int *arr2 = array_range(-3, 2);
// arr2 = {-3, -2, -1, 0, 1, 2}

// Single element
int *arr3 = array_range(42, 42);
// arr3 = {42}

// Invalid range
int *arr4 = array_range(10, 5);
// arr4 = NULL (min > max)
Size calculation:
min = 5, max = 10
Size = (10 - 5) + 1 = 6 elements
Array: {5, 6, 7, 8, 9, 10}

min = -2, max = 3
Size = (3 - (-2)) + 1 = 6 elements  
Array: {-2, -1, 0, 1, 2, 3}
Why sizeof(*ar) instead of sizeof(int)?
int *ar;
ar = malloc(sizeof(*ar) * n);  // Type-safe

// If you change type:
long *ar;  
ar = malloc(sizeof(*ar) * n);  // Still correct!

// Using sizeof(int):
long *ar;
ar = malloc(sizeof(int) * n);  // ⚠️ Wrong size!

Memory Reallocation

While not shown in the source files provided, realloc is a crucial function:

realloc() - Resize Allocated Memory

void *realloc(void *ptr, size_t new_size);
Behavior:
  • Resizes existing allocation
  • Preserves data (up to minimum of old/new size)
  • May move memory to new location
  • Returns new pointer (may differ from ptr)
Example implementation:
void *_realloc(void *ptr, unsigned int old_size, unsigned int new_size)
{
    void *new_ptr;
    unsigned int copy_size;
    
    if (new_size == 0)
    {
        free(ptr);
        return (NULL);
    }
    
    if (ptr == NULL)
        return (malloc(new_size));
    
    if (new_size == old_size)
        return (ptr);
    
    new_ptr = malloc(new_size);
    if (new_ptr == NULL)
        return (NULL);
    
    copy_size = (old_size < new_size) ? old_size : new_size;
    memcpy(new_ptr, ptr, copy_size);
    
    free(ptr);
    return (new_ptr);
}
Usage:
// Start with small array
int *arr = malloc(5 * sizeof(int));
for (int i = 0; i < 5; i++)
    arr[i] = i;

// Need more space
int *temp = _realloc(arr, 5 * sizeof(int), 10 * sizeof(int));
if (temp != NULL)
{
    arr = temp;  // Update pointer
    // arr[0-4] preserved, arr[5-9] available
    for (int i = 5; i < 10; i++)
        arr[i] = i;
}

free(arr);
Critical realloc rules:
  1. Never use old pointer after realloc
    int *arr = malloc(10);
    arr = realloc(arr, 20);  // ❌ BAD! Lost old pointer if realloc fails
    
    int *temp = realloc(arr, 20);  // ✅ GOOD
    if (temp != NULL)
        arr = temp;
    
  2. Don’t assume same address
    int *arr = malloc(10);
    int *old = arr;
    arr = realloc(arr, 20);
    // old might now be invalid!
    
  3. Free on failure
    int *temp = realloc(arr, new_size);
    if (temp == NULL)
    {
        free(arr);  // Original still allocated
        return (NULL);
    }
    arr = temp;
    

Advanced Patterns

Dynamic String Builder

typedef struct {
    char *data;
    size_t length;
    size_t capacity;
} StringBuilder;

StringBuilder *sb_create(size_t initial_capacity)
{
    StringBuilder *sb = malloc(sizeof(StringBuilder));
    if (sb == NULL)
        return (NULL);
    
    sb->data = malloc(initial_capacity);
    if (sb->data == NULL)
    {
        free(sb);
        return (NULL);
    }
    
    sb->data[0] = '\0';
    sb->length = 0;
    sb->capacity = initial_capacity;
    
    return (sb);
}

void sb_append(StringBuilder *sb, const char *str)
{
    size_t str_len = strlen(str);
    size_t new_length = sb->length + str_len;
    
    // Resize if needed
    if (new_length >= sb->capacity)
    {
        size_t new_capacity = sb->capacity * 2;
        if (new_capacity <= new_length)
            new_capacity = new_length + 1;
        
        char *new_data = realloc(sb->data, new_capacity);
        if (new_data == NULL)
            return;  // Handle error
        
        sb->data = new_data;
        sb->capacity = new_capacity;
    }
    
    strcpy(sb->data + sb->length, str);
    sb->length = new_length;
}

void sb_free(StringBuilder *sb)
{
    if (sb != NULL)
    {
        free(sb->data);
        free(sb);
    }
}

Memory Pool

typedef struct {
    void *memory;
    size_t size;
    size_t used;
} MemoryPool;

MemoryPool *pool_create(size_t size)
{
    MemoryPool *pool = malloc(sizeof(MemoryPool));
    if (pool == NULL)
        return (NULL);
    
    pool->memory = malloc(size);
    if (pool->memory == NULL)
    {
        free(pool);
        return (NULL);
    }
    
    pool->size = size;
    pool->used = 0;
    
    return (pool);
}

void *pool_alloc(MemoryPool *pool, size_t size)
{
    if (pool->used + size > pool->size)
        return (NULL);  // Pool exhausted
    
    void *ptr = (char *)pool->memory + pool->used;
    pool->used += size;
    
    return (ptr);
}

void pool_destroy(MemoryPool *pool)
{
    if (pool != NULL)
    {
        free(pool->memory);
        free(pool);
    }
}

Debugging and Optimization

Memory Usage Tracking

static size_t total_allocated = 0;
static size_t total_freed = 0;

void *tracked_malloc(size_t size)
{
    void *ptr = malloc(size);
    if (ptr != NULL)
        total_allocated += size;
    return (ptr);
}

void tracked_free(void *ptr, size_t size)
{
    if (ptr != NULL)
    {
        free(ptr);
        total_freed += size;
    }
}

void print_memory_stats(void)
{
    printf("Allocated: %zu bytes\n", total_allocated);
    printf("Freed: %zu bytes\n", total_freed);
    printf("Leaked: %zu bytes\n", total_allocated - total_freed);
}

Performance Tips

  1. Batch allocations when possible
    // Slower - many allocations
    for (int i = 0; i < 1000; i++)
        arr[i] = malloc(sizeof(int));
    
    // Faster - single allocation
    int *arr = malloc(1000 * sizeof(int));
    
  2. Use calloc only when needed
    // Unnecessary zeroing
    int *arr = calloc(100, sizeof(int));
    for (int i = 0; i < 100; i++)
        arr[i] = i;  // Overwriting zeros
    
    // Better - malloc and initialize once
    int *arr = malloc(100 * sizeof(int));
    for (int i = 0; i < 100; i++)
        arr[i] = i;
    
  3. Realloc geometrically
    // Bad - linear growth (many reallocations)
    new_size = old_size + 10;
    
    // Good - geometric growth (fewer reallocations)
    new_size = old_size * 2;
    

Key Takeaways

  • malloc_checked simplifies error handling but should be used judiciously
  • calloc zeros memory - use for arrays, structures, and security
  • calloc(n, size) can overflow - validate inputs
  • string_nconcat safely limits concatenation length
  • array_range demonstrates type-safe allocation with sizeof(*ptr)
  • realloc can move memory - always use temporary pointer
  • Never use old pointer after realloc without checking
  • Track allocations and frees to detect leaks
  • Batch allocations for better performance
  • Use geometric growth for dynamic arrays
  • Free in reverse order of allocation for complex structures

Build docs developers (and LLMs) love