Chapter 12 of 24

12. Structures, enumerations, and unions

Represent related information clearly

As a program grows, one fact may have several related pieces of information. A structure groups those pieces into one value. For example, a point can group an x coordinate and a y coordinate so they travel together.

  1. Define a struct with a member for each piece of information.
  2. Create a structure value and read or update members with the dot operator, such as point.x.
  3. Use an enum to give readable names to a set of choices; use a union only when different values intentionally share storage.

Choose the representation that makes the program's meaning obvious. The later sections explain initialization, copying, ownership, and layout details.

Structures

A struct groups related values:

#include <stdio.h>

struct Point {
    double x;
    double y;
};

int main(void)
{
    struct Point origin = {0.0, 0.0};
    printf("%.1f\n", origin.x);
    return 0;
}

Use . to access a member through a structure object and -> through a
pointer to a structure. Structures can be passed to and returned from
functions by value; large structures are often passed by pointer.

Enumerations

An enum gives names to a set of integer-like values:

enum Status {
    STATUS_OK,
    STATUS_NOT_FOUND,
    STATUS_ERROR
};

Named constants make states clearer than unexplained numeric values.

Unions

A union lets multiple members share the same storage. Only the member that
was most recently stored should generally be read. Use a tagged union when
different interpretations are possible, storing an enum alongside it to
record which member is active.

enum ValueKind {
    VALUE_INTEGER,
    VALUE_REAL
};

struct Value {
    enum ValueKind kind;
    union {
        int integer;
        double real;
    } data;
};

double value_as_double(const struct Value *value)
{
    if (value->kind == VALUE_INTEGER) {
        return (double)value->data.integer;
    }
    return value->data.real;
}

The tag says which union member is active. A complete API should validate
the pointer and tag before accessing members, especially when values come
from external input.

Structure padding and alignment are implementation-dependent. Do not assume a
structure's in-memory layout is a portable file or network format.

Designing records

Choose structure members that represent one concept. A struct Date, for
example, groups day, month, and year so a function can accept one meaningful
value instead of several unrelated integers. C does not automatically enforce
business rules such as a valid calendar date; provide a validation function
and define whether invalid states can be constructed.

Use an initializer to make the intended starting state visible:

struct Point {
    double x;
    double y;
};

int main(void)
{
    struct Point point = {.x = 2.0, .y = 3.0};
    return point.x == 2.0 && point.y == 3.0 ? 0 : 1;
}

Designated initializers are available in C99 and later. They name fields,
which helps avoid mistakes when a structure has several members. A pointer
to a structure uses ->, which is shorthand for dereferencing and selecting
a member: point_pointer->x means (*point_pointer).x.

An enum makes a set of alternatives readable, but input values from a file or
user are not automatically restricted to the enumerators. Validate external
values before treating them as a valid state. For a tagged union, update the
tag consistently with the active member and read only the member indicated
by the tag.

Checkpoint: Design a Book record with a title and page count. List the
validation rules you would apply before accepting a record from user input.

Copying structures and owning pointers

Assigning one structure to another copies each member's value. If a member is
a pointer, only the address is copied; the pointed-to allocation is not
duplicated. This is a shallow copy. Two structures may then refer to the
same resource, which raises questions about who may modify or free it.

For a structure that owns a string, define explicit operations such as
book_init, book_copy, and book_destroy, or use a fixed-size character
array when the maximum size is deliberately bounded. A deep-copy operation
allocates and copies the owned data, and must report allocation failure.
Never free a pointer just because it is a member: first establish whether the
structure owns that pointer or merely borrows it.

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

struct Label {
    char *text;  // This structure owns text. 
};

int copy_label(struct Label *destination, const struct Label *source)
{
    size_t length = strlen(source->text) + 1;
    char *copy = malloc(length);
    if (copy == NULL) {
        return 0;
    }

    memcpy(copy, source->text, length);
    destination->text = copy;
    return 1;
}

This is a deep copy of the string, but assumes valid non-null source and
destination pointers and an initialized destination that does not already
own another allocation. A production function must handle those conditions
and define whether it replaces or rejects existing destination content.

Passing a structure by value creates a copy. Passing a pointer avoids copying
the structure, but the function must observe its mutability and lifetime
contract. For read-only access, use const struct Point *point.

typedef, enums, and API clarity

typedef gives an existing type another name; it does not create a distinct
type with separate compatibility rules. It can make a public API easier to
read, but avoid hiding pointer-ness or important distinctions behind
unfamiliar aliases.

Enums are useful for states and return codes:

enum ParseResult {
    PARSE_OK,
    PARSE_EMPTY,
    PARSE_INVALID,
    PARSE_OUT_OF_RANGE
};

Use descriptive enumerator names to avoid collisions. Functions should return
one of the documented values, and callers should handle the cases they can
encounter, including an unexpected value if data came from outside the
program.

Layout, padding, and initialization

The compiler may insert padding between or after structure members to satisfy
alignment requirements. sizeof(struct Type) can therefore be larger than
the sum of member sizes. offsetof from <stddef.h> reports a member offset
for the current implementation, not a cross-platform serialization format.

#include <stddef.h>
#include <stdio.h>

struct Example {
    char tag;
    double measurement;
    int count;
};

int main(void)
{
    printf("size=%zu, measurement offset=%zu\n",
           sizeof(struct Example),
           offsetof(struct Example, measurement));
    return 0;
}

The printed values depend on the implementation. Use them to inspect a local
ABI, not to define a portable file layout.

Initialize structures with a full initializer or {0} when a zero initial
state is appropriate. Aggregate initialization initializes members
according to their types; it is different from assuming that an arbitrary
byte pattern represents every type's semantic zero.

Exercises: define a Book structure; represent a result with an enum and
message; create a tagged union for integer or floating-point values.


Suggested topics

0% read