Topic 82 of 100 Structures and Unions in C Interactive Tutorial

Self-Referential Structures in C: Linking Structures to Themselves

14 mins read Last updated: 22 Aug 2026 31,800 views
C Compiler 0.00%

What is Self-Referential Structures in C? Core Concept & Direct Summary

Core Objective & Problem Statement

How do self-referential structures containing pointer members to their own type form linked lists, trees, and graphs?

Direct Answer Summary

Most structs are self-contained. A Point holds two integers and nothing else. A Date holds three. They don't refer to anything outside themselves.

But some of the most important data structures in programming are built from structs that contain a pointer to another instance of their own type. A node in a linked list holds data plus a pointer to the next node. A node in a binary tree holds data plus pointers to a left and right child. Each is defined in terms of itself.

That's a self-referential structure — and it's the mechanism that makes chains, trees, and graphs possible in C. It's also the topic where a surprising compiler error appears ("incomplete type"), so it's worth understanding the rule and not just copying the pattern.

(Suggested image: the hero graphic — three node boxes labeled with data values, each containing a "next" arrow pointing to the following node, with the last arrow pointing to a crossed-out NULL)

How Does Self-Referential Structures in C Work? In-Depth Mechanics & Architecture

The Definition

main.c
Run Online
struct Node {
    int data;
    struct Node *next;
    // ← pointer to the SAME type
}
;

Two members: some data, and a pointer to another Node. That's the entire idea.

The type name inside the struct uses the tag:struct Node *next; — not Node *next;. That's not a style preference; it's required, and the reason is the next section.

(Suggested image: the Node definition annotated — the data member, the self-pointer, and the tag syntax highlighted)


Why a Pointer Is Required (The Incomplete Type Rule)

Here's what beginners try, and why it fails:

main.c
Run Online
struct Node {
    int data;
    struct Node nested;
    // ✗ ERROR: field has incomplete type 'struct Node'
}
;

The compiler rejects this, and the reason is worth understanding properly.

To determine sizeof(struct Node), the compiler must know the size of every member. Its members are an int (4 bytes) and a struct Node. To know the size of that member, it needs sizeof(struct Node) — which is exactly what it's trying to compute. The equation has no solution.

Worse, there isn't even a finite one: if Node contained a Node, that inner Node would contain another, forever. The size would be infinite. The compiler can't lay out such a type, so it refuses.

A pointer solves it completely — because a pointer's size is known without knowing what it points at:

main.c
Run Online
struct Node *next;
// ✓ 8 bytes (on a 64-bit system) — always

The compiler doesn't need to know Node's layout to reserve space for an address. It knows every pointer is the same size. So struct Node can be laid out as: 4 bytes (int) + padding + 8 bytes (pointer) = 16 bytes, fully determined.

The general rule:a struct can't contain an instance of itself, but it can contain a pointer to itself. (The same logic applies to two structs containing each other's instances — impossible — versus their pointers — fine.)

You'll also notice the tag is unavoidable here. Inside the braces, the type isn't complete yet — no typedef name exists yet, because the typedef statement hasn't finished. So you must use the full struct Node form. That's why this is the one place where the struct keyword is genuinely required even in modern code.

(Suggested image: the infinite-size paradox side by side with the working pointer version — one showing nested boxes that never terminate, the other showing a known-size pointer)


Declaring Self-Referential Structures

Two standard forms, and both work.

Form 1: Tagged struct, then typedef

main.c
Run Online
typedef struct Node {
    int data;
    struct Node *next;
}
Node;

This declares both the tag (struct Node) and the typedef name (Node). Inside, you must use struct Node *next. Outside, you can use Node:

main.c
Run Online
Node *head = NULL;
// ✓ uses the typedefNode *n = malloc(sizeof(Node));

This is the recommended form. It gives you both names — and the tag is the one that's required inside the struct, so you need it anyway.

Form 2: Plain tagged struct (no typedef)

main.c
Run Online
struct Node {
    int data;
    struct Node *next;
}
;
struct Node *head = NULL;
// must write "struct Node" everywherestruct Node *n = malloc(sizeof(struct Node));

Legal, but verbose. All your declarations need the struct prefix.

What does NOT work

main.c
Run Online
typedef struct {
    int data;
    Node *next;
    // ✗ ERROR — 'Node' isn't defined yet!
}
Node;

This fails because the typedef name Node only comes into existence after the closing brace and the name. Inside the braces, Node doesn't exist yet — you must use struct Node, which means you must give the struct a tag. So the anonymous-struct-plus-typedef shorthand, which works fine for regular structs, simply cannot work for self-referential ones. A genuine, practical rule worth knowing.

(Suggested image: three code variants with the working ones checked and the failing one marked with the compiler error, plus the explanation "the name doesn't exist yet inside its own definition")


First Look: Two Linked Nodes

The clearest way to understand self-reference is to build a tiny chain manually:

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
    int data;
    struct Node *next;
}
Node;
int main(void) {
    // --- Allocate three nodes on the heap ---    Node *first  = malloc(sizeof(Node));    Node *second = malloc(sizeof(Node));    Node *third  = malloc(sizeof(Node));
    if (first == NULL || second == NULL || third == NULL) {
        puts("Allocation failed.");
        free(first);
        free(second);
        free(third);
        return 1;
    }
    // --- Fill in the data ---    first->data  = 10;    second->data = 20;    third->data  = 30;
    // --- Link them: this is where self-reference pays off ---    first->next  = second;          // first points at second    second->next = third;           // second points at third    third->next  = NULL;            // third points at nothing — END MARKER
    // --- Walk the chain ---    printf("The linked list: ");
    Node *current = first;
    // start at the head    while (current != NULL) {       // stop when the chain runs out        printf("%d ", current->data);        current = current->next;    // follow the pointer to the next node    }    putchar('\n');
    // --- Free everything ---    free(third);    free(second);    free(first);
    return 0;
}

Output:

Look at what first->next = second; accomplishes: first is a Node *, so the compiler knows it has an 8-byte address. And first->next is also a Node * — an address of the same known size. Self-reference is legal precisely because a pointer's size is fixed, regardless of what it points at.

Three patterns to notice:

  1. NULL marks the end. It's the same convention as '\0' in strings — a sentinel value meaning "stop here." Every traversal loop checks for it.
  2. current = current->next is the fundamental move. It's pointer-following, one link at a time.
  3. The free order here is arbitrary (all three pointers are still in scope), but in general you free from the end backward, because freeing a node destroys the pointer to its successor.

(Suggested image: the three nodes in memory, with arrows drawn between them — plus a smaller version showing what the pointers look like as a data structure diagram)


The Traversal Pattern

Walking a chain is the most common operation, and it has exactly one correct shape:

main.c
Run Online
Node *current = head;
// start from the first nodewhile (current != NULL) {
// while there's a node to visit    // ... do something with current->data ...    current = current->next;         // advance — ALWAYS last
}

Three things that must be right:

  1. Use a separate current pointer. Never walk with head itself — you'd lose the start of the list permanently:

    main.c
    Run Online
    // ✗ NEVER do this:while (head != NULL) {
    printf("%d ", head->data);
    head = head->next;
    // you've just destroyed your list reference!
    }
    // head is now NULL — the entire list is unreachable and leaked

    This is the single most common linked-list mistake. Save the head; walk with a copy.

  2. Check for NULL in the condition, and only dereference when it's non-null. while (current != NULL) guarantees current->data is safe inside.

  3. Advance last. If you advance before using current->data, you've skipped the node — or read the wrong one.

A subtle variant for deleting: you need the previous node, so the loop tracks two pointers:

main.c
Run Online
Node *prev = NULL;
Node *cur = head;
while (cur != NULL && cur->data != target) {
    prev = cur;
    cur = cur->next;
}

That two-pointer pattern is essential for deletion in a singly linked list. (Full linked-list operations — insert, delete, reverse — get their own tutorial; this page is about the self-referential mechanism that makes them possible.)

(Suggested image: the traversal with current moving along the chain while head stays parked at the first node, labeled "head must never move")


Building a List From Input

The realistic version — growing a list as data arrives:

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
    int data;
    struct Node *next;
}
Node;
// --- Insert a new node at the FRONT. Returns the new head. ---Node *prepend(Node *head, int value) {    Node *newNode = malloc(sizeof(Node));
if (newNode == NULL) {
    return head;
    // allocation failed — list unchanged    }
    newNode->data = value;
    newNode->next = head;
    // new node points at the old first node    return newNode;                     // it becomes the new head}
    // --- Print the list ---void printList(const Node *head) {    printf("[ ");    for (const Node *cur = head; cur != NULL; cur = cur->next) {        printf("%d ", cur->data);    }    puts("]");}
    // --- Free the entire list ---void freeList(Node *head) {    while (head != NULL) {        Node *next = head->next;         // save the next pointer FIRST        free(head);                      // then free the current node        head = next;                     // move on    }}
    // --- Count the nodes ---int listLength(const Node *head) {    int count = 0;    for (const Node *cur = head; cur != NULL; cur = cur->next) {        count++;    }    return count;}
    // --- Sum the values ---int listSum(const Node *head) {    int total = 0;    for (const Node *cur = head; cur != NULL; cur = cur->next) {        total += cur->data;    }    return total;}
    // --- Find a value, return the node or NULL ---const Node *listFind(const Node *head, int value) {    for (const Node *cur = head; cur != NULL; cur = cur->next) {        if (cur->data == value) {            return cur;                  // found — return the node        }    }    return NULL;                         // not found}
    int main(void) {
        Node *head = NULL;
        // an EMPTY list is just a NULL pointer
        // --- Insert values at the front (each becomes the new head) ---    for (int i = 1; i <= 5; i++) {        head = prepend(head, i * 10);        printf("After prepend(%2d): ", i * 10);        printList(head);    }
        printf("\nLength: %d\n", listLength(head));
        printf("Sum   : %d\n", listSum(head));
        // --- Search ---    int targets[] = {30, 99};    for (int i = 0; i < 2; i++) {        const Node *found = listFind(head, targets[i]);        if (found != NULL) {            printf("Found %d — next node holds %d\n",                   targets[i],                   found->next != NULL ? found->next->data : -1);        } else {            printf("%d not found\n", targets[i]);        }    }
        freeList(head);
        head = NULL;
        return 0;
    }

Sample Run:

main.c
Run Online
After prepend(10): [ 10 ]After prepend(20): [ 20 10 ]After prepend(30): [ 30 20 10 ]After prepend(40): [ 40 30 20 10 ]After prepend(50): [ 50 40 30 20 10 ]
Length: 5Sum : 150Found 30 — next node holds 2099 not found

Notes on the design:

  • head is the list. There's no separate "list object" — the head pointer is the identity of the list. An empty list is head == NULL. That's an elegant, minimalist representation.
  • prepend returns the new head — because inserting at the front changes which node is first. The caller must accept the returned pointer (head = prepend(head, ...)). Forgetting to assign the return value is a common bug: you allocate a node that's immediately orphaned and leaked.
  • freeList saves the next pointer before freeing — essential, because after free(head) you can't read head->next anymore. This is the reverse-order cleanup rule from the dangling-pointers tutorial, and getting it wrong means either a leak or a use-after-free.
  • const Node * for read-only functions — printList, listLength, listSum, and listFind all take const pointers, because none of them modify the data. The compiler enforces it.
  • found->next != NULL ? found->next->data : -1 — a chained dereference with a null guard. Without the guard, finding the last node would crash. Chained -> access always needs each link checked.

(Suggested image: the prepend operation step by step — new node allocated, its next set to the old head, head updated — with before/after diagrams)


Beyond Lists: Trees and Graphs

The mechanism generalizes immediately. A binary tree node holds two self-referential pointers:

main.c
Run Online
typedef struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
}
TreeNode;
main.c
Run Online
TreeNode *root = malloc(sizeof(TreeNode));
root->data = 50;
root->left = malloc(sizeof(TreeNode));
root->left->data = 30;
root->left->left = NULL;
root->left->right = NULL;
// and so on...

And a graph node holds a pointer to a list of neighbors — a self-referential struct connecting to a self-referential struct:

main.c
Run Online
typedef struct AdjNode {
    // one neighbor in the adjacency list    int              vertex;
    struct AdjNode *nextNeighbor;
}
AdjNode;
typedef struct {
    AdjNode *neighbors;
    // head of a linked list of neighbors
}
GraphNode;

The idea is always the same: the type contains pointer(s) to more instances of itself, forming a recursive structure of arbitrary size. That's what makes it possible for a fixed size at compile time (one node's worth of bytes) to represent an unbounded amount of data at runtime.

And it's why recursion fits so naturally here. Processing a tree is inherently self-similar — process this node, then process its children the same way:

main.c
Run Online
int treeSum(const TreeNode *node) {
    if (node == NULL) {
        return 0;
        // base case: empty subtree
    }
    return node->data + treeSum(node->left) + treeSum(node->right);
}

That's the base-case-plus-recursive-case structure from the recursion tutorial, and trees are exactly where it shines.

(Suggested image: the tree diagram — root node with left/right arrows to children, each child having its own children, terminated by NULL — next to the linked list for comparison)


Comparing the Structures

StructureNode containsLinks per nodeShape
Singly linked liststruct Node *next1Linear chain
Doubly linked liststruct Node *prev, *next2Chain both ways
Binary treestruct TreeNode *left, *right2Branching hierarchy
Graph (adjacency list)pointer to a list of neighborsvariableArbitrary network

All of them come from the same self-referential idea. Learn the mechanism once and every one of these becomes approachable.


Common Mistakes

  1. Trying to nest the struct instead of pointing to it — struct Node nested; inside Node gives "incomplete type." Use a pointer.
  2. Using the typedef name inside the definition — Node *next; fails; you need struct Node *next; because the name doesn't exist yet.
  3. Forgetting the typedef + tag combination — the anonymous-struct shorthand can't work for self-referential types.
  4. Losing the head pointer — walking with head itself instead of a current copy. The list becomes unreachable and leaks.
  5. Not assigning the return value of an insert-at-front function — prepend(head, 5); allocates a node that's immediately orphaned. It must be head = prepend(head, 5);.
  6. Forgetting to set the last node's next to NULL — the traversal loop runs past the end into garbage memory.
  7. Not checking malloc — a NULL node is then dereferenced with ->, crashing immediately.
  8. Freeing without saving next first — after free(head), head->next is gone. Save it before freeing.
  9. Freeing only the head — that leaks every other node. Walk the whole chain.
  10. Lost list on an early return — a function that allocates and returns early without freeing leaks nodes, and any logic that reassigns head carelessly orphans the rest of the chain.
  11. Modifying the list while traversing it incorrectly — deleting during traversal requires the two-pointer (prev/cur) pattern; the simple single-pointer loop will crash or skip nodes.
  12. Assuming linked lists are always better than arrays — they're not. See below.

(Suggested image: "12 Linked Structure Mistakes" checklist graphic, with #4 and #8 highlighted)


A Word of Honest Advice: Lists vs Arrays

Since this page introduces the mechanism behind linked lists, it's worth saying plainly: linked lists are not automatically better than arrays. Choose deliberately.

ArrayLinked list
Access by index✓ O(1) — direct✗ O(n) — must walk
Insert/delete at front✗ O(n) — shift everything✓ O(1) — relink
Insert/delete in middle✗ O(n)O(n) search, then O(1) relink
Memory overheadNone beyond the data8 bytes per node (the pointer)
Cache performance✓ Excellent — contiguous✗ Poor — scattered nodes
SizeFixed (or one realloc)Naturally grows and shrinks
Simplicity✓ Much simplerMore code, more bug surface

The practical decision:

  • Need index-based access, or you'll mostly append and iterate? → Array (better cache behavior usually wins)
  • Need frequent insertion/deletion at arbitrary positions, or sizes that change constantly? → Linked list
  • Uncertain? → Array. It's simpler and faster in the common cases.

In real C code, dynamic arrays (like the resizable list from the dynamic memory tutorial) are used far more often than linked lists. Linked lists matter most as a teaching structure — they're the clearest introduction to pointer-based data structures — and for specific situations (intrusive lists in kernels, LRU caches, undo chains, and of course trees and graphs).

(Suggested image: array vs linked list comparison — the array as a contiguous row, the list as scattered nodes with arrows, with the access-cost labels)


Final Thoughts

Self-referential structures are the moment structs become recursive. One node is a fixed, small amount of memory; the pointer inside it lets that fixed shape describe arbitrarily large structures. That combination — bounded type, unbounded data — is genuinely powerful, and every dynamic data structure in C depends on it.

The concept reduces to a rule and a pattern. The rule: a struct can't contain an instance of itself (infinite size), but it can contain a pointer to itself (known size) — which is why the tag form and the pointer are both mandatory. The pattern: a current pointer that starts at the head, checks for NULL, follows ->next, and advances last — while the head pointer you walked from stays untouched.

Get those two things, and you have the foundation for linked lists, trees, graphs, and a great many algorithms. From here, the interesting question stops being "how do pointers work" and becomes "which structure fits my problem."

Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
    int data;
    struct Node *next;
    // Self-referential pointer
}
Node;
int main() {
    Node *head =(Node*)malloc(sizeof(Node));
    head->data = 10;
    head->next =(Node*)malloc(sizeof(Node));
    head->next->data = 20;
    head->next->next = NULL;
    printf("Linked List Traversal:\
");
    Node *curr = head;
    while (curr != NULL) {
        printf("[%d] -> ", curr->data);
        curr = curr->next;
    }
    printf("NULL\
");
    // Free nodes
    free(head->next);
    free(head);
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Always initialize self-referential pointers to NULL to signify the end of lists and prevent wild pointer crashes during traversal.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: Why is `struct Node { int data; struct Node next; };` illegal in C?

A: It causes infinite recursive size expansion, making it impossible for the compiler to calculate `sizeof(struct Node)`. Using a pointer `struct Node *next;` resolves this because pointers have a fixed 8-byte size.
Q

Q: Where are self-referential structures used in Linux?

A: In the `task_struct` (process table), file systems (VFS inode trees), and kernel memory allocators.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations