Dynamic Memory
Introduction to Dynamic Memory Allocation
Table of Contents
- •Overview
- •What is Dynamic Memory?
- •Static vs Dynamic Memory
- •Memory Layout of a C Program
- •The Heap
- •Why Use Dynamic Memory?
- •Memory Allocation Functions
- •The void Pointer
- •Memory Alignment
- •Common Use Cases
- •Risks and Responsibilities
- •Best Practices
- •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
- •Runtime Allocation: Memory size can be determined during program execution
- •Flexible Size: Allocations can vary based on user input or program state
- •Manual Management: Programmer is responsible for freeing allocated memory
- •Heap Storage: Dynamic memory comes from the heap region
- •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
| Feature | Static Memory | Dynamic Memory |
|---|---|---|
| Allocation Time | Compile time | Runtime |
| Size | Fixed | Variable |
| Location | Stack/Data segment | Heap |
| Lifetime | Scope-based (auto) | Until free() called |
| Speed | Fast | Slower |
| Management | Automatic | Manual |
| Flexibility | Limited | High |
| Risk of Leaks | None | Yes |
| Fragmentation | None | Possible |
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
- •Dynamic Size: Heap can grow as needed (up to system limits)
- •Non-Contiguous: Allocated blocks may not be adjacent
- •Longer Lifetime: Memory persists until explicitly freed
- •Overhead: Each allocation has metadata overhead
- •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
| Function | Purpose | Returns |
|---|---|---|
malloc() | Allocate uninitialized memory | Pointer or NULL |
calloc() | Allocate zero-initialized memory | Pointer or NULL |
realloc() | Resize allocated memory | Pointer or NULL |
free() | Deallocate memory | void |
malloc() - Memory Allocation
void *malloc(size_t size);
- •Allocates
sizebytes - •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
nmembelements ofsizebytes 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*?
- •Type Agnostic: malloc doesn't know what you're storing
- •Universal: Can be assigned to any pointer type
- •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
- •Performance: Aligned access is faster on most CPUs
- •Correctness: Some architectures require alignment
- •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
- •Dynamic memory is allocated at runtime from the heap
- •Static memory has fixed size known at compile time
- •The heap grows upward and requires manual management
- •malloc() allocates uninitialized memory
- •calloc() allocates zero-initialized memory
- •realloc() resizes existing allocations
- •free() releases memory back to the system
When to Use Dynamic Memory
| Use Dynamic Memory When | Use Static Memory When |
|---|---|
| Size unknown at compile time | Size is known and fixed |
| Large data structures | Small variables |
| Data must outlive scope | Scope-based lifetime OK |
| Flexible/resizable structures | Fixed arrays sufficient |
| Complex data structures | Simple 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()andfree() - •Using
calloc()andrealloc() - •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;