🎯 Pointers & Memory
Structs: inventing your own types
▶ Open the interactive lesson — free, no signupReal 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
#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;
}$ 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:
#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;
}$ 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.xp->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.
#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;
}$ 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:
#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;
}$ 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)