C Programming: Low-Level Mastery
Dynamic Memory

malloc() and free()

Master dynamic memory allocation with malloc() and free(). Learn heap allocation, memory management, common pitfalls, memory leaks, dangling pointers, and best practices for manual memory control in C.

Understanding Dynamic Memory

C provides two memory regions: the stack (automatic) and the heap (dynamic). Stack memory is fast but limited and automatically freed. Heap memory is larger, persists until explicitly freed, and requires manual management. malloc() allocates from the heap, free() returns it.

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

/* Stack vs Heap comparison */
void memory_regions_example(void) {
    /* Stack allocation (automatic) */
    int stack_array[100];  /* Fixed size, automatic cleanup */
    
    /* Heap allocation (dynamic) */
    int *heap_array = malloc(100 * sizeof(int));  /* Variable size, manual cleanup */
    
    if (heap_array == NULL) {
        printf("Allocation failed\n");
        return;
    }
    
    /* Use heap_array like regular array */
    heap_array[0] = 42;
    heap_array[99] = 100;
    
    /* Must free manually */
    free(heap_array);
    
    /* stack_array freed automatically when function returns */
}

/* malloc() - memory allocation */
void* malloc(size_t size);

/* Returns:
   - Pointer to allocated memory if successful
   - NULL if allocation fails
   - Memory is uninitialized (garbage values)
*/

/* Basic malloc usage */
void basic_malloc_example(void) {
    /* Allocate memory for 10 integers */
    int *numbers = malloc(10 * sizeof(int));
    
    /* ALWAYS check for NULL */
    if (numbers == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return;
    }
    
    /* Use the memory */
    for (int i = 0; i < 10; i++) {
        numbers[i] = i * 10;
    }
    
    /* Free when done */
    free(numbers);
    
    /* Good practice: Set pointer to NULL after free */
    numbers = NULL;
}

/* Memory lifecycle */
/*
   1. Allocate:  ptr = malloc(size);
   2. Check:     if (ptr == NULL) { error }
   3. Use:       ptr[0] = value;
   4. Free:      free(ptr);
   5. Nullify:   ptr = NULL;
*/

malloc() Best Practices

Always check malloc() return value, use sizeof for portability, free memory when done, and avoid double frees. These practices prevent crashes, memory leaks, and undefined behavior.

C
/* Practice 1: Always use sizeof */
/* BAD: Hardcoded size */
int *bad = malloc(4 * 100);  /* Assumes int is 4 bytes */

/* GOOD: Use sizeof */
int *good = malloc(100 * sizeof(int));  /* Works on all platforms */

/* Practice 2: Check for NULL */
/* BAD: No check */
int *bad2 = malloc(1000 * sizeof(int));
bad2[0] = 42;  /* CRASH if allocation failed! */

/* GOOD: Check return value */
int *good2 = malloc(1000 * sizeof(int));
if (good2 == NULL) {
    perror("malloc failed");
    return -1;
}
good2[0] = 42;  /* Safe */

/* Practice 3: Use sizeof with variable type */
typedef struct {
    char name[50];
    int age;
    float salary;
} Employee;

/* BAD: Might be wrong if struct changes */
Employee *emp1 = malloc(100);  /* Magic number */

/* GOOD: Correct size always */
Employee *emp2 = malloc(sizeof(Employee));
if (emp2 == NULL) {
    return -1;
}

/* For arrays */
Employee *employees = malloc(10 * sizeof(Employee));
if (employees == NULL) {
    return -1;
}

/* Practice 4: Free when done */
/* BAD: Memory leak */
void leaky_function(void) {
    int *data = malloc(100 * sizeof(int));
    /* ... use data ... */
    /* Function returns without free() - LEAK! */
}

/* GOOD: Always free */
void good_function(void) {
    int *data = malloc(100 * sizeof(int));
    if (data == NULL) {
        return;
    }
    
    /* ... use data ... */
    
    free(data);  /* No leak */
    data = NULL;  /* Prevent dangling pointer */
}

/* Practice 5: Don't free stack memory */
void dont_free_stack(void) {
    int stack_array[10];
    
    /* WRONG: Don't free stack memory! */
    // free(stack_array);  /* CRASH! */
    
    /* Only free heap memory */
    int *heap_array = malloc(10 * sizeof(int));
    if (heap_array != NULL) {
        free(heap_array);  /* OK */
    }
}

/* Practice 6: Don't double free */
void avoid_double_free(void) {
    int *ptr = malloc(sizeof(int));
    if (ptr == NULL) {
        return;
    }
    
    *ptr = 42;
    
    free(ptr);  /* First free: OK */
    // free(ptr);  /* Second free: CRASH (undefined behavior)! */
    
    /* Solution: Set to NULL after free */
    ptr = NULL;
    free(NULL);  /* Safe: free(NULL) does nothing */
}

/* Practice 7: Check size before allocation */
int* safe_malloc_array(size_t count, size_t element_size) {
    /* Check for overflow */
    if (count == 0 || element_size == 0) {
        return NULL;
    }
    
    size_t total_size = count * element_size;
    
    /* Check for multiplication overflow */
    if (total_size / element_size != count) {
        fprintf(stderr, "Size overflow\n");
        return NULL;
    }
    
    return malloc(total_size);
}

free() and Memory Deallocation

free() releases dynamically allocated memory back to the system. Only free heap memory, never stack memory or static memory. After freeing, the pointer becomes dangling - accessing it causes undefined behavior. Set freed pointers to NULL.

C
/* free() - free allocated memory */
void free(void *ptr);

/* Rules:
   1. Only free heap memory (from malloc/calloc/realloc)
   2. Free each allocation exactly once
   3. Don't use pointer after freeing
   4. free(NULL) is safe (does nothing)
*/

/* Basic free usage */
void free_example(void) {
    int *ptr = malloc(sizeof(int));
    if (ptr == NULL) {
        return;
    }
    
    *ptr = 100;
    printf("Value: %d\n", *ptr);
    
    free(ptr);  /* Release memory */
    
    /* After free: ptr is dangling */
    // printf("%d\n", *ptr);  /* UNDEFINED BEHAVIOR! */
    
    ptr = NULL;  /* Make pointer safe */
    
    /* Now safe to check */
    if (ptr != NULL) {
        printf("%d\n", *ptr);  /* Won't execute */
    }
}

/* Freeing NULL is safe */
void free_null_is_safe(void) {
    int *ptr = NULL;
    free(ptr);  /* OK: Does nothing */
    
    int *ptr2 = malloc(sizeof(int));
    if (ptr2 == NULL) {
        free(ptr2);  /* Safe even though NULL */
        return;
    }
    
    free(ptr2);
}

/* Dangling pointer problem */
void dangling_pointer_problem(void) {
    int *ptr = malloc(sizeof(int));
    if (ptr == NULL) {
        return;
    }
    
    *ptr = 42;
    free(ptr);
    
    /* ptr now dangling */
    // *ptr = 100;  /* UNDEFINED BEHAVIOR */
    // int x = *ptr;  /* UNDEFINED BEHAVIOR */
    
    /* Solution */
    ptr = NULL;  /* No longer dangling */
}

/* Freeing structures with pointers */
typedef struct {
    char *name;
    int *scores;
} Student;

void free_student(Student *student) {
    if (student == NULL) {
        return;
    }
    
    /* Free nested allocations first */
    free(student-&gt;name);
    free(student-&gt;scores);
    
    /* Then free the structure */
    free(student);
}

/* Complex cleanup */
typedef struct Node {
    int data;
    struct Node *next;
} Node;

void free_list(Node *head) {
    Node *current = head;
    
    while (current != NULL) {
        Node *next = current-&gt;next;  /* Save next before free */
        free(current);
        current = next;
    }
}

/* Cleanup with error handling */
int allocate_resources(void) {
    int *buffer1 = malloc(100 * sizeof(int));
    if (buffer1 == NULL) {
        return -1;
    }
    
    int *buffer2 = malloc(200 * sizeof(int));
    if (buffer2 == NULL) {
        free(buffer1);  /* Clean up buffer1 */
        return -1;
    }
    
    int *buffer3 = malloc(300 * sizeof(int));
    if (buffer3 == NULL) {
        free(buffer2);  /* Clean up buffer2 */
        free(buffer1);  /* Clean up buffer1 */
        return -1;
    }
    
    /* Use buffers... */
    
    /* Clean up all */
    free(buffer3);
    free(buffer2);
    free(buffer1);
    
    return 0;
}

Common Memory Errors

Memory errors are subtle and dangerous: leaks waste memory, use-after-free causes crashes, buffer overflows corrupt data, double frees crash. Understanding these patterns helps you write safer code and debug faster.

C
/* Error 1: Memory Leak */
void memory_leak(void) {
    int *ptr = malloc(100 * sizeof(int));
    /* ... use ptr ... */
    /* Function returns without free() */
    /* Memory leaked! */
}

/* Fix: Always free */
void no_leak(void) {
    int *ptr = malloc(100 * sizeof(int));
    if (ptr == NULL) {
        return;
    }
    /* ... use ptr ... */
    free(ptr);  /* Good! */
}

/* Error 2: Use After Free */
void use_after_free(void) {
    int *ptr = malloc(sizeof(int));
    if (ptr == NULL) {
        return;
    }
    
    *ptr = 42;
    free(ptr);
    
    /* WRONG: Using freed memory */
    printf("%d\n", *ptr);  /* Undefined behavior */
    *ptr = 100;  /* Undefined behavior */
}

/* Fix: Don't use after free */
void safe_usage(void) {
    int *ptr = malloc(sizeof(int));
    if (ptr == NULL) {
        return;
    }
    
    *ptr = 42;
    printf("%d\n", *ptr);  /* Use before free */
    
    free(ptr);
    ptr = NULL;  /* Prevent accidental use */
}

/* Error 3: Double Free */
void double_free(void) {
    int *ptr = malloc(sizeof(int));
    if (ptr == NULL) {
        return;
    }
    
    free(ptr);
    free(ptr);  /* CRASH: Double free */
}

/* Fix: Set to NULL after free */
void no_double_free(void) {
    int *ptr = malloc(sizeof(int));
    if (ptr == NULL) {
        return;
    }
    
    free(ptr);
    ptr = NULL;
    
    free(ptr);  /* Safe: free(NULL) does nothing */
}

/* Error 4: Not Checking NULL */
void not_checking_null(void) {
    int *ptr = malloc(1000000000 * sizeof(int));  /* Might fail */
    *ptr = 42;  /* CRASH if allocation failed */
}

/* Fix: Always check NULL */
void checking_null(void) {
    int *ptr = malloc(1000000000 * sizeof(int));
    if (ptr == NULL) {
        fprintf(stderr, "Allocation failed\n");
        return;
    }
    *ptr = 42;  /* Safe */
    free(ptr);
}

/* Error 5: Buffer Overflow */
void buffer_overflow(void) {
    int *arr = malloc(10 * sizeof(int));
    if (arr == NULL) {
        return;
    }
    
    /* WRONG: Writing past end */
    for (int i = 0; i <= 10; i++) {  /* Should be < 10 */
        arr[i] = i;  /* arr[10] is out of bounds! */
    }
    
    free(arr);
}

/* Fix: Respect bounds */
void no_overflow(void) {
    int *arr = malloc(10 * sizeof(int));
    if (arr == NULL) {
        return;
    }
    
    for (int i = 0; i < 10; i++) {  /* Correct bound */
        arr[i] = i;
    }
    
    free(arr);
}

/* Error 6: Losing Pointer */
void losing_pointer(void) {
    int *ptr = malloc(100 * sizeof(int));
    if (ptr == NULL) {
        return;
    }
    
    ptr = NULL;  /* LEAK: Lost reference, can't free */
    /* Memory leaked! */
}

/* Fix: Save pointer or free before reassigning */
void saving_pointer(void) {
    int *ptr = malloc(100 * sizeof(int));
    if (ptr == NULL) {
        return;
    }
    
    /* Use it... */
    
    free(ptr);  /* Free before losing reference */
    ptr = NULL;  /* Now safe */
}

/* Error 7: Freeing Part of Block */
void freeing_part(void) {
    int *arr = malloc(10 * sizeof(int));
    if (arr == NULL) {
        return;
    }
    
    /* WRONG: Can't free part of allocation */
    // free(&arr[5]);  /* CRASH! */
    
    /* Must free entire block */
    free(arr);  /* Correct */
}

/* Error 8: Returning Local Pointer */
int* return_local_pointer(void) {
    int local = 42;
    return &local;  /* WRONG: Returning stack address */
}

/* Fix: Return allocated memory */
int* return_allocated_memory(void) {
    int *ptr = malloc(sizeof(int));
    if (ptr != NULL) {
        *ptr = 42;
    }
    return ptr;  /* OK: Caller must free */
}

/* Caller responsibility */
void use_returned_memory(void) {
    int *value = return_allocated_memory();
    if (value != NULL) {
        printf("%d\n", *value);
        free(value);  /* Must free! */
    }
}

Dynamic Arrays and Structures

Dynamic allocation enables runtime-sized arrays and structures. Useful when size is unknown at compile time or when you need large amounts of memory. Always track the size separately and validate indices.

C
/* Dynamic array */
int* create_array(size_t size) {
    int *arr = malloc(size * sizeof(int));
    if (arr == NULL) {
        return NULL;
    }
    
    /* Initialize */
    for (size_t i = 0; i < size; i++) {
        arr[i] = 0;
    }
    
    return arr;
}

void use_dynamic_array(void) {
    size_t size = 100;
    int *arr = create_array(size);
    
    if (arr == NULL) {
        fprintf(stderr, "Failed to create array\n");
        return;
    }
    
    /* Use array */
    for (size_t i = 0; i < size; i++) {
        arr[i] = i * 2;
    }
    
    free(arr);
}

/* Dynamic structure */
typedef struct {
    char name[50];
    int age;
    float salary;
} Employee;

Employee* create_employee(const char *name, int age, float salary) {
    Employee *emp = malloc(sizeof(Employee));
    if (emp == NULL) {
        return NULL;
    }
    
    strncpy(emp-&gt;name, name, sizeof(emp-&gt;name) - 1);
    emp-&gt;name[sizeof(emp-&gt;name) - 1] = '\0';
    emp-&gt;age = age;
    emp-&gt;salary = salary;
    
    return emp;
}

void use_employee(void) {
    Employee *emp = create_employee("John Doe", 30, 50000.0);
    if (emp == NULL) {
        return;
    }
    
    printf("Name: %s, Age: %d, Salary: %.2f\n",
           emp-&gt;name, emp-&gt;age, emp-&gt;salary);
    
    free(emp);
}

/* Array of structures */
Employee* create_employee_array(size_t count) {
    return malloc(count * sizeof(Employee));
}

void use_employee_array(void) {
    size_t count = 10;
    Employee *employees = create_employee_array(count);
    
    if (employees == NULL) {
        return;
    }
    
    /* Initialize */
    for (size_t i = 0; i < count; i++) {
        snprintf(employees[i].name, sizeof(employees[i].name),
                 "Employee %zu", i);
        employees[i].age = 25 + i;
        employees[i].salary = 40000.0 + (i * 5000.0);
    }
    
    /* Use */
    for (size_t i = 0; i < count; i++) {
        printf("%s: $%.2f\n", employees[i].name, employees[i].salary);
    }
    
    free(employees);
}

/* Dynamic 2D array */
int** create_2d_array(size_t rows, size_t cols) {
    int **arr = malloc(rows * sizeof(int*));
    if (arr == NULL) {
        return NULL;
    }
    
    for (size_t i = 0; i < rows; i++) {
        arr[i] = malloc(cols * sizeof(int));
        if (arr[i] == NULL) {
            /* Cleanup on failure */
            for (size_t j = 0; j < i; j++) {
                free(arr[j]);
            }
            free(arr);
            return NULL;
        }
    }
    
    return arr;
}

void free_2d_array(int **arr, size_t rows) {
    if (arr == NULL) {
        return;
    }
    
    for (size_t i = 0; i < rows; i++) {
        free(arr[i]);
    }
    free(arr);
}

void use_2d_array(void) {
    size_t rows = 3, cols = 4;
    int **matrix = create_2d_array(rows, cols);
    
    if (matrix == NULL) {
        return;
    }
    
    /* Use matrix */
    for (size_t i = 0; i < rows; i++) {
        for (size_t j = 0; j < cols; j++) {
            matrix[i][j] = i * cols + j;
        }
    }
    
    free_2d_array(matrix, rows);
}

Summary & What's Next

Key Takeaways:

  • ✅ malloc() allocates memory from the heap
  • ✅ Always check malloc() return value for NULL
  • ✅ free() releases allocated memory
  • ✅ Set pointers to NULL after free()
  • ✅ Never free stack memory or double free
  • ✅ Memory leaks waste resources over time
  • ✅ Use-after-free is undefined behavior
  • ✅ Track allocation sizes separately

What's Next?

Let's learn about calloc() and realloc() for advanced dynamic memory management!