Structs, unions, enums and typedef · about 45 min

Structs

Why this matters

So far every value has been a single number, character or array of one type. Real data comes in bundles: a point has an x and a y, a person has a name and a birth date, a network packet has a source, a destination, a length and a payload. A struct groups related values of different types into one new type you can declare, pass, return and copy as a unit. Structs are how C programs model everything: the operating system's struct stat describes a file, struct tm a calendar time, FILE (which you will meet in the files module) is a struct. Combined with pointers and the heap, structs are also how every data structure in the next modules is built.

Defining and using a struct

#include <stdio.h>

struct point {
    int x;
    int y;
};

int main(void)
{
    struct point a;
    a.x = 3;
    a.y = 4;

    struct point b = { 10, 20 };
    struct point c = { .y = 7, .x = 1 };
    struct point d = { 0 };

    printf("a = (%d, %d)\n", a.x, a.y);
    printf("b = (%d, %d)\n", b.x, b.y);
    printf("c = (%d, %d)\n", c.x, c.y);
    printf("d = (%d, %d)\n", d.x, d.y);

    struct point e = b;     /* copies every member */
    e.x = 99;
    printf("b.x = %d, e.x = %d\n", b.x, e.x);
    printf("sizeof(struct point) = %zu\n", sizeof(struct point));
    return 0;
}
a = (3, 4)
b = (10, 20)
c = (1, 7)
d = (0, 0)
b.x = 10, e.x = 99
sizeof(struct point) = 8

The pieces:

What a struct is in memory

A struct variable is a block of memory with the members laid out in declaration order:

  struct point b = { 10, 20 };

  address:  b+0          b+4
            +------------+------------+
            |     10     |     20     |
            +------------+------------+
              b.x (int)    b.y (int)
            <---- sizeof(struct point) = 8 ---->

The compiler knows each member's offset from the start. b.y compiles to "the 4 bytes at the address of b plus 4". No names exist at run time, only offsets, exactly like local variables in a stack frame. (The compiler may insert padding between members for alignment; the next lesson is about that.)

Structs are values: copying, passing, returning

Unlike arrays, structs behave like plain values:

#include <stdio.h>

struct point {
    int x;
    int y;
};

struct point midpoint(struct point a, struct point b)
{
    struct point m = { (a.x + b.x) / 2, (a.y + b.y) / 2 };
    return m;
}

void try_to_move(struct point p)
{
    p.x += 100;          /* changes the copy only */
}

int main(void)
{
    struct point a = { 0, 0 }, b = { 10, 4 };
    struct point m = midpoint(a, b);
    printf("midpoint: (%d, %d)\n", m.x, m.y);
    try_to_move(a);
    printf("a after try_to_move: (%d, %d)\n", a.x, a.y);
    return 0;
}
midpoint: (5, 2)
a after try_to_move: (0, 0)

Returning m from midpoint is fine, even though m is a local: the value is copied out before the frame disappears. (Returning a pointer to m would be the dangling-pointer bug from the stack lesson.)

Copying is cheap for small structs like point. For a struct of several hundred bytes, every call copies all of them, so large structs are usually passed by pointer instead. That is the topic of the third lesson in this module.

Nested structs and arrays inside structs

Members can be any type, including arrays and other structs:

#include <stdio.h>
#include <string.h>

struct date {
    int year, month, day;
};

struct person {
    char name[16];
    struct date born;
};

int main(void)
{
    struct person ada = { "Ada", { 1815, 12, 10 } };
    struct person copy = ada;
    strcpy(copy.name, "Copy");
    copy.born.year = 2000;
    printf("%s born %d-%02d-%02d\n", ada.name, ada.born.year, ada.born.month, ada.born.day);
    printf("%s born %d\n", copy.name, copy.born.year);
    printf("sizeof(struct person) = %zu\n", sizeof(struct person));
    return 0;
}
Ada born 1815-12-10
Copy born 2000
sizeof(struct person) = 28

int year, month, day; declares three members at once. ada.born.year chains member accesses left to right. The name array lives inside the struct (16 bytes of it), so copying the struct copies the characters too: 16 + 3 * 4 = 28 bytes.

Compare that with a member declared as char *name;. Then the struct holds only an 8-byte address, and copying the struct copies the address: both copies point at the same characters. This is called a shallow copy, and it matters a great deal once structs own heap memory.

  char name[16] member:                 char *name member:
  ada:  [A d a \0 ...][born]            ada:  [ptr][born]--+
  copy: [C o p y \0..][born]                               +--> "Ada" (shared!)
                                        copy: [ptr][born]--+

Common mistakes

Recap

Going further: in the sandbox terminal, run grep -A12 'struct tm' /usr/include/x86_64-linux-gnu/bits/types/struct_tm.h to see struct tm, the standard library's calendar-time struct, with a comment on each member. Notice tm_year counts from 1900 and tm_mon from 0: documented quirks you can only learn by reading the definition.

Exercises

Exercise 1 · quiz

Predict the output

#include <stdio.h>
#include <string.h>

struct box {
    int size;
    char label[8];
};

int main(void)
{
    struct box a = { 1, "first" };
    struct box b = a;
    b.size = 2;
    strcpy(b.label, "second");
    printf("%d %s / %d %s\n", a.size, a.label, b.size, b.label);
    return 0;
}
Answer
  • 1 first / 2 second

Assigning a struct copies all of its bytes, including arrays stored inside it. b is an independent copy, so changing it leaves a alone. This is a curious corner of C: you cannot assign one array to another directly (b.label = a.label is an error), but wrapping the array in a struct makes it copyable. The catch comes with pointer members, which copy the address, not the data it points to.

Exercise 2 · write code

Functions on a struct

Define

struct rect {
    int width;
    int height;
};

and write three functions:

Keep the struct's member names and order exactly as shown; the test program declares the same struct. Your main can be anything (the starter's demo should print area 12).

File: main.c

Starting point

#include <stdio.h>

/* define struct rect here */

/* define area, scaled and fits_inside here */

int main(void)
{
    /* struct rect r = { 3, 4 };
       printf("area %d\n", area(r)); */
    return 0;
}
Hint 1

The struct definition ends with a semicolon after the closing brace: struct rect { int width; int height; };

Hint 2

In scaled, build a new struct: struct rect out = { r.width * k, r.height * k }; return out;

Hint 3

A comparison like a <= b && c <= d already evaluates to 1 or 0, so fits_inside can return it directly.

Reference solution
#include <stdio.h>

struct rect {
    int width;
    int height;
};

int area(struct rect r)
{
    return r.width * r.height;
}

struct rect scaled(struct rect r, int k)
{
    struct rect out = { r.width * k, r.height * k };
    return out;
}

int fits_inside(struct rect inner, struct rect outer)
{
    return inner.width <= outer.width && inner.height <= outer.height;
}

int main(void)
{
    struct rect r = { 3, 4 };
    printf("area %d\n", area(r));
    return 0;
}

Exercise 3 · write code

Time differences

Read two times of day from standard input in the form HH:MM:SS (24-hour clock, for example 09:15:00 17:45:30) and print how long it is from the first to the second, in the same form, zero-padded:

08:30:30

If the second time is earlier than the first, the interval wraps past midnight: from 23:00:00 to 01:30:00 is 02:30:00. If the input does not contain two times, print bad input and exit with status 1.

Use a struct:

struct hms { int h, m, s; };

with two helper functions, int to_seconds(struct hms t) and struct hms from_seconds(int secs). Read a time with scanf("%d:%d:%d", &t.h, &t.m, &t.s), which returns 3 on success.

File: main.c

Starting point

#include <stdio.h>

struct hms { int h, m, s; };

int to_seconds(struct hms t)
{
    return 0;
}

struct hms from_seconds(int secs)
{
    struct hms t = { 0, 0, 0 };
    return t;
}

int main(void)
{
    struct hms a, b;
    if (scanf("%d:%d:%d", &a.h, &a.m, &a.s) != 3 ||
        scanf("%d:%d:%d", &b.h, &b.m, &b.s) != 3) {
        printf("bad input\n");
        return 1;
    }
    /* compute and print the difference */
    return 0;
}

Example

input

09:15:00 17:45:30

expected output

08:30:30

Example

input

23:00:00 01:30:00

expected output

02:30:00
Hint 1

to_seconds is h * 3600 + m * 60 + s.

Hint 2

from_seconds: hours are secs / 3600, minutes are secs / 60 % 60, seconds are secs % 60.

Hint 3

Subtract the seconds; if the result is negative add 24 * 3600. Print with %02d:%02d:%02d\n.

Reference solution
#include <stdio.h>

struct hms { int h, m, s; };

int to_seconds(struct hms t)
{
    return t.h * 3600 + t.m * 60 + t.s;
}

struct hms from_seconds(int secs)
{
    struct hms t = { secs / 3600, secs / 60 % 60, secs % 60 };
    return t;
}

int main(void)
{
    struct hms a, b;
    if (scanf("%d:%d:%d", &a.h, &a.m, &a.s) != 3 ||
        scanf("%d:%d:%d", &b.h, &b.m, &b.s) != 3) {
        printf("bad input\n");
        return 1;
    }
    int diff = to_seconds(b) - to_seconds(a);
    if (diff < 0)
        diff += 24 * 3600;
    struct hms d = from_seconds(diff);
    printf("%02d:%02d:%02d\n", d.h, d.m, d.s);
    return 0;
}

Exercise 4 · quiz

Comparing structs

With struct point { int x; int y; } a = {1, 2}, b = {1, 2};, what happens with if (a == b) puts("same");?

Answer
  • It does not compile: C has no == for structs

gcc rejects it: invalid operands to binary == (have 'struct point' and 'struct point'). C can copy structs but not compare them, partly because structs may contain padding bytes with arbitrary values, so a byte-by-byte comparison would be unreliable. Compare member by member, usually in a function like int point_equal(struct point a, struct point b). (memcmp on structs is a classic bug for the same padding reason, covered in the next lesson.)