Register

C for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

malloc, calloc, realloc, free

Pointers and memory13 min readLesson 4 of 6

4.1 Memory requested at run time

A local array has a fixed size chosen when the code compiles. When the size is only known while the program runs, ask the heap for memory with malloc.

malloc(n) returns a pointer to n bytes, or NULL when it cannot satisfy the request. The bytes are uninitialised, so reading before writing gives an arbitrary value.

<stdlib.h> declares malloc, calloc, realloc, and free. Library functions such as fopen allocate internally; the four here are the ones you call directly.

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

int main(void)
{
    int count = 4;
    int *values = malloc((size_t)count * sizeof *values);
    if (values == NULL) {
        printf("allocation failed\n");
        return 1;
    }
    for (int i = 0; i < count; i++) {
        values[i] = (i + 1) * 10;
    }
    printf("%d %d %d %d\n", values[0], values[1], values[2], values[3]);
    free(values);
    return 0;
}

Output

10 20 30 40

4.2 sizeof the pointed-to type

The allocation size has to match the element type. malloc(count * sizeof *values) reads the type from the pointer itself, so a later change to the element type keeps the allocation correct.

sizeof *values is valid even though values has not been assigned yet, because sizeof examines the type without evaluating the expression. That idiom replaces sizeof(int), which breaks silently when the element type changes.

Leave the cast off the result. C converts void * to any object pointer type, and a cast only hides a missing declaration of malloc on very old compilers.

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

int main(void)
{
    size_t count = 3;
    double *samples = malloc(count * sizeof *samples);
    if (samples == NULL) return 1;
    samples[0] = 1.5;
    samples[1] = 2.5;
    samples[2] = 3.5;
    printf("%.1f %.1f %.1f\n", samples[0], samples[1], samples[2]);
    free(samples);
    return 0;
}

Output

1.5 2.5 3.5

4.3 calloc starts at zero

calloc(count, size) allocates space for count objects of size bytes and sets every byte to zero. The two arguments make the intent clearer than one multiplied expression.

Zeroed memory matters for arrays of counts, flags, and tables that are filled in later. All-zero bytes do not have to represent a null pointer by the standard, though mainstream platforms work that way, so check before relying on calloc for pointer arrays.

malloc plus memset does the same work. calloc states the goal once and can be faster when the system has zeroed pages ready.

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

int main(void)
{
    int *counts = calloc(4, sizeof *counts);
    if (counts == NULL) return 1;
    counts[2] += 5;
    for (int i = 0; i < 4; i++) {
        printf("%d%c", counts[i], i == 3 ? '\n' : ' ');
    }
    free(counts);
    return 0;
}

Output

0 0 5 0

4.4 Growing with realloc

realloc(ptr, new_size) asks for a block of a new size. It may return the same address, a different address, or NULL on failure.

The NULL case is the trap. Assigning the result straight back to the original pointer loses the old block when realloc fails, which leaks the memory and leaves nothing to free.

On success, the old contents are copied up to the smaller of the two sizes, and the original block is released. The old pointer must not be used afterwards.

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

int main(void)
{
    int *values = malloc(2 * sizeof *values);
    if (values == NULL) return 1;
    values[0] = 10;
    values[1] = 20;

    int *grown = realloc(values, 5 * sizeof *values);
    if (grown == NULL) { free(values); return 1; }
    values = grown;
    for (int i = 2; i < 5; i++) values[i] = values[i - 1] + 10;
    for (int i = 0; i < 5; i++) printf(i == 4 ? "%d\n" : "%d ", values[i]);
    free(values);
    return 0;
}

Output

10 20 30 40 50

4.5 free gives memory back

free(ptr) releases a block from malloc, calloc, or realloc. The pointer must be one an allocation function returned, and it must not have been freed before.

Passing NULL is allowed and does nothing, which lets cleanup code run without branches. Passing an address inside a block, a stack address, or a stale pointer is undefined behaviour, and most allocators corrupt their bookkeeping or crash.

After free, set the pointer to NULL when it stays in scope. A stale pointer that still holds the old address is the start of a use-after-free bug.

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

int main(void)
{
    int *block = malloc(3 * sizeof *block);
    if (block == NULL) return 1;
    block[0] = 7;
    printf("before free: %d\n", block[0]);

    free(block);
    block = NULL;
    if (block == NULL) printf("pointer cleared\n");
    free(block);
    return 0;
}

Output

before free: 7
pointer cleared

4.6 Who frees what

Every allocation needs an owner. The simplest rule gives ownership to the function that called malloc and frees the block in the same function, usually in the same block.

When a function returns allocated memory, the caller becomes the owner. Put that contract in the name or a comment: copy_text returns memory the caller must free. A function that takes ownership needs an equally clear name, such as consume.

Allocation order and free order do not have to match. Nesting is a convention for keeping the bookkeeping readable.

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *copy_text(const char *text)
{
    size_t size = strlen(text) + 1;
    char *copy = malloc(size);
    if (copy == NULL) return NULL;
    memcpy(copy, text, size);
    return copy;
}

int main(void)
{
    char *label = copy_text("heap copy");
    if (label == NULL) return 1;
    printf("%s\n", label);
    free(label);
    return 0;
}

Output

heap copy

4.7 Habits that prevent most heap bugs

  • check every allocation before using the pointer
  • size allocations with sizeof *ptr
  • keep the result of realloc in a temporary
  • free each block exactly once, and set the pointer to NULL afterwards
  • run cleanup on the failure path as well as the success path

A function with several allocations and an early return can leak the earlier blocks. Because free(NULL) is safe, cleanup can run unconditionally once every pointer holds a valid address or NULL.

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

int main(void)
{
    char *first = malloc(4);
    char *second = malloc(4);
    if (first != NULL && second != NULL) {
        first[0] = 'o'; first[1] = 'k'; first[2] = '\0';
        second[0] = '!'; second[1] = '\0';
        printf("%s%s\n", first, second);
    }
    free(first);
    free(second);
    return 0;
}

Output

ok!

Interview questions

3 questions from this note. Open one to see the answer; no account needed.

  • 01Why write `malloc(count * sizeof *ptr)` instead of `malloc(count * sizeof(int))`?

    The expression follows the pointer's element type, so changing the type later keeps the size correct without a second edit.

  • 02Why is `ptr = realloc(ptr, size)` risky?

    When `realloc` fails it returns `NULL` and leaves the old block allocated. Assigning that result to `ptr` loses the only pointer to the old block, so the memory cannot be freed.

  • 03What does `free(NULL)` do?

    Nothing. The standard defines it as safe, which lets cleanup code free pointers that were never allocated.