Learn C Language - Advanced Memory Techniques
Episode 17 of 24

Learn C Language - Advanced Memory Techniques

This episode deepens memory management: memory alignment, padding, and pointer arithmetic, custom allocators and a basic memory pool for fast allocation, memory sanitizer and leak detection, as well as how to write reliable low-level code for embedded and real-time.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Allocating memory with malloc is enough for almost every case, but systems that demand speed and determinism require a deeper understanding. Episode 17 discusses the advanced layers: how data is aligned in memory, how to build a custom allocator, and how to write reliable code for embedded and real-time.

Memory alignment determines how structs are laid out in memory and how much space is wasted as padding. Custom allocators enable fast, deterministic allocation for applications that must never stall. Sanitizers and leak detection ensure correctness at every level.

This is the episode closest to the hardware in this series, and its skills are used directly in episodes 18 and 21.

Memory Alignment, Padding, and Pointer Arithmetic

Why Alignment Matters

The CPU reads memory in chunks of a certain size. If an int is placed at an address that isn't a multiple of 4, access is slower or even an error on some architectures. The compiler aligns automatically, but adds padding between struct members:

Struct padding
#include <stdio.h>
#include <stddef.h>
 
struct Contoh {
    char a;
    int b;
    char c;
};
 
int main(void) {
    printf("ukuran: %zu\n", sizeof(struct Contoh));
    printf("offset b: %zu\n", offsetof(struct Contoh, b));
    return 0;
}

Even though char is 1 byte and int is 4, sizeof(struct Contoh) can produce 12 because padding aligns b to a multiple-of-4 offset. Ordering members so larger types come first reduces padding, as discussed in episode 15.

Pointer Arithmetic and Alignment

Pointer arithmetic advances an address by the size of its type. Incrementing an int * pointer by 1 advances the address by 4 bytes. To move raw memory blocks, use a pointer to unsigned char, whose step is 1 byte, or the memcpy function that handles overlap. Understand that applying pointer arithmetic to the wrong type yields undefined behavior.

Custom Allocators and Memory Pool

Why Build Your Own Allocator

malloc is versatile but carries lookup overhead and isn't deterministic — its execution time varies. Real-time systems and games often use a custom allocator that allocates once at startup, then recycles blocks quickly.

A Basic Memory Pool

A memory pool stores fixed-size blocks that are reused:

Simple memory pool
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
 
#define N_BLOK 64
#define UKURAN 256
 
static unsigned char arena[N_BLOK][UKURAN];
static int dipakai[N_BLOK];
 
void *pool_alloc(void) {
    for (int i = 0; i < N_BLOK; i++) {
        if (!dipakai[i]) {
            dipakai[i] = 1;
            return arena[i];
        }
    }
    return NULL;
}
 
void pool_free(void *p) {
    for (int i = 0; i < N_BLOK; i++) {
        if (arena[i] == p) {
            dipakai[i] = 0;
            return;
        }
    }
}

The function pool_alloc() looks for an available block linearly — deterministic and without system lookups. pool_free maps a pointer back to its block by address comparison. The arena is statically allocated, so there's no heap overhead at runtime.

Memory Pool Limitations

A fixed-size pool limits the number of blocks and their maximum size. Allocations beyond capacity return NULL, so users must design with those limits in mind from the start. This is the common trade-off in embedded: determinism is bought by sacrificing flexibility.

Memory Sanitizer and Leak Detection

Memory Check Layers

The combination of sanitizers finds almost all memory errors during development:

Thorough memory checking
gcc -g -fsanitize=address,undefined -fno-omit-frame-pointer program.c -o program
valgrind --leak-check=full --error-exitcode=1 ./program

The -fno-omit-frame-pointer flag keeps stack traces complete in sanitizer reports. valgrind --leak-check=full finds leaked blocks and checks for use-after-free. Run both at every stage of development — errors caught earlier are much cheaper.

Diagnosing Reports

AddressSanitizer reports show the wrong instruction and the call stack trace, while Valgrind reports on which line a block was allocated and leaked. Get into the habit of reading both carefully; the printed call stack almost always points straight at the root cause.

Reliable Low-Level Code for Embedded and Real-Time

Reliability Principles

Code for embedded and real-time demands rigor: no heap allocation on critical paths, no printf in time-sensitive loops, and no behavior relying on undocumented compiler optimizations. Every code path must be deterministic.

Applied Discipline

Two main practices: functions are fully responsible for the memory they allocate, and every memory access is bounds-checked. Compile with the strictest warnings:

Strict build for low-level
gcc -std=c11 -Wall -Wextra -Werror -pedantic program.c -o program

The combination -std=c11 -Wall -Wextra -Werror -pedantic forces code to obey the standard and treats all warnings as errors. This is the standard used in automotive and medical industry firmware — where a bug isn't just a cost but a safety threat.

Tip

In embedded, tools such as static analyzers (episode 11) and MISRA C help maintain discipline. Code that can be proven correct is worth more than code that is fast but doubtful.

Closing

Key takeaways:

  • Alignment lets the CPU read data efficiently; padding increases struct size.
  • Pointer arithmetic advances addresses by the size of the type.
  • A memory pool gives fast, deterministic allocation with fixed limits.
  • AddressSanitizer, UBSan, and Valgrind find almost all memory bugs.
  • Run sanitizers and leak checks at every stage of development.
  • Real-time code must be deterministic: no heap on critical paths and bounds always checked.

In the next episode 18 we will discuss system programming and OS interaction — basic system calls and wrappers, process creation with fork, exec, and wait, signals, pipes, and inter-process communication, as well as file descriptors, and select and poll for event-driven I/O.

Learn C Language - Advanced Memory Techniques | Learn C Language