C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
62: Nested and Self-Referential Structures
At this point in the course, you're comfortable with structs as a way to group related data. But real-world data rarely fits into a neat, flat box. Usually, data is either hierarchical—like a user profile that contains an address—or recursive, like a file system where a folder can contain other folders. This is where nested and self-referential structures come in.
The Infinite Size Paradox
I want to show you a mistake I see almost every beginner make when they first try to build a linked list. They think of a "Node" as something that simply contains a value and another "Node." It feels logically sound, so they write something like this:
struct Node {
int data;
struct Node next; // This is the mistake
};
If you try to compile this, your compiler is going to throw a fit. Why? Because the compiler needs to know exactly how many bytes a struct Node occupies in memory. But if a Node contains a Node, which contains a Node, and so on, the structure is infinitely large. It's a recursive definition that never terminates. You've essentially told C to create a box that must be large enough to hold itself, which is a physical impossibility in memory.
Breaking the Loop with Pointers
The fix is subtle but fundamental: we don't store the actual structure inside itself; we store a pointer to it. This is the core of a self-referential structure.
struct Node {
int data;
struct Node *next; // This works perfectly
};
Now, the compiler is happy. Why? Because regardless of what struct Node looks like, a pointer is always a fixed size (usually 4 or 8 bytes). We aren't nesting the box; we're putting a note inside the box that tells us where to find the next box. This allows us to build chains of data—linked lists, trees, or graphs—that can grow and shrink at runtime on the heap, rather than being locked into a fixed size on the stack.
Rigid Nesting vs. Dynamic Linking
Self-reference is for when you don't know how many elements you'll have. But what about when you do know the relationship is fixed? For example, let's say we're building a game engine. Every Entity has a Position. You could just put the X, Y, and Z coordinates directly in the Entity struct, but that's messy. Instead, we nest a Vector3 struct inside the Entity struct.
struct Vector3 {
float x, y, z;
};
struct Entity {
int id;
char name[32];
struct Vector3 position; // Nested structure
};
The trade-off here is about memory layout. By nesting Vector3 directly (by value) rather than using a pointer, the position data is stored contiguously with the id and name. When the CPU loads an Entity into the cache, it grabs the position data at the same time. If we had used a pointer to a Vector3, the CPU would have to jump to a different location in memory, which is a "cache miss" and slows your program down. I generally recommend nesting by value whenever the relationship is "is-a-part-of" and the size is fixed.
When the Nesting Gets Too Deep
While nesting is great for organization, be careful not to create a "Matryoshka doll" of structs that is five or six levels deep. If you find yourself writing player->stats->attributes->strength->current_value, you've gone too far. Not only is it a pain to type, but you're creating a rigid dependency chain. If you ever need to move strength to a different part of the system, you'll have to rewrite every single line of code that accesses it. If the data doesn't have a strict, permanent ownership relationship, stick to pointers and keep your hierarchy shallow.
📋 Practical Task
Implementing a Simple Undo History Stack
In this exercise, you will build a basic "Undo" system for a text editor. Instead of storing a full document, we will store "Change" objects. Each change will point to the change that happened immediately before it, creating a self-referential chain (a stack).
Your task:
- Define a struct called
Changethat contains an integerchange_idand a pointer to the previousChangestruct. - Write a function
push_changethat takes the current "head" of the history and a new ID, allocates a newChangenode on the heap, links it to the previous head, and returns the new head. - Write a function
pop_changethat removes the most recent change and returns the previous head to the caller, ensuring you properlyfree()the memory of the removed node. - In
main, simulate three edits (IDs 101, 102, 103), then "undo" two of them, and print the ID of the current active change.
There are no comments for now.