Malloc and Free
malloc() and free() - Memory Allocation and Deallocation
Table of Contents
- •Overview
- •Understanding malloc()
- •malloc() Syntax and Usage
- •The sizeof Operator
- •Type Casting with malloc()
- •Understanding free()
- •Memory Allocation Internals
- •Common Patterns
- •Error Handling
- •Common Mistakes
- •Debugging Memory Issues
- •Best Practices
- •Summary
Overview
malloc() (memory allocation) and free() are the fundamental functions for dynamic memory management in C. They allow programs to request and release memory at runtime, providing flexibility that static allocation cannot offer.
Learning Objectives
- •Master the syntax and usage of malloc()
- •Understand proper memory deallocation with free()
- •Learn about memory allocation internals
- •Identify and avoid common memory-related bugs
- •Apply best practices for memory management
Understanding malloc()
What is malloc()?
malloc() stands for memory allocation. It is a function that:
- •Requests a block of memory from the heap
- •Returns a pointer to the beginning of that block
- •Does NOT initialize the memory (contents are indeterminate)
Function Declaration
#include <stdlib.h>
void *malloc(size_t size);
Parameters
| Parameter | Type | Description |
|---|---|---|
size | size_t | Number of bytes to allocate |
Return Value
| Condition | Return Value |
|---|---|
| Success | Pointer to allocated memory (as void*) |
| Failure | NULL (typically when out of memory) |
Key Characteristics
- •Uninitialized Memory: Contains garbage values
- •Heap Allocation: Memory comes from the heap
- •Alignment: Memory is suitably aligned for any data type
- •Persistence: Memory remains until explicitly freed
- •Generic Pointer: Returns
void*for type flexibility
malloc() Syntax and Usage
Basic Syntax
pointer = (type*)malloc(number_of_bytes);
Common Usage Patterns
Allocating a Single Variable
// Allocate one integer
int *p = (int*)malloc(sizeof(int));
if (p == NULL) {
// Handle allocation failure
}
*p = 42;
// Allocate one double
double *d = (double*)malloc(sizeof(double));
if (d != NULL) {
*d = 3.14159;
}
Allocating an Array
// Allocate array of 10 integers
int *arr = (int*)malloc(10 * sizeof(int));
// Allocate array of n doubles (n determined at runtime)
int n = getUserInput();
double *data = (double*)malloc(n * sizeof(double));
Allocating a String
// Allocate string buffer
char *str = (char*)malloc(100 * sizeof(char));
// Allocate exact size for copy
const char *original = "Hello, World!";
char *copy = (char*)malloc(strlen(original) + 1); // +1 for '\0'
strcpy(copy, original);
Allocating a Structure
typedef struct {
int id;
char name[50];
double salary;
} Employee;
Employee *emp = (Employee*)malloc(sizeof(Employee));
if (emp != NULL) {
emp->id = 101;
strcpy(emp->name, "John Doe");
emp->salary = 50000.0;
}
The sizeof Operator
Why Use sizeof?
The sizeof operator ensures:
- •Portability: Type sizes vary across systems
- •Correctness: Exact number of bytes for the type
- •Maintainability: Changes automatically if type changes
Type Sizes (Typical)
| Type | 32-bit System | 64-bit System |
|---|---|---|
char | 1 byte | 1 byte |
short | 2 bytes | 2 bytes |
int | 4 bytes | 4 bytes |
long | 4 bytes | 8 bytes |
float | 4 bytes | 4 bytes |
double | 8 bytes | 8 bytes |
pointer | 4 bytes | 8 bytes |
Two Forms of sizeof
// Form 1: sizeof with type
int *p1 = malloc(sizeof(int));
int *arr1 = malloc(10 * sizeof(int));
// Form 2: sizeof with expression (PREFERRED)
int *p2 = malloc(sizeof(*p2));
int *arr2 = malloc(10 * sizeof(*arr2));
Why sizeof(*pointer) is Better
// If you change the type, only one place to update:
double *data = malloc(n * sizeof(*data));
// Type appears only in declaration, sizeof adjusts automatically
// Compare to:
double *data = malloc(n * sizeof(double));
// Type appears twice - easy to mismatch
sizeof for Arrays vs Pointers
int staticArray[10];
int *dynamicArray = malloc(10 * sizeof(int));
sizeof(staticArray); // Returns 40 (10 * 4 bytes)
sizeof(dynamicArray); // Returns 4 or 8 (just the pointer!)
Important: sizeof on a pointer returns the pointer size, not the allocated memory size. There's no way to determine allocation size from the pointer alone.
Type Casting with malloc()
In C
// Without cast (valid in C, implicit conversion from void*)
int *p1 = malloc(sizeof(int));
// With cast (explicit, shows intent)
int *p2 = (int*)malloc(sizeof(int));
In C++
// Cast is REQUIRED in C++
int *p = (int*)malloc(sizeof(int));
// Or prefer: int *p = new int;
Best Practice
For C code, the cast is optional. Many style guides recommend:
- •No cast: Relies on implicit conversion, cleaner
- •With cast: More explicit, compatible with C++
// Recommended pattern (no cast, sizeof with variable)
int *p = malloc(sizeof *p);
Understanding free()
What is free()?
free() returns dynamically allocated memory to the heap, making it available for future allocations.
Function Declaration
#include <stdlib.h>
void free(void *ptr);
Parameters
| Parameter | Type | Description |
|---|---|---|
ptr | void* | Pointer to memory previously allocated by malloc/calloc/realloc |
Return Value
free() returns nothing (void).
Key Behaviors
int *p = malloc(sizeof(int));
*p = 42;
free(p); // Memory is now deallocated
// p still holds the address, but memory is invalid
p = NULL; // Good practice: prevents accidental reuse
What free() Does
- •Marks memory as available for future allocations
- •Does NOT clear the memory contents
- •Does NOT change the pointer value
- •Updates internal heap management structures
Special Cases
// Passing NULL is safe (no operation)
free(NULL); // Does nothing, no error
// Passing unallocated memory is UNDEFINED BEHAVIOR
int x = 10;
free(&x); // WRONG! x is on stack
// Double free is UNDEFINED BEHAVIOR
int *p = malloc(100);
free(p);
free(p); // WRONG! Already freed
Memory Allocation Internals
How the Heap Works
Heap Memory
+---------+------------------+---------+------------------+----------+
| Header1 | Allocated Block1 | Header2 | Allocated Block2 | Free |
| (meta) | (your data) | (meta) | (your data) | Space |
+---------+------------------+---------+------------------+----------+
^ ^
| |
malloc returns malloc returns
this address this address
Metadata (Header) Contains
- •Size of the block
- •Allocation status (free or in use)
- •Pointers to adjacent blocks
- •Magic numbers for debugging
Memory Layout Example
malloc(100) returns:
+--------+----------------------------------+
| Header | 100 bytes |
| ~16 B | (your data) |
+--------+----------------------------------+
^
Pointer returned to user
Fragmentation
Over time, repeated malloc/free can cause fragmentation:
Before many allocations:
[====================Free Space====================]
After many alloc/free cycles:
[Used][Free][Used][Free][Used][Free][Used][Free][Used]
^^^^ ^^^^ ^^^^ ^^^^
These small free blocks may be too small for new allocations
Allocation Strategies
Memory allocators use various strategies:
- •First Fit: Use first block large enough
- •Best Fit: Use smallest block that fits
- •Worst Fit: Use largest block
- •Buddy System: Power-of-2 block sizes
Common Patterns
Pattern 1: Allocate-Use-Free
void processData(int n) {
// Allocate
int *data = malloc(n * sizeof(*data));
if (data == NULL) {
fprintf(stderr, "Allocation failed\n");
return;
}
// Use
for (int i = 0; i < n; i++) {
data[i] = i * 2;
}
// Free
free(data);
}
Pattern 2: Factory Function
// Function that creates and returns allocated memory
char* createGreeting(const char *name) {
// Calculate size: "Hello, " (7) + name + "!" (1) + null (1)
size_t size = 7 + strlen(name) + 2;
char *greeting = malloc(size);
if (greeting != NULL) {
sprintf(greeting, "Hello, %s!", name);
}
return greeting; // Caller must free
}
// Usage
char *msg = createGreeting("Alice");
printf("%s\n", msg);
free(msg); // Don't forget!
Pattern 3: Array of Pointers
// Array of strings
char **createStringArray(int count) {
char **arr = malloc(count * sizeof(char*));
if (arr == NULL) return NULL;
for (int i = 0; i < count; i++) {
arr[i] = malloc(100); // Each string up to 100 chars
if (arr[i] == NULL) {
// Cleanup on failure
while (--i >= 0) free(arr[i]);
free(arr);
return NULL;
}
}
return arr;
}
// Free in reverse
void freeStringArray(char **arr, int count) {
if (arr != NULL) {
for (int i = 0; i < count; i++) {
free(arr[i]);
}
free(arr);
}
}
Pattern 4: Structure with Pointer Members
typedef struct {
char *name;
int *scores;
int scoreCount;
} Student;
Student* createStudent(const char *name, int numScores) {
Student *s = malloc(sizeof(Student));
if (s == NULL) return NULL;
s->name = malloc(strlen(name) + 1);
if (s->name == NULL) {
free(s);
return NULL;
}
strcpy(s->name, name);
s->scores = malloc(numScores * sizeof(int));
if (s->scores == NULL) {
free(s->name);
free(s);
return NULL;
}
s->scoreCount = numScores;
return s;
}
void freeStudent(Student *s) {
if (s != NULL) {
free(s->scores);
free(s->name);
free(s);
}
}
Error Handling
Why malloc() Can Fail
- •Insufficient memory: System is out of RAM
- •Fragmentation: No contiguous block large enough
- •Process limits: Exceeded maximum heap size
- •Integer overflow: Requested size calculation overflowed
Always Check Return Value
// WRONG: No error check
int *p = malloc(SIZE);
*p = 10; // Crash if malloc failed!
// CORRECT: Check for NULL
int *p = malloc(SIZE);
if (p == NULL) {
fprintf(stderr, "Error: Memory allocation failed\n");
exit(EXIT_FAILURE);
}
*p = 10;
Error Handling Strategies
Strategy 1: Exit on Failure
void *safe_malloc(size_t size) {
void *ptr = malloc(size);
if (ptr == NULL) {
fprintf(stderr, "Fatal error: Out of memory\n");
exit(EXIT_FAILURE);
}
return ptr;
}
Strategy 2: Return Error Code
int allocateBuffer(char **buffer, size_t size) {
*buffer = malloc(size);
if (*buffer == NULL) {
return -1; // Error
}
return 0; // Success
}
Strategy 3: Graceful Degradation
// Try smaller allocation if large one fails
int *data = malloc(LARGE_SIZE);
if (data == NULL) {
data = malloc(SMALL_SIZE);
if (data == NULL) {
// Handle complete failure
}
// Work with smaller buffer
}
Common Mistakes
Mistake 1: Memory Leaks
// WRONG: Lost pointer causes leak
void leaky(void) {
int *p = malloc(100);
// No free() - memory leaked when function returns
}
// WRONG: Overwriting pointer
int *p = malloc(100);
p = malloc(200); // First allocation leaked!
// CORRECT:
int *p = malloc(100);
free(p);
p = malloc(200);
Mistake 2: Use After Free
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p); // WRONG: Using freed memory!
// CORRECT:
free(p);
p = NULL; // Prevents accidental use
Mistake 3: Double Free
int *p = malloc(100);
free(p);
free(p); // WRONG: Double free!
// CORRECT:
free(p);
p = NULL;
free(p); // Safe: free(NULL) does nothing
Mistake 4: Freeing Non-Heap Memory
int x = 10;
free(&x); // WRONG: x is on stack!
char str[] = "Hello";
free(str); // WRONG: str is on stack!
char *ptr = "Constant";
free(ptr); // WRONG: String literal in read-only memory!
Mistake 5: Buffer Overflow
char *str = malloc(5);
strcpy(str, "Hello"); // WRONG: "Hello\0" is 6 bytes!
// CORRECT:
char *str = malloc(strlen("Hello") + 1);
strcpy(str, "Hello");
Mistake 6: Wrong Size Calculation
// WRONG: sizeof(int*) instead of sizeof(int)
int *arr = malloc(10 * sizeof(int*)); // Allocates for pointers, not ints!
// CORRECT:
int *arr = malloc(10 * sizeof(int));
// Or better:
int *arr = malloc(10 * sizeof(*arr));
Mistake 7: Integer Overflow
// If n is large, n * sizeof(int) might overflow
size_t n = get_huge_number();
int *arr = malloc(n * sizeof(int)); // Might allocate less than expected!
// CORRECT: Check for overflow
if (n > SIZE_MAX / sizeof(int)) {
// Handle overflow
}
int *arr = malloc(n * sizeof(int));
Debugging Memory Issues
Tool 1: Valgrind (Linux)
# Compile with debug symbols
gcc -g program.c -o program
# Run with Valgrind
valgrind --leak-check=full ./program
Valgrind detects:
- •Memory leaks
- •Use of uninitialized memory
- •Invalid reads/writes
- •Double frees
- •Mismatched free (malloc with delete, etc.)
Tool 2: AddressSanitizer
# Compile with sanitizer
gcc -fsanitize=address -g program.c -o program
# Run normally
./program
AddressSanitizer detects:
- •Buffer overflows
- •Use after free
- •Double free
- •Memory leaks (with additional flags)
Tool 3: Debug malloc Libraries
Many systems provide debug versions:
- •Electric Fence
- •DUMA
- •tcmalloc (with heap checker)
Manual Debugging Techniques
// Technique 1: Wrapper functions with logging
void *debug_malloc(size_t size, const char *file, int line) {
void *ptr = malloc(size);
printf("MALLOC: %zu bytes at %p (%s:%d)\n", size, ptr, file, line);
return ptr;
}
void debug_free(void *ptr, const char *file, int line) {
printf("FREE: %p (%s:%d)\n", ptr, file, line);
free(ptr);
}
#define malloc(size) debug_malloc(size, __FILE__, __LINE__)
#define free(ptr) debug_free(ptr, __FILE__, __LINE__)
// Technique 2: Memory fill patterns
void *debug_malloc(size_t size) {
void *ptr = malloc(size);
if (ptr) memset(ptr, 0xAA, size); // Fill with pattern
return ptr;
}
void debug_free(void *ptr, size_t size) {
if (ptr) memset(ptr, 0xDD, size); // Fill before free
free(ptr);
}
Best Practices
1. Always Check Return Value
int *p = malloc(size);
if (p == NULL) {
// Handle error
return ERROR_CODE;
}
2. Use sizeof(*pointer)
// Good: type only in one place
double *data = malloc(n * sizeof(*data));
// Less good: type repeated
double *data = malloc(n * sizeof(double));
3. Set Pointer to NULL After Free
free(ptr);
ptr = NULL;
4. Free in Reverse Order of Allocation
a = malloc(...);
b = malloc(...);
c = malloc(...);
// Free in reverse
free(c);
free(b);
free(a);
5. Document Ownership
/**
* Creates a new string copy.
* @return Newly allocated string. Caller must free().
*/
char *duplicateString(const char *s);
/**
* Frees a Person structure and all its members.
*/
void destroyPerson(Person *p);
6. Initialize Memory After Allocation
int *arr = malloc(n * sizeof(*arr));
if (arr != NULL) {
memset(arr, 0, n * sizeof(*arr)); // Or use calloc
}
7. Match Allocation and Deallocation
| Allocation | Deallocation |
|---|---|
malloc() | free() |
calloc() | free() |
realloc() | free() |
new (C++) | delete |
new[] (C++) | delete[] |
8. Use Consistent Error Handling
// One approach: wrapper function
void *safe_malloc(size_t size) {
void *p = malloc(size);
if (p == NULL && size != 0) {
perror("malloc");
exit(EXIT_FAILURE);
}
return p;
}
Summary
Key Points
- •malloc(size) allocates
sizebytes and returns a pointer - •free(ptr) deallocates memory previously allocated
- •Always check if malloc returns NULL
- •Always free allocated memory when done
- •Set pointers to NULL after freeing
- •Use sizeof(*ptr) for reliable size calculation
- •Never use memory after freeing
- •Never free the same memory twice
- •Never free non-heap memory
Quick Reference
#include <stdlib.h>
// Allocate
int *p = malloc(sizeof(*p)); // Single value
int *arr = malloc(n * sizeof(*arr)); // Array
// Check
if (p == NULL) { /* error */ }
// Use
*p = 42;
arr[0] = 10;
// Deallocate
free(p);
p = NULL;
free(arr);
arr = NULL;
Memory Management Checklist
- • Include
<stdlib.h> - • Use
sizeoffor size calculation - • Check return value for NULL
- • Initialize memory if needed
- • Free all allocated memory
- • Set pointers to NULL after free
- • No use-after-free
- • No double-free
- • No freeing non-heap memory
- • Test with Valgrind or AddressSanitizer
Quick Reference Card
// === MALLOC ===
void *malloc(size_t size);
// Returns: pointer to allocated memory, or NULL on failure
// Memory is NOT initialized
// === FREE ===
void free(void *ptr);
// Returns: nothing
// Passing NULL is safe
// === PATTERNS ===
// Single value
Type *p = malloc(sizeof(*p));
// Array
Type *arr = malloc(count * sizeof(*arr));
// String copy
char *copy = malloc(strlen(orig) + 1);
// Structure
Struct *s = malloc(sizeof(*s));
// === SAFETY ===
if (ptr == NULL) { /* handle error */ }
free(ptr);
ptr = NULL;