Stack vs Heap Memory
Before diving into dynamic allocation functions, you need to understand where memory comes from. C programs use two main memory regions: the stack (automatic, fast, limited) and the heap (manual, flexible, larger). Understanding the difference is crucial for writing efficient C programs.
The Stack
The stack is where local variables and function call information are stored. When you declare a variable inside a function, it goes on the stack. The stack is managed automatically - memory is allocated when you enter a function and freed when you leave. This makes it fast, but the stack has a fixed, limited size (typically 1-8 MB).
#include <stdio.h>
void exampleFunction() {
int localVar = 42; // Stored on the stack
char buffer[100]; // 100 bytes on the stack
double values[10]; // 80 bytes on the stack
// All these are automatically freed when function returns
}
int main() {
int x = 10; // Stack variable
exampleFunction(); // Stack grows, then shrinks
return 0;
}
Stack variables are automatically allocated when declared and freed when the function returns. No manual memory management is needed, but you are limited by the stack size.
void dangerousFunction() {
int hugeArray[10000000]; // 40 MB - way too big for stack!
// This will likely cause a stack overflow crash
}
Large arrays should not be declared as local variables because they can exceed the stack size limit, causing a stack overflow crash.
The Heap
The heap is a larger, more flexible memory region. Unlike the stack, heap memory is not managed automatically - you must explicitly request it and explicitly free it when done. The heap can grow much larger (limited only by available RAM), making it suitable for large data or data whose size is not known until runtime.
#include <stdio.h>
#include <stdlib.h>
int main() {
// Request memory from the heap
int *heapArray = (int*)malloc(10000000 * sizeof(int));
if (heapArray != NULL) {
// Use the memory...
heapArray[0] = 42;
// MUST free when done!
free(heapArray);
}
return 0;
}
Heap memory is allocated using malloc() and must be freed using free(). The heap can handle much larger allocations than the stack but requires manual management.
| Feature | Stack | Heap |
|---|---|---|
| Management | Automatic | Manual (malloc/free) |
| Size | Limited (1-8 MB typical) | Large (limited by RAM) |
| Speed | Very fast | Slower (allocation overhead) |
| Lifetime | Function scope | Until explicitly freed |
| Use Case | Small, fixed-size local data | Large or dynamic-size data |
Practice Questions
Task: Look at the following code and identify which variables are on the stack and which are on the heap.
int globalVar = 10;
void func() {
int local = 5;
static int staticVar = 20;
int *ptr = (int*)malloc(sizeof(int));
*ptr = 30;
}
Show Solution
// globalVar - Global/Data segment (not stack or heap)
// local - Stack (local variable)
// staticVar - Data segment (static storage)
// ptr - Stack (the pointer variable itself)
// *ptr (the value 30) - Heap (allocated with malloc)
Task: The following code causes a stack overflow. Rewrite it to use heap allocation instead.
void processData() {
double data[1000000]; // Stack overflow!
for (int i = 0; i < 1000000; i++) {
data[i] = i * 1.5;
}
}
Show Solution
#include <stdlib.h>
void processData() {
double *data = (double*)malloc(1000000 * sizeof(double));
if (data == NULL) {
return; // Handle allocation failure
}
for (int i = 0; i < 1000000; i++) {
data[i] = i * 1.5;
}
free(data); // Don't forget to free!
}
Task: Write a program that prints the size in bytes for allocating: 100 integers, 50 doubles, and 200 characters.
Show Solution
#include <stdio.h>
int main() {
printf("100 integers: %zu bytes\n", 100 * sizeof(int));
printf("50 doubles: %zu bytes\n", 50 * sizeof(double));
printf("200 characters: %zu bytes\n", 200 * sizeof(char));
// Typical output on 64-bit system:
// 100 integers: 400 bytes
// 50 doubles: 400 bytes
// 200 characters: 200 bytes
return 0;
}
Task: This recursive function causes stack overflow. Rewrite it using iteration and heap memory to handle large values of n.
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // Deep recursion
}
Show Solution
#include <stdio.h>
#include <stdlib.h>
long long factorial(int n) {
if (n < 0) return -1;
if (n <= 1) return 1;
// Use heap for intermediate results if needed
long long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
int main() {
printf("20! = %lld\n", factorial(20));
return 0;
}
The iterative version uses constant stack space regardless of n, avoiding stack overflow for large inputs.
malloc() - Memory Allocation
The malloc() function is your primary tool for allocating memory on the heap.
It requests a block of memory of a specified size and returns a pointer to it. If the
allocation fails (not enough memory), it returns NULL.
malloc()
void* malloc(size_t size) - Allocates size bytes of uninitialized
memory on the heap. Returns a pointer to the allocated memory, or NULL if allocation fails.
Key points: Memory is NOT initialized (contains garbage values). Always check if the return value is NULL. Always free() when done.
Basic malloc() Usage
To use malloc(), you need to include <stdlib.h>. The function takes the
number of bytes to allocate. Since malloc() returns a void* (generic pointer),
you should cast it to the appropriate type.
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocate memory for a single integer
int *numPtr = (int*)malloc(sizeof(int));
if (numPtr == NULL) {
printf("Memory allocation failed!\n");
return 1;
}
*numPtr = 42;
printf("Value: %d\n", *numPtr); // Value: 42
free(numPtr);
return 0;
}
This example allocates memory for a single integer, stores a value in it, prints the value, and then frees the memory. Always use sizeof() to get the correct size.
Allocating Arrays with malloc()
The most common use of malloc() is to create dynamic arrays - arrays whose size is determined at runtime. Multiply the number of elements by the size of each element.
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("How many numbers? ");
scanf("%d", &n);
// Allocate array of n integers
int *numbers = (int*)malloc(n * sizeof(int));
if (numbers == NULL) {
printf("Allocation failed!\n");
return 1;
}
// Use the array just like a normal array
for (int i = 0; i < n; i++) {
numbers[i] = i * 10;
}
// Print values
for (int i = 0; i < n; i++) {
printf("%d ", numbers[i]); // 0 10 20 30 ...
}
free(numbers);
return 0;
}
This program asks the user for an array size at runtime, then allocates exactly that much memory. This is impossible with regular static arrays where size must be known at compile time.
int *arr = (int*)malloc(5 * sizeof(int));
// arr[0], arr[1], etc. contain GARBAGE - not zero!
// You must initialize them yourself:
for (int i = 0; i < 5; i++) {
arr[i] = 0;
}
Unlike global or static arrays which are zero-initialized, malloc() returns uninitialized memory. Always initialize your data before reading it.
Practice Questions
Task: Allocate memory for a string of 50 characters (including the null terminator), copy "Hello, Dynamic World!" into it, print it, and free the memory.
Show Solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *str = (char*)malloc(50 * sizeof(char));
if (str == NULL) {
printf("Allocation failed!\n");
return 1;
}
strcpy(str, "Hello, Dynamic World!");
printf("%s\n", str);
free(str);
return 0;
}
Task: Write a program that asks the user for the number of test scores, reads each score, calculates the average, and prints it.
Show Solution
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("How many scores? ");
scanf("%d", &n);
float *scores = (float*)malloc(n * sizeof(float));
if (scores == NULL) {
printf("Allocation failed!\n");
return 1;
}
float sum = 0;
for (int i = 0; i < n; i++) {
printf("Score %d: ", i + 1);
scanf("%f", &scores[i]);
sum += scores[i];
}
printf("Average: %.2f\n", sum / n);
free(scores);
return 0;
}
Task: Allocate an array of 5 integers using malloc(), initialize each element to its index squared (0, 1, 4, 9, 16), print them, and free the memory.
Show Solution
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr = (int*)malloc(5 * sizeof(int));
if (arr == NULL) {
printf("Allocation failed!\n");
return 1;
}
for (int i = 0; i < 5; i++) {
arr[i] = i * i;
}
printf("Squares: ");
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
printf("\n"); // Output: 0 1 4 9 16
free(arr);
return 0;
}
Task: Create a struct for a Student (name, age, grade). Allocate memory for 3 students, fill in their data, print all students, and properly free all memory.
Show Solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *name;
int age;
float grade;
} Student;
int main() {
int n = 3;
Student *students = (Student*)malloc(n * sizeof(Student));
if (students == NULL) return 1;
// Allocate and fill student 1
students[0].name = (char*)malloc(20);
strcpy(students[0].name, "Alice");
students[0].age = 20;
students[0].grade = 85.5;
// Student 2
students[1].name = (char*)malloc(20);
strcpy(students[1].name, "Bob");
students[1].age = 22;
students[1].grade = 90.0;
// Student 3
students[2].name = (char*)malloc(20);
strcpy(students[2].name, "Charlie");
students[2].age = 21;
students[2].grade = 78.5;
// Print all
for (int i = 0; i < n; i++) {
printf("%s, %d, %.1f\n",
students[i].name,
students[i].age,
students[i].grade);
}
// Free names first, then array
for (int i = 0; i < n; i++) {
free(students[i].name);
}
free(students);
return 0;
}
calloc() - Contiguous Allocation
The calloc() function is similar to malloc() but with two key differences:
it takes the number of elements and size separately, and it initializes all allocated
memory to zero. This makes it perfect for arrays where you need a clean slate.
calloc()
void* calloc(size_t num, size_t size) - Allocates memory for an array of
num elements, each of size bytes. All bytes are initialized to zero.
Key difference from malloc(): Memory is zero-initialized! All integers will be 0, all pointers will be NULL, all floats will be 0.0.
Basic calloc() Usage
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocate array of 5 integers, all initialized to 0
int *arr = (int*)calloc(5, sizeof(int));
if (arr == NULL) {
printf("Allocation failed!\n");
return 1;
}
// All values are already 0!
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]); // 0 0 0 0 0
}
free(arr);
return 0;
}
Unlike malloc(), calloc() automatically sets all bytes to zero. This is safer because you know exactly what values you are starting with.
malloc() vs calloc()
Both functions allocate memory, but they differ in syntax and initialization behavior. Here is a direct comparison:
// These allocate the same amount of memory:
int *arr1 = (int*)malloc(10 * sizeof(int)); // Uninitialized (garbage)
int *arr2 = (int*)calloc(10, sizeof(int)); // Zero-initialized
// malloc equivalent with manual zeroing:
int *arr3 = (int*)malloc(10 * sizeof(int));
if (arr3 != NULL) {
memset(arr3, 0, 10 * sizeof(int)); // Now zero-initialized
}
If you need zero-initialized memory, calloc() is cleaner than malloc() followed by memset(). However, if you are going to overwrite all values anyway, malloc() is slightly faster.
| Feature | malloc() | calloc() |
|---|---|---|
| Syntax | malloc(size) |
calloc(count, size) |
| Initialization | None (garbage values) | Zero-initialized |
| Speed | Slightly faster | Slightly slower (zeroing) |
| Best for | When you will initialize all values | When you need zeros or want safety |
Practice Questions
Task: Use calloc() to create an array of 100 doubles. Verify that the first, middle, and last elements are all 0.0 by printing them.
Show Solution
#include <stdio.h>
#include <stdlib.h>
int main() {
double *arr = (double*)calloc(100, sizeof(double));
if (arr == NULL) {
printf("Allocation failed!\n");
return 1;
}
printf("First: %f\n", arr[0]); // 0.000000
printf("Middle: %f\n", arr[49]); // 0.000000
printf("Last: %f\n", arr[99]); // 0.000000
free(arr);
return 0;
}
Task: Write a program that counts the frequency of each digit (0-9) in a given string. Use calloc() for the frequency array.
Show Solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char str[] = "a1b2c3d4e5f1g2h3";
// 10 counters for digits 0-9, all start at 0
int *freq = (int*)calloc(10, sizeof(int));
if (freq == NULL) return 1;
for (int i = 0; str[i] != '\0'; i++) {
if (str[i] >= '0' && str[i] <= '9') {
freq[str[i] - '0']++;
}
}
printf("Digit frequencies:\n");
for (int i = 0; i < 10; i++) {
if (freq[i] > 0) {
printf("%d: %d times\n", i, freq[i]);
}
}
free(freq);
return 0;
}
Task: Allocate 5 integers with malloc() and 5 with calloc(). Print all values without initializing them. What do you observe?
Show Solution
#include <stdio.h>
#include <stdlib.h>
int main() {
int *m = (int*)malloc(5 * sizeof(int));
int *c = (int*)calloc(5, sizeof(int));
if (m == NULL || c == NULL) return 1;
printf("malloc (uninitialized):\n");
for (int i = 0; i < 5; i++) {
printf(" [%d] = %d\n", i, m[i]);
}
printf("calloc (zero-initialized):\n");
for (int i = 0; i < 5; i++) {
printf(" [%d] = %d\n", i, c[i]);
}
free(m);
free(c);
return 0;
}
// malloc shows garbage values
// calloc always shows 0
Task: Create a 5x5 matrix of integers using calloc() to represent a visited grid (all false/0 initially). Mark positions (0,0), (2,2), and (4,4) as visited (1), then print the grid.
Show Solution
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 5, cols = 5;
// Allocate 2D array using calloc (all zeros)
int **grid = (int**)calloc(rows, sizeof(int*));
if (grid == NULL) return 1;
for (int i = 0; i < rows; i++) {
grid[i] = (int*)calloc(cols, sizeof(int));
if (grid[i] == NULL) return 1;
}
// Mark diagonal as visited
grid[0][0] = 1;
grid[2][2] = 1;
grid[4][4] = 1;
// Print grid
printf("Visited grid:\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", grid[i][j]);
}
printf("\n");
}
// Free memory
for (int i = 0; i < rows; i++) {
free(grid[i]);
}
free(grid);
return 0;
}
realloc() - Resizing Memory
What if you allocated an array but later need it to be bigger (or smaller)? The
realloc() function lets you resize previously allocated memory. It can
expand or shrink a memory block while preserving the existing data.
realloc()
void* realloc(void* ptr, size_t new_size) - Changes the size of the memory
block pointed to by ptr to new_size bytes. Existing data is preserved
(up to the smaller of old and new sizes).
Important: realloc() may move the data to a new location! Always use the returned pointer, not the old one.
Growing an Array
One of the most common uses of realloc() is growing an array when you need more
space. Unlike static arrays whose size is fixed at compile time, dynamically allocated arrays
can expand as your data grows. This is essential for building dynamic data structures like
resizable lists, buffers that grow with input, and any application where you do not know the
final size in advance.
Growing an Array with realloc()
Growing an array means increasing its capacity at runtime by requesting more memory.
The realloc() function handles this by:
- Preserving existing data: All elements in the original array remain intact
- Extending in place: If possible, memory is extended at the current location
- Relocating if needed: If no contiguous space exists, data is copied to a new location
- Returning new pointer: Always use the returned pointer as the address may change
Key Point: The new space added by realloc() is uninitialized and contains garbage values. You must initialize new elements before reading them.
#include <stdio.h>
#include <stdlib.h>
int main() {
// Start with 3 integers
int *arr = (int*)malloc(3 * sizeof(int));
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
First, we allocate space for 3 integers using malloc(). We then initialize each element with values 10, 20, and 30. This is our starting array that we will grow later.
printf("Original: ");
for (int i = 0; i < 3; i++) printf("%d ", arr[i]);
printf("\n");
We print the original array contents to verify our initial data. Output: "Original: 10 20 30"
// Grow to 5 integers
int *temp = (int*)realloc(arr, 5 * sizeof(int));
if (temp == NULL) {
free(arr); // Original still valid if realloc fails
return 1;
}
arr = temp; // Use the new pointer
This is the key part: we use realloc() to grow from 3 to 5 integers. We store the result in a temporary pointer first. If realloc() fails (returns NULL), we free the original memory and exit. If successful, we update arr to point to the new (possibly relocated) memory block.
// Add new elements
arr[3] = 40;
arr[4] = 50;
Now we have 5 slots available. The first 3 (indices 0-2) still contain our original data (10, 20, 30). We initialize the new slots (indices 3 and 4) with values 40 and 50.
printf("After realloc: ");
for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
printf("\n"); // 10 20 30 40 50
free(arr);
return 0;
}
We print the expanded array to confirm all 5 values. Output: "After realloc: 10 20 30 40 50". Finally, we free the memory before exiting. Note that we only call free() once on the final pointer, not on the original arr.
Safe realloc() Pattern
There is a common pitfall with realloc(): if you write ptr = realloc(ptr, newSize)
and realloc() fails (returns NULL), you lose the original pointer and create a memory leak!
Always use a temporary variable.
// DANGEROUS - memory leak if realloc fails!
arr = (int*)realloc(arr, newSize); // If NULL, original arr is lost!
// SAFE - use a temporary pointer
int *temp = (int*)realloc(arr, newSize);
if (temp == NULL) {
// Handle error - arr is still valid!
printf("Realloc failed, keeping original\n");
} else {
arr = temp; // Success - update the pointer
}
Always store the result of realloc() in a temporary variable first. This way, if allocation fails, you still have access to the original memory block.
Special Cases
// If ptr is NULL, realloc behaves like malloc
int *arr = (int*)realloc(NULL, 5 * sizeof(int)); // Same as malloc
// If new_size is 0, behavior is implementation-defined
// Some systems free the memory, some return NULL
// Better to explicitly call free() instead
realloc(NULL, size) works like malloc(size), which can be useful for writing flexible allocation code. However, avoid realloc(ptr, 0) as the behavior varies between systems.
Practice Questions
Task: Write a program that reads integers from the user until they enter -1. Store all numbers in a dynamically growing array (start with capacity 2, double it when full).
Show Solution
#include <stdio.h>
#include <stdlib.h>
int main() {
int capacity = 2;
int count = 0;
int *arr = (int*)malloc(capacity * sizeof(int));
if (arr == NULL) return 1;
int num;
printf("Enter numbers (-1 to stop):\n");
while (1) {
scanf("%d", &num);
if (num == -1) break;
// Check if we need more space
if (count >= capacity) {
capacity *= 2;
int *temp = (int*)realloc(arr, capacity * sizeof(int));
if (temp == NULL) {
free(arr);
return 1;
}
arr = temp;
printf("(Grew to capacity %d)\n", capacity);
}
arr[count++] = num;
}
printf("You entered: ");
for (int i = 0; i < count; i++) {
printf("%d ", arr[i]);
}
printf("\n");
free(arr);
return 0;
}
Task: Implement a function appendString(char **dest, const char *src) that appends src to dest, growing the memory as needed.
Show Solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int appendString(char **dest, const char *src) {
size_t destLen = *dest ? strlen(*dest) : 0;
size_t srcLen = strlen(src);
char *temp = (char*)realloc(*dest, destLen + srcLen + 1);
if (temp == NULL) return 0;
*dest = temp;
strcpy(*dest + destLen, src);
return 1;
}
int main() {
char *str = NULL;
appendString(&str, "Hello");
appendString(&str, " ");
appendString(&str, "World!");
printf("%s\n", str); // Hello World!
free(str);
return 0;
}
Task: Allocate an array of 10 integers, fill it with values 1-10, then use realloc() to shrink it to only 5 elements. Print the remaining elements.
Show Solution
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) return 1;
// Fill with 1-10
for (int i = 0; i < 10; i++) {
arr[i] = i + 1;
}
printf("Before shrink: ");
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// Shrink to 5 elements
int *temp = (int*)realloc(arr, 5 * sizeof(int));
if (temp == NULL) {
free(arr);
return 1;
}
arr = temp;
printf("After shrink: ");
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
printf("\n"); // Output: 1 2 3 4 5
free(arr);
return 0;
}
Task: Write a program that uses realloc() with a NULL pointer to allocate initial memory, then grows it twice. Demonstrate that realloc(NULL, size) works like malloc(size).
Show Solution
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr = NULL; // Start with NULL
int capacity = 0;
// First allocation using realloc
capacity = 3;
arr = (int*)realloc(arr, capacity * sizeof(int));
printf("Allocated %d elements\n", capacity);
arr[0] = 10; arr[1] = 20; arr[2] = 30;
// Grow to 5
capacity = 5;
int *temp = (int*)realloc(arr, capacity * sizeof(int));
if (temp) arr = temp;
printf("Grew to %d elements\n", capacity);
arr[3] = 40; arr[4] = 50;
// Grow to 8
capacity = 8;
temp = (int*)realloc(arr, capacity * sizeof(int));
if (temp) arr = temp;
printf("Grew to %d elements\n", capacity);
printf("Values: ");
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
printf("\n");
free(arr);
return 0;
}
free() and Memory Leaks
Every byte you allocate with malloc(), calloc(), or realloc() must eventually be returned
using free(). Failing to do so creates memory leaks - memory that is allocated
but never released, slowly eating up your system's resources.
free()
void free(void* ptr) - Releases the memory block pointed to by ptr,
which must have been returned by a previous call to malloc(), calloc(), or realloc().
After free(): The pointer becomes invalid. Using it (dangling pointer) causes undefined behavior. Set it to NULL after freeing to avoid accidental use.
Basic free() Usage
Calling free() tells the system that you are done with a block of memory and it
can be reused. After freeing, the pointer still holds the old address but that memory is no
longer valid for your program to access.
Basic free() Usage Pattern
The standard pattern for using free() involves three steps:
- Use the memory: Perform all operations with the allocated memory first
- Call free(ptr): Release the memory back to the system when done
- Set ptr = NULL: Nullify the pointer to prevent accidental reuse
Important: Only call free() on pointers returned by malloc(), calloc(), or realloc(). Never free stack variables, string literals, or already-freed memory.
#include <stdlib.h>
int main() {
int *arr = (int*)malloc(100 * sizeof(int));
if (arr == NULL) return 1;
// Use the memory...
arr[0] = 42;
// Release the memory when done
free(arr);
// Good practice: set to NULL after freeing
arr = NULL;
return 0;
}
After calling free(), set the pointer to NULL. This prevents accidental use of the freed memory and makes it easier to check if memory is allocated.
What is a Memory Leak?
A memory leak occurs when you allocate memory but lose all references to it without freeing it. The memory remains allocated until your program ends, but you can not use it or free it.
void memoryLeak() {
int *ptr = (int*)malloc(1000 * sizeof(int));
// Oops! Function returns without free()
// The 4000 bytes are leaked!
}
void anotherLeak() {
int *ptr = (int*)malloc(100 * sizeof(int));
ptr = (int*)malloc(200 * sizeof(int)); // Lost first allocation!
free(ptr); // Only frees the second block
}
In the first function, we forget to call free(). In the second, we overwrite the pointer before freeing, losing access to the first block forever.
Common Mistakes with free()
// 1. Double free - freeing the same memory twice
int *ptr = (int*)malloc(sizeof(int));
free(ptr);
free(ptr); // CRASH or corruption!
// 2. Freeing stack memory - only free heap memory!
int stackVar = 10;
free(&stackVar); // WRONG - stackVar is on stack, not heap!
// 3. Using after free (dangling pointer)
int *arr = (int*)malloc(5 * sizeof(int));
free(arr);
arr[0] = 42; // UNDEFINED BEHAVIOR - memory already freed!
All three mistakes cause undefined behavior. Double free can corrupt memory management structures. Freeing stack memory is invalid. Using freed memory can crash or produce wrong results.
Practice Questions
Task: Find and fix the memory leak in this code:
char* createMessage() {
char *msg = (char*)malloc(50);
strcpy(msg, "Hello!");
return msg;
}
int main() {
for (int i = 0; i < 1000; i++) {
char *m = createMessage();
printf("%s\n", m);
}
return 0;
}
Show Solution
int main() {
for (int i = 0; i < 1000; i++) {
char *m = createMessage();
printf("%s\n", m);
free(m); // Add this line to prevent leak!
}
return 0;
}
Each call to createMessage() allocates new memory. Without free(), 1000 allocations are leaked!
Task: Write a safe safeFree() function that frees memory and sets the pointer to NULL to prevent double-free and dangling pointer issues.
Show Solution
#include <stdio.h>
#include <stdlib.h>
void safeFree(void **ptr) {
if (ptr != NULL && *ptr != NULL) {
free(*ptr);
*ptr = NULL;
}
}
int main() {
int *arr = (int*)malloc(10 * sizeof(int));
printf("Before: %p\n", (void*)arr);
safeFree((void**)&arr);
printf("After: %p\n", (void*)arr); // (nil) or 0x0
// Safe to call again - does nothing
safeFree((void**)&arr);
return 0;
}
Task: The following code has a double-free bug. Identify it and fix it properly.
void processData(int *data) {
printf("Processing: %d\n", data[0]);
free(data);
}
int main() {
int *arr = (int*)malloc(5 * sizeof(int));
arr[0] = 42;
processData(arr);
free(arr); // Bug here!
return 0;
}
Show Solution
// Option 1: Don't free in the function
void processData(int *data) {
printf("Processing: %d\n", data[0]);
// Let caller handle memory
}
int main() {
int *arr = (int*)malloc(5 * sizeof(int));
arr[0] = 42;
processData(arr);
free(arr); // Only freed once here
arr = NULL;
return 0;
}
// Option 2: Set to NULL after free in function
// (requires passing pointer to pointer)
The bug is that arr is freed twice: once in processData() and again in main(). Either remove free from the function or from main, but not both.
Task: This code uses a dangling pointer. Find and fix the bug.
int main() {
int *ptr = (int*)malloc(sizeof(int));
*ptr = 100;
free(ptr);
printf("Value: %d\n", *ptr); // Bug!
return 0;
}
Show Solution
int main() {
int *ptr = (int*)malloc(sizeof(int));
*ptr = 100;
// Print BEFORE freeing
printf("Value: %d\n", *ptr);
free(ptr);
ptr = NULL; // Prevent accidental use
// Now accessing ptr would crash (NULL dereference)
// which is better than silent corruption
return 0;
}
Never access memory after calling free(). The pointer becomes a dangling pointer pointing to invalid memory. Always set pointers to NULL after freeing.
Best Practices
Dynamic memory management in C requires discipline. Following established best practices helps you avoid bugs, memory leaks, and undefined behavior. Here are the essential rules every C programmer should follow.
The Golden Rules
Always Check for NULL
Every allocation can fail. Always check the return value before using the memory.
int *p = (int*)malloc(size);
if (p == NULL) {
// Handle error
return -1;
}
Always Use sizeof()
Never hardcode sizes. Use sizeof() for portability across different systems.
// Good
int *arr = malloc(n * sizeof(int));
// Bad - assumes int is 4 bytes
int *arr = malloc(n * 4);
Free in Reverse Order
When allocating multiple blocks, free them in the reverse order of allocation.
// Allocate
int *a = malloc(100);
int *b = malloc(200);
// Free in reverse
free(b);
free(a);
Set Pointers to NULL
After freeing, set the pointer to NULL to prevent dangling pointer bugs.
free(ptr);
ptr = NULL; // Prevents use-after-free
Common Pitfalls Summary
| Pitfall | Description | Prevention |
|---|---|---|
| Memory Leak | Allocated memory never freed | Always pair malloc with free |
| Double Free | Calling free() twice on same pointer | Set pointer to NULL after free |
| Dangling Pointer | Using pointer after free() | Set pointer to NULL after free |
| Buffer Overflow | Writing beyond allocated size | Track size, check bounds |
| NULL Dereference | Using pointer without checking | Always check malloc return |
Debugging Tools
Several tools can help you find memory problems:
Valgrind
Linux tool that detects memory leaks, invalid reads/writes, and more.
valgrind ./myprogram
AddressSanitizer
Compiler feature (GCC/Clang) for detecting memory errors.
gcc -fsanitize=address
Dr. Memory
Windows alternative to Valgrind for memory debugging.
drmemory -- myprogram.exe
Practice Questions
Task: This code has multiple memory-related bugs. Find and fix them all.
int* createArray(int size) {
int *arr = malloc(size * 4);
for (int i = 0; i <= size; i++) {
arr[i] = i;
}
return arr;
}
int main() {
int *data = createArray(10);
printf("%d\n", data[5]);
int *data2 = createArray(5);
data = data2;
free(data);
free(data2);
return 0;
}
Show Solution
int* createArray(int size) {
// Bug 1: Use sizeof(int) not hardcoded 4
int *arr = malloc(size * sizeof(int));
// Bug 2: Check for NULL
if (arr == NULL) return NULL;
// Bug 3: i < size, not i <= size (buffer overflow)
for (int i = 0; i < size; i++) {
arr[i] = i;
}
return arr;
}
int main() {
int *data = createArray(10);
if (data == NULL) return 1; // Check for NULL
printf("%d\n", data[5]);
int *data2 = createArray(5);
if (data2 == NULL) {
free(data);
return 1;
}
// Bug 4: Memory leak - free data before reassigning
free(data);
data = data2;
// Bug 5: Double free - data and data2 point to same memory
free(data);
// free(data2); // Remove this - already freed!
data = NULL; // Good practice
return 0;
}
Task: Write functions to create and free a dynamic 2D array with proper error handling.
Show Solution
#include <stdio.h>
#include <stdlib.h>
int** create2DArray(int rows, int cols) {
int **arr = (int**)malloc(rows * sizeof(int*));
if (arr == NULL) return NULL;
for (int i = 0; i < rows; i++) {
arr[i] = (int*)calloc(cols, sizeof(int));
if (arr[i] == NULL) {
// Cleanup already allocated rows
for (int j = 0; j < i; j++) {
free(arr[j]);
}
free(arr);
return NULL;
}
}
return arr;
}
void free2DArray(int **arr, int rows) {
if (arr == NULL) return;
for (int i = 0; i < rows; i++) {
free(arr[i]);
}
free(arr);
}
int main() {
int rows = 3, cols = 4;
int **matrix = create2DArray(rows, cols);
if (matrix == NULL) {
printf("Allocation failed!\n");
return 1;
}
// Use the 2D array
matrix[1][2] = 42;
printf("matrix[1][2] = %d\n", matrix[1][2]);
free2DArray(matrix, rows);
return 0;
}
Task: What is wrong with this code? Identify the issue and explain why it is a problem.
int* getNumbers() {
int arr[5] = {1, 2, 3, 4, 5};
return arr;
}
Show Solution
// Problem: Returning pointer to local stack variable!
// arr is destroyed when function returns.
// Fixed version using heap allocation:
int* getNumbers() {
int *arr = (int*)malloc(5 * sizeof(int));
if (arr == NULL) return NULL;
arr[0] = 1; arr[1] = 2; arr[2] = 3;
arr[3] = 4; arr[4] = 5;
return arr; // Caller must free this!
}
The original code returns a pointer to stack memory that is deallocated when the function returns. This is a dangling pointer bug. The fix uses heap allocation so the memory persists.
Task: Write a cleanup function for a struct that contains dynamically allocated members. The struct has a name (char*), an array of scores (int*), and the number of scores.
Show Solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *name;
int *scores;
int numScores;
} Student;
Student* createStudent(const char *name, int numScores) {
Student *s = (Student*)malloc(sizeof(Student));
if (s == NULL) return NULL;
s->name = (char*)malloc(strlen(name) + 1);
if (s->name == NULL) {
free(s);
return NULL;
}
strcpy(s->name, name);
s->scores = (int*)calloc(numScores, sizeof(int));
if (s->scores == NULL) {
free(s->name);
free(s);
return NULL;
}
s->numScores = numScores;
return s;
}
void freeStudent(Student **s) {
if (s == NULL || *s == NULL) return;
free((*s)->name); // Free name first
free((*s)->scores); // Free scores
free(*s); // Free struct itself
*s = NULL; // Prevent dangling pointer
}
int main() {
Student *s = createStudent("Alice", 3);
if (s) {
s->scores[0] = 90;
s->scores[1] = 85;
s->scores[2] = 88;
printf("%s\n", s->name);
freeStudent(&s);
}
return 0;
}
Key Takeaways
Stack vs Heap
Stack is automatic and fast but limited. Heap is manual but flexible and large. Use heap for dynamic or large data.
malloc() Basics
malloc(size) allocates uninitialized bytes. Always check for NULL. Always use sizeof() for portability.
calloc() for Arrays
calloc(count, size) allocates zero-initialized memory. Safer than malloc() when you need a clean slate.
realloc() for Resizing
realloc(ptr, newSize) grows or shrinks memory. Use a temp pointer to avoid leaks if it fails.
free() Every Allocation
Every malloc/calloc/realloc needs a matching free(). Set pointers to NULL after freeing.
Avoid Common Pitfalls
Watch for memory leaks, double free, dangling pointers, and buffer overflows. Use debugging tools.
Knowledge Check
Quick Quiz
Test your understanding of dynamic memory allocation in C