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 integers
  • sizeof(int) tells us how many bytes one integer needs (usually 4)
  • malloc returns a pointer to the memory it found
  • If there’s no memory available, malloc returns NULL

The Rules of Dynamic Memory

This is where C trusts you to be responsible:

  1. Always check if malloc worked - It returns NULL if there’s no memory available
  2. Always free what you malloc - When you’re done with the memory, give it back with free()
  3. Never use freed memory - Once you call free(), that memory is gone
  4. 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: olleH

Memory 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).

A vertical diagram showing memory layout from top to bottom: Stack at the top for local variables and function calls, free space in the middle, Heap below that for dynamically allocated memory, Data/BSS section for global and static variables, and Code section at the bottom for program 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 with free. 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

  1. 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
  2. Create a program that asks how many names to store, allocates space for them, reads them, and prints them
  3. 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.