Question
In C, the dot (.) operator accesses a member of a structure or union value, while the arrow (->) operator accesses a member through a pointer to a structure or union.
A pointer does not expose the members of the object it points to directly. Since the compiler knows whether an expression is a pointer, why does C require a separate -> operator? Why was . not defined to automatically dereference a pointer when accessing a structure member?
Short Answer
You will learn that ptr->member is shorthand for (*ptr).member, why the dereference is intentionally explicit in C, and how this rule helps keep pointer access, types, and memory operations clear.
Concept
The arrow operator exists to combine two common operations:
- Dereference a pointer with
*. - Access a structure or union member with
..
ptr->name
means exactly:
(*ptr).name
The parentheses are required in the longer form because . has higher precedence than unary *. Without parentheses, *ptr.name would mean *(ptr.name), which is a different expression.
C deliberately keeps pointer dereferencing explicit. A pointer is a value that refers to an object; it is not the object itself. Accessing data through it can involve reading another memory location and is invalid when the pointer is NULL, dangling, or of an incompatible type.
If C made pointer.member implicitly dereference pointers, the same . syntax would sometimes mean “access a member of this value” and sometimes mean “follow an address, then access a member.” C generally avoids this kind of hidden pointer operation. The -> token makes the indirection visible while remaining shorter and easier to read than (*pointer).member.
Technically, C specifies pointers as an abstract language feature, not merely as numbers representing virtual-memory locations. On many systems an address-like representation is used, but portable C code should treat a pointer as a typed reference to an object, not as an ordinary integer.
Mental Model
Think of a structure as a filing cabinet and a pointer as a slip of paper containing the cabinet's location.
cabinet.drawermeans: open a drawer on the cabinet already in front of you.location->drawermeans: use the location slip to find the cabinet, then open its drawer.
The arrow makes the “go to the location first” step visible. C does not silently turn a location slip into the cabinet itself.
Syntax and Examples
Use . when you have a structure or union value. Use -> when you have a pointer to one.
#include <stdio.h>
struct Player {
const char *name;
int score;
};
int main(void) {
struct Player player = {"Mina", 42};
struct Player *player_ptr = &player;
printf("%s: %d\n", player.name, player.score);
printf("%s: %d\n", player_ptr->name, player_ptr->score);
return 0;
}
player is a struct Player value, so it uses ..
player_ptr has type struct Player *, so it uses ->. It points at player.
These two assignments are equivalent:
player_ptr->score = 100;
(*player_ptr).score = 100;
The arrow syntax is primarily convenient shorthand for the second form.
Step by Step Execution
Consider this program:
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point point = {3, 5};
struct Point *p = &point;
p->x = 10;
printf("%d, %d\n", point.x, point.y);
return 0;
}
Execution proceeds as follows:
pointis created withxequal to3andyequal to5.&pointproduces the address ofpoint.pstores that address, soppoints to thePointstructure.p->xis interpreted as(*p).x.*paccesses the structure thatppoints to:point..xaccesses that structure'sxmember and changes it to10.- The output is
10, 5because the originalpointobject was modified through its pointer.
Real World Use Cases
Structure pointers are extremely common in C because programs frequently pass objects by address instead of copying entire structures.
- Function updates: Pass a structure pointer so a function can modify the caller's object.
- Dynamic memory: Access an object returned by
malloc. - Linked lists and trees: Each node usually contains pointers to other nodes.
- System and library APIs: Many C APIs accept pointers to configuration structures, buffers, or context objects.
- Optional objects: A pointer can be
NULLto represent the absence of an object, when the code checks it before dereferencing.
Example with dynamically allocated memory:
struct Config {
int timeout_seconds;
};
struct Config *config = malloc(sizeof *config);
if (config != NULL) {
config->timeout_seconds = 30;
free(config);
}
Here config->timeout_seconds accesses the member in the allocated struct Config object.
Real Codebase Usage
In real C projects, -> commonly appears with validation and guard clauses. Since using -> dereferences a pointer, code should establish that the pointer is valid first.
struct User {
const char *email;
int active;
};
int user_can_receive_email(const struct User *user) {
if (user == NULL) {
return 0;
}
if (!user->active) {
return 0;
}
return user->email != NULL;
}
Common project patterns include:
- Read-only parameters:
const struct User *userprevents changing members throughuser. - Output parameters: A function receives
struct Result *outand fills inout->...fields. - Opaque types: Libraries often expose
struct Connection *while hiding the structure's definition, so callers cannot access its members directly. - Linked data structures:
node->nextandnode->datamake pointer traversal readable. - Error handling: Validate a pointer before member access, especially at public API boundaries.
For example, a linked-list traversal naturally uses ->:
for (struct Node *current = head; current != NULL; current = current->next) {
printf("%d\n", current->value);
}
Common Mistakes
Using . on a pointer
This does not compile because p is a pointer, not a structure value:
struct Point *p;
p.x = 4; // Incorrect
Use:
p->x = 4;
Forgetting parentheses in the expanded form
This is usually wrong:
*p.x = 4;
It is parsed as *(p.x), because . binds more tightly than *. Write:
(*p).x = 4;
Or, more clearly:
p->x = 4;
Dereferencing a NULL pointer
This has undefined behavior and commonly crashes:
struct Point *p = NULL;
p->x = 4; // Incorrect
Check first when NULL is possible:
if (p != NULL) {
p->x = 4;
}
Using -> on a non-pointer
struct Point point = {0, 0};
point->x = 4; // Incorrect
Use point.x because point is a structure value.
Assuming every pointer can use ->
A pointer must point to a structure or union type with the requested member. For example, an int * has no x member:
int *number;
number->x = 1; // Incorrect
Confusing a pointer member with a pointed-to member
struct Wrapper {
struct Point *point;
};
struct Wrapper wrapper;
wrapper.point is itself a pointer member. To access the pointed-to point's x, use:
wrapper.point->x
provided wrapper.point is not NULL and points to a valid struct Point.
Comparisons
| Expression | Required type of left side | Meaning |
|---|---|---|
value.member | A structure or union value | Access a member directly. |
pointer->member | Pointer to a structure or union | Dereference the pointer, then access a member. |
(*pointer).member | Pointer to a structure or union | Exactly equivalent to pointer->member. |
*pointer | Pointer to any dereferenceable object type | Access the object that the pointer refers to. |
A useful comparison is explicit versus implicit dereferencing:
| Design | Example | Trade-off |
|---|---|---|
| C's explicit design | p->x | Clearly shows that a pointer is followed. |
| Hypothetical implicit design | p.x | Shorter, but . would have two different behaviors based on the expression type. |
C chooses the explicit design and adds -> as readable shorthand. This is consistent with other C operations: indexing a pointer is written explicitly as p[i], and accessing a pointed-to value is written explicitly as *p.
Cheat Sheet
-
Use
.with a structure or union value.struct Item item; item.id = 7; -
Use
->with a pointer to a structure or union.struct Item *item_ptr = &item; item_ptr->id = 7; -
Equivalence:
ptr->member (*ptr).member -
Parentheses matter in the long form because
.has higher precedence than*. -
ptr->memberdereferencesptr; do not use it ifptrisNULL, dangling, or incorrectly typed. -
A pointer can point to a structure, but it is not itself that structure.
-
->works for bothstructandunionpointers.
FAQ
Is ptr->member exactly the same as (*ptr).member in C?
Yes. They access the same member of the same pointed-to structure or union object.
Why not always write (*ptr).member?
You can, but ptr->member is shorter and avoids the easy-to-miss parentheses required by operator precedence.
Does the arrow operator automatically check for NULL?
No. C performs no automatic NULL check. Dereferencing a NULL pointer has undefined behavior.
Can I use -> with an array?
Not directly on an array merely because it is an array. Use -> when the left expression is a pointer to a structure or union. If an array element is a structure pointer, such as items[i], then items[i]->member can be valid.
Can a pointer have members in C?
A pointer type does not have structure or union members. However, a structure can contain a pointer member, such as node->next.
Is a C pointer always an integer memory address?
Not in the C language model. Implementations often use address-like representations, but portable C code must treat pointers as pointer values with language-defined operations, not assume they are ordinary integers.
Does C++ use -> too?
Yes. C++ also uses -> for access through pointers, although C++ adds features such as overloaded operator-> for user-defined types.
Mini Project
Description
Build a small contact updater that stores a contact in a structure and updates it through a pointer. This demonstrates the everyday reason -> is useful: functions can modify a caller's structure without copying it.
Goal
Create a function that safely updates a contact's age through a struct Contact * parameter.
Requirements
Define a struct Contact with a name and age member.
Create one Contact value in main.
Write a function that accepts a pointer to struct Contact and a new age.
Return without changing anything when the pointer is NULL.
Use -> inside the function to update the age.
Print the contact before and after the update.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.