While text files are human-readable, binary files offer efficiency, precision, and direct memory representation. Binary I/O is essential for saving complex data structures, working with multimedia files, implementing databases, and communicating with hardware devices. Understanding binary file operations in C gives you the power to work with any file format and build high-performance data storage systems.
What Is Binary File I/O?
Binary file I/O reads and writes data in the same format as it's stored in memory, without any conversion or formatting. Unlike text mode, which may perform translations (like newline conversion), binary mode preserves the exact byte representation.
Text File: Binary File: "1234" as characters int 1234 as 4 bytes '1' '2' '3' '4' '\n' [0xD2][0x04][0x00][0x00] 5 bytes 4 bytes
Key Functions for Binary I/O
| Function | Purpose | Signature |
|---|---|---|
fopen | Open file | FILE *fopen(const char *filename, const char *mode) |
fclose | Close file | int fclose(FILE *stream) |
fread | Read binary data | size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) |
fwrite | Write binary data | size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) |
fseek | Move file position | int fseek(FILE *stream, long offset, int whence) |
ftell | Get file position | long ftell(FILE *stream) |
rewind | Reset to beginning | void rewind(FILE *stream) |
feof | Check end of file | int feof(FILE *stream) |
ferror | Check error | int ferror(FILE *stream) |
Opening Files in Binary Mode
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file;
// Open for writing (binary mode)
file = fopen("data.bin", "wb");
if (file == NULL) {
perror("Error opening file");
return 1;
}
// ... write data ...
fclose(file);
// Open for reading (binary mode)
file = fopen("data.bin", "rb");
if (file == NULL) {
perror("Error opening file");
return 1;
}
// ... read data ...
fclose(file);
// Open for reading and writing (binary mode)
file = fopen("data.bin", "r+b"); // Existing file
// or
file = fopen("data.bin", "w+b"); // Create new/truncate existing
return 0;
}
Basic Binary I/O Operations
1. Writing and Reading Single Values
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file;
// Write integers
file = fopen("numbers.bin", "wb");
if (file == NULL) {
perror("Error opening file");
return 1;
}
int numbers[] = {10, 20, 30, 40, 50};
int count = sizeof(numbers) / sizeof(numbers[0]);
// Write each number individually
for (int i = 0; i < count; i++) {
size_t written = fwrite(&numbers[i], sizeof(int), 1, file);
if (written != 1) {
printf("Error writing number %d\n", i);
fclose(file);
return 1;
}
}
fclose(file);
// Read integers back
file = fopen("numbers.bin", "rb");
if (file == NULL) {
perror("Error opening file");
return 1;
}
int read_num;
printf("Reading numbers from file:\n");
while (fread(&read_num, sizeof(int), 1, file) == 1) {
printf("%d ", read_num);
}
printf("\n");
fclose(file);
return 0;
}
2. Writing and Reading Arrays
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file;
// Write entire array at once
int data[100];
for (int i = 0; i < 100; i++) {
data[i] = i * i;
}
file = fopen("array.bin", "wb");
if (file == NULL) {
perror("Error opening file");
return 1;
}
size_t written = fwrite(data, sizeof(int), 100, file);
if (written != 100) {
printf("Error: wrote %zu elements instead of 100\n", written);
}
fclose(file);
// Read entire array at once
int read_data[100];
file = fopen("array.bin", "rb");
if (file == NULL) {
perror("Error opening file");
return 1;
}
size_t read = fread(read_data, sizeof(int), 100, file);
if (read != 100) {
printf("Error: read %zu elements instead of 100\n", read);
}
fclose(file);
// Verify first few elements
printf("First 10 elements:\n");
for (int i = 0; i < 10; i++) {
printf("data[%d] = %d\n", i, read_data[i]);
}
return 0;
}
Working with Structures
Binary I/O is perfect for saving and loading structures:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Structure with fixed-size fields
typedef struct {
int id;
char name[50];
float gpa;
char enrolled; // 'Y' or 'N'
} Student;
// Structure with pointer (requires special handling)
typedef struct {
int id;
char *name; // Pointer - can't write directly!
float gpa;
} DynamicStudent;
// Save array of students
int saveStudents(const char *filename, Student *students, int count) {
FILE *file = fopen(filename, "wb");
if (file == NULL) {
perror("Error opening file");
return 0;
}
// Write count first
fwrite(&count, sizeof(int), 1, file);
// Write all students
size_t written = fwrite(students, sizeof(Student), count, file);
fclose(file);
return written == count;
}
// Load array of students
Student* loadStudents(const char *filename, int *out_count) {
FILE *file = fopen(filename, "rb");
if (file == NULL) {
perror("Error opening file");
return NULL;
}
// Read count
int count;
if (fread(&count, sizeof(int), 1, file) != 1) {
fclose(file);
return NULL;
}
// Allocate memory
Student *students = malloc(count * sizeof(Student));
if (students == NULL) {
fclose(file);
return NULL;
}
// Read students
size_t read = fread(students, sizeof(Student), count, file);
fclose(file);
if (read != count) {
free(students);
return NULL;
}
*out_count = count;
return students;
}
// Save structure with pointer (using two-step process)
int saveDynamicStudent(const char *filename, DynamicStudent *s) {
FILE *file = fopen(filename, "wb");
if (file == NULL) return 0;
// Write fixed-size data
fwrite(&s->id, sizeof(int), 1, file);
// Write string length and content
int name_len = strlen(s->name);
fwrite(&name_len, sizeof(int), 1, file);
fwrite(s->name, sizeof(char), name_len, file);
// Write remaining fixed data
fwrite(&s->gpa, sizeof(float), 1, file);
fclose(file);
return 1;
}
// Load structure with pointer
DynamicStudent* loadDynamicStudent(const char *filename) {
FILE *file = fopen(filename, "rb");
if (file == NULL) return NULL;
DynamicStudent *s = malloc(sizeof(DynamicStudent));
if (s == NULL) {
fclose(file);
return NULL;
}
// Read fixed data
fread(&s->id, sizeof(int), 1, file);
// Read string length and allocate
int name_len;
fread(&name_len, sizeof(int), 1, file);
s->name = malloc(name_len + 1);
// Read string content
fread(s->name, sizeof(char), name_len, file);
s->name[name_len] = '\0';
// Read remaining data
fread(&s->gpa, sizeof(float), 1, file);
fclose(file);
return s;
}
int main() {
// Test fixed-size structures
Student class[3] = {
{101, "Alice Johnson", 3.85, 'Y'},
{102, "Bob Smith", 3.45, 'Y'},
{103, "Charlie Brown", 2.95, 'N'}
};
printf("Saving %d students...\n", 3);
if (saveStudents("students.bin", class, 3)) {
printf("Students saved successfully.\n");
}
int count;
Student *loaded = loadStudents("students.bin", &count);
if (loaded) {
printf("\nLoaded %d students:\n", count);
for (int i = 0; i < count; i++) {
printf(" ID: %d, Name: %s, GPA: %.2f, Enrolled: %c\n",
loaded[i].id, loaded[i].name,
loaded[i].gpa, loaded[i].enrolled);
}
free(loaded);
}
// Test dynamic structure
DynamicStudent ds = {201, "Dynamic Dave", 3.75};
ds.name = malloc(50);
strcpy(ds.name, "Dynamic Dave");
saveDynamicStudent("dynamic.bin", &ds);
DynamicStudent *loaded_ds = loadDynamicStudent("dynamic.bin");
if (loaded_ds) {
printf("\nLoaded dynamic student:\n");
printf(" ID: %d, Name: %s, GPA: %.2f\n",
loaded_ds->id, loaded_ds->name, loaded_ds->gpa);
free(loaded_ds->name);
free(loaded_ds);
}
free(ds.name);
return 0;
}
Random Access with fseek and ftell
#include <stdio.h>
#include <stdlib.h>
#define RECORD_SIZE sizeof(int)
void demonstrateRandomAccess() {
FILE *file = fopen("random.bin", "w+b");
if (file == NULL) {
perror("Error opening file");
return;
}
// Write 100 integers
for (int i = 0; i < 100; i++) {
int value = i * 10;
fwrite(&value, sizeof(int), 1, file);
}
// Read specific records
int position, value;
// Read record at index 50
fseek(file, 50 * sizeof(int), SEEK_SET);
fread(&value, sizeof(int), 1, file);
printf("Record 50: %d\n", value);
// Read last record
fseek(file, -sizeof(int), SEEK_END);
fread(&value, sizeof(int), 1, file);
printf("Last record: %d\n", value);
// Get current position
long pos = ftell(file);
printf("Current position: %ld\n", pos);
// Go back 10 records
fseek(file, -10 * sizeof(int), SEEK_CUR);
fread(&value, sizeof(int), 1, file);
printf("Record at -10 from end: %d\n", value);
// Update a record
int new_value = 999;
fseek(file, 25 * sizeof(int), SEEK_SET);
fwrite(&new_value, sizeof(int), 1, file);
// Verify update
fseek(file, 25 * sizeof(int), SEEK_SET);
fread(&value, sizeof(int), 1, file);
printf("Updated record 25: %d\n", value);
fclose(file);
}
File Positioning Constants
| Constant | Value | Description |
|---|---|---|
SEEK_SET | 0 | Seek from beginning of file |
SEEK_CUR | 1 | Seek from current position |
SEEK_END | 2 | Seek from end of file |
Reading/Writing Complex Data Structures
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Nested structures
typedef struct {
int day;
int month;
int year;
} Date;
typedef struct {
char street[100];
char city[50];
char state[30];
int zip;
} Address;
typedef struct {
int id;
char name[50];
Date birth_date;
Address address;
float salary;
int num_children;
int *children_ages; // Dynamic array
} Employee;
// Save employee with dynamic children array
int saveEmployee(const char *filename, Employee *emp) {
FILE *file = fopen(filename, "wb");
if (file == NULL) return 0;
// Save fixed-size data
fwrite(&emp->id, sizeof(int), 1, file);
fwrite(emp->name, sizeof(char), 50, file);
fwrite(&emp->birth_date, sizeof(Date), 1, file);
fwrite(&emp->address, sizeof(Address), 1, file);
fwrite(&emp->salary, sizeof(float), 1, file);
fwrite(&emp->num_children, sizeof(int), 1, file);
// Save dynamic children ages
if (emp->num_children > 0) {
fwrite(emp->children_ages, sizeof(int), emp->num_children, file);
}
fclose(file);
return 1;
}
// Load employee with dynamic children array
Employee* loadEmployee(const char *filename) {
FILE *file = fopen(filename, "rb");
if (file == NULL) return NULL;
Employee *emp = malloc(sizeof(Employee));
if (emp == NULL) {
fclose(file);
return NULL;
}
// Load fixed-size data
fread(&emp->id, sizeof(int), 1, file);
fread(emp->name, sizeof(char), 50, file);
fread(&emp->birth_date, sizeof(Date), 1, file);
fread(&emp->address, sizeof(Address), 1, file);
fread(&emp->salary, sizeof(float), 1, file);
fread(&emp->num_children, sizeof(int), 1, file);
// Load dynamic children ages
if (emp->num_children > 0) {
emp->children_ages = malloc(emp->num_children * sizeof(int));
fread(emp->children_ages, sizeof(int), emp->num_children, file);
} else {
emp->children_ages = NULL;
}
fclose(file);
return emp;
}
// Free employee
void freeEmployee(Employee *emp) {
if (emp) {
free(emp->children_ages);
free(emp);
}
}
int main() {
// Create employee
Employee emp;
emp.id = 1001;
strcpy(emp.name, "John Smith");
emp.birth_date = (Date){15, 6, 1985};
strcpy(emp.address.street, "123 Main St");
strcpy(emp.address.city, "Anytown");
strcpy(emp.address.state, "CA");
emp.address.zip = 12345;
emp.salary = 75000.50;
emp.num_children = 2;
emp.children_ages = malloc(2 * sizeof(int));
emp.children_ages[0] = 5;
emp.children_ages[1] = 7;
// Save to file
if (saveEmployee("employee.dat", &emp)) {
printf("Employee saved successfully.\n");
}
free(emp.children_ages);
// Load from file
Employee *loaded = loadEmployee("employee.dat");
if (loaded) {
printf("\nLoaded Employee:\n");
printf(" ID: %d\n", loaded->id);
printf(" Name: %s\n", loaded->name);
printf(" Birth: %d/%d/%d\n",
loaded->birth_date.day,
loaded->birth_date.month,
loaded->birth_date.year);
printf(" Address: %s, %s, %s %d\n",
loaded->address.street,
loaded->address.city,
loaded->address.state,
loaded->address.zip);
printf(" Salary: %.2f\n", loaded->salary);
printf(" Children ages: ");
for (int i = 0; i < loaded->num_children; i++) {
printf("%d ", loaded->children_ages[i]);
}
printf("\n");
freeEmployee(loaded);
}
return 0;
}
Error Handling in Binary I/O
#include <stdio.h>
#include <errno.h>
#include <string.h>
typedef struct {
char message[256];
int error_code;
} FileError;
FileError last_error = {"", 0};
int safe_fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) {
size_t written = fwrite(ptr, size, nmemb, stream);
if (written != nmemb) {
if (feof(stream)) {
strcpy(last_error.message, "Unexpected end of file");
last_error.error_code = 1;
} else if (ferror(stream)) {
strcpy(last_error.message, strerror(errno));
last_error.error_code = errno;
} else {
strcpy(last_error.message, "Write operation incomplete");
last_error.error_code = -1;
}
return 0;
}
return 1;
}
int safe_fread(void *ptr, size_t size, size_t nmemb, FILE *stream) {
size_t read = fread(ptr, size, nmemb, stream);
if (read != nmemb) {
if (feof(stream)) {
strcpy(last_error.message, "Unexpected end of file");
last_error.error_code = 1;
} else if (ferror(stream)) {
strcpy(last_error.message, strerror(errno));
last_error.error_code = errno;
} else {
strcpy(last_error.message, "Read operation incomplete");
last_error.error_code = -1;
}
return 0;
}
return 1;
}
void clear_error() {
last_error.message[0] = '\0';
last_error.error_code = 0;
}
const char* get_last_error() {
return last_error.message;
}
int main() {
FILE *file = fopen("test.bin", "wb");
if (!file) {
perror("Error opening file");
return 1;
}
int data[] = {1, 2, 3, 4, 5};
clear_error();
if (!safe_fwrite(data, sizeof(int), 5, file)) {
printf("Write failed: %s (code %d)\n",
get_last_error(), last_error.error_code);
} else {
printf("Write successful\n");
}
fclose(file);
return 0;
}
Binary File Utilities
1. File Copy
int copy_file(const char *src, const char *dst) {
FILE *source = fopen(src, "rb");
if (!source) {
perror("Error opening source");
return 0;
}
FILE *dest = fopen(dst, "wb");
if (!dest) {
perror("Error opening destination");
fclose(source);
return 0;
}
char buffer[4096];
size_t bytes;
while ((bytes = fread(buffer, 1, sizeof(buffer), source)) > 0) {
if (fwrite(buffer, 1, bytes, dest) != bytes) {
perror("Error writing");
fclose(source);
fclose(dest);
return 0;
}
}
fclose(source);
fclose(dest);
return 1;
}
2. File Size
long get_file_size(const char *filename) {
FILE *file = fopen(filename, "rb");
if (!file) return -1;
fseek(file, 0, SEEK_END);
long size = ftell(file);
fclose(file);
return size;
}
3. Hex Dump
void hex_dump(const char *filename) {
FILE *file = fopen(filename, "rb");
if (!file) {
perror("Error opening file");
return;
}
unsigned char buffer[16];
size_t bytes;
long offset = 0;
printf("Offset Hex Data ASCII\n");
printf("-------- ---------------------------------------- ----------------\n");
while ((bytes = fread(buffer, 1, sizeof(buffer), file)) > 0) {
// Print offset
printf("%08lX ", offset);
// Print hex
for (size_t i = 0; i < 16; i++) {
if (i < bytes) {
printf("%02X ", buffer[i]);
} else {
printf(" ");
}
if (i == 7) printf(" ");
}
printf(" ");
// Print ASCII
for (size_t i = 0; i < bytes; i++) {
printf("%c", (buffer[i] >= 32 && buffer[i] <= 126) ? buffer[i] : '.');
}
printf("\n");
offset += bytes;
}
fclose(file);
}
4. File Checksum
unsigned int file_checksum(const char *filename) {
FILE *file = fopen(filename, "rb");
if (!file) return 0;
unsigned int checksum = 0;
int c;
while ((c = fgetc(file)) != EOF) {
checksum += c;
}
fclose(file);
return checksum;
}
Practical Example: Simple Database
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_RECORDS 1000
#define NAME_LEN 50
typedef struct {
int id;
char name[NAME_LEN];
float score;
int active; // 1 = active, 0 = deleted
} Record;
typedef struct {
FILE *file;
int record_count;
int next_id;
} Database;
// Open database
Database* db_open(const char *filename) {
Database *db = malloc(sizeof(Database));
// Try to open existing file
db->file = fopen(filename, "r+b");
if (db->file == NULL) {
// Create new file
db->file = fopen(filename, "w+b");
if (db->file == NULL) {
free(db);
return NULL;
}
db->record_count = 0;
db->next_id = 1;
// Write header
fwrite(&db->record_count, sizeof(int), 1, db->file);
fwrite(&db->next_id, sizeof(int), 1, db->file);
} else {
// Read header from existing file
fread(&db->record_count, sizeof(int), 1, db->file);
fread(&db->next_id, sizeof(int), 1, db->file);
}
return db;
}
// Close database
void db_close(Database *db) {
if (db) {
// Update header
fseek(db->file, 0, SEEK_SET);
fwrite(&db->record_count, sizeof(int), 1, db->file);
fwrite(&db->next_id, sizeof(int), 1, db->file);
fclose(db->file);
free(db);
}
}
// Add record
int db_add(Database *db, const char *name, float score) {
if (db->record_count >= MAX_RECORDS) return -1;
Record rec;
rec.id = db->next_id++;
strncpy(rec.name, name, NAME_LEN - 1);
rec.name[NAME_LEN - 1] = '\0';
rec.score = score;
rec.active = 1;
// Seek to end of file
fseek(db->file, 0, SEEK_END);
// Write record
fwrite(&rec, sizeof(Record), 1, db->file);
db->record_count++;
return rec.id;
}
// Find record by ID
Record* db_find(Database *db, int id) {
fseek(db->file, 2 * sizeof(int), SEEK_SET); // Skip header
Record *rec = malloc(sizeof(Record));
for (int i = 0; i < db->record_count; i++) {
fread(rec, sizeof(Record), 1, db->file);
if (rec->active && rec->id == id) {
return rec;
}
}
free(rec);
return NULL;
}
// Update record
int db_update(Database *db, int id, float new_score) {
fseek(db->file, 2 * sizeof(int), SEEK_SET);
Record rec;
long pos;
for (int i = 0; i < db->record_count; i++) {
pos = ftell(db->file);
fread(&rec, sizeof(Record), 1, db->file);
if (rec.active && rec.id == id) {
rec.score = new_score;
fseek(db->file, pos, SEEK_SET);
fwrite(&rec, sizeof(Record), 1, db->file);
return 1;
}
}
return 0;
}
// Delete record (mark as inactive)
int db_delete(Database *db, int id) {
fseek(db->file, 2 * sizeof(int), SEEK_SET);
Record rec;
long pos;
for (int i = 0; i < db->record_count; i++) {
pos = ftell(db->file);
fread(&rec, sizeof(Record), 1, db->file);
if (rec.active && rec.id == id) {
rec.active = 0;
fseek(db->file, pos, SEEK_SET);
fwrite(&rec, sizeof(Record), 1, db->file);
return 1;
}
}
return 0;
}
// List all active records
void db_list(Database *db) {
fseek(db->file, 2 * sizeof(int), SEEK_SET);
Record rec;
printf("\nID Name Score\n");
printf("--- ------------------- -----\n");
for (int i = 0; i < db->record_count; i++) {
fread(&rec, sizeof(Record), 1, db->file);
if (rec.active) {
printf("%3d %-20s %5.2f\n",
rec.id, rec.name, rec.score);
}
}
}
int main() {
Database *db = db_open("database.bin");
if (!db) {
printf("Failed to open database\n");
return 1;
}
// Add some records
db_add(db, "Alice Johnson", 95.5);
db_add(db, "Bob Smith", 87.0);
db_add(db, "Charlie Brown", 92.5);
db_add(db, "Diana Prince", 98.0);
printf("Initial database:");
db_list(db);
// Update a record
db_update(db, 2, 91.5);
// Delete a record
db_delete(db, 3);
printf("\nAfter updates:");
db_list(db);
// Find a record
Record *rec = db_find(db, 2);
if (rec) {
printf("\nFound: %s (score %.2f)\n", rec->name, rec->score);
free(rec);
}
db_close(db);
return 0;
}
Binary vs Text Mode Comparison
| Aspect | Binary Mode | Text Mode |
|---|---|---|
| Newline handling | No conversion | \n ↔ platform-specific |
| Data representation | Raw bytes | Human-readable |
| File size | Smaller | Larger |
| Precision | Exact | May lose precision |
| Portability | Platform-dependent | More portable |
| Random access | Easy (fixed size) | Difficult (variable lines) |
| Debugging | Hex dump needed | Can view in editor |
Common Pitfalls
// Pitfall 1: Not checking return values
fwrite(data, size, count, file); // Did it write all?
// Pitfall 2: Opening in wrong mode
FILE *f = fopen("data.bin", "r"); // Text mode! Use "rb"
// Pitfall 3: Not flushing
fwrite(&data, sizeof(data), 1, file);
// Crash before fclose - data may not be written!
// Pitfall 4: Assuming structure packing
struct { char c; int i; } s; // May have padding!
fwrite(&s, sizeof(s), 1, file); // Writes padding bytes
// Pitfall 5: Not handling endianness
int x = 0x12345678;
fwrite(&x, sizeof(x), 1, file); // Written in host byte order
// Pitfall 6: Seeking from wrong position
fseek(file, -10, SEEK_SET); // Negative from beginning - invalid!
Best Practices
- Always check return values of fread/fwrite
- Use binary mode for non-text data
- Flush files when necessary with
fflush() - Handle errors gracefully
- Consider endianness for cross-platform data
- Pack structures if exact layout matters
- Use fixed-size types (
int32_t, etc.) for portability - Close files as soon as possible
- Test with edge cases (empty files, full disks)
- Document file format for future reference
Conclusion
Binary file I/O in C provides:
- Efficient storage - No conversion overhead
- Exact representation - Perfect for saving program state
- Random access - Jump to any record instantly
- Complex data structures - Save anything that fits in memory
- High performance - Direct memory transfers
Key principles:
- Use
"b"in fopen mode for binary access fread/fwritework directly with memory- Handle dynamic data with multiple writes
- Use
fseek/ftellfor random access - Always check for errors
- Consider portability issues
Mastering binary I/O is essential for systems programming, game development, database implementation, and any application that needs to store and retrieve data efficiently. It's a fundamental skill that separates beginners from advanced C programmers.