C / Intermediate: Memory and Reliable Components / Lesson 9
Structures and Data Models
Group related values without confusing an in-memory representation with ownership, equality, or an external file format.
On this page
Lesson goals
- Maintain invariants across structure fields.
- Explain structure assignment and shallow pointer copies.
- Use enums and tagged unions to represent alternatives.
Fields form one logical record
A struct groups named members, each with its own type. The compiler may insert padding between members and at the end to meet alignment requirements. The logical record consists of member values, not a promise that the bytes have a compact or universal layout. Use member names to describe the domain rather than relying on positional assumptions.
A useful model states relationships between fields. For an interval, lower must not exceed upper. For a buffer, length must not exceed capacity, and a nonzero capacity requires valid storage. The constructor and every mutating operation must preserve those relationships; exposing fields publicly makes all callers responsible for doing so.
Work with a value record
The first example uses a structure containing only values. Assignment copies those member values into a second record; changing the copy’s x member does not change the original. Designated initializers tie values to member names, making intent clearer when the declaration order changes. Members omitted by an initializer are initialized according to the usual aggregate initialization rules.
For a record with embedded fixed-size arrays, structure assignment copies their stored elements as part of the value. This differs from copying a structure that merely contains pointers to external allocations. The latter copies pointer values and can create shared aliases, so its ownership contract requires separate thought.
#include <stdio.h>
struct point { int x; int y; };
int main(void)
{
struct point first = {.x = 2, .y = 5};
struct point copy = first;
copy.x = 9;
printf("%d,%d %d,%d\n", first.x, first.y, copy.x, copy.y);
return 0;
}
2,5 9,5
The two structure objects have independent int members.
Pointer members require a copy policy
If a structure owns char *name, ordinary assignment duplicates the address, not the allocated characters. Treating both copies as exclusive owners would lead to double release. Choose among a noncopyable ownership convention, an explicit deep-copy operation, shared ownership with a correct reference protocol, or a borrowed-view type with no release responsibility.
C does not generate constructors or destructors. Define initialization and destruction functions, document whether destruction accepts an empty object, and reset fields to the neutral invariant when useful. A typedef can shorten a type name but does not add ownership behavior or make accidental assignment safe.
Represent one active alternative
A union shares storage among its members. A tagged union adds an enum saying which member the program currently treats as active. The tag and payload must be updated together under the type’s invariant. This makes a value that is either an integer or a real number explicit, instead of using a magic numeric sentinel.
The example writes the integer member and selects it only after checking the tag. For portable application logic, do not use an unrelated union member as a general conversion trick; representation reinterpretation has restrictions and can expose trap or unsuitable representations. Converting a value explicitly communicates a different and usually clearer operation.
#include <stdio.h>
enum kind { INTEGER_VALUE, REAL_VALUE };
struct number {
enum kind tag;
union { int integer; double real; } data;
};
int main(void)
{
struct number value = {.tag = INTEGER_VALUE, .data.integer = 7};
if (value.tag == INTEGER_VALUE) printf("integer=%d\n", value.data.integer);
else if (value.tag == REAL_VALUE) printf("real=%.1f\n", value.data.real);
return 0;
}
integer=7
The tag selects the member initialized with the integer seven.
Serialize meaning, not raw padding
Writing sizeof(struct record) bytes directly to a file couples the format to padding, byte order, integer representation, and compiler ABI choices. It may also expose uninitialized padding. Define external fields and their encodings explicitly, then encode and decode each field with range and length checks. A text format can be simpler when size and speed do not dominate.
Equality is similarly semantic. memcmp can disagree for logically equal structures because padding bytes need not match. Compare the meaningful members under the domain’s equality rule. For a floating field, that rule must also address approximation and special values rather than inheriting a bytewise comparison accidentally.
Common errors and fixes
Copying a record later causes a double free.
- Why it happens
- Pointer members were shallow-copied into two apparent owners.
- How to fix it
- Implement an explicit copy or transfer policy.
- Prevent it next time
- Separate owning records from borrowed views in interfaces.
A saved structure cannot be read on another target.
- Why it happens
- Raw layout was mistaken for a portable format.
- How to fix it
- Encode fields with a specified byte order and widths.
- Prevent it next time
- Document serialization independently from the in-memory type.
The tag says real but the code reads integer.
- Why it happens
- Tag and payload invariants were not enforced.
- How to fix it
- Switch on the tag and access only its documented alternative.
- Prevent it next time
- Centralize constructors and test each variant.
Check your understanding
1. A record owns a dynamically allocated label and a count. Design what a deep copy must do if allocating the new label fails.
Show worked solution
Keep the destination in an empty valid state. Determine and check the label size, allocate new storage, and copy the complete label only after allocation succeeds. Copy the count as a value. If allocation fails, return failure without transferring the source’s pointer or changing the source. The destination must not become a second owner of the original label.
2. Why can two equal point records fail a bytewise equality test on a hypothetical implementation? Provide the correct comparison.
Show worked solution
Padding bytes may differ even when x and y match. Compare first.x == second.x && first.y == second.y to express value equality. This uses only meaningful members and remains valid if the implementation changes padding. If the domain later treats coordinates approximately, update the semantic comparison deliberately rather than switching to raw byte equality.
Key takeaways
- A structure’s invariant connects its fields and its ownership rules.
- Assignment copies pointer values without duplicating their allocations.
- External formats and semantic equality should be defined independently of padding.