Procedural Programming with C · Intermediate · Week 21

Structures

An array groups values of one type. A structure groups values of different types under one name — which is how C stops passing six loose parameters around and starts modelling the thing those parameters describe.

By the end of this week you can
  • Declare and initialize a structure, including with designated initializers.
  • Choose between . and -> without thinking about it.
  • Decide whether to pass a structure by value or by pointer, with a reason.
  • Build and traverse an array of structures.
  • Declare a self-referential type and explain why it must contain a pointer.

1Declaring a structure

struct Point {
    int x;
    int y;
};                       /* the semicolon is required */

struct Point origin;     /* every use needs the keyword 'struct' */

Having to write struct Point everywhere is tedious, so almost all real code introduces an alias:

typedef struct {
    int x;
    int y;
} Point;

Point origin;            /* now just the name */

The convention in this course, and in most modern C, is typedef with a capitalized type name. Week 22 covers typedef in its own right.

Initializing

Point a = { 3, 4 };                 /* positional */
Point b = { .x = 3, .y = 4 };       /* designated — C99 */
Point c = { .y = 4 };               /* x is zero-filled */
Point d = { 0 };                    /* every member zeroed */
Point e = a;                        /* structures assign by copy */

Prefer designated initializers. They are readable without counting, they survive a reordering of the members, and they make it obvious which fields you deliberately left at zero. Positional initialization of a six-member structure is a bug waiting for a maintenance change.

Note the last line: unlike arrays, structures can be assigned and copied wholesale. e = a copies every member.

2Access: . and ->

Point p = { .x = 1, .y = 2 };
Point *q = &p;

p.x          /* a structure: use dot   */
q->x         /* a pointer:   use arrow */
(*q).x       /* identical to q->x, and nobody writes it */

-> exists purely because (*q).x is unpleasant, and because the parentheses are mandatory — *q.x parses as *(q.x), since . binds tighter than *. That is a real source of confusion in old code; the arrow removes it.

The rule is mechanical: dot for an object, arrow for a pointer to one. Since most structures are passed around by pointer, you will write -> far more often.

3By value or by pointer?

Structures obey week 11's rule exactly: passing one to a function copies every member.

void move_broken(Point p)      { p.x += 10; }   /* modifies the copy  */
void move(Point *p)            { p->x += 10; }  /* modifies the caller's */
int  area(const Rect *r)       { return r->w * r->h; }  /* reads only */
Pass by value whenPass by pointer when
The structure is small — two or three scalarsThe structure is large
The function must not modify itThe function must modify it
You want a private copy to alter freelyIt contains allocations you must not duplicate

The default in practice is a pointer, with const when the function only reads. It avoids copying, it is uniform, and the const makes the contract explicit and compiler-checked, exactly as week 16 established.

Returning a structure by value is fine and common — Point make_point(int x, int y) — because the copy happens once and the caller owns the result with no lifetime question at all.

4Nesting and arrays

typedef struct {
    Point top_left;
    Point bottom_right;
} Rect;

Rect r = {
    .top_left     = { .x = 0, .y = 0 },
    .bottom_right = { .x = 4, .y = 3 }
};

int width = r.bottom_right.x - r.top_left.x;

Nested members chain with ., or with -> at the first step if you start from a pointer: rp->top_left.x.

Arrays of structures

typedef struct {
    char   name[32];
    int    score;
    double average;
} Student;

Student class[3] = {
    { .name = "Ada",    .score = 95 },
    { .name = "Dennis", .score = 88 },
    { .name = "Ken",    .score = 91 }
};

for (size_t i = 0; i < 3; i++) {
    printf("%-10s %3d\n", class[i].name, class[i].score);
}

Note that name is an array inside the structure, so the text is stored in the structure itself rather than pointed at. That makes the structure self-contained and copyable — Student b = a; duplicates the name too. The alternative, char *name, stores only a pointer, which is smaller and more flexible but raises the ownership question from week 19 for every copy.

5Self-referential structures

A structure cannot contain itself — the size would be infinite. It can contain a pointer to itself, which is the basis of every linked structure in week 32.

typedef struct Node {        /* the tag is required here */
    int          value;
    struct Node *next;       /* fine: a pointer has a known size */
} Node;

The struct tag Node after the struct keyword is necessary because the typedef name does not exist yet inside its own definition. This is one of the few places where the tag cannot be omitted.

Node third  = { .value = 3, .next = NULL };
Node second = { .value = 2, .next = &third };
Node first  = { .value = 1, .next = &second };

for (Node *n = &first; n != NULL; n = n->next) {
    printf("%d ", n->value);
}

That loop is the shape of every list traversal you will write. Week 32 replaces the stack-allocated nodes with heap allocations and adds insertion and removal.

6What a structure does not support

OperationWorks?Instead
Assignment a = bYes
Passing and returning by valueYes
Comparison a == bNoCompare members, or memcmp — with care
ArithmeticNoWrite a function
Printing directlyNoWrite a print function

Do not compare structures with memcmp. It compares every byte, including the padding the compiler inserts between members for alignment — and padding bytes hold whatever was in that memory, even in two structures whose members are all equal. Two logically identical structures can therefore compare unequal. Compare the members you care about, one by one. Week 22 explains where the padding comes from.

7Worked example: records, by value and by pointer

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

#define NAME_LEN 32

typedef struct {
    int x;
    int y;
} Point;

typedef struct {
    Point top_left;
    Point bottom_right;
} Rect;

typedef struct {
    char   name[NAME_LEN];
    int    score;
    int    attempts;
} Student;

/* Small and read-only: by value is fine and reads well. */
static int manhattan(Point a, Point b)
{
    int dx = a.x - b.x;
    int dy = a.y - b.y;
    return (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy);
}

/* Read-only: const pointer. No copy, contract enforced. */
static int rect_area(const Rect *r)
{
    return (r->bottom_right.x - r->top_left.x)
         * (r->bottom_right.y - r->top_left.y);
}

/* Modifies: non-const pointer. */
static void rect_translate(Rect *r, int dx, int dy)
{
    r->top_left.x     += dx;  r->top_left.y     += dy;
    r->bottom_right.x += dx;  r->bottom_right.y += dy;
}

/* Returning by value: the caller owns the result, no lifetime question. */
static Point point_make(int x, int y)
{
    return (Point){ .x = x, .y = y };    /* compound literal — week 38 */
}

/* Structures have no ==; compare what actually matters. */
static bool point_equal(Point a, Point b)
{
    return a.x == b.x && a.y == b.y;
}

static void student_print(const Student *s)
{
    printf("  %-10s score %3d  attempts %d\n",
           s->name, s->score, s->attempts);
}

/* An array of structures, searched and aggregated. */
static const Student *find_student(const Student *list, size_t n,
                                   const char *name)
{
    for (size_t i = 0; i < n; i++) {
        if (strcmp(list[i].name, name) == 0) {
            return &list[i];             /* points into the caller's array */
        }
    }
    return NULL;
}

static bool class_average(const Student *list, size_t n, double *out)
{
    if (n == 0) {
        return false;
    }
    long total = 0;
    for (size_t i = 0; i < n; i++) {
        total += list[i].score;
    }
    *out = (double)total / (double)n;
    return true;
}

/* A self-referential type: the basis of week 32. */
typedef struct Node {
    int          value;
    struct Node *next;
} Node;

int main(void)
{
    puts("== points ==");
    Point a = { .x = 1, .y = 2 };
    Point b = point_make(4, 6);
    printf("  a = (%d,%d), b = (%d,%d), manhattan = %d\n",
           a.x, a.y, b.x, b.y, manhattan(a, b));
    printf("  a == b ? %s   (compared member by member)\n",
           point_equal(a, b) ? "yes" : "no");

    Point copy = a;
    copy.x = 99;
    printf("  after copying a and changing the copy: a.x = %d\n", a.x);
    puts("  structures assign by value — unlike arrays");

    puts("\n== nesting, and const versus mutable pointers ==");
    Rect r = {
        .top_left     = { .x = 0, .y = 0 },
        .bottom_right = { .x = 4, .y = 3 }
    };
    printf("  area = %d\n", rect_area(&r));
    rect_translate(&r, 10, 10);
    printf("  after translating: top_left = (%d,%d), area = %d\n",
           r.top_left.x, r.top_left.y, rect_area(&r));
    puts("  the area is unchanged, as it must be");

    puts("\n== an array of structures ==");
    Student class[] = {
        { .name = "Ada",    .score = 95, .attempts = 1 },
        { .name = "Dennis", .score = 88, .attempts = 2 },
        { .name = "Ken",    .score = 91, .attempts = 1 },
        { .name = "Grace",  .score = 78, .attempts = 3 }
    };
    const size_t n = sizeof class / sizeof class[0];

    for (size_t i = 0; i < n; i++) {
        student_print(&class[i]);
    }

    double average;
    if (class_average(class, n, &average)) {
        printf("  average of %zu students: %.2f\n", n, average);
    }

    const Student *found = find_student(class, n, "Ken");
    printf("  lookup \"Ken\"  : %s\n",
           found ? "found" : "not found");
    if (found != NULL) {
        student_print(found);
    }
    printf("  lookup \"Linus\": %s\n",
           find_student(class, n, "Linus") ? "found" : "not found");

    puts("\n== sizes and copying cost ==");
    printf("  sizeof(Point)   = %2zu\n", sizeof(Point));
    printf("  sizeof(Rect)    = %2zu\n", sizeof(Rect));
    printf("  sizeof(Student) = %2zu   (%d-byte name plus two ints)\n",
           sizeof(Student), NAME_LEN);
    printf("  sizeof(Student *) = %zu — which is why large structures\n",
           sizeof(Student *));
    puts("  are passed by pointer");

    puts("\n== self-referential ==");
    Node third  = { .value = 3, .next = NULL };
    Node second = { .value = 2, .next = &third };
    Node first  = { .value = 1, .next = &second };
    printf("  list: ");
    for (Node *p = &first; p != NULL; p = p->next) {
        printf("%d ", p->value);
    }
    puts("\n  a struct cannot contain itself, but it can point to itself");

    return EXIT_SUCCESS;
}
gcc -std=c17 -Wall -Wextra -g -fsanitize=address -o structs structs.c
./structs

Three things the output shows

Structures copy on assignment. Changing copy.x leaves a.x alone. Arrays do not behave this way — int b[5] = a; does not even compile — which is one of the few places structures are more convenient than arrays.

const is doing real work. Add r->top_left.x = 0; inside rect_area and the build fails with assignment of member in read-only object. The signature promised not to modify, and the promise is checked.

Size drives the calling convention. Student is 40 bytes; a pointer is 8. Passing forty students by value copies 1.6 KB; passing pointers copies 320 bytes. For Point at 8 bytes the difference is nil, which is why by-value is reasonable there.

The memcmp trap, demonstrated

Add this and run it:

typedef struct {
    char  flag;      /* 1 byte, then 3 bytes of padding */
    int   value;     /* 4 bytes */
} Padded;

Padded p1, p2;
memset(&p1, 0x00, sizeof p1);        /* clear all bytes, padding included */
memset(&p2, 0xFF, sizeof p2);        /* fill all bytes                    */
p1.flag = 'A'; p1.value = 7;
p2.flag = 'A'; p2.value = 7;         /* logically identical now */

printf("members equal: %s\n",
       (p1.flag == p2.flag && p1.value == p2.value) ? "yes" : "no");
printf("memcmp equal : %s\n",
       memcmp(&p1, &p2, sizeof p1) == 0 ? "yes" : "no");
printf("sizeof(Padded) = %zu, but 1 + 4 = 5\n", sizeof(Padded));
members equal: yes
memcmp equal : no
sizeof(Padded) = 8, but 1 + 4 = 5

Every member matches and memcmp still reports a difference, because the three padding bytes after flag differ. Week 22 explains where those three bytes come from and how to inspect them.

8Common mistakes

MistakeWhat happensFix
Omitting the semicolon after }Confusing errors on the next declarationA struct definition is a declaration; it needs one.
Using . on a pointerCompile error, or worse with a castDot for objects, arrow for pointers.
*p.x meaning (*p).xParses as *(p.x)Write p->x.
Comparing structures with ==Compile errorCompare members in a function.
Comparing structures with memcmpPadding makes equal structures differCompare members explicitly.
Passing a large structure by value in a loopUnnecessary copyingconst T *.
Positional initializers on a wide structReordering members silently breaks itDesignated initializers.
Forgetting the tag in a self-referential structunknown type namestruct Node *next; inside struct Node.

9Check yourself

Why can you assign one structure to another but not one array to another?

Because a structure is a single object of its type, and C defines assignment for it as a member-by-member copy. An array name decays to a pointer in an assignment context, so a = b would be an attempt to assign to a non-modifiable address. Copy arrays with memcpy or a loop.

When should a structure be passed by value?

When it is small — two or three scalars — and the function does not need to modify the caller's copy. Beyond that, pass a pointer, with const if the function only reads. The pointer avoids copying and makes the read-only contract explicit and compiler-checked.

Why is memcmp unreliable for comparing structures?

Because it compares padding bytes as well as members. The compiler inserts padding for alignment, and its contents are unspecified — two structures with identical member values can differ in those bytes and compare unequal. Compare the members you care about individually.

Why must a self-referential structure contain a pointer rather than an instance?

Because a structure containing itself would have infinite size — the compiler could not compute sizeof. A pointer has a fixed, known size regardless of what it points to, so the type is well defined. This is exactly why every linked data structure in C is built from pointers.

Why prefer designated initializers over positional ones?

They state which member each value belongs to, so the code is readable without counting and survives a reordering of the declaration. They also default every unmentioned member to zero, making partial initialization explicit rather than accidental. On a structure with more than about three members, positional initialization is a maintenance hazard.

10Where this leads

Week 22 completes the picture of user-defined types: enumerations for a closed set of named values, unions for storing one of several things in the same space, typedef in its own right, and the padding rules that explained the memcmp result above. Together they give you everything needed to design the abstract data type of week 33.