Dynamic Memory
So far, all our variables were created when we wrote the code. We said “I need 5 integers” and that was that.
But what if you don’t know how much space you need until the program is running?
What if you’re writing a program that stores names, but you don’t know how many names until the user tells you?
C lets you ask for memory while the program runs.
Asking for Memory: malloc
The malloc function (short for “memory allocate”) asks the computer for space:
#include <stdio.h>
#include <stdlib.h> // For malloc and free
int main(void) {
int n;
printf("How many numbers? ");
scanf("%d", &n);
// Ask for space for n integers
int *numbers = malloc(n * sizeof(int));
// Check if it worked
if (numbers == NULL) {
printf("Couldn't get memory!\n");
return 1;
}
// Use it just like an array
for (int i = 0; i < n; i++) {
numbers[i] = i * 10;
}
// Print them
for (int i = 0; i < n; i++) {
printf("%d ", numbers[i]);
}
printf("\n");
// IMPORTANT: Give the memory back when done
free(numbers);
return 0;
}Let’s break this down:
malloc(n * sizeof(int))asks for enough bytes to hold n integerssizeof(int)tells us how many bytes one integer needs (usually 4)mallocreturns a pointer to the memory it found- If there’s no memory available,
mallocreturnsNULL
The Rules of Dynamic Memory
This is where C trusts you to be responsible:
- Always check if malloc worked - It returns NULL if there’s no memory available
- Always free what you malloc - When you’re done with the memory, give it back with
free() - Never use freed memory - Once you call
free(), that memory is gone - Never free the same memory twice - This will crash or corrupt your program
Other languages have “garbage collectors” that automatically clean up memory you’re not using. They exist because managing memory yourself is easy to mess up.
But when you learn to manage memory yourself, you understand something fundamental. And your programs don’t randomly pause while a garbage collector runs.
Memory Leaks
If you forget to free memory, your program keeps using more and more:
void leak_memory(void) {
int *data = malloc(1000 * sizeof(int));
// Do stuff with data...
// Oops! Forgot to free(data)
// That memory is now lost until the program ends
}This is called a memory leak. Call this function 1000 times and you’ve leaked 4 million bytes.
Memory leaks won’t crash your program immediately. It’ll just slowly use more memory until eventually your computer runs out.
Pointers to Pointers
Yes, you can have a pointer to a pointer. Follow the logic one step at a time:
int x = 42;
int *p = &x; // p holds the address of x
int **pp = &p; // pp holds the address of p
printf("x = %d\n", x); // 42
printf("*p = %d\n", *p); // 42 (follow one star)
printf("**pp = %d\n", **pp); // 42 (follow two stars)Think of it as following the stars:
*pp- follow one star, get to p**pp- follow two stars, get to x
This is useful when a function needs to change a pointer itself (not just what it points to). You’ll see it in more advanced C programming.
Strings Are Pointers Too
Remember how strings are arrays of characters? Since arrays and pointers are related, you can work with strings using pointers:
char greeting[] = "Hello";
// These do the same thing:
printf("%c\n", greeting[0]); // H (array notation)
printf("%c\n", *greeting); // H (pointer notation)
// Walk through a string with a pointer
for (char *p = greeting; *p != '\0'; p++) {
printf("%c ", *p); // H e l l o
}
printf("\n");Complete Example: Reverse a String
Here’s a function that reverses a string using pointers:
#include <stdio.h>
#include <string.h>
void reverse(char *str) {
int len = strlen(str);
char *start = str; // Points to first character
char *end = str + len - 1; // Points to last character
while (start < end) {
// Swap the characters
char temp = *start;
*start = *end;
*end = temp;
// Move toward the center
start++;
end--;
}
}
int main(void) {
char word[] = "Hello";
printf("Before: %s\n", word);
reverse(word);
printf("After: %s\n", word);
return 0;
}Output:
Before: Hello
After: olleHMemory Layout
When your program runs, memory is organized into different areas. From top to bottom: the Stack (where local variables and function calls live), free space in the middle, the Heap (where malloc’d memory goes), the Data/BSS section (global and static variables), and the Code section (your program’s instructions).
The important parts:
The Stack
This is where local variables live. When you call a function, the computer creates a stack frame - a chunk of memory for that function’s variables.
Think of a stack frame like a sticky note for each function call. The note holds that function’s local variables. When you call another function, a new note goes on top. When a function returns, its note is removed.
This is why local variables disappear when a function ends - their sticky note is gone.
The direction the stack grows (up or down in memory) depends on your computer’s architecture. Most desktop computers grow it downward, but it varies.
The Heap
This is where malloc gets memory from. Unlike the stack, you control when this memory comes and goes. It stays until you call free().
Why You Can’t Return Local Variables
This is why you can’t return a pointer to a local variable:
int *bad_function(void) {
int x = 42;
return &x; // BAD! x disappears when this function ends
}When bad_function returns, its stack frame is gone. The address you returned now points to garbage.
But returning malloc’d memory is fine:
int *good_function(void) {
int *p = malloc(sizeof(int));
*p = 42;
return p; // OK - heap memory stays until you free it
}The heap memory stays until you explicitly free it.
Dynamic Arrays
You can create arrays of any size at runtime:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int size;
printf("How many grades? ");
scanf("%d", &size);
// Allocate array
int *grades = malloc(size * sizeof(int));
if (grades == NULL) {
printf("Out of memory!\n");
return 1;
}
// Read grades
for (int i = 0; i < size; i++) {
printf("Grade %d: ", i + 1);
scanf("%d", &grades[i]);
}
// Calculate average
int total = 0;
for (int i = 0; i < size; i++) {
total += grades[i];
}
double average = (double)total / size;
printf("Average: %.1f\n", average);
// Clean up
free(grades);
return 0;
}The Mental Model
Here’s what to remember:
- Stack: Automatic. Variables created and destroyed for you. Limited in size.
- Heap: Manual. You ask for memory with
malloc, give it back withfree. Much larger.
Most programs use both. Local variables go on the stack (fast, automatic). Data that needs to outlive a function, or that’s very large, goes on the heap.
Try It Yourself
- Write a program that uses malloc to create an array of 5 numbers, fills them with user input, prints them backwards, and frees the memory
- Create a program that asks how many names to store, allocates space for them, reads them, and prints them
- What happens if you try to use memory after freeing it? (Be careful - this might crash!)
Common Mistakes
- Forgetting to free: Memory leak - your program slowly uses more and more memory
- Using freed memory: Unpredictable behavior - might crash, might corrupt data
- Freeing twice: Crash or corruption
- Forgetting NULL check: Your program crashes if malloc fails
If You Made It This Far
Congratulations. You now understand something that intimidates most programmers their entire careers.
When someone talks about “memory safety” or “pointer bugs,” you know exactly what they mean. When a job posting says “must understand low-level systems,” they’re describing you now.
Next Up
In Part 12, we’ll learn about enums - how to give meaningful names to numbers and make your code easier to read.
Enjoyed This?
If this helped something click, subscribe to my YouTube channel. More content like this, same approach - making things stick without insulting your intelligence. It’s free, it helps more people find this stuff, and it tells me what’s worth making more of.