Docs

README

Memory Leaks and Debugging in C

Table of Contents

  1. •Introduction
  2. •Understanding Memory Leaks
  3. •Common Causes of Memory Leaks
  4. •Memory Debugging Tools
  5. •Using Valgrind
  6. •Using AddressSanitizer
  7. •Manual Debugging Techniques
  8. •Memory Corruption Issues
  9. •Best Practices for Prevention
  10. •Memory Management Patterns
  11. •Summary

Introduction

Memory leaks and memory-related bugs are among the most common and difficult-to-debug issues in C programming. Unlike languages with garbage collection, C requires programmers to manually manage memory allocation and deallocation. When memory is allocated but never freed, it creates a memory leak. When memory is accessed incorrectly, it causes memory corruption.

Why Memory Debugging Matters

Program Start
     ↓
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  Allocations:  ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ     │
│  Deallocations: ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ                            │
│  LEAKED:                        ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ    │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
     ↓
Over Time: Memory usage grows continuously
     ↓
Eventually: System runs out of memory / Program crashes

Types of Memory Problems

Problem TypeDescriptionConsequence
Memory LeakAllocated memory never freedMemory exhaustion
Double FreeFreeing already freed memoryCrash, corruption
Use After FreeAccessing freed memoryUndefined behavior
Buffer OverflowWriting beyond allocated boundsCorruption, security issues
Buffer UnderflowWriting before allocated boundsCorruption
Uninitialized ReadReading uninitialized memoryUnpredictable behavior
Invalid FreeFreeing non-heap pointerCrash

Understanding Memory Leaks

What is a Memory Leak?

A memory leak occurs when dynamically allocated memory is no longer accessible but hasn't been freed.

void memory_leak_example(void) {
    int *ptr = malloc(100 * sizeof(int));

    // Do some work with ptr...

    // PROBLEM: Function returns without freeing ptr
    // The 400 bytes are now "leaked"
}  // ptr goes out of scope, but memory is still allocated!

Visualizing Memory Leaks

Before function call:
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Heap Memory                              │
│ [Free                                  ] │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

During function:
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Heap Memory                              │
│ [ā–ˆā–ˆā–ˆā–ˆā–ˆ ptr ā–ˆā–ˆā–ˆā–ˆ][Free                  ] │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
          ↑
        ptr variable points here

After function returns (LEAK!):
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Heap Memory                              │
│ [ā–ˆā–ˆā–ˆā–ˆā–ˆ LOST ā–ˆā–ˆā–ˆ][Free                  ] │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
          ↑
        No variable points here anymore!
        Memory cannot be freed!

Types of Memory Leaks

1. Direct Leak (Lost Pointer)

void direct_leak(void) {
    char *str = malloc(100);
    str = malloc(200);  // First allocation is now leaked!
    free(str);          // Only frees the second allocation
}

2. Indirect Leak (Leaked Data Structure)

typedef struct Node {
    int data;
    struct Node *next;
} Node;

void indirect_leak(void) {
    Node *head = malloc(sizeof(Node));
    head->next = malloc(sizeof(Node));
    head->next->next = malloc(sizeof(Node));

    free(head);  // Only frees first node!
    // Second and third nodes are leaked
}

3. Reachable but Not Freed (At Program Exit)

int *global_ptr;

int main() {
    global_ptr = malloc(1000);
    // Program ends without freeing global_ptr
    // Memory is reachable but not freed
    return 0;
}

Common Causes of Memory Leaks

1. Forgetting to Free

// WRONG
void process_data(void) {
    char *buffer = malloc(1024);
    // ... process data ...
    return;  // Oops! Forgot to free buffer
}

// CORRECT
void process_data_fixed(void) {
    char *buffer = malloc(1024);
    // ... process data ...
    free(buffer);  // Don't forget!
    return;
}

2. Early Return Without Cleanup

// WRONG
int process_file(const char *filename) {
    FILE *file = fopen(filename, "r");
    char *buffer = malloc(1024);

    if (file == NULL) {
        return -1;  // buffer is leaked!
    }

    if (buffer == NULL) {
        fclose(file);
        return -1;
    }

    // ...process...

    free(buffer);
    fclose(file);
    return 0;
}

// CORRECT - Using goto for cleanup
int process_file_fixed(const char *filename) {
    FILE *file = NULL;
    char *buffer = NULL;
    int result = -1;

    file = fopen(filename, "r");
    if (file == NULL) goto cleanup;

    buffer = malloc(1024);
    if (buffer == NULL) goto cleanup;

    // ...process...
    result = 0;

cleanup:
    free(buffer);  // free(NULL) is safe
    if (file) fclose(file);
    return result;
}

3. Reassigning Pointer Without Freeing

// WRONG
char *get_message(int type) {
    char *msg = malloc(100);

    if (type == 1) {
        msg = "Hello";  // Original allocation leaked!
    } else {
        strcpy(msg, "Goodbye");
    }

    return msg;
}

// CORRECT
char *get_message_fixed(int type) {
    char *msg = malloc(100);
    if (msg == NULL) return NULL;

    if (type == 1) {
        strcpy(msg, "Hello");
    } else {
        strcpy(msg, "Goodbye");
    }

    return msg;
}

4. Lost Reference in Data Structures

// WRONG - When removing node, we lose reference to removed node
void list_remove_wrong(Node **head, int value) {
    if (*head == NULL) return;

    if ((*head)->data == value) {
        *head = (*head)->next;  // Original head is leaked!
        return;
    }
    // ...
}

// CORRECT
void list_remove_correct(Node **head, int value) {
    if (*head == NULL) return;

    if ((*head)->data == value) {
        Node *temp = *head;
        *head = (*head)->next;
        free(temp);  // Free the removed node
        return;
    }
    // ...
}

5. Exception-Like Control Flow

// WRONG - Using longjmp
#include <setjmp.h>
jmp_buf jump_buffer;

void risky_operation(void) {
    char *data = malloc(1000);

    if (some_error_condition) {
        longjmp(jump_buffer, 1);  // data is leaked!
    }

    free(data);
}

6. Lost Return Value

// WRONG
void waste_memory(void) {
    malloc(1000);  // Allocated but pointer not stored!
    strdup("hello");  // strdup allocates, but we ignore result!
}

Memory Debugging Tools

Overview of Tools

ToolPlatformTypeSpeedFeatures
ValgrindLinux, macOSDynamic10-50x slowerComprehensive
AddressSanitizerCross-platformCompile-time2x slowerFast, detailed
Electric FenceLinuxLibraryModerateSimple
Dr. MemoryWindows, LinuxDynamic10x slowerSimilar to Valgrind
mtraceLinuxLibraryMinimalBasic tracking

When to Use Each Tool

Development Phase          Recommended Tool
─────────────────────────────────────────────
Early development     →    AddressSanitizer (fast)
Debugging specific    →    Valgrind (detailed)
CI/CD pipeline        →    AddressSanitizer
Production testing    →    Valgrind memcheck
Windows development   →    Dr. Memory or ASAN

Using Valgrind

Introduction to Valgrind

Valgrind is a powerful memory debugging tool that detects:

  • •Memory leaks
  • •Invalid memory access
  • •Use of uninitialized memory
  • •Double frees
  • •Memory corruption

Basic Usage

# Compile with debug symbols
gcc -g -O0 -o myprogram myprogram.c

# Run with Valgrind
valgrind ./myprogram

# Run with detailed leak check
valgrind --leak-check=full ./myprogram

# Show all leak sources
valgrind --leak-check=full --show-leak-kinds=all ./myprogram

# Track origins of uninitialized values
valgrind --track-origins=yes ./myprogram

Understanding Valgrind Output

// Example program with bugs
#include <stdlib.h>

int main() {
    int *arr = malloc(10 * sizeof(int));
    arr[10] = 5;  // Buffer overflow
    return 0;    // Memory leak (no free)
}
==12345== Invalid write of size 4
==12345==    at 0x400534: main (example.c:6)
==12345==  Address 0x4c3a068 is 0 bytes after a block of size 40 alloc'd
==12345==    at 0x4A06A2E: malloc (vg_replace_malloc.c:270)
==12345==    by 0x400525: main (example.c:5)
==12345==
==12345== HEAP SUMMARY:
==12345==     in use at exit: 40 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4A06A2E: malloc (vg_replace_malloc.c:270)
==12345==    by 0x400525: main (example.c:5)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 40 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

Interpreting Leak Types

Leak TypeMeaning
definitely lostNo pointer to the memory exists
indirectly lostMemory pointed to by definitely lost memory
possibly lostPointer exists but may not point to start of block
still reachableMemory not freed but still has valid pointer

Valgrind Memcheck Options

# Full command with useful options
valgrind \
    --leak-check=full \
    --show-leak-kinds=all \
    --track-origins=yes \
    --verbose \
    --log-file=valgrind-output.txt \
    ./myprogram

# For suppressions (ignore known leaks in libraries)
valgrind --suppressions=mysuppressions.supp ./myprogram

Creating Suppression Files

# Example suppression file
{
   <insert_a_suppression_name_here>
   Memcheck:Leak
   match-leak-kinds: reachable
   fun:malloc
   obj:/usr/lib/libsomelib.so
}

Using AddressSanitizer

Introduction to AddressSanitizer (ASan)

AddressSanitizer is a compile-time memory error detector that's faster than Valgrind but requires recompilation.

Enabling AddressSanitizer

# GCC
gcc -fsanitize=address -g -O1 -o myprogram myprogram.c

# Clang
clang -fsanitize=address -g -O1 -o myprogram myprogram.c

# For memory leak detection (GCC/Clang)
gcc -fsanitize=address -fsanitize=leak -g -o myprogram myprogram.c

ASan Example Output

// Program with buffer overflow
#include <stdlib.h>

int main() {
    int *arr = malloc(10 * sizeof(int));
    arr[10] = 5;  // Overflow!
    free(arr);
    return 0;
}
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000ef...
WRITE of size 4 at 0x60200000ef... thread T0
    #0 0x4005d6 in main example.c:6
    #1 0x7f0a12345 in __libc_start_main

0x60200000ef... is located 0 bytes to the right of 40-byte region [...]

SUMMARY: AddressSanitizer: heap-buffer-overflow example.c:6 in main

ASan Runtime Options

# Environment variables for ASan
export ASAN_OPTIONS="detect_leaks=1:halt_on_error=0:print_stats=1"

# Useful options:
# detect_leaks=1          - Enable leak detection
# halt_on_error=0         - Continue after error
# print_stats=1           - Print memory statistics
# check_initialization_order=1 - Check global init order
# detect_stack_use_after_return=1 - Detect stack use-after-return

Other Sanitizers

# Memory Sanitizer (uninitialized memory)
gcc -fsanitize=memory -g -o program program.c

# Undefined Behavior Sanitizer
gcc -fsanitize=undefined -g -o program program.c

# Thread Sanitizer (data races)
gcc -fsanitize=thread -g -o program program.c

# Combining sanitizers
gcc -fsanitize=address,undefined -g -o program program.c

Manual Debugging Techniques

1. Wrapper Functions for Tracking

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

/* Debug mode toggle */
#define DEBUG_MEMORY 1

#if DEBUG_MEMORY

/* Allocation tracking */
typedef struct AllocationInfo {
    void *ptr;
    size_t size;
    const char *file;
    int line;
    struct AllocationInfo *next;
} AllocationInfo;

static AllocationInfo *allocation_list = NULL;
static size_t total_allocated = 0;
static size_t total_freed = 0;
static int allocation_count = 0;

void *debug_malloc(size_t size, const char *file, int line) {
    void *ptr = malloc(size);
    if (ptr) {
        AllocationInfo *info = malloc(sizeof(AllocationInfo));
        info->ptr = ptr;
        info->size = size;
        info->file = file;
        info->line = line;
        info->next = allocation_list;
        allocation_list = info;
        total_allocated += size;
        allocation_count++;

        printf("[ALLOC] %zu bytes at %p (%s:%d)\n", size, ptr, file, line);
    }
    return ptr;
}

void debug_free(void *ptr, const char *file, int line) {
    if (ptr == NULL) return;

    AllocationInfo **current = &allocation_list;
    while (*current) {
        if ((*current)->ptr == ptr) {
            AllocationInfo *to_free = *current;
            *current = to_free->next;

            printf("[FREE]  %zu bytes at %p (%s:%d)\n",
                   to_free->size, ptr, file, line);

            total_freed += to_free->size;
            allocation_count--;
            free(to_free);
            free(ptr);
            return;
        }
        current = &(*current)->next;
    }

    printf("[ERROR] Freeing unknown pointer %p (%s:%d)\n", ptr, file, line);
}

void debug_memory_report(void) {
    printf("\n=== Memory Report ===\n");
    printf("Total allocated: %zu bytes\n", total_allocated);
    printf("Total freed: %zu bytes\n", total_freed);
    printf("Currently allocated: %zu bytes\n", total_allocated - total_freed);
    printf("Outstanding allocations: %d\n", allocation_count);

    if (allocation_list) {
        printf("\nLeaked allocations:\n");
        for (AllocationInfo *info = allocation_list; info; info = info->next) {
            printf("  %zu bytes at %p (%s:%d)\n",
                   info->size, info->ptr, info->file, info->line);
        }
    }
    printf("====================\n");
}

#define malloc(size) debug_malloc(size, __FILE__, __LINE__)
#define free(ptr) debug_free(ptr, __FILE__, __LINE__)

#endif /* DEBUG_MEMORY */

/* Usage */
int main(void) {
    int *arr = malloc(10 * sizeof(int));
    char *str = malloc(100);

    free(arr);
    /* Note: str is not freed - will be reported */

    debug_memory_report();
    return 0;
}

2. Canary Values for Buffer Overflow Detection

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

#define CANARY_VALUE 0xDEADBEEF
#define CANARY_SIZE sizeof(uint32_t)

void *safe_malloc(size_t size) {
    /* Allocate extra space for canaries */
    size_t total = size + 2 * CANARY_SIZE;
    char *raw = malloc(total);
    if (!raw) return NULL;

    /* Place canaries */
    *(uint32_t *)raw = CANARY_VALUE;
    *(uint32_t *)(raw + CANARY_SIZE + size) = CANARY_VALUE;

    return raw + CANARY_SIZE;
}

int check_canaries(void *ptr, size_t size) {
    char *raw = (char *)ptr - CANARY_SIZE;

    uint32_t front = *(uint32_t *)raw;
    uint32_t back = *(uint32_t *)(raw + CANARY_SIZE + size);

    if (front != CANARY_VALUE) {
        printf("ERROR: Front canary corrupted! (buffer underflow)\n");
        return 0;
    }

    if (back != CANARY_VALUE) {
        printf("ERROR: Back canary corrupted! (buffer overflow)\n");
        return 0;
    }

    return 1;
}

void safe_free(void *ptr, size_t size) {
    check_canaries(ptr, size);
    free((char *)ptr - CANARY_SIZE);
}

int main(void) {
    int *arr = safe_malloc(10 * sizeof(int));

    /* Normal access */
    for (int i = 0; i < 10; i++) {
        arr[i] = i;
    }

    /* This would trigger canary check: */
    /* arr[10] = 100; */  /* Buffer overflow! */

    safe_free(arr, 10 * sizeof(int));
    return 0;
}

3. Memory Fill Patterns

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

/* Fill patterns */
#define ALLOC_PATTERN  0xAA  /* Freshly allocated */
#define FREE_PATTERN   0xDD  /* Recently freed */

void *debug_calloc(size_t count, size_t size) {
    void *ptr = malloc(count * size);
    if (ptr) {
        /* Fill with pattern (not zero) to detect uninitialized reads */
        memset(ptr, ALLOC_PATTERN, count * size);
    }
    return ptr;
}

void debug_free(void *ptr, size_t size) {
    if (ptr) {
        /* Fill with pattern to detect use-after-free */
        memset(ptr, FREE_PATTERN, size);
        free(ptr);
    }
}

int main(void) {
    int *arr = debug_calloc(10, sizeof(int));

    /* Check pattern - uninitialized read would show 0xAAAAAAAA */
    printf("Uninitialized value: 0x%X\n", arr[0]);

    arr[0] = 42;
    printf("After assignment: %d\n", arr[0]);

    debug_free(arr, 10 * sizeof(int));

    /* Use after free would show 0xDDDDDDDD */
    /* DANGEROUS: printf("Use after free: 0x%X\n", arr[0]); */

    return 0;
}

Memory Corruption Issues

1. Buffer Overflow

/* WRONG - Writing past allocated memory */
void buffer_overflow_example(void) {
    char *buffer = malloc(10);
    strcpy(buffer, "This string is too long!");  // Overflow!
    free(buffer);
}

/* CORRECT - Check bounds */
void buffer_overflow_fixed(void) {
    char *buffer = malloc(10);
    strncpy(buffer, "This string is too long!", 9);
    buffer[9] = '\0';  // Ensure null termination
    free(buffer);
}

2. Use After Free

/* WRONG */
void use_after_free_example(void) {
    int *ptr = malloc(sizeof(int));
    *ptr = 42;
    free(ptr);
    printf("%d\n", *ptr);  // Use after free!
}

/* CORRECT - Set pointer to NULL after free */
void use_after_free_fixed(void) {
    int *ptr = malloc(sizeof(int));
    *ptr = 42;
    printf("%d\n", *ptr);  // Use before free
    free(ptr);
    ptr = NULL;  // Prevent accidental use
}

3. Double Free

/* WRONG */
void double_free_example(void) {
    int *ptr = malloc(sizeof(int));
    free(ptr);
    free(ptr);  // Double free!
}

/* CORRECT */
void double_free_fixed(void) {
    int *ptr = malloc(sizeof(int));
    free(ptr);
    ptr = NULL;  // Set to NULL
    free(ptr);   // free(NULL) is safe
}

4. Invalid Free

/* WRONG - Freeing stack memory */
void invalid_free_example(void) {
    int x = 10;
    int *ptr = &x;
    free(ptr);  // Error! Can't free stack memory
}

/* WRONG - Freeing middle of block */
void invalid_free_middle(void) {
    int *arr = malloc(10 * sizeof(int));
    int *middle = arr + 5;
    free(middle);  // Error! Not the start of allocation
}

5. Uninitialized Memory Access

/* WRONG */
void uninitialized_access(void) {
    int *arr = malloc(10 * sizeof(int));
    int sum = 0;
    for (int i = 0; i < 10; i++) {
        sum += arr[i];  // Reading garbage values!
    }
    printf("Sum: %d\n", sum);  // Unpredictable
    free(arr);
}

/* CORRECT - Initialize memory */
void uninitialized_access_fixed(void) {
    int *arr = calloc(10, sizeof(int));  // Zero-initialized
    int sum = 0;
    for (int i = 0; i < 10; i++) {
        sum += arr[i];
    }
    printf("Sum: %d\n", sum);  // 0
    free(arr);
}

Best Practices for Prevention

1. Allocation/Deallocation Pairing

/*
 * Rule: Every allocation function should have a corresponding
 * deallocation function at the same abstraction level
 */

typedef struct {
    int *data;
    size_t size;
} IntArray;

/* Paired create/destroy functions */
IntArray *intarray_create(size_t size) {
    IntArray *arr = malloc(sizeof(IntArray));
    if (!arr) return NULL;

    arr->data = malloc(size * sizeof(int));
    if (!arr->data) {
        free(arr);
        return NULL;
    }

    arr->size = size;
    return arr;
}

void intarray_destroy(IntArray *arr) {
    if (arr) {
        free(arr->data);
        free(arr);
    }
}

2. Ownership Semantics

/*
 * Clear ownership: who is responsible for freeing?
 * Document it in comments/function names
 */

/* Returns newly allocated string - CALLER must free */
char *create_greeting(const char *name) {
    char *greeting = malloc(strlen(name) + 10);
    sprintf(greeting, "Hello, %s!", name);
    return greeting;  // Ownership transfers to caller
}

/* Borrows pointer - does NOT free */
void print_greeting(const char *greeting) {
    printf("%s\n", greeting);
    // Does NOT free - just borrows
}

/* Takes ownership - WILL free the pointer */
void consume_greeting(char *greeting) {
    printf("%s\n", greeting);
    free(greeting);  // Takes ownership and frees
}

3. Defensive Coding

/* Set pointer to NULL after freeing */
#define SAFE_FREE(ptr) do { free(ptr); ptr = NULL; } while(0)

/* Check allocation success */
#define CHECK_ALLOC(ptr) \
    do { \
        if ((ptr) == NULL) { \
            fprintf(stderr, "Allocation failed at %s:%d\n", \
                    __FILE__, __LINE__); \
            exit(EXIT_FAILURE); \
        } \
    } while(0)

/* Usage */
int main(void) {
    int *arr = malloc(100 * sizeof(int));
    CHECK_ALLOC(arr);

    // Use arr...

    SAFE_FREE(arr);
    // arr is now NULL

    return 0;
}

4. RAII-Like Pattern in C

/* Resource Acquisition Is Initialization (adapted for C) */

typedef struct {
    FILE *file;
    char *buffer;
    size_t buffer_size;
} FileReader;

FileReader *filereader_open(const char *filename, size_t bufsize) {
    FileReader *reader = malloc(sizeof(FileReader));
    if (!reader) goto fail;

    reader->buffer = malloc(bufsize);
    if (!reader->buffer) goto fail_buffer;

    reader->file = fopen(filename, "r");
    if (!reader->file) goto fail_file;

    reader->buffer_size = bufsize;
    return reader;

    /* Cleanup on failure */
fail_file:
    free(reader->buffer);
fail_buffer:
    free(reader);
fail:
    return NULL;
}

void filereader_close(FileReader *reader) {
    if (reader) {
        if (reader->file) fclose(reader->file);
        free(reader->buffer);
        free(reader);
    }
}

/* Single point of cleanup */

5. Use Static Analysis Tools

# GCC static analysis
gcc -fanalyzer -Wall -Wextra myprogram.c

# Clang static analyzer
scan-build gcc myprogram.c

# cppcheck
cppcheck --enable=all myprogram.c

# PVS-Studio, Coverity, etc. (commercial)

Memory Management Patterns

1. Object Pool Pattern

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

#define POOL_SIZE 100

typedef struct {
    int data;
    int in_use;
} PoolObject;

typedef struct {
    PoolObject objects[POOL_SIZE];
    int free_count;
} ObjectPool;

ObjectPool *pool_create(void) {
    ObjectPool *pool = calloc(1, sizeof(ObjectPool));
    if (pool) {
        pool->free_count = POOL_SIZE;
    }
    return pool;
}

PoolObject *pool_acquire(ObjectPool *pool) {
    if (pool->free_count == 0) return NULL;

    for (int i = 0; i < POOL_SIZE; i++) {
        if (!pool->objects[i].in_use) {
            pool->objects[i].in_use = 1;
            pool->free_count--;
            return &pool->objects[i];
        }
    }
    return NULL;
}

void pool_release(ObjectPool *pool, PoolObject *obj) {
    if (obj && obj->in_use) {
        obj->in_use = 0;
        obj->data = 0;
        pool->free_count++;
    }
}

void pool_destroy(ObjectPool *pool) {
    free(pool);  // All objects freed at once
}

2. Arena Allocator

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

typedef struct {
    char *buffer;
    size_t size;
    size_t used;
} Arena;

Arena *arena_create(size_t size) {
    Arena *arena = malloc(sizeof(Arena));
    if (!arena) return NULL;

    arena->buffer = malloc(size);
    if (!arena->buffer) {
        free(arena);
        return NULL;
    }

    arena->size = size;
    arena->used = 0;
    return arena;
}

void *arena_alloc(Arena *arena, size_t size) {
    /* Align to 8 bytes */
    size = (size + 7) & ~(size_t)7;

    if (arena->used + size > arena->size) {
        return NULL;  // Out of space
    }

    void *ptr = arena->buffer + arena->used;
    arena->used += size;
    return ptr;
}

void arena_reset(Arena *arena) {
    arena->used = 0;  // "Free" all allocations at once
}

void arena_destroy(Arena *arena) {
    if (arena) {
        free(arena->buffer);
        free(arena);
    }
}

/* No individual free needed! */

3. Reference Counting

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

typedef struct {
    char *data;
    int ref_count;
} SharedString;

SharedString *sharedstring_create(const char *str) {
    SharedString *ss = malloc(sizeof(SharedString));
    if (!ss) return NULL;

    ss->data = strdup(str);
    if (!ss->data) {
        free(ss);
        return NULL;
    }

    ss->ref_count = 1;
    return ss;
}

SharedString *sharedstring_acquire(SharedString *ss) {
    if (ss) {
        ss->ref_count++;
    }
    return ss;
}

void sharedstring_release(SharedString *ss) {
    if (ss) {
        ss->ref_count--;
        if (ss->ref_count <= 0) {
            free(ss->data);
            free(ss);
        }
    }
}

/* Usage */
int main(void) {
    SharedString *s1 = sharedstring_create("Hello");
    SharedString *s2 = sharedstring_acquire(s1);  // ref_count = 2
    SharedString *s3 = sharedstring_acquire(s1);  // ref_count = 3

    printf("%s (refs: %d)\n", s1->data, s1->ref_count);

    sharedstring_release(s2);  // ref_count = 2
    sharedstring_release(s3);  // ref_count = 1
    sharedstring_release(s1);  // ref_count = 0, freed

    return 0;
}

Summary

Key Concepts

  1. •Memory Leaks: Allocated memory that's never freed
  2. •Memory Corruption: Invalid memory access (overflow, use-after-free, etc.)
  3. •Tools: Valgrind, AddressSanitizer, static analyzers
  4. •Prevention: Ownership semantics, defensive coding, proper cleanup

Debugging Checklist

  • • Compile with -g flag for debug symbols
  • • Run with Valgrind or AddressSanitizer
  • • Check all allocation return values
  • • Ensure every malloc/calloc has matching free
  • • Set pointers to NULL after freeing
  • • Use static analysis tools
  • • Review ownership semantics in code review

Common Commands

# Valgrind full leak check
valgrind --leak-check=full --show-leak-kinds=all ./program

# Compile with AddressSanitizer
gcc -fsanitize=address -g -o program program.c

# Run with leak detection
ASAN_OPTIONS=detect_leaks=1 ./program

# Static analysis
gcc -fanalyzer -Wall -Wextra program.c
cppcheck --enable=all program.c

Prevention Summary

ProblemPrevention
Memory leakMatch every alloc with free
Double freeSet pointer to NULL after free
Use after freeSet pointer to NULL, avoid stale refs
Buffer overflowCheck bounds, use size-aware functions
Uninitialized readUse calloc or memset

Next Steps

After mastering memory debugging:

  • •Learn about memory-safe coding standards (CERT C, MISRA)
  • •Explore automated testing with fuzzing
  • •Study secure coding practices
  • •Implement custom memory allocators

This tutorial is part of the C Programming Learning Series.

README - C Programming Tutorial | DeepML