Skip to main content

Records as Bytes

intermediate17 min readLesson 129 of 148

fwrite/fread move bytes, not fields. Struct layout, endianness, and the record header that makes files self-describing.

fwrite moves bytes — including padding

struct Rec { long id; int qty; };
struct Rec r = {7, 3};
fwrite(&r, sizeof r, 1, f);   /* writes 16 bytes on a 64-bit libc:
                                 8 (id) + 4 (qty) + 4 (padding!) */

The four padding bytes are garbage carried to disk — worse, they are non-portable: a different compiler or ABI lays the struct out differently and your file format breaks. Portable record I/O writes fields, not structs:

/* field-by-field, fixed widths, defined by YOU */
uint32_t id = (uint32_t)r.id;
fwrite(&id, sizeof id, 1, f);

Endianness is a tax you pay once

Little-endian machines store the low byte first. If a file is written as raw uint32_t, an x86 writer and a big-endian reader disagree on every multi-byte field. Formats that live beyond one machine define a byte order (network order = big-endian) and convert at the boundary:

/* little-endian write of a 32-bit value, byte by byte */
unsigned char b[4] = { v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF, (v >> 24) & 0xFF };
fwrite(b, 4, 1, f);

Byte-by-byte I/O is slower but defines the format — no platform can disagree with you about it.

The self-describing header

Every non-trivial binary format starts with a header: magic number (your format's signature), version, count. Readers check all three before trusting anything else:

unsigned char hdr[8];
fread(hdr, 1, 8, f) == 8 || die();
memcmp(hdr, "CJR1", 4) == 0 || die("wrong magic");
/* hdr[4..7] = record count, little-endian */

fread returning fewer bytes than asked is the normal failure mode of short files — not an exotic event. Counting your bytes is the discipline.