Three Facts, One Object

Chapter 5 finished with a promise: a title, a page count and a price would stop travelling as three separate variables. Three loose variables mean three declarations, three parameters in every function that handles a book, three arguments to keep in the right order at every call, and one more fact about a book next month means editing every one of those signatures. A struct ends that. It is one object built out of named members, each with a type of its own, and a definition like struct Book { char title[32]; int pages; double price; }; creates the type rather than any object of it, which is why that line reserves no memory at all and struct Book pick; is the line that costs bytes. The semicolon after the closing brace is part of the syntax rather than decoration, and leaving it off produces the error everyone meets once, blamed on the line below rather than on the line that is wrong: error: expected ';', identifier or '(' before 'int', because the compiler is still reading a declaration you never finished. Read a struct against chapter 4's array and the split is clean. An array holds many objects of one type reached by an index, a distance from the start; a struct holds several objects of different types reached by a name, written with the dot operator as pick.pages. There is no way to ask a struct for member number 1, because its members are not a sequence you count through.

#include <stdio.h>

struct Book
{
    char title[32];
    int pages;
    double price;
};

int main(void)
{
    struct Book pick = {.title = "Deep C", .pages = 300, .price = 19.99};
    struct Book draft = {.title = "Untitled"};
    struct Book blank = {0};

    printf("pick:  [%s] %d pages, $%.2f\n", pick.title, pick.pages, pick.price);
    printf("draft: [%s] %d pages, $%.2f\n", draft.title, draft.pages, draft.price);
    printf("blank: [%s] %d pages, $%.2f\n", blank.title, blank.pages, blank.price);

    return 0;
}
pick:  [Deep C] 300 pages, $19.99
draft: [Untitled] 0 pages, $0.00
blank: [] 0 pages, $0.00

{.title = "Deep C", .pages = 300, .price = 19.99} is a designated initializer, and naming each member you set is the habit to form now. It reads as documentation at the point of use, and it survives someone reordering the members of the struct later, which the positional form {"Deep C", 300, 19.99} silently does not. It also carries chapter 4's initializer rule, and the middle line of that output is the proof: draft named only its title and came out with 0 pages and a price of 0.00 rather than with whatever the stack happened to hold, because a member left out of a designated initializer is zeroed. Push that to its limit and you get the struct spelling of an idiom you already have. struct Book blank = {0}; zeroes every member of any struct at any size, exactly as int counts[5] = {0}; zeroed a whole array. What has not changed is the case to avoid: struct Book pick; with no initializer at all gives every member an indeterminate value, and reading one before you write it is undefined behaviour.

The Type Is Called struct Book

Something in that program is easy to read straight past. The type's name is two words. struct Book is the type and the bare name Book does not exist, because Book on its own is a tag, an identifier that means nothing without the struct keyword in front of it. This is the single most common thing a C++ programmer carries into C by mistake, since C++ lets the tag stand alone, and it is worth seeing what dropping the keyword actually costs you.

book.c: In function 'main':
book.c:12:5: error: unknown type name 'Book'; use 'struct' keyword to refer to the type
   12 |     Book pick = {.title = "Deep C", .pages = 300, .price = 19.99};
      |     ^~~~
      |     struct

gcc names the fix in the message, which is generous, and then reports ten more errors underneath it, because a declaration whose type is unknown poisons every line that touches pick. Read the first error and ignore the rest; the cascade has one cause. One line makes the bare name real: typedef struct Book { char title[32]; int pages; double price; } Book; defines the type and an alias for it at once, after which Book pick; compiles and means precisely what struct Book pick; means. When you write one, give the tag and the alias the same name as above. There is no ambiguity, since C keeps tags in a namespace of their own, and keeping the tag means the type can still refer to itself, which is what the next lesson needs. This course writes struct Book in full and does not typedef it, so that every declaration and every parameter keeps saying out loud that this is an aggregate which gets copied. A great deal of real C takes the other side and typedefs everything, and both conventions are defensible; the part that is not optional is picking one and holding it.

Assignment Copies Every Member

Chapter 5 made a point of arrays not being assignable, and it still holds: second = first; on two char arrays is error: assignment to expression with array type. Wrap those same bytes in a struct and the restriction lifts. struct Book copy = original; is legal, and so is a plain copy = original; further down, because a struct is assigned by value, member by member, and an array member is copied along with everything else. That is the exception worth holding on to, since the array inside a struct is copied by an assignment that the very same array on its own would refuse.

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

struct Book
{
    char title[32];
    int pages;
    double price;
};

int same_book(struct Book a, struct Book b)
{
    return strcmp(a.title, b.title) == 0 && a.pages == b.pages && a.price == b.price;
}

void halve_price(struct Book book)
{
    book.price = book.price / 2.0;
    printf("inside the function: $%.2f\n", book.price);
}

int main(void)
{
    struct Book original = {.title = "Deep C", .pages = 300, .price = 19.99};
    struct Book copy = original;
    printf("same book: %d\n", same_book(original, copy));

    strcpy(copy.title, "Deeper C");
    copy.pages = 400;
    printf("original: %s, %d pages\n", original.title, original.pages);
    printf("copy:     %s, %d pages\n", copy.title, copy.pages);
    printf("same book: %d\n", same_book(original, copy));

    halve_price(original);
    printf("after the call:     $%.2f\n", original.price);

    return 0;
}
same book: 1
original: Deep C, 300 pages
copy:     Deeper C, 400 pages
same book: 0
inside the function: $9.99
after the call:     $19.99

copy started life identical to original and was then rewritten through its own members, and original did not move, which is what copying member by member means: two separate objects of 48 bytes each, sharing nothing at all. Hold on to that 48, because the members of a struct Book add up to 44 and the last section of this lesson is about the difference. halve_price is that same fact one level out. Chapter 3's rule that a function receives a copy of its argument has not been amended, only the object being copied has grown, so the price the function halves is the price in its own frame and the caller's book comes back at 19.99 untouched. Copying three members is cheap and copying fifty is not, and the way to hand a function a struct without copying it is a pointer to the struct, which is the whole of the next lesson.

Structs Do Not Compare with ==

same_book looks laborious, and the reason it is written out longhand is that the obvious spelling does not compile at all: a == b on two structs is error: invalid operands to binary == (have 'struct Book' and 'struct Book'). C defines no comparison operator for structs, neither == nor <, so you compare a struct member by member, and the function that does it is where your program writes down what "the same book" actually means. Notice which operator each member gets. The char array takes strcmp(a.title, b.title) == 0, because chapter 5's rule that == on strings compares addresses rather than characters is no less true inside a struct, and the numbers take == directly. a.price == b.price asks two doubles to be identical bit for bit, which is exactly right for a price copied from one struct to another and a poor test for values that arrived by arithmetic.

There is a shortcut nearly everyone reaches for once here, and it is wrong. memcmp(&a, &b, sizeof a) compares the two objects byte by byte, which sounds like the same question and is not, because a struct contains bytes that belong to no member of it. Seeing why needs a smaller struct.

sizeof Is Not the Sum of the Members

Take the smallest struct that shows the problem, one char next to one int. One byte plus four bytes is five bytes of members, and this program asks what the struct actually costs, where its two members sit, and what memcmp makes of two of them holding identical values.

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

struct Sample
{
    char flag;
    int count;
};

int main(void)
{
    struct Sample a;
    struct Sample b;

    memset(&a, 0x00, sizeof a);
    memset(&b, 0xFF, sizeof b);
    a.flag = 'y';
    a.count = 7;
    b.flag = 'y';
    b.count = 7;

    printf("its members add up to %zu\n", sizeof(char) + sizeof(int));
    printf("sizeof(struct Sample) is %zu\n", sizeof(struct Sample));
    printf("&a.flag  is %p\n", (void *)&a.flag);
    printf("&a.count is %p\n", (void *)&a.count);
    printf("members equal: %d\n", a.flag == b.flag && a.count == b.count);
    printf("memcmp equal:  %d\n", memcmp(&a, &b, sizeof a) == 0);

    return 0;
}
its members add up to 5
sizeof(struct Sample) is 8
&a.flag  is 0xfbff82df0020
&a.count is 0xfbff82df0024
members equal: 1
memcmp equal:  0

Eight, not five, and the two addresses say where the difference went. flag occupies one byte at offset 0 and count begins at offset 4 rather than at offset 1, so three bytes sit between the members owned by neither of them. Those are padding bytes, inserted so that each member lands at an address its type is happy with (a four-byte int on a four-byte boundary here), and the struct's total size is rounded up for the same reason, so that every element of an array of these structs stays aligned too. Which members get padding, and how much, is the implementation's business in exactly the way that the address of a frame and the layout of the heap were, so the rule is blunt: never compute a struct's size by adding up its members, and never assume a member sits at the offset your arithmetic predicts. sizeof is the only correct answer to how large a struct is, and unlike your arithmetic it is correct on every implementation.

The last two lines are the memcmp trap paid off in full. Both objects were filled with a known byte pattern first, one all zeroes and the other all 0xFF, and then given identical member values. Every member matches, and memcmp still reports a difference, correctly: the padding bytes are part of the object representation it compares, they were never part of any member, and the standard leaves their values unspecified, so the assignments above had no reason to touch them and no obligation to. That is the entire case against memcmp on structs. It answers "are these bytes identical" when the question you asked was "are these books the same", and the two answers differ by exactly the bytes nobody wrote.

Key Takeaways

  • A struct gathers members of different types under one name, reached by name with the dot operator rather than by index. struct Book { ... }; defines a type and reserves nothing, and the semicolon after the closing brace is required.
  • The type is struct Book; the bare tag Book is not a type name unless a typedef makes it one, which is C++'s habit and C's most common import error. When you do typedef, give the tag and the alias the same name. This course writes struct in full throughout.
  • Initialize with designated initializers, {.pages = 300}, which survive member reordering and zero every member you leave out; = {0} zeroes the whole struct, and no initializer at all leaves every member indeterminate.
  • Assignment copies every member, the array member included, even though that same array on its own could not be assigned. Passing a struct to a function copies it too, so the function's changes never reach the caller.
  • == is not defined for structs. Compare member by member, with strcmp for a char array member, and never with memcmp.
  • sizeof(struct S) is not the sum of its members, because the implementation inserts padding to keep members aligned. Ask sizeof, never your own arithmetic, and remember that padding bytes hold unspecified values.