Structs, unions, enums and typedef · about 55 min

Pointers to structs and arrays of structs

Why this matters

Passing structs by value is fine for small ones, but it copies every byte and the function cannot change the caller's struct. Real C code almost always handles structs through pointers: the standard library's FILE *, the kernel's struct task_struct *, every node of every linked list and tree. Arrays of structs are the C equivalent of a table of records, and sorting them is an everyday task. This lesson combines what you know about pointers, the heap and structs into the patterns used for the rest of the track.

The arrow operator

With a pointer to a struct, you could reach a member by dereferencing first: (*p).x. The parentheses are required because . binds tighter than *. That is so common and so clumsy that C has a shorthand:

p->x     /* exactly the same as (*p).x */

Read -> as "the member of what p points to". Memory-wise, p->x means: take the address in p, add the offset of x, and access the bytes there.

  stack                              (anywhere: stack, heap, global)
  +---------------------+            +----------------+----------------+
  | p = 0x7ffd...a0  ---+----------> |   x = 1        |   y = 2        |
  +---------------------+            +----------------+----------------+
                                      p->x = *(p + 0)   p->y = bytes at p + 4

Use . on a struct, -> on a pointer to a struct. gcc tells you when you mix them up:

main.c:3:82: error: 'p' is a pointer; did you mean to use '->'?
main.c:3:104: error: invalid type argument of '->' (have 'struct point')

Passing structs by pointer

Two reasons to pass a pointer instead of the struct:

  1. To modify the caller's struct. The function receives the address, so changes land in the original.
  2. To avoid copying a large struct. A pointer is 8 bytes no matter how big the struct is.

When a function only reads, declare the parameter const struct item *. You get the speed of a pointer with the promise of a value: the compiler rejects any attempt to modify through it, and the reader of the signature knows the struct will not change.

Arrays of structs and qsort

An array of structs is a contiguous run of structs, each sizeof(struct item) bytes, padding included:

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

struct item {
    char name[16];
    int price;      /* in cents */
    int qty;
};

int item_total(const struct item *it)
{
    return it->price * it->qty;
}

void restock(struct item *it, int amount)
{
    it->qty += amount;
}

int by_total_desc(const void *a, const void *b)
{
    const struct item *x = a, *y = b;
    return item_total(y) - item_total(x);
}

int main(void)
{
    struct item stock[] = {
        { "pen", 150, 10 },
        { "notebook", 400, 2 },
        { "stapler", 900, 3 },
    };
    size_t n = sizeof stock / sizeof stock[0];

    restock(&stock[1], 5);
    qsort(stock, n, sizeof stock[0], by_total_desc);

    for (size_t i = 0; i < n; i++)
        printf("%-10s %3d x %4d = %5d\n", stock[i].name, stock[i].qty,
               stock[i].price, item_total(&stock[i]));
    return 0;
}
notebook     7 x  400 =  2800
stapler      3 x  900 =  2700
pen         10 x  150 =  1500

Things to notice:

Warning: return y_total - x_total; is a common comparator shortcut, and it is fine for small values like these. With large or negative values the subtraction can overflow, which is undefined behavior and gives wrong orderings. The safe form is return (a > b) - (a < b); or explicit if comparisons.

  stock (72 bytes)
  +-------------------------+-------------------------+-------------------------+
  | "notebook"  400    7    | "stapler"   900    3    | "pen"       150   10    |
  +-------------------------+-------------------------+-------------------------+
  ^ stock[0], stock + 0      ^ stock + 1 (24 bytes on)  ^ stock + 2

Structs on the heap

When the number of structs is only known at run time, or a struct must outlive the function that creates it, allocate it on the heap. If the struct itself owns heap memory, provide a create/destroy pair as in the ownership lesson:

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

struct person {
    char *name;     /* owned: heap copy */
    int age;
};

/* Returns a new person, or NULL. Release with person_free. */
struct person *person_new(const char *name, int age)
{
    struct person *p = malloc(sizeof *p);
    if (p == NULL)
        return NULL;
    p->name = malloc(strlen(name) + 1);
    if (p->name == NULL) {
        free(p);
        return NULL;
    }
    strcpy(p->name, name);
    p->age = age;
    return p;
}

void person_free(struct person *p)
{
    if (p == NULL)
        return;
    free(p->name);
    free(p);
}

int main(void)
{
    struct person *ada = person_new("Ada", 36);
    if (ada == NULL)
        return 1;
    printf("%s is %d\n", ada->name, ada->age);
    ada->age++;
    printf("%s is now %d\n", ada->name, ada->age);
    person_free(ada);
    return 0;
}
Ada is 36
Ada is now 37
  stack            heap
  +-------+        +-------------------+         +---+---+---+----+
  | ada --+------> | name ---------------------> | A | d | a | \0 |
  +-------+        | age = 37          |         +---+---+---+----+
                   +-------------------+
                   struct person (16 bytes)      separate allocation

person_free frees the inner allocation first and then the struct, children before parent. Freeing p first and then reading p->name would read freed memory.

ada->age++ works because -> binds tighter than ++: it increments the member.

Deep and shallow copies

struct person b = *ada; copies the struct: b.age is independent, but b.name is the same address as ada->name. Both refer to one string. Free one and the other dangles; free both and it is a double free. If you need an independent copy, write a function that allocates a new string too (a deep copy):

struct person *person_clone(const struct person *src)
{
    return person_new(src->name, src->age);
}

When you design a struct, decide whether each pointer member is owned (the struct frees it and copies need deep copies) or borrowed (it points at something someone else manages), and write that in a comment next to the member.

Common mistakes

Recap

Going further: change the leaderboard exercise to store an array of struct player * (each player allocated separately) and sort that. Work out what type the comparator's arguments really point to before you write it.

Exercises

Exercise 1 · quiz

Arrow and dot

Given struct point pt = {1, 2}; struct point *p = &pt;, which expressions evaluate to the x member of pt? Select all that apply.

Answer
  • pt.x

  • p->x

  • (*p).x

p->x is shorthand for (*p).x: dereference, then select the member. The parentheses matter because . binds tighter than *, so *p.x means *(p.x), which is an error since p is a pointer and has no members. gcc says so helpfully: 'p' is a pointer; did you mean to use '->'?

Exercise 2 · write code

Modify a struct through a pointer

Given

struct account {
    char owner[32];
    long balance;     /* in cents */
    int overdrafts;   /* failed withdrawals */
};

write:

Keep the struct exactly as shown; the test declares the same struct and calls your functions.

File: main.c

Starting point

#include <stdio.h>

struct account {
    char owner[32];
    long balance;     /* in cents */
    int overdrafts;   /* failed withdrawals */
};

void deposit(struct account a, long amount)
{
    a.balance += amount;
}

int main(void)
{
    struct account acc = { "Ada", 1000, 0 };
    deposit(acc, 500);
    printf("%s has %ld\n", acc.owner, acc.balance);
    return 0;
}
Hint 1

The starter's deposit takes the struct by value, so it changes a copy. Take struct account *a and use a->balance.

Hint 2

In withdraw, check a->balance < amount first: increment a->overdrafts and return 0, otherwise subtract and return 1.

Hint 3

In net_worth, accts[i] is a struct (not a pointer), so use accts[i].balance.

Reference solution
#include <stdio.h>

struct account {
    char owner[32];
    long balance;     /* in cents */
    int overdrafts;   /* failed withdrawals */
};

void deposit(struct account *a, long amount)
{
    a->balance += amount;
}

int withdraw(struct account *a, long amount)
{
    if (a->balance < amount) {
        a->overdrafts++;
        return 0;
    }
    a->balance -= amount;
    return 1;
}

long net_worth(const struct account *accts, int n)
{
    long sum = 0;
    for (int i = 0; i < n; i++)
        sum += accts[i].balance;
    return sum;
}

int main(void)
{
    struct account acc = { "Ada", 1000, 0 };
    deposit(&acc, 500);
    printf("%s has %ld\n", acc.owner, acc.balance);
    return 0;
}

Exercise 3 · write code

A sorted leaderboard

Read a count n, then n lines each holding a player name (one word, at most 31 characters) and an integer score. Store them in a heap-allocated array of structs, sort it with qsort by score, highest first, with ties broken by name in alphabetical order (strcmp), and print the ranking as

1. carol 90
2. alice 75
3. bob 75

If the count is missing or not positive, print bad count and exit with status 1. If a record cannot be read, print bad record and exit with status 1 (freeing the array first). The check runs under valgrind, so free the array.

Use scanf("%31s %d", p->name, &p->score) to read a record into struct player *p.

File: main.c

Starting point

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

struct player {
    char name[32];
    int score;
};

/* comparison function for qsort: return negative if a should come first */
int compare_players(const void *a, const void *b)
{
    return 0;
}

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("bad count\n");
        return 1;
    }
    /* allocate, read, sort, print, free */
    return 0;
}

Example

input

3
alice 75
carol 90
bob 75

expected output

1. carol 90
2. alice 75
3. bob 75

Example

input

5
a 1
b 2
c 3
d 4
e 5

expected output

1. e 5
2. d 4
3. c 3
4. b 2
5. a 1
Hint 1

struct player *players = malloc(n * sizeof *players);, then read into &players[i].

Hint 2

In the comparator, convert: const struct player *x = a, *y = b;. If the scores differ, the higher score comes first (return a negative number when x->score > y->score).

Hint 3

For equal scores return strcmp(x->name, y->name);. Call qsort(players, n, sizeof *players, compare_players);.

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

struct player {
    char name[32];
    int score;
};

int compare_players(const void *a, const void *b)
{
    const struct player *x = a, *y = b;
    if (x->score != y->score)
        return x->score > y->score ? -1 : 1;
    return strcmp(x->name, y->name);
}

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("bad count\n");
        return 1;
    }
    struct player *players = malloc(n * sizeof *players);
    if (players == NULL)
        return 1;

    for (int i = 0; i < n; i++) {
        struct player *p = &players[i];
        if (scanf("%31s %d", p->name, &p->score) != 2) {
            printf("bad record\n");
            free(players);
            return 1;
        }
    }

    qsort(players, n, sizeof *players, compare_players);

    for (int i = 0; i < n; i++)
        printf("%d. %s %d\n", i + 1, players[i].name, players[i].score);

    free(players);
    return 0;
}

Exercise 4 · quiz

Shallow copy trouble

struct person { char *name; int age; };   /* name is a heap string the struct owns */

struct person *a = person_new("Ada", 36);  /* mallocs the struct and the name */
struct person b = *a;                      /* copy the struct */
person_free(a);                            /* frees a->name, then a */
printf("%s\n", b.name);

What is the problem?

Answer
  • b.name points at the same heap string, which person_free already released, so the printf reads freed memory

Struct assignment copies the pointer value, not the string it points to: a shallow copy. After person_free, b.name dangles (valgrind: Invalid read ... inside a block free'd). If copies need their own data, write a deep-copy function (person_clone) that allocates a new string, and give each copy its own person_free. If both copies are freed, it is a double free.