C / STRUCTS, UNIONS AND ENUMS
Nested structs and modelling composed data
Model composed data by nesting structs by value: chain member access, copy whole sub-structs in one assignment, and know when to store a pointer instead.
What you will learn
- Chain one dot per nesting level to reach a leaf: w.frame.origin.x
- Assign a whole nested member in one statement to overwrite all of its fields
- Initialise deep types with matching braces or designators like .box.size.x = 32
- Embed by value for owned parts; use a pointer for shared, optional or recursive data
Understanding Nested structs and modelling composed data
When you name one struct type as a member of another, the inner object is stored inline: the outer object's bytes literally contain the inner object's bytes at a fixed offset the compiler knows. So w.frame.origin.x is not a chain of run-time lookups; it is one address formed by adding three constant offsets while compiling, exactly as cheap as a flat member. The mental model is a tree of storage with a single root object, not a graph of separate objects wired together.
Because the inner object is part of the outer one, anything that copies a struct copies its whole subtree, and every sub-struct along the way is itself a first-class value you can assign, pass or return. w.frame.size = other.size writes both leaves in one statement, while struct Point p = w.frame.origin hands you an independent copy whose edits can never reach w. This is also why an initialiser's brace structure mirrors the type's shape, and why a nested designator such as .box.size.x = 32 sets one leaf and leaves every other field, at every level, zeroed.
Nesting by value is the right default when the inner data has no life of its own: a rectangle's corner, an employee's hire date, a meeting's start time. Storage, lifetime and copying then come for free in one allocation. Store a pointer instead when several outer objects must observe the same inner object, when the member is genuinely optional, or when a type refers to itself, since a struct can never contain itself by value without requiring infinite size. The price of the pointer is a second lifetime you must manage: the inner object has to outlive every struct that points at it.
<stdio.h>
struct Point {
int x;
int y;
};
struct Rect {
struct Point origin; /* stored inline, not a pointer */
struct Point size;
};
struct Window {
struct Rect frame;
int id;
};
int main(void)
{
struct Window w = { .frame = { .origin = { 10, 20 },
.size = { 300, 200 } },
.id = 7 };
/* One dot per level of nesting. */
printf("origin: (%d, %d)\n", w.frame.origin.x, w.frame.origin.y);
/* A sub-struct is a value: this copies both of its fields. */
struct Point p = w.frame.origin;
p.x = 999;
printf("copy edited, w.frame.origin.x is still %d\n", w.frame.origin.x);
/* Assigning a sub-struct overwrites all of its fields at once. */
w.frame.size = (struct Point){ 640, 480 };
printf("size: %d x %d\n", w.frame.size.x, w.frame.size.y);
/* The inner objects live inside the outer one. */
printf("sizeof(struct Point) = %zu\n", sizeof(struct Point));
printf("sizeof(struct Rect) = %zu\n", sizeof(struct Rect));
printf("sizeof(struct Window) = %zu\n", sizeof(struct Window));
return 0;
}
A nested struct member is stored inline inside its parent, so the parent owns those bytes and every initialisation, copy or assignment acts on the whole subtree at once.
Worked examples
Owning a part versus pointing at one
Shows that an embedded struct member is a snapshot taken at initialisation, while a struct pointer member keeps seeing the original object.
<stdio.h>
struct Engine {
int horsepower;
};
struct CarOwns {
struct Engine engine; /* the engine lives inside the car */
};
struct CarRefers {
struct Engine *engine; /* the engine lives somewhere else */
};
int main(void)
{
struct Engine shared = { 120 };
struct CarOwns a = { shared }; /* copies 120 into a */
struct CarRefers b = { &shared }; /* stores an address */
shared.horsepower = 300;
printf("a.engine.horsepower = %d\n", a.engine.horsepower);
printf("b.engine->horsepower = %d\n", b.engine->horsepower);
return 0;
}
Example explained
Line 1struct CarOwns a = { shared }; copies the engine's bytes into a, so a is finished being built and no longer connected to shared.
Line 2struct CarRefers b = { &shared }; stores only an address, so reading b.engine->horsepower re-reads shared and sees 300.
Line 3The update shared.horsepower = 300 is visible through exactly one of the two members, which is the whole difference between composition by value and by pointer.
Line 4sizeof(struct CarOwns) is the size of an Engine, while sizeof(struct CarRefers) is the size of a pointer no matter how large Engine grows.
Nested braces in an array of composed records
Builds an array whose elements each contain a nested struct, then replaces one whole nested member with a single assignment.
<stdio.h>
struct Date {
int year;
int month;
int day;
};
struct Employee {
char name[8];
struct Date hired;
};
int main(void)
{
struct Employee team[3] = {
{ "Ada", { 2019, 3, 14 } },
{ "Grace", { 2021, 11, 2 } },
{ "Linus", { 2020, 7, 30 } }
};
for (int i = 0; i < 3; i++) {
printf("%-6s %04d-%02d-%02d\n",
team[i].name,
team[i].hired.year, team[i].hired.month, team[i].hired.day);
}
team[0].hired = team[2].hired; /* three fields, one statement */
printf("%s now shows %04d-%02d-%02d\n",
team[0].name, team[0].hired.year,
team[0].hired.month, team[0].hired.day);
return 0;
}
Example explained
Line 1Each element's initialiser has two parts, a string for name and an inner braced list for hired, matching the two levels of the type.
Line 2team[i].hired.year needs three steps: index the array, select the nested member, then select its leaf.
Line 3team[0].hired = team[2].hired; copies year, month and day together because hired is a value stored inside the element.
Line 4Only hired changes; team[0].name still prints Ada, since the assignment touched one subtree and nothing around it.
Naming one deep leaf and zeroing the rest
Uses a nested designated initialiser to set a single field three levels down while every other field in the object becomes zero.
<stdio.h>
struct Point { int x, y; };
struct Rect { struct Point origin, size; };
struct Sprite {
struct Rect box;
int layer;
};
int main(void)
{
struct Sprite s = { .box.size.x = 32 };
printf("origin=(%d,%d) size=(%d,%d) layer=%d\n",
s.box.origin.x, s.box.origin.y,
s.box.size.x, s.box.size.y, s.layer);
return 0;
}
Example explained
Line 1.box.size.x = 32 is a single designator that walks two struct levels before naming the leaf, so no intermediate braces are needed.
Line 2Because the object has an initialiser, every field not mentioned is set to zero, including the ones inside nested members.
Line 3s.box.size.y is 0 even though its sibling x was named: partial initialisation applies leaf by leaf, not member by member.
Line 4Writing struct Sprite s; instead would leave all of these fields indeterminate, so the zeroing comes from the initialiser, not from nesting.
Important notes
The member name and the type tag are independent: frame is the member, struct Rect is the type, and one parent can hold several members of the same nested type, such as origin and size.
The printed sizes assume a 4-byte int; the point being shown is that the parent's size includes its nested members, since they are stored in it rather than referenced from it.
Common mistakes
Writing struct Node { int value; struct Node next; }; — inside its own definition the type is still incomplete and would need infinite size, so the compiler rejects the member; a recursive link must be struct Node *next.
Doing struct Point p = w.frame.origin; p.x = 5; and expecting w to change — the nested member is a value, so you edited a copy and the update is silently thrown away.
Flattening the initialiser across levels, as in struct Window w = { 10, 20, 300, 200, 7 }; — it compiles, but insert or reorder one field inside struct Point and every later value lands in the wrong member with no error, only a -Wmissing-braces warning at best.
Try it yourself
Change, predict, then run
Define struct Time { int h, m; }; and struct Meeting { struct Time start, end; char title[16]; };, build two meetings with nested braces, and print each as "standup 09:00-09:15". Then make the second meeting begin exactly when the first ends using one assignment of a whole nested member, and print it again.
Open the C workspaceCheck your understanding
struct Window has the member struct Rect frame;. You run struct Rect r = w.frame; r.origin.x = 99;. What is w.frame.origin.x afterwards, and why?
- Unchanged, because frame is stored inline in w and the initialisation copied its entire subtree into r
- 99, because members that are structs are reached by reference rather than copied
- Unchanged, but only because struct Rect is small enough to be held in registers
- It does not compile, because whole nested structs cannot be copied, only their leaf fields
Show answer
w.frame is an object living inside w, so initialising r from it duplicates all four ints; writing through r cannot reach w. Option 1 is the tempting one because it describes what would happen if the member were struct Rect *frame, where you would be storing an address and sharing one object; with a value member there is no sharing. Size is irrelevant, and struct assignment copies whole objects, nested members included, so options 2 and 3 fail too.