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 */ }
ConceptMeaningTypical mistake
structA value type grouping related membersExpecting == to compare two of them
typedefA name for an existing typeHiding a pointer behind a typedef and losing track of ownership
Alignment and paddingThe compiler leaves gaps so members land on boundariesAssuming sizeof is the sum of the members
Array of structContiguous records, no bounds checkingIndexing 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 fopen needs a matching fclose; forgetting one leaks the descriptor and can lose buffered writes.
  • fgets keeps the newline and always terminates the buffer, while fread does neither and may read fewer items than you asked for.
  • Check ferror after a read loop: feof alone 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.

ConstructWhy it is undefined
Reading uninitialised memoryThe value is indeterminate and may be a trap representation
Signed integer overflowOptimisers assume the arithmetic never wraps
Out-of-bounds array accessThe memory may belong to something else entirely
Dereferencing a dangling pointerThe lifetime of the object has ended
Double free, or freeing a non-heap pointerIt corrupts the allocator's own bookkeeping
Mismatched printf conversionsA variadic call reads the wrong type and size
Modifying a string literalLiterals live in read-only storage
Shifting by a negative count or by the operand widthUndefined 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.

Pointers and memory Syntax and the compiled toolchain

Last refreshed 2026-09-18.