Skip to content

C Structs, Unions, Enumerations, and Bit Fields

C aggregate types let programs give structure to bytes without adding automatic encapsulation or lifetime behavior. Their layout is useful within one implementation and dangerous when mistaken for a universal wire format.

Structures group independently stored members

c
#include <stdio.h>

struct point {
    double x;
    double y;
};

static struct point translated(struct point point, double dx, double dy) {
    point.x += dx;
    point.y += dy;
    return point;
}

int main(void) {
    struct point p = {.x = 1.0, .y = 2.0};
    p = translated(p, 3.0, -1.0);
    printf("%.1f, %.1f\n", p.x, p.y);
    return 0;
}

Structures are values: assignment and parameter passing copy the members, including padding bytes as permitted by the implementation. A pointer uses -> to access members; an object uses ..

C99 designated initializers make field meaning visible and tolerate declaration reordering better than positional initialization. C23 adds related initialization improvements, but support should be checked before choosing a baseline.

Compound literals create unnamed objects with a type and a lifetime determined by their scope:

c
draw_line((struct point){.x = 1.0, .y = 2.0},
          (struct point){.x = 4.0, .y = 6.0});

At block scope, the compound-literal object has automatic storage duration associated with the enclosing block. A pointer to it must not escape that lifetime. At file scope it has static storage duration. C++ has superficially similar braced temporaries but does not adopt C compound-literal rules wholesale.

Padding is not payload

Members appear in declaration order, but the implementation may insert padding for alignment. sizeof(struct point) is therefore not simply the sum of member sizes in general. Padding can hold unspecified values, so bytewise equality with memcmp is not a reliable semantic equality operation for arbitrary structs.

Serialize fields deliberately. Define widths, byte order, permitted values, and encoding rather than writing the in-memory struct image.

Unions share storage

All union members overlap:

c
union payload {
    long integer;
    double decimal;
};

The union alone does not remember which member is active. A tagged union pairs it with an explicit discriminator:

c
enum value_kind { VALUE_INTEGER, VALUE_DECIMAL };

struct value {
    enum value_kind kind;
    union {
        long integer;
        double decimal;
    } data;
};

Every function handling struct value must keep kind and data consistent. This models a closed set of alternatives but does not receive the exhaustive checking of a Swift enum or C# discriminated-union library automatically.

Enumerations name integer constants

c
enum status {
    STATUS_OK,
    STATUS_INVALID,
    STATUS_IO_ERROR
};

Traditional C enumerators have integer type behavior and underlying representation rules that differ from strongly typed enums in C++ or Swift. C23 adds fixed underlying types and other refinements, subject to implementation support.

External input can contain values outside named enumerators. Validate integers received from files, networks, FFI, or casts before treating them as a meaningful status.

Bit fields are implementation-sensitive

Bit fields can compact flags inside a structure:

c
struct flags {
    unsigned ready : 1;
    unsigned mode : 3;
};

Allocation order, packing, alignment, and interactions across units are implementation-defined. They can be appropriate for implementation-controlled hardware or ABI layouts backed by documentation, but ordinary portable protocols should use masks over explicitly sized integers and explicit byte-order conversion.

Flexible array members support one-allocation records

C99 permits the final member of a structure to be an incomplete array type:

c
struct packet {
    size_t length;
    unsigned char data[];
};

sizeof(struct packet) excludes payload elements but can include trailing padding. Allocate enough space only after checking the addition:

c
#include <stdint.h>
#include <stdlib.h>

if (length > SIZE_MAX - sizeof(struct packet)) {
    return NULL;
}

struct packet *packet = malloc(sizeof *packet + length);
if (packet != NULL) {
    packet->length = length;
}

The flexible member must be last, the structure must contain another named member, and assignment of the structure copies only the fixed portion. The allocation remains one object owned through the structure pointer. Older “one-element” or zero-length-array techniques are not equivalent portable C.

Opaque structures protect invariants

Publicly exposing members lets every caller construct any representable combination. When invariants or binary stability matter, expose an incomplete type and functions. When a type is a plain data record shared by value, a public struct can be the clearest C design. Encapsulation is a tradeoff, not an automatic virtue.

Optional prompts

Explain: Why is memcmp(&left, &right, sizeof left) not general struct equality?

Answer: Padding bytes may contain unspecified values, and member representations can differ while values compare equal. Compare meaningful members according to the domain.

Explain: What supplies safety for a C tagged union?

Answer: Program logic maintains and checks the discriminator. The language overlays storage but does not automatically prevent reading a member inconsistent with the tag.

Explain: Why is a flexible array member different from unsigned char data[1]?

Answer: It is standardized incomplete-array syntax for the final member and contributes no element to sizeof by itself. The one-element workaround has a real element and different sizing and bounds semantics.

Further reference