Kodewick
Lesson 24 of 30Contents

Pointers to structs

Lesson 24 of 30 · Part 5 · Your own types · about 15 minutes

In the last lesson, a struct passed to a function was a copy, so the function couldn't change the one outside. In lesson 16 we solved the same problem with pointers: give the function the address. Structs work the same way. First, a pointer to a struct:

main.c
1#include <stdio.h>2 3struct student {4    char name[20];5    int score;6};7 8int main(void) {9    struct student s = { "Ana", 80 };10    struct student *p = &s;11    (*p).score = 90;12    printf("%s: %d points\n", (*p).name, s.score);13    return 0;14}
Output
Ana: 90 points

struct student *p is a pointer that points to a struct student. *p is s itself, so (*p).score = 90 changes s.score. Press Step: p is a small cell, with an arrow from it to the big box of s.

The arrow ->

The parentheses in (*p).score are required. The dot . goes before *, so without them, *p.score means "first take the member score of p", and p is a pointer, which has no members:

main.c
1#include <stdio.h>2 3struct student {4    char name[20];5    int score;6};7 8int main(void) {9    struct student s = { "Ana", 80 };10    struct student *p = &s;11    *p.score = 90;12    printf("%d\n", s.score);13    return 0;14}
Output
main.c: In function 'main':
main.c:11:7: error: 'p' is a pointer; did you mean to use '->'?
   11 |     *p.score = 90;
      |       ^
      |       ->

The compiler even guesses what you meant: did you mean to use '->'?. Writing (*p). every time is a chore, so C has a shorthand for it, the arrow ->: p->score means exactly the same as (*p).score.

main.c
1#include <stdio.h>2 3struct student {4    char name[20];5    int score;6};7 8int main(void) {9    struct student s = { "Ana", 80 };10    struct student *p = &s;11    p->score = 90;12    p->score += 5;13    printf("%s: %d points\n", p->name, p->score);14    return 0;15}
Output
Ana: 95 points

The rule is simple: if you have a struct, use .; if you have a pointer to a struct, use ->.

Changing a struct through a pointer

The deposit from the last lesson couldn't save any money because it got a copy. Make the parameter a pointer, pass the address &acc when calling, and the function can change the one in main directly:

main.c
1#include <stdio.h>2 3struct account {4    char owner[20];5    int balance;6};7 8void deposit(struct account *a, int amount) {9    a->balance += amount;10}11 12int main(void) {13    struct account acc = { "Ana", 100 };14    deposit(&acc, 50);15    deposit(&acc, 30);16    printf("%s's balance: %d\n", acc.owner, acc.balance);17    return 0;18}
Output
Ana's balance: 180

Press Step and go into deposit: its stack frame no longer holds a full copy, only a pointer a with an arrow back to acc in the frame of main. a->balance += amount changes the number over there. Passing a pointer has another upside: however big the struct is, only an address gets copied.

Tip: When a function only reads the struct, write the parameter as const struct account *a. Nothing is copied, and if the function changes it by mistake, the compiler gives an error.

Pointing into an array

A pointer can also point to one element of an array of structs. top below finds the student with the highest score and returns the address of that element:

Guess first

What does this program print?

1#include <stdio.h>2 3struct student {4    char name[20];5    int score;6};7 8struct student *top(struct student list[], int n) {9    struct student *best = &list[0];10    for (int i = 1; i < n; i++) {11        if (list[i].score > best->score) {12            best = &list[i];13        }14    }15    return best;16}17 18int main(void) {19    struct student group[3] = { { "Ana", 82 }, { "Ben", 91 }, { "Chloe", 77 } };20    struct student *p = top(group, 3);21    p->score = 100;22    printf("%d\n", group[1].score);23    return 0;24}

A struct on the heap

A struct can live on the heap too (lessons 19 and 20). malloc(sizeof(struct product)) asks for a block just big enough for one struct. What you get back is a pointer to it, so from then on you use ->:

main.c
1#include <stdio.h>2#include <stdlib.h>3#include <string.h>4 5struct product {6    char name[20];7    double price;8};9 10int main(void) {11    struct product *p = malloc(sizeof(struct product));12    if (p == NULL) {13        printf("Out of memory\n");14        return 1;15    }16    strcpy(p->name, "Water bottle");17    p->price = 19.0;18    printf("%s %.2f\n", p->name, p->price);19    free(p);20    return 0;21}
Output
Water bottle 19.00

Press Step and watch a box appear on the heap with name and price inside, and an arrow from p in main pointing to it. After free(p), the box turns gray and dashed, marked "freed": that memory has been given back.

The function makes it, the caller frees it

Building a struct on the heap is often wrapped in a function: it gets the memory, fills in the members and hands back the pointer. Things on the heap outlive the function, so this is fine (returning the address of a local variable would not be, lesson 18). The price: the caller has to remember to free it. The parameter const char *name is the usual way to take a string the function only reads: it gets the address of the first character (lesson 17), and const (lesson 18) promises not to change it.

main.c
1#include <stdio.h>2#include <stdlib.h>3#include <string.h>4 5struct product {6    char name[20];7    double price;8};9 10struct product *new_product(const char *name, double price) {11    struct product *p = malloc(sizeof(struct product));12    if (p != NULL) {13        strcpy(p->name, name);14        p->price = price;15    }16    return p;17}18 19int main(void) {20    struct product *a = new_product("Water bottle", 19.0);21    struct product *b = new_product("Umbrella", 12.5);22    if (a == NULL || b == NULL) {23        return 1;24    }25    printf("%s %.2f\n", a->name, a->price);26    printf("%s %.2f\n", b->name, b->price);27    printf("Total %.2f\n", a->price + b->price);28    free(a);29    free(b);30    return 0;31}
Output
Water bottle 19.00
Umbrella 12.50
Total 31.50

Two common mistakes

The first: using it after free. The box has been given back, but p still points there:

main.c
1#include <stdio.h>2#include <stdlib.h>3#include <string.h>4 5struct product {6    char name[20];7    double price;8};9 10int main(void) {11    struct product *p = malloc(sizeof(struct product));12    if (p == NULL) {13        return 1;14    }15    strcpy(p->name, "Umbrella");16    p->price = 12.5;17    free(p);18    printf("%s %.2f\n", p->name, p->price);19    return 0;20}
Output
heap-use-after-free

What happened: Stopped at line 18. This memory is no longer yours: it was freed, or it belonged to a function that has returned.

Real C may not complain: it might still show the old values, or it might show garbage. Our engine stops on this line. Press "see how it went wrong" and see that after free, the box is gray and p's arrow has turned into a red dashed line.

The second: the pointer is NULL, and you use -> on it anyway. This happens when, say, a function returns NULL because it found nothing, and the caller forgets to check. Here p is simply set to NULL:

main.c
1#include <stdio.h>2#include <string.h>3 4struct student {5    char name[20];6    int score;7};8 9int main(void) {10    struct student *p = NULL;11    strcpy(p->name, "Ana");12    printf("End\n");13    return 0;14}
Output
store to null pointer

What happened: Stopped at line 11. This pointer is NULL: nothing can be read or written through it.

NULL points to nothing, so p->name has nowhere to go. On a real computer, a program like this usually just crashes. Whenever a pointer might be NULL, check if (p != NULL) before you use ->.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

Use the arrow

This program doesn't compile. Change every dot after p into an arrow so that it prints Desk lamp 89.00.

It should print
Desk lamp 89.00
exercise1.c
1#include <stdio.h>2 3struct product {4    char name[20];5    double price;6};7 8int main(void) {9    struct product item = { "Desk lamp", 99.0 };10    struct product *p = &item;11    p.price = p.price - 10;12    printf("%s %.2f\n", p.name, p.price);13    return 0;14}
Output
Press Run or Ctrl+Enter
Exercise 2

Add points

Write a function add_points that takes a pointer to a struct player and a number of points, and adds the points to that player. The program should print Ben: 35 points.

It should print
Ben: 35 points
exercise2.c
1#include <stdio.h>2 3struct player {4    char name[20];5    int points;6};7 8// write add_points here9 10int main(void) {11    struct player pl = { "Ben", 20 };12    add_points(&pl, 10);13    add_points(&pl, 5);14    printf("%s: %d points\n", pl.name, pl.points);15    return 0;16}
Output
Press Run or Ctrl+Enter
Exercise 3

A book on the heap

Use malloc to make a struct book on the heap, set its title to "Emma" and its pages to 474, print Emma: 474 pages, and free it at the end.

It should print
Emma: 474 pages
exercise3.c
1#include <stdio.h>2#include <stdlib.h>3#include <string.h>4 5struct book {6    char title[20];7    int pages;8};9 10int main(void) {11 12    return 0;13}
Output
Press Run or Ctrl+Enter

What you learned

  • struct student *p = &s; is a pointer to a struct, and *p is s.
  • p->score is the same as (*p).score: use . on a struct and -> on a pointer to one.
  • Make the parameter a struct pointer and pass &acc, and the function can change the struct outside, with no copy.
  • A function can return the address of one element of an array; changes through it change that element.
  • malloc(sizeof(struct product)) makes a struct on the heap; a function can build one and return the pointer, and the caller must free it.
  • Don't use it after free; when a pointer might be NULL, check it before using ->.