Step 7 of 8
Binary files
So far every file has been text: fprintf(f, "%d", 1234) writes the characters 1, 2, 3, 4. A binary file stores the raw bytes of values exactly as they sit in memory instead: an int is always 4 bytes (on this site), whatever its value. Binary files are smaller, faster to read and write, and let you jump straight to the 1,000th record without reading the 999 before it. Images, audio, databases and save games are binary.
#include <stdio.h>
struct Score {
char name[16];
int points;
};
int main(void) {
struct Score out[3] = {{"ada", 90}, {"linus", 75}, {"grace", 88}};
FILE *f = fopen("scores.bin", "wb");
if (f == NULL) return 1;
size_t written = fwrite(out, sizeof out[0], 3, f);
fclose(f);
f = fopen("scores.bin", "rb");
if (f == NULL) return 1;
fseek(f, 0, SEEK_END);
long bytes = ftell(f); /* position at the end = the file's size */
fseek(f, sizeof(struct Score), SEEK_SET); /* jump straight to record 1 */
struct Score s;
size_t got = fread(&s, sizeof s, 1, f);
fclose(f);
printf("wrote %zu records, %ld bytes\n", written, bytes);
printf("record 1: %s %d (read %zu)\n", s.name, s.points, got);
return 0;
}
wrote 3 records, 60 bytes
record 1: linus 75 (read 1)
How it works
- The modes
"wb"and"rb"addbfor binary. On Linux and macOS it makes no difference, but on Windows text mode translates line endings, which would corrupt binary data, so always include it for binary files. fwrite(pointer, size, count, f)writescountitems ofsizebytes each, starting atpointer, and returns how many items it wrote. Here that's 3 records of 20 bytes, 60 bytes in total.fread(pointer, size, count, f)is the reverse. It returns how many complete items it read, which is less thancountat the end of the file or on an error, so check it.fseek(f, offset, from)moves the read/write position: from the start (SEEK_SET), the current position (SEEK_CUR) or the end (SEEK_END).ftell(f)reports the current position. Seeking to the end and askingftellis a common way to get a file's size.- Because every record has the same size, record
istarts at bytei * sizeof(struct Score). That's random access: reading one record costs the same no matter how big the file is.
Raw bytes aren't portable
Writing a struct's bytes directly is fine for a file your own program reads back on the same machine. It's not a safe file format for anything else:
- Another compiler or machine may lay out the struct differently (padding between members), use a different size for
int, or store multi-byte numbers in the opposite byte order (endianness). - Never write pointers. An address means nothing in another run of the program.
Real file formats (PNG, ZIP, databases) define the exact size, order and byte order of every field, and programs write them field by field using fixed-size types like uint32_t (from <stdint.h>, covered in the integer types module).
Your turn: write three functions for a file of raw ints. save_ints writes n values and returns 0, or -1 if the file can't be opened or not everything was written. load_ints reads up to max values into out and returns how many it read, or -1 if the file can't be opened. read_at reads the value at position index into *out and returns 1, returns 0 if there's no value there, or -1 if the file can't be opened.
Previous: Safe formatting with snprintf Next: Challenge: a CSV report