Dynamic Memory and I/O
Introduction
So far, most variables you used were automatic — created when a function starts and destroyed when it ends (on the stack), or global — living for the whole program. That works until you need an array whose size depends on user input, a log file on an SD card, or program options from the command line.
This article explains where different kinds of data live in memory, how heap allocation (malloc / free) works, how files let you keep data after the program stops, and how command-line arguments configure a program without recompiling.
On a bare-metal MCU you may rarely use malloc or fopen; on embedded Linux (Raspberry Pi, industrial gateway) you will. Understanding both worlds makes you a stronger engineer.
Where does your program live in memory?
When the operating system (or bootloader) starts your program, it loads several regions into RAM and flash. Think of them as departments in a building, each with a different job.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
| Region | What lives here | Lifetime |
|---|---|---|
| Text | Machine instructions | Whole program |
| Data | Initialized globals/statics | Whole program |
| BSS | Zero-initialized globals/statics | Whole program |
| Stack | Locals, function call chain | While function runs |
| Heap | malloc blocks |
Until you free them |
In plain terms
Stack = your desk: quick notes you throw away when you leave the room (function ends).
Heap = a storage unit you rent: stays until you explicitly return the key (free).
Globals = a whiteboard in the office: always there for everyone.
Embedded reality
Many MCUs have no heap or only a few kilobytes. Firmware teams often ban malloc in production and use static buffers or memory pools so behaviour is predictable. If you do use heap on MCU, check linker script heap size and handle NULL from malloc — out-of-memory is real.
See Memory systems for flash vs RAM layout.
Dynamic allocation — renting memory at runtime
malloc, calloc, and free come from <stdlib.h>. C has no garbage collector — if you allocate, you must free, or memory leaks until reboot.
| Function | What it does |
|---|---|
malloc(n) |
Reserve n bytes; contents are garbage until you write |
calloc(count, size) |
Reserve count * size bytes; all bits set to zero |
free(ptr) |
Give memory back to the heap |
malloc returns void * — you cast to the type you need. Always check for NULL (allocation failed).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
Why sizeof(int)? So the size is correct on any platform. Same pattern for structs: malloc(n * sizeof(sensor_reading_t)).
calloc(num, sizeof(int)) is nicer when you want zero-initialized memory (counters, flags) without a manual loop.
Common mistakes
| Mistake | What happens |
|---|---|
Forget free |
Leak — heap shrinks over time |
free twice |
Crash / heap corruption |
Use after free |
Undefined behaviour — silent bugs |
| Write past end of block | Corrupts heap metadata |
On Linux, run under Valgrind or compile with -fsanitize=address while learning.
File I/O — data that survives after exit
Variables vanish when the program ends. Files store data on disk (or flash filesystem) for logging, configuration, or post-mortem analysis.
C models a file as a FILE * stream — an opaque handle managed by the C library.
| Function | Role |
|---|---|
fopen(path, mode) |
Open or create |
fclose(fp) |
Close and flush |
fprintf / fscanf |
Formatted write/read (like printf/scanf) |
fgets / fputs |
Line-based text |
Modes matter:
| Mode | Behaviour |
|---|---|
"r" |
Read only; file must exist |
"w" |
Write; creates or wipes existing file |
"a" |
Append; adds to end without erasing |
"r+" |
Read and write |
Writing a sensor log
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Each line is two numbers and a newline — easy to read back or plot in Python later.
Reading it back
1 2 3 4 5 6 7 8 | |
fscanf returns the number of items matched; EOF means end of file.
On MCU without a filesystem, the same idea applies to LittleFS on external flash or streaming over UART — you still produce a byte stream; only the API differs.
Command-line arguments — configure without recompiling
1 2 3 4 5 | |
Running ./program 115200 /dev/ttyUSB0 gives:
argc == 3argv[0]→"./program"argv[1]→"115200"argv[2]→"/dev/ttyUSB0"
All arguments are strings. Convert with atoi, strtol, or sscanf when you need numbers.
Why useful on embedded Linux: test scripts, CI, and field technicians can change baud rate or log level without flashing new firmware. getopt_long parses flags like -n 3 --until 12 in a standard way — handy for gateway daemons and host-side tools that talk to your device.
Relevant topics
Starting points
- malloc a user-sized array, fill it, free it — run under Valgrind.
- Log fake sensor data to a file; write a second program that reads and prints it.
- Pass arguments to your program and print
argc/argv. - Open your MCU linker script — find
_heap_sizeor equivalent.
Focus points
- Every code path that malloc's should free — including errors.
fopencan fail — always check; paths differ on target vs PC."w"destroys existing files — use"a"for logs.- On MCU: question every
malloc— static pools are often safer.
Key points
- Programs use stack, heap, globals, and code regions for different lifetimes.
malloc/freeare manual — no garbage collection in C.- Files persist data;
FILE *andfopen/fprintf/fscanfare the standard C API. argc/argvpass runtime configuration intomain.