Skip to content
VLSI Mentor

C Part I · Module 9 · Memory and Object Lifetime

malloc and free

The previous chapter named three walls that automatic and static objects cannot get past, and said the third storage duration removes all of them. Here it is, and the striking thing about it is how little is new. The function hands back a pointer, you use it exactly as you have used pointers since Module 7, and you hand it back when you are finished. Every piece of syntax is something you already have — the return type is the generic pointer from Chapter 7.12, the failure value is the null pointer from Chapter 7.4, and the result is indexed by the rules from Chapter 7.8. What is genuinely new is an obligation: for the first time in this curriculum, an object's lifetime does not end on its own, and three habits have to become automatic before you write anything that depends on it.

Intermediate18 min readCPart IFundamentalsMemory

Part I · Module 9 · Chapter 9.6 · malloc and free

1. Two Functions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    #include <stdlib.h>

    void *malloc(size_t size);
    void  free(void *pointer);

malloc asks for size bytes and returns the address of a block that large, or NULL if it cannot provide one.

free ends the lifetime of a block obtained that way.

Both come from <stdlib.h>. Without the include, the calls are an error — Chapter 5.5's rule, and a useful one here.

Read the signatures against Module 7 and almost everything is already familiar:

Part of the signatureWhat you already know
returns void *Chapter 7.12 — an address with the type removed, convertible without a cast
returns NULL on failureChapter 7.4 — the deliberate "no object" value
takes a size_tChapter 2.12 — the type for sizes and counts
free takes a void *any object pointer converts to it implicitly

The new thing is in neither signature. It is that the lifetime of the block begins at the malloc and ends at the free, and nothing else ends it.

2. The Smallest Complete Example

Azvya Education Pvt. Ltd.VLSI Mentor
one object, obtained and released
#include <stdio.h>
#include <stdlib.h>

static int static_object = 1;

int main(void)
{
    int automatic_object = 0;

    int *allocated = malloc(sizeof *allocated);
    if (allocated == NULL) {
        printf("allocation failed\n");
        return 1;
    }

    *allocated = 42;

    printf("static object    %p\n", (void *)&static_object);
    printf("automatic object %p\n", (void *)&automatic_object);
    printf("allocated object %p   holds %d\n", (void *)allocated, *allocated);

    free(allocated);

    return 0;
}

One run:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static object    0x10097c000
automatic object 0x16f48a778
allocated object 0x1010b9a00   holds 42

Chapter 9.4 §8 asked you to predict where the third line would land. The answer is a third region — near neither the static object nor the automatic one. That is the heap, and it is the first time in this curriculum that an object has appeared there.

Everything after the allocation is ordinary. *allocated = 42; is Chapter 7.2's write through a pointer. allocated is an int * like any other, and nothing about using it differs from a pointer to a local.

Five stages in the life of an allocated object. First, ask for the bytes with malloc, passing a count multiplied by the size of one element. Second, check the result before using it, because a null pointer means the request was refused. Third, use it like any other storage, with ordinary pointer and index operations. Fourth, release it exactly once with free, which ends the lifetime. Fifth, the pointer variable still holds the old address and must not be used again.The life of one allocation01ASK FOR THE BYTESmalloc(count * sizeof *p)02CHECK THE RESULT BEFORE USING ITNULL means the request was refused03USE IT LIKE ANY OTHER STORAGEordinary pointer and index operations04RELEASE IT EXACTLY ONCEfree(p) ends the lifetime05THE POINTER STILL HOLDS THE OLD ADDRESSand must not be used again · 9.11
Figure 1 — the five stages every allocation goes through. The request can be refused, which is why the check comes before any use. Between the check and the release, the block is ordinary storage and every pointer rule from Module 7 applies unchanged. The release ends the lifetime — and the final stage is the one that makes allocation different from the other two storage durations: your pointer variable still holds the same address afterwards, and it is now unusable. Chapters 9.10 and 9.11 are about that last row.

3. Obligation One: Check Every Request

malloc can fail, and when it does it returns NULL. This is not a theoretical possibility you check for ceremonially:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    void *huge = malloc((size_t)-1);
    printf("returned NULL ? %d\n", huge == NULL);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
returned NULL ? 1

A request for the largest representable size is refused, and the refusal is reported as a value you can test.

Check the result of every allocation before using it. Chapter 7.4 §5's table named this exact case — a pointer from an operation that is allowed to fail — and said it must be checked at the boundary, once.

The shape is Chapter 5.4's early return:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int *values = malloc(count * sizeof *values);
    if (values == NULL) {
        return -1;              /* or whatever failure means here */
    }
    /* from here on, values is known good */

Skipping the check does not produce a clean failure. It produces a null dereference the moment you write through the pointer, which is Chapter 7.4 §3's undefined behaviour — and Chapter 7.4 §4 established that nothing warns you.

4. Obligation Two: Ask for the Right Number of Bytes

malloc takes bytes, not elements. For an array of count items:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int *readings = malloc(count * sizeof *readings);

Two spellings are common and one is better:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    double *a = malloc(10 * sizeof *a);          /* preferred */
    double *b = malloc(10 * sizeof(double));     /* repeats the type */

Both work. sizeof *a cannot disagree with the declaration — change double *a to float *a and the first line follows automatically while the second silently allocates twice what is needed.

This is the same argument Chapter 6.1 made for sizeof arr / sizeof arr[0] over a literal count, and Chapter 6.2 made for int a[] = {...} over a hand-written size: derive the number from the thing it describes rather than restating it. Note also that sizeof *a does not dereference anything — Chapter 2.12 established that sizeof does not evaluate its operand, so writing it before the pointer is valid is fine.

Azvya Education Pvt. Ltd.VLSI Mentor
an array whose size is decided at run time
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    size_t wanted = 7;                      /* imagine this arrived with the data */

    int *readings = malloc(wanted * sizeof *readings);
    if (readings == NULL) {
        return 1;
    }

    for (size_t i = 0; i < wanted; i++) {
        readings[i] = (int)(i * i);
    }

    printf("%zu elements:", wanted);
    for (size_t i = 0; i < wanted; i++) {
        printf(" %d", readings[i]);
    }
    printf("\n");

    free(readings);
    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
7 elements: 0 1 4 9 16 25 36

This is Chapter 9.5's first wall removed. wanted is an ordinary run-time value; nothing about it has to be known while compiling.

Note what the loop looks like: readings[i] on a pointer, bounded by a count carried alongside. That is Chapter 7.7's pointer-plus-length discipline, and it is now mandatory rather than merely advisable — there is no array object here at all, so sizeof readings is a pointer's size and the count exists only because you kept it.

5. Obligation Three: Release It Exactly Once

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    free(readings);

free ends the block's lifetime. After it, Chapter 9.1's rule applies in full: every use of that pointer is undefined.

Three properties are worth having precisely.

free(NULL) is defined and does nothing. Verified. This is deliberately useful — a cleanup path can call free on a pointer that may never have been set, provided it was initialised to NULL, without a conditional.

The pointer passed must be one a previous allocation returned, unchanged. Passing a pointer to a local, or one that has been advanced with pointer arithmetic, is undefined.

Exactly once. Not zero times — that is Chapter 9.9's leak. Not twice — that is Chapter 9.10's double-free. The pairing is a property of the program, not of any single function, which is why Chapter 9.8 is about designing interfaces that make it obvious who is responsible.

6. Two Details Worth Knowing Now

You do not need a cast.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int *p = malloc(n * sizeof *p);          /* correct C */
    int *p = (int *)malloc(n * sizeof *p);   /* also compiles */

Chapter 7.12 §2 established that a void * converts to any object pointer type implicitly in C, and that the cast you see everywhere is a C++ compatibility habit. It also gave the reason not to add it reflexively: a cast silences diagnostics, so a stray one can hide a genuine mismatch. Write the first form in C.

A fresh block is not zeroed.

The bytes malloc returns hold indeterminate values — not zero, and not anything you may rely on. This is the same situation Chapter 2.3 described for an uninitialised local, and the full account of what indeterminate actually means is Chapter 9.12's, because the answer is more subtle than "a random number".

Until then, the practical rule is Chapter 6.2's applied here: initialise it before you read it. Chapter 9.7 introduces a function that allocates and zeroes in one step, for when that is what you want.

malloc(0) is implementation-defined. It may return NULL, or a non-null pointer that must not be dereferenced. On this implementation it returned non-null. Either way, nothing useful can be read or written through the result, so the sensible position is to avoid asking for zero bytes rather than to learn which answer your platform gives.

7. A Complete Example

Azvya Education Pvt. Ltd.VLSI Mentor
allocate, use, release — with every obligation met
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    size_t count = 5;

    int *samples = malloc(count * sizeof *samples);
    if (samples == NULL) {
        printf("could not allocate %zu samples\n", count);
        return 1;
    }

    for (size_t i = 0; i < count; i++) {
        samples[i] = (int)(10 * (i + 1));
    }

    int total = 0;
    for (size_t i = 0; i < count; i++) {
        total = total + samples[i];
    }

    printf("samples:");
    for (size_t i = 0; i < count; i++) {
        printf(" %d", samples[i]);
    }
    printf("\ntotal: %d\n", total);

    free(samples);
    samples = NULL;

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
samples: 10 20 30 40 50
total: 150

Five things in that program are the pattern to copy.

The size comes from sizeof *samples, so it tracks the declaration.

The result is checked immediately, before any use, and the failure path returns rather than continuing.

The count travels with the pointer through every loop, because there is no array to ask.

free is called exactly once, on the pointer the allocation returned, unmodified.

samples = NULL; after the free is a habit worth adopting now and understanding fully in Chapter 9.11. It costs one assignment and converts a particular class of later mistake from undefined behaviour into a null dereference — which is not good, but is far more likely to be noticed.

8. What You Should Be Able to Say Now

  1. malloc returns a block of the requested size, or NULL; free ends its lifetime. Both from <stdlib.h>.
  2. The lifetime begins at the malloc and ends at the free — Chapter 9.1's allocated duration.
  3. The block lands in a third region, near neither the statics nor the automatics.
  4. The return type is void * — Chapter 7.12 — so no cast is needed in C.
  5. Check every allocation before using it. malloc((size_t)-1) returns NULL, verified.
  6. malloc takes bytes. Use count * sizeof *p, which cannot disagree with the declaration.
  7. The count must travel with the pointer — there is no array to ask, so Chapter 7.7's pairing is mandatory.
  8. free exactly once, on the unmodified pointer the allocation returned. free(NULL) is defined and does nothing.
  9. A fresh block is not zeroed; malloc(0) is implementation-defined.
  10. Nothing releases it but you, on every path — which is what the next five chapters are about.

9. Exercises

Exercise 1 — write the allocation. For each, write the malloc call and the check, using the sizeof *p form.

  • 12 int objects
  • one double
  • n objects of type unsigned char, where n is a size_t variable

Exercise 2 — find the sizing bug. Each of these asks for the wrong number of bytes. Say what it actually allocates and give the correction.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int *a = malloc(10);
double *b = malloc(10 * sizeof(int));
int *c = malloc(sizeof c);
char *d = malloc(n);                 /* n characters plus a terminator */

Exercise 3 — predict the output. Do not compile yet, and say which line would be undefined if the check were removed and the allocation failed.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int *p = malloc(3 * sizeof *p);
if (p == NULL) { return 1; }
p[0] = 1; p[1] = 2; p[2] = 3;
printf("%d %d %d\n", p[0], p[1], p[2]);
free(p);

Exercise 4 — classify four claims. Say whether each is an ISO C guarantee, implementation-defined, or false.

  • malloc returns zeroed memory
  • free(NULL) is safe
  • malloc(0) returns NULL
  • a cast is required when assigning malloc's result in C

Exercise 5 — repair the function. This has three faults. Name all three and give a corrected version.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int *make_buffer(int n)
{
    int *b = (int *)malloc(n);
    for (int i = 0; i < n; i++) { b[i] = 0; }
    return b;
}

Exercise 6 — count the paths. A function allocates a buffer and has three return statements. Say how many free calls the function needs in the worst case, what happens if one path is missed, and which chapter covers that failure.

Exercise 7 — explain the third region. §2's output shows three addresses in three places. Say which storage duration each corresponds to, and whether the clustering is a C guarantee.

Exercise 8 — justify the habit. samples = NULL; after the free changes nothing about the block. Explain what it does change, what class of later mistake it converts, and why the result is still a bug rather than a fix.

10. Summary

  • malloc and free give you allocated storage duration — a lifetime you begin and end.
  • The return is void *, needing no cast in C; failure is NULL.
  • The block lands in a third region, which is Chapter 9.4's heap.
  • Check every request before using it — the refusal is a value, not a crash.
  • Ask in bytes: count * sizeof *p, so the size tracks the declaration.
  • Carry the count — there is no array object to ask.
  • Free exactly once, on the unmodified returned pointer; free(NULL) does nothing.
  • A fresh block is not zeroed; malloc(0) is implementation-defined.
  • Nothing releases it but you, on every path — the subject of Chapters 9.8 to 9.12.

One obligation in §4 came with an awkward consequence: a fresh block holds nothing you can rely on, so an allocation intended to start from zero needs a loop immediately after it. That is common enough that the standard library provides a function which does both in one call. There is also a harder problem lurking in §5's fixed count — a block's size is decided when you ask for it, and a program that discovers it needs more has no way yet to say so without losing what it already has. The next chapter covers both functions, and the second one has a failure mode that destroys your data if you call it in the obvious way.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.