Dynamic memory · about 55 min

calloc, realloc and growing buffers

Why this matters

malloc alone forces you to know the size before you allocate. Much of the time you do not: you are reading input of unknown length, collecting results one at a time, or loading a file. The standard answer, used inside every dynamic array, string builder and vector in every language runtime, is a buffer that grows by reallocation. Python lists, C++ std::vector, Go slices and Rust Vec all work this way underneath. This lesson shows the two remaining allocation functions and the growth pattern built on them.

calloc: zeroed memory, overflow-checked

void *calloc(size_t count, size_t size);

calloc allocates room for count elements of size bytes each and sets every byte to zero. Two advantages over malloc(count * size):

  1. The memory starts zeroed, so integers are 0, pointers are null (on every platform you will meet), and strings are empty.
  2. It checks the multiplication for overflow. count * size is computed in size_t, which wraps around silently; a huge count can wrap to a small number, and malloc would then happily return a block far smaller than you think.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int main(void)
{
    int *a = calloc(5, sizeof *a);
    if (a == NULL) return 1;
    printf("%d %d %d %d %d\n", a[0], a[1], a[2], a[3], a[4]);

    size_t huge = SIZE_MAX / 2;
    int *b = calloc(huge, sizeof *b);
    printf("calloc of %zu ints: %s\n", huge, b ? "succeeded" : "NULL");
    printf("%zu\n", huge * sizeof(int));
    free(a);
    return 0;
}
0 0 0 0 0
calloc of 9223372036854775807 ints: NULL
18446744073709551612

The last line shows the danger: huge * 4 wrapped around to a number that is wrong (it is SIZE_MAX - 3; with other values it can wrap to something small). calloc refused the request instead. When sizes come from input, this matters for security.

realloc: change a block's size

void *realloc(void *ptr, size_t new_size);

realloc resizes the block at ptr to new_size bytes and returns a pointer to the resized block. What happens underneath:

  in place:   [hdr| a b c d |  free space  ]   ->  [hdr| a b c d ? ? ? ? ]   same address

  moved:      [hdr| a b c d ][hdr| in use ]        old block freed
                                                   [hdr| a b c d ? ? ? ? ]   new address

Special cases: realloc(NULL, n) behaves like malloc(n), which is handy for buffers that start empty. New bytes are uninitialized.

Because the old block survives a failure, the correct pattern always uses a temporary:

int *tmp = realloc(data, new_cap * sizeof *data);
if (tmp == NULL) {
    /* data is still valid: report the error, free(data), or carry on with the old size */
} else {
    data = tmp;
}

And because the block may move, never keep pointers into a buffer across a realloc. Keep indexes instead.

Capacity and length

A growable buffer tracks two numbers:

  data -> [ 3 | 1 | 4 | 1 | . | . | . | . ]
            <---- length 4 ---->
            <------------ capacity 8 ------------>

Appending is cheap while length < capacity. When they are equal, grow. The crucial design choice is by how much:

The growth pattern in full

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

int main(void)
{
    size_t count = 0, capacity = 4;
    int *data = malloc(capacity * sizeof *data);
    if (data == NULL)
        return 1;

    int x;
    while (scanf("%d", &x) == 1) {
        if (count == capacity) {
            size_t new_cap = capacity * 2;
            int *bigger = realloc(data, new_cap * sizeof *data);
            if (bigger == NULL) {
                free(data);
                printf("out of memory\n");
                return 1;
            }
            data = bigger;
            capacity = new_cap;
            printf("grew to %zu\n", capacity);
        }
        data[count++] = x;
    }

    long sum = 0;
    for (size_t i = 0; i < count; i++)
        sum += data[i];
    printf("%zu numbers, sum %ld\n", count, sum);
    free(data);
    return 0;
}
$ seq 1 20 | ./prog
grew to 8
grew to 16
grew to 32
20 numbers, sum 210

Twenty appends, three reallocations. With a million numbers there would be only about eighteen.

data[count++] = x; uses the post-increment you met in the operators module: store at index count, then add one to count.

Strings of unknown length

The same idea reads a line of any length. Grow when there is no room for one more character plus the terminator:

char *buf = NULL;
size_t len = 0, cap = 0;
int c;
while ((c = getchar()) != EOF && c != '\n') {
    if (len + 2 > cap) {                       /* char + '\0' */
        size_t new_cap = cap ? cap * 2 : 16;
        char *tmp = realloc(buf, new_cap);     /* realloc(NULL, n) == malloc(n) */
        if (tmp == NULL) { free(buf); return NULL; }
        buf = tmp;
        cap = new_cap;
    }
    buf[len++] = (char)c;
}

POSIX systems provide getline, which does exactly this for you; you will use it in the files module. Writing it once yourself is how you understand what it does.

Common mistakes

Recap

Going further: read the getline(3) manual page (man 3 getline with manpages-dev installed, or man7.org) and compare its interface with the loop above. Then try growing by capacity + 1 instead of doubling in the example and time seq 1 200000 | ./prog against the doubling version with time.

Exercises

Exercise 1 · quiz

The classic realloc mistake

What is wrong with this line, assuming buf is the only pointer to a heap block?

buf = realloc(buf, new_size);
Answer
  • If realloc fails it returns NULL, buf is overwritten with NULL, and the original block leaks

When realloc cannot grow the block it returns NULL and leaves the original block untouched and still allocated. Assigning the result straight back to buf throws away the only pointer to it. Store the result in a temporary, check it, and only then assign: char *tmp = realloc(buf, n); if (tmp == NULL) { /* buf still valid: free it or keep using it */ } else buf = tmp;. Shrinking with realloc is allowed.

Exercise 2 · quiz

Pointers into a block that moved

int *data = malloc(4 * sizeof *data);
int *third = &data[2];
int *tmp = realloc(data, 1000 * sizeof *data);
if (tmp != NULL)
    data = tmp;

After this code (realloc succeeded), which statements are true? Select all that apply.

Answer
  • data[0] .. data[3] still hold the old values

  • third may now point into freed memory and must not be used

realloc copies the old contents (up to the smaller of the old and new sizes) and may move the block to a new address, freeing the old one. Any pointer into the old block, like third, is then dangling. Keep indexes instead of pointers into a growable buffer, or recompute pointers after every realloc. The newly added part is uninitialized, not zero; realloc has no calloc-like variant.

Exercise 3 · write code

Collect numbers until end of input

Read integers from standard input until scanf stops returning 1 (end of input). You do not know how many there will be. Store them in a heap array that starts with capacity 4 and doubles with realloc whenever it is full. Then print, each on its own line:

count: N
reversed: a b c ...

where the second line lists the numbers in reverse order separated by single spaces. With no numbers at all, print count: 0 and reversed: (nothing after the colon).

Check every allocation, and free the array before exiting. The check feeds your program 5000 numbers under valgrind.

File: main.c

Starting point

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

int main(void)
{
    size_t count = 0, capacity = 4;
    int *data = malloc(capacity * sizeof *data);
    if (data == NULL)
        return 1;

    int x;
    while (scanf("%d", &x) == 1) {
        /* grow when full: count == capacity */
        data[count++] = x;
    }

    printf("count: %zu\n", count);
    printf("reversed:");
    /* print the numbers in reverse, each preceded by a space */
    printf("\n");
    return 0;
}

Example

input

7 8 9

expected output

count: 3
reversed: 9 8 7

Example

input

1 2 3 4 5 6 7 8 9 10

expected output

count: 10
reversed: 10 9 8 7 6 5 4 3 2 1
Hint 1

Before storing, if (count == capacity), realloc to capacity * 2 elements into a temporary pointer.

Hint 2

If the temporary is NULL, free the old block and exit; otherwise assign it to data and update capacity.

Hint 3

size_t is unsigned, so a loop for (size_t i = count - 1; i >= 0; i--) never ends. Loop for (size_t i = count; i > 0; i--) and use data[i - 1].

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

int main(void)
{
    size_t count = 0, capacity = 4;
    int *data = malloc(capacity * sizeof *data);
    if (data == NULL)
        return 1;

    int x;
    while (scanf("%d", &x) == 1) {
        if (count == capacity) {
            size_t new_cap = capacity * 2;
            int *bigger = realloc(data, new_cap * sizeof *data);
            if (bigger == NULL) {
                free(data);
                printf("out of memory\n");
                return 1;
            }
            data = bigger;
            capacity = new_cap;
        }
        data[count++] = x;
    }

    printf("count: %zu\n", count);
    printf("reversed:");
    for (size_t i = count; i > 0; i--)
        printf(" %d", data[i - 1]);
    printf("\n");
    free(data);
    return 0;
}

Exercise 4 · write code

Reverse the lines of any input

Write a program that reads all of standard input and prints its lines in reverse order (last line first), like the tac command. Lines can be of any length, so you cannot use a fixed-size buffer, and there can be any number of lines.

Rules:

Example: input one\ntwo\nthree\n prints three\ntwo\none\n.

File: main.c

Starting point

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

int main(void)
{
    char **lines = NULL;     /* growing array of strings */
    size_t nlines = 0, lines_cap = 0;

    char *cur = NULL;        /* the line being read */
    size_t len = 0, cap = 0;

    int c;
    while ((c = getchar()) != EOF) {
        /* append c to cur (growing it), or on '\n' finish the line */
    }
    /* a final line without '\n'? */

    for (size_t i = nlines; i > 0; i--)
        printf("%s\n", lines[i - 1]);

    /* free everything */
    return 0;
}

Example

input

one
two
three

expected output

three
two
one

Example

input

alpha
beta

expected output

beta
alpha
Hint 1

Write a helper that grows a char buffer when len + 2 > cap (room for one more character plus a terminator), doubling the capacity.

Hint 2

On '\n': terminate cur with '\0', append the pointer cur to lines (growing lines when nlines == lines_cap), then set cur = NULL, len = cap = 0 so the next line starts a fresh buffer.

Hint 3

After the loop, if len > 0 (or a character arrived since the last newline), finish that line the same way. At the end free each lines[i], then lines, then cur.

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

/* Make sure *buf has room for at least need bytes, doubling its capacity. */
static int reserve(char **buf, size_t *cap, size_t need)
{
    if (need <= *cap)
        return 1;
    size_t new_cap = *cap ? *cap : 16;
    while (new_cap < need)
        new_cap *= 2;
    char *tmp = realloc(*buf, new_cap);
    if (tmp == NULL)
        return 0;
    *buf = tmp;
    *cap = new_cap;
    return 1;
}

static int push_line(char ***lines, size_t *n, size_t *cap, char *line)
{
    if (*n == *cap) {
        size_t new_cap = *cap ? *cap * 2 : 8;
        char **tmp = realloc(*lines, new_cap * sizeof *tmp);
        if (tmp == NULL)
            return 0;
        *lines = tmp;
        *cap = new_cap;
    }
    (*lines)[(*n)++] = line;
    return 1;
}

int main(void)
{
    char **lines = NULL;
    size_t nlines = 0, lines_cap = 0;

    char *cur = NULL;
    size_t len = 0, cap = 0;
    int ok = 1, pending = 0;

    int c;
    while (ok && (c = getchar()) != EOF) {
        if (c == '\n') {
            ok = reserve(&cur, &cap, len + 1);
            if (ok) {
                cur[len] = '\0';
                ok = push_line(&lines, &nlines, &lines_cap, cur);
                if (ok) { cur = NULL; len = cap = 0; }
            }
            pending = 0;
        } else {
            ok = reserve(&cur, &cap, len + 2);
            if (ok)
                cur[len++] = (char)c;
            pending = 1;
        }
    }
    if (ok && pending) {
        cur[len] = '\0';
        ok = push_line(&lines, &nlines, &lines_cap, cur);
        if (ok)
            cur = NULL;
    }

    if (ok)
        for (size_t i = nlines; i > 0; i--)
            printf("%s\n", lines[i - 1]);
    else
        printf("out of memory\n");

    free(cur);
    for (size_t i = 0; i < nlines; i++)
        free(lines[i]);
    free(lines);
    return ok ? 0 : 1;
}