Deciding Who Owns What

This is the last program of the course and there is nothing new in it. It reads commands from standard input for as long as there are any, keeps a list of stock items in memory while it runs, and gives every byte back before it exits: add <name> <quantity> <price> records an item, find <name> prints one, total prints what the stock is worth. What follows is the whole program in three blocks which concatenate, in that order, into one file, with nothing elided and nothing added between them. Every decision in it is one of this course's rules being applied, and the point of the lesson is to watch the rules do the deciding, so each one is named with the chapter it came from. The first decision is the one C forces you to make before any other: what is on the heap, and who is responsible for freeing it.

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

struct Item
{
    char *name;
    int quantity;
    double price;
};

struct Inventory
{
    struct Item *items;
    size_t count;
    size_t capacity;
};

static int make_item(struct Item *item, const char *name, int quantity, double price)
{
    item->name = malloc(strlen(name) + 1);

    if (item->name == NULL)
    {
        return 0;
    }
    strcpy(item->name, name);
    item->quantity = quantity;
    item->price = price;
    return 1;
}

static void free_item(struct Item *item)
{
    free(item->name);
    item->name = NULL;
}

struct Item holds a char *name, so it owns a string rather than containing one (chapter 6 lesson 2), and C supplies neither a constructor to allocate it nor a destructor to release it. make_item and free_item are those two absent things written by hand, named as a pair so that a reader can see they are one. Inside make_item, malloc(strlen(name) + 1) is chapter 5's sizing with the terminator counted, and its result is checked against NULL before anything touches it (chapter 4). The shape of the function is chapter 7 lesson 2's convention: a status returned, the real result written through an out-parameter, and on the failure path nothing at all is written, so a caller who ignores the status never finds a half-built item. free_item frees the member and then sets the pointer to NULL, which is not superstition here but load bearing, because the array is about to contain items whose names have already been released and free(NULL) does nothing, making a second pass over them harmless. struct Inventory is chapter 5's capacity and length pair with a struct for an element type: count is where the next item goes, capacity is the number you check before writing, and they have no reason to agree.

The Array That Grows Under Structs

Chapter 5 grew a char buffer with realloc; this grows an array of structs, and that is the one genuinely new construction in the program. Nothing about realloc changes: the request is capacity * sizeof *grown rather than a byte count, which is chapter 4's sizing idiom asking for whole elements, padding included. The result goes into a temporary named grown, which is checked, and only then is inventory->items overwritten (chapter 5 lesson 4), because p = realloc(p, n) on the day the allocator says no destroys the only pointer to a block that is still live and still yours. Capacity doubles rather than growing by one, which reaches n items in about log2(n) reallocations instead of n, and the first doubling starts from zero through realloc(NULL, size), which is malloc and so needs no special case at the start.

static int inventory_add(struct Inventory *inventory, const struct Item *item)
{
    assert(inventory->count <= inventory->capacity);

    if (inventory->count == inventory->capacity)
    {
        size_t capacity = (inventory->capacity == 0) ? 2 : inventory->capacity * 2;
        struct Item *grown = realloc(inventory->items, capacity * sizeof *grown);

        if (grown == NULL)
        {
            return 0;
        }
        inventory->items = grown;
        inventory->capacity = capacity;
    }
    inventory->items[inventory->count] = *item;
    inventory->count += 1;
    return 1;
}

static struct Item *inventory_find(struct Inventory *inventory, const char *name)
{
    for (size_t i = 0; i < inventory->count; ++i)
    {
        if (strcmp(inventory->items[i].name, name) == 0)
        {
            return &inventory->items[i];
        }
    }
    return NULL;
}

static double inventory_total(const struct Inventory *inventory)
{
    double total = 0.0;
    for (size_t i = 0; i < inventory->count; ++i)
    {
        total += inventory->items[i].quantity * inventory->items[i].price;
    }
    return total;
}

static void inventory_free(struct Inventory *inventory)
{
    for (size_t i = 0; i < inventory->count; ++i)
    {
        free_item(&inventory->items[i]);
    }
    free(inventory->items);
    inventory->items = NULL;
    inventory->count = 0;
    inventory->capacity = 0;
}

Three lines there are worth stopping on. assert(inventory->count <= inventory->capacity) is chapter 7 lesson 3's third bin and not the first: it validates nothing the user typed, it is a claim about this file's own code, that no path here has ever let count climb past capacity, and it is what licenses the == on the next line to mean "full" rather than the defensive >= you would write if you did not know. Then inventory->items[inventory->count] = *item; is a struct assignment, which copies all three members including the name pointer, so it is chapter 6 lesson 2's shallow copy happening on purpose. Here the shallow copy is exactly what you want, because it is an ownership transfer: the caller's item and the array element name the same string for one instant, and from the next instant the array element is its owner and the caller's copy is a stale duplicate to be neither used nor freed. That is why inventory_free is the one place that frees an item, and why its order is every name first, then the block holding the structs (chapter 6 lesson 2's "members, then the struct", applied to a whole array). Freeing items first would leave the names unreachable, and reading items[i].name afterwards to catch them would be heap-use-after-free. Count the allocations in the finished program and there are N names plus one array, and inventory_free performs exactly N + 1 frees.

The Loop That Reads Commands

The reading is chapter 5's pattern unchanged: fgets bounded by sizeof(line), looping while it does not return NULL, then sscanf on the line it produced, with the returned count saying how many of the four fields were really there. Two details differ from chapter 5's version and both are deliberate. There is no newline strip, because %s, %d and %lf all skip leading whitespace and stop at the next, so the trailing newline never becomes part of a token; chapter 5 stripped it because strcmp was about to compare the whole line. And %15s and %63s carry field widths, which is the missing size that made bare scanf("%s", word) impossible to make safe: the width is what tells the conversion how much room it has, and it must be one less than the array to leave the terminator its byte.

int main(void)
{
    struct Inventory inventory = {NULL, 0, 0};
    char line[256];

    while (fgets(line, sizeof(line), stdin) != NULL)
    {
        struct Item item = {NULL, 0, 0.0};
        char command[16] = "";
        char name[64] = "";
        int quantity = 0;
        double price = 0.0;
        int fields = sscanf(line, "%15s %63s %d %lf", command, name, &quantity, &price);

        if (strcmp(command, "add") == 0 && fields == 4 && quantity >= 0 && price >= 0.0)
        {
            if (!make_item(&item, name, quantity, price) || !inventory_add(&inventory, &item))
            {
                free_item(&item);
                fprintf(stderr, "add: out of memory\n");
                continue;
            }
            printf("added %s x%d at $%.2f\n", name, quantity, price);
        }
        else if (strcmp(command, "find") == 0 && fields >= 2)
        {
            struct Item *found = inventory_find(&inventory, name);
            if (found == NULL)
            {
                fprintf(stderr, "find: no item named [%s]\n", name);
                continue;
            }
            printf("%s x%d at $%.2f\n", found->name, found->quantity, found->price);
        }
        else if (strcmp(command, "total") == 0)
        {
            printf("total $%.2f\n", inventory_total(&inventory));
        }
        else if (fields >= 1)
        {
            fprintf(stderr, "bad line [%s]\n", command);
        }
    }
    inventory_free(&inventory);
    return 0;
}

Dispatch is strcmp(command, "add") == 0, spelled against zero because strcmp returns an ordering and not a boolean (chapter 5 lesson 5), and every branch guards on fields as well as on the word, so a line beginning add with only two fields on it is not treated as an add at all. Every complaint goes to standard error and no complaint ends the program (chapter 7 lesson 2): a bad line is reported and the loop reads the next one, which is the recovery that reading whole lines buys you. The declarations at the top of the loop body are re-initialized on every iteration for the same reason, because sscanf leaves untouched whatever it did not convert and a price left over from three lines ago would otherwise be spent as though it had been read. The failure branch of add is the ownership rule in one line: both failures leave the item owned by this frame, so one free_item covers them, releasing the name if make_item got that far and doing nothing if it did not. Only after inventory_add returns 1 has ownership moved, and from that point the local item is never touched again.

$ printf 'add bolt 10 0.25\nadd washer 4 0.10\nadd hinge 2 3.50\nsell bolt 1\ntotal\nfind washer\n' | ./inventory
added bolt x10 at $0.25
added washer x4 at $0.10
added hinge x2 at $3.50
total $9.90
washer x4 at $0.10

The sell line produced bad line [sell] on standard error, where it neither joined the results nor stopped the run, and the program exited 0 with AddressSanitizer silent: no overflow, no use after free, and no leak report, on this session and on an empty input and on one that ends mid-command. That silence was designed in rather than debugged in, which is the entire argument of the last seven chapters. One honest note on the money. total $9.90 is %.2f doing its job, and the double behind it is 9.9000000000000004, because a price of 0.25 is exact in binary while 0.10 is not (chapter 7 lesson 1). %.2f rounds for display and changes nothing about the stored value, so it is the right way to print money and the wrong thing to trust if you ever need two totals to compare equal.

Key Takeaways

  • Decide ownership before you write anything else. A struct holding a char * owns a string, so it needs a make_ and a free_ written by hand as a pair, with the allocation checked against NULL and the failure path leaving nothing half built.
  • A growable array of structs is chapter 5's buffer with sizeof *grown in place of a byte count. Capacity doubles, realloc(NULL, n) starts it from empty with no special case, and the result always lands in a checked temporary before the real pointer is overwritten.
  • items[count] = *item is a shallow copy used deliberately as an ownership transfer. Both name the same string for an instant; afterwards the array element is the owner and the local must be neither used nor freed. On the failure path ownership never moved, so the local is the one that must be freed.
  • Tear down in the same order you built up, inverted: every item's name first, then the block holding the structs. Count the allocations and there are N names plus one array, which is exactly N + 1 frees.
  • Assert invariants, check library calls, validate input, three different bins that never swap. count <= capacity is an assertion because this file guarantees it; malloc is checked because it can genuinely fail; a typed command is validated because a user can type anything, and the complaint goes to standard error while the loop carries on.