Question

Why C Has the Arrow Operator (->) for Struct Pointers

cpointersdereference

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:

  1. Dereference a pointer with *.
  2. 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.drawer means: open a drawer on the cabinet already in front of you.
  • location->drawer means: 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.

Take Quiz

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:

  1. point is created with x equal to 3 and y equal to 5.
  2. &point produces the address of point.
  3. p stores that address, so p points to the Point structure.
  4. p->x is interpreted as (*p).x.
  5. *p accesses the structure that p points to: point.
  6. .x accesses that structure's x member and changes it to 10.
  7. The output is 10, 5 because the original point object 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 NULL to 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.

Take Quiz

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 *user prevents changing members through user.
  • Output parameters: A function receives struct Result *out and fills in out->... 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->next and node->data make 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

ExpressionRequired type of left sideMeaning
value.memberA structure or union valueAccess a member directly.
pointer->memberPointer to a structure or unionDereference the pointer, then access a member.
(*pointer).memberPointer to a structure or unionExactly equivalent to pointer->member.
*pointerPointer to any dereferenceable object typeAccess the object that the pointer refers to.

A useful comparison is explicit versus implicit dereferencing:

DesignExampleTrade-off
C's explicit designp->xClearly shows that a pointer is followed.
Hypothetical implicit designp.xShorter, 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.

Take Quiz

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->member dereferences ptr; do not use it if ptr is NULL, dangling, or incorrectly typed.

  • A pointer can point to a structure, but it is not itself that structure.

  • -> works for both struct and union pointers.

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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more