Docs

Dynamic Memory

Introduction to Dynamic Memory Allocation

Table of Contents

  1. Overview
  2. What is Dynamic Memory?
  3. Static vs Dynamic Memory
  4. Memory Layout of a C Program
  5. The Heap
  6. Why Use Dynamic Memory?
  7. Memory Allocation Functions
  8. The void Pointer
  9. Memory Alignment
  10. Common Use Cases
  11. Risks and Responsibilities
  12. Best Practices
  13. Summary

Overview

Dynamic memory allocation is one of the most powerful features of C programming. It allows programs to request memory at runtime, enabling flexible data structures and efficient memory usage. Understanding dynamic memory is essential for writing sophisticated C programs.

Learning Objectives

  • Understand the difference between static and dynamic memory
  • Learn about the memory layout of C programs
  • Understand what the heap is and how it works
  • Know when and why to use dynamic memory
  • Learn about the memory allocation function family
  • Understand the responsibilities of manual memory management

What is Dynamic Memory?

Definition

Dynamic memory allocation is the process of allocating memory at runtime (during program execution) rather than at compile time. The allocated memory persists until explicitly freed by the programmer.

Key Characteristics

  1. Runtime Allocation: Memory size can be determined during program execution
  2. Flexible Size: Allocations can vary based on user input or program state
  3. Manual Management: Programmer is responsible for freeing allocated memory
  4. Heap Storage: Dynamic memory comes from the heap region
  5. Pointer Access: Accessed through pointers returned by allocation functions

Basic Concept

Compile Time                    Runtime
+-------------------+           +-------------------+
| Memory size must  |           | Memory size can   |
| be known          |           | be calculated     |
| int arr[100];     |           | int *arr = malloc(|
|                   |           |   n * sizeof(int))|
+-------------------+           +-------------------+

Static vs Dynamic Memory

Static Memory Allocation

// Examples of static allocation
int globalVar;           // Global variable (data segment)
static int staticVar;    // Static variable (data segment)

void function(void) {
    int localVar;        // Local variable (stack)
    int array[100];      // Fixed-size array (stack)
    char buffer[256];    // Fixed-size buffer (stack)
}

Characteristics:

  • Size must be known at compile time
  • Allocated automatically by compiler
  • Deallocated automatically when scope ends
  • Fast allocation and access
  • Limited by stack size

Dynamic Memory Allocation

#include <stdlib.h>

void function(int size) {
    // Examples of dynamic allocation
    int *array = malloc(size * sizeof(int));
    char *buffer = malloc(1024);

    // ... use memory ...

    free(array);   // Must free manually
    free(buffer);
}

Characteristics:

  • Size can be determined at runtime
  • Allocated explicitly by programmer
  • Must be deallocated explicitly
  • Slower than stack allocation
  • Limited by available RAM

Comparison Table

FeatureStatic MemoryDynamic Memory
Allocation TimeCompile timeRuntime
SizeFixedVariable
LocationStack/Data segmentHeap
LifetimeScope-based (auto)Until free() called
SpeedFastSlower
ManagementAutomaticManual
FlexibilityLimitedHigh
Risk of LeaksNoneYes
FragmentationNonePossible

Memory Layout of a C Program

Understanding Program Memory

When a C program runs, the operating system allocates memory divided into distinct regions:

High Memory Addresses
+---------------------------+
|       Command-line        |
|    arguments & environ    |
+---------------------------+
|          Stack            |  ← Grows downward
|   (local variables,       |
|    function calls)        |
+---------------------------+
|            ↓              |
|                           |
|      (Free Space)         |  ← Available for growth
|                           |
|            ↑              |
+---------------------------+
|          Heap             |  ← Grows upward
|   (dynamically allocated  |
|         memory)           |
+---------------------------+
|     Uninitialized Data    |  ← BSS Segment
|  (uninitialized globals)  |
+---------------------------+
|    Initialized Data       |  ← Data Segment
| (initialized global/static)|
+---------------------------+
|          Text             |  ← Code Segment
|    (program code,         |
|     constants)            |
+---------------------------+
Low Memory Addresses

Detailed Description of Each Region

1. Text Segment (Code Segment)

// All your code lives here
int main(void) {
    printf("Hello");  // This code is in text segment
    return 0;
}
  • Contains compiled machine code
  • Read-only to prevent modification
  • Shared between processes running same program

2. Data Segment (Initialized Data)

int globalInit = 42;           // In data segment
static int staticInit = 100;   // In data segment
char *str = "Hello";           // Pointer in data, string in text
  • Contains initialized global and static variables
  • Read-write

3. BSS Segment (Uninitialized Data)

int globalUninit;              // In BSS (initialized to 0)
static int staticUninit;       // In BSS (initialized to 0)
  • Contains uninitialized global and static variables
  • Initialized to zero by the system
  • BSS = "Block Started by Symbol"

4. Stack

void function(int param) {     // param on stack
    int local = 10;            // local on stack
    char buffer[100];          // buffer on stack
}
  • Contains local variables and function parameters
  • Grows downward (toward lower addresses)
  • Managed automatically (LIFO - Last In, First Out)
  • Limited size (typically 1-8 MB)

5. Heap

int *ptr = malloc(100 * sizeof(int));  // Memory from heap
// ptr itself is on stack, but points to heap memory
  • Source of dynamically allocated memory
  • Grows upward (toward higher addresses)
  • Managed manually by programmer
  • Limited only by available system memory

The Heap

What is the Heap?

The heap is a region of memory used for dynamic memory allocation. Unlike the stack, which has automatic memory management, the heap requires explicit allocation and deallocation.

Heap Characteristics

+-----------------------------------------------+
|                    HEAP                        |
+-----------------------------------------------+
| +---------+ +-----------+ +-------+ +-------+ |
| | Block 1 | | Block 2   | | Free  | | Block | |
| | 100 B   | | 500 B     | | Space | | 200 B | |
| | (used)  | | (used)    | |       | | (used)| |
| +---------+ +-----------+ +-------+ +-------+ |
+-----------------------------------------------+
     ↑                          ↑
     Allocated                  Available for
     blocks                     future allocation

Key Properties

  1. Dynamic Size: Heap can grow as needed (up to system limits)
  2. Non-Contiguous: Allocated blocks may not be adjacent
  3. Longer Lifetime: Memory persists until explicitly freed
  4. Overhead: Each allocation has metadata overhead
  5. Fragmentation: Can become fragmented over time

How the Heap Works

#include <stdlib.h>

int main(void) {
    // Request 100 bytes from heap
    void *block1 = malloc(100);

    // Heap now has:
    // [metadata][100 bytes data][remaining free space]

    // Request 200 more bytes
    void *block2 = malloc(200);

    // Heap now has:
    // [meta][100 B][meta][200 B][remaining free space]

    // Free first block
    free(block1);

    // Heap now has:
    // [free:100 B][meta][200 B][remaining free space]
    // The freed space can be reused

    return 0;
}

Why Use Dynamic Memory?

1. Unknown Size at Compile Time

// Cannot do this with static allocation
void processData(void) {
    int n;
    printf("How many numbers? ");
    scanf("%d", &n);

    // Dynamic: size determined by user input
    int *numbers = malloc(n * sizeof(int));

    // Process numbers...

    free(numbers);
}

2. Large Data Structures

// Stack overflow risk with large arrays
void riskyFunction(void) {
    // This might overflow the stack!
    int hugeArray[1000000];  // ~4 MB on stack
}

// Safe alternative using heap
void safeFunction(void) {
    // Heap can handle large allocations
    int *hugeArray = malloc(1000000 * sizeof(int));
    if (hugeArray != NULL) {
        // Use array...
        free(hugeArray);
    }
}

3. Data That Must Outlive Function Scope

// This is WRONG - returns pointer to local variable
int* createArrayWrong(int size) {
    int array[100];  // Local - destroyed when function returns
    return array;    // UNDEFINED BEHAVIOR!
}

// This is CORRECT - heap memory persists
int* createArrayCorrect(int size) {
    int *array = malloc(size * sizeof(int));
    return array;    // Valid - caller must free
}

4. Flexible Data Structures

// Linked list node - each node allocated dynamically
struct Node {
    int data;
    struct Node *next;
};

struct Node* createNode(int value) {
    struct Node *newNode = malloc(sizeof(struct Node));
    if (newNode != NULL) {
        newNode->data = value;
        newNode->next = NULL;
    }
    return newNode;
}

5. Resizable Arrays

// Array that can grow as needed
int *array = malloc(10 * sizeof(int));
int capacity = 10;
int size = 0;

// When array is full, resize it
if (size >= capacity) {
    capacity *= 2;
    array = realloc(array, capacity * sizeof(int));
}

Memory Allocation Functions

C provides four main functions for dynamic memory management in <stdlib.h>:

Function Overview

FunctionPurposeReturns
malloc()Allocate uninitialized memoryPointer or NULL
calloc()Allocate zero-initialized memoryPointer or NULL
realloc()Resize allocated memoryPointer or NULL
free()Deallocate memoryvoid

malloc() - Memory Allocation

void *malloc(size_t size);
  • Allocates size bytes
  • Returns pointer to allocated memory
  • Memory is NOT initialized (contains garbage)
  • Returns NULL if allocation fails
int *ptr = malloc(10 * sizeof(int));  // 40 bytes on most systems

calloc() - Contiguous Allocation

void *calloc(size_t nmemb, size_t size);
  • Allocates memory for nmemb elements of size bytes each
  • Memory is initialized to zero
  • Returns NULL if allocation fails
int *ptr = calloc(10, sizeof(int));  // 10 integers, all zero

realloc() - Reallocate Memory

void *realloc(void *ptr, size_t size);
  • Changes size of previously allocated memory
  • May move memory to new location
  • Preserves existing data (up to smaller of old/new size)
  • Returns NULL if allocation fails (original memory unchanged)
ptr = realloc(ptr, 20 * sizeof(int));  // Now holds 20 integers

free() - Free Memory

void free(void *ptr);
  • Deallocates memory previously allocated
  • Does not return a value
  • Passing NULL is safe (no operation)
  • Double-free is undefined behavior
free(ptr);
ptr = NULL;  // Good practice to avoid dangling pointer

The void Pointer

Understanding void*

All memory allocation functions return void*, a generic pointer type:

void *malloc(size_t size);

Why void*?

  1. Type Agnostic: malloc doesn't know what you're storing
  2. Universal: Can be assigned to any pointer type
  3. Flexible: Same function works for all data types

Casting void*

In C, casting is optional but makes intent clear:

// Without cast (valid in C)
int *p1 = malloc(sizeof(int));

// With cast (explicit intent)
int *p2 = (int*)malloc(sizeof(int));

// Note: In C++, casting is required

Type Safety Considerations

// Better practice: use sizeof with the variable
int *p = malloc(sizeof(*p));  // sizeof the dereferenced pointer type

// This is more maintainable - type only appears once
// If you change p to double*, only one change needed

Memory Alignment

What is Alignment?

Memory alignment means placing data at memory addresses that are multiples of certain values, typically the data type's size.

Memory Address: 0x1000  0x1001  0x1002  0x1003  0x1004  0x1005  0x1006  0x1007
              +-------+-------+-------+-------+-------+-------+-------+-------+
              |   int (4 bytes aligned at 0x1000)     |  int at 0x1004...     |
              +-------+-------+-------+-------+-------+-------+-------+-------+

Why Alignment Matters

  1. Performance: Aligned access is faster on most CPUs
  2. Correctness: Some architectures require alignment
  3. Portability: Proper alignment ensures code works everywhere

malloc() and Alignment

  • malloc() returns memory aligned for any standard type
  • Guaranteed to work for any fundamental type
  • Typically aligned to 8 or 16 bytes
// malloc guarantees suitable alignment for:
int *i = malloc(sizeof(int));           // 4-byte alignment OK
double *d = malloc(sizeof(double));     // 8-byte alignment OK
long double *ld = malloc(sizeof(long double));  // Max alignment OK

Common Use Cases

1. Dynamic Arrays

int* createDynamicArray(int size) {
    int *arr = malloc(size * sizeof(int));
    return arr;  // Caller must free
}

2. String Handling

char* duplicateString(const char *original) {
    size_t len = strlen(original) + 1;
    char *copy = malloc(len);
    if (copy != NULL) {
        strcpy(copy, original);
    }
    return copy;  // Caller must free
}

3. Linked Lists

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

Node* createLinkedList(int *values, int count) {
    Node *head = NULL, *tail = NULL;

    for (int i = 0; i < count; i++) {
        Node *newNode = malloc(sizeof(Node));
        newNode->data = values[i];
        newNode->next = NULL;

        if (head == NULL) {
            head = tail = newNode;
        } else {
            tail->next = newNode;
            tail = newNode;
        }
    }

    return head;
}

4. Two-Dimensional Arrays

int** create2DArray(int rows, int cols) {
    // Allocate array of row pointers
    int **array = malloc(rows * sizeof(int*));

    // Allocate each row
    for (int i = 0; i < rows; i++) {
        array[i] = malloc(cols * sizeof(int));
    }

    return array;
}

5. Structures with Flexible Members

typedef struct {
    int length;
    char data[];  // Flexible array member
} Buffer;

Buffer* createBuffer(int size) {
    Buffer *buf = malloc(sizeof(Buffer) + size);
    if (buf != NULL) {
        buf->length = size;
    }
    return buf;
}

Risks and Responsibilities

1. Memory Leaks

Memory that is allocated but never freed:

void memoryLeak(void) {
    int *ptr = malloc(100 * sizeof(int));
    // Forgot to free(ptr)!
    // Memory is lost when function returns
}

2. Dangling Pointers

Pointers that reference freed memory:

int *ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
// ptr is now dangling
*ptr = 10;  // UNDEFINED BEHAVIOR!

3. Double Free

Freeing the same memory twice:

int *ptr = malloc(sizeof(int));
free(ptr);
free(ptr);  // UNDEFINED BEHAVIOR!

4. Buffer Overflow

Writing beyond allocated memory:

int *arr = malloc(5 * sizeof(int));
for (int i = 0; i <= 5; i++) {  // Off-by-one error
    arr[i] = i;  // arr[5] is out of bounds!
}

5. Use After Free

Using memory after it's been freed:

char *str = malloc(100);
strcpy(str, "Hello");
free(str);
printf("%s\n", str);  // UNDEFINED BEHAVIOR!

6. NULL Pointer Dereference

Not checking if allocation succeeded:

int *ptr = malloc(1000000000 * sizeof(int));
// If system runs out of memory, ptr is NULL
*ptr = 42;  // Crash if ptr is NULL!

Best Practices

1. Always Check Return Values

int *ptr = malloc(size * sizeof(int));
if (ptr == NULL) {
    fprintf(stderr, "Memory allocation failed\n");
    exit(EXIT_FAILURE);  // Or handle gracefully
}

2. Use sizeof with Variables, Not Types

// Good - type info in one place
int *arr = malloc(n * sizeof(*arr));

// Less good - must update two places if type changes
int *arr = malloc(n * sizeof(int));

3. Set Pointers to NULL After Free

free(ptr);
ptr = NULL;  // Prevents accidental reuse

4. Match Every malloc with free

// Establish clear ownership
int *ptr = malloc(sizeof(int));
// ... use ptr ...
free(ptr);

5. Free Memory in Reverse Order of Allocation

char *a = malloc(100);
char *b = malloc(200);
char *c = malloc(300);

// Free in reverse order
free(c);
free(b);
free(a);

6. Document Memory Ownership

/**
 * Creates a new string. Caller is responsible for freeing
 * the returned string.
 */
char* createString(void) {
    return malloc(100);
}

7. Use Wrapper Functions

void* safe_malloc(size_t size) {
    void *ptr = malloc(size);
    if (ptr == NULL) {
        fprintf(stderr, "Fatal: Out of memory\n");
        exit(EXIT_FAILURE);
    }
    return ptr;
}

Summary

Key Concepts

  1. Dynamic memory is allocated at runtime from the heap
  2. Static memory has fixed size known at compile time
  3. The heap grows upward and requires manual management
  4. malloc() allocates uninitialized memory
  5. calloc() allocates zero-initialized memory
  6. realloc() resizes existing allocations
  7. free() releases memory back to the system

When to Use Dynamic Memory

Use Dynamic Memory WhenUse Static Memory When
Size unknown at compile timeSize is known and fixed
Large data structuresSmall variables
Data must outlive scopeScope-based lifetime OK
Flexible/resizable structuresFixed arrays sufficient
Complex data structuresSimple variables

Memory Safety Checklist

  • Always check if malloc/calloc/realloc returns NULL
  • Free all dynamically allocated memory
  • Never access memory after freeing
  • Never free the same memory twice
  • Set pointers to NULL after freeing
  • Match allocations with deallocations
  • Use tools like Valgrind to detect leaks

Next Steps

In the following topics, you will learn:

  • Detailed usage of malloc() and free()
  • Using calloc() and realloc()
  • Debugging memory leaks
  • Building dynamic data structures

Quick Reference

#include <stdlib.h>

// Allocate memory
void *malloc(size_t size);              // Uninitialized
void *calloc(size_t n, size_t size);    // Zero-initialized

// Resize memory
void *realloc(void *ptr, size_t size);

// Free memory
void free(void *ptr);

// Common patterns
int *arr = malloc(n * sizeof(*arr));    // Dynamic array
if (arr == NULL) { /* handle error */ }
free(arr);
arr = NULL;
Dynamic Memory - C Programming Tutorial | DeepML