Binary Files

Text files are human-readable. You can open them in Notepad. But sometimes you want to save data exactly as it exists in memory.

That’s what binary files are for.

Why Binary Files?

Text files store everything as readable characters. The number 1000000 takes 7 characters (1, 0, 0, 0, 0, 0, 0).

Binary files store data as raw bytes. That same number takes just 4 bytes (the same space as in memory).

Benefits of binary:

  • Faster - No conversion to/from text
  • Smaller - Numbers take less space
  • Exact - No rounding errors with decimals

Downsides:

  • Not human-readable (looks like garbage in Notepad)
  • Might not work on different types of computers

Writing Binary Data

Use fwrite to write binary data:

#include <stdio.h>

int main(void) {
    int numbers[] = {10, 20, 30, 40, 50};
    int count = sizeof(numbers) / sizeof(numbers[0]);

    FILE *file = fopen("data.bin", "wb");  // "wb" = write binary
    if (file == NULL) return 1;

    fwrite(numbers, sizeof(int), count, file);
    fclose(file);

    printf("Saved %d numbers to binary file\n", count);
    return 0;
}

fwrite(data, size, count, file) means:

  • Write from data
  • Each item is size bytes
  • Write count items
  • Write to file

Reading Binary Data

Use fread to read binary data:

#include <stdio.h>

int main(void) {
    int numbers[5];

    FILE *file = fopen("data.bin", "rb");  // "rb" = read binary
    if (file == NULL) return 1;

    int count = fread(numbers, sizeof(int), 5, file);
    fclose(file);

    printf("Read %d numbers: ", count);
    for (int i = 0; i < count; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");

    return 0;
}

fread returns how many items it actually read. Check this to know if you got everything.

Saving Structs to Binary Files

Binary files are perfect for saving structs:

#include <stdio.h>

typedef struct {
    char name[50];
    int age;
    double gpa;
} Student;

int main(void) {
    Student students[] = {
        {"Alice", 15, 3.8},
        {"Bob", 16, 3.2},
        {"Charlie", 15, 3.9}
    };
    int count = sizeof(students) / sizeof(students[0]);

    // Save
    FILE *file = fopen("students.bin", "wb");
    if (file == NULL) return 1;
    fwrite(&count, sizeof(int), 1, file);  // Save count first
    fwrite(students, sizeof(Student), count, file);
    fclose(file);
    printf("Saved %d students\n", count);

    // Load
    Student loaded[10];
    int loaded_count;

    file = fopen("students.bin", "rb");
    if (file == NULL) return 1;
    fread(&loaded_count, sizeof(int), 1, file);  // Read count first
    fread(loaded, sizeof(Student), loaded_count, file);
    fclose(file);

    printf("\nLoaded students:\n");
    for (int i = 0; i < loaded_count; i++) {
        printf("  %s, age %d, GPA %.1f\n",
               loaded[i].name, loaded[i].age, loaded[i].gpa);
    }

    return 0;
}

We save the count first so we know how many structs to read back.

File Position

Every open file has a “current position” - like a cursor. Reading and writing happen at this position and move it forward.

You can move this cursor around:

long pos = ftell(file);        // Get current position
fseek(file, 0, SEEK_SET);      // Go to beginning
fseek(file, 0, SEEK_END);      // Go to end
fseek(file, 10, SEEK_CUR);     // Move 10 bytes forward
rewind(file);                  // Same as fseek(file, 0, SEEK_SET)

The fseek function takes three arguments:

  • The file
  • How far to move (in bytes)
  • Where to measure from

Where to measure from:

  • SEEK_SET - from the beginning of the file
  • SEEK_CUR - from the current position
  • SEEK_END - from the end of the file

Getting File Size

FILE *file = fopen("data.bin", "rb");
fseek(file, 0, SEEK_END);      // Go to end
long size = ftell(file);        // Position at end = file size
fseek(file, 0, SEEK_SET);      // Go back to start
printf("File size: %ld bytes\n", size);

Complete Example: Simple Database

Here’s a program that acts like a tiny database:

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

typedef struct {
    int id;
    char name[50];
    double balance;
} Account;

void save_accounts(Account *accounts, int count, const char *filename) {
    FILE *file = fopen(filename, "wb");
    if (file == NULL) {
        perror("Error saving");
        return;
    }
    fwrite(&count, sizeof(int), 1, file);
    fwrite(accounts, sizeof(Account), count, file);
    fclose(file);
}

int load_accounts(Account *accounts, int max, const char *filename) {
    FILE *file = fopen(filename, "rb");
    if (file == NULL) {
        return 0;  // No file yet
    }
    int count;
    fread(&count, sizeof(int), 1, file);
    if (count > max) count = max;  // Safety check
    fread(accounts, sizeof(Account), count, file);
    fclose(file);
    return count;
}

void print_accounts(Account *accounts, int count) {
    printf("\n%-5s %-20s %10s\n", "ID", "Name", "Balance");
    printf("--------------------------------------\n");
    for (int i = 0; i < count; i++) {
        printf("%-5d %-20s $%9.2f\n",
               accounts[i].id, accounts[i].name, accounts[i].balance);
    }
}

int main(void) {
    const char *filename = "accounts.dat";
    Account accounts[100];
    int count = load_accounts(accounts, 100, filename);

    printf("Loaded %d accounts\n", count);
    print_accounts(accounts, count);

    // Add a new account
    Account new_acc = {count + 1, "", 0.0};
    printf("\nNew account name: ");
    scanf("%49s", new_acc.name);
    printf("Initial balance: ");
    scanf("%lf", &new_acc.balance);

    accounts[count] = new_acc;
    count++;

    save_accounts(accounts, count, filename);
    printf("Saved %d accounts\n", count);

    return 0;
}

Run it multiple times and watch your accounts persist!

Standard Streams

C gives you three “files” that are already open:

printf("hello") is actually the same as fprintf(stdout, "hello").

Use stderr for error messages:

fprintf(stderr, "Something went wrong!\n");

Why use stderr? Because normal output and error messages can be separated. When you run a program, you might send its output to a file but still see errors on screen.

Text vs Binary: Quick Reference

Text FilesBinary Files
Human readableYesNo
Functionsfprintf, fscanf, fgetsfwrite, fread
Mode“r”, “w”, “a”“rb”, “wb”, “ab”
NumbersStored as charactersStored as bytes
SizeLargerSmaller
SpeedSlowerFaster
PortableYesMaybe not

Try It Yourself

  1. Write a program that saves an array of 5 test scores to a binary file, then reads and displays them
  2. Modify the accounts database to let you update an account’s balance
  3. Write a program that copies a binary file byte by byte
  4. Write a program that shows the size of any file the user specifies

Common Mistakes

  • Forgetting the “b” in the mode (“wb” vs “w”)
  • Mixing text and binary operations on the same file
  • Not saving the count before an array (so you know how many to read)
  • Assuming binary files work across different computer types

Next Up

In Part 23, we’ll learn about networking and sockets - how to make programs talk to each other across a network.


Enjoyed This?

If this helped something click, subscribe to my YouTube channel. More content like this, same approach - making things stick without insulting your intelligence. It’s free, it helps more people find this stuff, and it tells me what’s worth making more of.