Skip to content

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

point_t  pt = {1, 2};
point_t *p  = &pt;
    • pt.x - correct. pt is a struct, so use ..
  1. p.x - wrong. p is a pointer; you cannot dot into a pointer. Use p->x.
  2. p->x - correct. Arrow follows the pointer, then selects x.
  3. (*p).y - correct. Dereference p to get the struct, then .y. This is exactly what p->y means.
  4. *p.y - wrong. . binds tighter than *, so this parses as *(p.y), and p.y is invalid. Write (*p).y or p->y.

  5. Two ways to set pt.y to 9 through p:

p->y = 9;
(*p).y = 9;
  1. Use . when you hold the struct itself; use -> when you hold a pointer to the struct.

Exercise A2 - How big is the union?

struct s { int a; double b; char c; };
union  u { int a; double b; char c; };
  1. struct s needs at least 4 + 8 + 1 = 13 bytes, 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."
  2. union u needs 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.
  3. Reading x.a after writing x.b is not meaningful: x.b = 2.0 overwrote the same bytes, so x.a now reinterprets the low bytes of a double as an int. A union does not track which member is currently valid. A tagged union adds an enum field 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;
}
Ada: 100.00
Ada: 130.00
withdraw 200: denied
Ada: 130.00
  • Both functions take account_t * and use ->, so they modify the caller's real account. deposit always adds; withdraw only subtracts when there is enough, leaving the account unchanged and returning 0 otherwise.
  • The call sites pass &acct. Passing acct by value would let the functions change only a copy, and the balance in main would 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;
}
[urgent] priority 5
  • make_tag allocates strlen(label) + 1 bytes 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_tag frees the heap string and NULLs the pointer, reaching the member with ->. One malloc, one free; valgrind ./myprog reports 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;
}
GREEN -> YELLOW -> RED -> GREEN
  • next_light switches on the enum and returns the next state. Because the enum values are just named ints, the switch reads like the actual state diagram.
  • light_name maps each state to a string for printing. Keeping the names in the enum (not scattered 0/1/2 literals) 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;
}
23.5 C
100.0 m
  • print_measurement checks m->kind first, then reads the member that the tag says is valid. Reading m->meters on a KIND_TEMP value would reinterpret the wrong meaning of the same bytes.
  • The union holds only one double worth 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;
}
total area: 21.9911
  • shape_area switches on the tag and reads only the matching union member; total_area walks 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 kind that says what it is. This tagged-union-in-an-array is a common real-world shape: a heterogeneous collection whose elements identify themselves.