C Part I · Module 10 · Structures, Unions, Enums and User-Defined Types
Why C Needs User-Defined Types
Nine modules in, you can express a surprising amount with nothing but scalars, arrays and pointers — and that is exactly why this chapter has to argue rather than announce. You will be told that structures let you group related data, which is true and persuades nobody, because grouping sounds like tidiness and tidiness is optional. The real case is sharper and can be measured. This chapter takes one small table held in three parallel arrays, deletes a single line from the function that sorts it, and shows the result: a table that is perfectly sorted and in which every row is a lie, with not one warning from the compiler. Then it shows the same program written with a type, where that line cannot be deleted because it does not exist.
Foundation16 min readCPart IFundamentalsTypes
Part I · Module 10 · Chapter 10.1 · Why C Needs User-Defined Types
1. The Shape of the Problem
Real programs are full of data that comes in groups. A sensor reading has an identifier, a measured value and a quality grade. A point has an x and a y. A parsed field has an offset, a width and a name. In each case the group is the thing the program actually deals with — you never want half a reading.
Everything C has given you so far handles one of those at a time:
| What you have | What it holds |
|---|---|
| a variable (Module 2) | one value of one type |
| an array (Module 6) | many values, all of the same type |
| a pointer (Module 7) | the address of one object, or of an array's first element |
Nothing in that list holds a few values of different types, as one thing. So the conventional answer is to take the group apart and keep the pieces in step yourself.
2. Parallel Arrays
Here is the usual shape. One table of four readings, held as three arrays:
static int id[4] = { 7, 3, 9, 1 };
static int value[4] = { 70, 30, 90, 10 };
static char grade[4] = {'a','b','c','d'};id[2], value[2] and grade[2] are one reading. That sentence is the entire specification of how this data works, and it exists in exactly one place: your head.
This arrangement works. It is not a crime, and for a table read once and never rearranged it is perfectly reasonable. The trouble starts the moment anything moves.
3. The Measurement
Sort the table by id. Every swap has to move all three arrays:
#include <stdio.h>
static int id[4] = { 7, 3, 9, 1 };
static int value[4] = { 70, 30, 90, 10 };
static char grade[4] = {'a','b','c','d'};
static void show(const char *tag)
{
printf("%-22s", tag);
for (int i = 0; i < 4; i++) {
printf(" %d/%d/%c", id[i], value[i], grade[i]);
}
printf("\n");
}
static void sort_by_id(void)
{
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3 - i; j++) {
if (id[j] > id[j + 1]) {
int t1 = id[j]; id[j] = id[j + 1]; id[j + 1] = t1;
int t2 = value[j]; value[j] = value[j + 1]; value[j + 1] = t2;
char t3 = grade[j]; grade[j] = grade[j + 1]; grade[j + 1] = t3;
}
}
}
}
int main(void)
{
show("before:");
sort_by_id();
show("after correct sort:");
return 0;
}Output:
before: 7/70/a 3/30/b 9/90/c 1/10/d
after correct sort: 1/10/d 3/30/b 7/70/a 9/90/cCorrect. The grades travelled with their rows: 1 still has d, 7 still has a.
Now delete one line — the swap of grade — and change nothing else:
before: 7/70/a 3/30/b 9/90/c 1/10/d
after broken sort: 1/10/a 3/30/b 7/70/c 9/90/dRead that output carefully, because it is the whole argument.
The table is sorted. The ids ascend: 1, 3, 7, 9.
The values are right. Every value still sits with its own id — 1 with 10, 9 with 90.
Every grade is wrong. The grades never moved, so they are still in their original positions, attached to whichever row happens to be there now. Reading 1's grade was d; the program now reports a.
And the compiler said nothing. Compiled at the curriculum's standard warning level with no output at all:
$ cc -std=c17 -Wall -Wextra -Wpedantic broken.c -o broken
$There was nothing for it to say. Three arrays were declared, three arrays were used, every index was in bounds, every type matched. The program is a completely valid C program that computes the wrong thing, and it does so in the most dangerous way available: by producing output that passes inspection.
The bug is not in the code. It is in what the code was unable to say. Nowhere does the program state that
id,valueandgradeare three views of one table — so no tool can check that an operation preserved the relationship, because no tool has been told the relationship exists.
4. What a Type Would Have Changed
struct reading {
int id;
int value;
char grade;
};That declaration does one thing that matters above all others: it writes the relationship down. struct reading now names a kind of thing that has an id, a value and a grade — and from here on, the compiler knows it too.
(The syntax above is deliberately not explained in this chapter. Chapter 10.2 covers declaring structures, creating objects of them, and initialising them; here it appears only so the comparison is concrete.)
5. The Same Sort, With Nothing to Forget
#include <stdio.h>
struct reading {
int id;
int value;
char grade;
};
static struct reading table[4] = {
{ 7, 70, 'a' },
{ 3, 30, 'b' },
{ 9, 90, 'c' },
{ 1, 10, 'd' },
};
static void show(const char *tag)
{
printf("%-22s", tag);
for (int i = 0; i < 4; i++) {
printf(" %d/%d/%c", table[i].id, table[i].value, table[i].grade);
}
printf("\n");
}
static void sort_by_id(void)
{
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3 - i; j++) {
if (table[j].id > table[j + 1].id) {
struct reading t = table[j]; /* one assignment */
table[j] = table[j + 1];
table[j + 1] = t;
}
}
}
}
int main(void)
{
show("before:");
sort_by_id();
show("after sort:");
printf("sizeof(struct reading) = %zu\n", sizeof(struct reading));
return 0;
}Output:
before: 7/70/a 3/30/b 9/90/c 1/10/d
after sort: 1/10/d 3/30/b 7/70/a 9/90/c
sizeof(struct reading) = 12The same correct answer — and look at what the swap became:
struct reading t = table[j];
table[j] = table[j + 1];
table[j + 1] = t;Three lines, no temporaries per member, and nothing to leave out. There is no "the grade line" to delete. Adding a fourth member to struct reading tomorrow does not touch this function at all; with parallel arrays it would need a fourth swap, in every place anything is rearranged.
This is the difference between a convenience and a correctness tool. Parallel arrays make the §3 bug possible to write; a struct makes it impossible to write. Discipline is what you need when the language cannot help; a type is the language helping.
Note the last line of the output, and then set it aside: sizeof(struct reading) is 12, while the members add up to 4 + 4 + 1 = 9. That is not a mistake and it is not padding you did anything wrong to deserve. Chapter 10.5 is entirely about where the other three bytes went.
6. A Struct Is Also a Value
The grouping argument is the half everyone makes. Here is the half that gets left out, and it is at least as important: an object of structure type is a single value, and C's existing rules for values apply to it immediately.
#include <stdio.h>
struct reading { int id; int value; char grade; };
/* A function returns exactly one value — Chapter 5.4.
With a struct, one value can be a whole row. */
static struct reading best_of(struct reading a, struct reading b)
{
return (a.value >= b.value) ? a : b;
}
int main(void)
{
struct reading x = { 7, 70, 'a' };
struct reading y = { 9, 90, 'c' };
struct reading w = best_of(x, y);
printf("winner: id=%d value=%d grade=%c\n", w.id, w.value, w.grade);
struct reading copy = w; /* copies every member */
copy.value = 0;
printf("after copy.value = 0 : w.value=%d copy.value=%d\n",
w.value, copy.value);
return 0;
}Output:
winner: id=9 value=90 grade=c
after copy.value = 0 : w.value=90 copy.value=0Three consequences, each unavailable to parallel arrays at any price.
Assignment copies the whole object. copy = w; duplicates all three members. The second output line proves it is a real copy and not a shared view — changing copy.value left w.value at 90. Compare Chapter 6.1's rule that an array cannot be assigned at all; a struct can, even one containing an array.
A function can return one. Chapter 5.4 established that a C function returns exactly one value, and Chapter 7.5 gave you out-parameters as the workaround when you needed two things back. A struct removes the need for the workaround: best_of returns a whole reading, by value, in the most ordinary way possible.
An array of them is a table. struct reading table[4] is a single array whose element type is the record — which is why §5's sort only had to think about one array. That is Chapter 10.6's subject.
7. What a Struct Is Not
If you have met another language first, you are likely to be importing expectations that do not apply, so it is worth saying plainly what C does not give you here.
| You may expect | C's answer |
|---|---|
| member functions / methods | no — a struct holds data; functions stay separate and take it as a parameter |
private members | no — every member of a visible struct is accessible |
| inheritance | no — no base types, no overriding |
| automatic construction or destruction | no — Chapter 9.8's ownership rules do not become automatic |
comparison with == | no — two structs cannot be compared with ==; compare members |
8. When Parallel Arrays Are Still the Right Answer
The honest position is not "always use a struct". There are real cases where keeping the columns apart is better, and knowing them stops this chapter from becoming a slogan.
When nothing is ever rearranged or moved as a unit. The §3 bug needs something that reorders. A table that is filled once, read, and discarded cannot desync.
When one column is processed alone, repeatedly, in bulk. Summing a million values touches far less memory if the values are contiguous than if they are spread 12 bytes apart. That is a performance argument, it is measurable, and it is real — but it is also the only argument on this side, and it is the wrong default for code whose shape is still changing.
When the number of columns is not fixed. If the set of attributes is decided at run time, a struct cannot describe it; some arrangement of arrays can.
Default to the type. Depart from it for a reason you can state — and "it felt faster" is not one of them until something has been measured.
9. What You Should Be Able to Say Now
- A variable holds one value, an array holds many of one type, and neither holds a few values of different types as one thing.
- Parallel arrays leave the relationship unwritten, so no tool can check that an operation preserved it.
- Measured: deleting one swap line produced a perfectly sorted table with every grade attached to the wrong row, and zero warnings at
-Wall -Wextra -Wpedantic. - The failure's danger is that the output passes inspection.
- A
structdeclaration writes the relationship down, which is what makes it a correctness tool rather than a convenience. - With a struct, the swap is one assignment — there is no line to forget, and adding a member does not touch the code that moves records.
- A struct object is a value: assignment copies every member (verified), a function can return one, and an array of them is a table.
- Returning a struct removes the need for Chapter 7.5's out-parameter in many cases, because Chapter 5.4's one return value can now be a whole record.
- C gives no methods, no
private, no inheritance, no automatic construction, and no==on structs. sizeof(struct reading)was 12 where the members sum to 9 — a fact to carry to Chapter 10.5.
10. Exercises
Exercise 1 — name the record. For each description, list the members a struct would need and give each one a type from Module 2.
- a point on a grid
- a parsed command-line option with a name, a short letter and a flag for whether it takes a value
- a measurement with a timestamp, a reading and a unit code
Exercise 2 — find the desync. This function is supposed to delete element k from a three-array table by shifting everything down. It has one bug of the §3 kind. Find it, and say what the output would look like to someone who did not know.
for (int i = k; i < n - 1; i++) {
id[i] = id[i + 1];
value[i] = value[i + 1];
grade[i] = grade[i + 1];
}
n = n - 1;Then say what changes about your answer if n = n - 1; is omitted instead.
Exercise 3 — count the edits. A table has five parallel arrays and is sorted in two functions, filtered in one, and reversed in one. A sixth column is added. How many places must change? Now answer the same question for one array of structs.
Exercise 4 — the two halves. §5 and §6 make two different arguments for structs. State each in one sentence, and say which one applies to a program that never sorts anything.
Exercise 5 — predict the output. Do not compile first.
struct point { int x; int y; };
struct point a = { 1, 2 };
struct point b = a;
b.x = 99;
printf("%d %d %d %d\n", a.x, a.y, b.x, b.y);Then say which line would fail to compile if it were if (a == b).
Exercise 6 — the honest exception. §8 gives one performance argument for parallel arrays. Explain the mechanism in your own words using Chapter 2.8's memory model, and say what you would have to measure before acting on it.
Exercise 7 — translate the expectation. A colleague from another language writes reading.print(); and reading.value is private. Rewrite both in C and say what is lost and what is gained.
Exercise 8 — the missing three bytes. struct reading { int id; int value; char grade; }; has members totalling 9 bytes and sizeof 12. Write down your best guess at where the extra bytes are and why, then keep it to check against Chapter 10.5.
11. Summary
- A variable is one value; an array is many of one type. Neither is a few values of different types as one thing.
- Parallel arrays leave the relationship unwritten, so nothing can check it.
- Measured: one deleted swap line gave a sorted table with every grade wrong and no warnings.
- The danger is plausible output, not a crash.
- A
structwrites the relationship down — the swap becomes one assignment with nothing to omit. - A struct is also a value: it copies on assignment (verified), a function can return one, and an array of them is a table.
- C gives no methods, no access control, no inheritance, no
==— the type says what the data is, functions say what you can do with it. - Default to the type; depart only for a reason you can state and measure.
sizeofwas 12 for 9 bytes of members — Chapter 10.5's subject.
Everything in this chapter used a structure without explaining a single piece of its syntax — the declaration, the braces in the initialiser, the dot between a variable and a member. That was deliberate, because the argument for having types had to come before the mechanics of writing them. The next chapter is the mechanics, and there is more in them than the three lines above suggest: there are several distinct ways to declare a structure type and they are not interchangeable, there is a form of initialiser that names each member instead of relying on order — which matters a great deal the first time someone inserts a member in the middle — and structures nest, so a member can itself be a record and reaching into it is a chain rather than a single step.
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.
