C Pointers and Structs: The "Under the Hood" Guide

Under the Hood: A No-Nonsense Guide to C Pointers & Structs
Pointers are the "boogeyman" of C programming for many, but they don’t have to be. Strip away the jargon, and they are essentially just variables that store the addresses of other values.
When we create a standard variable in C, like this:
int x = 5;
This tells C to take a block of memory, put the value 5 into it, and slap the name x on it. But that block of memory has an actual physical address on your RAM. Pointers store that address. They don't contain any "objective" data themselves; they only contain the map to where a value lives.
The Syntax of the Map
Here is how we write a pointer in C:
int *y = &x;
This is essentially saying: "Hey C, find the address where the value of x is stored, give me another block of memory to store that address, and slap the name y on it." The * tells C that y is a pointer.
You might be wondering, "Why do we need to specify the type for a pointer?" The truth is, the type isn't for the pointer itself—it’s for the value living at the destination. It’s telling the computer: "When you get to this address, treat the data there as an integer." (Ohhh, sweet Python... I just write x = 5 and everything just works. But we're in the trenches now.)
Accessing the Data (Dereferencing)
We can access the value at the address stored by a pointer using dereferencing. When we dereference, we're basically telling C to go to that memory address and bring back whatever is inside.
int x = 5;
int *y = &x;
int z = *y;
In that block above, we're telling the computer:
Give me 4 bytes of memory, store
5inside, and call itx.Give me 8 bytes of memory (standard for an address on 64-bit architecture), store the memory address of
xin there, and note that what's at that address is anint. Call ity.Lastly, go to the address inside
y, grab the value you find there, give me another 4 bytes of memory, and put that value intoz.
What’s the point of pointers? (Did you see that??)
You might ask, "What's the point of all these extra steps?" It boils down to two things: Mutation and Speed.
1. Mutation (Changing stuff)
When we create a function in C, the computer creates "stack memory" for that function—essentially its own private scratchpad. If we pass variables normally, C just makes copies.
// The "Copy" Problem
void swap(int a, int b){
int temp = a;
a = b;
b = temp;
// This only swaps the COPIES. The real x and y are untouched.
}
We can't modify the real data this way. But with pointers, we pass the address. The function then knows exactly where the real data lives and can change it directly.
// The Pointer Solution
void swap(int *a, int *b){
int temp = *a;
*a = *b; // Go to the address of 'a' and change it
*b = temp; // Go to the address of 'b' and change it
}
2. Speed (The heavy lifting)
Imagine a massive struct:
typedef struct Employee {
char name[50];
short access_lvl;
unsigned int id;
// Imagine 100 more fields here...
} Employee;
If we pass this struct into a function normally, the computer has to copy every single byte into the function's stack. That takes time. Instead, we just pass the address (8 bytes). The function gets a tiny map to the massive object rather than the object itself. FASTER YEAH??
The Next Level: Structs & Relationships
Things get interesting when structs start interacting. There are two main ways they relate: Hierarchy vs. Mutual Reference.
1. Hierarchy (The "Box inside a Box")
This is when one struct fully contains another. Think of a Car that comes with a Wheel installed. The Wheel is physically inside the Car's memory.
struct wheel { int size; };
struct Car {
char *model;
struct wheel myWheel; // Embedded directly!
};
Since it's physically there, we just drill down using the Dot (.) Operator:
struct Car c;
c.myWheel.size = 20; // Open car, open wheel, set size.
2. Mutual Structs (The "Infinite Loop" Trap)
But what if a Person owns a Computer, and that Computer is assigned to that Person? If we try to put Person inside Computer and Computer inside Person, the compiler creates an infinite Russian nesting doll and crashes (Infinite size!).
The Fix: We use pointers again. We don't store the actual Computer; we store the address of the Computer. We also use a Forward Declaration to tell C, "Trust me, this struct exists, you'll see it in a second."
struct Computer; // "Trust me, this is coming."
struct Person {
struct Computer *myComp; // Just a pointer (8 bytes). Safe.
};
struct Computer {
struct Person *owner; // Just a pointer. Safe.
};
The Golden Rule of Access
This is where people trip up. How do we access the data?
Do you have the object itself? Use the Dot (
.).- Example:
myCar.model
- Example:
Do you have a pointer (an address)? Use the Arrow (
->).- Example:
myCarPtr->model
- Example:
The arrow is just a shortcut for: "Go to this address *, then get the field .".
Putting it all together: The "Bridge" Logic
struct wheel;
struct Car {
char *model;
int tire;
struct wheel *Wheel;
};
struct wheel {
int diameter;
struct Car *car;
};
int main() {
struct wheel Mywheel;
struct Car Mycar;
// 1. Link them up (Slap the address of one into the pointer of the other)
Mycar.model = "Toyota";
Mywheel.diameter = 30;
Mycar.tire = 4;
Mywheel.car = &Mycar;
Mycar.Wheel = &Mywheel;
// 2. Cross the bridge
// "Start at Mycar, use the 'Wheel' pointer to cross to the wheel struct,
// then grab the diameter."
int d = Mycar.Wheel->diameter;
}
By doing this, we create pointers to each struct inside the other. We can now access and modify data remotely. It’s like giving your neighbor a key to your house so they can water your plants without actually moving in.
Happy coding!
