Structures & Pointers
Structs are frequently accessed through pointers rather than by value — for the same reasons pointers matter everywhere else in C: avoiding copies, and letting a function modify the caller's data. A pointer to a struct works like a pointer to anything else, but C gives it its own dedicated member-access syntax: the arrow operator.
Declaring a pointer to a struct
struct Point {
int x;
int y;
};
struct Point pt = {3, 4};
struct Point *p = &pt; /* p holds the address of pt */Two ways to reach a member through a pointer
Since p is a pointer, not a struct itself, the dot operator cannot be applied to p directly — you first have to dereference it to get back to the actual struct, then use dot. That reads awkwardly because . binds tighter than *, so the dereference needs parentheses.
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point pt = {3, 4};
struct Point *p = &pt;
/* Form 1: explicit dereference, then dot. Parentheses are required. */
printf("(*p).x = %d\n", (*p).x);
/* Form 2: the arrow operator does the same thing, more cleanly. */
printf("p->x = %d\n", p->x);
return 0;
}(*p).x = 3 p->x = 3
p->x is exactly equivalent to (*p).x — the arrow operator exists purely as convenient, less error-prone sugar for "dereference, then access a member." In practice, virtually all real C code uses -> and reserves (*p).x for explaining what -> actually does.
Why pass structs by pointer
Passing a struct to a function by value copies every single byte of that struct onto the function's stack frame. For a small struct like Point (two ints), that is cheap. For a struct holding several arrays, buffers, or many fields, the copy can be a real, measurable performance cost — and it happens every time the struct is passed, including when it's passed back out as a return value. Passing a pointer instead copies only the address (typically 4 or 8 bytes), regardless of how large the pointed-to struct is.
struct BigRecord {
char buffer[4096];
double samples[500];
int metadata[128];
};
/* Copies ~8KB+ every call — expensive and wasteful. */
void processByValue(struct BigRecord record) { /* ... */ }
/* Copies only a pointer, regardless of BigRecord's size. */
void processByPointer(struct BigRecord *record) { /* ... */ }Modifying the caller's struct through a pointer
This directly follows from how pass-by-reference works in C: since the function receives the actual address of the caller's struct, writes made through that pointer are visible to the caller after the function returns. This is the standard way to write functions that update a struct "in place."
#include <stdio.h>
struct Point {
int x;
int y;
};
void translate(struct Point *p, int dx, int dy) {
p->x += dx;
p->y += dy;
}
int main(void) {
struct Point pt = {0, 0};
translate(&pt, 5, 3);
printf("pt = (%d, %d)\n", pt.x, pt.y);
return 0;
}pt = (5, 3)
Without the pointer, translate would only be able to modify its own local copy of the struct, and pt in main would remain unchanged after the call returns.
Syntax | Meaning | Typical usage |
|---|---|---|
| Access a member of a struct value directly | When you have an actual struct variable, not a pointer |
| Dereference the pointer, then access the member | Rare in practice — mostly used to explain |
| Shorthand for | The idiomatic way to access members through a pointer |
p->memberis shorthand for(*p).member— both mean the same thing.Passing a pointer avoids copying the whole struct, which matters as structs grow.
A function that receives a pointer can modify the caller's original struct.
Use
const struct T *for pointer parameters that should only read, never write.