Skip to content

Pointers to Structures in C

Updated 8 min read Aimerscrew

In the last lesson, you saw that passing a struct to a function hands over a copy, so the function can't change the original. Pointers to structures in C solve that problem. By passing the struct's address instead, a function can update the real thing, and your program skips copying the whole record every time. In this lesson, you'll learn the arrow operator ->, write functions that modify structs, protect read-only data with const, and create structs on the heap with malloc().

🌍 Real-life example

When you deposit money at a bank, the teller doesn't photocopy your account, add money to the photocopy and hand it back. They look up your account by its account number and update the real balance. A struct pointer is that account number: it tells a function where the real record lives, so changes stick.

What is a pointer to a structure?

A pointer to a structure is a pointer variable that holds the address of a struct. You declare it with the struct type and a star, and fill it with the & (address-of) operator, exactly as with pointers to ints:

struct Account {
    char owner[30];
    double balance;
};

struct Account acc = {"Priya", 250.00};
struct Account *p = &acc;   /* p holds the address of acc */

Now p doesn't contain an account. It contains the location of one.

The arrow operator -> vs (*p).field

To reach a member through a pointer, you could first dereference the pointer and then use the dot: (*p).balance. The parentheses are required because the dot binds more tightly than the star. Since that's awkward to type, C offers a shortcut: the arrow operator ->. Writing p->balance means exactly the same thing as (*p).balance.

#include <stdio.h>

struct Account {
    char owner[30];
    double balance;
};

int main(void) {
    struct Account acc = {"Priya", 250.00};
    struct Account *p = &acc;

    printf("%s has $%.2f\n", (*p).owner, (*p).balance);
    printf("%s has $%.2f\n", p->owner, p->balance);
    return 0;
}
Priya has $250.00
Priya has $250.00
You haveUseExample
A struct variableDot .acc.balance
A pointer to a structArrow ->p->balance
A pointer, the long wayStar, then dot(*p).balance

How the arrow operator updates a struct

Because p holds the address of acc, writing through p->balance changes acc.balance itself. There is only one account; the pointer is just another way to reach it.

Animation: acc is a struct at example address 0x1000 holding owner Priya and balance 250.00; p at 0x2000 stores 0x1000 and points to acc; p->balance += 100 follows the arrow and changes the balance to 350.00
p stores the address of acc. p->balance follows the arrow and updates the real balance. (Addresses are made-up examples.)

Passing a struct pointer to a function to modify it

This is the most common use of struct pointers. First, here's what goes wrong with pass-by-value: the function updates its own copy, and the caller's balance never changes.

#include <stdio.h>

struct Account {
    char owner[30];
    double balance;
};

void deposit_copy(struct Account acc, double amount) {
    acc.balance += amount;   /* changes the copy only */
    printf("Inside function: $%.2f\n", acc.balance);
}

int main(void) {
    struct Account acc = {"Priya", 250.00};

    deposit_copy(acc, 100.00);
    printf("Balance: $%.2f\n", acc.balance);
    return 0;
}
Inside function: $350.00
Balance: $250.00

Now pass the address with &acc and accept a struct Account *. Inside the function, use -> to reach the real members:

#include <stdio.h>

struct Account {
    char owner[30];
    double balance;
};

void deposit(struct Account *acc, double amount) {
    acc->balance += amount;
}

int withdraw(struct Account *acc, double amount) {
    if (amount > acc->balance) {
        return 0;   /* not enough money */
    }
    acc->balance -= amount;
    return 1;
}

int main(void) {
    struct Account acc = {"Priya", 250.00};

    deposit(&acc, 100.00);
    printf("After deposit: $%.2f\n", acc.balance);

    if (!withdraw(&acc, 500.00)) {
        printf("Withdrawal of $500.00 declined\n");
    }

    withdraw(&acc, 75.50);
    printf("After withdrawal: $%.2f\n", acc.balance);
    return 0;
}
After deposit: $350.00
Withdrawal of $500.00 declined
After withdrawal: $274.50

This is the same idea as pass by reference with plain ints, applied to a whole record.

📝 Note

Pointers also save work. A struct can be large, say a customer record with a long address and order history. Passing it by value copies every byte on each call. Passing a pointer copies only an address, which is typically 8 bytes on a 64-bit system, no matter how big the struct is.

const struct pointers: read without changing

Sometimes a function only needs to read a struct, like printing a statement. You still want the speed of a pointer, but you don't want the function to change anything by accident. Mark the pointer const:

#include <stdio.h>

struct Account {
    char owner[30];
    double balance;
};

void print_statement(const struct Account *acc) {
    printf("Owner: %s\n", acc->owner);
    printf("Balance: $%.2f\n", acc->balance);
}

int main(void) {
    struct Account acc = {"Diego", 1200.75};

    print_statement(&acc);
    return 0;
}
Owner: Diego
Balance: $1200.75

If print_statement tried acc->balance = 0;, the compiler would stop you:

error: assignment of member 'balance' in read-only object

💡 Tip

A good habit: functions that change a struct take struct T *, and functions that only read it take const struct T *. Anyone reading the function's first line instantly knows whether their data is safe.

Looping over an array of structs with a pointer

An array name acts like a pointer to its first element, so a struct pointer can walk through an array of structs. Each a++ moves forward by one whole struct, as you saw in pointer arithmetic.

#include <stdio.h>

struct Account {
    char owner[30];
    double balance;
};

int main(void) {
    struct Account bank[3] = {
        {"Ana", 120.50},
        {"Ben", 75.25},
        {"Chloe", 300.00}
    };
    double total = 0.0;

    for (struct Account *a = bank; a < bank + 3; a++) {
        printf("%s: $%.2f\n", a->owner, a->balance);
        total += a->balance;
    }
    printf("Total held: $%.2f\n", total);
    return 0;
}
Ana: $120.50
Ben: $75.25
Chloe: $300.00
Total held: $495.75

Dynamically allocated structs with malloc

When you don't know in advance how many records you'll need, you create structs on the heap with malloc(). It returns a pointer, so -> is the natural way to use the new struct:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Account {
    char owner[30];
    double balance;
};

int main(void) {
    struct Account *acc = malloc(sizeof *acc);
    if (acc == NULL) {
        printf("Out of memory\n");
        return 1;
    }

    strcpy(acc->owner, "Tom");
    acc->balance = 40.00;
    acc->balance += 60.00;

    printf("%s has $%.2f\n", acc->owner, acc->balance);

    free(acc);
    acc = NULL;
    return 0;
}
Tom has $100.00

sizeof *acc asks for "the size of whatever acc points to", so the line stays correct even if you later rename the struct. Memory from malloc() isn't initialized, so set every member before reading it, and always free() it when you're done to avoid memory leaks. You'll build whole chains of heap structs in the linked list lesson.

Common mistakes with struct pointers

1. Using a dot on a pointer

Writing p.balance when p is a pointer fails. GCC even suggests the fix: 'p' is a pointer; did you mean to use '->'?

2. Forgetting the parentheses in (*p).balance

*p.balance means *(p.balance), because the dot runs before the star. It doesn't compile. Use p->balance and the problem disappears.

3. Using a pointer that points nowhere

struct Account *p;      /* not initialized */
p->balance = 10.0;      /* undefined behavior */

An uninitialized pointer holds a random address, so this may crash or silently corrupt memory. Always point it at a real struct (&acc) or at memory from malloc() first.

4. Returning the address of a local struct

A function that returns &local_account hands back an address that stops being valid the moment the function returns. Return the struct by value, or allocate it with malloc() and let the caller free() it.

Practice exercise

🏋️ Try it yourself

Create a struct Player with a name, a score and a level. Write void add_points(struct Player *pl, int points) that adds the points and, when the score reaches level * 100 or more, increases the level by one and prints a message. Start "Zoe" at score 0, level 1, add 60, then 50, then 30 points, and print her final stats.

Show solution
#include <stdio.h>

struct Player {
    char name[20];
    int score;
    int level;
};

void add_points(struct Player *pl, int points) {
    pl->score += points;
    if (pl->score >= pl->level * 100) {
        pl->level++;
        printf("%s reached level %d!\n", pl->name, pl->level);
    }
}

int main(void) {
    struct Player hero = {"Zoe", 0, 1};

    add_points(&hero, 60);
    add_points(&hero, 50);
    add_points(&hero, 30);

    printf("%s: score %d, level %d\n", hero.name, hero.score, hero.level);
    return 0;
}
Zoe reached level 2!
Zoe: score 140, level 2

After 60 points the score is below 100. After 50 more it's 110, so Zoe reaches level 2. The final 30 points bring her to 140, which is below the 200 needed for level 3.

What's next?

Structs give every member its own space. Next, meet their space-saving cousin: unions in C, where all members share the same memory.

Key takeaways

  • A pointer to a structure stores the address of a struct, for example struct Account *p = &acc;.
  • The arrow operator p->member is shorthand for (*p).member.
  • Pass a struct pointer to a function when the function must change the caller's struct.
  • Passing a pointer copies only an address, so it avoids copying large structs.
  • Use const struct T * for functions that only read the struct.
  • Structs created with malloc are used through -> and must be released with free.

Frequently asked questions

What does the -> operator do in C?

The arrow operator reaches a member of a struct through a pointer. p->balance is the same as (*p).balance: follow the pointer to the struct, then take its balance member.

When should I use . and when should I use -> in C?

Use the dot when you have a struct variable, like acc.balance. Use the arrow when you have a pointer to a struct, like p->balance.

Why pass a pointer to a struct instead of the struct itself?

Two reasons: the function can modify the original struct, and only an address is copied instead of the whole struct, which is faster for large records.

How do you allocate a struct dynamically in C?

Call malloc(sizeof *p) or malloc(sizeof(struct Name)), check that the result is not NULL, set the members with ->, and call free when you no longer need it.

Why does *p.member not work in C?

The dot operator has higher precedence than the star, so *p.member means *(p.member). Write (*p).member or, more simply, p->member.