C Programming Dynamic Memory
Advanced Concept

C Dynamic Memory Allocation - Complete Guide

Master C dynamic memory allocation including static vs dynamic memory, malloc, calloc, realloc, free functions, memory management techniques, and programming best practices.

Static vs Dynamic

Memory comparison

4 Functions

malloc, calloc, realloc, free

Memory Errors

Leaks, dangling pointers

Best Practices

Safe memory management

Introduction to Dynamic Memory Allocation

Dynamic memory allocation allows programs to request memory at runtime rather than compile time. This enables flexible data structures that can grow or shrink as needed, efficient memory usage, and handling of data whose size is unknown at compile time.

Why Dynamic Memory Matters
  • Runtime Flexibility: Allocate memory as needed during execution
  • Efficient Memory Usage: Use only what you need
  • Data Structures: Essential for linked lists, trees, graphs
  • Unknown Size: Handle data of unpredictable size
  • Large Data: Allocate memory beyond stack limits
  • Shared Memory: Create memory that persists beyond function calls
Memory Allocation Functions
  • malloc(): Allocate raw memory block
  • calloc(): Allocate and zero-initialize
  • realloc(): Resize existing allocation
  • free(): Release allocated memory
  • Memory Leak: Allocated memory not freed
  • Dangling Pointer: Pointer to freed memory

Core Concepts of Dynamic Memory

Dynamic memory is allocated from the heap segment, managed manually by the programmer. Every malloc/calloc must have a corresponding free. The heap grows upward, while the stack grows downward. Understanding this memory model is crucial for preventing memory errors and writing efficient programs.

Static vs Dynamic Memory Comparison

Here is a comprehensive comparison of static and dynamic memory allocation in C:

Feature Static Memory Allocation Dynamic Memory Allocation
Allocation Time Compile time Runtime
Memory Segment Stack/Data segment Heap segment
Size Determination Fixed at compile time Determined at runtime
Lifetime Automatic (function scope) or Program duration Until explicitly freed
Management Automatic by compiler Manual by programmer
Speed Faster (compile-time) Slower (runtime overhead)
Flexibility Fixed size Can grow/shrink
Risk Stack overflow if too large Memory leaks if not freed
Usage Small, known-size data Large, variable-size data
Example int arr[100]; int *arr = malloc(100*sizeof(int));

Memory Segments Visualization:

Code Segment
Program instructions
main()
func1()
func2()
Data Segment
Static/Global variables
global_var
static_var
const_data
Heap Segment
Dynamic memory (grows upward)
malloc()
calloc()
freed
available
Stack Segment
Local variables (grows downward)
main vars
func1 vars
func2 vars
Choosing Between Static and Dynamic: Use static allocation for small, fixed-size data with known lifetime. Use dynamic allocation for large data, data with unknown size at compile time, or data that needs to persist beyond function scope. Always consider memory constraints and performance requirements when choosing.

malloc() Function - Complete Guide

The malloc() (memory allocation) function allocates a block of memory of specified size in bytes and returns a pointer to the beginning of the block. The allocated memory contains garbage values (uninitialized).

Syntax:
void* malloc(size_t size);

Parameters: size - Number of bytes to allocate

Return Value: Pointer to allocated memory, or NULL if allocation fails

Key Characteristics:

  • Uninitialized Memory: Contains garbage values (random data)
  • Return Type: void* (generic pointer) - requires casting
  • NULL Check: Always check if malloc returns NULL
  • Size Calculation: Use sizeof() operator for portability
  • Memory Alignment: Returns memory aligned for any data type
  • Heap Allocation: Memory comes from heap segment

Memory Lifecycle Visualization:

1. Request Memory
2. malloc() Called
3. Memory Allocated
4. Use Memory
5. free() Called
6. Memory Freed

malloc() Examples:

Example 1: Basic malloc() Usage
#include <stdio.h>
#include <stdlib.h>  // Required for malloc, free
#include <string.h>

int main() {
    printf("=== BASIC malloc() USAGE ===\n\n");
    
    printf("1. Allocating single variable:\n");
    
    // Allocate memory for one integer
    int *singleInt = (int*)malloc(sizeof(int));
    
    // CRITICAL: Check if allocation succeeded
    if(singleInt == NULL) {
        printf("   Error: Memory allocation failed!\n");
        return 1;  // Exit with error code
    }
    
    // Use the allocated memory
    *singleInt = 42;
    printf("   Allocated integer: %d\n", *singleInt);
    printf("   Address: %p\n", (void*)singleInt);
    
    // Free the memory when done
    free(singleInt);
    singleInt = NULL;  // Good practice to prevent dangling pointer
    
    printf("\n2. Allocating array with malloc:\n");
    
    int arraySize = 5;
    
    // Allocate memory for 5 integers
    int *dynamicArray = (int*)malloc(arraySize * sizeof(int));
    
    if(dynamicArray == NULL) {
        printf("   Error: Array allocation failed!\n");
        return 1;
    }
    
    printf("   Array allocated at: %p\n", (void*)dynamicArray);
    
    // Initialize array (contains garbage values initially)
    printf("   Initial (garbage) values: ");
    for(int i = 0; i < arraySize; i++) {
        printf("%d ", dynamicArray[i]);  // Random values
    }
    
    // Set values
    for(int i = 0; i < arraySize; i++) {
        dynamicArray[i] = i * 10;
    }
    
    printf("\n   After initialization: ");
    for(int i = 0; i < arraySize; i++) {
        printf("%d ", dynamicArray[i]);
    }
    
    // Calculate memory size
    size_t totalBytes = arraySize * sizeof(int);
    printf("\n   Total memory allocated: %zu bytes\n", totalBytes);
    
    // Free the array
    free(dynamicArray);
    dynamicArray = NULL;
    
    printf("\n3. String allocation with malloc:\n");
    
    // Allocate memory for string (including null terminator)
    char *dynamicString = (char*)malloc(50 * sizeof(char));
    
    if(dynamicString == NULL) {
        printf("   Error: String allocation failed!\n");
        return 1;
    }
    
    // Copy string to allocated memory
    strcpy(dynamicString, "Hello, Dynamic Memory!");
    printf("   Dynamic string: %s\n", dynamicString);
    printf("   String length: %zu\n", strlen(dynamicString));
    printf("   Memory used: %zu bytes\n", strlen(dynamicString) + 1);
    
    free(dynamicString);
    dynamicString = NULL;
    
    printf("\n4. Structure allocation:\n");
    
    typedef struct {
        int id;
        char name[50];
        float salary;
    } Employee;
    
    // Allocate memory for one Employee
    Employee *emp = (Employee*)malloc(sizeof(Employee));
    
    if(emp != NULL) {
        emp->id = 101;
        strcpy(emp->name, "John Doe");
        emp->salary = 55000.50;
        
        printf("   Employee: %d, %s, $%.2f\n", 
               emp->id, emp->name, emp->salary);
        printf("   Structure size: %zu bytes\n", sizeof(Employee));
        
        free(emp);
        emp = NULL;
    }
    
    printf("\n5. Common malloc() mistakes:\n");
    
    // Mistake 1: Forgetting to check NULL
    int *badPtr = (int*)malloc(1000000000 * sizeof(int));  // Too large!
    // if(badPtr == NULL) ...  // MISSING: Program may crash
    
    // Mistake 2: Wrong size calculation
    // int *wrongSize = malloc(5);  // WRONG: Should be 5 * sizeof(int)
    
    // Mistake 3: Not freeing memory (memory leak)
    // int *leaky = malloc(sizeof(int));
    // Use leaky...
    // Forgot: free(leaky);  // MEMORY LEAK!
    
    // Mistake 4: Using freed memory
    // int *dangling = malloc(sizeof(int));
    // free(dangling);
    // *dangling = 10;  // DANGEROUS: Use after free
    
    printf("   Always: Check NULL, Calculate size correctly, Free memory\n");
    
    return 0;
}
Output:
=== BASIC malloc() USAGE ===

1. Allocating single variable:
Allocated integer: 42
Address: 0x55a1b2c3d4e5 (example)

2. Allocating array with malloc:
Array allocated at: 0x55a1b2c3d510 (example)
Initial (garbage) values: 0 0 4196432 0 0 (random)
After initialization: 0 10 20 30 40
Total memory allocated: 20 bytes

3. String allocation with malloc:
Dynamic string: Hello, Dynamic Memory!
String length: 22
Memory used: 23 bytes

4. Structure allocation:
Employee: 101, John Doe, $55000.50
Structure size: 60 bytes

5. Common malloc() mistakes:
Always: Check NULL, Calculate size correctly, Free memory
CRITICAL: malloc() Safety Rules
  1. Always include <stdlib.h>
  2. Always check if malloc returns NULL
  3. Use sizeof() for correct size calculation
  4. Cast the return value to appropriate type
  5. Initialize memory before use (contains garbage)
  6. Always call free() when done
  7. Set pointer to NULL after free()
  8. Don't call free() on non-malloc memory
Example 2: Advanced malloc() Patterns
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Safe malloc wrapper
void* safe_malloc(size_t size) {
    void *ptr = malloc(size);
    if(ptr == NULL) {
        fprintf(stderr, "Fatal error: Memory allocation failed!\n");
        exit(EXIT_FAILURE);
    }
    return ptr;
}

// Function to demonstrate dynamic array growth
int* create_dynamic_array(int initial_size) {
    int *arr = (int*)safe_malloc(initial_size * sizeof(int));
    
    // Initialize with sequence
    for(int i = 0; i < initial_size; i++) {
        arr[i] = i * i;  // Squares
    }
    
    return arr;
}

// Function to demonstrate ragged array (array of strings)
char** create_string_array(int count, const char* prefix) {
    // Allocate array of string pointers
    char **strings = (char**)safe_malloc(count * sizeof(char*));
    
    for(int i = 0; i < count; i++) {
        // Calculate needed memory for this string
        int needed = snprintf(NULL, 0, "%s%d", prefix, i) + 1;
        
        // Allocate exact amount needed
        strings[i] = (char*)safe_malloc(needed * sizeof(char));
        
        // Create the string
        snprintf(strings[i], needed, "%s%d", prefix, i);
    }
    
    return strings;
}

int main() {
    printf("=== ADVANCED malloc() PATTERNS ===\n\n");
    
    printf("1. Dynamic 2D array using single malloc:\n");
    
    int rows = 3, cols = 4;
    
    // Allocate contiguous memory for entire matrix
    int *matrix = (int*)safe_malloc(rows * cols * sizeof(int));
    
    // Initialize as flat array
    for(int i = 0; i < rows * cols; i++) {
        matrix[i] = i + 1;
    }
    
    // Access using row-major order: matrix[row * cols + col]
    printf("   Matrix (%dx%d):\n", rows, cols);
    for(int i = 0; i < rows; i++) {
        printf("   ");
        for(int j = 0; j < cols; j++) {
            printf("%3d ", matrix[i * cols + j]);
        }
        printf("\n");
    }
    
    free(matrix);
    matrix = NULL;
    
    printf("\n2. Dynamic 2D array using array of pointers:\n");
    
    // Allocate array of row pointers
    int **matrix2 = (int**)safe_malloc(rows * sizeof(int*));
    
    // Allocate each row separately
    for(int i = 0; i < rows; i++) {
        matrix2[i] = (int*)safe_malloc(cols * sizeof(int));
        for(int j = 0; j < cols; j++) {
            matrix2[i][j] = (i + 1) * (j + 1);  // Multiplication table
        }
    }
    
    printf("   Matrix2 (%dx%d):\n", rows, cols);
    for(int i = 0; i < rows; i++) {
        printf("   ");
        for(int j = 0; j < cols; j++) {
            printf("%3d ", matrix2[i][j]);
        }
        printf("\n");
    }
    
    // Free each row, then the array of pointers
    for(int i = 0; i < rows; i++) {
        free(matrix2[i]);
        matrix2[i] = NULL;
    }
    free(matrix2);
    matrix2 = NULL;
    
    printf("\n3. Ragged array (array of strings):\n");
    
    int string_count = 4;
    char **strings = create_string_array(string_count, "Item");
    
    printf("   String array:\n");
    for(int i = 0; i < string_count; i++) {
        printf("   [%d] %s (length: %zu)\n", 
               i, strings[i], strlen(strings[i]));
    }
    
    // Free each string, then the array
    for(int i = 0; i < string_count; i++) {
        free(strings[i]);
        strings[i] = NULL;
    }
    free(strings);
    strings = NULL;
    
    printf("\n4. Memory allocation for function return:\n");
    
    int initial_size = 5;
    int *dynamic_array = create_dynamic_array(initial_size);
    
    printf("   Dynamic array from function:\n   ");
    for(int i = 0; i < initial_size; i++) {
        printf("%d ", dynamic_array[i]);
    }
    printf("\n");
    
    // Important: Caller must free this memory!
    free(dynamic_array);
    dynamic_array = NULL;
    
    printf("\n5. Memory allocation patterns comparison:\n");
    
    // Pattern A: Single allocation (contiguous)
    int *patternA = (int*)safe_malloc(100 * sizeof(int));
    // Pros: Contiguous, cache-friendly
    // Cons: Fixed size, hard to resize
    
    // Pattern B: Array of pointers
    int **patternB = (int**)safe_malloc(10 * sizeof(int*));
    for(int i = 0; i < 10; i++) {
        patternB[i] = (int*)safe_malloc(10 * sizeof(int));
    }
    // Pros: Flexible rows, easy to resize rows independently
    // Cons: Not contiguous, more allocations/frees
    
    // Cleanup pattern A
    free(patternA);
    patternA = NULL;
    
    // Cleanup pattern B
    for(int i = 0; i < 10; i++) {
        free(patternB[i]);
        patternB[i] = NULL;
    }
    free(patternB);
    patternB = NULL;
    
    printf("   Pattern A: Single allocation - fast, contiguous\n");
    printf("   Pattern B: Array of pointers - flexible, independent rows\n");
    
    return 0;
}

calloc() Function - Complete Guide

The calloc() (contiguous allocation) function allocates memory for an array of elements, initializes all bytes to zero, and returns a pointer to the memory. It's ideal for arrays and structures that need zero initialization.

Syntax:
void* calloc(size_t num, size_t size);

Parameters: num - Number of elements, size - Size of each element in bytes

Return Value: Pointer to allocated memory (all bits zero), or NULL if allocation fails

Key Characteristics:

  • Zero-Initialized: All bits set to zero (0 for integers, 0.0 for floats, NULL for pointers)
  • Two Parameters: Takes number of elements and size of each element
  • Array-Oriented: Designed for allocating arrays
  • Slower than malloc: Extra overhead for zero initialization
  • Memory Calculation: Total memory = num × size bytes
  • Safer: Zero initialization prevents garbage values

calloc() vs malloc() Comparison:

Feature malloc() calloc()
Parameters 1 (total bytes) 2 (count, element size)
Initialization Garbage values All bits zero
Speed Faster Slower (initialization overhead)
Use Case General allocation Arrays, need initialization
Syntax Example malloc(10 * sizeof(int)) calloc(10, sizeof(int))
Memory Content Unpredictable Predictable (zeros)

calloc() Examples:

Example: Comprehensive calloc() Usage
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    printf("=== COMPREHENSIVE calloc() USAGE ===\n\n");
    
    printf("1. Basic calloc() for arrays:\n");
    
    int array_size = 5;
    
    // Allocate and zero-initialize array of 5 integers
    int *zero_array = (int*)calloc(array_size, sizeof(int));
    
    if(zero_array == NULL) {
        printf("   Error: calloc() failed!\n");
        return 1;
    }
    
    printf("   calloc() array (all zeros): ");
    for(int i = 0; i < array_size; i++) {
        printf("%d ", zero_array[i]);  // All zeros
    }
    
    // Compare with malloc()
    int *garbage_array = (int*)malloc(array_size * sizeof(int));
    printf("\n   malloc() array (garbage): ");
    for(int i = 0; i < array_size; i++) {
        printf("%d ", garbage_array[i]);  // Random values
    }
    
    free(zero_array);
    free(garbage_array);
    
    printf("\n\n2. Structure array with calloc():\n");
    
    typedef struct {
        int id;
        char name[30];
        float score;
        int *grades;
    } Student;
    
    int student_count = 3;
    
    // Allocate array of Student structures
    Student *students = (Student*)calloc(student_count, sizeof(Student));
    
    if(students == NULL) {
        printf("   Error: Student allocation failed!\n");
        return 1;
    }
    
    printf("   Student structures after calloc():\n");
    for(int i = 0; i < student_count; i++) {
        printf("   Student %d: id=%d, name='%s', score=%.2f, grades=%p\n",
               i, students[i].id, 
               students[i].name,  // Empty string (first char is '\0')
               students[i].score, // 0.0
               (void*)students[i].grades);  // NULL pointer
    }
    
    // Initialize students
    for(int i = 0; i < student_count; i++) {
        students[i].id = 1000 + i;
        snprintf(students[i].name, 30, "Student%d", i);
        students[i].score = 75.5 + i * 5;
        
        // Allocate grades array for each student
        students[i].grades = (int*)calloc(5, sizeof(int));
        for(int j = 0; j < 5; j++) {
            students[i].grades[j] = 60 + (i * 5) + j;
        }
    }
    
    printf("\n   After initialization:\n");
    for(int i = 0; i < student_count; i++) {
        printf("   Student %d: %s, Score: %.1f, Grades: ",
               students[i].id, students[i].name, students[i].score);
        for(int j = 0; j < 5; j++) {
            printf("%d ", students[i].grades[j]);
        }
        printf("\n");
    }
    
    // Free nested allocations first
    for(int i = 0; i < student_count; i++) {
        free(students[i].grades);
        students[i].grades = NULL;
    }
    
    // Then free the main array
    free(students);
    students = NULL;
    
    printf("\n3. 2D array with calloc():\n");
    
    int rows = 3, cols = 4;
    
    // Method 1: Single calloc() for contiguous memory
    int *matrix1 = (int*)calloc(rows * cols, sizeof(int));
    
    // Initialize with values
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            matrix1[i * cols + j] = i * cols + j + 1;
        }
    }
    
    printf("   Matrix1 (contiguous):\n");
    for(int i = 0; i < rows; i++) {
        printf("   ");
        for(int j = 0; j < cols; j++) {
            printf("%3d ", matrix1[i * cols + j]);
        }
        printf("\n");
    }
    
    free(matrix1);
    matrix1 = NULL;
    
    // Method 2: Array of pointers with calloc()
    printf("\n   Matrix2 (array of pointers):\n");
    
    int **matrix2 = (int**)calloc(rows, sizeof(int*));
    for(int i = 0; i < rows; i++) {
        matrix2[i] = (int*)calloc(cols, sizeof(int));
        for(int j = 0; j < cols; j++) {
            matrix2[i][j] = (i + 1) * (j + 1);
        }
    }
    
    for(int i = 0; i < rows; i++) {
        printf("   ");
        for(int j = 0; j < cols; j++) {
            printf("%3d ", matrix2[i][j]);
        }
        printf("\n");
    }
    
    // Free matrix2
    for(int i = 0; i < rows; i++) {
        free(matrix2[i]);
        matrix2[i] = NULL;
    }
    free(matrix2);
    matrix2 = NULL;
    
    printf("\n4. String array with calloc():\n");
    
    int word_count = 4;
    int max_word_len = 20;
    
    // Allocate array of string pointers (all NULL initially)
    char **words = (char**)calloc(word_count, sizeof(char*));
    
    // Allocate and initialize each string
    const char *word_list[] = {"apple", "banana", "cherry", "date"};
    
    for(int i = 0; i < word_count; i++) {
        // Allocate exact size for each word
        words[i] = (char*)calloc(strlen(word_list[i]) + 1, sizeof(char));
        strcpy(words[i], word_list[i]);
    }
    
    printf("   String array:\n");
    for(int i = 0; i < word_count; i++) {
        printf("   [%d] '%s' (length: %zu)\n", 
               i, words[i], strlen(words[i]));
    }
    
    // Free strings
    for(int i = 0; i < word_count; i++) {
        free(words[i]);
        words[i] = NULL;
    }
    free(words);
    words = NULL;
    
    printf("\n5. When to use calloc() vs malloc():\n");
    
    printf("   Use calloc() when:\n");
    printf("   • You need zero-initialized memory\n");
    printf("   • Working with arrays of numeric types\n");
    printf("   • Allocating structures with pointer fields (initialized to NULL)\n");
    printf("   • Security-sensitive data (no leftover data from previous use)\n");
    printf("\n   Use malloc() when:\n");
    printf("   • Performance is critical\n");
    printf("   • You'll immediately overwrite all values\n");
    printf("   • Memory will be used as raw buffer\n");
    
    printf("\n6. Performance consideration:\n");
    
    // Performance test
    const int large_size = 1000000;
    
    clock_t start, end;
    double malloc_time, calloc_time;
    
    // Time malloc
    start = clock();
    int *malloc_test = (int*)malloc(large_size * sizeof(int));
    // Initialize manually
    for(int i = 0; i < large_size; i++) {
        malloc_test[i] = 0;
    }
    end = clock();
    malloc_time = ((double)(end - start)) / CLOCKS_PER_SEC;
    free(malloc_test);
    
    // Time calloc
    start = clock();
    int *calloc_test = (int*)calloc(large_size, sizeof(int));
    end = clock();
    calloc_time = ((double)(end - start)) / CLOCKS_PER_SEC;
    free(calloc_test);
    
    printf("   For %d integers:\n", large_size);
    printf("   • malloc() + manual zero: %.4f seconds\n", malloc_time);
    printf("   • calloc(): %.4f seconds\n", calloc_time);
    printf("   Note: calloc() may use optimized zeroing\n");
    
    return 0;
}
calloc() Memory Note: calloc() not only sets memory to zero but also ensures memory is "clean" from any previous data. This is particularly important for security-sensitive applications where leftover data in memory could be a security risk. Additionally, some operating systems implement calloc() using lazy allocation, where physical memory isn't actually allocated until it's written to.

realloc() Function - Complete Guide

The realloc() (reallocate) function changes the size of a previously allocated memory block. It can expand or shrink the allocation while preserving existing data (as much as possible).

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

Parameters: ptr - Pointer to previously allocated memory, new_size - New size in bytes

Return Value: Pointer to reallocated memory (may be different), or NULL if allocation fails

Key Characteristics:

  • Size Change: Can increase or decrease allocation size
  • Data Preservation: Preserves existing data up to minimum of old and new size
  • Pointer May Change: May return different pointer address
  • NULL Pointer: If ptr is NULL, realloc() behaves like malloc()
  • Zero Size: If new_size is 0 and ptr is not NULL, memory is freed
  • Performance: May need to copy data if can't expand in place

realloc() Behavior Scenarios:

Scenario realloc() Behavior Result
ptr is NULL Acts like malloc(new_size) New allocation
new_size is 0 Acts like free(ptr) Memory freed, returns NULL
Enough space after block Extends in place Same pointer, larger size
Not enough space after Allocates new block, copies data, frees old New pointer, old data copied
Smaller new_size Shrinks block (may free excess) Same pointer, smaller size
Allocation fails Returns NULL, original block unchanged Need to handle failure

realloc() Examples:

Example: Comprehensive realloc() Usage
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Safe realloc wrapper
void* safe_realloc(void *ptr, size_t new_size) {
    void *new_ptr = realloc(ptr, new_size);
    if(new_ptr == NULL && new_size > 0) {
        fprintf(stderr, "Error: realloc() failed!\n");
        free(ptr);  // Free old memory on failure
        exit(EXIT_FAILURE);
    }
    return new_ptr;
}

// Dynamic array implementation
typedef struct {
    int *data;
    size_t size;
    size_t capacity;
} DynamicArray;

DynamicArray* create_dynamic_array(size_t initial_capacity) {
    DynamicArray *arr = (DynamicArray*)malloc(sizeof(DynamicArray));
    arr->data = (int*)calloc(initial_capacity, sizeof(int));
    arr->size = 0;
    arr->capacity = initial_capacity;
    return arr;
}

void push_back(DynamicArray *arr, int value) {
    // If array is full, double the capacity
    if(arr->size >= arr->capacity) {
        size_t new_capacity = arr->capacity * 2;
        if(new_capacity == 0) new_capacity = 1;
        
        arr->data = (int*)safe_realloc(arr->data, new_capacity * sizeof(int));
        
        // Zero out new elements
        for(size_t i = arr->capacity; i < new_capacity; i++) {
            arr->data[i] = 0;
        }
        
        arr->capacity = new_capacity;
        printf("   Resized: %zu -> %zu elements\n", arr->capacity/2, arr->capacity);
    }
    
    arr->data[arr->size] = value;
    arr->size++;
}

int main() {
    printf("=== COMPREHENSIVE realloc() USAGE ===\n\n");
    
    printf("1. Basic realloc() operations:\n");
    
    // Start with small array
    int *arr = (int*)malloc(3 * sizeof(int));
    for(int i = 0; i < 3; i++) {
        arr[i] = (i + 1) * 10;
    }
    
    printf("   Original array (size 3): ");
    for(int i = 0; i < 3; i++) {
        printf("%d ", arr[i]);
    }
    
    // Expand array
    arr = (int*)realloc(arr, 6 * sizeof(int));
    if(arr == NULL) {
        printf("   Error: realloc() failed!\n");
        free(arr);
        return 1;
    }
    
    // Initialize new elements
    for(int i = 3; i < 6; i++) {
        arr[i] = (i + 1) * 10;
    }
    
    printf("\n   After expansion (size 6): ");
    for(int i = 0; i < 6; i++) {
        printf("%d ", arr[i]);
    }
    
    // Shrink array
    arr = (int*)realloc(arr, 4 * sizeof(int));
    printf("\n   After shrink (size 4): ");
    for(int i = 0; i < 4; i++) {
        printf("%d ", arr[i]);
    }
    
    free(arr);
    arr = NULL;
    
    printf("\n\n2. String resizing with realloc():\n");
    
    char *str = (char*)malloc(10 * sizeof(char));
    strcpy(str, "Hello");
    printf("   Original: '%s' (capacity: 10)\n", str);
    
    // Need more space
    str = (char*)realloc(str, 50 * sizeof(char));
    strcat(str, ", World! This is a longer string.");
    printf("   After realloc: '%s' (capacity: 50)\n", str);
    
    // Shrink to fit exact size
    size_t needed = strlen(str) + 1;
    str = (char*)realloc(str, needed * sizeof(char));
    printf("   After exact fit: '%s' (capacity: %zu)\n", str, needed);
    
    free(str);
    str = NULL;
    
    printf("\n3. Dynamic array implementation:\n");
    
    DynamicArray *dyn_arr = create_dynamic_array(2);
    
    printf("   Adding 10 elements:\n");
    for(int i = 1; i <= 10; i++) {
        push_back(dyn_arr, i * 100);
        printf("   Added %d, size: %zu, capacity: %zu\n", 
               i * 100, dyn_arr->size, dyn_arr->capacity);
    }
    
    printf("\n   Final array (%zu elements):\n   ", dyn_arr->size);
    for(size_t i = 0; i < dyn_arr->size; i++) {
        printf("%d ", dyn_arr->data[i]);
        if((i + 1) % 5 == 0 && i + 1 < dyn_arr->size) {
            printf("\n   ");
        }
    }
    
    // Cleanup
    free(dyn_arr->data);
    free(dyn_arr);
    
    printf("\n\n4. Common realloc() patterns:\n");
    
    // Pattern 1: Reallocating with temporary variable
    printf("   Pattern 1: Using temp variable for safety:\n");
    
    int *data = (int*)malloc(5 * sizeof(int));
    for(int i = 0; i < 5; i++) data[i] = i + 1;
    
    int *temp = (int*)realloc(data, 10 * sizeof(int));
    if(temp == NULL) {
        printf("   Realloc failed, keeping original\n");
        // data still valid with original 5 elements
    } else {
        data = temp;  // Only assign if realloc succeeded
        printf("   Realloc succeeded\n");
    }
    
    free(data);
    
    // Pattern 2: Growing array exponentially
    printf("\n   Pattern 2: Exponential growth (amortized O(1)):\n");
    
    int *exp_arr = NULL;
    size_t exp_capacity = 0;
    size_t exp_size = 0;
    
    for(int i = 0; i < 20; i++) {
        if(exp_size >= exp_capacity) {
            // Double capacity (or start with 1)
            size_t new_capacity = exp_capacity == 0 ? 1 : exp_capacity * 2;
            int *new_arr = (int*)realloc(exp_arr, new_capacity * sizeof(int));
            
            if(new_arr == NULL) {
                printf("   Growth failed at i=%d\n", i);
                break;
            }
            
            exp_arr = new_arr;
            exp_capacity = new_capacity;
            printf("   Grow to %zu elements\n", exp_capacity);
        }
        
        exp_arr[exp_size] = i * 10;
        exp_size++;
    }
    
    free(exp_arr);
    
    printf("\n5. realloc() with different data types:\n");
    
    // Structure reallocation
    typedef struct {
        char name[50];
        int age;
    } Person;
    
    Person *people = (Person*)malloc(2 * sizeof(Person));
    strcpy(people[0].name, "Alice");
    people[0].age = 30;
    strcpy(people[1].name, "Bob");
    people[1].age = 25;
    
    printf("   Original people array:\n");
    for(int i = 0; i < 2; i++) {
        printf("   %s, %d\n", people[i].name, people[i].age);
    }
    
    // Add more people
    people = (Person*)realloc(people, 4 * sizeof(Person));
    strcpy(people[2].name, "Charlie");
    people[2].age = 35;
    strcpy(people[3].name, "Diana");
    people[3].age = 28;
    
    printf("   After realloc (4 people):\n");
    for(int i = 0; i < 4; i++) {
        printf("   %s, %d\n", people[i].name, people[i].age);
    }
    
    free(people);
    
    printf("\n6. Important realloc() warnings:\n");
    
    // WARNING 1: Don't use original pointer after failed realloc
    int *warning1 = (int*)malloc(5 * sizeof(int));
    int *original = warning1;  // Save original
    
    warning1 = (int*)realloc(warning1, 1000000000000ULL * sizeof(int));
    if(warning1 == NULL) {
        printf("   Warning 1: realloc() failed\n");
        // original is still valid here!
        // free(original);  // Should free original
    }
    
    // WARNING 2: Zero-size realloc frees memory
    int *warning2 = (int*)malloc(10 * sizeof(int));
    warning2 = (int*)realloc(warning2, 0);  // Equivalent to free(warning2)
    // warning2 is now NULL
    
    printf("   Always: Check return value, Handle failure gracefully\n");
    
    return 0;
}
CRITICAL: realloc() Safety Rules
  1. Always use temporary variable: new_ptr = realloc(old_ptr, size)
  2. Check if realloc returns NULL before assigning
  3. If realloc fails, original pointer is still valid
  4. realloc(NULL, size) behaves like malloc(size)
  5. realloc(ptr, 0) behaves like free(ptr)
  6. When expanding, new memory area is uninitialized
  7. When shrinking, excess memory is freed (data may be lost)
  8. Pointer may change address - update all references

free() Function - Complete Guide

The free() function deallocates memory previously allocated by malloc(), calloc(), or realloc(). It returns the memory to the heap for reuse. Proper memory deallocation is crucial to prevent memory leaks.

Syntax:
void free(void* ptr);

Parameters: ptr - Pointer to memory block to free

Return Value: None (void)

Key Characteristics:

  • Memory Deallocation: Returns memory to heap
  • No Return Value: Function returns void
  • NULL Safe: free(NULL) does nothing (safe to call)
  • Dangling Pointers: Pointer becomes invalid after free()
  • Memory Leak: Forgetting to free() causes memory leak
  • Double Free: Calling free() twice on same pointer causes undefined behavior
  • Heap Corruption: Freeing non-heap memory or corrupted pointers

Memory Leak Visualization:

Memory Leak Over Time
Block 1
Block 2
Block 3
Block 4
Leaked!
Block 5
Block 6
Leaked!
Leaked!

Memory blocks that are allocated but never freed become leaks, reducing available memory over time

free() Examples:

Example: Comprehensive free() Usage and Memory Management
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Safe free wrapper that sets pointer to NULL
#define SAFE_FREE(ptr) \
    do { \
        free(ptr); \
        (ptr) = NULL; \
    } while(0)

// Function demonstrating proper cleanup
char* create_greeting(const char *name) {
    // Calculate needed memory
    size_t len = strlen("Hello, ") + strlen(name) + strlen("!") + 1;
    char *greeting = (char*)malloc(len * sizeof(char));
    
    if(greeting == NULL) {
        return NULL;
    }
    
    snprintf(greeting, len, "Hello, %s!", name);
    return greeting;
}

// Complex structure with nested allocations
typedef struct {
    char *name;
    int *scores;
    int score_count;
    struct Node *next;
} Node;

Node* create_node(const char *name, int *scores, int count) {
    Node *node = (Node*)malloc(sizeof(Node));
    if(node == NULL) return NULL;
    
    // Allocate and copy name
    node->name = (char*)malloc((strlen(name) + 1) * sizeof(char));
    if(node->name == NULL) {
        free(node);
        return NULL;
    }
    strcpy(node->name, name);
    
    // Allocate and copy scores
    node->scores = (int*)malloc(count * sizeof(int));
    if(node->scores == NULL) {
        free(node->name);
        free(node);
        return NULL;
    }
    memcpy(node->scores, scores, count * sizeof(int));
    
    node->score_count = count;
    node->next = NULL;
    
    return node;
}

// Proper cleanup for Node
void destroy_node(Node *node) {
    if(node == NULL) return;
    
    // Free nested allocations first
    free(node->name);
    free(node->scores);
    
    // Then free the node itself
    free(node);
}

int main() {
    printf("=== COMPREHENSIVE free() USAGE ===\n\n");
    
    printf("1. Basic free() operations:\n");
    
    // Single allocation
    int *single = (int*)malloc(sizeof(int));
    *single = 42;
    printf("   Before free: %d at %p\n", *single, (void*)single);
    
    free(single);
    // single is now a dangling pointer!
    printf("   After free: pointer is dangling\n");
    
    // Good practice: Set to NULL after free
    single = NULL;
    printf("   After NULL: pointer is safe\n");
    
    printf("\n2. Array deallocation:\n");
    
    int *array = (int*)malloc(5 * sizeof(int));
    for(int i = 0; i < 5; i++) {
        array[i] = i * 10;
    }
    
    printf("   Array contents: ");
    for(int i = 0; i < 5; i++) {
        printf("%d ", array[i]);
    }
    
    free(array);
    array = NULL;
    printf("\n   Array freed and pointer set to NULL\n");
    
    printf("\n3. Using SAFE_FREE macro:\n");
    
    char *str = (char*)malloc(50 * sizeof(char));
    strcpy(str, "This will be safely freed");
    printf("   String: %s\n", str);
    
    SAFE_FREE(str);
    printf("   After SAFE_FREE: str = %p\n", (void*)str);
    
    // Safe to call SAFE_FREE on NULL pointer
    SAFE_FREE(str);  // Does nothing (free(NULL) is safe)
    
    printf("\n4. Function returning allocated memory:\n");
    
    char *greeting = create_greeting("World");
    if(greeting != NULL) {
        printf("   Greeting: %s\n", greeting);
        
        // Caller is responsible for freeing!
        free(greeting);
        greeting = NULL;
        printf("   Greeting freed by caller\n");
    }
    
    printf("\n5. Complex structure cleanup:\n");
    
    int test_scores[] = {85, 90, 78};
    Node *student = create_node("Alice", test_scores, 3);
    
    if(student != NULL) {
        printf("   Created node: %s\n", student->name);
        printf("   Scores: ");
        for(int i = 0; i < student->score_count; i++) {
            printf("%d ", student->scores[i]);
        }
        printf("\n");
        
        // Proper cleanup
        destroy_node(student);
        student = NULL;
        printf("   Node completely destroyed\n");
    }
    
    printf("\n6. Linked list with proper cleanup:\n");
    
    // Create simple linked list
    Node *head = NULL;
    Node *current = NULL;
    
    int scores1[] = {90, 85};
    int scores2[] = {88, 92, 86};
    int scores3[] = {95};
    
    // Add nodes to list
    head = create_node("Bob", scores1, 2);
    if(head != NULL) {
        current = head;
        
        Node *second = create_node("Charlie", scores2, 3);
        if(second != NULL) {
            current->next = second;
            current = second;
        }
        
        Node *third = create_node("Diana", scores3, 1);
        if(third != NULL) {
            current->next = third;
        }
    }
    
    // Traverse and print list
    printf("   Linked list:\n");
    current = head;
    while(current != NULL) {
        printf("   • %s: ", current->name);
        for(int i = 0; i < current->score_count; i++) {
            printf("%d ", current->scores[i]);
        }
        printf("\n");
        current = current->next;
    }
    
    // Properly free entire list
    current = head;
    while(current != NULL) {
        Node *next = current->next;
        destroy_node(current);
        current = next;
    }
    head = NULL;
    printf("   Entire list freed\n");
    
    printf("\n7. Common free() errors:\n");
    
    // ERROR 1: Double free
    printf("   Error 1: Double free\n");
    int *error1 = (int*)malloc(sizeof(int));
    free(error1);
    // free(error1);  // ERROR: Double free - undefined behavior!
    error1 = NULL;  // Solution: Set to NULL after first free
    
    // ERROR 2: Freeing stack memory
    printf("   Error 2: Freeing stack memory\n");
    int stack_var = 100;
    int *error2 = &stack_var;
    // free(error2);  // ERROR: Not heap memory!
    
    // ERROR 3: Freeing uninitialized pointer
    printf("   Error 3: Freeing uninitialized pointer\n");
    int *error3;  // Uninitialized
    // free(error3);  // ERROR: Contains garbage address!
    
    // ERROR 4: Freeing part of allocation
    printf("   Error 4: Freeing part of allocation\n");
    int *error4 = (int*)malloc(10 * sizeof(int));
    // free(error4 + 5);  // ERROR: Not the start of allocation!
    free(error4);  // Correct: Free from start
    
    // ERROR 5: Using memory after free
    printf("   Error 5: Use after free\n");
    int *error5 = (int*)malloc(sizeof(int));
    *error5 = 50;
    free(error5);
    // *error5 = 60;  // ERROR: Dangling pointer!
    error5 = NULL;  // Solution: Set to NULL
    
    printf("\n8. Memory leak demonstration:\n");
    
    // This function would leak memory
    void leaky_function() {
        char *leak = (char*)malloc(100 * sizeof(char));
        strcpy(leak, "This memory will leak");
        // Forgot to free(leak)!
    }
    
    printf("   leaky_function() allocates memory but doesn't free it\n");
    printf("   Result: Memory leak - allocated memory never returned\n");
    
    printf("\n9. Best practices summary:\n");
    
    printf("   • Always free() what you malloc()/calloc()/realloc()\n");
    printf("   • Set pointers to NULL after free()\n");
    printf("   • free(NULL) is safe - does nothing\n");
    printf("   • Free in reverse order of allocation (nested structures)\n");
    printf("   • Use tools like valgrind to detect memory leaks\n");
    printf("   • Document memory ownership in your code\n");
    printf("   • Consider using RAII patterns or garbage collection in C++\n");
    
    return 0;
}
Memory Management Best Practices
  1. Pair Allocation with Free: Every malloc/calloc should have a corresponding free
  2. Set to NULL After Free: Prevents dangling pointer errors
  3. Free in Reverse Order: For nested structures, free inner allocations first
  4. Use Memory Debuggers: Valgrind, AddressSanitizer to catch leaks
  5. Document Ownership: Clearly indicate who allocates and who frees
  6. Check Return Values: Always check if allocation functions succeed
  7. Avoid Memory Fragmentation: Reuse allocations when possible
  8. Use RAII Patterns: In C++, use constructors/destructors for automatic cleanup

Common Memory Errors and Solutions

Memory management errors are among the most common and difficult-to-debug issues in C programming. Understanding these errors and their solutions is crucial for writing robust programs.

Error 1: Memory Leak
void leaky_function() { int *ptr = malloc(100 * sizeof(int)); // Use ptr... // Forgot to free(ptr)! - MEMORY LEAK }
Solution: Always free allocated memory void fixed_function() { int *ptr = malloc(100 * sizeof(int)); // Use ptr... free(ptr); ptr = NULL; }
Error 2: Dangling Pointer
int *ptr = malloc(sizeof(int)); *ptr = 42; free(ptr); *ptr = 100; // ERROR: Use after free - DANGLING POINTER
Solution: Set pointer to NULL after free int *ptr = malloc(sizeof(int)); *ptr = 42; free(ptr); ptr = NULL; // Now safe
Error 3: Double Free
int *ptr = malloc(sizeof(int)); free(ptr); free(ptr); // ERROR: Double free - UNDEFINED BEHAVIOR
Solution: Set to NULL after first free int *ptr = malloc(sizeof(int)); free(ptr); ptr = NULL; free(ptr); // Safe: free(NULL) does nothing
Error 4: Buffer Overflow
char *str = malloc(10 * sizeof(char)); strcpy(str, "This is too long!"); // OVERFLOW
Solution: Use bounds-checking functions char *str = malloc(10 * sizeof(char)); strncpy(str, "This is too long!", 9); str[9] = '\0'; // Ensure null termination
Error 5: Uninitialized Memory Access
int *ptr = malloc(5 * sizeof(int)); int sum = 0; for(int i = 0; i < 5; i++) { sum += ptr[i]; // GARBAGE VALUES }
Solution: Initialize memory or use calloc() int *ptr = calloc(5, sizeof(int)); // All zeros // OR initialize after malloc int *ptr = malloc(5 * sizeof(int)); for(int i = 0; i < 5; i++) ptr[i] = 0;
Error 6: Memory Allocation Failure
int *ptr = malloc(1000000000 * sizeof(int)); *ptr = 42; // CRASH if malloc returned NULL
Solution: Always check return value int *ptr = malloc(1000000000 * sizeof(int)); if(ptr == NULL) { // Handle error gracefully fprintf(stderr, "Memory allocation failed!\n"); return; } *ptr = 42; // Safe
Memory Error Prevention Strategies:
  1. Use Memory Debuggers: Valgrind, AddressSanitizer, Electric Fence
  2. Code Reviews: Have others review memory management code
  3. Static Analysis: Use tools like Coverity, Clang Analyzer
  4. Defensive Programming: Add asserts and sanity checks
  5. Memory Pools: For performance-critical code with fixed-size allocations
  6. RAII Patterns: In C++, use smart pointers (unique_ptr, shared_ptr)
  7. Testing: Write tests that stress memory usage
  8. Documentation: Clearly document memory ownership
  9. Avoid Manual Memory When Possible: Use standard containers in C++
  10. Educate Team: Ensure all team members understand memory management
Example: Using Valgrind to Detect Memory Errors
// Compile with: gcc -g -o test test.c
// Run with: valgrind --leak-check=full ./test

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

void memory_leak_example() {
    int *leak = malloc(100 * sizeof(int));
    // Forgot to free - MEMORY LEAK
}

void use_after_free() {
    int *ptr = malloc(sizeof(int));
    *ptr = 42;
    free(ptr);
    *ptr = 100;  // USE AFTER FREE
}

void double_free_example() {
    int *ptr = malloc(sizeof(int));
    free(ptr);
    free(ptr);  // DOUBLE FREE
}

int main() {
    printf("Testing memory errors...\n");
    
    // Uncomment to test specific errors
    
    // memory_leak_example();
    // use_after_free();
    // double_free_example();
    
    printf("Test completed.\n");
    return 0;
}
Valgrind Output Example (for memory leak):
==12345== Memcheck, a memory error detector
==12345== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==12345== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==12345== Command: ./test
==12345==
Testing memory errors...
Test completed.
==12345==
==12345== HEAP SUMMARY:
==12345== in use at exit: 400 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 400 bytes allocated
==12345==
==12345== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x1086B1: memory_leak_example (test.c:6)
==12345== by 0x108721: main (test.c:24)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 400 bytes in 1 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 0 bytes in 0 blocks
==12345== still reachable: 0 bytes in 0 blocks
==12345== suppressed: 0 bytes in 0 blocks
==12345==
==12345== For counts of detected and suppressed errors, rerun with: -v
==12345== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Key Takeaways

  • Dynamic memory is allocated from the heap at runtime using malloc(), calloc(), realloc()
  • Static memory is allocated at compile time in stack/data segments
  • malloc() allocates uninitialized memory, calloc() allocates zero-initialized memory
  • realloc() resizes existing allocations, may move memory to new location
  • Always check if allocation functions return NULL
  • Every malloc()/calloc() must have a corresponding free()
  • Set pointers to NULL after free() to prevent dangling pointers
  • Memory leaks occur when allocated memory is never freed
  • Dangling pointers point to freed memory - causes undefined behavior
  • Double free occurs when memory is freed twice - causes crashes
  • Use sizeof() for portable memory size calculations
  • For nested structures, free inner allocations before outer ones
  • Use memory debuggers like Valgrind to detect memory errors
  • Consider using calloc() for arrays and structures needing initialization
  • realloc() with NULL pointer behaves like malloc()
  • realloc() with size 0 behaves like free()
  • Document memory ownership clearly in your code
Next Topics: We'll explore C stuctures and unions in detail, including structure implementation, stucture vs union etc.