Structs, enums, and organization recap

Chapter 5 left you with a title that knew where it ended and no way to keep it next to the other facts about the book it named. This chapter gave those facts one name. A struct gathers members of different types into a single object reached by name rather than by index, and nearly everything else in the chapter follows either from that or from programs growing large enough to need it. The type is two words, struct Book, and the bare tag is not a type name, which is a rule that then repeats itself unchanged for enum and for union. An aggregate is copied member by member wherever you assign one or pass one, which is chapter 3's by-value rule with a much bigger object in it, and which becomes a trap the moment one of those members is a pointer, because copying a pointer copies an address rather than the thing at it. Once a struct owns heap memory, chapter 4's ownership question arrives with a second half attached, namely the order the frees go in. Then the last lesson stops talking about data at all and turns to programs, splitting one file into several, at which point chapter 3's prototype grows into a header, static acquires a second meaning with nothing in common with its first, and the failures move a stage later: names the compiler used to check within one file are now checked by the linker across all of them. Let's review each lesson before you test yourself.

Structs: Grouping Related Data

Three facts about a book meant three declarations, three parameters in every signature and three arguments in the right order at every call, and struct Book { char title[32]; int pages; double price; }; ends all of that by building one object out of named members. That definition creates a type and reserves no memory, so struct Book pick; is the line that costs bytes, and the semicolon after the closing brace is syntax rather than decoration, its absence producing error: expected ';', identifier or '(' before 'int' blamed on the innocent line below. Set an array beside it and the split is clean: an array holds many objects of one type reached by an index, a distance, while a struct holds several objects of different types reached by a name, written with the dot operator as pick.pages, and there is no way to ask for member number 1 because members are not a sequence you count through. The type's name is two words. struct Book is the type and the bare Book is only a tag, meaningless without the keyword, which is the single most common thing carried into C from C++ and which gcc answers with error: unknown type name 'Book'; use 'struct' keyword to refer to the type followed by ten more errors that all have that one cause. A typedef giving the tag and the alias the same name makes the short spelling real; this course writes struct in full so that every declaration keeps saying out loud that this is an aggregate which gets copied. Initialize with a designated initializer, {.title = "Deep C", .pages = 300}, which documents itself at the point of use and survives someone reordering the members later, where the positional form silently does not, and which carries chapter 4's rule that a member you leave out is zeroed, so = {0} zeroes a struct of any size while no initializer at all leaves every member indeterminate. Assignment copies every member, the char array member included, which is the exception worth holding: that same array on its own is error: assignment to expression with array type, and inside a struct it is copied by the assignment it would have refused. Passing a struct to a function copies it too, so a function that halves book.price halves its own copy and the caller's book comes back untouched. What you cannot do is compare two of them, since a == b is error: invalid operands to binary == (have 'struct Book' and 'struct Book'), so you compare member by member in a function that writes down what "the same book" means, with strcmp(a.title, b.title) == 0 for the array member because chapter 5's rule about == on strings is no less true inside a struct. And memcmp is not the shortcut it looks like, because sizeof(struct S) is not the sum of its members: the implementation inserts padding so each member lands on an address its type is happy with, one char next to one int costing 8 bytes rather than 5, and those padding bytes belong to no member and hold unspecified values, so two objects whose every member is equal can and did compare unequal byte for byte. Ask sizeof, never your own arithmetic.

Structs, Pointers, and the Heap

Handing a function the address instead of the object is chapter 3's fix with a 48 byte argument in it, and the only new thing is how you reach a member through the pointer. (*p).pages is the longhand and those parentheses are required, because the dot binds tighter than the *, so *p.pages asks a pointer for a member no pointer has and gcc replies by naming the operator you wanted: error: 'p' is a pointer; did you mean to use '->'?. p->pages means exactly (*p).pages, dereference and select in one token, so the dot is what you write when you hold the struct and the arrow is what you write when you hold its address. A struct Book * parameter therefore reaches the caller's object and can genuinely change it, while const struct Book * is the default for a function that only reads one: nothing is copied whatever the struct's size, and the promise is checked rather than trusted, since assigning through it is error: assignment of member 'price' in read-only object. An array of structs is chapter 4's array with a struct element type and neither half bends, so each element takes its own braced designated initializer, a member is reached as shelf[1].title with the subscript picking the struct and the dot picking the member, and sizeof(shelf) / sizeof(shelf[0]) still counts elements only in the scope that declared the array, which is why functions take a pointer and a count. Those elements sit sizeof(struct Book) apart, padding included, 48 bytes rather than the 44 the members add up to, because spacing them by the members alone would leave the second book's double misaligned. On the heap nothing is new except the sizing idiom returning to its full form: malloc(sizeof *book) asks for one of whatever book points at, padding and future members included, and malloc(count * sizeof *shelf) asks for an array of them, with the NULL check, the arrow and exactly one free unchanged. A make_book returning such a pointer is chapter 4's ownership convention with a struct in it, and the obligation still travels in the name and the documentation rather than in the type. Then the fact that changes everything: a struct holding a pointer is copied shallowly, so both copies point at the same object. Assignment copied the address, one string had three structs claiming it, and writing through one of them changed what the others printed. C has no copy constructor and no deep copy to reach for; if you want a second string you call the copying function yourself. That makes freeing "both" a mistake reported as attempting double-free, because chapter 4's rule is one free per allocation and a shallow copy is not an allocation. The order matters too: free the members, then the struct, because free(book); free(book->title); reads a pointer out of a block that no longer exists, which is heap-use-after-free and which gcc warns about as pointer 'book' used after 'free' before you ever run it.

Enums and Unions

int heading = 2; keeps the fact and throws away its meaning, and an enum is C's way of saying the meaning out loud: enum Direction { DIRECTION_NORTH, DIRECTION_EAST, DIRECTION_SOUTH, DIRECTION_WEST }; names a set of values, reserves no memory and needs its closing semicolon, exactly as a struct definition does. Enumerators count up from 0 in the order you wrote them, an explicit value resets the count and the next one resumes from there, and the type is enum Direction while the bare tag is not a type name, which is lesson 1's rule with a different keyword. Two things surprise people. First, enum constants have type int, four bytes and printed with %d, because C has no enum class, no scoped enumeration and no type that refuses to convert to an integer. Second, the enumerators land in the scope enclosing the enum rather than inside it, so a second enum wanting a plain NORTH is error: redeclaration of enumerator 'NORTH', and the fix is convention rather than language: prefix every enumerator with the name of its enum. Because zero is the first enumerator, the slots chapter 4 zeroed for you read as DIRECTION_NORTH rather than as nothing, which is the argument for making the first enumerator a sensible default or an explicit none. A switch over an enum is chapter 2's switch with integer constant labels that happen to have names, and gcc will tell you warning: enumeration value 'DIRECTION_WEST' not handled in switch only while the switch has no default, since a default handles everything by definition. The two protections are in tension and this course takes a side: always write the default, because the exhaustiveness warning guards a mistake you make while editing while the default guards a value that was never in the set, and enum Direction rogue = 42; compiles without a word under -Wall -Wextra. An enum is documentation, not a guarantee. The enum also settles chapter 2's IOU about const, because a const int is an object you may not assign to rather than a value known at compile time, so static int table[capacity]; is error: storage size of 'table' isn't constant while enum { CAPACITY = 8 }; is an integer constant expression and sizes the array happily. That makes enum { NAME = value }; the idiomatic C spelling of a compile-time integer constant, and a better answer than #define because enumerators are typed, obey scope and survive into the debugger where a macro has already been erased. A union is declared like a struct with one word changed, and the change is the whole idea: every member shares the same bytes and the union is as large as its largest member, so it holds one member at a time and writing one overwrites another. Reading a member other than the one written last is implementation defined, never a technique, which leaves the union unable to say which member is live, so you say it: put an enum tag next to the union inside a struct, switch on the tag before every read, and write the member and the tag together in one place. Get that wrong and nothing is diagnosed and nothing is undefined; the program simply reports the low half of a double as a number of items, at run time, in a perfectly plausible format.

Headers and Multi-File Programs

One file holding every struct, every helper and main stops working sooner than you would think, and the fix is the module: one concern given a header with the extension .h and a source file with the extension .c, arranged by one sentence that the rest of the lesson is the compiler and the linker enforcing. Headers declare; source files define. The header carries what a caller needs, which is the struct definitions it must have to declare a variable, the prototypes chapter 3 introduced, and extern int summarize_calls; for a shared variable, and not one of those reserves a byte. It must also compile on its own, so a header using size_t includes <stddef.h> itself rather than hoping its includer got there first, and the way you prove that is to include a module's own header first in its .c file, above everything else, where nothing has been included yet to cover for it. Quotes and angle brackets differ in where the preprocessor looks: "..." for your project's headers, <...> for the implementation's. Each .c file is then preprocessed and compiled on its own into a translation unit and an object file, which is why the compiler knows nothing about stats.c while compiling main.c beyond what stats.h claimed, and the linker joins them afterwards by matching names. Say the division once and most multi-file confusion dissolves: the compiler checks that you used a name correctly, and the linker checks that the name exists exactly once. Every failure in the lesson is one of those halves. A header reached by two routes in one translation unit is error: redefinition of 'struct Summary' from the compiler, and every header gets an include guard, named after its path so two files called stats.h cannot collide, and never spelled with a leading underscore and capital or a double underscore anywhere, since those are reserved for the implementation. #pragma once does the same job in one line and gcc supports it, and this course teaches the guard anyway because #pragma once is not in the C standard. The other failures wait for the linker. Never put a function body or a variable definition in a header, because two translation units including it is multiple definition from ld with or without an initializer, which is why the shared variable is declared extern int summarize_calls; in the header and defined int summarize_calls = 0; in exactly one .c file. And static at file scope is not the static chapter 3 taught: on a local it is storage duration, when the object exists, and at file scope it is internal linkage, who can see the name. Marking everything outside the interface static lets two files hold a private helper of the same name without colliding, and lets the compiler report warning: 'halved' defined but not used, which it cannot do for a name the whole program can reach. Finally, chapter 1's undefined reference to 'summarize' is now fully explained, since a declaration is a promise that the definition exists somewhere else and the linker is where that promise is collected. undefined reference and multiple definition are its only two complaints: the name was found zero times, or more than once. Here is the chapter in one program.

#include <stdio.h>

enum ReadingKind
{
    READING_COUNT,
    READING_CELSIUS
};

enum { LOG_SIZE = 3 };

struct Reading
{
    enum ReadingKind kind;
    union
    {
        int count;
        double celsius;
    } value;
};

static void print_reading(const struct Reading *reading)
{
    switch (reading->kind)
    {
        case READING_COUNT:
            printf("%d items\n", reading->value.count);
            break;
        case READING_CELSIUS:
            printf("%.1f C\n", reading->value.celsius);
            break;
        default:
            printf("unknown reading\n");
            break;
    }
}

int main(void)
{
    struct Reading log[LOG_SIZE] = {
        {.kind = READING_COUNT, .value.count = 12},
        {.kind = READING_CELSIUS, .value.celsius = 36.6},
        {.kind = 9},
    };
    printf("a Reading is %zu bytes, %zu of them the union\n", sizeof(struct Reading), sizeof log[0].value);

    for (int i = 0; i < LOG_SIZE; ++i)
    {
        print_reading(&log[i]);
    }

    return 0;
}

That prints a Reading is 16 bytes, 8 of them the union, then 12 items, 36.6 C and unknown reading, with nothing on standard error. Every line of it is a rule from this chapter. enum { LOG_SIZE = 3 }; is the compile-time constant a const int could not be, sizing the array and bounding the loop from one name. The tag rule is in force three times over, since enum ReadingKind, struct Reading and the union are all spelled with their keywords, and the enumerators carry the READING_ prefix that keeps them from colliding at file scope. Each element takes its own braced designated initializer, naming the union member it writes as .value.count or .value.celsius and leaving out whatever it does not set, so the third reading's union is zeroed while its tag keeps the 9 it was handed. print_reading is the shape lesson 2 made the default: a const struct Reading *, so nothing of any size is copied and the compiler refuses any write through it, reached with the arrow because what the function holds is an address, then ->value.count, one selection for the union member and another for the member inside it. The switch reads the tag before it reads anything else, and its default is what turns the third reading into unknown reading rather than into a number, because a tag of 9 is an ordinary int that no diagnostic was obliged to question. The first output line is the union's specification and lesson 1's padding in one sentence, since 8 is the size of the largest member rather than the 12 its two members add up to, and 16 is 4 for kind plus 4 the implementation inserted plus those 8. And static on print_reading is lesson 4's file scope keyword, internal linkage rather than storage duration, which changes nothing in a single file and is exactly what you would write if this were reading.c with a reading.h beside it declaring only struct Reading and the printer.

Key Terminology

  • Struct, tag and member: one object built from named members of different types, defined by struct Book { ... }; which reserves nothing and needs its closing semicolon; the type is struct Book and the bare tag Book is not a type name unless a typedef makes it one, a rule that holds identically for enum and union
  • Designated initializer: {.pages = 300}, which documents itself, survives members being reordered, and zeroes every member you leave out, where no initializer at all leaves them indeterminate and = {0} zeroes a struct of any size
  • Padding, sizeof and comparison: the bytes the implementation inserts to keep members aligned, which is why sizeof(struct S) exceeds the sum of its members and why array elements sit sizeof(struct S) apart; structs have no ==, so compare member by member with strcmp for a char array, never with memcmp, whose answer includes padding bytes nobody wrote
  • Dot, arrow and const struct T *: the dot for a struct you hold and p->member, exactly (*p).member, for an address you hold; a pointer parameter reaches the caller's object and copies nothing, so const struct T * is the default for a function that only reads, checked by the compiler rather than trusted
  • Shallow copy and teardown order: assignment copies a pointer member's address rather than what it points at, so both copies share one allocation and freeing "both" is a double free, one free belonging to each allocation; free the members and then the struct, since the other order reads a member out of a block that is already gone
  • Enum, the prefix rule and enum { NAME = value };: named int constants counting from 0, with no scoping of their own so every enumerator carries its enum's name as a prefix, freely assignable from any integer so every switch on one keeps its default; an enumerator is an integer constant expression and so is the idiomatic C compile-time constant, usable as an array size where a const int is not
  • Union, tagged union, and the module: members sharing one set of bytes with the size of the largest, where reading a member other than the one last written is implementation defined, so an enum tag stored beside the union in a struct is what records which member is live; and the same declare-versus-define split scaled up, headers declaring and source files defining, one include guard per header, extern in the header with the definition in exactly one translation unit, static at file scope for internal linkage, and undefined reference and multiple definition as the linker's only two complaints

Looking Forward

You can now group facts into one object, hand it around by pointer without copying it, name a program's states instead of numbering them, keep a tag honest beside a union, and split a program into files that a linker will join, which together are most of what separates a C program from a C exercise. What is left is the part that decides whether a working program is a trustworthy one. Chapter 7 opens with floating point in depth, since a.price == b.price was fine for a double copied from one struct to another and is a poor test for anything that arrived by arithmetic, and the reason is worth understanding rather than working around. Then handling failure turns the NULL checks and the return values you have been writing into a deliberate strategy, followed by assert and reading diagnostics, which is about catching the mistake at the moment it happens and about reading what the compiler, the linker and the sanitizer are actually telling you. Then the course gathers every undefined behaviour it has named into the undefined behaviour catalogue, one place to see why the standard promising nothing is a rule about your whole program rather than about one line. The chapter finishes with a capstone, a complete program that reads real input, allocates what it needs, structures what it read, and gives all of it back.