Learn › Learn C › C in Practice
malloc - a hands-on Linux lab on a real virtual machine.
Group related values into a struct, a shaped box with named fields, and give it a life longer than one function by renting memory on the heap with malloc and returning it with free. Learn the ownership rule (one free per malloc), the malloc(sizeof u) idiom, and the dot-versus-arrow distinction. Then put valgrind in front of the program and read its audit: the clean no-leaks-possible summary when the books balance, and 36 bytes definitely lost when a single free goes missing.
You are about to meet a program that does everything right. It compiles with zero warnings. It prints exactly the line it was built to print. It exits with code 0, the shell's signal for success. By every test you have learned so far, it works.
Then you run it under a tool called valgrind, and valgrind reports this:
36 bytes in 1 blocks are definitely lost in loss record 1 of 1
Definitely lost. Not maybe. The program borrowed 36 bytes of memory, used them, and walked away without giving them back. Nothing on the screen ever hinted at it.
This lesson is about those 36 bytes. Their shape is a new kind of variable called a struct. Their landlord is a region of memory called the heap. And the auditor that catches the unpaid debt is valgrind. By the end you will build the debt yourself, watch valgrind catch it, and pay it back.
The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. Every output shown was captured from the real lab machine (Debian 12, gcc 12.2.0), the same image your labs boot. Two things will differ on a live VM: memory addresses change every run because the kernel randomizes layout (ASLR), and process ids are new each time. Sizes like 36 bytes and the 8-byte pointer are for this 64-bit machine.
So far every variable you have declared holds one thing. An int holds one number. A char array holds one string. But real data travels in groups. Think of one line of /etc/passwd, a file you read back in Linux Foundations: a user name, a user id, a home directory, all describing one user.
C lets you define a new type that groups related values under one name. That type is a struct, short for structure. A struct is a shaped box: a box with labeled compartments, where you choose the labels and what fits in each one.
Here is the shape this whole lesson uses:
struct user { char name[32]; /* compartment 1: up to 31 characters plus the NUL */ int uid; /* compartment 2: one whole number */ };
Each labeled compartment is called a field (you will also hear member). This struct has two. name is an array of 32 characters; remember from the strings lesson that a C string ends with a hidden NUL byte, so it holds at most 31 visible characters. uid is one int.
Why does struct exist? Without it, a program tracking 50 users needs 50 loose name variables and 50 loose uid variables, and nothing ties name number 17 to uid number 17 except your discipline. With struct, each user is one box. A function can take a whole user. An array can hold whole users. The CSV parser you build at the end of this module reads a file into exactly these boxes, one per row.
The closing brace of a struct definition must be followed by a semicolon: }; and not }. Forgetting it is the classic struct typo. The gcc complaint it triggers is waiting for you in the errors step below.
Declare a struct variable the way you declare an int, and reach into its compartments with a dot. Here is a complete program:
#include <stdio.h> #include <string.h>
struct user { char name[32]; int uid; };
int main(void) { struct user u;
strcpy(u.name, "operator"); u.uid = 1000;
printf("%s has uid %d (struct is %zu bytes)\n", u.name, u.uid, sizeof u); return 0; }
Three new things to read. struct user u; declares a variable named u whose type is struct user: one whole box, living on the stack like every local variable you met in the functions lesson. u.name reaches into the box: the dot means inside u, the compartment called name. And strcpy, from the strings lesson, copies a string into that compartment.
%zu is the printf placeholder for the kind of number sizeof produces. sizeof u asks the compiler how many bytes the whole box occupies.
>>> sizeof u counts every compartment: 32 bytes of name plus 4 bytes of int is 36. One honest caveat: the compiler is allowed to slip invisible padding bytes between fields to keep them aligned, so a struct can be bigger than the sum of its parts. This one needs no padding. The 4-byte int and the 36 total are what this 64-bit machine uses.
prompt: operator@camp:~$ answer: gcc -Wall box.c -o box && ./box output: operator has uid 1000 (struct is 36 bytes) hint: Same shape as every compile so far: gcc -Wall box.c -o box && ./box
There are the 36 bytes from the cold open, and now you know their shape: one struct user. Notice this version has no leak, and no heap. u is a local variable, so it sits in main's stack frame, and the functions lesson told you the rest: when main returns, the frame is torn down and u vanishes. Automatic cleanup is the stack's gift. Its price is that nothing on the stack outlives the function that made it. That price is exactly why the heap exists.
Picture your program's memory as a building. The stack is the front desk: fast, tidy, and cleared the moment each visitor (each function call) leaves. The heap is the storage floor: a big open space where you can rent a room of any size. A rented room stays yours across any number of function calls, until you give it back.
Renting is done with malloc, short for memory allocate, from stdlib.h. You tell it how many bytes you need. It finds a free block that big on the heap and hands back the block's address, which you keep in a pointer. Remember from the pointers lesson: a pointer is a variable that holds an address.
Giving the room back is free. It takes the pointer malloc gave you and returns the block to the heap. That pairing has a name engineers use constantly: ownership. Whoever holds the pointer from malloc owns the block, and owes the system exactly one free for it.
#include <stdio.h> #include <stdlib.h> #include <string.h>
struct user { char name[32]; int uid; };
int main(void) { struct user *u = malloc(sizeof *u); if (u == NULL) { return 1; }
strcpy(u->name, "operator"); u->uid = 1000;
printf("%s has uid %d (struct is %zu bytes)\n", u->name, u->uid, sizeof *u);
free(u); return 0; }
Read the highlighted lines slowly. malloc(sizeof *u) says: give me enough bytes for the thing u points to. The check on the next line matters because malloc can fail: when the heap has no room it returns NULL, the address that means nowhere, and touching NULL crashes the program. And free(u) at the end pays the debt.
One more new mark: the arrow. u->name means follow the pointer u, then open the name compartment. It is shorthand for (*u).name. Dot for a struct variable, arrow for a pointer to one.
>>> sizeof *u measures the thing u points at: the whole 36-byte struct, so malloc rents exactly one box. Without the star, sizeof u is 8, the pointer itself, and malloc would rent a box far too small. That is why malloc(sizeof *u) is the idiom working C engineers reach for: if the struct grows a field next month, the size stays correct with no other edit.
prompt: operator@camp:~$ answer: gcc -Wall users.c -o users && ./users output: operator has uid 1000 (struct is 36 bytes) hint: New file, same pattern: gcc -Wall users.c -o users && ./users
Identical output, different home. The box no longer lives in main's stack frame. It lives on the heap, and the only thing in the frame is u: an 8-byte address pointing at it. Here is the rented block itself, byte by byte.
{ "kind": "memory", "cells": [ { "addr": "u + 0", "bytes": "6f 70 65 72 61 74 6f 72 00 ..", "label": "name[32]: 'operator', its NUL, then the unused rest" }, { "addr": "u + 32", "bytes": "e8 03 00 00", "label": "uid: 1000, low byte first (little-endian)" } ], "caption": "One struct user on the heap: 32 bytes of name, then 4 bytes of uid." }
The cells are labeled u + 0 and u + 32, offsets from wherever the block landed. That is because the real address changes on every run: the kernel randomizes memory layout (ASLR), so your run's address will differ from anyone else's. The offsets and the bytes are stable. The hex 6f 70 65 72 61 74 6f 72 is the word operator, one byte per letter, and e8 03 is 1000 written low byte first, the ordering this machine uses.
How do you know the free really balanced the malloc? You could reread the code, but eyes miss things. valgrind does not. valgrind runs your program inside a monitor that records every allocation and every free, then prints an audit when the program exits. No recompile needed: you put the word valgrind in front of the program.
Keep your eyes on the pair it will audit:
struct user *u = malloc(sizeof *u); /* rent: one 36-byte block */ /* ... fill the box, print the box ... */ free(u); /* return: the same block */
>>> Surprise: two allocations. The first time printf runs it rents a 4,096-byte buffer for its output, and the C library frees that buffer at exit. 4,096 plus your 36 is 4,132. This is normal, and it is a lesson in itself: library code uses the heap too. What you audit is the balance: allocs equal frees, and in use at exit reads 0 bytes.
prompt: operator@camp:~$ answer: valgrind ./users output: HEAP SUMMARY: in use at exit: 0 bytes in 0 blocks total heap usage: 2 allocs, 2 frees, 4,132 bytes allocated All heap blocks were freed -- no leaks are possible ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) hint: Put the auditor in front of the binary: valgrind ./users
This is the clean bill of health, and the line to memorize is All heap blocks were freed -- no leaks are possible. Read the audit top to bottom. in use at exit: 0 bytes means nothing was still rented when the program ended. 2 allocs, 2 frees is the balance you predicted: your malloc plus printf's buffer, your free plus the library's. ERROR SUMMARY: 0 errors closes the case. From this module on, clean under valgrind is part of what done means.
On the VM, valgrind prefixes every line it prints with the process id between equals signs, like ==1234==. That number is new on every run, so it is trimmed here. Your program's own printf line appears in the middle, without the prefix.
Now build the cold open yourself. Here is users.c with exactly one change: the free is gone. Everything else is identical, down to the byte.
#include <stdio.h> #include <stdlib.h> #include <string.h>
struct user { char name[32]; int uid; };
int main(void) { struct user *u = malloc(sizeof *u); if (u == NULL) { return 1; }
strcpy(u->name, "operator"); u->uid = 1000;
printf("%s has uid %d (struct is %zu bytes)\n", u->name, u->uid, sizeof *u);
return 0; /* free(u) used to be right here */ }
>>> The program is not wrong; it is in debt. When main returns, its frame is torn down and u, the only copy of the block's address, is destroyed with it. The 36-byte block is still rented on the heap, but no pointer to it exists anywhere anymore, so it can never be freed. valgrind calls that condition definitely lost. Note what did NOT happen: no crash, no warning, no wrong output. Leaks are silent, which is exactly why the auditor exists.
prompt: operator@camp:~$ answer: gcc -Wall leaky.c -o leaky && valgrind --leak-check=full ./leaky output: 36 bytes in 1 blocks are definitely lost in loss record 1 of 1 definitely lost: 36 bytes in 1 blocks ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) hint: Chain a compile and a full audit: gcc -Wall leaky.c -o leaky && valgrind --leak-check=full ./leaky
Caught. --leak-check=full asks valgrind to name every lost block instead of just counting them, and there is your struct: 36 bytes, 1 block, definitely lost. On the VM the full report also shows the trail of calls that made the lost block. Compile with -g (debug info, from the gdb lesson coming next) and valgrind names the exact file and line of the guilty malloc.
Why care, when the program exits a moment later and the operating system reclaims everything a dead process held? Because real programs do not exit. A server that leaks one 36-byte struct per request has leaked a gigabyte after 30 million requests, and then the kernel kills it. Leaks are how long-running programs die slowly. Here is the exact moment the debt became unpayable.
{ "kind": "stack", "frames": [ { "fn": "main", "locals": [ { "name": "u", "value": "the block's address (destroyed with this frame)" } ], "ret": "the C runtime, then exit" } ], "caption": "The frame is torn down, u dies with it, and the 36-byte block on the heap is left with no pointer aiming at it. That is a leak." }
Four messages, all captured from this machine. Learn their shapes now so they read as clues later, not as noise.
1. The leak verdict
36 bytes in 1 blocks are definitely lost in loss record 1 of 1
What it means: a block was malloc'd, never freed, and no pointer to it survived to the end of the program. The byte count and block count tell you which allocation to hunt for.
What to check first: every path out of the function. The classic escape is an early return between the malloc and the free, which skips the free on that path.
2. The crash
Segmentation fault
What it means: the program touched memory it does not own, and the kernel stopped it. The shell reports exit code 139. In heap code the two usual causes are using the NULL that an unchecked malloc returned, and touching a block after freeing it. Both are undefined behavior: the C standard makes no promise about what happens, so the crash is the friendly outcome. The unfriendly one is silent corruption that surfaces somewhere else.
What to check first: is malloc's return value checked against NULL, and does anything read or write through u after free(u).
3. valgrind's bottom line
ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
What it means: valgrind found at least one real problem, and the details are printed above this line. Zero errors is the only passing grade.
What to check first: scroll up to the first error paragraph and fix that one first. Later errors are often echoes of the first.
4. The missing struct semicolon
broken.c:8:1: error: expected ';', identifier or '(' before 'int'
What it means: this is the struct typo the warning above promised. The closing brace of struct user { ... } was written as } with no semicolon, so gcc kept reading past it, ran into int main on line 8, and only then realized the definition never ended. Notice the trap: gcc points at the innocent line it tripped over, not at the line with the mistake. It also names whatever token it hit, so the wording after expected ';' varies from program to program.
What to check first: look above the line gcc names for a struct definition, and make sure its closing brace has its own semicolon: }; and not }.
Scaffolding off. No code panel this time, and no command shown above the box.
You opened leaky.c and put free(u); back where it belongs, just before the return. Now prove the fix the way an engineer would: rebuild the program with warnings on, and run it under the auditor, chained as one line. You want to see the no-leaks-possible verdict with your own eyes.
prompt: operator@camp:~$ answer: gcc -Wall leaky.c -o leaky && valgrind ./leaky|||gcc -Wall leaky.c -o leaky && valgrind --leak-check=full ./leaky output: HEAP SUMMARY: in use at exit: 0 bytes in 0 blocks total heap usage: 2 allocs, 2 frees, 4,132 bytes allocated All heap blocks were freed -- no leaks are possible ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) hint: Two tools, one line: gcc -Wall on leaky.c, then && to run valgrind on ./leaky.
That verdict is yours now: allocs equal frees, 0 bytes in use at exit, 0 errors. This loop you just ran, edit, recompile, audit, is how C engineers actually work. The compiler checks your spelling; valgrind checks your debts. Neither one trusts you, and that is a feature.
You earned this table. Every row is something you predicted, ran, or fixed:
The sentence to keep: every malloc creates a debt, and free is the only way to pay it. valgrind is how you prove the books balance.
Everything in this lesson was one struct and one malloc. The module lab scales it to real work.
Lab: CSV Parser boots a real VM with a data file of comma-separated rows waiting in your home directory. You will define a struct that matches one row, and open the file with the fopen you learned in the file I/O lesson. Then you malloc one struct per row as you read, print a report, and free every block you rented. The grader compiles your program with gcc -Wall -Wextra, runs it against inputs you have not seen, and then runs valgrind on it. The clean summary, allocs equal to frees and zero bytes in use at exit, is part of the grade.
One difference from this lesson: the lab shows no code. You read the goal, recall the struct shape, malloc(sizeof *u), the
Practice struct and the heap in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.