malloc & free - Dynamic Memory Allocation
Dynamic memory allocation allows programs to request memory at runtime from the heap. This is essential for creating flexible data structures whose size isn’t known at compile time.
Understanding Memory Allocation
Stack vs Heap
Stack:
- Automatic allocation/deallocation
- Limited size
- Fast access
- Variables go out of scope automatically
int x = 5; // Stack
char str[100]; // Stack
Heap:
- Manual allocation/deallocation
- Large size (limited by system memory)
- Slightly slower access
- Must manually free memory
int *x = malloc(sizeof(int)); // Heap
char *str = malloc(100); // Heap
Memory allocated on the heap must be manually freed. Failure to free memory causes memory leaks, where your program consumes more and more memory until it crashes or slows down the system.
malloc() Syntax
void *malloc(size_t size);
Parameters:
size - Number of bytes to allocate
Returns:
- Pointer to allocated memory on success
NULL on failure (insufficient memory)
Key points:
- Returns
void * (generic pointer) - must be cast to appropriate type
- Memory is uninitialized (contains garbage values)
- Always check for
NULL before using
Function Prototypes
char *create_array(unsigned int size, char c);
char *_strdup(char *str);
char *str_concat(char *s1, char *s2);
int **alloc_grid(int width, int height);
void free_grid(int **grid, int height);
Basic malloc Examples
create_array() - Allocate and Initialize Array
#include <stdlib.h>
/**
* create_array - creates an array of chars.
* @size: size of the array.
* @c: storaged char
*
* Return: pointer of an array of chars
*/
char *create_array(unsigned int size, char c)
{
char *cr;
unsigned int i;
if (size == 0)
return (NULL);
cr = malloc(sizeof(c) * size);
if (cr == NULL)
return (NULL);
for (i = 0; i < size; i++)
cr[i] = c;
return (cr);
}
Key steps:
- Validate input: Return
NULL for size 0
- Allocate memory:
malloc(sizeof(c) * size)
- Check allocation: Return
NULL if malloc fails
- Initialize memory: Set all bytes to specified character
- Return pointer: Caller is responsible for freeing
Usage:
char *buffer = create_array(10, 'A');
if (buffer != NULL)
{
// buffer = "AAAAAAAAAA"
printf("%s\n", buffer);
free(buffer); // Must free!
}
Always use sizeof() to calculate memory size:malloc(sizeof(char) * 100) // 100 bytes
malloc(sizeof(int) * 50) // 200 bytes (if int is 4 bytes)
malloc(sizeof(double) * 10) // 80 bytes (if double is 8 bytes)
_strdup() - Duplicate String
#include <stdlib.h>
/**
* _strdup - returns a pointer to a newly allocated space in memory.
* @str: string.
*
* Return: pointer of an array of chars
*/
char *_strdup(char *str)
{
char *strout;
unsigned int i, j;
if (str == NULL)
return (NULL);
for (i = 0; str[i] != '\0'; i++)
;
strout = (char *)malloc(sizeof(char) * (i + 1));
if (strout == NULL)
return (NULL);
for (j = 0; j <= i; j++)
strout[j] = str[j];
return (strout);
}
Algorithm:
- Check for NULL input
- Calculate string length (including null terminator)
- Allocate memory: Size = length + 1 for
\0
- Copy string including null terminator
- Return new string
Example:
char *original = "Hello";
char *copy = _strdup(original);
if (copy != NULL)
{
printf("%s\n", copy); // "Hello"
copy[0] = 'J'; // Modify copy
printf("%s\n", copy); // "Jello"
printf("%s\n", original); // "Hello" (unchanged)
free(copy);
}
Don’t forget the null terminator!// ❌ BAD - not enough space for \0
malloc(sizeof(char) * strlen(str));
// ✅ GOOD - includes null terminator
malloc(sizeof(char) * (strlen(str) + 1));
Advanced malloc Usage
str_concat() - Concatenate Strings
#include <stdlib.h>
/**
* str_concat - concatenates two strings.
* @s1: first string.
* @s2: second string.
*
* Return: pointer of an array of chars
*/
char *str_concat(char *s1, char *s2)
{
char *strout;
unsigned int i, j, k, limit;
if (s1 == NULL)
s1 = "";
if (s2 == NULL)
s2 = "";
for (i = 0; s1[i] != '\0'; i++)
;
for (j = 0; s2[j] != '\0'; j++)
;
strout = malloc(sizeof(char) * (i + j + 1));
if (strout == NULL)
{
free(strout);
return (NULL);
}
for (k = 0; k < i; k++)
strout[k] = s1[k];
limit = j;
for (j = 0; j <= limit; k++, j++)
strout[k] = s2[j];
return (strout);
}
Process:
- Handle NULL inputs: Treat as empty strings
- Calculate total length: len(s1) + len(s2) + 1
- Allocate memory for combined string
- Copy s1 to result
- Copy s2 after s1 (including null terminator)
Example:
char *s1 = "Hello";
char *s2 = " World";
char *result = str_concat(s1, s2);
if (result != NULL)
{
printf("%s\n", result); // "Hello World"
free(result);
}
// Handles NULL
char *result2 = str_concat(NULL, "Test");
// result2 = "Test"
Multi-Dimensional Dynamic Arrays
alloc_grid() - 2D Integer Array
#include <stdlib.h>
/**
* alloc_grid - returns a pointer to a 2 dimensional array of integers.
* @width: width of the array.
* @height: height of the array.
*
* Return: pointer of an array of integers
*/
int **alloc_grid(int width, int height)
{
int **gridout;
int i, j;
if (width < 1 || height < 1)
return (NULL);
gridout = malloc(height * sizeof(int *));
if (gridout == NULL)
{
free(gridout);
return (NULL);
}
for (i = 0; i < height; i++)
{
gridout[i] = malloc(width * sizeof(int));
if (gridout[i] == NULL)
{
for (i--; i >= 0; i--)
free(gridout[i]);
free(gridout);
return (NULL);
}
}
for (i = 0; i < height; i++)
for (j = 0; j < width; j++)
gridout[i][j] = 0;
return (gridout);
}
Understanding 2D allocation:
gridout → [ptr0] → [int][int][int]...[int] (width elements)
[ptr1] → [int][int][int]...[int]
[ptr2] → [int][int][int]...[int]
...
[ptrN] → [int][int][int]...[int] (height rows)
Steps:
- Allocate array of row pointers:
int ** (array of pointers)
- For each row, allocate array of integers
- Error handling: Free previously allocated rows if any allocation fails
- Initialize all elements to 0
Example:
int **grid = alloc_grid(3, 4); // 3 columns, 4 rows
if (grid != NULL)
{
// Access elements
grid[0][0] = 1;
grid[1][2] = 5;
grid[3][1] = 9;
// Print grid
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 3; j++)
printf("%d ", grid[i][j]);
printf("\n");
}
// Must free properly (see next section)
}
**Why int ?
int * - pointer to integer
int ** - pointer to pointer to integer (array of pointers)
Each row is a separate malloc, so we have an array of pointers where each pointer points to a row.
free_grid() - Free 2D Array
#include <stdlib.h>
/**
* free_grid - frees a 2 dimensional grid.
* @grid: multidimensional array of integers.
* @height: height of the grid.
*
* Return: no return
*/
void free_grid(int **grid, int height)
{
if (grid != NULL && height != 0)
{
for (; height >= 0; height--)
free(grid[height]);
free(grid);
}
}
Critical rule for 2D arrays:
Free in reverse order of allocation:
- Free each row (the
int * arrays)
- Free the array of pointers (the
int **)
Memory leak example:
// ❌ BAD - memory leak!
int **grid = alloc_grid(10, 10);
free(grid); // Leaks all row arrays!
// ✅ GOOD - frees everything
int **grid = alloc_grid(10, 10);
free_grid(grid, 10);
Order matters!// ❌ WRONG - frees pointer array first
free(grid);
for (int i = 0; i < height; i++)
free(grid[i]); // ⚠️ Accessing freed memory!
// ✅ CORRECT - frees rows first, then pointer array
for (int i = 0; i < height; i++)
free(grid[i]);
free(grid);
Memory Management Best Practices
1. Always Check malloc Return Value
// ❌ BAD - assumes success
int *ptr = malloc(sizeof(int) * 100);
ptr[0] = 42; // Crash if malloc failed!
// ✅ GOOD - checks for NULL
int *ptr = malloc(sizeof(int) * 100);
if (ptr == NULL)
{
fprintf(stderr, "Memory allocation failed\n");
return (1);
}
ptr[0] = 42;
2. Initialize Pointers to NULL
int *ptr = NULL;
if (condition)
ptr = malloc(sizeof(int));
if (ptr != NULL)
free(ptr); // Safe even if malloc never called
3. Set Pointers to NULL After Freeing
free(ptr);
ptr = NULL; // Prevents double-free and use-after-free
4. Match Every malloc with free
// For every malloc...
char *str = malloc(100);
// ...there must be a free
free(str);
5. Don’t Free Stack Memory
char stack_array[100];
char *heap_array = malloc(100);
free(heap_array); // ✓ OK
free(stack_array); // ❌ CRASH! Only free malloc'd memory
Common Memory Errors
1. Memory Leak
// ❌ Memory leak
void function()
{
char *str = malloc(100);
strcpy(str, "Hello");
// Forgot to free!
} // str goes out of scope, memory lost forever
// ✅ Proper cleanup
void function()
{
char *str = malloc(100);
if (str == NULL)
return;
strcpy(str, "Hello");
free(str);
}
2. Double Free
// ❌ Double free
char *ptr = malloc(100);
free(ptr);
free(ptr); // ⚠️ Undefined behavior!
// ✅ Safe
char *ptr = malloc(100);
free(ptr);
ptr = NULL;
if (ptr != NULL) // Check prevents double free
free(ptr);
3. Use After Free
// ❌ Use after free
char *ptr = malloc(100);
free(ptr);
ptr[0] = 'A'; // ⚠️ Accessing freed memory!
// ✅ Correct order
char *ptr = malloc(100);
ptr[0] = 'A'; // Use first
free(ptr); // Free last
4. Buffer Overflow
// ❌ Buffer overflow
char *str = malloc(5);
strcpy(str, "Hello World"); // ⚠️ Writes beyond allocated memory!
// ✅ Proper sizing
char *src = "Hello World";
char *str = malloc(strlen(src) + 1);
strcpy(str, src);
5. Returning Stack Address
// ❌ Returns address of local variable
char *function()
{
char local[100] = "Hello";
return (local); // ⚠️ Dangling pointer!
}
// ✅ Returns heap-allocated memory
char *function()
{
char *str = malloc(100);
if (str != NULL)
strcpy(str, "Hello");
return (str); // Caller must free
}
Debugging Memory Issues
Using Valgrind
# Compile with debug symbols
gcc -g program.c -o program
# Run with valgrind
valgrind --leak-check=full ./program
Valgrind output example:
==12345== HEAP SUMMARY:
==12345== in use at exit: 100 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 100 bytes allocated
==12345==
==12345== 100 bytes in 1 blocks are definitely lost
This shows a memory leak - 1 allocation but 0 frees.
Practical Examples
Dynamic String Building
char *build_message(char *name, int age)
{
char *msg;
int len;
// Calculate required size
len = strlen("Name: ") + strlen(name) +
strlen(", Age: ") + 10; // 10 for number
msg = malloc(len);
if (msg == NULL)
return (NULL);
sprintf(msg, "Name: %s, Age: %d", name, age);
return (msg);
}
// Usage
char *message = build_message("Alice", 25);
if (message != NULL)
{
printf("%s\n", message);
free(message);
}
Resizing Arrays
int *resize_array(int *old_array, int old_size, int new_size)
{
int *new_array = malloc(new_size * sizeof(int));
int copy_size = (old_size < new_size) ? old_size : new_size;
if (new_array == NULL)
return (NULL);
// Copy old data
for (int i = 0; i < copy_size; i++)
new_array[i] = old_array[i];
// Initialize new elements to 0
for (int i = copy_size; i < new_size; i++)
new_array[i] = 0;
return (new_array);
}
Key Takeaways
malloc() allocates memory on the heap at runtime
- Always check if
malloc() returns NULL (allocation failed)
- Memory from
malloc() is uninitialized (garbage values)
- Every
malloc() must have a corresponding free()
- Free memory in reverse order of allocation (especially for 2D arrays)
- Memory leaks occur when allocated memory is not freed
- Set pointers to
NULL after freeing to prevent errors
- Use
sizeof() to calculate allocation size correctly
- For 2D arrays: allocate array of pointers, then allocate each row
- Never free the same pointer twice
- Never access memory after freeing it
- Use Valgrind to detect memory leaks and errors