Dynamic memory in C — theory with malloc, calloc, realloc and free, a class’s grades array that grows (and the Segmentation fault that taught me where everything lives)

First month of IC2, the Thursday lab. I had to store a class’s grades and thought: “I’ll write a function create_grades() that builds the array and hands it back to me, like in Python”. I wrote it in two minutes, compiled, ran it… and got this:

#include <stdio.h>

/* I wanted a function that "built" the array of grades and handed it back to me */
double *create_grades(void) {
    double grades[3] = {7.5, 4.0, 9.0};  /* LOCAL array: it lives on the stack of create_grades */
    return grades;                       /* I return its address... but on exit, it stops existing */
}

int main(void) {
    double *v = create_grades();
    printf("Array created. First grade:\n");
    printf("%.1f\n", v[0]);             /* I try to read an array that no longer exists */
    return 0;
}

When compiling with gcc -Wall -std=c11, the compiler was already warning me (and of course I didn’t read it):

hook.c: In function 'create_grades':
hook.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr]

And when running it:

Array created. First grade:
Segmentation fault (core dumped)

The program prints the first line and dies. Why? Because grades is a local array: it lives inside create_grades and disappears as soon as the function hits return. I was handing main the address of something that no longer existed. GCC, when it spots this, even swaps that address for NULL, and reading v[0] means reading address 0: Segmentation fault.

Today you’ll learn to request memory that survives functions and whose size you decide while the program is running: dynamic memory. We’ll use malloc, calloc, realloc and free with an example that stays with us through the three articles: a class’s grades, when you don’t know how many students there are until the last minute. If addresses and the * aren’t fresh in your mind, go over pointers in C and functions in C first. When you’re done, the dynamic memory practice is waiting with three complete programs.

Two places to store data: the stack and the heap

When you run a C program, its variables can live in two very different memory areas.

The stack is where local variables and parameters live. Every time you call a function, a “frame” with its variables is pushed, and when the function ends, the whole frame is removed. It’s fast and automatic: you don’t have to do anything to free it. In exchange, it has two limitations:

  • The size of what you store is fixed at compile time: double grades[30] is 30 slots, full stop.
  • Whatever is in a function’s frame dies with it.

The heap (or free store) is a large area the program can request at run time. You ask for the bytes you need, when you need them, and they stay there until you say you don’t want them anymore. That’s dynamic memory.

The stack and the heapOn the left, the stack: the frame of create_grades with its local array, which disappears on return, and the frame of main with the pointer grades. On the right, the heap: a block of 6 doubles reserved with malloc, pointed to by grades.STACKHEAP (dynamic memory)create_grades()7.54.09.0destroyed on returnmain()double *gradesmalloc(6 * sizeof(double))7.54.09.06.53.58.001234548 bytes · lives until free(grades)Fixed size · freed automaticallySize known at run time · you free it
Local data dies with its function; whatever you request with malloc lives on the heap until you call free.

Look at the drawing: on the left, the frame of create_grades with its local array, which is wiped on return (that’s what broke the hook). Below it, the frame of main with a pointer. On the right, the heap block: main‘s pointer points to it, and that block doesn’t depend on any function.

C has no garbage collector like Python or Java. If you request heap memory, you are responsible for giving it back. That’s the price of being in control.

When do I need dynamic memory?

SituationStack or heap?Why
I know the size when writing the code (int days[7])StackIt’s fixed, the stack is faster and frees itself
The size comes from the user or a file at run timeHeapThe stack needs the size at compile time
A function has to return an arrayHeapLocal data dies with the function
The array has to grow or shrinkHeapOnly realloc changes a block’s size
Lots of data (millions of slots)HeapThe stack is small (a few MB)

sizeof: malloc counts bytes, not slots

Before requesting memory you need to know how much to ask for, and here’s trap number one of this topic: the dynamic memory functions work in bytes. If you want 6 double grades, you don’t ask for 6: you ask for 6 × the size of a double. That’s what sizeof is for.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    double fixed[5] = {7.5, 4.0, 9.0, 6.5, 8.0};  /* static: 5 slots forever */
    double *dyn = malloc(5 * sizeof(double));      /* dynamic: 5 slots requested from the heap */
    if (dyn == NULL) return 1;                     /* no memory: we stop */

    printf("sizeof(double) = %zu bytes\n", sizeof(double));
    printf("sizeof(fixed)  = %zu bytes (5 x 8)\n", sizeof(fixed));
    printf("sizeof(dyn)    = %zu bytes (it is a pointer)\n", sizeof(dyn));
    printf("bytes reserved = %zu\n", 5 * sizeof(double));
    printf("fixed slots    = %zu\n", sizeof(fixed) / sizeof(fixed[0]));

    free(dyn);   /* what you request with malloc you return with free */
    return 0;
}

Output:

sizeof(double) = 8 bytes
sizeof(fixed)  = 40 bytes (5 x 8)
sizeof(dyn)    = 8 bytes (it is a pointer)
bytes reserved = 40
fixed slots    = 5

Line by line:

  • sizeof(double) is 8: each double takes 8 bytes on a modern computer.
  • sizeof(fixed) is 40: it’s a real array, so sizeof measures the whole array, 5 × 8 = 40.
  • sizeof(dyn) is 8, not 40. dyn isn’t an array, it’s a pointer, and a pointer takes 8 bytes no matter what it points to. sizeof has no idea how many bytes sit at the other end of the arrow.
  • 5 * sizeof(double) = 5 × 8 = 40: the bytes we actually requested.
  • sizeof(fixed) / sizeof(fixed[0]) = 40 / 8 = 5: the classic trick to count slots… which only works with arrays, never with pointers.
malloc counts bytes, not slotsA ruler of 48 bytes. On top, malloc(6) reserves only the first 6 bytes. Below, malloc(6 * sizeof(double)) reserves all 48 bytes, grouped into 6 slots of 8 bytes, from grades[0] to grades[5].malloc(6)6 bytes: not even one double fitsmalloc(6 * sizeof(double))48 bytes = 6 slots of 8 bytesgrades[0]grades[1]grades[2]grades[3]grades[4]grades[5]1 slot = sizeof(double) = 8 bytes
malloc takes bytes: if you want 6 doubles, multiply by sizeof(double).

The drawing makes it clear: malloc(6) gives you 6 bytes, not even enough for one grade. malloc(6 * sizeof(double)) gives you 48 bytes, exactly 6 slots of 8.

Writing malloc(n) thinking “n grades”. The compiler doesn’t complain, the program sometimes seems to work… and you’re writing to memory that isn’t yours. We’ll see it live in the exercises.

Instead of repeating the type, write malloc(n * sizeof *grades). sizeof *grades means “the size of whatever grades points to”. If one day you change double to float, the line is still correct without touching it.

malloc: requesting memory at run time

malloc (from memory allocation) lives in <stdlib.h>. It takes a number of bytes and returns a pointer to the start of the reserved block, or NULL if there isn’t enough memory:

double *grades = malloc(n * sizeof(double));

What it returns is a void *, a “pointer to anything”. In C it converts automatically to the variable’s type, which is why the cast isn’t needed. In your notes you’ll see (int*) malloc(...): it’s the same, just longer (in C++ it is mandatory).

This program reserves room for a number of grades that “arrives” at run time:

#include <stdio.h>
#include <stdlib.h>   /* malloc, calloc, realloc and free live here */

int main(void) {
    int n = 6;   /* imagine the teacher typed it: we did not know it at compile time */
    const double data[] = {7.5, 4.0, 9.0, 6.5, 3.5, 8.0};

    /* n double slots = n * sizeof(double) bytes (not n bytes) */
    double *grades = malloc(n * sizeof(double));
    if (grades == NULL) {            /* malloc returns NULL if there is no memory */
        printf("No memory for %d grades\n", n);
        return 1;
    }

    double sum = 0;
    for (int i = 0; i < n; i++) {
        grades[i] = data[i];        /* used like a normal array: grades[i] */
        sum += grades[i];
    }
    printf("Reserved %zu bytes for %d grades\n", n * sizeof(double), n);
    printf("Average: %.2f\n", sum / n);

    free(grades);     /* we give the memory back */
    grades = NULL;    /* and we make clear it no longer points to anything valid */
    return 0;
}

Output:

Reserved 48 bytes for 6 grades
Average: 6.42

Between the lines:

  • **int n = 6;** stands in for “what the teacher types”. With scanf it would work the same way: the size is decided at run time, which is what matters.
  • **if (grades == NULL)** isn’t paranoia: if malloc can’t get memory it returns NULL, and using grades[i] on NULL is a guaranteed Segmentation fault. Always check.
  • **grades[i]**: once reserved, the block is used exactly like an array. Remember from pointers in C that grades[i] is the same as *(grades + i).
  • The bytes: 6 × 8 = 48, as the output says.
  • The average: 7.5 + 4.0 + 9.0 + 6.5 + 3.5 + 8.0 = 38.5, and 38.5 / 6 = 6.4166…, which %.2f prints as 6.42.
  • **free(grades); grades = NULL;** gives the block back and leaves the pointer “switched off”. We explain it properly below.

malloc does not zero what it reserves. Freshly requested slots contain whatever was in that memory before (garbage). If you read them before writing them, the result is unpredictable.

calloc: memory that starts at zero

calloc (from contiguous allocation) does the same as malloc with two differences:

  1. It takes two arguments: how many slots and how big each one is: calloc(11, sizeof(int)).
  2. It sets every byte to zero.

When does that matter? When the block is going to be a set of counters. This program builds a histogram of grades: one counter for each grade from 0 to 10.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    const int grades[] = {5, 7, 7, 4, 9, 5, 6, 7, 3, 5, 10, 7};
    int n = sizeof(grades) / sizeof(grades[0]);   /* 12 grades */

    /* 11 counters (grades 0..10) that MUST start at zero: a job for calloc */
    int *counts = calloc(11, sizeof(int));
    if (counts == NULL) return 1;

    for (int i = 0; i < n; i++)
        counts[grades[i]]++;        /* the grade is the index of its counter */

    for (int grade = 0; grade <= 10; grade++) {
        printf("%2d | ", grade);
        for (int k = 0; k < counts[grade]; k++)
            printf("*");             /* one star per student with that grade */
        printf(" (%d)\n", counts[grade]);
    }

    free(counts);
    return 0;
}

Output:

 0 |  (0)
 1 |  (0)
 2 |  (0)
 3 | * (1)
 4 | * (1)
 5 | *** (3)
 6 | * (1)
 7 | **** (4)
 8 |  (0)
 9 | * (1)
10 | * (1)

The key line is counts[grades[i]]++: the grade acts as the index. When a 7 arrives, one is added to counts[7]. If the counters didn’t start at zero, each ++ would add on top of garbage. Let’s check two rows by hand: the list has three 5s (positions 0, 5 and 9) and four 7s (positions 1, 2, 7 and 11). Total: 1 + 1 + 3 + 1 + 4 + 1 + 1 = 12 grades, the same as the array holds.

malloc(n * size)calloc(n, size)
ArgumentsTotal bytesSlots and size of each one
Initial contentGarbageAll zeros
SpeedSlightly fasterSlightly slower (it has to write zeros)
Use it for…Data you’ll fill in straight awayCounters, accumulators, tables that start empty

With malloc plus a loop writing zeros you’d get the same, but calloc does it in one line and nothing gets forgotten.

realloc: changing the size without losing data

Here’s the big advantage of dynamic memory: a block can grow or shrink. realloc(pointer, new_bytes) tries to extend the block where it is; if it can’t, it reserves a new one, copies the data and frees the old one. It returns the address of the good block (which may be different) or NULL if there’s no memory.

In our class, two students have arrived late:

#include <stdio.h>
#include <stdlib.h>

void show(const double *v, int n) {
    printf("[");
    for (int i = 0; i < n; i++)
        printf(i == 0 ? "%.1f" : ", %.1f", v[i]);
    printf("] (%d grades)\n", n);
}

int main(void) {
    int n = 4;
    double *grades = malloc(n * sizeof(double));
    if (grades == NULL) return 1;
    const double first[] = {7.5, 4.0, 9.0, 6.5};
    for (int i = 0; i < n; i++) grades[i] = first[i];
    show(grades, n);

    /* 2 students arrive late: we need 6 slots */
    int new_n = n + 2;
    double *tmp = realloc(grades, new_n * sizeof(double));  /* result in a TEMPORARY pointer */
    if (tmp == NULL) {              /* if it fails, grades is intact: we lose nothing */
        printf("Could not grow; keeping %d grades\n", n);
        free(grades);
        return 1;
    }
    grades = tmp;                    /* only on success do we update grades */
    grades[4] = 5.5;                 /* the new slots arrive uninitialised: */
    grades[5] = 8.0;                 /* we have to fill them in ourselves */
    n = new_n;
    show(grades, n);              /* the first 4 grades are still there */

    free(grades);
    return 0;
}

Output:

[7.5, 4.0, 9.0, 6.5] (4 grades)
[7.5, 4.0, 9.0, 6.5, 5.5, 8.0] (6 grades)

The first 4 grades are intact after growing: realloc kept them. Let’s zoom into the delicate part:

/* 2 students arrive late: we need 6 slots */
int new_n = n + 2;
double *tmp = realloc(grades, new_n * sizeof(double));  /* result in a TEMPORARY pointer */
if (tmp == NULL) {              /* if it fails, grades is intact: we lose nothing */
    printf("Could not grow; keeping %d grades\n", n);
    free(grades);
    return 1;
}
grades = tmp;                    /* only on success do we update grades */
grades[4] = 5.5;                 /* the new slots arrive uninitialised: */
grades[5] = 8.0;                 /* we have to fill them in ourselves */
n = new_n;
show(grades, n);              /* the first 4 grades are still there */

Why that tmp? Because realloc can fail. If you write grades = realloc(grades, ...) directly and it returns NULL, you’ve just overwritten your only pointer to the old block with NULL: you’ve lost the grades and, on top of that, the block stays reserved with nobody able to free it (a memory leak). With the temporary, if it fails, grades still points to the same old data.

realloc with a temporary pointerThree cases. If realloc works, grades and tmp point to the new 6-slot block with the 4 grades copied. If it fails with a temporary, tmp is NULL and grades still points to the 4-slot block. If it fails without a temporary, grades is NULL and the 4-slot block is left with nobody pointing to it.Good: realloc workstmp = realloc(grades, …)7.54.09.06.5??gradestmpdata safe, 6 slotsGood: realloc failstmp = realloc(grades, …)7.54.09.06.5gradestmp = NULLgrades is still validBad: no temporarygrades = realloc(grades, …)7.54.09.06.5nobody points to itgrades = NULLdata lost + leak
Store the result of realloc in tmp: if it is NULL, your original array is still intact.

Look at the three cases in the drawing. In the two on the left, whatever happens, you never lose the array. In the one on the right, a failed realloc leaves you with NULL and an orphaned block.

v = realloc(v, ...). It works 99.9 % of the time, and that’s exactly why it’s so hard to track down when it fails. Always use a temporary pointer.

The new slots added by realloc aren’t initialised (just like with malloc). That’s why the program fills grades[4] and grades[5] before showing them.

Two special cases worth knowing: realloc(NULL, bytes) behaves like malloc(bytes), and shrinking a block (going from 8 to 7 slots, for example) is also done with realloc. We’ll use it in the practice.

free: giving back what you requested

Every block you request with malloc, calloc or realloc is given back once with free(pointer). Now we can fix the hook: the function reserves on the heap and main, which is the one using the array, frees it.

#include <stdio.h>
#include <stdlib.h>

/* Now the array lives on the heap: it survives the return. The caller must free it. */
double *create_grades(int n, const double *data) {
    double *v = malloc(n * sizeof *v);   /* sizeof *v = size of what v points to */
    if (v == NULL) return NULL;          /* the failure is passed on to the caller */
    for (int i = 0; i < n; i++)
        v[i] = data[i];
    return v;                            /* we return an address that is still valid */
}

int main(void) {
    const double data[] = {7.5, 4.0, 9.0};
    double *v = create_grades(3, data);
    if (v == NULL) return 1;

    printf("Array created. First grade:\n");
    printf("%.1f\n", v[0]);

    free(v);       /* main requested it (through create_grades): main frees it */
    v = NULL;
    free(v);       /* free(NULL) does nothing: that is why you set NULL after freeing */
    printf("Freed; the second free(NULL) did nothing\n");
    return 0;
}

Output:

Array created. First grade:
7.5
Freed; the second free(NULL) did nothing

This time the first grade comes out fine, because the block is on the heap and doesn’t die with create_grades. Three important ideas:

  1. Whoever receives the block owns it. create_grades reserves it, but hands it over to main, and main has to free it. It’s a good habit to say so in a comment above the function.
  2. **free doesn’t change the pointer.** After free(v), v still holds the same address, but that memory isn’t yours anymore: it’s a dangling pointer. Using it is a bug.
  3. **That’s why you set v = NULL.** free(NULL) does nothing, so if someone frees that pointer again by mistake, nothing happens. And reading v[0] with v set to NULL fails immediately and clearly, instead of silently returning garbage.

Memory leaks and how to see them

What if you forget the free? The program doesn’t fail: that memory just stays reserved until the program ends. If it happens inside a loop or in a program that runs for hours, the memory piles up. It’s a memory leak. This program computes the average over a copy and never frees it:

#include <stdio.h>
#include <stdlib.h>

/* Computes the average of a copy of the grades... and forgets to free the copy */
double copy_average(const double *grades, int n) {
    double *copy = malloc(n * sizeof *copy);
    if (copy == NULL) return -1;
    double sum = 0;
    for (int i = 0; i < n; i++) {
        copy[i] = grades[i];
        sum += copy[i];
    }
    return sum / n;          /* MISSING free(copy): these bytes stay reserved */
}

int main(void) {
    const double grades[] = {7.5, 4.0, 9.0, 6.5, 3.5, 8.0};
    for (int day = 1; day <= 3; day++)   /* three queries = three lost copies */
        printf("Query %d: average %.2f\n", day, copy_average(grades, 6));
    return 0;
}

Normally you wouldn’t see anything odd. But if you compile with GCC’s memory error detector, AddressSanitizer (gcc -Wall -std=c11 -g -fsanitize=address leak.c -o leak), it shows you the leak when the program ends. This is the output (trimmed to the important lines):

Query 1: average 6.42
Query 2: average 6.42
Query 3: average 6.42
=================================================================
==831==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 144 byte(s) in 3 object(s) allocated from:
    #1 in copy_average leak.c:6
    #2 in main leak.c:19
SUMMARY: AddressSanitizer: 144 byte(s) leaked in 3 allocation(s).

Read it from the bottom up: 144 bytes have been lost in 3 blocks. 3 queries × 6 grades × 8 bytes = 144. And it tells you where they were reserved: in copy_average, line 6 of leak.c, called from main on line 19. The fix is a free(copy); before the return.

Get used to compiling your dynamic memory labs with -g -fsanitize=address while you test them. It catches leaks, writes outside the block and double free calls on the spot. On Fedora you also have valgrind --leak-check=full ./program, which does something similar without recompiling.

Common mistakes with dynamic memory

Returning the address of a local array (the hook). If a function has to return an array, reserve it with malloc.

Forgetting sizeof: malloc(n) reserves n bytes, not n slots.

Not checking for NULL after malloc, calloc or realloc.

v = realloc(v, ...) without a temporary pointer: if it fails, you lose the array and leave it as a leak.

Using a pointer after free or freeing it twice. After free(v), write v = NULL.

Using sizeof(pointer) to find out how many slots a dynamic array has. It’s always 8. You have to keep the size yourself in a variable (n).

Watch it step by step in Python Tutor

Python Tutor isn’t just for Python: pick C (gcc) in the language drop-down, paste this program and click Visualize Execution. It runs the code line by line and draws the Stack on the left and the Heap on the right, with arrows for pointers. It’s exactly what we need to see dynamic memory.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n = 3;
    double *grades = malloc(n * sizeof(double));
    if (grades == NULL) return 1;
    grades[0] = 7.5;
    grades[1] = 4.0;
    grades[2] = 9.0;

    double *tmp = realloc(grades, 5 * sizeof(double));
    if (tmp == NULL) { free(grades); return 1; }
    grades = tmp;
    grades[3] = 6.5;
    grades[4] = 8.0;
    n = 5;

    printf("Last grade: %.1f\n", grades[n - 1]);
    free(grades);
    grades = NULL;
    return 0;
}

What to watch step by step:

  1. After the malloc, a 3-slot block with unknown values appears in the Heap area, and grades (on the stack, inside main) is an arrow pointing to it.
  2. As the three assignments run, the block fills with 7.5, 4.0 and 9.0. Notice that grades doesn’t change: what changes is what’s at the other end of the arrow.
  3. After the realloc, the heap shows a 5-slot block with the 3 grades copied. Depending on the tool, the block may have moved: tmp points to the new block and, after grades = tmp, so does grades.
  4. After free(grades), the block disappears from the heap and grades becomes an arrow to nothing, until grades = NULL leaves it explicitly empty.

Summary

FunctionWhat it doesReturnsWatch out for…
malloc(bytes)Reserves a blockPointer to the block or NULLIt doesn’t initialise; multiply by sizeof
calloc(n, size)Reserves n slots of size bytes, zeroedPointer or NULLIdeal for counters
realloc(p, bytes)Changes the size keeping the dataPointer (maybe new) or NULLUse a temporary; new slots aren’t initialised
free(p)Gives the block backNothingOnce per block; afterwards, p = NULL

Three ideas to take away

  1. Stack for what’s fixed and local, heap for what you decide at run time or what has to outlive a function.
  2. Bytes, not slots: n * sizeof *v, and always check for NULL.
  3. **Every block has one owner and a single free.** realloc always with a temporary and, after freeing, NULL.

Now it’s time to practise: in the dynamic memory in C practice we build a grade report that shrinks to the valid grades, an array that grows as students arrive and a dynamic table of grades per test. And in the next topic of the IC2 syllabus, structs and strings, you’ll see that dynamic memory is what lets you store as many records as you need.

Next step: practiceDynamic memory in C — practice with a grade report that shrinks, an array that grows as students arrive and a table of grades on the heap (and the realloc that nearly left me without data)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *