Lecture 7 - Structs: Bundling Related Data¶
Last week ended on the heap: memory whose lifetime you control, and the promise
that we would put it to work building real data structures. We start today. So far
every variable has held one value - an int, a char, a pointer. But most
real things are made of several values that belong together: a point is an x and
a y; a person is a name and an age. A struct lets you bundle those values
into a single named type, so they travel together as one unit.
1. Recap and the problem structs solve¶
- The heap gives us memory sized at run time and memory that outlives its function.
We now need a way to describe what lives in that memory once it is more than a
bare array of
ints. - Suppose you are keeping a small roster: for each person you have a name and an age. Without structs you are forced into parallel arrays - one array per field, held together only by a shared index:
char names[100][32]; /* names[i] is the name of person i */
int ages[100]; /* ages[i] is the age of person i */
- This works, but it is fragile, and notice the fields are not even the same type, so you cannot fold them into one array. The two arrays can fall out of step; if you sort by age you must remember to swap the matching name; a function that takes "a person" needs two parameters; and adding a third field (a GPA, an ID) means threading a third array through every loop and call.
- A
structfixes this by making "a person" a single type with named parts - anameand anagebundled together. One variable, one parameter, one array element holds the whole record, name and age travelling as one unit.
In-class exercise: Warm-up (on the computer) - build the roster the hard way first, with parallel arrays: read a count, then read each person's name and age into two arrays, and print them back. Feel how the two arrays must move in lockstep - that friction is exactly what a struct removes. Once we have
struct(Section 2), you will rewrite this same roster as a single array ofperson_t; keep the program handy. Also, consider the various options to input a name if it has a space in it.
2. Defining and using a struct¶
A structure groups several variables - its members (or fields) - under one type name. You define the shape once, then declare variables of that type. Here is the type the roster wanted, bundling a name and an age into one record:
struct person {
char name[32]; /* a fixed-size string, stored inside the struct */
int age;
}; /* note the semicolon after the closing brace */
struct personis now a type, just likeint- except its members are of different types, achararray and anint. That is precisely what two parallel arrays could never really capture. The definition allocates no storage by itself; it is a blueprint.- The semicolon after the closing brace is required and easy to forget. A struct definition is a statement.
To drill the pure mechanics, we will keep a second, even simpler struct beside it -
just two ints, x and y:
Declaring and initializing¶
struct point p; /* two ints' worth of storage, members not yet set */
p.x = 3; /* the dot operator reaches a member */
p.y = 4;
struct point q = {1, 2}; /* initialize members in order: x=1, y=2 */
struct point r = {.x = 5, .y = 6}; /* or name them (designated initializers) */
- The dot operator
.selects a member of a struct:p.xis thexinsidep. You read and writep.xexactly like anyint. {1, 2}fills the members in declaration order. The named form{.x = 5, .y = 6}is clearer and does not depend on order.- A member left out of an initializer is set to zero:
struct point z = {0};zeroes every field.
Using members¶
struct point p = {3, 4};
int dist_sq = p.x * p.x + p.y * p.y; /* members are just ints */
printf("(%d, %d)\n", p.x, p.y); /* (3, 4) */
- Once you have selected a member with
., it behaves like a normal variable of its type. There is nothing special aboutp.xother than where it lives.
Dropping the word struct with typedef¶
Writing struct point and struct person on every declaration is a mouthful. A
typedef gives a type a second, shorter name; after it, you use that name
wherever the long one worked.
First, let's look at how typedef works with a simple type:
typedef unsigned int uint; /* "uint" now means "unsigned int" */
uint x = 42; /* same as "unsigned int x = 42;" */
It is common to name the the new type with a _t suffix, so you can tell at a glance that it is a derived/defined type:
typedef unsigned int uint_t; /* "uint_t" now means "unsigned int" */
uint_t y = 99; /* same as "unsigned int y = 99;" */
So, now, we can do the same with structs, after the struct is defined:
struct point { int x; int y; };
struct person { char name[32]; int age; };
typedef struct point point_t; /* "point_t" now means "struct point" */
typedef struct person person_t; /* "person_t" now means "struct person" */
point_t p = {3, 4}; /* no need to write "struct point" */
person_t pers = {"Ada", 36};
typedef struct point point_t;does not create a new type - it makespoint_tan alias for the existingstruct point. The two names mean the same type and can be mixed freely.- Most code combines the struct definition and the
typedefinto one statement, dropping the tag entirely:
typedef struct {
char name[32];
int age;
} person_t; /* define the shape and name it person_t at once */
- The convention we follow is to suffix a typedef name with
_t(point_t,person_t) so a reader can tell at a glance that the word is a type. From here on we use these short names, andtypedefeach new struct the same way. typedefis pure syntactic sugar: it changes how you write the type, never how the value is laid out or copied. Everything about members, the dot operator, and copy semantics below is exactly the same.
3. A struct is a value: copy semantics¶
This is the idea that surprises people, so we make it explicit early. A struct is a
value, like an int. Assigning one struct to another copies every member,
and passing a struct to a function copies the whole struct into the parameter.
point_t a = {1, 2};
point_t b = a; /* b is a full, independent copy: b.x=1, b.y=2 */
b.x = 99; /* changes b only; a is untouched */
printf("%d %d\n", a.x, b.x); /* 1 99 */
Passing a struct to a function copies it¶
void bump(point_t p) {
p.x = p.x + 1; /* changes the COPY inside bump, not the caller's */
}
point_t origin = {0, 0};
bump(origin);
printf("%d\n", origin.x); /* still 0 - bump modified its own copy */
- Just like an
int, a struct passed by value cannot be changed by the callee. The function gets its own copy. (Next lecture we pass a pointer to a struct when we do want the function to change the caller's struct - the same move asswap.)
Returning a struct¶
point_t make_point(int x, int y) {
point_t p = {x, y};
return p; /* the whole struct is copied back to the caller */
}
point_t c = make_point(7, 8); /* c.x = 7, c.y = 8 */
- Returning a struct copies it out to the caller. Unlike returning the address of a local (which dangles), returning the struct by value is perfectly safe - it is a copy, not a pointer into the dead frame.
In-class exercise: Part A1 (pen and paper) - predict the output of a program that copies a struct and passes one to a function.
4. Nested structs¶
A struct member can itself be a struct. This is how you build bigger shapes out of smaller ones.
typedef struct {
point_t top_left;
point_t bottom_right;
} rect_t;
rect_t box = { {0, 10}, {20, 0} }; /* two points, each {x, y} */
int width = box.bottom_right.x - box.top_left.x; /* chain the dots */
int height = box.top_left.y - box.bottom_right.y;
printf("%d x %d\n", width, height); /* 20 x 10 */
- To reach a member of a nested struct you chain the dot operator:
box.top_left.xis thexof thetop_leftpoint ofbox. Read it left to right, one dot at a time. - The nested initializer mirrors the shape:
{ {0, 10}, {20, 0} }- an outer brace for therect, an inner brace for eachpoint. - Copy semantics still apply, all the way down: copying a
rectcopies both of its points, which copies all fourints. A struct copy is deep for its members (though not, as we will see, for anything a member merely points at).
5. Arrays of structs¶
Because a struct is a type, you can make an array of structs - a table where
each row is a full record. This is the warm-up done right: one array of person
records instead of two parallel arrays that have to move in lockstep.
person_t roster[3] = {
{"Ada", 36},
{"Alan", 41},
{"Grace", 79},
};
for (int i = 0; i < 3; i++) {
printf("%s: %d\n", roster[i].name, roster[i].age);
}
roster[i]is one wholeperson_t;roster[i].agereaches into that element. First index the array, then dot into the element.- Each element carries its own copy of every field, including the 32-byte
namebuffer. The array is one contiguous block of records - and there is no second array to keep in step. - Finding the oldest person is the same max-scan as always, but the "value" is now a record and the key is one of its fields:
int best = 0;
for (int i = 1; i < 3; i++) {
if (roster[i].age > roster[best].age) {
best = i;
}
}
printf("oldest: %s\n", roster[best].name); /* oldest: Grace */
In-class exercise: Part B, Exercise B2 (on the computer) - build an array of records and scan it for the largest field.
6. Wrap-up¶
- A
structbundles several named members into one type. Define it once (with the trailing semicolon), then declare variables and reach members with the dot operator.. - A
typedefgives a struct a shorter name:typedef struct person person_t;(or define and name it in one shot) lets you writeperson_tinstead ofstruct person. It is pure sugar - the layout and copy behaviour are unchanged; we suffix such names with_t. - A struct is a value: assignment and function calls copy every member. Passing by value means a function cannot change the caller's struct; returning a struct by value is safe (it is a copy, not a dangling address).
- Structs nest (chain the dots:
box.top_left.x) and go into arrays (index first, then dot:roster[i].age), which is how you build tables of records. - Cliffhanger: copying a whole struct into every function is wasteful, and we
still cannot change a caller's struct. Next lecture: pointers to structs and
the
->operator fix both, which also let a struct own heap memory through a pointer member; thenenumandunionround out our type toolkit.