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
ArrayListor Python'sdict), 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 withvoid*, 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:
#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;
}
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)):
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 namedcalculatethat points to a function accepting two integers and returning an integer (int).
#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:
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:
#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:
#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;
}
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!
#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 *));
#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:
#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;
}
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:
#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;
}
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.
-
Part 1.1: Core Syntax & Memory Foundations
Revisit fundamental pointer mechanics, stack vs. heap allocation, and fixed-width
<stdint.h>types. -
Linux Process Management & System Calls
Inspect running ELF processes, signals (
SIGTERM/SIGKILL),stracesyscall traces, and/procinternals.