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:

C
#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;
}
Output
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:

C
#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;
}
Output
record 2: 3 Chloe 1200.75
96 bytes = 3 records
1 Ana      150.00
2 Ben      100.00
3 Chloe   1200.75
FunctionDoes
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:

C
#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;
}
Output
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.

Try it yourself · main.c
Edit the code, copy it into main.c, then compile and run it with cc -std=c17 -Wall main.c -o main && ./main. How to set up a C compiler
Output
1. ZED  9800
2. AMY  8750
3. KIT  7020