Skip to content

C Dynamic Data Structures on Linux: Struct Padding, Linked Lists & Function Pointers

Created: 2026-11-07 Last updated: 2026-11-07

Executive Summary: Unlike managed languages with built-in collection libraries (such as Java's ArrayList or Python's dict), C provides zero out-of-the-box dynamic data structures. Mastering C systems programming means building data structures tailored precisely to memory hardware constraints. This guide forms Part 2.1 (Intermediate) of our C curriculum: deconstructing CPU memory alignment and struct padding (offsetof), architecting generic doubly linked lists with void*, decoupling logic with function pointers, and engineering a production-grade circular ring buffer with watermark event callbacks.

  • Alignment & Struct Padding


    Discover invisible compiler padding bytes with offsetof(), optimize cache-line alignment, and reduce RAM footprints by 33%.

  • Function Pointers & Callbacks


    Demystify callback mechanics, decouple algorithms from data records, and construct clean, branchless Finite State Machines (FSM).

  • Generic Doubly Linked Lists


    Implement type-agnostic node storage using void*, manage head/tail references, and enforce leak-free $O(n)$ heap teardowns via callback destructors.

  • Circular Ring Buffer Lab


    Engineer a fixed-allocation FIFO ring buffer for high-throughput streaming with watermark threshold alarms and zero dynamic reallocation.


1. Memory Alignment, Hardware Boundaries & Struct Padding

Modern 64-bit processors (x86_64 and aarch64) do not read single bytes from physical RAM. Instead, the memory controller fetches data in 64-bit (8-byte) aligned words.

If a multi-byte variable (such as an 8-byte uint64_t) crosses an 8-byte boundary, the CPU must issue two memory bus cycles and perform bitwise shifting to assemble the value—degrading performance. To prevent this, the compiler automatically inserts invisible padding bytes between struct members.

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph Bad ["❌ Unoptimized Struct (24 Bytes Total)"]
        direction TB
        B1["Slot 1: uint8_t a (1 Byte)<br/>⚠️ + 7 Bytes Invisible Padding!"]
        B2["Slot 2: uint64_t b (8 Bytes)<br/>Aligned on 8-byte boundary"]
        B3["Slot 3: uint8_t c (1 Byte)<br/>⚠️ + 7 Bytes Tail Padding!"]
        B1 --> B2 --> B3
    end

    subgraph Good ["✅ Optimized Struct (16 Bytes Total)"]
        direction TB
        G1["Slot 1: uint64_t b (8 Bytes)<br/>Aligned on 8-byte boundary"]
        G2["Slot 2: uint8_t a (1 Byte) + uint8_t c (1 Byte)<br/>+ 6 Bytes Tail Padding"]
        G1 --> G2
    end

Inspecting Padding with offsetof() (<stddef.h>)

Create ~/c_struct_lab/padding_demo.c to observe padding directly:

~/c_struct_lab/padding_demo.c
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

// 1. Poorly ordered struct: Members alternate between small and large types
typedef struct {
    uint8_t  flag_a;     // 1 byte  (+ 7 bytes padding!)
    uint64_t counter;    // 8 bytes (aligned at offset 8)
    uint8_t  flag_b;     // 1 byte  (+ 7 bytes tail padding!)
} UnoptimizedNode;

// 2. Optimized struct: Sorted by descending member size
typedef struct {
    uint64_t counter;    // 8 bytes (offset 0)
    uint8_t  flag_a;     // 1 byte  (offset 8)
    uint8_t  flag_b;     // 1 byte  (offset 9, + 6 bytes tail padding)
} OptimizedNode;

int main(void) {
    printf("=== STRUCT PADDING INSPECTION (x86_64) ===\n\n");

    printf("1. UnoptimizedNode Size: %zu bytes\n", sizeof(UnoptimizedNode));
    printf("   - offset of flag_a:   %zu\n", offsetof(UnoptimizedNode, flag_a));
    printf("   - offset of counter:  %zu (7 bytes wasted before!)\n", offsetof(UnoptimizedNode, counter));
    printf("   - offset of flag_b:   %zu\n\n", offsetof(UnoptimizedNode, flag_b));

    printf("2. OptimizedNode Size:   %zu bytes\n", sizeof(OptimizedNode));
    printf("   - offset of counter:  %zu\n", offsetof(OptimizedNode, counter));
    printf("   - offset of flag_a:   %zu\n", offsetof(OptimizedNode, flag_a));
    printf("   - offset of flag_b:   %zu\n", offsetof(OptimizedNode, flag_b));

    return 0;
}
Compiling & Running Padding Inspector
gcc -std=c17 -Wall -Wextra padding_demo.c -o padding_demo
./padding_demo

Key Takeaway: By grouping members of identical size or sorting them in descending order, the struct shrinks from 24 bytes to 16 bytes—an instant 33.3% RAM savings across arrays or database cache buffers!


When to Force Zero Padding: __attribute__((packed))

In networking and operating system development, packets arriving over the wire (such as IP or Ethernet headers) follow rigid protocol specifications that contain no compiler padding. We enforce zero padding using GCC's __attribute__((packed)):

Packed Binary Network Header
typedef struct __attribute__((packed)) {
    uint8_t  version_ihl;   // 1 byte (IP version & Header Length)
    uint8_t  type_of_service;
    uint16_t total_length;   // 2 bytes
    uint16_t packet_id;      // 2 bytes
    uint16_t flags_fragment; // 2 bytes
    uint8_t  ttl;            // 1 byte
    uint8_t  protocol;       // 1 byte
    uint16_t checksum;       // 2 bytes
    uint32_t src_ip;         // 4 bytes
    uint32_t dst_ip;         // 4 bytes
} IPv4HeaderWireFormat; // Exactly 20 bytes on disk or network!

Performance Trade-off with Packed Structs

Do not use __attribute__((packed)) for general in-memory structures! On architectures like ARM, unaligned memory access can trigger hardware alignment faults or require multiple CPU bus cycles, severely slowing down execution. Use it strictly for serialization and wire protocols.


2. Function Pointers: Memory Layout, Syntax & Standard Library Mechanics

To master advanced C, one must discard the illusion that functions are abstract syntax constructs. In a compiled ELF binary, executable machine instructions reside in the .text code segment of physical RAM.

A Function Pointer is simply an unsigned 64-bit address variable that points to the entry point byte of an executable machine instruction block.

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph VirtualMemory ["Process Address Space"]
        subgraph TextSegment [".text Code Segment (Executable / Read-Only)"]
            FN_ADD["0x00401120: add()<br/>push %rbp<br/>mov %rsp, %rbp<br/>add %esi, %edi<br/>ret"]
            FN_MUL["0x00401140: multiply()<br/>push %rbp<br/>imul %esi, %edi<br/>ret"]
        end
        subgraph StackFrame ["Stack Frame (Local Variables)"]
            PTR["Variable: op_ptr<br/>Address: 0x7ffd08<br/>Value: 0x00401120"]
        end
    end

    PTR ==>|"Holds entry point address of"| FN_ADD

When the CPU invokes op_ptr(10, 20), the compiler generates an indirect jump (call *%rax in x86_64 assembly), redirecting the hardware Instruction Pointer (RIP) to 0x00401120.


1. The Operator Precedence Trap: Parsing the Syntax

In C, the function call operator () binds tighter than the dereference operator *. This leads to the infamous precedence confusion:

  • int *calculate(int a, int b);
    Declares a regular function that returns a pointer to an integer (int *).
  • int (*calculate)(int a, int b);
    Parentheses force grouping: declares a pointer variable named calculate that points to a function accepting two integers and returning an integer (int).
~/c_struct_lab/fn_ptr_basics.c
#include <stdio.h>
#include <stdint.h>

int32_t add(int32_t a, int32_t b) { return a + b; }
int32_t multiply(int32_t a, int32_t b) { return a * b; }

int main(void) {
    // 1. Raw declaration without typedef
    int32_t (*operation)(int32_t, int32_t) = add;
    printf("Direct invocation via pointer: %d\n", operation(10, 5));

    // Reassign pointer to a different function at runtime!
    operation = multiply;
    printf("Reassigned invocation via pointer: %d\n", operation(10, 5));

    // 2. Production C: Always encapsulate signatures via typedef
    typedef int32_t (*MathBinaryOp)(int32_t, int32_t);
    MathBinaryOp ops[] = { add, multiply };
    printf("Invoked from array jump table: %d\n", ops[0](20, 30));

    return 0;
}

2. Standard Library Paradigm: Generic Sorting with qsort()

The most prominent university-level demonstration of function pointers in the C Standard Library is qsort() (<stdlib.h>). It implements an in-place Quicksort algorithm capable of sorting any arbitrary data type:

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

Because qsort has no knowledge of whether the memory contains integers, strings, or complex server structures, it delegates the ordering policy to a comparison callback function:

~/c_struct_lab/qsort_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

typedef struct {
    char hostname[32];
    uint32_t ram_mb;
    double cpu_load;
} NodeMetrics;

// Comparison callback: Sort nodes by RAM descending
int compare_by_ram_desc(const void *a, const void *b) {
    const NodeMetrics *node_a = (const NodeMetrics *)a;
    const NodeMetrics *node_b = (const NodeMetrics *)b;

    if (node_b->ram_mb > node_a->ram_mb) return 1;
    if (node_b->ram_mb < node_a->ram_mb) return -1;
    return 0;
}

// Comparison callback: Sort nodes by CPU Load ascending
int compare_by_cpu_asc(const void *a, const void *b) {
    const NodeMetrics *node_a = (const NodeMetrics *)a;
    const NodeMetrics *node_b = (const NodeMetrics *)b;

    if (node_a->cpu_load > node_b->cpu_load) return 1;
    if (node_a->cpu_load < node_b->cpu_load) return -1;
    return 0;
}

int main(void) {
    NodeMetrics cluster[3] = {
        { "db01.lan",  32768, 1.85 },
        { "app01.lan", 16384, 0.42 },
        { "gw01.lan",   8192, 0.15 }
    };

    printf("=== Sort by RAM (Descending) ===\n");
    qsort(cluster, 3, sizeof(NodeMetrics), compare_by_ram_desc);
    for (int i = 0; i < 3; i++) {
        printf("  %-12s | RAM: %5u MB | Load: %.2f\n", cluster[i].hostname, cluster[i].ram_mb, cluster[i].cpu_load);
    }

    printf("\n=== Sort by CPU Load (Ascending) ===\n");
    qsort(cluster, 3, sizeof(NodeMetrics), compare_by_cpu_asc);
    for (int i = 0; i < 3; i++) {
        printf("  %-12s | RAM: %5u MB | Load: %.2f\n", cluster[i].hostname, cluster[i].ram_mb, cluster[i].cpu_load);
    }

    return 0;
}

3. Callbacks, Inversion of Control & Finite State Machines (FSM)

Why do systems engineers design software around callbacks? The core architectural rationale is Inversion of Control (IoC), colloquially known as the Hollywood Principle:

„Don't call us, we'll call you.“

In classical sequential programming, the caller controls the flow. In an event-driven architecture, a generic engine provides the mechanism (e.g., packet arrival, timer expiration), while the application injects the policy via callback pointers.

The State Machine Problem: Why switch Statements Collapse

In network protocols (TCP, HTTP, TLS), systems transition between states based on incoming events:

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    IDLE["1. IDLE<br/>Awaiting connection"]
    HANDSHAKE["2. HANDSHAKE<br/>Parsing headers"]
    STREAMING["3. STREAMING<br/>Transferring payload"]
    CLOSED["4. CLOSED<br/>Releasing resources"]

    IDLE ==>|"EV_CONNECT"| HANDSHAKE
    HANDSHAKE ==>|"EV_HEADER_OK"| STREAMING
    HANDSHAKE -.->|"EV_ERROR"| CLOSED
    STREAMING ==>|"EV_EOF"| CLOSED

A naive implementation uses nested switch(state) and switch(event) blocks. In real-world protocols with 15 states and 10 events, this produces 150 conditional branches. At runtime, this introduces severe CPU branch mispredictions, stalling hardware execution pipelines.

Table-Driven FSM: Constant-Time $O(1)$ State Dispatching

Enterprise daemons like Nginx and Linux device drivers replace branching with a State Transition Table: a two-dimensional matrix of function pointers indexed directly by state:

~/c_struct_lab/fsm_protocol_engine.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>

typedef enum {
    STATE_IDLE = 0,
    STATE_HANDSHAKE,
    STATE_STREAMING,
    STATE_CLOSED,
    STATE_COUNT
} FsmState;

typedef enum {
    EVENT_CONNECT = 0,
    EVENT_DATA_ARRIVED,
    EVENT_DISCONNECT,
    EVENT_COUNT
} FsmEvent;

// Forward declaration of Context
struct SessionContext;

// State Handler Callback signature
typedef FsmState (*FsmTransitionHandler)(struct SessionContext *ctx);

typedef struct SessionContext {
    int session_id;
    FsmState current_state;
    uint32_t bytes_transferred;
} SessionContext;

// Concrete Transition Handlers
FsmState on_connect(SessionContext *ctx) {
    printf("[Session %d] Connection established -> Moving to HANDSHAKE\n", ctx->session_id);
    return STATE_HANDSHAKE;
}

FsmState on_data(SessionContext *ctx) {
    ctx->bytes_transferred += 4096;
    printf("[Session %d] Streamed 4096 bytes (Total: %u) -> STREAMING\n", ctx->session_id, ctx->bytes_transferred);
    return STATE_STREAMING;
}

FsmState on_disconnect(SessionContext *ctx) {
    printf("[Session %d] Disconnection signal received -> CLOSED\n", ctx->session_id);
    return STATE_CLOSED;
}

FsmState on_invalid(SessionContext *ctx) {
    printf("[Session %d] Invalid event in state %d! Dropping.\n", ctx->session_id, ctx->current_state);
    return ctx->current_state; // Ignore invalid transitions
}

// Two-dimensional Transition Matrix: State x Event -> Transition Handler
// Constant time O(1) table lookup with ZERO CPU branching!
static const FsmTransitionHandler transition_table[STATE_COUNT][EVENT_COUNT] = {
    [STATE_IDLE]      = { [EVENT_CONNECT] = on_connect,    [EVENT_DATA_ARRIVED] = on_invalid, [EVENT_DISCONNECT] = on_invalid },
    [STATE_HANDSHAKE] = { [EVENT_CONNECT] = on_invalid,    [EVENT_DATA_ARRIVED] = on_data,    [EVENT_DISCONNECT] = on_disconnect },
    [STATE_STREAMING] = { [EVENT_CONNECT] = on_invalid,    [EVENT_DATA_ARRIVED] = on_data,    [EVENT_DISCONNECT] = on_disconnect },
    [STATE_CLOSED]    = { [EVENT_CONNECT] = on_invalid,    [EVENT_DATA_ARRIVED] = on_invalid, [EVENT_DISCONNECT] = on_invalid }
};

// Event Dispatcher Engine
void fsm_dispatch(SessionContext *ctx, FsmEvent event) {
    FsmTransitionHandler action = transition_table[ctx->current_state][event];
    if (action) {
        ctx->current_state = action(ctx);
    }
}

int main(void) {
    SessionContext session = { .session_id = 101, .current_state = STATE_IDLE, .bytes_transferred = 0 };

    printf("=== FINITE STATE MACHINE ENGINE INITIALIZED ===\n\n");
    fsm_dispatch(&session, EVENT_CONNECT);       // IDLE -> HANDSHAKE
    fsm_dispatch(&session, EVENT_DATA_ARRIVED);  // HANDSHAKE -> STREAMING
    fsm_dispatch(&session, EVENT_DATA_ARRIVED);  // STREAMING -> STREAMING
    fsm_dispatch(&session, EVENT_DISCONNECT);    // STREAMING -> CLOSED

    printf("\nSession %d reached terminal state (%d). Total bytes: %u\n", 
           session.session_id, session.current_state, session.bytes_transferred);
    return 0;
}
Compiling & Running the FSM Engine
gcc -std=c17 -Wall -Wextra fsm_protocol_engine.c -o fsm_protocol_engine
./fsm_protocol_engine

4. Generic Doubly Linked Lists

Arrays require contiguous memory blocks and cannot resize efficiently without reallocating and copying all elements. A Doubly Linked List allocates each element dynamically on the heap and connects them via forward (next) and backward (prev) pointers.

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart LR
    HEAD["Head Pointer"] --> N1["Node A<br/>data: 0x10<br/>prev: NULL"]
    N1 <==>|"next / prev"| N2["Node B<br/>data: 0x20"]
    N2 <==>|"next / prev"| N3["Node C<br/>data: 0x30<br/>next: NULL"]
    TAIL["Tail Pointer"] --> N3

Implementation: Generic Linked List (dlist.h & dlist.c)

We use a generic void *data pointer so our list can hold integers, strings, or complex structs without duplicating code. Notice how the destructor function dlist_destroy utilizes the function pointer callback pattern we mastered in Sections 2 and 3 to cleanly deallocate user data!

~/c_struct_lab/dlist.h
#pragma once

#include <stddef.h>
#include <stdbool.h>

typedef struct DListNode {
    void *data;
    struct DListNode *prev;
    struct DListNode *next;
} DListNode;

typedef struct {
    DListNode *head;
    DListNode *tail;
    size_t count;
} DList;

// Function Prototypes
DList *dlist_create(void);
bool dlist_push_back(DList *list, void *data);
bool dlist_push_front(DList *list, void *data);
void *dlist_pop_front(DList *list);
void dlist_destroy(DList *list, void (*free_data_fn)(void *));
~/c_struct_lab/dlist.c
#include <stdlib.h>
#include "dlist.h"

DList *dlist_create(void) {
    DList *list = malloc(sizeof(DList));
    if (!list) return NULL;
    list->head = NULL;
    list->tail = NULL;
    list->count = 0;
    return list;
}

bool dlist_push_back(DList *list, void *data) {
    if (!list) return false;

    DListNode *node = malloc(sizeof(DListNode));
    if (!node) return false;

    node->data = data;
    node->next = NULL;
    node->prev = list->tail;

    if (list->tail) {
        list->tail->next = node;
    } else {
        list->head = node;
    }
    list->tail = node;
    list->count++;
    return true;
}

bool dlist_push_front(DList *list, void *data) {
    if (!list) return false;

    DListNode *node = malloc(sizeof(DListNode));
    if (!node) return false;

    node->data = data;
    node->prev = NULL;
    node->next = list->head;

    if (list->head) {
        list->head->prev = node;
    } else {
        list->tail = node;
    }
    list->head = node;
    list->count++;
    return true;
}

void *dlist_pop_front(DList *list) {
    if (!list || !list->head) return NULL;

    DListNode *node = list->head;
    void *data = node->data;

    list->head = node->next;
    if (list->head) {
        list->head->prev = NULL;
    } else {
        list->tail = NULL;
    }

    free(node);
    list->count--;
    return data;
}

void dlist_destroy(DList *list, void (*free_data_fn)(void *)) {
    if (!list) return;

    DListNode *current = list->head;
    while (current) {
        DListNode *next = current->next;
        if (free_data_fn && current->data) {
            free_data_fn(current->data);
        }
        free(current);
        current = next;
    }
    free(list);
}

Complete Usage Example: Consuming the Doubly Linked List

Create ~/c_struct_lab/dlist_demo.c to exercise all list capabilities with structured records:

~/c_struct_lab/dlist_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "dlist.h"

typedef struct {
    int id;
    char hostname[32];
    uint32_t ram_mb;
} ServerNode;

// Custom cleanup callback for heap-allocated payload
void free_server_node(void *payload) {
    ServerNode *node = (ServerNode *)payload;
    free(node);
}

ServerNode *create_server(int id, const char *hostname, uint32_t ram) {
    ServerNode *srv = malloc(sizeof(ServerNode));
    if (!srv) return NULL;
    srv->id = id;
    snprintf(srv->hostname, sizeof(srv->hostname), "%s", hostname);
    srv->ram_mb = ram;
    return srv;
}

int main(void) {
    printf("=== GENERIC DOUBLY LINKED LIST DEMO ===\n\n");

    // 1. Initialize generic doubly linked list
    DList *cluster = dlist_create();

    // 2. Append elements to tail with dlist_push_back
    dlist_push_back(cluster, create_server(1, "srv01.prod.lan", 16384));
    dlist_push_back(cluster, create_server(2, "srv02.prod.lan", 32768));

    // 3. Prepend high-priority element to head with dlist_push_front
    dlist_push_front(cluster, create_server(0, "gateway.prod.lan", 8192));

    printf("List contains %zu nodes.\n", cluster->count);

    // 4. Forward traversal (Head -> Tail via next pointers)
    printf("\nForward Traversal (Head -> Tail):\n");
    DListNode *curr = cluster->head;
    while (curr) {
        ServerNode *s = (ServerNode *)curr->data;
        printf("  [#%d] %-18s | RAM: %5u MB\n", s->id, s->hostname, s->ram_mb);
        curr = curr->next;
    }

    // 5. Backward traversal (Tail -> Head via prev pointers)
    printf("\nBackward Traversal (Tail -> Head):\n");
    curr = cluster->tail;
    while (curr) {
        ServerNode *s = (ServerNode *)curr->data;
        printf("  [#%d] %-18s | RAM: %5u MB\n", s->id, s->hostname, s->ram_mb);
        curr = curr->prev;
    }

    // 6. Pop head element
    printf("\nPopping head node with dlist_pop_front...\n");
    ServerNode *popped = (ServerNode *)dlist_pop_front(cluster);
    if (popped) {
        printf("  Popped: [#%d] %s (Remaining count: %zu)\n", popped->id, popped->hostname, cluster->count);
        free_server_node(popped);
    }

    // 7. Safely destroy list and all remaining heap nodes
    printf("\nDestroying list and freeing all allocated memory...\n");
    dlist_destroy(cluster, free_server_node);
    printf("Cluster list destroyed cleanly with zero memory leaks.\n");

    return 0;
}
Compiling & Running the Doubly Linked List Demo
gcc -std=c17 -Wall -Wextra dlist.c dlist_demo.c -o dlist_demo
./dlist_demo

5. Hands-On Lab: Generic High-Performance Circular Ring Buffer

A Ring Buffer (Circular Buffer) is a fixed-size queue with head and tail pointers that wrap around the end of the array. It provides: 1. Zero dynamic allocations during runtime operation ($O(1)$ push and pop). 2. Guaranteed cache locality (elements are stored in contiguous memory). 3. Ideal for streaming I/O: Audio buffers, network packet queues, and inter-thread channels.

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph Ring ["Fixed Array Memory (Capacity: 8)"]
        direction LR
        S0["[0]"] --- S1["[1]"] --- S2["[2]"] --- S3["[3]"] --- S4["[4]"] --- S5["[5]"] --- S6["[6]"] --- S7["[7]"]
    end

    TAIL["Tail Pointer (Read Index)<br/>Advances on Pop"] -.-> S1
    HEAD["Head Pointer (Write Index)<br/>Advances on Push"] ==> S5

Implementing ring_buffer.c with Watermark Alarms

Create ~/c_struct_lab/ring_buffer.c:

~/c_struct_lab/ring_buffer.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

typedef struct {
    uint8_t *buffer;
    size_t capacity;
    size_t element_size;
    size_t head;
    size_t tail;
    size_t count;
    // Callback when buffer reaches >= 80% watermark capacity
    void (*on_watermark_alert)(size_t current_count, size_t capacity);
} RingBuffer;

RingBuffer *rb_create(size_t capacity, size_t element_size, void (*alert_fn)(size_t, size_t)) {
    RingBuffer *rb = malloc(sizeof(RingBuffer));
    if (!rb) return NULL;

    rb->buffer = malloc(capacity * element_size);
    if (!rb->buffer) {
        free(rb);
        return NULL;
    }

    rb->capacity = capacity;
    rb->element_size = element_size;
    rb->head = 0;
    rb->tail = 0;
    rb->count = 0;
    rb->on_watermark_alert = alert_fn;
    return rb;
}

bool rb_push(RingBuffer *rb, const void *item) {
    if (rb->count == rb->capacity) {
        return false; // Buffer is full!
    }

    void *dest = rb->buffer + (rb->head * rb->element_size);
    memcpy(dest, item, rb->element_size);

    rb->head = (rb->head + 1) % rb->capacity; // Circular wrap
    rb->count++;

    // Check 80% watermark threshold
    if (rb->on_watermark_alert && (rb->count * 100 / rb->capacity) >= 80) {
        rb->on_watermark_alert(rb->count, rb->capacity);
    }
    return true;
}

bool rb_pop(RingBuffer *rb, void *dest) {
    if (rb->count == 0) {
        return false; // Buffer is empty!
    }

    void *src = rb->buffer + (rb->tail * rb->element_size);
    memcpy(dest, src, rb->element_size);

    rb->tail = (rb->tail + 1) % rb->capacity; // Circular wrap
    rb->count--;
    return true;
}

void rb_destroy(RingBuffer *rb) {
    if (!rb) return;
    free(rb->buffer);
    free(rb);
}

// Watermark event listener
void my_buffer_alert(size_t current, size_t total) {
    printf("⚠️  [ALERT: BUFFER WATERMARK] Usage at %zu / %zu slots (>= 80%%)!\n", current, total);
}

int main(void) {
    // Create ring buffer for 10 integers with watermark alarm
    RingBuffer *rb = rb_create(10, sizeof(int), my_buffer_alert);

    printf("=== CIRCULAR RING BUFFER DEMO ===\n");

    // Push 9 items to cross 80% watermark (8 / 10 = 80%)
    for (int i = 1; i <= 9; i++) {
        int val = i * 10;
        printf("Pushing item: %d\n", val);
        rb_push(rb, &val);
    }

    printf("\nPopping 4 items from buffer:\n");
    for (int i = 0; i < 4; i++) {
        int popped_val = 0;
        if (rb_pop(rb, &popped_val)) {
            printf("Popped: %d\n", popped_val);
        }
    }

    printf("\nPushing 3 new items into circular slots:\n");
    for (int i = 100; i <= 102; i++) {
        printf("Pushing: %d\n", i);
        rb_push(rb, &i);
    }

    printf("\nClearing remaining items:\n");
    int remaining = 0;
    while (rb_pop(rb, &remaining)) {
        printf("Extracted: %d\n", remaining);
    }

    rb_destroy(rb);
    printf("Buffer safely destroyed with zero leaks.\n");
    return 0;
}
Compiling & Running Ring Buffer Lab
gcc -std=c17 -Wall -Wextra ring_buffer.c -o ring_buffer
./ring_buffer

6. Architectural Comparison: Linked List vs. Ring Buffer

Metric Doubly Linked List Circular Ring Buffer
Memory Allocation Dynamic ($1 \times \text{malloc}$ per node) Single contiguous block allocated up-front
Insertion / Deletion $O(1)$ at head and tail $O(1)$ at head and tail
Memory Overhead High ($16\text{ bytes}$ for next + prev pointers per node!) Zero per-element pointer overhead
CPU Cache Locality Poor (nodes scattered across heap) Superb (contiguous sequential RAM cells)
Max Capacity Bound only by total system RAM Fixed predefined capacity
Best Used For Dynamic collections with unpredictable size High-frequency queues, audio, streaming I/O

Where to Go Next

Continue your journey through the C Systems Programming curriculum:

  • Part 2.2: POSIX File I/O & Syscalls


    Master raw kernel file descriptors, unbuffered I/O, atomic flock locking, and Unix daemonization.

    POSIX Syscalls Guide

  • Part 1.1: Core Syntax & Memory Foundations


    Revisit fundamental pointer mechanics, stack vs. heap allocation, and fixed-width <stdint.h> types.

    Core Foundations Guide

  • Linux Process Management & System Calls


    Inspect running ELF processes, signals (SIGTERM/SIGKILL), strace syscall traces, and /proc internals.

    Process Management