Whenever I write data structures in C, I’m reminded that C doesn’t give me objects, references, or garbage collection out of the box the way languages like Java or Python do. Instead, I have to build the notion of “an object with fields, connected to other objects” myself, using structs and raw pointers, and I have to manage memory for those objects by hand. This topic is about exactly that: how I represent linked, object-like data using pointers in C, and — just as interestingly — how I could represent the same ideas without pointers at all, using plain arrays, in languages or environments where pointers aren’t available.
I find this a genuinely important topic because it’s the foundation underneath every linked list, tree, and graph structure I ever build in C. Understanding it well means I understand exactly what a “pointer-based object” costs in memory and time, and it also demystifies how higher-level languages implement their own object references under the hood.
History and Background
Pointers as a language-level concept trace back to early systems programming languages, most notably BCPL (developed by Martin Richards in 1966) and B (developed by Ken Thompson at Bell Labs around 1969), both direct ancestors of C. Dennis Ritchie developed C at Bell Labs in the early 1970s, refining pointer syntax and semantics into the form still recognizable today, with explicit pointer types tied to the type of data they referenced — a meaningful improvement over B’s untyped approach.
The idea of representing objects and pointer-based structures using plain arrays — sometimes called the “multiple-array” or “single-array” representation — predates widespread pointer support in early programming environments and assembly-level programming, where explicit memory addresses had to be simulated using array indices when a language or platform didn’t expose direct pointer arithmetic. This technique is still taught today, notably in Cormen, Leiserson, Rivest, and Stein’s “Introduction to Algorithms,” as a way of illustrating that “a pointer is really just an index,” a conceptual link that helps me understand pointers more deeply rather than treating them as pure magic.
Problem Statement
I want to represent structured, linked objects in memory — such as the nodes of a linked list or tree, each containing several fields and one or more references to other objects — using the low-level tools C actually gives me: structs, pointers, and manual memory allocation. I also want to understand the alternative technique of representing the same kind of linked structure using plain arrays and integer indices, for situations where true pointers aren’t available, aren’t desired (e.g., for serialization), or where I want tighter control over memory layout.
Core Concepts
- Struct: A C construct that groups multiple named fields (of potentially different types) together into a single compound type — the closest thing C has to an “object.”
- Pointer: A variable that stores a memory address, allowing indirect access to another variable or struct.
NULL: The special pointer value conventionally used to represent “points to nothing,” analogous toNILin pseudocode.- Dynamic memory allocation: Using functions like
mallocto request memory from the heap at runtime, since the number of objects I’ll need often isn’t known at compile time. - Multiple-array representation: Representing a set of objects using several parallel arrays, one per field, where a single index $i$ across all arrays represents “object number $i$,” and integer indices (instead of pointers) represent references between objects.
- Single-array (object-based) representation: Packing all the fields of one logical object into contiguous slots of a single array, so that object $k$’s fields occupy a fixed-size block starting at some computable offset.
- Free list: A linked list (or equivalent) of currently unused slots/nodes, used to efficiently allocate and deallocate objects from a fixed-size pool without needing full dynamic memory management.
How It Works
In C, I define a struct for my object type, listing its fields, including pointer fields that reference other objects of the same (or different) struct type. To create a new object, I call malloc to allocate enough memory to hold one instance of the struct, and I get back a pointer to that memory, which I then use to set the object’s fields, including linking it to other objects by assigning their addresses to my pointer fields.
For the array-based alternative, I instead pre-allocate one or more arrays, each large enough to hold up to $n$ objects’ worth of a particular field. Object number $i$’s data for a given field lives at index $i$ of that field’s array. A “pointer” in this scheme is simply an integer — the index of the referenced object — and NULL is typically represented by a reserved sentinel index, like $-1$ or $0$ (depending on convention), that can never be a valid object index.
To manage which array slots are currently “allocated” versus “free” in the array-based scheme, I maintain a free list: I link together all currently unused slots using one of the array’s own fields (commonly reusing the next field for this purpose), and I keep a single variable, free, pointing to (indexing) the head of this list. Allocating a new object means popping the head of the free list; freeing an object means pushing its index back onto the free list.
Working Principle
The pointer-based approach in C directly leverages the hardware’s ability to address memory arbitrarily: a pointer is, physically, just a memory address, and dereferencing it is a hardware-supported operation that the CPU executes essentially at the same speed regardless of where in memory the target object lives. This gives me flexible, dynamically-growable structures without needing to predict the maximum number of objects I’ll ever need.
The array-based approach, by contrast, works by pre-committing to a maximum capacity $n$ up front, in exchange for a very compact and predictable memory layout — all data lives within known array bounds, which can be advantageous for cache locality, serialization (writing the whole structure to disk as a flat block), or environments without dynamic memory allocation. The free list is what allows this approach to still support object creation and deletion efficiently, in $O(1)$ time, despite not having a general-purpose memory allocator behind it — it’s essentially a hand-rolled, specialized allocator restricted to fixed-size objects.
Mathematical Foundation
Space overhead of pointer-based objects. Each struct instance requires space for its declared fields plus, on most platforms, potential padding/alignment overhead imposed by the compiler for performance reasons. If a struct has $f$ fields with total declared size $S$ bytes, actual allocated size $S’$ often satisfies:
$$ S’ \ge S $$
due to alignment padding, and each malloc call typically carries additional hidden bookkeeping overhead $O_{malloc}$ used by the memory allocator itself, so the true per-object cost is:
$$ \text{Cost per object} = S’ + O_{malloc} $$
Space of the array-based (multiple-array) representation. For $n$ objects with $f$ fields each of size $s_1, s_2, \dots, s_f$ bytes, the total space is simply:
$$ \text{Total space} = n \sum_{i=1}^{f} s_i $$
with no per-object allocator overhead, since all memory is claimed once, up front, in a small number of large array allocations rather than $n$ separate malloc calls.
Free list operation cost. Both ALLOCATE-OBJECT and FREE-OBJECT on a free-list-managed array take:
$$ O(1) $$
time, since both operations only touch the head of the free list and the specific slot being allocated or freed — no scanning or searching is required.
Diagrams
flowchart TD
A[Request new object] --> B{free list head is NULL?}
B -->|Yes| C[Error: out of space<br/>no free slots remain]
B -->|No| D[x = free list head]
D --> E[Advance free list head to next free slot]
E --> F[Return x as the newly allocated object index]
G[Free an object x] --> H[Set x.next = current free list head]
H --> I[Set free list head = x]Pseudocode
Pointer-based linked list node operations (for comparison):
ALLOCATE-OBJECT()
if free == NIL
error "out of space"
else
x = free
free = x.next
return x
FREE-OBJECT(x)
x.next = free
free = x
Multiple-array representation initialization (n objects, using indices 1..n, with 0 or n+1 as NIL sentinel):
INIT-FREE-LIST(key, next, prev, n)
free = 1
for i = 1 to n - 1
next[i] = i + 1
next[n] = NIL
Using the array-based object system to build a simple linked list:
LIST-INSERT-ARRAY(L, key, next, prev, free, k)
x = ALLOCATE-OBJECT()
key[x] = k
next[x] = L
prev[x] = NIL
if L != NIL
prev[L] = x
L = x
return L
Step-by-Step Example
Let me trace the array-based (multiple-array) representation with capacity $n = 5$, using arrays key[], next[], and a free list.
Initial state after INIT-FREE-LIST: free = 1, and the free list threads through next[] as 1 -> 2 -> 3 -> 4 -> 5 -> NIL.
I call ALLOCATE-OBJECT() to get a slot for key 10:
x = free = 1free = next[1] = 2- I set
key[1] = 10, and this object is now “allocated” at index 1.
I call ALLOCATE-OBJECT() again for key 20:
x = free = 2free = next[2] = 3- I set
key[2] = 20.
Now I call FREE-OBJECT(1) to release the first object:
next[1] = free = 3free = 1
The free list is now 1 -> 3 -> 4 -> 5 -> NIL, and slot 1 is available for reuse. If I now call ALLOCATE-OBJECT() again for key 30:
x = free = 1free = next[1] = 3- I set
key[1] = 30, reusing the exact same array slot that previously held key10.
This demonstrates the free list correctly recycling freed slots in constant time, without needing to scan the array for an empty spot.
Time Complexity
- Allocating a new object (both pointer-based
mallocand array-based free-list allocation): $O(1)$ amortized, thoughmallocin practice has small but real constant-factor overhead from the underlying heap allocator’s bookkeeping. - Freeing an object: $O(1)$ for both representations, for the same reasons.
- Accessing a field of an object: $O(1)$ for both — a single pointer dereference in the pointer-based case, or a single array index computation in the array-based case.
- Traversing a linked structure of $k$ objects (e.g., a linked list): $\Theta(k)$ for both representations, since each step is $O(1)$ regardless of representation.
- Initializing the free list for $n$ objects (array-based only, one-time setup cost): $\Theta(n)$, since every slot must be linked into the initial free chain.
Space Complexity
Pointer-based objects use $\Theta(n)$ total space for $n$ live objects, plus per-object allocator overhead that, while technically constant per object, has a real, sometimes significant, constant factor (commonly 16–32 bytes of hidden bookkeeping per allocation on many malloc implementations). The array-based representation uses $\Theta(n)$ space as well, but for the maximum number of objects $n$ ever needed, since the arrays must be pre-sized — this can waste space if far fewer than $n$ objects end up being used simultaneously, but it avoids malloc overhead entirely and is often more cache-friendly since array elements are stored contiguously.
Correctness Analysis
Correctness of the pointer-based approach follows directly from C’s memory model: as long as I only dereference pointers that point to valid, allocated memory, and I never use a pointer after freeing the memory it points to (a “dangling pointer” or “use-after-free” bug), the structure behaves exactly as intended, since each pointer genuinely and uniquely identifies one memory location.
Correctness of the array-based approach requires an additional invariant I have to maintain carefully myself: the free list must, at all times, contain exactly the set of indices that are not currently in use as live objects, and no index should ever appear in the free list more than once (which would corrupt the free list into a cycle or cause the same slot to be “allocated” twice simultaneously). As long as ALLOCATE-OBJECT and FREE-OBJECT are the only two operations that modify the free list, and they’re implemented exactly as shown (push/pop from the head), this invariant is preserved by induction on the sequence of operations performed.
Advantages
Pointer-based (native C pointers):
- Naturally supports dynamically-sized structures without needing to predict a maximum size in advance.
- Idiomatic and directly supported by the language, with strong tooling (debuggers, sanitizers) built around it.
- Simpler mental model for most C programmers, since it mirrors how the underlying hardware actually works.
Array-based (multiple-array representation):
- Excellent cache locality, since related data is stored contiguously in memory.
- Trivial to serialize (save/load) as a flat block of memory, without needing to “fix up” pointers on reload.
- Avoids per-object heap allocator overhead, which matters in memory-constrained or performance-critical environments.
- Useful in languages or environments without true pointer support.
Disadvantages
Pointer-based:
- Susceptible to memory bugs: dangling pointers, memory leaks, double frees, and buffer overruns are all common and often hard to debug.
- Poorer cache locality in general, since dynamically allocated objects can end up scattered across memory.
- Serialization requires extra work to convert pointers into a storable, address-independent format.
Array-based:
- Requires committing to a maximum capacity $n$ up front; exceeding it requires a potentially expensive resize (reallocating and copying the whole array).
- Slightly less intuitive for programmers used to thinking in terms of direct object references rather than integer indices.
- “Pointer” arithmetic becomes index arithmetic, which can be a source of subtle off-by-one bugs, especially around sentinel value conventions (0 vs. -1 vs. n+1 for NIL).
Applications
- Nearly every dynamic data structure implemented in C — linked lists, trees, graphs — relies on pointer-based objects as the default approach.
- Memory-mapped file formats and custom serialization formats often use the array-based (index-based) representation specifically because it avoids the need to translate pointers when data is saved to or loaded from disk.
- Game engines and other performance-critical software frequently use array-based “object pools” with free lists (often called “slot maps” or “handle systems”) to get predictable memory layout and avoid heap fragmentation from frequent allocation and deallocation.
- Embedded systems programming, where dynamic memory allocation (
malloc) might be discouraged or unavailable, commonly relies on pre-allocated arrays with manual free-list management.
Implementation in C
I’ll show both representations side by side for a simple linked list of integers, so the contrast is direct and concrete.
#include <stdio.h>
#include <stdlib.h>
/* ---------- Pointer-based representation ---------- */
typedef struct PtrNode {
int key;
struct PtrNode *next;
} PtrNode;
PtrNode* ptrListInsert(PtrNode* head, int key) {
PtrNode* node = (PtrNode*)malloc(sizeof(PtrNode));
node->key = key;
node->next = head;
return node; /* new head of the list */
}
void ptrListPrint(PtrNode* head) {
while (head != NULL) {
printf("%d ", head->key);
head = head->next;
}
printf("\n");
}
void ptrListFree(PtrNode* head) {
while (head != NULL) {
PtrNode* temp = head;
head = head->next;
free(temp);
}
}
/* ---------- Array-based (multiple-array) representation ---------- */
#define CAPACITY 5
#define NIL_IDX -1
int key[CAPACITY];
int next[CAPACITY];
int freeHead;
void initFreeList(void) {
freeHead = 0;
for (int i = 0; i < CAPACITY - 1; i++) {
next[i] = i + 1;
}
next[CAPACITY - 1] = NIL_IDX;
}
int allocateObject(void) {
if (freeHead == NIL_IDX) {
printf("Error: out of space\n");
return NIL_IDX;
}
int x = freeHead;
freeHead = next[x];
return x;
}
void freeObject(int x) {
next[x] = freeHead;
freeHead = x;
}
int arrayListInsert(int listHead, int k) {
int x = allocateObject();
if (x == NIL_IDX) return listHead;
key[x] = k;
next[x] = listHead;
return x; /* new head of the list */
}
void arrayListPrint(int listHead) {
while (listHead != NIL_IDX) {
printf("%d ", key[listHead]);
listHead = next[listHead];
}
printf("\n");
}
int main(void) {
/* --- Pointer-based demo --- */
printf("Pointer-based list:\n");
PtrNode* pHead = NULL;
pHead = ptrListInsert(pHead, 30);
pHead = ptrListInsert(pHead, 20);
pHead = ptrListInsert(pHead, 10);
ptrListPrint(pHead);
ptrListFree(pHead);
/* --- Array-based demo --- */
printf("\nArray-based list:\n");
initFreeList();
int aHead = NIL_IDX;
aHead = arrayListInsert(aHead, 30);
aHead = arrayListInsert(aHead, 20);
aHead = arrayListInsert(aHead, 10);
arrayListPrint(aHead);
return 0;
}
I want to draw attention to how closely arrayListInsert mirrors ptrListInsert — the logic is nearly identical, just substituting array-index “pointers” for real ones, and substituting allocateObject()/freeObject() for malloc()/free(). This side-by-side similarity is exactly the point: once I understand the correspondence, I can translate almost any pointer-based structure into an array-based one, and vice versa.
Sample Input and Output
Input:
Pointer-based: insert 30, then 20, then 10 (each at the head)
Array-based: insert 30, then 20, then 10 (each at the head)
Output:
Pointer-based list:
10 20 30
Array-based list:
10 20 30
Both representations produce identical logical results, confirming that the array-based version faithfully reproduces the same linked-list behavior as the native pointer-based version, just using a different underlying mechanism.
Optimization Techniques
- Custom memory pool allocators: Even within pointer-based C code, I can implement a
malloc-like pool allocator on top of one large pre-allocated array (essentially blending both approaches) to reduce heap allocator overhead while keeping pointer syntax. - Struct-of-arrays instead of array-of-structs: For performance-critical code (especially with SIMD or cache-sensitive workloads), storing each field in its own separate array (as in the multiple-array representation) can improve cache performance compared to array-of-structs layouts, because unrelated fields aren’t interleaved in memory.
- Compact index types: Using a smaller integer type (like
uint16_tinstead of a full pointer orint) for array-based “pointers” can meaningfully reduce memory usage when the maximum object count fits within a smaller range. - Avoiding fragmentation: For pointer-based structures with heavy allocate/free churn, periodically compacting or using a dedicated slab allocator can reduce heap fragmentation that accumulates over a long-running program’s lifetime.
Common Mistakes
- Dangling pointers: Using a pointer after the memory it points to has been freed — a classic and dangerous C bug that array-based indices are naturally immune to (an old, stale index at worst points to a still-valid, just differently-repurposed, array slot).
- Memory leaks: Forgetting to
free()allocated objects, especially when a structure has multiple exit paths or error conditions that skip cleanup code. - Double free: Calling
free()twice on the same pointer, which corrupts the heap allocator’s internal state and can cause crashes far from the actual bug’s location. - Confusing sentinel conventions in array-based code: Using
0as both a valid index and the “NIL” sentinel simultaneously is a very common source of subtle bugs — I need to pick one consistent convention (commonly using-1ornas a reserved out-of-range sentinel) and apply it everywhere. - Forgetting to reinitialize freed slot fields: In the array-based approach, reusing a freed slot without resetting all its fields (not just the ones I explicitly set) can leave stale data from the slot’s previous occupant, causing subtle correctness bugs.
Further Reading
- Cormen, Leiserson, Rivest, and Stein, Introduction to Algorithms, Chapter 10: “Elementary Data Structures,” MIT Press: https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/
- Kernighan, Brian W., and Dennis M. Ritchie, The C Programming Language, 2nd Edition, Prentice Hall: https://www.pearson.com/en-us/subject-catalog/p/c-programming-language-the/P200000000473
- Ritchie, Dennis M., “The Development of the C Language,” ACM SIGPLAN History of Programming Languages Conference (1993): https://dl.acm.org/doi/10.1145/155360.155580
- GeeksforGeeks, “Pointers in C”: https://www.geeksforgeeks.org/c/pointers-in-c/
- MIT OpenCourseWare, “Introduction to Algorithms” lecture materials on elementary data structures: https://ocw.mit.edu/courses/6-006-introduction-to-algorithms-spring-2020/
