Binary Files and File Positions
Text files store numbers as readable characters. Binary files store the raw bytes instead — compact, fast, and exact. fwrite and fread move blocks of memory to and from a file, and fseek/ftell let you jump straight to any record.
fwrite and fread
fwrite(ptr, size, count, file) writes count items of size bytes; fread reads them back. Both return the number of complete items transferred:
#include <stdio.h> int main(void) { double readings[5] = { 21.5, 22.0, 21.75, 23.25, 22.5 }; FILE *out = fopen("readings.bin", "wb"); if (out == NULL) return 1; size_t written = fwrite(readings, sizeof readings[0], 5, out); fclose(out); printf("wrote %zu values\n", written); double loaded[5]; FILE *in = fopen("readings.bin", "rb"); if (in == NULL) return 1; size_t got = fread(loaded, sizeof loaded[0], 5, in); fclose(in); printf("read %zu values:", got); for (size_t i = 0; i < got; i++) printf(" %.2f", loaded[i]); printf("\n"); return 0; }
wrote 5 values read 5 values: 21.50 22.00 21.75 23.25 22.50
Always use "wb"/"rb" for binary data so that Windows doesn't translate any bytes.
Records: an array of structs
Fixed-size structs make a simple database: record n starts at byte n × sizeof(record), so you can jump directly to it with fseek:
#include <stdio.h> #include <string.h> typedef struct { int id; char name[20]; double balance; } Account; int main(void) { Account accounts[] = { { 1, "Ana", 150.00 }, { 2, "Ben", 82.50 }, { 3, "Chloe", 1200.75 }, }; FILE *f = fopen("accounts.dat", "w+b"); if (f == NULL) return 1; fwrite(accounts, sizeof accounts[0], 3, f); // Jump to record 2 (the third) and read it Account a; fseek(f, 2L * (long) sizeof(Account), SEEK_SET); if (fread(&a, sizeof a, 1, f) == 1) { printf("record 2: %d %s %.2f\n", a.id, a.name, a.balance); } // Update record 1 in place Account updated = { 2, "Ben", 100.00 }; fseek(f, 1L * (long) sizeof(Account), SEEK_SET); fwrite(&updated, sizeof updated, 1, f); // File size tells us how many records there are fseek(f, 0, SEEK_END); long size = ftell(f); printf("%ld bytes = %ld records\n", size, size / (long) sizeof(Account)); rewind(f); while (fread(&a, sizeof a, 1, f) == 1) { printf("%d %-6s %8.2f\n", a.id, a.name, a.balance); } fclose(f); return 0; }
record 2: 3 Chloe 1200.75 96 bytes = 3 records 1 Ana 150.00 2 Ben 100.00 3 Chloe 1200.75
| Function | Does |
|---|---|
fseek(f, offset, SEEK_SET) | Move to offset bytes from the start |
fseek(f, offset, SEEK_CUR) | Move relative to the current position |
fseek(f, offset, SEEK_END) | Move relative to the end |
ftell(f) | Current position in bytes |
rewind(f) | Back to the start |
When a file is open for both reading and writing, call fseek (or fflush) between switching from writing to reading and vice versa — the standard requires it.
Portability warning
Writing structs directly is quick, but the file then depends on how this compiler laid out the struct: padding, the size of int and double, and byte order (endianness, lesson 42). A file written on one platform may not read correctly on another. For files that are shared between systems, write each field explicitly using fixed-width types and a defined byte order:
#include <stdio.h> #include <stdint.h> void write_u32_le(FILE *f, uint32_t v) { unsigned char b[4] = { v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF, (v >> 24) & 0xFF }; fwrite(b, 1, 4, f); } uint32_t read_u32_le(FILE *f) { unsigned char b[4] = { 0 }; if (fread(b, 1, 4, f) != 4) return 0; return (uint32_t) b[0] | (uint32_t) b[1] << 8 | (uint32_t) b[2] << 16 | (uint32_t) b[3] << 24; } int main(void) { FILE *f = fopen("header.bin", "w+b"); if (f == NULL) return 1; write_u32_le(f, 0xCAFE); write_u32_le(f, 2026); rewind(f); uint32_t magic = read_u32_le(f), year = read_u32_le(f); printf("magic 0x%X, year %u, file is %ld bytes\n", (unsigned) magic, (unsigned) year, ftell(f)); fclose(f); return 0; }
magic 0xCAFE, year 2026, file is 8 bytes
This gives the same bytes on every machine. Real formats such as PNG and ZIP are specified exactly this way.
Detecting the end and errors
When fread returns fewer items than requested, check why: feof(f) is true if the end of the file was reached, ferror(f) if a read error happened. Don't use feof as a loop condition before reading — test the result of the read itself, as the examples do.
Try it yourself
Save a high-score table of structs to a binary file, then load it back and print it.
1. ZED 9800 2. AMY 8750 3. KIT 7020