C Part I · Module 10 · Structures, Unions, Enums and User-Defined Types
Defining a struct and Accessing Its Members
The previous chapter used a structure for a whole sorting program without explaining one character of its syntax. Here is the syntax, and there is more to it than the three-line declaration suggested. There are three distinct ways to declare a structure type and they are not interchangeable. The tag you give it is not a type name, which is why the type is always two words. There are two families of initialiser, and the choice between them is not a style preference — one of them silently reassigns your data when somebody reorders the declaration, measured here with no warning from the compiler. And there is one fact about structures that sits oddly against everything Module 6 taught about arrays, which is worth meeting now rather than being surprised by later.
Foundation19 min readCPart IFundamentalsTypes
Part I · Module 10 · Chapter 10.2 · Defining a struct and Accessing Its Members
1. Declaring the Type
struct reading {
int id;
int value;
char grade;
};That declares a structure type whose tag is reading and which has three members. Read the pieces precisely:
structis the keyword. It is not optional and it is not decoration.readingis the tag — a name for this structure, living in its own namespace.- The braces hold member declarations, each written exactly like an ordinary variable declaration.
- The semicolon after the closing brace is required. Forgetting it produces a confusing error about whatever follows.
The declaration creates no objects. It describes a kind of thing; nothing of that kind exists yet.
2. The Tag Is Not a Type Name
This is the first thing that trips up readers arriving from another language. The type is struct reading — two words. reading alone is not a type:
struct reading a; /* correct */
reading b; /* error */error: must use 'struct' tag to refer to type 'reading'The compiler's message is exact: the tag refers to the type, it is not the type. Tags live in a separate namespace from ordinary identifiers, which is why you may legally have a variable called reading and a struct reading in the same program without a collision.
Writing struct twice on every line is tiresome, and C has a mechanism for giving the type a one-word name. That mechanism is typedef, it has real trade-offs, and it is Chapter 10.9's subject — so every type in this chapter and the next few is spelled out in full on purpose.
3. Three Ways to Declare
#include <stdio.h>
/* form 1: a tag only — the type now exists, no objects yet */
struct pair { int a; int b; };
/* form 2: a tag AND objects in one declaration */
struct flags { int on; int off; } global_flags = { 1, 0 };
/* form 3: no tag at all — the objects are the only way to reach the type */
struct { int lo; int hi; } range = { 2, 9 };
int main(void)
{
struct pair p = { 3, 4 };
printf("p.a=%d p.b=%d\n", p.a, p.b);
printf("global_flags.on=%d off=%d\n", global_flags.on, global_flags.off);
printf("range.lo=%d hi=%d\n", range.lo, range.hi);
return 0;
}p.a=3 p.b=4
global_flags.on=1 off=0
range.lo=2 hi=9| Form | When it is right |
|---|---|
| tag only | almost always — the type is reusable, nameable in function signatures |
| tag + objects | rarely worth it; it mixes "what this type is" with "here is one" |
| no tag | only when you genuinely need exactly these objects and nothing else |
The untagged form has a real consequence: without a tag there is no way to name the type again, so you cannot declare another object of it, cannot write a function that takes one, and cannot even mention it in a comment usefully. Treat it as a deliberate narrowing, not a shortcut.
Use form 1 by default. The type's purpose is to be referred to; a type that cannot be named has given that up.
4. A Tag Has Scope
A structure declared inside a block exists only inside that block — the same scoping rules as any other declaration, from Chapter 5.6.
#include <stdio.h>
static void local_type(void)
{
struct helper { int n; }; /* declared here */
struct helper h = { 42 };
printf("inside local_type: h.n=%d\n", h.n);
}
int main(void)
{
local_type();
/* struct helper g; <- not usable here */
return 0;
}inside local_type: h.n=42What happens if you try is worth seeing, because the error message is misleading:
error: variable has incomplete type 'struct helper'Not "unknown type". The reason is that struct helper in a new scope declares a new tag — an incomplete structure type, one whose members have not been given. C lets you declare an incomplete structure type on purpose (it is how a pointer to a not-yet-defined structure works), so the compiler cannot say the name is unknown; it says you tried to create an object of a type whose size it does not know.
Declare a structure type at file scope unless you have a reason to confine it. A type that several functions need, declared inside one of them, is a type the others cannot use.
5. Creating and Initialising Objects
Every form below is legal C17. The outputs are from one program.
#include <stdio.h>
struct reading {
int id;
int value;
char grade;
};
int main(void)
{
struct reading full = { 7, 70, 'a' }; /* every member, in order */
struct reading partial = { 7 }; /* rest zero-initialised */
struct reading zeroed = { 0 }; /* all members zero */
struct reading named = { .grade = 'c', .id = 9 }; /* by name, any order */
printf("full id=%d value=%d grade=%d('%c')\n",
full.id, full.value, full.grade, full.grade);
printf("partial id=%d value=%d grade=%d\n",
partial.id, partial.value, partial.grade);
printf("zeroed id=%d value=%d grade=%d\n",
zeroed.id, zeroed.value, zeroed.grade);
printf("named id=%d value=%d grade=%d('%c')\n",
named.id, named.value, named.grade, named.grade);
struct reading later; /* uninitialised */
later.id = 4;
later.value = 40;
later.grade = 'b';
printf("later id=%d value=%d grade=%c\n",
later.id, later.value, later.grade);
return 0;
}Output:
full id=7 value=70 grade=97('a')
partial id=7 value=0 grade=0
zeroed id=0 value=0 grade=0
named id=9 value=0 grade=99('c')
later id=4 value=40 grade=bFour facts in that output, all guarantees rather than observations.
A partial initialiser zero-initialises the rest. partial gave only id and got value = 0, grade = 0. This is the identical rule Chapter 6.2 §4 established for arrays, applied to structures: list some, and every member you did not list is set as if it were a static object.
{ 0 } therefore zeroes everything. One initialiser, all members zero — and it works no matter how many members the structure has or how they are ordered.
Named members may appear in any order, and any member you leave out is zero-initialised. named set .grade first and .id second, and value came out 0.
struct reading later; with no initialiser is uninitialised, with every consequence Chapter 9.12 established — indeterminate values, undefined to read. The three assignments after it are what make it usable.
A member of type
charprinted with%dshows its integer value —'a'is 97 — which is Chapter 2.6's point that acharis a small integer type. It is printed both ways above so the zero inpartialis unambiguous.
6. Why Named Members, Measured
Positional and named initialisers are usually presented as a readability choice. They are not. Here is the difference, run twice.
Case A — a member is inserted in the middle. Someone adds revision to the structure and does not touch the initialiser, which was written long ago:
struct reading {
int id;
int revision; /* <-- added later */
int value;
char grade;
};
struct reading positional = { 7, 70, 'a' };
struct reading designated = { .id = 7, .value = 70, .grade = 'a' };positional: id=7 revision=70 value=97 grade=0
designated: id=7 revision=0 value=70 grade=97('a')The positional object is wrong in three members at once: 70 landed in revision, the character 'a' (97) landed in value, and grade became zero. The designated object is entirely correct, with the new member zero-initialised — which is almost always what you want for a member the old code never heard of.
This case the compiler does catch, because the number of initialisers no longer matches the number of members:
warning: missing field 'grade' initializer [-Wmissing-field-initializers]Case B — two members are reordered. Now the member count is unchanged:
struct reading { int value; int id; char grade; }; /* value and id swapped */
struct reading positional = { 7, 70, 'a' };
struct reading designated = { .id = 7, .value = 70, .grade = 'a' };positional: id=70 value=7
designated: id=7 value=70$ cc -std=c17 -Wall -Wextra -Wpedantic reorder.c -o reorder
$No warning. None. The positional object has its id and value exchanged, both are valid ints, the count matches, and there is nothing for the compiler to object to.
Note also what a code reviewer sees: the initialiser line did not change. A diff shows only the structure declaration moving two members around — a change that looks cosmetic and is not.
7. The Fact That Sits Oddly With Module 6
Chapter 6.1 established, firmly, that an array cannot be assigned. other = plain; is not valid C. Now consider a structure with an array member:
#include <stdio.h>
struct frame { int tag; int bytes[4]; };
int main(void)
{
struct frame a = { .tag = 1, .bytes = { 10, 20, 30, 40 } };
struct frame b = a; /* one assignment */
b.bytes[0] = 99;
printf("a.bytes: %d %d %d %d\n", a.bytes[0], a.bytes[1], a.bytes[2], a.bytes[3]);
printf("b.bytes: %d %d %d %d\n", b.bytes[0], b.bytes[1], b.bytes[2], b.bytes[3]);
printf("sizeof(struct frame) = %zu\n", sizeof(struct frame));
int plain[4] = { 1, 2, 3, 4 };
int other[4];
/* other = plain; <- not valid C: an array is not assignable */
for (int i = 0; i < 4; i++) { other[i] = plain[i]; }
printf("a bare array still needs the loop: %d %d %d %d\n",
other[0], other[1], other[2], other[3]);
return 0;
}Output:
a.bytes: 10 20 30 40
b.bytes: 99 20 30 40
sizeof(struct frame) = 20a bare array still needs the loop: 1 2 3 4b = a; copied the array. Changing b.bytes[0] left a.bytes untouched, so it is a genuine copy and not a shared view — while the bare int[4] two lines down still needs its loop.
This is not an inconsistency. The structure is one object with a type that can be assigned; the array is not. Chapter 6.1's restriction is a fact about arrays, and wrapping an array inside a structure gives you an assignable thing whose contents happen to include an array. It is the standard way to get a copyable fixed-size buffer in C, and it is why a function can return a structure containing an array when it cannot return an array.
8. Reaching Members, Including Through Nesting
The member access operator is ., and its left operand is an object of structure type:
full.id = 8;
int v = full.value;A member may itself be a structure, and then access is a chain:
#include <stdio.h>
struct point { int x; int y; };
struct window {
struct point origin;
struct point size;
char title[8];
};
int main(void)
{
struct window w = {
.origin = { .x = 10, .y = 20 },
.size = { .x = 80, .y = 24 },
.title = "main",
};
printf("origin %d,%d size %d,%d title \"%s\"\n",
w.origin.x, w.origin.y, w.size.x, w.size.y, w.title);
w.origin.y = 21; /* reach in, two steps */
printf("after w.origin.y = 21 : %d,%d\n", w.origin.x, w.origin.y);
struct point corner = w.origin; /* copy out a whole nested member */
corner.x = 0;
printf("corner %d,%d w.origin %d,%d (independent)\n",
corner.x, corner.y, w.origin.x, w.origin.y);
printf("sizeof(struct point)=%zu sizeof(struct window)=%zu\n",
sizeof(struct point), sizeof(struct window));
return 0;
}Output:
origin 10,20 size 80,24 title "main"
after w.origin.y = 21 : 10,21
corner 0,21 w.origin 10,21 (independent)
sizeof(struct point)=8 sizeof(struct window)=24Four things to take from that.
w.origin.x is two applications of one operator, left to right: w.origin is a struct point, and .x selects from it. There is no depth limit.
A nested member can be assigned as a whole. struct point corner = w.origin; copies both of its members, and the last output line confirms the copy is independent.
Nested initialisers nest too. .origin = { .x = 10, .y = 20 } is a designated initialiser containing a designated initialiser, which is how a nested record stays readable.
A char array member initialised from a string literal works as Chapter 8.2 described — "main" is four characters plus the terminator, and title has room for eight.
One restriction: a structure cannot contain an object of its own type, because its size would have to be known before it was known. It may contain a pointer to its own type, which is how C builds structures that refer to each other, and reaching a member through such a pointer needs an operator this chapter has not covered. That is Chapter 10.3.
9. Two Things You Cannot Do
Compare with ==.
struct point a = { 1, 2 }, b = { 1, 2 };
if (a == b) { } /* error */error: invalid operands to binary expression ('struct point' and 'struct point')There is no structure equality operator in C. To compare, compare the members:
if (a.x == b.x && a.y == b.y) { }The reason is worth knowing rather than memorising: a structure may contain padding bytes whose contents are not specified, so a byte-by-byte comparison of two structures with equal members could legitimately report them different. Chapter 10.5 is where that becomes concrete; for now, treat "compare member by member" as the rule, including when you are tempted to compare the bytes directly.
Omit the semicolon. } without ; after a structure declaration produces an error naming whatever came next, which can be several lines away and very confusing. It is the single most common structure typo.
10. What You Should Be Able to Say Now
struct tag { members };declares a type and creates no objects. The trailing semicolon is required.- The tag is not a type name — the type is
struct tag, two words: "must use 'struct' tag to refer to type". - Three declaration forms: tag only (the default), tag plus objects, and untagged — the last gives up the ability to name the type again.
- A tag has scope. Declared in a block, it is unusable outside, and the error is "variable has incomplete type" because a new incomplete tag is declared instead.
- A partial initialiser zero-initialises the rest, exactly as for arrays (6.2), so
{ 0 }zeroes everything. - Named members may come in any order; omitted members are zeroed.
- Measured: an inserted member silently shifted a positional initialiser — the compiler did warn, on the count. A reordered pair shifted it with no warning at all, and the initialiser line did not even appear in the diff.
-Wmissing-field-initializersalso fires on a legal partial initialiser;{ 0 }is exempt,{ 1 }is not. Write{ .member = v }and the question disappears.- An array cannot be assigned, but a struct containing one can — verified, a real copy.
.reaches a member; nesting chains it (w.origin.x); a struct cannot contain itself, only a pointer to itself.- No
==on structures — compare members, because padding bytes are unspecified.
11. Exercises
Exercise 1 — write the declarations. Declare a structure type for each, at file scope, using form 1.
- a date with a year, month and day
- a named counter with a label of up to 16 characters and a count
- a rectangle made of two corner points, reusing a
struct point
Exercise 2 — predict the members. For each, give every member's value.
struct s { int a; int b; char c; };
struct s p = { 1, 2, 'x' };
struct s q = { 1 };
struct s r = { 0 };
struct s t = { .c = 'y' };
struct s u = { .b = 5, .a = 4 };Exercise 3 — find the silent break. Someone swaps width and height in this declaration and changes nothing else. Say what each object holds afterwards, which one is still correct, and whether the compiler warns.
struct box { int width; int height; char tag; };
struct box a = { 80, 24, 'a' };
struct box b = { .width = 80, .height = 24, .tag = 'b' };Exercise 4 — silence the warning properly. This draws -Wmissing-field-initializers. Give two rewrites that do not, and say which you would ship.
struct reading r = { 7 };Exercise 5 — the array question. Explain in two sentences why b = a; works in §7 but other = plain; does not. Then say what this gives you that Module 6 could not: name one thing a function can now do with a fixed-size buffer.
Exercise 6 — reach the member. Given §8's struct window w, write the expression for each.
- the origin's y
- the title's first character
- a copy of the size as its own object
- the width of a window whose
sizeyou want to leave unchanged
Exercise 7 — compare two. Write a function that reports whether two struct point objects are equal. Then say why comparing their bytes directly is not a valid shortcut, naming the chapter that explains it.
Exercise 8 — the scope trap. A program declares struct helper { int n; }; inside main and then a second function tries to take a struct helper parameter. Say what the compiler reports, why the message mentions incompleteness rather than an unknown name, and give the one-line fix.
12. Summary
struct tag { ... };declares a type, creates nothing, and needs the semicolon.- The type is
struct tag— the tag alone is not a type name. - Prefer the tag-only form; an untagged struct cannot be named again.
- Tags have scope; declare types at file scope unless you mean to confine them.
- Partial initialisers zero the rest (so
{ 0 }zeroes all); named members work in any order. - Measured: an insert warns on the count; a reorder is completely silent and the initialiser line does not change. Use named members.
{ 0 }is exempt from-Wmissing-field-initializers;{ 1 }is not.- An array cannot be assigned; a struct containing one can — a real copy, verified.
.selects a member, nesting chains it; a struct may hold a pointer to its own type but not an object of it.- No
==— compare members; padding bytes are unspecified.
Every structure in this chapter was reached by name, as an object sitting in the function that declared it, and that is the one situation real C code is least often in. Structures are large enough that programs pass their addresses around instead of their contents, which means most of the structure code you will ever read reaches members through a pointer rather than through a variable. Doing that with the tools you already have is possible and genuinely unpleasant — it needs a dereference and a set of parentheses in an order that is easy to get wrong, and getting it wrong produces something that sometimes compiles. C provides a second member-access operator for exactly this case, and the next chapter is about it, about why the parenthesised form it replaces is a trap worth seeing once, and about the one structure shape that cannot be built without it.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
