Structs and strings in C — practice with a cinema hall, a viewer finder and a box office on the heap (and the Lucía the program swore didn’t exist)

When I finished the viewer finder in the lab, I handed it to a classmate to test. He typed “lucia perez” and the program answered that there was nobody with that name. I had the list right in front of me and Lucía was there, in A3. The problem: in my data she was written as ” Lucia Perez”, with spaces in front and two spaces in the middle. For strcmp, that and “lucia perez” are different strings, and it’s completely right: it compares bytes, not people.

That lesson served me for the whole lab: real data never arrives clean. It comes with random capital letters, extra spaces, names that don’t fit and seats that don’t exist. A program that only works with perfect data isn’t finished.

This practice puts into play everything from the structs and strings in C theory with the same example: cinema seats. There are three medium-level programs: a hall that receives bookings with errors, a finder that normalises and sorts names, and a box office that stores titles on the heap with what you learnt in dynamic memory in C. They all use data that looks like the real thing: repeated, too long, badly typed.

Before you start

It’s worth being clear on:

  • How a struct is declared and how you reach its fields with the dot (b.taken) and with the arrow (p->taken).
  • That a string is a char array ending in '\0', and that char name[12] holds 11 letters.
  • That text is copied with strcpy/snprintf and compared with strcmp, never with = or ==.
  • That malloc, realloc and free reserve, grow and release memory on the heap.

If any of them sounds shaky, go over the theory before carrying on.

Always compile with gcc -Wall -std=c11 program.c -o program. In program 3, which uses the heap, add -g -fsanitize=address: if you forget a free or go past a block, AddressSanitizer tells you when the program ends. I compiled it like that and it gave no warnings at all.

Program 1: the cinema hall

The online box office sends a list of bookings: row (a letter), seat number (as the audience counts it, from 1) and the viewer’s name. The program has to store each booking in its seat, reject the ones that are already taken or don’t exist, cut the names that don’t fit and draw the seating map with the occupancy percentage. The tricky parts: going from “C6” to array indices, modifying the real seat (not a copy) and not overflowing the name.

The idea

The hall is a matrix of structs: struct seat room[3][6], 3 rows and 6 seats. Each seat stores just two things: whether it’s taken and in whose name. The steps for each booking:

  1. Turn the letter into a row: since letters are consecutive numbers, 'C' - 'A' gives 2.
  2. Turn the number into a column: the audience says 6, the array says 5.
  3. Check that the seat exists before touching the array (otherwise we’d write outside it).
  4. Get a pointer to that seat and, if it’s free, take it and copy the name with snprintf, which cuts without overflowing.

The function returns a code (0, 1 or 2) instead of printing, so that main decides what to show and keeps count.

Before coding, think about what would happen with the booking “D2” if you didn’t check the limits: room[3][1] is outside the matrix, and writing there means overwriting someone else’s memory, like in the theory hook.

The code

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

#define ROWS 3
#define SEATS 6
// 11 characters + '\0'
#define MAX_NAME 12

struct seat {
    int taken;                // 0 free, 1 taken
    char name[MAX_NAME];
};

// What arrives from the online box office
struct booking {
    char row;                 // 'A', 'B' or 'C'
    int number;               // from 1 to 6, as the audience counts
    const char *name;
};

// Returns 0 if it books, 1 if it was already taken and 2 if the seat does not exist
int book(struct seat room[ROWS][SEATS], struct booking r) {
    // Letters are consecutive numbers: 'A' - 'A' = 0, 'B' - 'A' = 1...
    int f = r.row - 'A';
    // The audience counts from 1; the array, from 0
    int a = r.number - 1;
    if (f < 0 || f >= ROWS || a < 0 || a >= SEATS)
        return 2;
    // Pointer to the real seat (not a copy), so we can modify it
    struct seat *b = &room[f][a];
    if (b->taken)
        return 1;
    b->taken = 1;
    // snprintf cuts the name if it does not fit and always leaves the '\0'
    snprintf(b->name, sizeof b->name, "%s", r.name);
    return 0;
}

void draw(struct seat room[ROWS][SEATS]) {
    printf("     1 2 3 4 5 6\n");
    for (int f = 0; f < ROWS; f++) {
        printf("  %c  ", 'A' + f);
        for (int a = 0; a < SEATS; a++)
            printf("%c ", room[f][a].taken ? 'X' : '.');
        printf("\n");
    }
}

int main(void) {
    // = {0}: all free and all names empty (0 is '\0')
    struct seat room[ROWS][SEATS] = {0};
    struct booking requests[] = {
        {'A', 3, "Lucia"}, {'A', 4, "Marcos"}, {'B', 1, "Ana"},
        {'A', 3, "Pepito"},             // repeated: A3 is already Lucia's
        {'C', 6, "Maximiliano Ruiz"},   // long name: it gets cut
        {'D', 2, "Juan"},               // row D does not exist
        {'B', 7, "Carla"},              // neither does seat 7
        {'C', 5, "Ivan"},
    };
    int n = sizeof requests / sizeof requests[0];
    const char *reason[] = {"ok", "taken", "doesn't exist"};
    int done = 0;
    for (int i = 0; i < n; i++) {
        int res = book(room, requests[i]);
        printf("%c%d %-17s -> %s\n", requests[i].row, requests[i].number, requests[i].name, reason[res]);
        if (res == 0)
            done++;
    }
    draw(room);
    printf("Occupancy: %d of %d (%.1f %%)\n", done, ROWS * SEATS, 100.0 * done / (ROWS * SEATS));
    printf("C6 is booked under \"%s\" (%zu characters)\n", room[2][5].name, strlen(room[2][5].name));
    return 0;
}

The heart of the program is book. Let’s zoom in:

// Returns 0 if it books, 1 if it was already taken and 2 if the seat does not exist
int book(struct seat room[ROWS][SEATS], struct booking r) {
    // Letters are consecutive numbers: 'A' - 'A' = 0, 'B' - 'A' = 1...
    int f = r.row - 'A';
    // The audience counts from 1; the array, from 0
    int a = r.number - 1;
    if (f < 0 || f >= ROWS || a < 0 || a >= SEATS)
        return 2;
    // Pointer to the real seat (not a copy), so we can modify it
    struct seat *b = &room[f][a];
    if (b->taken)
        return 1;
    b->taken = 1;
    // snprintf cuts the name if it does not fit and always leaves the '\0'
    snprintf(b->name, sizeof b->name, "%s", r.name);
    return 0;
}

Three important decisions:

  • **struct seat *b = &room[f][a];**: we store the seat’s address. That way, b->taken = 1 changes the seat in the hall. If we’d written struct seat b = room[f][a]; (no asterisk), we’d have a copy, we’d take it and the hall would stay empty.
  • **The booking r arrives by value**: we only read it, so a copy does no harm (it’s small: a char, an int and a pointer).
  • **snprintf(b->name, sizeof b->name, "%s", r.name)**: sizeof b->name is 12, the real size of the field. snprintf writes at most 11 letters and the '\0'. With strcpy, “Maximiliano Ruiz” (17 bytes) would have overflowed the field and overwritten the next seat, which is exactly the theory hook.

And why can the matrix be modified inside the function if we don’t pass an explicit pointer? Because an array as a parameter is always passed as the address of its first element: struct seat room[ROWS][SEATS] inside the parentheses is really a pointer.

Output

A3 Lucia             -> ok
A4 Marcos            -> ok
B1 Ana               -> ok
A3 Pepito            -> taken
C6 Maximiliano Ruiz  -> ok
D2 Juan              -> doesn't exist
B7 Carla             -> doesn't exist
C5 Ivan              -> ok
     1 2 3 4 5 6
  A  . . X X . . 
  B  X . . . . . 
  C  . . . . X X 
Occupancy: 5 of 18 (27.8 %)
C6 is booked under "Maximiliano" (11 characters)

Let’s check it by hand, booking by booking:

Requestf = row − ‘A’a = number − 1Exists?Taken?Resultdone
A3 Lucia02yesnook1
A4 Marcos03yesnook2
B1 Ana10yesnook3
A3 Pepito02yesyes (Lucia)taken3
C6 Maximiliano Ruiz25yesnook (cut)4
D2 Juan31no (f = 3 ≥ 3)—doesn’t exist4
B7 Carla16no (a = 6 ≥ 6)—doesn’t exist4
C5 Ivan24yesnook5

5 bookings out of 18 seats: 5 / 18 = 0.2777…, which rounded to one decimal place is the 27.8% in the output. The last line confirms the cut: “Maximiliano” has 11 characters, exactly what fits in char name[12] with its null. This is how the hall ends up:

The seating map after the bookingsGrid of 3 rows by 6 seats. Taken: A3 Lucia, A4 Marcos, B1 Ana, C5 Ivan and C6 Maximiliano, cut down. Rejected: A3 Pepito because it was taken, D2 Juan and B7 Carla because they don't exist.SCREEN123456A..XLuciaXMarcos..BXAna.....C....XIvanXMaximilianocut to 11RejectedA3 PepitotakenD2 Juandoesn't existB7 Carladoesn't exist5 of 18 → 27.8%
Each cell is a struct seat: taken and name. C6 keeps only the 11 characters that fit.

Questions you might ask

**Why does struct seat room[ROWS][SEATS] = {0}; leave all the names empty?** Because in a brace initialisation everything you don’t name is 0. Every taken is 0 (free) and every byte of every name is 0, which is '\0': 18 empty strings. Without the = {0}, a local matrix would contain garbage and some seats could appear taken.

Why is the error code a number and not a text? Because that way the function doesn’t decide how it’s shown. main uses the number as an index into reason[] (“ok”, “taken”, “doesn’t exist”) and also counts the bookings made with if (res == 0). Comparing numbers is easy; comparing texts would force us to use strcmp.

**Why do I check f < 0 if letters are always uppercase?** Because data isn’t always what you expect. If someone sends row ‘@’ (right before ‘A’ in ASCII), '@' - 'A' gives −1, and room[-1] is memory before the matrix.

How much space does the hall take? Each struct seat has an int (4 bytes) and 12 chars: 16 bytes, a multiple of 4, no padding. The 18 seats are 18 × 16 = 288 bytes in a row on the stack.

**Does snprintf warn that it has cut something? It doesn’t print anything, but it returns how many characters it would have needed**. If you want to warn, compare: if (snprintf(...) >= (int)sizeof b->name).

Writing struct seat b = room[f][a]; and then b.taken = 1;. It compiles with no warnings and the output says “ok”, but the map comes out empty: you took a copy. To modify, pointer and arrow.

Try it yourself

  • Add a double price field to struct seat and make row A cost €9.50 and the rest €7.00. At the end, print the takings.
  • Write a function free_seat(room, 'A', 3) that leaves the seat free and with an empty name (hint: setting name[0] = '\0' is enough).

Program 2: the viewer finder

Now we have the list of viewers exactly as they typed it in the form: with spaces before and after, double spaces and random capitals. The program has to create a normalised version of each name (lowercase, with a single space between words), sort the list alphabetically, find a person even if they’re typed differently and list the ones that start with a given text. It’s the problem from my story, solved.

The idea

The key is to separate what you show from what you compare. Each struct viewer stores the original name and a normalised key; every comparison (sorting, searching) uses the key. And what we look for is normalised with the same function, so we always compare “in the same language”.

To normalise, we walk the source character by character and write into the destination only what we want, with an index j of its own. The trick is in the spaces: we don’t write a space when we see it, we leave it pending and only write it if a letter comes afterwards. That way trailing spaces are never written and double ones become a single one.

To sort we use bubble sort: compare neighbours with strcmp and swap them if they’re the wrong way round. And the swap is simple because a struct is assigned whole: name, key and seat move together.

Before coding, think about where the destination’s '\0' goes. Since we skip characters, j and the source position don’t match: the null goes in dest[j], not at the position where the source ended.

The code

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

#define MAX 20

struct viewer {
    char name[MAX];   // exactly as it came from the form
    char key[MAX];    // normalised: no extra spaces and in lowercase
    char seat[4];     // "A3", "C12"...: 3 characters + '\0'
};

// Copies src into dest removing leading, trailing and repeated spaces,
// and turning everything into lowercase. size is the size of dest
void normalize(char *dest, const char *src, size_t size) {
    size_t j = 0;
    int pending_space = 0;
    // Skips the leading spaces
    while (*src == ' ')
        src++;
    // size - 1: we always leave room for the '\0'
    for (; *src != '\0' && j < size - 1; src++) {
        if (*src == ' ') {
            // We do not write it yet: if it is a trailing space, it will never be written
            pending_space = 1;
            continue;
        }
        // A single space between words, and only if it fits together with the next letter
        if (pending_space && j < size - 2)
            dest[j++] = ' ';
        pending_space = 0;
        // (unsigned char): tolower expects a non-negative value
        dest[j++] = (char)tolower((unsigned char)*src);
    }
    // Without this it would not be a string
    dest[j] = '\0';
}

// A struct can be assigned whole: the swap moves all its fields at once
void swap(struct viewer *a, struct viewer *b) {
    struct viewer tmp = *a;
    *a = *b;
    *b = tmp;
}

// Bubble sort by key: strcmp(...) > 0 means "v[j] goes after v[j + 1]"
void sort_list(struct viewer v[], int n) {
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < n - 1 - i; j++)
            if (strcmp(v[j].key, v[j + 1].key) > 0)
                swap(&v[j], &v[j + 1]);
}

int main(void) {
    struct viewer v[] = {
        {"  Lucia  Perez", "", "A3"},
        {"marcos", "", "A4"},
        {"ANA lopez ", "", "B1"},
        {"Ivan", "", "C5"},
        {"ana Garcia", "", "B2"},
    };
    int n = sizeof v / sizeof v[0];
    for (int i = 0; i < n; i++)
        normalize(v[i].key, v[i].name, sizeof v[i].key);
    sort_list(v, n);
    printf("Sorted list:\n");
    for (int i = 0; i < n; i++)
        printf("  %-3s [%s]\n", v[i].seat, v[i].key);

    // Exact search: what we look for is normalised too and compared with strcmp, never with ==
    char wanted[MAX];
    normalize(wanted, "  LUCIA perez", sizeof wanted);
    for (int i = 0; i < n; i++)
        if (strcmp(v[i].key, wanted) == 0)
            printf("\"%s\" is in %s\n", wanted, v[i].seat);

    // Prefix search: strncmp only compares the first strlen(prefix) characters
    const char *prefix = "ana";
    printf("Starting with \"%s\":", prefix);
    for (int i = 0; i < n; i++)
        if (strncmp(v[i].key, prefix, strlen(prefix)) == 0)
            printf(" %s", v[i].seat);
    printf("\n");
    return 0;
}

The function that does the fine work is normalize:

// Copies src into dest removing leading, trailing and repeated spaces,
// and turning everything into lowercase. size is the size of dest
void normalize(char *dest, const char *src, size_t size) {
    size_t j = 0;
    int pending_space = 0;
    // Skips the leading spaces
    while (*src == ' ')
        src++;
    // size - 1: we always leave room for the '\0'
    for (; *src != '\0' && j < size - 1; src++) {
        if (*src == ' ') {
            // We do not write it yet: if it is a trailing space, it will never be written
            pending_space = 1;
            continue;
        }
        // A single space between words, and only if it fits together with the next letter
        if (pending_space && j < size - 2)
            dest[j++] = ' ';
        pending_space = 0;
        // (unsigned char): tolower expects a non-negative value
        dest[j++] = (char)tolower((unsigned char)*src);
    }
    // Without this it would not be a string
    dest[j] = '\0';
}

This is how it works with ” Lucia Perez” (size 20):

Character readWhat does it do?pending_spacej afterdest so far
‘ ‘, ‘ ‘The initial while skips them00(empty)
LWrites ‘l’ (lowercase)01l
u, c, i, aWrites each letter05lucia
‘ ‘Leaves it pending15lucia
‘ ‘Still pending (doesn’t add another)15lucia
PWrites the pending space and then ‘p’07lucia p
e, r, e, zWrites each letter011lucia perez
(end)dest[11] = '\0'—11lucia perez

Notice the two conditions in the loop: j < size - 1 guarantees there’s always a last slot left for the null, and j < size - 2 on the space guarantees that, if we write a space, there’s still room for at least one letter after it. Without that, a very long name could end in a space.

The (unsigned char) cast in tolower isn’t fussiness: tolower expects a value between 0 and 255 (or EOF), and a char holding an accented letter can be negative on Linux.

And the swap, in three lines:

// A struct can be assigned whole: the swap moves all its fields at once
void swap(struct viewer *a, struct viewer *b) {
    struct viewer tmp = *a;
    *a = *b;
    *b = tmp;
}

tmp = *a copies the whole struct (the 20 + 20 + 4 = 44 bytes of the three arrays) and *a = *b does the same the other way round. If the names were pointers (char *) instead of arrays, the pointers would be copied and not the texts; here, since they’re arrays inside the struct, they travel with it.

Output

Sorted list:
  B2  [ana garcia]
  B1  [ana lopez]
  C5  [ivan]
  A3  [lucia perez]
  A4  [marcos]
"lucia perez" is in A3
Starting with "ana": B2 B1

Let’s check it. The keys before sorting were: “lucia perez”, “marcos”, “ana lopez”, “ivan” and “ana garcia”. Bubble sort makes 4 passes; in each one, the biggest of the remaining ones ends up at the end:

PassList after the pass
1lucia perez, ana lopez, ivan, ana garcia, marcos
2ana lopez, ivan, ana garcia, lucia perez, marcos
3ana lopez, ana garcia, ivan, lucia perez, marcos
4ana garcia, ana lopez, ivan, lucia perez, marcos

The last comparison is the interesting one: “ana lopez” against “ana garcia”. The first 4 characters (“ana “) are equal, and in the fifth ‘l’ (108) is greater than ‘g’ (103), so strcmp gives a positive number and they’re swapped. That’s why B2 comes before B1.

The search normalises ” LUCIA perez” to “lucia perez”, which matches the key of A3: what failed in my story. And the prefix “ana” compares only strlen("ana") = 3 characters, so it matches “ana garcia” and “ana lopez”, but not “ivan” (which contains “an”, but doesn’t start with “ana”).

Questions you might ask

Why don’t I normalise the name directly, without keeping a copy? Because you’d lose the original. If tomorrow you want to print the list as each person typed it (or with a capital initial), you need it. Keeping the key separately costs 20 bytes per viewer and saves a lot of trouble.

**Why strncmp and not strcmp for the prefix?** strcmp("ana garcia", "ana") doesn’t give 0: it also compares what comes after “ana”. strncmp(a, b, 3) stops after 3 characters, which is exactly “does it start with?”.

What about names with accents, like “Lucía”? In UTF-8, the “í” takes 2 bytes, so strlen("Lucía") gives 6, not 5, and tolower doesn’t convert it. That’s why the sample data has no accents. For real accented text you need the wide-character functions (wchar.h), which are outside IC2.

**Why do I pass sizeof v[i].key and not 20 by hand?** Because if tomorrow you change MAX, the size updates itself. Careful: inside normalize, sizeof dest would give 8 (the size of a pointer), which is why the size has to be passed as a parameter.

Isn’t bubble sort slow? For 5 viewers it makes 10 comparisons (4 + 3 + 2 + 1); for a real hall you’d use qsort from stdlib.h, which receives a function that compares with strcmp. The logic of comparing and swapping structs is the same.

Forgetting dest[j] = '\0'; at the end of normalize. With the key initialised to "" it sometimes works by chance (the rest of the array was already zeros), but if you reuse a buffer, the string stays glued to the leftovers of the previous one.

strcmp sorts by ASCII codes: all capitals (65-90) go before all lowercase letters (97-122). Without normalising, “Zara” would come before “ana”. Another reason to compare lowercase keys.

Try it yourself

  • Make normalize capitalise the first letter of each word (“Lucia Perez”) with toupper, and use it to print nice names.
  • Replace bubble sort with qsort(v, n, sizeof v[0], compare) by writing a function int compare(const void *a, const void *b) that converts the pointers to const struct viewer * and compares their keys.

Program 3: the box office with dynamic memory

At the end of the day, the box office records each sale: movie, number of tickets and the price of each one. We don’t know how many sales there will be or how long the titles are (“Up” has 2 letters; others, 30). The program stores the sales in a dynamic array that grows with realloc, stores each title on the heap with the exact size, prints a summary with the amount of each sale and the total, counts the tickets for one movie and frees everything in the right order.

The idea

Two levels of dynamic memory:

  1. The array of sales: a block of struct sale that starts with 2 slots and doubles when it fills up, like the growing array in dynamic memory.
  2. Each title: instead of a fixed char movie[30] (which wastes 28 bytes with “Up” and doesn’t fit a long title), we store a char * pointing to a copy on the heap of strlen + 1 bytes.

And so as not to keep passing three loose variables (the pointer, how many there are and how many fit) to every function, we put them in another struct: struct box_office. The functions receive struct box_office *t and use the arrow: t->n, t->cap, t->sales.

Before coding, think about the order of the free calls. If you free the array first, the pointers to the titles that were inside disappear with it and you can no longer free the titles: a memory leak.

The code

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

struct sale {
    char *movie;    // pointer: each title takes up exactly what it needs on the heap
    int tickets;
    double price;   // price of one ticket
};

// A struct inside another: the dynamic array and its two counters travel together
struct box_office {
    struct sale *sales;
    int n;          // sales stored
    int cap;        // slots reserved
};

// Copies the text onto the heap: strlen + 1 so the '\0' fits
char *copy_text(const char *s) {
    char *copy = malloc(strlen(s) + 1);
    if (copy != NULL)
        strcpy(copy, s);
    return copy;
}

// Adds a sale; if the array is full, doubles its capacity with realloc
int add(struct box_office *t, const char *film, int tickets, double price) {
    if (t->n == t->cap) {
        int new_cap = t->cap * 2;
        // tmp: if realloc fails, t->sales still points to the old block and nothing is lost
        struct sale *tmp = realloc(t->sales, new_cap * sizeof *tmp);
        if (tmp == NULL)
            return 0;
        t->sales = tmp;
        printf("  (realloc: capacity %d -> %d)\n", t->cap, new_cap);
        t->cap = new_cap;
    }
    // v points to the first free slot
    struct sale *v = &t->sales[t->n];
    v->movie = copy_text(film);
    if (v->movie == NULL)
        return 0;
    v->tickets = tickets;
    v->price = price;
    t->n++;
    printf("  + %-12s -> %zu bytes for the title\n", film, strlen(film) + 1);
    return 1;
}

// First what is inside (each title) and then the array: the other way round we would lose the pointers
void release(struct box_office *t) {
    for (int i = 0; i < t->n; i++)
        free(t->sales[i].movie);
    free(t->sales);
    t->sales = NULL;
    t->n = t->cap = 0;
}

int main(void) {
    struct box_office t = {malloc(2 * sizeof(struct sale)), 0, 2};
    if (t.sales == NULL)
        return 1;
    printf("Sales of the day:\n");
    add(&t, "Coco", 12, 6.50);
    add(&t, "Interstellar", 30, 8.00);
    add(&t, "Up", 7, 6.50);
    // Another showing of the same movie: it is another sale
    add(&t, "Coco", 5, 6.50);
    add(&t, "Wonka", 18, 7.00);

    double total = 0;
    char line[64];
    printf("Summary:\n");
    for (int i = 0; i < t.n; i++) {
        struct sale *v = &t.sales[i];
        double amount = v->tickets * v->price;
        total += amount;
        // The line is built in a buffer of known size: snprintf never goes past 64
        snprintf(line, sizeof line, "  %-12s %3d x %5.2f = %7.2f", v->movie, v->tickets, v->price, amount);
        puts(line);
    }
    printf("Total for the day: %.2f euros\n", total);

    // How many Coco tickets? We compare the text, not the pointers
    int coco = 0;
    for (int i = 0; i < t.n; i++)
        if (strcmp(t.sales[i].movie, "Coco") == 0)
            coco += t.sales[i].tickets;
    printf("Coco tickets: %d\n", coco);
    release(&t);
    return 0;
}

The most delicate function is add:

// Adds a sale; if the array is full, doubles its capacity with realloc
int add(struct box_office *t, const char *film, int tickets, double price) {
    if (t->n == t->cap) {
        int new_cap = t->cap * 2;
        // tmp: if realloc fails, t->sales still points to the old block and nothing is lost
        struct sale *tmp = realloc(t->sales, new_cap * sizeof *tmp);
        if (tmp == NULL)
            return 0;
        t->sales = tmp;
        printf("  (realloc: capacity %d -> %d)\n", t->cap, new_cap);
        t->cap = new_cap;
    }
    // v points to the first free slot
    struct sale *v = &t->sales[t->n];
    v->movie = copy_text(film);
    if (v->movie == NULL)
        return 0;
    v->tickets = tickets;
    v->price = price;
    t->n++;
    printf("  + %-12s -> %zu bytes for the title\n", film, strlen(film) + 1);
    return 1;
}

Piece by piece:

  • **if (t->n == t->cap)**: the array is full when the stored sales equal the reserved slots. Then we ask for double with realloc and keep the result in tmp, not directly in t->sales: if realloc failed, it would return NULL and we’d have lost the only address of the old block.
  • **new_cap * sizeof *tmp**: sizeof *tmp is the size of a struct sale (8 for the pointer + 4 for the int + 4 of padding + 8 for the double = 24 bytes on 64-bit Fedora). That way there’s no need to write the type or work it out by hand.
  • **struct sale *v = &t->sales[t->n];**: the first free slot. t->sales[t->n] is a struct and & gives us its address, to fill it in with the arrow.
  • **copy_text(film)** reserves strlen(film) + 1 bytes and copies with strcpy. Here strcpy is safe because we’ve just reserved exactly what it needs.

The result in memory, after the five sales:

The box office in memoryThe box office struct keeps a pointer to the sales array, n equal to 5 and cap equal to 8. Each sale has a movie pointer to its title copied into the heap with strlen plus 1 bytes.struct box_office tsales●n5cap8t.sales: 8 slots (cap), 5 used (n)[0]●126.50[1]●308.00[2]●76.50[3]●56.50[4]●187.00[5]free[6]free[7]freeheapCoco\05 bytesInterstellar\013 bytesUp\03 bytesCoco\05 bytesWonka\06 bytes1st free(each title)2nd free(t.sales)
Two levels of dynamic memory: the array of structs and, inside each one, its title. They're freed from the inside out.

Output

Sales of the day:
  + Coco         -> 5 bytes for the title
  + Interstellar -> 13 bytes for the title
  (realloc: capacity 2 -> 4)
  + Up           -> 3 bytes for the title
  + Coco         -> 5 bytes for the title
  (realloc: capacity 4 -> 8)
  + Wonka        -> 6 bytes for the title
Summary:
  Coco          12 x  6.50 =   78.00
  Interstellar  30 x  8.00 =  240.00
  Up             7 x  6.50 =   45.50
  Coco           5 x  6.50 =   32.50
  Wonka         18 x  7.00 =  126.00
Total for the day: 522.00 euros
Coco tickets: 17

Let’s check it, sale by sale:

Salen beforecap beforerealloc?Title bytesn aftercap after
Coco02no4 + 1 = 512
Interstellar12no12 + 1 = 1322
Up22yes, 2 → 42 + 1 = 334
Coco34no544
Wonka44yes, 4 → 85 + 1 = 658

The amounts: 12 × 6.50 = 78; 30 × 8 = 240; 7 × 6.50 = 45.50; 5 × 6.50 = 32.50; 18 × 7 = 126. Total: 78 + 240 + 45.50 + 32.50 + 126 = €522. And the Coco tickets are those of its two sales: 12 + 5 = 17.

Notice that the two “Coco” sales each have their own copy of the title (5 bytes each). That’s why the final comparison uses strcmp(t.sales[i].movie, "Coco") == 0: the pointers are different, but the text is the same.

Questions you might ask

**Why char *movie and not char movie[30]?** With the array, every sale takes 30 bytes of title even if it’s “Up”, and a 30-letter title no longer fits. With the pointer, the struct only stores an address (8 bytes) and the text takes exactly what it needs. In exchange, you’re in charge of the malloc and the free of each title.

**Why isn’t v->movie = film; enough? Because you’d copy the address**, not the text. Here it would work because film points to literals that live for the whole program, but if the title came from a buffer that gets reused (like a line read from a file), every sale would end up pointing to the same text: the last one read.

**Why does release set t->sales = NULL and the counters to 0?** To leave the box office in a consistent state. If someone called release twice, free(NULL) does nothing, whereas freeing the same address twice breaks the program (the double free from dynamic memory).

**Why do I build the line with snprintf in a buffer and then puts?** To practise the pattern you’ll use when writing to files or building messages: assemble the text in a buffer of known size with no risk of overflowing it. Here line has 64 bytes and the longest one (” Interstellar 30 x 8.00 = 240.00″) takes 36.

**What would happen if I forgot the loop in release and only did free(t->sales)?** The program would work the same, but when compiled with -fsanitize=address AddressSanitizer would warn at the end with SUMMARY: AddressSanitizer: 32 byte(s) leaked in 5 allocation(s).: the 5 titles (5 + 13 + 3 + 5 + 6 = 32 bytes) nobody freed. I checked it by removing that loop.

malloc(strlen(s)) without the + 1. The strcpy writes the '\0' one byte outside the block. Often nothing visible happens… until the next malloc breaks. AddressSanitizer flags it as heap-buffer-overflow.

On Linux there’s strdup(s), which does exactly what copy_text does. It isn’t in standard C11 (it is in C23 and in POSIX), so here we write it by hand: that way you can see what it does inside.

Try it yourself

  • Add int best_seller(const struct box_office *t) that returns the index of the sale with the most tickets, and use it to print “The biggest sale: Interstellar (30)”.
  • Change add so that, if the movie already exists with the same price, it adds the tickets to that sale instead of creating another one (hint: search first with strcmp).

Common mistakes in this practice

  • Modifying a copy of the struct. struct seat b = room[f][a]; copies the seat; to change the hall you need struct seat *b = &room[f][a]; and the arrow.
  • Not checking the limits before indexing. A row ‘D’ in a 3-row hall is room[3], outside the matrix. Check first, touch later.
  • Comparing names without normalising. Spaces and capitals make strcmp say “different” about two names a person would read as the same.
  • **Forgetting the '\0' when building a string by hand.** If you write with an index of your own, the null goes in dest[j].
  • **Thinking that sizeof of a parameter gives the array size.** Inside a function, sizeof dest is the size of a pointer; the real size is passed as a parameter.
  • Freeing in the wrong order. First what’s inside each struct (the titles), then the array.

Watch it step by step in Python Tutor

In Python Tutor choose C and paste this reduced version of program 1, with a one-row hall and 8-byte names:

#include <stdio.h>

struct seat {
    int taken;
    char name[8];
};

int book(struct seat *b, const char *name) {
    if (b->taken)
        return 0;
    b->taken = 1;
    snprintf(b->name, sizeof b->name, "%s", name);
    return 1;
}

int main(void) {
    struct seat row[3] = {0};
    book(&row[1], "Lucia");
    int ok = book(&row[1], "Pepito");
    book(&row[2], "Maximiliano");
    printf("%d | %s | %s | %s\n", ok, row[0].name, row[1].name, row[2].name);
    return 0;
}

Its output:

0 |  | Lucia | Maximil

What to watch step by step:

  1. When row is created, the three seats appear with taken at 0 and the 8 bytes of name at \0: that’s the effect of = {0}.
  2. In the first call to book, the parameter b is an arrow pointing to row[1], not a copy. Watch the letters of “Lucia” fill in directly in main‘s seat.
  3. In the second call, b->taken is already 1, so the function returns 0 without touching anything: that’s why ok is 0 and Pepito doesn’t get in.
  4. In the third, notice that snprintf writes only 7 letters of “Maximiliano” and puts the \0 in slot 7: the output shows “Maximil”.
  5. row[0] is never touched: in the output it’s the empty gap between the first two bars.

Summary

  • A matrix of structs (struct seat room[3][6]) models table-shaped data; letters become indices by subtracting 'A'.
  • To modify a struct from a function you need its address: &room[f][a] and b->field.
  • snprintf(dst, sizeof dst, "%s", src) copies text without ever overflowing and always ends in '\0'.
  • Separate what you show from what you compare: normalise (lowercase, spaces) and compare with strcmp/strncmp.
  • A struct is swapped whole with =, arrays inside included; pointers, on the other hand, only copy addresses.
  • With char * in the struct, each text takes strlen + 1 on the heap, and it’s freed from the inside out.

Checklist: have you practised it?

  • Have you run all three programs and checked the outputs?
  • Can you explain why book needs a pointer to the seat and not a copy?
  • Could you write normalize without looking, putting the '\0' in the right place?
  • Do you know why “ana lopez” goes after “ana garcia” according to strcmp?
  • Do you know in which order the box office is freed and why?
  • Have you tried the “Try it yourself” tasks without looking at the theory?

If you answered yes to almost everything, you’re ready for the structs and strings in C exercises, where I no longer give you the code. See you there!

Next step: exercisesStructs and strings in C — exercises with the movie listings, your own string.h functions and viewer records that arrive broken (and the VIP ticket that always cost 8 euros)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *