The C Path — learn C, visually

🎯 Pointers & Memory

Structs: inventing your own types

⏱ 13 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

Real data travels in bundles: a game character has a name, health, and a position; a contact has a name and a number. An array can't hold that mix, because every element must be the same type. Structs let you weld different pieces into one value you can copy, pass to functions, and return — your first step from using C's types to inventing your own.

Arrays hold many values of one type. But the world is made of records: a point has an x and a y; a player has a name, a score, and health. A struct bundles differently-typed members into one new type — your first taste of designing types instead of just using them.

Defining and using

point.c
#include <stdio.h>

struct Point {          /* a new type: struct Point */
    int x;
    int y;
};

int main(void) {
    struct Point p = { .x = 3, .y = 7 };  /* designated init */

    p.x += 1;                     /* dot: access a member    */
    printf("p = (%d, %d)\n", p.x, p.y);

    struct Point q = p;           /* copies BOTH members     */
    q.y = 0;
    printf("p.y=%d q.y=%d\n", p.y, q.y);
    return 0;
}
terminal
$ gcc point.c -o point && ./point
p = (4, 7)
p.y=7 q.y=0
# q was a full copy — changing it left p alone

Note what assignment did: struct Point q = p; copied every member. Structs are values — they copy, pass, and return whole, unlike arrays (which decay into pointers the moment you look at them).

🧠 Checkpoint: After struct Point q = p; q.x = 99; — what is p.x?

  • 99
  • Unchanged — struct assignment copies the whole value
  • Undefined behavior
  • Compile error: structs can’t be assigned
Show answer

Unchanged — struct assignment copies the whole value — Structs are first-class values: =, argument passing, and return all copy member-by-member. If you WANT sharing, pass a pointer — that’s the next section.

Pointers to structs: the -> arrow

Copying a big struct into every function call is wasteful, and copies can't modify the original — so in practice you pass a pointer to the struct. Accessing a member through a pointer is so common it earned its own operator: p->x is sugar for (*p).x:

arrow.c
#include <stdio.h>

struct Point { int x, y; };

void move(struct Point *p, int dx, int dy) {
    p->x += dx;                /* same as (*p).x += dx */
    p->y += dy;
}

int main(void) {
    struct Point pt = {10, 20};
    move(&pt, 1, -2);          /* pass the ADDRESS     */
    printf("(%d, %d)\n", pt.x, pt.y);
    return 0;
}
terminal
$ gcc arrow.c -o arrow && ./arrow
(11, 18)
# move() reached back through the pointer — swap() all over again
⚠️

Why the parentheses in (*p).x? Because . binds tighter than *: the unparenthesized *p.x parses as *(p.x) — "dereference the member x of p" — which is a type error. The arrow exists precisely so you never have to remember this.

🧠 Checkpoint: You have struct Point *p. Which expression reads member x?

  • p.x
  • *p.x
  • p->x (equivalently (*p).x)
  • &p.x
Show answer

p->x (equivalently (*p).x) — Dot needs an actual struct, not a pointer. *p.x parses as *(p.x) — wrong. The arrow is exactly "dereference, then dot", precedence handled for you.

Memory layout: mind the gaps

You might expect struct { char c; int n; } to take 1 + 4 = 5 bytes. It takes 8. Why? CPUs load an int fastest when its address is a multiple of 4, so the compiler inserts invisible padding after c to push n to an aligned offset:

This spot has an interactive memgrid widget — open the interactive lesson to play with it.

padding.c
#include <stdio.h>
#include <stddef.h>

struct Bad  { char a; int n; char b; };   /* 1+3pad+4+1+3pad */
struct Good { int n; char a; char b; };   /* 4+1+1+2pad      */

int main(void) {
    printf("Bad : %zu bytes\n", sizeof(struct Bad));
    printf("  a@%zu n@%zu b@%zu\n",
           offsetof(struct Bad, a),
           offsetof(struct Bad, n),
           offsetof(struct Bad, b));
    printf("Good: %zu bytes\n", sizeof(struct Good));
    return 0;
}
terminal
$ gcc padding.c -o padding && ./padding
Bad : 12 bytes
  a@0 n@4 b@8
Good: 8 bytes
# same three members, 33% smaller — order matters
💡

Order members from largest to smallest and padding mostly disappears — that's how Good saved 4 bytes over Bad with identical members. In an array of a million structs, that's 4 MB for free. (The exact padding is implementation-defined; offsetof from <stddef.h> tells you the truth on your platform. And never compare structs with memcmp — the padding bytes are indeterminate.)

🧠 Checkpoint: Why is sizeof(struct { char c; int n; }) typically 8, not 5?

  • Compilers round every struct to a power of two
  • Three padding bytes align n to a 4-byte boundary
  • The struct tag itself occupies bytes
  • char secretly takes 4 bytes inside structs
Show answer

Three padding bytes align n to a 4-byte boundary — Alignment: int wants an address divisible by 4, so the compiler pads after c. The struct’s total size is also padded to a multiple of the strictest alignment so arrays of it stay aligned.

The typedef struct pattern

Tired of typing struct Point everywhere? typedef gives the type a one-word name — this is the idiom you'll see in virtually every C library, along with designated initializers and compound literals:

vec2.c
#include <stdio.h>

typedef struct {
    double x, y;
} Vec2;                      /* now just "Vec2"          */

Vec2 add(Vec2 a, Vec2 b) {   /* in by value, out by value */
    return (Vec2){ a.x + b.x, a.y + b.y };
}

int main(void) {
    Vec2 v = add((Vec2){1, 2}, (Vec2){3, 4});
    printf("(%g, %g)\n", v.x, v.y);
    return 0;
}
terminal
$ gcc vec2.c -o vec2 && ./vec2
(4, 6)

Passing and returning structs by value like this is perfectly fine for small types (a couple of words). For big structs, pass const struct Big * instead: pointer-sized cost, and const documents that you won't modify it.

This spot has an interactive editor widget — open the interactive lesson to play with it.

A struct gives every member its own bytes. Next: a stranger beast where all the members share the same bytes.

▶ Practice this lesson interactively (with live gcc)