Capstone: A Complete Program
Bring it all together in one program with structs, heap allocation, string parsing, and error handling, memory-clean under AddressSanitizer from the first run.
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 amake_and afree_written by hand as a pair, with the allocation checked againstNULLand the failure path leaving nothing half built. - A growable array of structs is chapter 5's buffer with
sizeof *grownin 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] = *itemis 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
Nnames plus one array, which is exactlyN + 1frees. - Assert invariants, check library calls, validate input, three different bins that never swap.
count <= capacityis an assertion because this file guarantees it;mallocis 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.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Capstone: A Complete Program - Quiz
Test your understanding of the lesson.
Practice Exercises
Finish the Capstone: the remove Command
You are given the capstone program complete except for one command, and it is the command that undoes an add. main already has a remove branch wired to inventory_remove(struct Inventory *inventory, const char *name), which returns 1 when it removed something and 0 when nothing carried that name; on 1 main prints removed followed by the name on standard output, and on 0 it reports the miss on standard error and prints nothing at all. Everything else is finished and correct: make_item and free_item, the doubling append in inventory_add, inventory_find, inventory_total, inventory_free, and the fgets and sscanf command loop. What you write is the body of inventory_remove, which the template stubs out so that the program still compiles warning-free and still runs with defined behaviour without it, reporting remove: not implemented on standard error and failing only the test cases that use remove. Three steps. First, call inventory_find(inventory, name), which hands back a pointer to the element or NULL when no item carries that name, and return 0 immediately on NULL, since main is the one that reports the miss. Second, free the name of the element you found with free_item, because the array element owns that string: the assignment inside inventory_add copied a pointer rather than the characters, and from that moment the element is the only owner, so nothing else in the program will ever free it and skipping this step leaks it on every single removal. fail_on_memory_leak is set on this exercise, so a run that prints every expected line and loses one name still fails. Third, close the gap the removal left, and the way to do it is to copy the LAST element over the one you have just emptied: *found = inventory->items[inventory->count - 1]; followed by inventory->count -= 1;. That one struct assignment moves all three members, the surviving name pointer among them, so the slot has an owner again and the element that used to sit at the end is no longer reachable at its old index, which is exactly why the decrement has to come after it. Do it unconditionally, including when the element you found is already the last one, since a struct assigned to itself is defined and costs nothing while an if written to avoid it is one more branch to get wrong. The order of those two steps is chapter 6's order in a new place: free the name first and copy over the slot second, because copying first would overwrite the only pointer to the string and leak it, and freeing after the copy would free the survivor's name instead and leave a freed pointer sitting in the array. Note what removal deliberately does not do. It does not shrink the array and it does not call realloc, so capacity stays wherever the doublings left it, the surviving items keep their addresses except for the one that moved, and inventory_free at the end still frees exactly one block plus one name for each of the count items still present. Nothing else in the file needs changing. No message wording is checked, because every diagnostic in this program goes to standard error and the checker compares standard output only, and main returns 0 on every path including the empty input, since a nonzero exit status is a failure however right the printed output looks.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!