Text file I/O is what most tutorials cover. Binary file I/O is what you need when reading image files, database records, audio samples, or any fixed-layout data format. The difference is not just the "rb" mode flag — it changes how newlines are handled, how you find positions, and how struct layout must match the file format.

Opening in Binary Mode

#include <stdio.h>

FILE *fp = fopen("data.bin", "rb");   /* read binary */
FILE *fw = fopen("out.bin",  "wb");   /* write binary */
FILE *fa = fopen("log.bin",  "ab");   /* append binary */

On Windows, text mode translates rn to n on read and back on write. In binary mode, no translation happens — bytes are transferred exactly. Always use "rb"/"wb" for binary data, even on Linux (where there is no translation, but the intent is documented).

Writing a Struct to a File

#include <stdio.h>
#include <stdint.h>
#include <string.h>

typedef struct {
    uint32_t id;
    float    temperature;
    char     label[16];
} Record;

int main() {
    Record r;
    r.id          = 42;
    r.temperature = 23.5f;
    strncpy(r.label, "sensor-A", sizeof(r.label) - 1);
    r.label[sizeof(r.label)-1] = '';

    FILE *fp = fopen("records.bin", "wb");
    if (!fp) { perror("fopen"); return 1; }

    size_t written = fwrite(&r, sizeof(Record), 1, fp);
    if (written != 1) { perror("fwrite"); fclose(fp); return 1; }

    fclose(fp);
    printf("Wrote %zu bytesn", sizeof(Record));
    return 0;
}

Always check fwrite‘s return value — it returns the number of items written, not bytes. If it returns fewer than requested, an error occurred.

Reading the Struct Back

#include <stdio.h>
#include <stdint.h>

typedef struct {
    uint32_t id;
    float    temperature;
    char     label[16];
} Record;

int main() {
    Record r;

    FILE *fp = fopen("records.bin", "rb");
    if (!fp) { perror("fopen"); return 1; }

    size_t n = fread(&r, sizeof(Record), 1, fp);
    if (n != 1) {
        if (feof(fp))   fprintf(stderr, "End of filen");
        else            perror("fread");
        fclose(fp);
        return 1;
    }

    fclose(fp);
    printf("ID: %u, Temp: %.1f, Label: %sn", r.id, r.temperature, r.label);
    return 0;
}

Random Access with fseek, ftell, rewind

#include <stdio.h>

int main() {
    FILE *fp = fopen("records.bin", "rb");
    if (!fp) { perror("fopen"); return 1; }

    /* Jump to position 8 from beginning */
    fseek(fp, 8, SEEK_SET);

    /* SEEK_SET = from beginning, SEEK_CUR = from current, SEEK_END = from end */

    /* Get current position */
    long pos = ftell(fp);
    printf("Current position: %ldn", pos);   /* 8 */

    /* Jump to end to get file size */
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);
    printf("File size: %ld bytesn", file_size);

    /* Reset to beginning */
    rewind(fp);   /* equivalent to fseek(fp, 0, SEEK_SET) + clears error flag */

    fclose(fp);
    return 0;
}

Reading Multiple Records

#include <stdio.h>
#include <stdint.h>

typedef struct {
    uint32_t id;
    float    temperature;
    char     label[16];
} Record;

void read_record_n(FILE *fp, size_t n, Record *out) {
    fseek(fp, (long)(n * sizeof(Record)), SEEK_SET);
    fread(out, sizeof(Record), 1, fp);
}

int main() {
    /* Write 3 records */
    FILE *fw = fopen("records.bin", "wb");
    Record records[3] = {
        {1, 20.0f, "sensor-A"},
        {2, 21.5f, "sensor-B"},
        {3, 19.8f, "sensor-C"},
    };
    fwrite(records, sizeof(Record), 3, fw);
    fclose(fw);

    /* Read record 1 (second one, 0-indexed) */
    FILE *fr = fopen("records.bin", "rb");
    Record r;
    read_record_n(fr, 1, &r);
    printf("Record 1: id=%u temp=%.1f label=%sn", r.id, r.temperature, r.label);
    fclose(fr);

    return 0;
}

Cross-Platform Pitfalls

Struct padding: The compiler may add padding between struct fields. A struct written on one platform may not read correctly on another with different alignment. Use __attribute__((packed)) or manually serialize field-by-field for cross-platform binary formats.

Endianness: An int written on a little-endian x86 machine has its bytes in a different order than on a big-endian ARM. Use htonl/ntohl or read/write byte-by-byte for portable formats.

sizeof may differ: int is 4 bytes on most 64-bit systems but was 2 bytes on 16-bit systems. Use uint32_t, int64_t, etc. from <stdint.h> for fixed-size types in binary formats.

For text file reading patterns, see our guide on reading files in C. Test binary I/O concepts in our c online compiler by writing to a buffer with fmemopen to simulate file operations without creating actual files.

TL;DR

  • Always use "rb"/"wb" for binary files — avoids newline translation on Windows
  • Check fread/fwrite return values — they return item count, not byte count
  • fseek(fp, offset, SEEK_SET/CUR/END) jumps to a position; ftell returns current position
  • Use rewind to reset position and clear the error flag simultaneously
  • Use uint32_t/int64_t in structs for cross-platform binary formats
  • Handle endianness explicitly if the file may be read on a different architecture
  • Use __attribute__((packed)) or serialize field-by-field to eliminate padding in portable formats