07 · Structs¶
🎥 Video walkthrough¶
C's built-in types (int, double, char, arrays) only get you so far when
modeling real-world data. A struct lets you bundle several related values
of different types together under one name, so a "point" or a "student" can
be passed around as a single unit instead of a scattered handful of loose
variables.
Defining and declaring a struct¶
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point p1; // declare a variable of type "struct Point"
p1.x = 3; // access members with the dot operator
p1.y = 7;
printf("p1 = (%d, %d)\n", p1.x, p1.y);
// Structs can also be initialized at declaration time
struct Point p2 = {10, 20};
printf("p2 = (%d, %d)\n", p2.x, p2.y);
return 0;
}
// Output:
// p1 = (3, 7)
// p2 = (10, 20)
The struct Point declaration itself doesn't allocate any memory or create a
variable — it just describes the shape of the data. struct Point p1; is
what actually creates a variable with that shape, the same way int x;
creates an int variable after int is defined as a type by the language.
The dot operator¶
Once you have a struct variable, . accesses (reads or writes) its members,
exactly like p1.x and p1.y above. Structs can be compared member-by-member
(there's no built-in == for whole structs), copied, and passed around:
struct Point p3 = p2; // copies all members: p3.x = 10, p3.y = 20
p3.x = 99; // only p3 changes -- p2 is untouched
Nesting: structs containing arrays and other structs¶
Struct members can themselves be arrays or other structs, which is how you model richer records:
#include <stdio.h>
#include <string.h>
struct Point {
int x;
int y;
};
struct Student {
char name[50]; // fixed-size array member
int age;
double gpa;
struct Point office; // a nested struct
};
int main(void) {
struct Student s;
strcpy(s.name, "Maria Chen"); // can't assign strings directly to char arrays
s.age = 21;
s.gpa = 3.8;
s.office.x = 4; // reach into the nested struct with another dot
s.office.y = 12;
printf("%s, age %d, GPA %.1f, office at (%d, %d)\n",
s.name, s.age, s.gpa, s.office.x, s.office.y);
return 0;
}
// Output:
// Maria Chen, age 21, GPA 3.8, office at (4, 12)
Note that s.name = "Maria Chen"; would not compile — arrays (including
char arrays used as strings) can't be assigned with = after declaration,
so strcpy is used instead, same as in
Module 5, Arrays & Strings.
typedef — dropping the struct keyword¶
Writing struct Point everywhere is repetitive. typedef creates an alias so
you can just write Point:
#include <stdio.h>
typedef struct {
int x;
int y;
} Point; // "Point" is now a type name on its own
int main(void) {
Point p1 = {3, 7}; // no "struct" keyword needed
Point p2 = {10, 20};
printf("p1 = (%d, %d)\n", p1.x, p1.y);
printf("p2 = (%d, %d)\n", p2.x, p2.y);
return 0;
}
// Output:
// p1 = (3, 7)
// p2 = (10, 20)
This pattern — an anonymous struct immediately given a name via typedef —
is by far the most common way structs are declared in real C code, and it's
what the rest of this module uses.
Arrays of structs¶
Just like arrays of int or char, you can have arrays of structs — useful
any time you're managing a list of records (contacts, students, inventory
items):
#include <stdio.h>
#include <string.h>
typedef struct {
char name[50];
double gpa;
} Student;
int main(void) {
Student roster[3];
strcpy(roster[0].name, "Alice");
roster[0].gpa = 3.9;
strcpy(roster[1].name, "Bob");
roster[1].gpa = 3.2;
strcpy(roster[2].name, "Carol");
roster[2].gpa = 3.6;
for (int i = 0; i < 3; i++) {
printf("%-10s GPA: %.1f\n", roster[i].name, roster[i].gpa);
}
return 0;
}
// Output:
// Alice GPA: 3.9
// Bob GPA: 3.2
// Carol GPA: 3.6
This exact pattern — an array of a struct with char fields — is the
backbone of the contact book you'll build in
Module 10.
Passing structs to functions¶
Passing a struct to a function by value copies the entire struct, member by
member, into the function's parameter — just like passing an int copies the
value:
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
void printPoint(Point p) { // p is a full copy of whatever was passed in
printf("(%d, %d)\n", p.x, p.y);
}
void tryToMove(Point p) {
p.x += 100; // modifies the local copy only
}
int main(void) {
Point origin = {0, 0};
printPoint(origin); // (0, 0)
tryToMove(origin);
printPoint(origin); // still (0, 0) -- tryToMove only changed its own copy
return 0;
}
// Output:
// (0, 0)
// (0, 0)
For a small struct like Point, copying is cheap and harmless. For larger
structs, or when a function genuinely needs to modify the caller's struct,
you pass a pointer to the struct instead — combined with the -> operator
to access members through the pointer without an explicit dereference. That's
a deliberate deep dive in
Level 2, Module 4, building on the
pointer basics from Module 6.
| Concept | Example | Meaning |
|---|---|---|
| Define a struct type | struct Point { int x; int y; }; |
Describes a shape of grouped data |
| Declare a variable | struct Point p; |
Creates a variable with that shape |
| Access a member | p.x |
Dot operator reads/writes a field |
typedef |
typedef struct {...} Point; |
Lets you write Point instead of struct Point |
| Nesting | struct Student { struct Point office; }; |
A struct field can itself be a struct |
| Pass by value | void f(Point p) |
Function gets a full copy |
How It Actually Works¶
A struct's memory layout is simply its members laid out one after another
in the order they're declared, with the compiler choosing the struct's
total size and each member's offset at compile time. For
struct Point { int x; int y; }, x sits at offset 0 and y at offset 4
(assuming 4-byte ints), and p1.y compiles to exactly the same kind of
address arithmetic as array indexing: *(char*)&p1 + 4, reinterpreted as
an int. There's no per-field bookkeeping or name lookup at runtime — .y
is resolved to a fixed byte offset entirely at compile time, which is why
struct field access costs nothing more than reading a variable directly.
Compilers usually don't pack members edge-to-edge, though — they insert
padding so each member starts at an address matching its own alignment
requirement (a double typically needs to start at an address divisible
by 8, for instance). A struct { char c; int n; } is commonly not 5
bytes but 8: 1 byte for c, 3 padding bytes, then 4 bytes for n, because
the CPU can load an aligned 4-byte value in one instruction but may need
two (or a slower unaligned instruction) for a misaligned one. Reordering
members from largest to smallest is a common real-world trick to shrink
padding and total struct size, since padding is only inserted between and
after members, never compacted retroactively by the compiler.
Copying a struct (struct Point p3 = p2;) is a flat, member-by-member
byte copy of the whole block — for Point that's memcpy-equivalent of 8
bytes, done in one or two instructions. This scales badly for large
structs, which is exactly why passing a struct by value to a function
(as printPoint/tryToMove do) copies the entire block onto the callee's
stack frame: for a small struct like Point that's cheap, but for a struct
holding kilobytes of data, every by-value call/return silently copies all
of it — the reason Level 2's struct module introduces passing a Point *
instead, so the function receives only an 8-byte address rather than the
whole payload.
typedef struct {...} Point; doesn't change any of this layout — it's a
purely compile-time naming convenience telling the compiler "wherever you
see Point, substitute this anonymous struct type." No extra memory,
indirection, or runtime cost is introduced; the generated machine code for
Point p1 = {3,7}; is identical to what struct Point p1 = {3,7}; would
produce for a named struct with the same members.
🔀 See this in another language¶
Exercise¶
Define a typedef struct called Rectangle with members width and height
(both double), and a nested Point topLeft (reusing the Point struct from
this module) marking where the rectangle sits on a grid. Write a function
double area(Rectangle r) that returns r.width * r.height, and a function
void printRectangle(Rectangle r) that prints the rectangle's top-left
position, width, height, and area. Create an array of 3 Rectangle values in
main, fill them in, and print all three using your function.