Skip to main content
@shmVirus

Composite Types

Structures, nested records, structure arrays and pointers, unions, enumerations, type aliases, memory alignment, and padding.

Arrays group values of one element type. Composite types let a program describe values with several named parts or several possible interpretations. A student record, coordinate, date, or message kind becomes one typed value instead of a loose collection of variables whose relationship exists only in the programmer’s memory.

Structures

A structure definition introduces a record layout:

struct Point {
    double x;
    double y;
};

struct Point is a type. x and y are members. The definition does not itself create a Point object.

struct Point origin;
struct Point cursor;

Each object contains its own x and y subobjects. Structure members may have different types:

enum { NAME_CAPACITY = 64 };

struct Student {
    unsigned long id;
    char name[NAME_CAPACITY];
    double average;
    bool is_active;
};

A structure cannot contain an instance of itself directly because that would require infinite size. It may contain a pointer to its own type, whose size is known:

struct Node {
    int value;
    struct Node *next;
};

This chapter uses the declaration only to explain C’s type rule. Building linked structures belongs to the Data Structures course.

Structure Initialization

Initialize members in declaration order:

struct Point location = {3.5, -2.0};

Designated initializers attach values to names and remain clear when the member order changes:

struct Student student = {
    .id = 20260017UL,
    .name = "Rina Ahmed",
    .average = 86.25,
    .is_active = true
};

Unmentioned members receive zero initialization:

struct Point origin = {0};

For nested aggregates, designators can reach nested members:

struct Rectangle {
    struct Point top_left;
    struct Point bottom_right;
};

struct Rectangle box = {
    .top_left = {.x = 1.0, .y = 5.0},
    .bottom_right = {.x = 4.0, .y = 2.0}
};

Initialization occurs when the object is created. Later replacement uses assignment.

Member Access

The dot operator selects a member from a structure object:

student.average = 88.0;
printf("%s: %.2f\n", student.name, student.average);

A member is an ordinary subobject with its declared type. Array-member rules still apply: student.name usually converts to a pointer in expressions, but the array itself cannot be assigned.

/* student.name = "New Name"; */ /* invalid array assignment */

Use a bounded string operation or initialize a new structure.

Functions can accept or return structures by value:

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

The parameter is a copy, so the caller’s original point remains unchanged. Returning a modest structure by value is clear and lets the implementation choose an efficient calling convention.

Structure Assignment

Structures of compatible type are assignable:

struct Point a = {.x = 1.0, .y = 2.0};
struct Point b;

b = a;

Every member value is copied, including array members. This differs from direct array assignment, which is invalid.

Assignment is shallow for pointer members:

struct View {
    const char *text;
    size_t length;
};

struct View first = {"hello", 5};
struct View second = first;

Both pointers designate the same characters. Structure assignment does not duplicate dynamically allocated targets or transfer ownership automatically. Copy semantics must be part of the type’s documented contract.

Comparing two structures with == is not allowed. Compare relevant members:

bool point_equal(struct Point left, struct Point right) {
    return left.x == right.x && left.y == right.y;
}

Whether exact floating equality is appropriate depends on how the coordinates were produced.

Nested Structures

Structures can model containment:

struct Date {
    int year;
    int month;
    int day;
};

struct Event {
    char title[80];
    struct Date date;
};

Access follows the nesting:

event.date.year = 2026;
printf("%s occurs in %d\n", event.title, event.date.year);

Nested types help enforce vocabulary. Three independent integers named year, month, and day can be passed in the wrong order; a struct Date preserves their relationship. The structure alone does not enforce that month is 1–12 or that the date exists. Constructor-like functions and validation establish semantic invariants:

bool date_create(int year, int month, int day, struct Date *result);

Structure Arrays

An array can contain structure elements:

struct Point path[] = {
    {.x = 0.0, .y = 0.0},
    {.x = 1.5, .y = 2.0},
    {.x = 3.0, .y = 1.0}
};

Traversal combines array indexing with member access:

size_t count = sizeof path / sizeof path[0];

for (size_t i = 0; i < count; i++) {
    printf("(%g, %g)\n", path[i].x, path[i].y);
}

Each element is a complete struct Point. path[i].x groups as (path[i]).x because indexing and member selection have high postfix precedence.

Passing an array of structures follows ordinary array-parameter rules:

void print_students(const struct Student students[], size_t count);

The const view prevents member modification through the parameter.

Structure Pointers

Given a pointer to a structure, -> selects a member through the pointer:

void deactivate(struct Student *student) {
    if (student != NULL) {
        student->is_active = false;
    }
}

student->is_active is shorthand for (*student).is_active. Parentheses are necessary in the expanded form because . binds more tightly than unary *.

Use a pointer when a function must modify the caller’s structure, when copying is undesirable, or when null is a meaningful optional state. Use const struct Type * for a read-only borrowed view:

void print_student(const struct Student *student) {
    if (student == NULL) {
        return;
    }

    printf("%lu  %-20s  %6.2f  %s\n",
           student->id,
           student->name,
           student->average,
           student->is_active ? "active" : "inactive");
}

The pointer must designate a live, correctly aligned struct Student object throughout the call.

Unions

A union’s members share overlapping storage:

union Measurement {
    long count;
    double temperature;
};

Its size is sufficient for its largest member plus any alignment requirements. Storing one member generally makes that member the current meaningful interpretation:

union Measurement value;
value.temperature = 21.75;
printf("%.2f\n", value.temperature);

Reading a different member is not a portable general-purpose type-conversion technique. The result can be implementation-defined, unspecified, or constrained by special rules; use memcpy for controlled examination of object representations.

Tagged Unions

A separate tag records which union member is active:

enum ValueKind {
    VALUE_INTEGER,
    VALUE_DECIMAL
};

struct Value {
    enum ValueKind kind;
    union {
        long integer;
        double decimal;
    } data;
};

Safe access checks the tag:

void print_value(const struct Value *value) {
    switch (value->kind) {
    case VALUE_INTEGER:
        printf("%ld\n", value->data.integer);
        break;
    case VALUE_DECIMAL:
        printf("%g\n", value->data.decimal);
        break;
    }
}

The invariant is: kind agrees with the last stored member of data. The language does not enforce this relationship; constructors and update functions must preserve it.

Enumerations

An enumeration introduces named integer constants and an enumeration type:

enum TrafficLight {
    LIGHT_RED,
    LIGHT_AMBER,
    LIGHT_GREEN
};

By default, values begin at zero and increase by one. Values can be explicit:

enum Permission {
    PERMISSION_READ = 1U << 0,
    PERMISSION_WRITE = 1U << 1,
    PERMISSION_SHARE = 1U << 2
};

An enum is excellent for a closed vocabulary of states, modes, or outcomes:

enum ParseStatus {
    PARSE_OK,
    PARSE_EMPTY,
    PARSE_INVALID,
    PARSE_RANGE
};

Enumerations in C are not strongly type-safe algebraic variants. An object of enumeration type can still receive integer values through conversions, including values that do not match a named enumerator. Validate values arriving from files, networks, or casts.

A switch over an enum makes states explicit. Omitting default can allow some compilers to warn about missing named cases; adding default can handle corrupted or future values. Choose according to whether exhaustive diagnostics or defensive runtime handling is more important, and document the policy.

Type Aliases

typedef creates an alias for an existing type:

typedef unsigned long StudentId;
typedef struct Point Point;

Now declarations can use:

Point location = {.x = 1.0, .y = 2.0};
StudentId id = 20260017UL;

The alias does not create a distinct type with additional safety. StudentId remains compatible with unsigned long. Use aliases to express domain vocabulary or simplify complex declarators, not to hide whether a value is a pointer.

A combined structure definition and alias is common:

typedef struct {
    double x;
    double y;
} Vector;

The anonymous structure can only be named through Vector. A tagged form is preferable when the type must refer to itself or when forward declarations are useful:

typedef struct Employee Employee;

struct Employee {
    unsigned long id;
    const Employee *manager;
};

Memory Alignment

Types can require addresses divisible by particular powers or implementation-specific units. _Alignof(type) reports a type’s alignment requirement in C17:

printf("int: size=%zu alignment=%zu\n",
       sizeof(int), _Alignof(int));

malloc returns storage suitably aligned for any object type with a fundamental alignment requirement. Converting an arbitrary character-buffer offset to a structure pointer may violate alignment even when enough bytes remain.

Members appear in declaration order at nondecreasing addresses, but padding may exist between them and after the final member.

Padding

Consider:

struct Example {
    char tag;
    int value;
    char flag;
};

A common layout is:

tag | padding | value | flag | trailing padding

but exact sizes and offsets are implementation-defined. <stddef.h> supplies offsetof:

printf("value offset: %zu\n", offsetof(struct Example, value));

Padding has important consequences:

  • sizeof(struct Example) may exceed the sum of member sizes;
  • structure arrays include trailing padding so each next element is aligned;
  • padding bytes may hold indeterminate values;
  • memcmp is not a valid general structure-value comparison;
  • writing raw structure bytes is not a portable file or network format.

Reordering members can reduce padding on a particular ABI, but layout clarity and external compatibility may matter more. Measure under the target contract instead of guessing.

Portable Serialization

Do not serialize a structure with:

fwrite(&record, sizeof record, 1, file);

when the file must be portable. Padding, endianness, integer widths, floating representation, and version changes all affect the bytes. Define a format and encode each field deliberately; Chapter 13 develops file I/O.

Record State Trace

struct Value value = {
    .kind = VALUE_INTEGER,
    .data.integer = 12
};

value.kind = VALUE_DECIMAL;
value.data.decimal = 2.5;
Momenttagactive interpretationsafe read
after initializationVALUE_INTEGERdata.integer12
after tag update onlyVALUE_DECIMALstill last stored as integerinvariant temporarily broken
after decimal storeVALUE_DECIMALdata.decimal2.5

A constructor or setter should perform both updates so callers cannot observe the broken intermediate state:

void value_set_decimal(struct Value *value, double decimal) {
    value->data.decimal = decimal;
    value->kind = VALUE_DECIMAL;
}

Writing the data before publishing the tag is also a useful state-transition discipline.

Measurement Variant

A tagged union is useful when every record holds exactly one of several alternatives. This complete program models an integer count, a temperature, or an explicit missing value. Constructors publish only valid tag-and-payload combinations.

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

typedef enum {
    MEASUREMENT_MISSING,
    MEASUREMENT_COUNT,
    MEASUREMENT_CELSIUS
} MeasurementKind;

typedef struct {
    MeasurementKind kind;
    union {
        long count;
        double celsius;
    } data;
} Measurement;

static Measurement measurement_missing(void) {
    Measurement value = {.kind = MEASUREMENT_MISSING, .data.count = 0};
    return value;
}

static Measurement measurement_count(long count) {
    Measurement value = {.kind = MEASUREMENT_COUNT, .data.count = count};
    return value;
}

static Measurement measurement_celsius(double celsius) {
    Measurement value = {
        .kind = MEASUREMENT_CELSIUS,
        .data.celsius = celsius
    };
    return value;
}

static bool measurement_print(const Measurement *value) {
    if (value == NULL) {
        return false;
    }

    switch (value->kind) {
        case MEASUREMENT_MISSING:
            puts("missing");
            return true;
        case MEASUREMENT_COUNT:
            printf("count: %ld\n", value->data.count);
            return true;
        case MEASUREMENT_CELSIUS:
            printf("temperature: %.2f C\n", value->data.celsius);
            return true;
        default:
            return false;
    }
}

int main(void) {
    Measurement readings[] = {
        measurement_count(17),
        measurement_celsius(23.5),
        measurement_missing()
    };
    size_t count = sizeof readings / sizeof readings[0];

    for (size_t i = 0; i < count; i++) {
        if (!measurement_print(&readings[i])) {
            fputs("invalid measurement tag\n", stderr);
            return EXIT_FAILURE;
        }
    }
    return EXIT_SUCCESS;
}

The invariant is relational:

kind == MEASUREMENT_COUNT   => data.count is the meaningful member
kind == MEASUREMENT_CELSIUS => data.celsius is the meaningful member
kind == MEASUREMENT_MISSING => no payload has domain meaning

The constructors return complete structure values, so client code never observes “new tag, old payload.” The missing constructor still initializes union storage. Although the missing variant does not semantically use a payload, initializing the whole returned value makes tools and future byte-level handling less surprising.

measurement_print includes default because a Measurement may arrive from unchecked input, an unchecked conversion, or future code that violates the constructor discipline. Returning false keeps an invalid tag distinct from a valid missing measurement. In a closed internal switch where compiler exhaustiveness warnings are more valuable, omitting default may be a deliberate alternative.

Copy Semantics

Structure assignment copies every member value. For the Measurement type, that creates an independent complete value because it contains no pointers:

Measurement first = measurement_count(9);
Measurement second = first;
second.data.count = 12;

first remains count 9 and second becomes count 12. Their bytes occupy different structure objects.

Now consider:

typedef struct {
    char *name;
    size_t length;
} Label;

Assigning one Label to another copies the pointer and length, not the characters. Both records then designate the same external array. Questions immediately arise: who owns that array, may either record modify it, and which record frees it? The language calls the assignment valid, but the type’s design may call the resulting shared ownership invalid.

Composite types therefore need a copy policy:

  • value-only record: ordinary assignment creates independent values;
  • borrowed-pointer record: assignment copies views whose target lifetime is managed elsewhere;
  • shared-owner record: assignment requires an explicit reference-count operation;
  • unique-owner record: ordinary copying should be avoided in favor of clone or transfer functions;
  • fixed-array record: structure assignment copies the array members as part of the whole structure.

Document that policy next to constructors and cleanup functions. A pointer member is a representation fact; ownership is a semantic rule built around it.

Layout Inspection

The portable way to inspect an implementation’s layout is to ask it:

#include <stddef.h>

printf("kind offset: %zu\n", offsetof(Measurement, kind));
printf("data offset: %zu\n", offsetof(Measurement, data));
printf("size: %zu align: %zu\n",
       sizeof(Measurement), _Alignof(Measurement));

Suppose an implementation uses four bytes for the enumeration, eight-byte alignment for double, and a union size of eight. One plausible layout is:

offset 0..3   kind
offset 4..7   padding
offset 8..15  union storage
total         16 bytes

That is an observation about one ABI, not a C guarantee. Reordering can change padding, and compiler options can change ABI rules. Arrays of Measurement include any trailing padding in each element’s stride so every next union remains aligned.

Padding bytes are not semantic members. Two values with equal tags and payloads can have different padding contents, so memcmp may report inequality. Conversely, byte equality would not necessarily define domain equality for floating not-a-number values or records whose pointers designate equal text in different allocations.

Wire Format Case

Imagine a portable textual representation:

MISSING
COUNT 17
CELSIUS 23.5

The grammar, not memory layout, defines the file. A reader must validate the tag word, exact field count, numeric syntax, numeric range, trailing text, and any domain rule. It should parse into a temporary Measurement, then assign to the caller’s object only after the full line succeeds.

A binary format needs equally explicit decisions: tag width and values, integer width and byte order, floating representation or decimal encoding, versioning, and treatment of unknown future tags. Writing sizeof(Measurement) raw bytes silently delegates all those decisions to one compiler and machine, including padding that was never part of the data model.

Constructor Boundary

C does not enforce constructors, but a module can require callers to obtain values through functions that establish invariants. For a rectangle represented by two corners:

typedef struct {
    double x;
    double y;
} Point;

typedef struct {
    Point minimum;
    Point maximum;
} Rectangle;

the invariant might be:

minimum.x <= maximum.x
minimum.y <= maximum.y
all coordinates are finite

Public setters should preserve the relationship. Setting only minimum.x can invalidate an existing maximum, so an operation may accept a complete candidate point, validate against the other corner, then assign. Exposing writable members makes that discipline conventional rather than enforced; an opaque module can enforce it structurally.

Designated initializers improve review because names remain attached to values:

Point origin = {.x = 0.0, .y = 0.0};

Positional initialization is concise but fragile when several same-typed members change order. Missing designated members are initialized as if from zero, which is useful only when zero is a valid default under the invariant.

Structure Parameters

Structures can be passed and returned by value:

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

The local point is a copy; callers retain their original. This value style is clear for small records. Large structures may be passed through const pointers to avoid copying, but performance depends on the ABI and optimizer. Choose semantics first, then measure.

Returning a structure is not inherently unsafe. The implementation arranges value transfer; it is different from returning a pointer to a local object. Pointer members inside the returned structure still refer to their original targets, so lifetime and ownership remain part of the copied value’s contract.

Array of Records

An array of structures keeps each record’s members together:

students[0]: id, mark, status
students[1]: id, mark, status
students[2]: id, mark, status

This layout is convenient when processing complete records. Separate arrays—one for IDs, one for marks, one for statuses—can be better for operations that scan only one field at very large scale. Both designs represent the same logical table with different locality and update invariants.

In the separate-array form, every array must have the same logical count and matching index identity. In the array-of-structures form, that cross-array invariant disappears, but padding repeats in every element. Representation follows dominant access patterns, not a universal “structures are better” rule.

Enum Evolution

Enumeration constants give states names, but external storage should not assume their numeric representation or size unless a format explicitly encodes chosen numbers. Adding a new enumerator can expose switches that assumed the old closed set.

For internal exhaustive handling, compile with warnings that report omitted named cases and consider no default, allowing a new enumerator to trigger review. For untrusted input, validate the integer before conversion or handle a defensive default. These are complementary boundaries: compile-time evolution and runtime corruption.

Never use an enum object as an unchecked array index. Even if every named enumerator is contiguous today, a cast or external value can lie outside the table. Validate or map with a switch.

Semantic Equality

Define equality member by member according to meaning:

Point equality            -> coordinate policy, perhaps exact stored values
student equality          -> stable ID, not necessarily every mutable field
owned string record       -> character contents, not pointer addresses
tagged value equality     -> equal tags and equal active payloads
floating measurement      -> domain tolerance may or may not be appropriate

memcmp sees padding and representations. Pointer comparison sees identity, not pointed-to content. A named equality function records domain policy and can evolve with the type.

Boundary Validation

A structure loaded from bytes is not valid merely because every field fits its C type. Validate cross-member relationships:

start <= end
count <= capacity
tag matches active union member
length fits within the terminated character array
status permits the accompanying payload

Parse into a temporary record, validate the complete invariant, then assign it to live state. Field-by-field assignment to the destination can expose combinations that no operation is supposed to observe.

Validation functions should not dereference pointer members until their presence and ownership state make that safe. For cyclic or shared records, validation may need a visited-set design from the data-structures course; this chapter’s simple records should keep ownership acyclic and explicit.

Representation Review

Before finalizing a composite type, ask:

  1. Which member combinations are valid?
  2. What is the canonical empty/default state?
  3. Does ordinary assignment provide correct copy semantics?
  4. Which members own, borrow, or merely identify external data?
  5. Can construction fail, and what remains on failure?
  6. Is equality identity-based or value-based?
  7. Does an external format depend accidentally on padding or enum layout?
  8. Which operations need complete definition visibility?

These questions turn a collection of members into a data model. The strongest representation is not the one with the fewest bytes; it is the one whose valid states and transitions are easiest to preserve under the actual workload.

Representation Hazards

  • Trying to assign an array member: copy into it or assign an entire compatible structure.
  • Assuming structure assignment performs a deep copy: pointer targets remain shared.
  • Comparing structures with == or memcmp: compare semantic members.
  • Reading a union member inconsistent with its tag: preserve a tagged-union invariant.
  • Assuming every enum object contains a named value: validate external values.
  • Treating typedef as a new incompatible type: it is an alias.
  • Assuming no padding: query sizeof, _Alignof, and offsetof under the implementation.
  • Writing raw structures as portable data: define a representation independent of memory layout.

Type Selection

  • Structures combine named members into one value; unions overlay alternative members.
  • Designated initializers connect initialization to member names.
  • Compatible structures can be assigned and returned by value, but pointer members copy shallowly.
  • . selects from an object and -> selects through a structure pointer.
  • A tagged union pairs a discriminator with the currently meaningful member.
  • Enumerations name integral states but do not enforce that every stored value is named.
  • typedef supplies an alias, not a distinct semantic type.
  • Alignment can introduce padding; raw memory layout is not a portable serialization format.

Record Problems

Read the Layout

  1. Distinguish structure definition, object declaration, initialization, and assignment.
  2. Why does structure assignment not necessarily create an independent copy?
  3. State the invariant of a tagged union.
  4. Why is memcmp unsuitable for general structure equality?

Follow the Members

  1. Draw a nested Event object and label the expressions used to access each Date member.
  2. Trace two structure values after assigning one structure containing a pointer member to the other, then modifying the pointed-to object.
  3. Print and explain the size, alignment, and member offsets for three differently ordered versions of struct Example on your implementation.

Repair the Variant

  1. Correct code that compares strcmp-compatible character-array members using ==.
  2. Repair a union printer that always reads data.decimal regardless of its tag.
  3. Identify the lifetime risk in a structure whose pointer member is initialized to a local array that soon leaves scope.

Model a Record

  1. Define and validate a Date structure. Include leap-year handling and a function that formats a valid date.
  2. Define a Rectangle from two Point members and implement width, height, and area functions with a stated coordinate convention.
  3. Implement a tagged value that can contain an integer, decimal, or fixed-capacity string. Provide setters and a printer that preserve and check the tag invariant.

Choose a Representation

  1. Design copy semantics for a record with an owned dynamically allocated name. Specify what assignment, duplication, and cleanup must mean before implementing it in Chapter 11.
  2. Propose a portable textual representation for Student. Explain delimiters, escaping, numeric formats, versioning, and validation.