Solutions: Lecture 8 In-Class Exercises¶
Solutions to the Lecture 8 in-class exercises. Try each exercise yourself before reading these.
Part A - Pointers and unions (pen and paper)¶
Exercise A1 - Dots and arrows¶
-
pt.x- correct.ptis a struct, so use..
p.x- wrong.pis a pointer; you cannot dot into a pointer. Usep->x.p->x- correct. Arrow follows the pointer, then selectsx.(*p).y- correct. Dereferencepto get the struct, then.y. This is exactly whatp->ymeans.-
*p.y- wrong..binds tighter than*, so this parses as*(p.y), andp.yis invalid. Write(*p).yorp->y. -
Two ways to set
pt.yto9throughp:
- Use
.when you hold the struct itself; use->when you hold a pointer to the struct.
Exercise A2 - How big is the union?¶
struct sneeds at least4 + 8 + 1 = 13bytes, because the members sit side by side - the size is the sum. In practice the compiler rounds up (to 16 or 24) for alignment, but the idea is "all members at once, one after another."union uneeds about 8 bytes - the size of its largest member (double b). The members sit on top of one another in the same storage, so the union only has to be big enough for whichever one is currently in use.- Reading
x.aafter writingx.bis not meaningful:x.b = 2.0overwrote the same bytes, sox.anow reinterprets the low bytes of adoubleas anint. Auniondoes not track which member is currently valid. A tagged union adds anenumfield that records the active member, and you check it before reading.
Part B - At the keyboard¶
Exercise B1 - Deposit and withdraw through a pointer¶
#include <stdio.h>
typedef struct {
char owner[32];
double balance;
} account_t;
void deposit(account_t *a, double amount) {
a->balance += amount;
}
int withdraw(account_t *a, double amount) {
if (a->balance >= amount) {
a->balance -= amount;
return 1;
}
return 0;
}
int main(void) {
account_t acct = {"Ada", 100.0};
printf("%s: %.2f\n", acct.owner, acct.balance);
deposit(&acct, 30.0);
printf("%s: %.2f\n", acct.owner, acct.balance);
if (!withdraw(&acct, 200.0)) {
printf("withdraw 200: denied\n");
}
printf("%s: %.2f\n", acct.owner, acct.balance);
return 0;
}
- Both functions take
account_t *and use->, so they modify the caller's real account.depositalways adds;withdrawonly subtracts when there is enough, leaving the account unchanged and returning0otherwise. - The call sites pass
&acct. Passingacctby value would let the functions change only a copy, and the balance inmainwould never move.
Exercise B2 - A struct that owns a heap string¶
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *label;
int priority;
} tag_t;
tag_t make_tag(const char *label, int priority) {
tag_t t;
t.priority = priority;
t.label = malloc(strlen(label) + 1); /* +1 for the '\0' */
if (t.label != NULL) {
strcpy(t.label, label);
}
return t;
}
void free_tag(tag_t *t) {
free(t->label);
t->label = NULL;
}
int main(void) {
tag_t t = make_tag("urgent", 5);
if (t.label == NULL) {
return 1;
}
printf("[%s] priority %d\n", t.label, t.priority);
free_tag(&t);
return 0;
}
make_tagallocatesstrlen(label) + 1bytes on the heap and copies the characters in, so the struct owns an independent copy of the string, not the caller's buffer.- The struct is returned by value (a pointer plus an
int), but the pointer still names the same heap block - the copy is shallow across the pointer, which is exactly why ownership matters. free_tagfrees the heap string and NULLs the pointer, reaching the member with->. Onemalloc, onefree;valgrind ./myprogreports no leaks.
Exercise B3 - A traffic light state machine¶
#include <stdio.h>
typedef enum { GREEN, YELLOW, RED } light_t;
light_t next_light(light_t current) {
switch (current) {
case GREEN: return YELLOW;
case YELLOW: return RED;
case RED: return GREEN;
}
return GREEN; /* unreachable, but keeps the compiler happy */
}
const char *light_name(light_t l) {
switch (l) {
case GREEN: return "GREEN";
case YELLOW: return "YELLOW";
case RED: return "RED";
}
return "?";
}
int main(void) {
light_t l = GREEN;
for (int i = 0; i < 4; i++) {
printf("%s", light_name(l));
if (i < 3) {
printf(" -> ");
}
l = next_light(l);
}
printf("\n");
return 0;
}
next_lightswitches on the enum and returns the next state. Because the enum values are just named ints, theswitchreads like the actual state diagram.light_namemaps each state to a string for printing. Keeping the names in the enum (not scattered0/1/2literals) is what makes this readable and lets the compiler warn if a case is missing.
Exercise B4 - A tagged union of measurements¶
#include <stdio.h>
typedef enum { KIND_TEMP, KIND_DIST } kind_t;
typedef struct {
kind_t kind;
union {
double celsius;
double meters;
};
} measurement_t;
void print_measurement(const measurement_t *m) {
switch (m->kind) {
case KIND_TEMP: printf("%.1f C\n", m->celsius); break;
case KIND_DIST: printf("%.1f m\n", m->meters); break;
}
}
int main(void) {
measurement_t t = {.kind = KIND_TEMP, .celsius = 23.5};
measurement_t d = {.kind = KIND_DIST, .meters = 100.0};
print_measurement(&t);
print_measurement(&d);
return 0;
}
print_measurementchecksm->kindfirst, then reads the member that the tag says is valid. Readingm->meterson aKIND_TEMPvalue would reinterpret the wrong meaning of the same bytes.- The union holds only one
doubleworth of storage; the tag is the only thing that records whether those bytes mean Celsius or meters.
Stretch¶
An array of shapes with a total area¶
#include <stdio.h>
typedef enum { CIRCLE, RECT } shape_kind_t;
typedef struct {
shape_kind_t kind;
union {
double radius; /* CIRCLE */
struct { double w, h; }; /* RECT */
};
} shape_t;
double shape_area(const shape_t *s) {
switch (s->kind) {
case CIRCLE: return 3.14159 * s->radius * s->radius;
case RECT: return s->w * s->h;
}
return 0.0;
}
double total_area(const shape_t *shapes, int n) {
double total = 0.0;
for (int i = 0; i < n; i++) {
total += shape_area(&shapes[i]);
}
return total;
}
int main(void) {
shape_t shapes[3] = {
{.kind = CIRCLE, .radius = 1.0},
{.kind = RECT, .w = 2.0, .h = 3.0},
{.kind = CIRCLE, .radius = 2.0},
};
printf("total area: %.4f\n", total_area(shapes, 3));
return 0;
}
shape_areaswitches on the tag and reads only the matching union member;total_areawalks the array and sums the per-shape areas.&shapes[i]passes each element by address, so no shape is copied.- One array holds a mix of circles and rectangles, each carrying a
kindthat says what it is. This tagged-union-in-an-array is a common real-world shape: a heterogeneous collection whose elements identify themselves.