Structures, Enumerations, and Unions
Good C programs do not merely store bytes; they name relationships and make each valid state obvious. Structures group fields that belong together, enumerations label a closed set of choices, and unions save storage when one object can validly take one of several forms.
Choose the representation that matches the promise
Structures model one coherent record
Use a struct when several values describe one thing. The declaration defines a new shape; a variable of that type reserves storage for every field. Unlike an array, fields can have different types and are selected by name.
struct Product {
char code[12];
double price;
int stock;
};
int main(void) {
struct Product keyboard = {"KB-104", 49.99, 12};
keyboard.stock = keyboard.stock - 1;
printf("%s: %.2f (%d left)\n", keyboard.code, keyboard.price, keyboard.stock);
return 0;
}struct Product is the type name here, and keyboard is one object with all three fields. Use the dot operator for an object: keyboard.price. A field is not a separate global variable; it belongs to the structure object selected before the dot.
Run a record update through named fields
This bounded runner reads the Reading record, applies the visible field update, and prints the resulting record. Edit the designated initializer or the value update, then run it again.
Edit the model, predict the result, then run it.
Initialize records deliberately
Positional initialization follows field order and becomes fragile when the type evolves. C also supports designated initializers, which name each target field and are easier to review.
struct Product keyboard = {
.code = "KB-104",
.price = 49.99,
.stock = 12,
};Prefer designated initializers in examples, tests, and long-lived code where a field-order mistake would be expensive. The compiler can then connect each supplied value to its intended field. A member you omit receives zero initialization when the object has static storage or when you use an initializer for an automatic aggregate; still make important defaults visible in code.
Use dot for an object and arrow for a pointer
When a function receives the address of a structure, it has a pointer. The arrow operator selects a member through that pointer: product->stock means (*product).stock. The parentheses in the expanded form are essential because dot binds before unary *.
int sell_one(struct Product *product) {
if (product == NULL || product->stock <= 0) {
return 0;
}
product->stock -= 1;
return 1;
}
void print_product(const struct Product *product) {
if (product == NULL) {
return;
}
printf("%s: %.2f\n", product->code, product->price);
}struct Product product
The function receives a complete copy. Small read-only records can be passed this way, but changes do not update the caller.
struct Product *product
The function may update the caller-owned record after validating the pointer and its field invariants.
const struct Product *product
The function avoids a copy and promises not to change fields through this access path.
Arrays of structures keep related fields aligned
An array of structures is often safer than parallel arrays because each row of data travels as one object. With struct Product inventory[3], inventory[index].price and inventory[index].stock refer to fields from the same product at the same index.
double inventory_value(const struct Product products[], int count) {
double total = 0.0;
for (int index = 0; index < count; index++) {
total += products[index].price * products[index].stock;
}
return total;
}As with every array parameter, products converts to a pointer and does not retain its length. Keep the count in the contract, validate it before the loop, and use the same bound for every field access.
Enumerations give states readable names
An enum declares integral constants with domain-specific names. It makes control flow easier to read and lets you avoid unexplained literals such as 3 scattered through your program.
enum OrderStatus {
ORDER_PENDING,
ORDER_PAID,
ORDER_SHIPPED,
ORDER_CANCELLED,
};
const char *status_label(enum OrderStatus status) {
switch (status) {
case ORDER_PENDING: return "pending";
case ORDER_PAID: return "paid";
case ORDER_SHIPPED: return "shipped";
case ORDER_CANCELLED: return "cancelled";
default: return "invalid status";
}
}The default underlying values begin at zero and increase by one, but your code should rely on named constants—not their current numbers. Assign explicit values only when an external format, protocol, or stored data contract requires stable numeric codes.
Run a named state instead of a magic number
The runner validates the declared ConnectionState names and reports the active enum constant with its current ordinal. It makes visible why names are the stable contract.
Edit the model, predict the result, then run it.
Unions trade independent fields for shared storage
Every structure field has its own storage. Union members instead start at the same location and share the union's storage. Write one member when the object represents one variant at a time; do not treat a union as if it holds every member independently.
union SettingValue {
int whole_number;
double decimal_number;
char label[24];
};
union SettingValue value = { .decimal_number = 1.25 };The union is sized to accommodate its largest member, with any alignment needed by the platform. That saves space when variants are exclusive, but C does not retain a built-in “active member” label for you. Code that writes decimal_number must not later pretend the same bytes are a valid label.
Select exactly one union member
This runner validates the selected Metric variant. It will report the active member and reject a starter that does not establish one unambiguous interpretation.
Edit the model, predict the result, then run it.
Pair a union with an enum tag
A tagged union makes the active interpretation part of your program's data contract. Store a tag and union together in a structure; set both in the same construction or update function; switch on the tag before reading the matching union field.
enum SettingKind {
SETTING_WHOLE,
SETTING_DECIMAL,
SETTING_LABEL,
};
struct Setting {
enum SettingKind kind;
union SettingValue value;
};
void print_setting(const struct Setting *setting) {
if (setting == NULL) return;
switch (setting->kind) {
case SETTING_WHOLE:
printf("%d\n", setting->value.whole_number);
break;
case SETTING_DECIMAL:
printf("%.2f\n", setting->value.decimal_number);
break;
case SETTING_LABEL:
printf("%s\n", setting->value.label);
break;
default:
printf("invalid setting\n");
break;
}
}- 01Choose the variant
Set
kindfirst or in the same designated initializer as the value. - 02Write the matching member
For
SETTING_DECIMAL, assign onlyvalue.decimal_number. - 03Read through the tag
Every consumer switches on
kindbefore selecting a union field. - 04Handle impossible data
Use
defaultto reject or report a corrupt/unsupported tag rather than guessing.
Independent lab: build a typed configuration record
Use a local C compiler for this lesson's data-model exercises. Start with the completed type below, then add a constructor function for each variant so callers cannot accidentally set a tag that disagrees with the union member.
struct Setting make_whole_setting(int number) {
return (struct Setting) {
.kind = SETTING_WHOLE,
.value.whole_number = number,
};
}
struct Setting make_label_setting(const char label[]) {
struct Setting setting = { .kind = SETTING_LABEL };
/* Copy only after checking label fits setting.value.label. */
return setting;
}struct Book with title, page count, and price. Initialize it with designators.int restock(struct Product *product, int amount) that rejects null pointers and negative amounts.Setting, then update its enum, union, constructor, and printer together.Lesson review
- A
structgroups related fields that coexist; select fields with.from an object and->from a pointer. - Use a pointer parameter to mutate a caller-owned structure and
const struct Type *for large read-only records. - An array of structures preserves each record's related fields; pass the valid count separately.
- An
enumnames a limited set of meaningful states. Switch on the names, not unexplained integer values. - A
unionoverlays members in shared storage and does not automatically track the valid interpretation. - A tagged union pairs an enum discriminator with the union and reads a member only after checking the matching tag.