Lecture 8 - Pointers to Structs, Heap-Owned Members, Enums, and Unions¶
Last lecture we built structs (and named them with typedef) and saw that they are
values: every assignment and every function call copies the whole thing. That is
convenient but has two costs - a function cannot change the caller's struct, and
copying a large struct on every call is wasteful. Today we fix both with pointers
to structs, which also let a struct take charge of heap memory through a
pointer member. Then we round out our type toolkit with two more constructs you will
use constantly: enum for named integer constants, and union for
overlapping storage.
1. Recap and today's problem¶
- A struct groups named members; you reach them with the dot operator and copy the whole struct on assignment or a by-value call.
- A stack-allocated struct is copy-by-value all the way down, so a struct that contains another struct copies the inner struct too. That is convenient but can be wasteful for large structs, and it prevents a function from changing the caller's struct.
typedef struct { double m[16]; } matrix_t; /* 128 bytes */
/* By value: the whole 128-byte struct is copied onto the stack for the
parameter, and copied AGAIN for the return value - 256 bytes moved. */
matrix_t scale_by_value(matrix_t a, double k) {
for (int i = 0; i < 16; i++) {
a.m[i] *= k; /* edits the local copy; caller's matrix is untouched */
}
return a; /* another full 128-byte copy back out */
}
matrix_t big = { ... };
big = scale_by_value(big, 2.0); /* two 128-byte copies just to scale in place */
- Every call copies all 128 bytes in, and the return copies them all out again - work that grows with the struct's size and happens on every call. The by-pointer version below moves a single 8-byte address instead, and edits the caller's matrix directly, so nothing is copied at all.
- The two costs of by-value: (a)
bump(point_t p)changed only its copy, so the caller was untouched; (b) passing a big struct copies every byte each call. - The fix is the same one we used for
swap: pass an address. A pointer to a struct lets a function reach into the caller's actual struct - to read it without copying, or to change it in place.
2. Pointers to structs and the -> operator¶
A pointer to a struct is just an address, made with & and followed with *, like
any pointer. The only new wrinkle is reaching a member through it.
typedef struct { int x; int y; } point_t;
point_t pt = {3, 4};
point_t *p = &pt; /* p holds the address of pt */
To read x through p, you must dereference then select the member. Because
. binds tighter than *, you need parentheses:
That is clumsy, so C gives you the arrow operator -> as shorthand:
int b = p->x; /* exactly the same as (*p).x, but readable */
p->y = 10; /* write through the pointer: changes pt.y in the caller */
p->membermeans(*p).member. Follow the pointer to the struct, then select the member. Use->whenever you have a pointer to a struct, and.when you have the struct itself.- Writing through the pointer (
p->y = 10) changes the originalpt, becausepnamespt's storage. This is the whole point of using a pointer.
3. Passing structs by pointer¶
Now a function can change the caller's struct, or read a large one without copying, by taking a pointer to it.
Changing the caller's struct¶
void move(point_t *p, int dx, int dy) {
p->x += dx; /* reaches into the caller's struct */
p->y += dy;
}
point_t pt = {0, 0};
move(&pt, 3, 4); /* pass the ADDRESS */
printf("(%d, %d)\n", pt.x, pt.y); /* (3, 4) - the real pt changed */
- Pass
&pt; inside, use->. Compare with last lecture'sbump, which took the struct by value and could not change the caller. The single character difference - a*in the parameter and a&at the call - is what makes the change stick.
Reading a big struct without copying: const pointer¶
double area(const rect_t *r) { /* const: this function only reads *r */
int w = r->bottom_right.x - r->top_left.x;
int h = r->top_left.y - r->bottom_right.y;
return (double) w * h;
}
- Passing
const rect_t *hands over one address instead of copying the whole rectangle. Theconstpromises the function will only read the struct - the compiler enforces it, and it documents intent to the caller. - Rule of thumb: pass a pointer when you want to change the caller's struct or
to avoid copying a large one; add
constwhen you are only reading.
In-class exercise: Part B, Exercise B1 (on the computer) - change a caller's struct in place through a pointer.
4. Heap-owned members: a struct that owns memory¶
Until now our structs stored everything inline - a char name[32] lived right inside
the struct. That wastes room for short names and cannot hold a long one. The
alternative, and the pattern behind nearly every real data structure, is a member
that is a pointer to memory the struct allocates on the heap:
- Now
person_tis small and fixed-size (a pointer plus anint), no matter how long the name is. The characters live on the heap; the struct just holds the address. - The catch: someone has to allocate that string and someone has to free it. A
pointer member makes the struct responsible for heap memory - the ownership idea
from the heap lecture, now inside a struct and reached with the
->and pointer parameters from the last two sections.
Building one: allocate and copy the string¶
#include <stdlib.h>
#include <string.h>
person_t make_person(const char *name, int age) {
person_t pers;
pers.age = age;
pers.name = malloc(strlen(name) + 1); /* +1 for the '\0' terminator */
if (pers.name != NULL) {
strcpy(pers.name, name); /* copy the characters in */
}
return pers;
}
- Allocate
strlen(name) + 1bytes - one per character plus one for the terminating'\0'. Forgetting the+ 1is the classic off-by-one that corrupts memory. (You builtstrlen/strcpyyourself in HW1; here we use the<string.h>versions.) - Copy the characters into our own heap block so the struct owns an independent copy,
not a borrowed pointer into the caller's
name.
Freeing one: free the members, then you are done¶
void free_person(person_t *pers) {
free(pers->name); /* free the heap string first */
pers->name = NULL; /* avoid a dangling pointer */
}
free_persontakes a pointer so that settingnametoNULLsticks in the caller's struct, and reaches the member with->- exactly the moves from Sections 2 and 3.- A struct copy does not copy what a pointer member points at. If you write
person_t b = a;, botha.nameandb.namepoint at the same heap string. Freeing one and then using or freeing the other is a use-after-free or double-free. A struct copy is shallow across a pointer member. - The heap rule still holds, now per member: every
mallochas exactly onefree. A struct that owns heap memory needs a function whose job is to free it; when you allocate a member, decide right then who will free it. - This shallow-copy hazard is why real code writes explicit "copy" and "free" functions for owning structs, and it is the seed of the linked-list and hash-table ownership rules coming up.
In-class exercise: Part B, Exercise B2 (on the computer) - write a struct that owns a heap string, with functions to build it and free it.
5. Enumerations¶
Programs are full of small integer constants that stand for something - a status
code, a direction, a color. Rather than sprinkle raw numbers like 0 and 1 through
the code, we name them once, usually in a header or at global scope so every file can
see them. C already gives you two ways to do this. The first is a #define, a
preprocessor directive that textually replaces the name with its value before the
compiler runs:
The second is a const int, a real variable the compiler knows about, which the
const keyword makes read-only:
- Both work, but they share a weakness for a set of related values: nothing ties the
three names together, and you assign every value by hand, which makes it easy to
duplicate a number or skip one. Neither gives you a "color" type - a
#defineis gone by the time the compiler runs, and eachconst intis just a stand-alone integer. - An
enumis built for exactly this case: it declares a set of named integer constants in one line, numbers them for you, and gives the set a name you can use as a type.
Instead of remembering that 0 means red and 1 means green, you give the values
names - and let the compiler assign and track them:
enum color { RED, GREEN, BLUE }; /* RED=0, GREEN=1, BLUE=2 by default */
enum color c = GREEN;
if (c == GREEN) {
printf("go\n");
}
- Members are numbered from 0 and increase by one unless you say otherwise. You
can set values explicitly:
enum status { OK = 200, NOT_FOUND = 404 };, and any member you leave unset continues from the previous one. - Under the hood an
enumvalue is just anint, so it works in comparisons,switchlabels, and array indices. The names exist for you: they make code read like its intent and let the compiler warn on aswitchthat forgets a case.
switch (c) {
case RED: printf("stop\n"); break;
case GREEN: printf("go\n"); break;
case BLUE: printf("cool\n"); break;
}
- Combined with
typedef, the usual idiom drops theenumkeyword at the use site:
typedef enum { NORTH, EAST, SOUTH, WEST } direction_t;
direction_t d = NORTH;
d = (d + 1) % 4; /* turn right: NORTH -> EAST */
In-class exercise: Part B, Exercise B3 (on the computer) - use a typedef'd enum to model a small set of states.
6. Unions¶
Pacing note (overflow): this section is the designated spillover for the lecture. Cover it here if we are on schedule; otherwise it opens the next lecture, just before linked lists. Nothing later in the course depends on it having been reached today, and the
enumtag it builds on is already in place from Section 5.
A union looks like a struct but its members share the same storage - they
overlap in memory. A union is exactly big enough for its largest member, and
only one member holds a valid value at a time.
union number {
int i;
double d;
};
union number n;
n.i = 42; /* the storage now holds an int */
n.d = 3.14; /* the SAME storage now holds a double; the int is gone */
- In a struct, members sit side by side and the size is the sum of the members. In a union, members sit on top of one another and the size is the largest member. Writing one member overwrites the others.
- A union by itself does not remember which member is currently valid. Reading
n.iafter writingn.dreinterprets the bytes and is almost always a bug. So a union is nearly always paired with a tag that records the active member.
Tagged unions: the real use¶
Pair an enum tag with a union inside a struct. The tag says which union member
is live; you always check the tag before touching the union.
typedef enum { SHAPE_CIRCLE, SHAPE_RECT } shape_kind_t;
typedef struct {
shape_kind_t kind; /* which member of the union is valid */
union {
double radius; /* valid when kind == SHAPE_CIRCLE */
struct { double w, h; }; /* valid when kind == SHAPE_RECT */
};
} shape_t;
double shape_area(const shape_t *s) {
switch (s->kind) {
case SHAPE_CIRCLE: return 3.14159 * s->radius * s->radius;
case SHAPE_RECT: return s->w * s->h;
}
return 0.0;
}
- The
kindtag is the source of truth: the code readsradiusonly whenkindisSHAPE_CIRCLE, andw/honly when it isSHAPE_RECT. Check the tag, then read the matching member. - This "tag plus union" is how C represents a value that is one of several shapes - a JSON value, a token from a parser, a message of several kinds. It saves the space a struct-with-every-field would waste, at the cost of the discipline of always consulting the tag.
In-class exercise: Part B, Exercise B4 (on the computer) - build a tagged union and switch on its tag.
7. Wrap-up¶
- A pointer to a struct lets a function change the caller's struct or read a big
one without copying. Reach members through it with
->(which is(*p).member); addconstto the pointer parameter when you only read. - A struct can own heap memory through a pointer member: allocate it
(
strlen + 1, copy in) and give the struct afree_*function. A struct copy does not copy what a pointer member points at - the shallow-copy hazard - so everymallocstill has exactly onefree. enumnames a set of integer constants (from 0 by default), for readable comparisons andswitchlabels.unionoverlaps its members in one block sized to the largest; only one is valid at a time, so it is almost always wrapped in a tagged union - anenumtag plus the union - and you check the tag before reading. (This is the overflow topic; if we did not reach it today, it opens the next lecture.)- Cliffhanger: we now have structs, pointers to them, and the heap. Next week we combine all three: a struct with a pointer member that points at another struct of the same type - a self-referential struct - is a linked list, our first real dynamic data structure.