Updated Jul 14, 2026

Structs & Typedef

Every type you've used so far holds one kind of thing: an int holds a number, a char[] holds a run of characters. But real data is rarely that simple. A point on a screen is an x and a y. A person is a name and an age and a balance. You could track these as separate variables - int x; int y; - but nothing stops you from updating x and forgetting y, and you can't pass "a point" to a function as one thing. You'd have to pass two arguments and hope you keep them in sync forever.

A struct is C's answer: a new type you define, made of other types glued together, that the compiler treats as one value. Move the struct, and every field moves with it. Pass it to a function, and all its fields go together, correctly, every time. This is the last basic building block before the deep half of this guide, and it matters more than it looks: chapter 10 onward, you'll build linked lists and trees out of structs that point to other structs.

Defining and using a struct

Here's a struct for a point in 2D space:

#include <stdio.h>

struct point {
    int x;
    int y;
};

int main(void) {
    struct point p1 = { 3, 4 };   // fields set in order: x=3, y=4
    printf("p1 = (%d, %d)\n", p1.x, p1.y);

    p1.x = 10;                    // dot (.) accesses a field on the struct itself
    printf("p1 = (%d, %d)\n", p1.x, p1.y);

    return 0;
}
$ gcc point.c -o point && ./point
p1 = (3, 4)
p1 = (10, 4)

What just happened: struct point { ... }; defines a new type called struct point - notice the keyword struct is part of the type's name, not optional decoration. p1.x reads "the x field of p1", using the dot operator. That's the whole model: a struct is a labeled box of smaller boxes, and the dot reaches into one of them.

Struct assignment copies everything

This is the detail that surprises people coming from other languages, and it connects straight back to what you learned about arrays in phase 6: a plain struct point p2 = p1; does not make p2 point at p1's data. It copies every field, field by field, into a brand new struct.

struct point p1 = { 3, 4 };
struct point p2 = p1;   // full copy: p2.x = 3, p2.y = 4

p2.x = 99;
printf("%d %d\n", p1.x, p2.x);   // 3 99 - p1 was never touched

Unlike an array, a struct is a value you can assign, copy, and return from a function whole - the compiler generates the field-by-field copy for you. That's genuinely convenient for small structs. For big ones (say, a struct holding a 10,000-element array), it means p2 = p1 silently copies a lot of bytes, which is worth knowing before you do it in a hot loop.

Passing structs to functions: by value, or by pointer

Because assignment copies, passing a struct as a function argument copies it too - the function gets its own private copy and any changes it makes vanish when it returns:

void move_wrong(struct point p) {
    p.x += 1;   // only changes the local copy
}

void move_right(struct point *p) {
    p->x += 1;  // changes the original, through the pointer
}

int main(void) {
    struct point p1 = { 3, 4 };
    move_wrong(p1);
    printf("%d\n", p1.x);   // 3 - unchanged, move_wrong only touched a copy

    move_right(&p1);
    printf("%d\n", p1.x);   // 4 - the real thing changed
}

move_right takes a struct point * - a pointer to a point, exactly the pointer mental model from phase 5. Inside, p->x means "follow the pointer, then access field x" - it's shorthand for (*p).x, which is clunky enough that C gives you the arrow operator instead. Rule of thumb: pass small structs by value when you just need to read them; pass a pointer when the function needs to modify the original, or when the struct is large enough that copying it is wasteful.

Passing style Syntax Function sees Use when
By value f(struct point p) a copy struct is small, read-only use
By pointer f(struct point *p) the original, via p->field need to modify it, or it's large

Nested structs

Structs can contain other structs. A rectangle is naturally two points:

struct rect {
    struct point top_left;
    struct point bottom_right;
};

struct rect r = { { 0, 0 }, { 10, 5 } };
printf("width = %d\n", r.bottom_right.x - r.top_left.x);   // 10

Chain the dots to reach through the nesting: r.bottom_right.x means "in r, get bottom_right, then get its x." Nothing new is happening here, it's the same dot operator, just applied twice.

typedef: giving a struct a shorter name

Notice that every declaration above had to say struct point, not just point. That's because in C, defining struct point only creates the tag point under the struct namespace - it doesn't create a plain type name you can use on its own. Writing struct everywhere works fine, but it's noisy, and almost every real C codebase uses typedef to skip it:

typedef struct {
    int x;
    int y;
} Point;

int main(void) {
    Point p1 = { 3, 4 };   // no "struct" needed
    printf("(%d, %d)\n", p1.x, p1.y);
    return 0;
}

What typedef actually is: it defines an alias for a type, nothing more. typedef struct { ... } Point; says "define an anonymous struct, then let Point be another name for it." It doesn't change how the struct works, doesn't add behavior, doesn't cost anything at runtime - it's purely a name the compiler substitutes at compile time. You'll see this exact typedef struct { ... } Name; pattern constantly; it's the standard way to define a struct type in C, precisely so you never have to type struct again.

You can also typedef a named struct separately, which matters once structs start referencing themselves (linked lists, trees - coming in the deep half of this guide):

typedef struct point Point;   // Point is now an alias for struct point
struct point { int x; int y; };

Both spellings show up in real code; the anonymous version above is more common for simple data-only structs.

A quick look ahead: self-referential structs

One thing a struct can't contain is itself, directly - struct node { struct node next; } would need infinite memory, since every node would contain another whole node forever. But a struct absolutely can contain a pointer to its own type, because a pointer is always the same fixed size regardless of what it points to:

typedef struct node {
    int value;
    struct node *next;   // a pointer to another node - this is legal
} Node;

This one pattern - a struct holding a pointer to its own type - is the entire foundation of linked lists, trees, and most of the dynamic data structures you'll build once you reach dynamic memory in phase 10. File it away; you'll use it constantly from here on.

Recap

  1. A struct bundles different types into one named type; access fields with .
  2. Assigning a struct copies every field. It's a real value, not a reference - unlike an array.
  3. Pass structs by value to give a function a private copy, or by pointer (-> to access fields) so it can modify the original or avoid copying a large struct.
  4. Structs can nest; chain . to reach through the layers.
  5. typedef just creates an alias for a type - typedef struct { ... } Name; is the standard way to skip writing struct everywhere.
  6. A struct can hold a pointer to its own type (not itself directly) - the pattern behind every linked data structure to come.

Check yourself

[
  {
    "q": "After `struct point p2 = p1;` followed by `p2.x = 99;`, what happens to `p1.x`?",
    "choices": [
      "It also becomes 99, since p2 points at p1's data",
      "It stays whatever it was before - p2 is a separate copy",
      "It's undefined behavior",
      "It becomes 0"
    ],
    "answer": 1,
    "explain": "Struct assignment copies every field into a brand new struct, unlike an array; p1 and p2 share nothing after the copy."
  },
  {
    "q": "Why does `move_wrong(struct point p)` fail to change the caller's struct, while `move_right(struct point *p)` succeeds?",
    "choices": [
      "move_wrong has a bug in its syntax",
      "move_wrong receives a copy of the struct, so changes to it vanish when the function returns; move_right receives a pointer to the original",
      "Structs can never be modified inside a function",
      "move_right is faster, which is the only real difference"
    ],
    "answer": 1,
    "explain": "Passing a struct by value copies it just like assignment does; passing a pointer lets the function reach through to the original via ->."
  },
  {
    "q": "Why can `struct node { struct node *next; }` compile, when `struct node { struct node next; }` cannot?",
    "choices": [
      "The pointer version is a stylistic choice with no real difference",
      "A pointer is always a fixed size no matter what it points to, so it doesn't require infinite memory; a direct struct-inside-itself would",
      "C only allows pointers to be named next",
      "The non-pointer version is legal too, just slower"
    ],
    "answer": 1,
    "explain": "A struct containing itself directly would need infinite memory, since each copy holds another whole copy; a pointer is a fixed-size address regardless of what type it points to."
  }
]

Phase 8: Header Files & the Preprocessor →

Check your understanding 3 questions

1. After `struct point p2 = p1;` followed by `p2.x = 99;`, what happens to `p1.x`?

2. Why does `move_wrong(struct point p)` fail to change the caller's struct, while `move_right(struct point *p)` succeeds?

3. Why can `struct node { struct node *next; }` compile, when `struct node { struct node next; }` cannot?