Skip to content

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.