Nested structs, typedef, and enums
Records, files and recursion12 min readLesson 1 of 6
1.1 A struct member can be a struct
Nested structs describe records with parts. A struct Date inside a struct Event keeps the three date fields together and lets the code treat event.when as one value.
The member is stored inside the outer struct, not behind a pointer. sizeof(struct Event) includes the whole struct Date, plus any padding the compiler adds for alignment.
Initialisation follows the nesting: braces inside braces, in member order. Named initialisers avoid counting positions and keep the code working after a member is reordered.
#include <stdio.h>
struct Date { int day; int month; int year; };
struct Event {
char title[24];
struct Date when;
};
int main(void)
{
struct Event launch = {"Release", {14, 3, 2027}};
printf("%s on %02d/%02d/%04d\n",
launch.title, launch.when.day, launch.when.month, launch.when.year);
return 0;
}Output
Release on 14/03/2027
1.2 typedef names the type once
typedef struct Point { int x; int y; } Point; creates the alias Point for struct Point. After that, declarations read as Point p; instead of struct Point p;.
The alias is not a new type. The compiler treats Point and struct Point as the same type, and a function declared with one matches a call using the other.
A common style omits the tag: typedef struct { int x; int y; } Point;. That works until the struct needs a pointer to its own type, so self-referential structs keep the tag.
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
static Point move(Point p, int dx, int dy)
{
Point result = {p.x + dx, p.y + dy};
return result;
}
int main(void)
{
Point start = {2, 3};
Point end = move(start, 4, -1);
printf("(%d,%d) -> (%d,%d)\n", start.x, start.y, end.x, end.y);
return 0;
}Output
(2,3) -> (6,2)
1.3 enum gives states readable names
enum declares a set of integer constants. enum Color { COLOR_RED, COLOR_GREEN, COLOR_BLUE }; makes COLOR_RED 0, COLOR_GREEN 1, and COLOR_BLUE 2 by default.
An enum variable is an integer type large enough to hold the values, usually int. The names follow the scope of the declaration and cannot collide with other identifiers, which is why prefixing helps in a larger program.
A switch over an enum reads well when every case names one state. An array of names indexed by the enum is shorter when the values run from 0 upward with no gaps.
#include <stdio.h>
enum Color { COLOR_RED, COLOR_GREEN, COLOR_BLUE };
static const char *names[] = {"red", "green", "blue"};
int main(void)
{
enum Color color = COLOR_BLUE;
printf("%s\n", names[color]);
printf("%d %d %d\n", COLOR_RED, COLOR_GREEN, COLOR_BLUE);
return 0;
}Output
blue 0 1 2
1.4 Numbers on purpose
Enum values can be chosen. A set of error codes with explicit values stays stable across releases and can be written to a log without renumbering later.
enum Status { STATUS_OK = 0, STATUS_INPUT = 10, STATUS_FULL = 20 }; gives each name a fixed number. Gaps leave room to insert codes without changing the ones already stored.
Do not rely on an enum's size in a file or a network packet. Write the number with a fixed-width type such as int32_t and convert, or store text.
#include <stdio.h>
enum Status { STATUS_OK = 0, STATUS_INPUT = 10, STATUS_FULL = 20 };
int main(void)
{
enum Status status = STATUS_FULL;
switch (status) {
case STATUS_OK: puts("ok"); break;
case STATUS_INPUT: puts("bad input"); break;
case STATUS_FULL: puts("storage full"); break;
}
printf("%d\n", status);
return 0;
}Output
storage full 20
1.5 Enums as function results
An enum makes a function's possible results visible in its declaration. enum ParseState check(const char *text, int limit); tells the reader that there are a handful of outcomes.
The caller switches on the value and handles each case. A missing case is easier to spot than a missing if around an int return.
The compiler does not require every case to be handled, and some compilers warn with -Wswitch or C4062. Building with warnings on catches the case you added and forgot to handle.
#include <stdio.h>
enum ParseState { PARSE_OK, PARSE_EMPTY, PARSE_TOO_LONG };
static const char *state_names[] = {"ok", "empty", "too long"};
static enum ParseState check(const char *text, int limit)
{
if (text[0] == '\0') return PARSE_EMPTY;
for (int i = 0; text[i] != '\0'; i++)
if (i >= limit) return PARSE_TOO_LONG;
return PARSE_OK;
}
int main(void)
{
printf("%s %s\n", state_names[check("fine", 10)], state_names[check("much too long", 4)]);
return 0;
}Output
ok too long
1.6 Where nesting stops being useful
Two levels read well: an address inside a customer inside an order still follows the sentence. Four levels make every access a chain of dots, and passing one inner value to a function means passing the whole outer struct or a pointer into it.
A const pointer to the inner struct is the escape hatch. void print_date(const struct Date *date); can be called with &shipment.sent without copying the shipment.
Large nested arrays multiply the copy cost. A struct passed by value copies every embedded member, so records with text arrays get expensive quickly.
#include <stdio.h>
struct Date { int day; int month; int year; };
struct Shipment {
char city[16];
struct Date sent;
};
static void print_date(const struct Date *date)
{
printf("%04d-%02d-%02d\n", date->year, date->month, date->day);
}
int main(void)
{
struct Shipment box = {"Pune", {9, 11, 2026}};
print_date(&box.sent);
printf("%s\n", box.city);
return 0;
}Output
2026-11-09 Pune
1.7 Padding and layout
A struct is not a packed list of members. The compiler inserts padding between members so each one starts on an address that suits its alignment, and it may add padding at the end so an array of the struct keeps every element aligned.
A char followed by an int can leave three unused bytes on a typical 64-bit build. That is why sizeof(struct Date) can exceed the sum of its members.
sizeof is the only safe size to use for allocation and for qsort. Reordering members from largest to smallest can shrink a struct, and the exact saving depends on the compiler.
