A struct passed to a function is copied. That is fine for a two-int point and wasteful for a 200-byte record - and it makes mutation impossible, because the function only ever sees its own copy. Pointing at a struct solves both problems at once, and it is also the only way to build structures that grow: lists, trees, graphs.
Taking a Pointer to a Struct
Nothing special is needed. & gives you the address, and the type is "pointer to struct":
All three print 3. The last two are the same operation written two ways.
Why (*ptr).x Needs Its Parentheses
You might try to drop them and write *ptr.x. That compiles to something quite different, or more often refuses to compile at all - because the member operator . has higher precedence than the dereference *:
*ptr.x // parses as *(ptr.x) -- wrong
(*ptr).x // dereference first, then take the member -- correct
ptr->x // the same thing, said shortly
ptr.x asks for a member of the pointer, and a pointer has no members. The compiler's complaint ("request for member 'x' in something not a structure or union") is a precedence bug in disguise.
Because that parenthesized form is ugly and easy to get wrong, C provides ->. In practice you will almost never see (*p).x in real code; reach for p->x and forget the long form exists, except as the explanation for what the arrow means.
Passing a Struct by Pointer
A pointer parameter gives the function the caller's actual struct, so changes stick:
Two habits worth forming here:
- Mark read-only pointers
const.const struct Point *ppromises the function will not change whatppoints at. If someone later addsp->x = 0inside, the compile fails instead of the bug shipping. It also documents intent at the call site. - Pass a pointer for large structs even when reading. A struct with a 64-byte name buffer costs 64 bytes of copying per call by value; a pointer costs 8.
Small structs are still fine to pass by value - struct Point at 8 bytes copies as cheaply as a pointer, and the by-value version cannot be NULL, which removes a whole failure mode.
Pointers Into Arrays of Structs
Pointer arithmetic works on struct arrays exactly as it does on int arrays: p + 1 advances by one whole struct, padding included.
An array name decays to a pointer to its first element, so staff is already a struct Employee * at the call. That is also why the function needs n passed separately - sizeof inside the function would measure the pointer, not the array.
Allocating a Struct on the Heap
Stack structs die at the end of their scope. To make one that outlives the function that created it - or to make as many as the input demands - allocate with malloc:
Four details in that small function are all load-bearing:
sizeof *e, notsizeof(struct Employee). It reads as "the size of whateverepoints at", so if the type ever changes the allocation follows automatically. There is no way for the two to drift apart.- Check for
NULL.mallocreturnsNULLwhen it cannot satisfy the request. Writinge->idthrough a null pointer is a segmentation fault. - Initialize every member.
mallocdoes not zero memory; the struct arrives full of garbage. (calloc(1, sizeof *e)zeroes it for you.) - Someone must
freeit. Returning an allocated pointer transfers that duty to the caller. Say so in a comment - an unclear ownership rule is how memory leaks start.
The Payoff: a Linked List Node
Here is the thing structs cannot do without pointers. A struct cannot contain itself - that would be infinitely large - but it can contain a pointer to one of its own kind, and that single trick builds every linked data structure in C.
Note that struct Node *next; refers to struct Node while struct Node is still being declared. That is legal precisely because a pointer has a known size regardless of what it points at - the compiler does not need the full definition yet. It is also the one place where you must use the struct Node tag even if you also wrote a typedef: the typedef name does not exist yet inside its own definition.
The free_list loop saves head->next before calling free(head). Reading head->next after the free is a use-after-free - the memory is no longer yours, and the value you read is whatever the allocator put there.
Common Mistakes
p->xon an uninitialized orNULLpointer. The most common cause of a crash in struct-heavy code. Initialize pointers toNULLand check before dereferencing.- Returning a pointer to a local struct.
struct Point *bad(void) { struct Point p = {1,2}; return &p; }returns the address of memory that stops existing the moment the function returns. Return the struct by value, or allocate it. - Freeing twice, or forgetting to free. Each
mallocpairs with exactly onefree. After freeing, set the pointer toNULLso a later accidental use crashes loudly instead of corrupting memory quietly. - Copying a struct that contains a pointer. Struct assignment copies the pointer value, not what it points at. Both structs now reference the same buffer, and whichever one frees it first leaves the other dangling.
Frequently Asked Questions
What does the -> operator do in C?
-> operator do in C?p->x accesses member x through the pointer p. It is exactly equivalent to (*p).x - dereference the pointer, then take the member. The arrow exists because pointers to structs are so common that the longer form clutters code.
Why does *p.x not work for a struct pointer?
*p.x not work for a struct pointer?Because . binds tighter than *. *p.x parses as *(p.x) - take member x of p (which is a pointer, not a struct) and dereference that. You need (*p).x, or just write p->x.
Should I pass a struct by value or by pointer in C?
Pass by pointer when the function must modify the caller's struct, or when the struct is large and copying it would be wasteful. Pass by value for small read-only structs, where the copy is cheap and the code is simpler. Mark read-only pointer parameters const struct T * so the compiler enforces the promise.
How do you allocate a struct with malloc in C?
struct Node *n = malloc(sizeof *n); - the sizeof *n form asks for the size of whatever n points at, so it stays correct if you later change the type. Check the result against NULL, initialize every member, and free(n) when done.