Skip to content
BoKSA

Dynamic Memory and I/O

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
high address
┌─────────────────────────┐
│  Command-line args      │  "program name", "-v", etc.
├─────────────────────────┤
│  Stack                  │  local variables, return addresses
│    (grows downward)     │  int x inside main(), temporary stuff
│         ↓               │
│    free gap             │
│         ↑               │
│  Heap                   │  malloc() grabs from here
│    (grows upward)       │
├─────────────────────────┤
│  BSS                    │  global int count; (starts as zero)
├─────────────────────────┤
│  Data                   │  global int led = 13; (has initial value)
├─────────────────────────┤
│  Text (code)            │  your compiled instructions (in flash on MCU)
low address
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
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *ptr, num;

    printf("Enter number of elements: ");
    scanf("%d", &num);

    ptr = (int *)malloc(num * sizeof(int));
    if (ptr == NULL) {
        printf("ERROR: out of memory.\n");
        return 1;
    }

    for (int i = 0; i < num; i++) {
        printf("Enter element: ");
        scanf("%d", &ptr[i]);
    }

    for (int i = 0; i < num; i++)
        printf("%d ", ptr[i]);

    free(ptr);
    ptr = NULL;   /* good habit — avoids accidental reuse */
    return 0;
}

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
FILE *log = fopen("sensor_data.txt", "a");
if (log == NULL) {
    perror("fopen");
    return 1;
}

for (int i = 0; i < num_readings; i++) {
    int r1 = rand() % 100;
    int r2 = rand() % 100;
    fprintf(log, "%d %d\n", r1, r2);
}

fclose(log);

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
FILE *in = fopen("sensor_data.txt", "r");
if (in == NULL) { /* handle */ }

int d1, d2;
while (fscanf(in, "%d %d", &d1, &d2) == 2)
    printf("Read: %d - %d\n", d1, d2);

fclose(in);

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
int main(int argc, char *argv[]) {
    for (int i = 1; i < argc; i++)
        printf("arg %d: %s\n", i, argv[i]);
    return 0;
}

Running ./program 115200 /dev/ttyUSB0 gives:

  • argc == 3
  • argv[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

  1. malloc a user-sized array, fill it, free it — run under Valgrind.
  2. Log fake sensor data to a file; write a second program that reads and prints it.
  3. Pass arguments to your program and print argc / argv.
  4. Open your MCU linker script — find _heap_size or equivalent.

Focus points

  • Every code path that malloc's should free — including errors.
  • fopen can 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 / free are manual — no garbage collection in C.
  • Files persist data; FILE * and fopen/fprintf/fscanf are the standard C API.
  • argc / argv pass runtime configuration into main.