Structs, files and undefined behaviour
Group related data, read and write files without leaking descriptors, and recognise the undefined behaviour the compiler is allowed to punish.
Structs
#include <string.h>
struct point { double x, y; };
struct record {
int id;
char name[32];
struct point pos;
double scores[4];
};
struct record r = { .id = 7, .name = "Ada", .pos = { .x = 1.5, .y = -2.0 } };
struct record *pr = &r;
pr->id == (*pr).id /* the arrow is dereference plus member access */
sizeof r /* includes padding inserted for alignment *//* returning a struct by value copies it, so the result is safe to use */
struct point midpoint(struct point a, struct point b) {
struct point m = { (a.x + b.x) / 2.0, (a.y + b.y) / 2.0 };
return m;
}
/* structs are compared member by member; == does not compile for them */
if (r1.id == r2.id && strcmp(r1.name, r2.name) == 0) { /* equal */ }| Concept | Meaning | Typical mistake |
|---|---|---|
struct | A value type grouping related members | Expecting == to compare two of them |
typedef | A name for an existing type | Hiding a pointer behind a typedef and losing track of ownership |
| Alignment and padding | The compiler leaves gaps so members land on boundaries | Assuming sizeof is the sum of the members |
| Array of struct | Contiguous records, no bounds checking | Indexing past the end of the array |
Reading and writing files
#include <stdio.h>
FILE *f = fopen("data.txt", "r"); /* "r" text, "rb" binary, "w", "a" */
if (f == NULL) {
perror("fopen"); /* prints the reason on stderr */
return EXIT_FAILURE;
}
char line[256];
while (fgets(line, sizeof line, f) != NULL) { /* always bounded */
fputs(line, stdout);
}
if (ferror(f)) { perror("read"); }
if (fclose(f) != 0) { perror("fclose"); } /* buffered write errors surface here */
/* binary round trip: fwrite and fread take an element size and a count */
struct point pts[2] = { { 1, 2 }, { 3, 4 } };
FILE *out = fopen("points.bin", "wb");
fwrite(pts, sizeof pts[0], 2, out);
fclose(out);- Every successful
fopenneeds a matchingfclose; forgetting one leaks the descriptor and can lose buffered writes. fgetskeeps the newline and always terminates the buffer, whilefreaddoes neither and may read fewer items than you asked for.- Check
ferrorafter a read loop:feofalone cannot distinguish the end of the file from a read error. - Binary data written on one machine depends on endianness, padding and type sizes; for a portable file format, serialise one field at a time.
Undefined behaviour
The standard defines some operations as undefined behaviour: the compiler may assume they never occur and optimise accordingly. The consequence is not a predictable crash but whatever the generated code happens to do.
| Construct | Why it is undefined |
|---|---|
| Reading uninitialised memory | The value is indeterminate and may be a trap representation |
| Signed integer overflow | Optimisers assume the arithmetic never wraps |
| Out-of-bounds array access | The memory may belong to something else entirely |
| Dereferencing a dangling pointer | The lifetime of the object has ended |
| Double free, or freeing a non-heap pointer | It corrupts the allocator's own bookkeeping |
Mismatched printf conversions | A variadic call reads the wrong type and size |
| Modifying a string literal | Literals live in read-only storage |
| Shifting by a negative count or by the operand width | Undefined for both signed and unsigned operands |
# make these visible instead of silent
gcc -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer demo.c -o demo
./demo
# AddressSanitizer: heap-buffer-overflow on address 0x602000000014
# runtime error: signed integer overflow: 2147483647 + 1 cannot be represented⚠️
A program that appears to work today because of undefined behaviour can break when the compiler, the optimisation flags or the machine change. Sanitisers and
-Wall -Wextra are cheap; chasing a crash that only appears in a release build is not.FAQ
Is <code>sizeof(struct)</code> the sum of its members?
Not usually. The compiler pads members to satisfy alignment, so the size depends on declaration order. Order members from largest to smallest when size matters, and never persist a struct by writing its raw bytes.
Does <code>-O0</code> make undefined behaviour safe?
No. It changes which symptoms you see, not the guarantee. Code that seems fine at
-O0 and misbehaves at -O2 is a classic sign of undefined behaviour.Related
Pointers and memory Syntax and the compiled toolchain
Last refreshed 2026-09-18.