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.
- 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 when | Pass by pointer when |
|---|---|
| The structure is small — two or three scalars | The structure is large |
| The function must not modify it | The function must modify it |
| You want a private copy to alter freely | It 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
| Operation | Works? | Instead |
|---|---|---|
Assignment a = b | Yes | — |
| Passing and returning by value | Yes | — |
Comparison a == b | No | Compare members, or memcmp — with care |
| Arithmetic | No | Write a function |
| Printing directly | No | Write 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
./structsThree 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 = 5Every 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
| Mistake | What happens | Fix |
|---|---|---|
Omitting the semicolon after } | Confusing errors on the next declaration | A struct definition is a declaration; it needs one. |
Using . on a pointer | Compile error, or worse with a cast | Dot for objects, arrow for pointers. |
*p.x meaning (*p).x | Parses as *(p.x) | Write p->x. |
Comparing structures with == | Compile error | Compare members in a function. |
Comparing structures with memcmp | Padding makes equal structures differ | Compare members explicitly. |
| Passing a large structure by value in a loop | Unnecessary copying | const T *. |
| Positional initializers on a wide struct | Reordering members silently breaks it | Designated initializers. |
| Forgetting the tag in a self-referential struct | unknown type name | struct 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.