Binary vs Text Files
While text files store data as human-readable characters, binary files store data in its raw, machine-native format. Binary files are more compact, faster to read and write, and essential for working with images, audio, executables, and complex data structures. Understanding the differences is crucial for choosing the right approach for your data.
What Makes Binary Files Different?
Text files represent everything as printable characters. The number 12345 takes 5 bytes (one per digit). In a binary file, the same number stored as an int takes just 4 bytes (on most systems), and floating-point numbers maintain their exact precision instead of being rounded in text representation.
Text Files
- Human-readable content
- Newline translation (CR/LF)
- Larger file size for numeric data
- Easy to edit with any text editor
- Use: Config files, logs, CSV, source code
Binary Files
- Machine-readable raw bytes
- No translation (exact bytes preserved)
- Compact storage for all data types
- Faster read/write operations
- Use: Images, audio, databases, structs
Size Comparison Example
Let us see how the same data takes different amounts of space in text vs binary format:
| Data | Text File Size | Binary File Size | Savings |
|---|---|---|---|
| Integer: 2147483647 | 10 bytes (10 digits) | 4 bytes (int) | 60% |
| Float: 3.14159265359 | 13 bytes (chars) | 4 bytes (float) | 69% |
| 1000 integers | ~6-10 KB | 4 KB exactly | 40-60% |
Opening Files in Binary Mode
To work with binary files, add b to the file mode string. This is critical on Windows
where text mode performs newline translation that would corrupt binary data.
// Binary mode - add 'b' to the mode
FILE *fp1 = fopen("data.bin", "rb"); // Read binary
FILE *fp2 = fopen("data.bin", "wb"); // Write binary
FILE *fp3 = fopen("data.bin", "ab"); // Append binary
FILE *fp4 = fopen("data.bin", "rb+"); // Read/write binary
FILE *fp5 = fopen("data.bin", "wb+"); // Read/write, truncate
Windows Warning: Always Use Binary Mode!
On Windows, text mode converts \n to \r\n on write and vice versa on read.
For binary data, this corrupts bytes that happen to equal 0x0A or 0x0D. Always use "rb"
and "wb" for non-text files, even if your code runs on Linux (for portability).
When to Use Binary Files
Use Binary When
- Storing structs or arrays directly
- Working with images, audio, video
- Creating custom database files
- Performance is critical
- Exact numeric precision matters
Use Text When
- Humans need to read/edit the file
- Data must be portable across systems
- Working with config or log files
- Debugging requires file inspection
- Interoperability with other tools
Practice Questions
Task: Write a function that opens a binary file for reading with proper error handling and returns the FILE pointer.
Show Solution
#include <stdio.h>
FILE* openBinaryRead(const char *filename) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) {
fprintf(stderr, "Error: Cannot open '%s' for reading\n", filename);
perror("System error");
return NULL;
}
return fp;
}
int main() {
FILE *fp = openBinaryRead("data.bin");
if (fp != NULL) {
printf("File opened successfully!\n");
// Work with file...
fclose(fp);
}
return 0;
}
Task: Write a program that saves 1000 integers both as text and binary, then compares file sizes.
Show Solution
#include <stdio.h>
int main() {
int numbers[1000];
for (int i = 0; i < 1000; i++) {
numbers[i] = i * 1000; // Various sized numbers
}
// Write as text
FILE *textFile = fopen("numbers.txt", "w");
for (int i = 0; i < 1000; i++) {
fprintf(textFile, "%d\n", numbers[i]);
}
fclose(textFile);
// Write as binary
FILE *binFile = fopen("numbers.bin", "wb");
fwrite(numbers, sizeof(int), 1000, binFile);
fclose(binFile);
// Get file sizes
textFile = fopen("numbers.txt", "rb");
fseek(textFile, 0, SEEK_END);
long textSize = ftell(textFile);
fclose(textFile);
binFile = fopen("numbers.bin", "rb");
fseek(binFile, 0, SEEK_END);
long binSize = ftell(binFile);
fclose(binFile);
printf("Text file: %ld bytes\n", textSize);
printf("Binary file: %ld bytes\n", binSize);
printf("Savings: %.1f%%\n", 100.0 * (textSize - binSize) / textSize);
return 0;
}
Writing Binary Data with fwrite()
The fwrite() function writes raw binary data from memory directly to a file. Unlike fprintf() which converts data to text, fwrite() copies the exact bytes from your variables to the file. This makes it perfect for saving arrays, structs, and any data that needs to preserve its exact memory representation.
Understanding fwrite()
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
// Parameters:
// ptr - pointer to data to write
// size - size of each element in bytes
// count - number of elements to write
// stream - file to write to
// Returns:
// Number of elements successfully written (not bytes!)
// If return < count, an error occurred
fwrite()
fwrite() writes a block of data from memory to a file. It takes a pointer to the
data, the size of each element, the number of elements, and the file stream. It writes
size * count bytes total.
Key insight: fwrite() returns the number of elements written, not bytes.
Always compare the return value to count to check for errors.
Writing Basic Data Types
#include <stdio.h>
int main() {
FILE *fp = fopen("data.bin", "wb");
if (fp == NULL) {
perror("Cannot create file");
return 1;
}
// Write a single integer
int number = 42;
fwrite(&number, sizeof(int), 1, fp);
// Write a single double
double pi = 3.14159265359;
fwrite(&pi, sizeof(double), 1, fp);
// Write a character
char letter = 'A';
fwrite(&letter, sizeof(char), 1, fp);
fclose(fp);
printf("Data written: %zu bytes\n",
sizeof(int) + sizeof(double) + sizeof(char));
return 0;
}
Writing Arrays
fwrite() excels at writing entire arrays in one call, which is much faster than writing element by element:
#include <stdio.h>
int main() {
FILE *fp = fopen("scores.bin", "wb");
if (fp == NULL) return 1;
// Write an array of integers
int scores[] = {95, 87, 92, 78, 88, 91, 85, 90, 82, 94};
size_t count = sizeof(scores) / sizeof(scores[0]);
size_t written = fwrite(scores, sizeof(int), count, fp);
if (written != count) {
fprintf(stderr, "Error: Only wrote %zu of %zu elements\n",
written, count);
} else {
printf("Successfully wrote %zu integers (%zu bytes)\n",
count, count * sizeof(int));
}
fclose(fp);
return 0;
}
Writing Structs
One of the most powerful uses of fwrite() is saving entire structs directly to disk:
#include <stdio.h>
#include <string.h>
typedef struct {
int id;
char name[50];
float salary;
int department;
} Employee;
int main() {
FILE *fp = fopen("employees.bin", "wb");
if (fp == NULL) return 1;
// Create employee records
Employee emp1 = {101, "Alice Johnson", 75000.00, 1};
Employee emp2 = {102, "Bob Smith", 68000.00, 2};
Employee emp3 = {103, "Carol Davis", 82000.00, 1};
// Write each employee
fwrite(&emp1, sizeof(Employee), 1, fp);
fwrite(&emp2, sizeof(Employee), 1, fp);
fwrite(&emp3, sizeof(Employee), 1, fp);
printf("Wrote 3 employees (%zu bytes each)\n", sizeof(Employee));
printf("Total file size: %zu bytes\n", 3 * sizeof(Employee));
fclose(fp);
return 0;
}
Writing Arrays of Structs
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
int savePoints(const char *filename, Point points[], int count) {
FILE *fp = fopen(filename, "wb");
if (fp == NULL) return -1;
// Write count first (so we know how many to read later)
fwrite(&count, sizeof(int), 1, fp);
// Write all points in one call
size_t written = fwrite(points, sizeof(Point), count, fp);
fclose(fp);
return (written == count) ? count : -1;
}
int main() {
Point polygon[] = {
{0, 0}, {100, 0}, {100, 100}, {50, 150}, {0, 100}
};
int numPoints = sizeof(polygon) / sizeof(polygon[0]);
if (savePoints("polygon.bin", polygon, numPoints) > 0) {
printf("Saved %d points\n", numPoints);
}
return 0;
}
Struct Padding Warning
Compilers may add padding bytes to structs for alignment. A struct with char + int
might be 8 bytes, not 5. This is fine as long as you read with the same struct definition on
the same platform. For cross-platform files, consider using packed structs or manual serialization.
Practice Questions
Task: Write a program that saves an array of 5 high scores (integers) to a binary file.
Show Solution
#include <stdio.h>
int main() {
int highScores[] = {10000, 8500, 7200, 6800, 5500};
FILE *fp = fopen("highscores.bin", "wb");
if (fp == NULL) {
perror("Cannot create file");
return 1;
}
size_t written = fwrite(highScores, sizeof(int), 5, fp);
if (written == 5) {
printf("High scores saved successfully!\n");
} else {
printf("Error saving scores\n");
}
fclose(fp);
return 0;
}
Task: Create a Student struct with id, name, and gpa. Write an array of 3 students to a binary file.
Show Solution
#include <stdio.h>
#include <string.h>
typedef struct {
int id;
char name[30];
float gpa;
} Student;
int main() {
Student students[3] = {
{1001, "Alice Brown", 3.85},
{1002, "Bob Wilson", 3.42},
{1003, "Carol Lee", 3.91}
};
FILE *fp = fopen("students.bin", "wb");
if (fp == NULL) {
perror("Cannot create file");
return 1;
}
// Write number of students first
int count = 3;
fwrite(&count, sizeof(int), 1, fp);
// Write all students
size_t written = fwrite(students, sizeof(Student), 3, fp);
if (written == 3) {
printf("Saved %d students (%zu bytes)\n",
count, count * sizeof(Student));
}
fclose(fp);
return 0;
}
Task: Create a simple image file format with a header (magic number, width, height, bit depth) followed by pixel data.
Show Solution
#include <stdio.h>
#include <stdint.h>
typedef struct {
char magic[4]; // "IMG\0"
uint32_t width;
uint32_t height;
uint8_t bitDepth;
} ImageHeader;
int writeImage(const char *filename, int w, int h, uint8_t *pixels) {
FILE *fp = fopen(filename, "wb");
if (fp == NULL) return -1;
// Create and write header
ImageHeader header = {"IMG", w, h, 8};
fwrite(&header, sizeof(ImageHeader), 1, fp);
// Write pixel data (grayscale, 1 byte per pixel)
size_t pixelCount = w * h;
fwrite(pixels, sizeof(uint8_t), pixelCount, fp);
fclose(fp);
return 0;
}
int main() {
// Create a small 4x4 gradient image
uint8_t pixels[16] = {
0, 50, 100, 150,
50, 100, 150, 200,
100, 150, 200, 250,
150, 200, 250, 255
};
if (writeImage("test.img", 4, 4, pixels) == 0) {
printf("Image saved!\n");
}
return 0;
}
Reading Binary Data with fread()
The fread() function is the reading counterpart to fwrite(). It reads raw binary data from a file directly into memory, making it extremely fast for loading arrays, structs, and large datasets. Unlike fscanf() which parses text, fread() simply copies bytes from the file into your variables.
Understanding fread()
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
// Parameters:
// ptr - pointer to buffer where data will be stored
// size - size of each element in bytes
// count - number of elements to read
// stream - file to read from
// Returns:
// Number of elements successfully read
// If return < count, either EOF reached or error occurred
fread()
fread() reads a block of data from a file into a memory buffer. It reads up to
size * count bytes and returns the number of complete elements read.
Return value check: If fread() returns less than count, use
feof() to check if you hit end-of-file, or ferror() to check for errors.
Reading Basic Data Types
#include <stdio.h>
int main() {
FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) {
perror("Cannot open file");
return 1;
}
// Read a single integer
int number;
fread(&number, sizeof(int), 1, fp);
printf("Integer: %d\n", number);
// Read a single double
double value;
fread(&value, sizeof(double), 1, fp);
printf("Double: %.10f\n", value);
// Read a character
char letter;
fread(&letter, sizeof(char), 1, fp);
printf("Character: %c\n", letter);
fclose(fp);
return 0;
}
Reading Arrays
#include <stdio.h>
int main() {
FILE *fp = fopen("scores.bin", "rb");
if (fp == NULL) {
perror("Cannot open file");
return 1;
}
int scores[10];
size_t read = fread(scores, sizeof(int), 10, fp);
printf("Read %zu scores:\n", read);
for (size_t i = 0; i < read; i++) {
printf(" Score %zu: %d\n", i + 1, scores[i]);
}
fclose(fp);
return 0;
}
Reading Structs
#include <stdio.h>
typedef struct {
int id;
char name[50];
float salary;
int department;
} Employee;
int main() {
FILE *fp = fopen("employees.bin", "rb");
if (fp == NULL) {
perror("Cannot open file");
return 1;
}
Employee emp;
int count = 0;
// Read employees one by one until EOF
while (fread(&emp, sizeof(Employee), 1, fp) == 1) {
count++;
printf("Employee %d:\n", count);
printf(" ID: %d\n", emp.id);
printf(" Name: %s\n", emp.name);
printf(" Salary: $%.2f\n", emp.salary);
printf(" Dept: %d\n\n", emp.department);
}
printf("Total employees read: %d\n", count);
fclose(fp);
return 0;
}
Reading with Count Header
A common pattern is to store the count at the beginning of the file, then read that many records:
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int x;
int y;
} Point;
Point* loadPoints(const char *filename, int *outCount) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) {
*outCount = 0;
return NULL;
}
// Read the count first
int count;
if (fread(&count, sizeof(int), 1, fp) != 1) {
fclose(fp);
*outCount = 0;
return NULL;
}
// Allocate memory for points
Point *points = malloc(count * sizeof(Point));
if (points == NULL) {
fclose(fp);
*outCount = 0;
return NULL;
}
// Read all points
size_t read = fread(points, sizeof(Point), count, fp);
fclose(fp);
if (read != count) {
free(points);
*outCount = 0;
return NULL;
}
*outCount = count;
return points;
}
int main() {
int numPoints;
Point *polygon = loadPoints("polygon.bin", &numPoints);
if (polygon != NULL) {
printf("Loaded %d points:\n", numPoints);
for (int i = 0; i < numPoints; i++) {
printf(" (%d, %d)\n", polygon[i].x, polygon[i].y);
}
free(polygon);
}
return 0;
}
Checking for EOF vs Errors
#include <stdio.h>
int main() {
FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) return 1;
int value;
while (1) {
size_t read = fread(&value, sizeof(int), 1, fp);
if (read == 1) {
printf("Read: %d\n", value);
} else {
// Check why we stopped
if (feof(fp)) {
printf("Reached end of file\n");
} else if (ferror(fp)) {
printf("Error reading file\n");
}
break;
}
}
fclose(fp);
return 0;
}
Practice Questions
Task: Write a program that reads the high scores saved in Exercise 1 of fwrite and displays them.
Show Solution
#include <stdio.h>
int main() {
FILE *fp = fopen("highscores.bin", "rb");
if (fp == NULL) {
perror("Cannot open file");
return 1;
}
int highScores[5];
size_t read = fread(highScores, sizeof(int), 5, fp);
fclose(fp);
printf("=== HIGH SCORES ===\n");
for (size_t i = 0; i < read; i++) {
printf("%zu. %d\n", i + 1, highScores[i]);
}
return 0;
}
Task: Read the student records saved earlier and display them in a formatted table.
Show Solution
#include <stdio.h>
typedef struct {
int id;
char name[30];
float gpa;
} Student;
int main() {
FILE *fp = fopen("students.bin", "rb");
if (fp == NULL) {
perror("Cannot open file");
return 1;
}
// Read count
int count;
fread(&count, sizeof(int), 1, fp);
// Print header
printf("%-6s %-20s %s\n", "ID", "Name", "GPA");
printf("%-6s %-20s %s\n", "------", "--------------------", "----");
// Read and display students
Student student;
for (int i = 0; i < count; i++) {
if (fread(&student, sizeof(Student), 1, fp) == 1) {
printf("%-6d %-20s %.2f\n",
student.id, student.name, student.gpa);
}
}
fclose(fp);
return 0;
}
Task: Write a function that reads the image header from Exercise 3 and validates the magic number.
Show Solution
#include <stdio.h>
#include <stdint.h>
#include <string.h>
typedef struct {
char magic[4];
uint32_t width;
uint32_t height;
uint8_t bitDepth;
} ImageHeader;
int validateImage(const char *filename, ImageHeader *header) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) return -1;
// Read header
if (fread(header, sizeof(ImageHeader), 1, fp) != 1) {
fclose(fp);
return -2; // Read error
}
fclose(fp);
// Validate magic number
if (strncmp(header->magic, "IMG", 3) != 0) {
return -3; // Invalid magic
}
return 0; // Success
}
int main() {
ImageHeader header;
int result = validateImage("test.img", &header);
if (result == 0) {
printf("Valid image file!\n");
printf(" Width: %u\n", header.width);
printf(" Height: %u\n", header.height);
printf(" Bit Depth: %u\n", header.bitDepth);
printf(" Pixel Count: %u\n", header.width * header.height);
} else {
printf("Invalid or corrupted file (error %d)\n", result);
}
return 0;
}
File Positioning: fseek(), ftell(), rewind()
Unlike sequential text file reading, binary files often require jumping to specific positions - to read the 100th record, update a field in the middle of a file, or append data at a calculated offset. The file positioning functions give you complete control over where reading and writing occur.
Understanding fseek()
fseek() moves the file position indicator to any location in the file. You specify an offset and a starting point (beginning, current position, or end of file).
int fseek(FILE *stream, long offset, int whence);
// whence values:
// SEEK_SET - offset from beginning of file
// SEEK_CUR - offset from current position
// SEEK_END - offset from end of file (usually negative)
// Returns:
// 0 on success, non-zero on failure
| Operation | Code | Description |
|---|---|---|
| Go to start | fseek(fp, 0, SEEK_SET) |
Move to beginning of file |
| Skip 100 bytes | fseek(fp, 100, SEEK_SET) |
Move to byte 100 |
| Skip forward | fseek(fp, 50, SEEK_CUR) |
Move 50 bytes forward from current |
| Skip backward | fseek(fp, -20, SEEK_CUR) |
Move 20 bytes backward from current |
| Go to end | fseek(fp, 0, SEEK_END) |
Move to end of file |
| Last 10 bytes | fseek(fp, -10, SEEK_END) |
Move to 10 bytes before end |
Understanding ftell()
ftell() returns the current position in the file (as bytes from the beginning). It is commonly used to get file size or to save position before seeking elsewhere.
long ftell(FILE *stream);
// Returns:
// Current position (bytes from start), or -1L on error
Understanding rewind()
rewind() is a shortcut for seeking to the beginning. It also clears the error indicator.
void rewind(FILE *stream);
// Equivalent to:
// fseek(stream, 0, SEEK_SET);
// clearerr(stream);
Getting File Size
#include <stdio.h>
long getFileSize(const char *filename) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) return -1;
fseek(fp, 0, SEEK_END); // Go to end
long size = ftell(fp); // Get position (= size)
fclose(fp);
return size;
}
int main() {
long size = getFileSize("data.bin");
if (size >= 0) {
printf("File size: %ld bytes\n", size);
printf("That's %.2f KB\n", size / 1024.0);
}
return 0;
}
Random Access: Reading Specific Records
With fixed-size records (like structs), you can jump directly to any record by calculating its offset:
#include <stdio.h>
typedef struct {
int id;
char name[50];
float salary;
} Employee;
// Read employee at index n (0-based)
int readEmployee(FILE *fp, int index, Employee *emp) {
// Calculate offset: header + (index * record_size)
long offset = index * sizeof(Employee);
if (fseek(fp, offset, SEEK_SET) != 0) {
return -1; // Seek failed
}
if (fread(emp, sizeof(Employee), 1, fp) != 1) {
return -2; // Read failed
}
return 0; // Success
}
int main() {
FILE *fp = fopen("employees.bin", "rb");
if (fp == NULL) return 1;
Employee emp;
// Read employee at index 2 (third employee)
if (readEmployee(fp, 2, &emp) == 0) {
printf("Employee #3: %s (ID: %d)\n", emp.name, emp.id);
}
// Read employee at index 0 (first employee)
if (readEmployee(fp, 0, &emp) == 0) {
printf("Employee #1: %s (ID: %d)\n", emp.name, emp.id);
}
fclose(fp);
return 0;
}
Updating Records In-Place
A powerful feature of binary files is modifying data without rewriting the entire file:
#include <stdio.h>
typedef struct {
int id;
char name[50];
float salary;
} Employee;
int updateSalary(const char *filename, int index, float newSalary) {
FILE *fp = fopen(filename, "rb+"); // Read/write mode
if (fp == NULL) return -1;
// Seek to the salary field of the target record
long recordOffset = index * sizeof(Employee);
long salaryOffset = recordOffset + sizeof(int) + 50; // Skip id and name
if (fseek(fp, salaryOffset, SEEK_SET) != 0) {
fclose(fp);
return -2;
}
// Write new salary
if (fwrite(&newSalary, sizeof(float), 1, fp) != 1) {
fclose(fp);
return -3;
}
fclose(fp);
return 0;
}
int main() {
// Give employee at index 1 a raise
if (updateSalary("employees.bin", 1, 72000.00) == 0) {
printf("Salary updated successfully!\n");
}
return 0;
}
Pro Tip: Safer Record Updates
Instead of calculating field offsets manually (which is error-prone), read the entire struct, modify it in memory, seek back, and write the whole struct. It is slightly less efficient but much safer and easier to maintain.
Practice Questions
Task: Write a function that calculates how many Employee records are in a binary file by using file size.
Show Solution
#include <stdio.h>
typedef struct {
int id;
char name[50];
float salary;
} Employee;
int countRecords(const char *filename) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) return -1;
// Get file size
fseek(fp, 0, SEEK_END);
long fileSize = ftell(fp);
fclose(fp);
// Calculate record count
return fileSize / sizeof(Employee);
}
int main() {
int count = countRecords("employees.bin");
if (count >= 0) {
printf("File contains %d employee records\n", count);
} else {
printf("Cannot open file\n");
}
return 0;
}
Task: Write a function that reads the last N records from a binary file of integers.
Show Solution
#include <stdio.h>
#include <stdlib.h>
int* readLastN(const char *filename, int n, int *actualCount) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) {
*actualCount = 0;
return NULL;
}
// Get total count
fseek(fp, 0, SEEK_END);
long fileSize = ftell(fp);
int totalRecords = fileSize / sizeof(int);
// Adjust n if file has fewer records
if (n > totalRecords) n = totalRecords;
// Allocate result array
int *result = malloc(n * sizeof(int));
if (result == NULL) {
fclose(fp);
*actualCount = 0;
return NULL;
}
// Seek to start of last n records
long offset = (totalRecords - n) * sizeof(int);
fseek(fp, offset, SEEK_SET);
// Read records
*actualCount = fread(result, sizeof(int), n, fp);
fclose(fp);
return result;
}
int main() {
int count;
int *lastFive = readLastN("numbers.bin", 5, &count);
if (lastFive != NULL) {
printf("Last %d numbers:\n", count);
for (int i = 0; i < count; i++) {
printf(" %d\n", lastFive[i]);
}
free(lastFive);
}
return 0;
}
Task: Write a function that swaps two records at given indices in a binary file without loading the entire file.
Show Solution
#include <stdio.h>
typedef struct {
int id;
char name[50];
float salary;
} Employee;
int swapRecords(const char *filename, int idx1, int idx2) {
FILE *fp = fopen(filename, "rb+");
if (fp == NULL) return -1;
Employee emp1, emp2;
// Read first record
fseek(fp, idx1 * sizeof(Employee), SEEK_SET);
if (fread(&emp1, sizeof(Employee), 1, fp) != 1) {
fclose(fp);
return -2;
}
// Read second record
fseek(fp, idx2 * sizeof(Employee), SEEK_SET);
if (fread(&emp2, sizeof(Employee), 1, fp) != 1) {
fclose(fp);
return -3;
}
// Write first record to second position
fseek(fp, idx2 * sizeof(Employee), SEEK_SET);
fwrite(&emp1, sizeof(Employee), 1, fp);
// Write second record to first position
fseek(fp, idx1 * sizeof(Employee), SEEK_SET);
fwrite(&emp2, sizeof(Employee), 1, fp);
fclose(fp);
return 0;
}
int main() {
if (swapRecords("employees.bin", 0, 2) == 0) {
printf("Records swapped successfully!\n");
}
return 0;
}
Practical Binary File Patterns
Now let us combine everything into practical patterns you will use in real applications. These patterns cover common scenarios like creating simple databases, implementing save/load functionality for games or applications, and working with file headers for format validation.
Pattern 1: Simple Database with Header
A robust binary file format includes a header with metadata (version, record count, etc.):
#include <stdio.h>
#include <string.h>
#define MAGIC "EMPDB"
#define VERSION 1
typedef struct {
char magic[6];
int version;
int recordCount;
} DatabaseHeader;
typedef struct {
int id;
char name[50];
float salary;
int active;
} Employee;
int createDatabase(const char *filename) {
FILE *fp = fopen(filename, "wb");
if (fp == NULL) return -1;
DatabaseHeader header = {MAGIC, VERSION, 0};
fwrite(&header, sizeof(DatabaseHeader), 1, fp);
fclose(fp);
return 0;
}
int addEmployee(const char *filename, Employee *emp) {
FILE *fp = fopen(filename, "rb+");
if (fp == NULL) return -1;
// Read and validate header
DatabaseHeader header;
fread(&header, sizeof(DatabaseHeader), 1, fp);
if (strcmp(header.magic, MAGIC) != 0) {
fclose(fp);
return -2; // Invalid file
}
// Seek to end and write new record
fseek(fp, 0, SEEK_END);
fwrite(emp, sizeof(Employee), 1, fp);
// Update record count in header
header.recordCount++;
fseek(fp, 0, SEEK_SET);
fwrite(&header, sizeof(DatabaseHeader), 1, fp);
fclose(fp);
return header.recordCount;
}
int main() {
createDatabase("company.db");
Employee emp1 = {101, "Alice Johnson", 75000, 1};
Employee emp2 = {102, "Bob Smith", 68000, 1};
addEmployee("company.db", &emp1);
addEmployee("company.db", &emp2);
printf("Database created with 2 employees\n");
return 0;
}
Pattern 2: Game Save/Load System
#include <stdio.h>
#include <string.h>
#include <time.h>
#define SAVE_MAGIC "GSAV"
#define SAVE_VERSION 2
typedef struct {
char magic[5];
int version;
time_t saveTime;
} SaveHeader;
typedef struct {
char playerName[32];
int level;
int health;
int score;
float posX, posY;
int inventory[10];
} GameState;
int saveGame(const char *filename, GameState *state) {
FILE *fp = fopen(filename, "wb");
if (fp == NULL) return -1;
// Write header
SaveHeader header;
strcpy(header.magic, SAVE_MAGIC);
header.version = SAVE_VERSION;
header.saveTime = time(NULL);
fwrite(&header, sizeof(SaveHeader), 1, fp);
// Write game state
fwrite(state, sizeof(GameState), 1, fp);
fclose(fp);
return 0;
}
int loadGame(const char *filename, GameState *state) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) return -1;
// Read and validate header
SaveHeader header;
if (fread(&header, sizeof(SaveHeader), 1, fp) != 1) {
fclose(fp);
return -2;
}
if (strcmp(header.magic, SAVE_MAGIC) != 0) {
fclose(fp);
return -3; // Invalid save file
}
if (header.version != SAVE_VERSION) {
fclose(fp);
return -4; // Version mismatch
}
// Read game state
if (fread(state, sizeof(GameState), 1, fp) != 1) {
fclose(fp);
return -5;
}
fclose(fp);
return 0;
}
int main() {
// Create game state
GameState state = {
"Hero123", 15, 85, 12500,
156.5, 89.2,
{1, 3, 0, 5, 2, 0, 0, 1, 0, 4}
};
// Save game
if (saveGame("save1.dat", &state) == 0) {
printf("Game saved!\n");
}
// Load game
GameState loaded;
if (loadGame("save1.dat", &loaded) == 0) {
printf("Loaded: %s, Level %d, Score %d\n",
loaded.playerName, loaded.level, loaded.score);
}
return 0;
}
Pattern 3: Indexed Record Access
For larger databases, maintain a separate index for fast lookups:
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int id;
long fileOffset; // Where this record is in the data file
} IndexEntry;
typedef struct {
int id;
char name[50];
char email[100];
float balance;
} Customer;
// Find record by ID using index
Customer* findCustomer(const char *dataFile, const char *indexFile, int id) {
FILE *idx = fopen(indexFile, "rb");
if (idx == NULL) return NULL;
IndexEntry entry;
long targetOffset = -1;
// Search index for matching ID
while (fread(&entry, sizeof(IndexEntry), 1, idx) == 1) {
if (entry.id == id) {
targetOffset = entry.fileOffset;
break;
}
}
fclose(idx);
if (targetOffset < 0) return NULL; // Not found
// Read customer from data file
FILE *data = fopen(dataFile, "rb");
if (data == NULL) return NULL;
Customer *customer = malloc(sizeof(Customer));
if (customer == NULL) {
fclose(data);
return NULL;
}
fseek(data, targetOffset, SEEK_SET);
if (fread(customer, sizeof(Customer), 1, data) != 1) {
free(customer);
fclose(data);
return NULL;
}
fclose(data);
return customer;
}
int main() {
Customer *c = findCustomer("customers.dat", "customers.idx", 1005);
if (c != NULL) {
printf("Found: %s (%s)\n", c->name, c->email);
printf("Balance: $%.2f\n", c->balance);
free(c);
} else {
printf("Customer not found\n");
}
return 0;
}
Pattern 4: Binary Configuration File
#include <stdio.h>
#include <string.h>
typedef struct {
// Display settings
int screenWidth;
int screenHeight;
int fullscreen;
// Audio settings
int masterVolume;
int musicVolume;
int sfxVolume;
// Game settings
int difficulty;
int showTutorials;
char language[16];
} AppConfig;
// Default configuration
AppConfig getDefaultConfig() {
AppConfig config = {
1920, 1080, 0, // Display
80, 70, 100, // Audio
1, 1, "English" // Game
};
return config;
}
int saveConfig(const char *filename, AppConfig *config) {
FILE *fp = fopen(filename, "wb");
if (fp == NULL) return -1;
fwrite(config, sizeof(AppConfig), 1, fp);
fclose(fp);
return 0;
}
int loadConfig(const char *filename, AppConfig *config) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) {
// File does not exist, use defaults
*config = getDefaultConfig();
return 1; // Using defaults
}
if (fread(config, sizeof(AppConfig), 1, fp) != 1) {
fclose(fp);
*config = getDefaultConfig();
return 2; // Read error, using defaults
}
fclose(fp);
return 0; // Loaded successfully
}
int main() {
AppConfig config;
int result = loadConfig("settings.bin", &config);
if (result == 0) {
printf("Settings loaded from file\n");
} else {
printf("Using default settings\n");
}
printf("Resolution: %dx%d\n", config.screenWidth, config.screenHeight);
printf("Volume: %d%%\n", config.masterVolume);
// Modify and save
config.masterVolume = 90;
saveConfig("settings.bin", &config);
return 0;
}
Portability Considerations
Binary files written on one system may not be readable on another due to differences in byte order (endianness), struct padding, and type sizes. For cross-platform files, consider using a standardized format, explicit byte ordering, or text-based formats like JSON.
Practice Questions
Task: Create functions to save and load a todo list (array of tasks with description and completed flag).
Show Solution
#include <stdio.h>
typedef struct {
char description[100];
int completed;
} Task;
int saveTodoList(const char *filename, Task tasks[], int count) {
FILE *fp = fopen(filename, "wb");
if (fp == NULL) return -1;
// Write count first
fwrite(&count, sizeof(int), 1, fp);
// Write all tasks
fwrite(tasks, sizeof(Task), count, fp);
fclose(fp);
return count;
}
int loadTodoList(const char *filename, Task tasks[], int maxTasks) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) return -1;
int count;
fread(&count, sizeof(int), 1, fp);
if (count > maxTasks) count = maxTasks;
fread(tasks, sizeof(Task), count, fp);
fclose(fp);
return count;
}
int main() {
Task myTasks[3] = {
{"Learn C file handling", 1},
{"Build a project", 0},
{"Practice more", 0}
};
saveTodoList("todos.bin", myTasks, 3);
Task loaded[10];
int count = loadTodoList("todos.bin", loaded, 10);
printf("Loaded %d tasks:\n", count);
for (int i = 0; i < count; i++) {
printf("[%c] %s\n",
loaded[i].completed ? 'X' : ' ',
loaded[i].description);
}
return 0;
}
Task: Create a binary log format that stores timestamped events. Write functions to append events and read events within a time range.
Show Solution
#include <stdio.h>
#include <time.h>
#include <string.h>
typedef struct {
time_t timestamp;
int level; // 0=INFO, 1=WARN, 2=ERROR
char message[200];
} LogEntry;
int appendLog(const char *filename, int level, const char *msg) {
FILE *fp = fopen(filename, "ab");
if (fp == NULL) return -1;
LogEntry entry;
entry.timestamp = time(NULL);
entry.level = level;
strncpy(entry.message, msg, 199);
entry.message[199] = '\0';
fwrite(&entry, sizeof(LogEntry), 1, fp);
fclose(fp);
return 0;
}
int readLogsInRange(const char *filename, time_t start, time_t end) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) return -1;
const char *levels[] = {"INFO", "WARN", "ERROR"};
LogEntry entry;
int count = 0;
while (fread(&entry, sizeof(LogEntry), 1, fp) == 1) {
if (entry.timestamp >= start && entry.timestamp <= end) {
char timeStr[26];
ctime_r(&entry.timestamp, timeStr);
timeStr[24] = '\0';
printf("[%s] [%s] %s\n",
timeStr, levels[entry.level], entry.message);
count++;
}
}
fclose(fp);
return count;
}
int main() {
// Write some logs
appendLog("app.binlog", 0, "Application started");
appendLog("app.binlog", 0, "User logged in");
appendLog("app.binlog", 1, "High memory usage");
appendLog("app.binlog", 2, "Connection timeout");
// Read all logs from last hour
time_t now = time(NULL);
time_t oneHourAgo = now - 3600;
printf("=== Logs from last hour ===\n");
readLogsInRange("app.binlog", oneHourAgo, now);
return 0;
}
Key Takeaways
Binary vs Text
Binary files store raw bytes (compact, fast), text files store characters (human-readable, portable)
fwrite() for Output
Write raw bytes directly from memory - perfect for arrays and structs, returns elements written
fread() for Input
Read raw bytes directly into memory - check return value and use feof()/ferror() for diagnostics
File Positioning
Use fseek() to jump to any position, ftell() to get current position, rewind() to go to start
Random Access
With fixed-size records, calculate offset = index * sizeof(record) for direct access
Use File Headers
Include magic numbers and version info for file validation and forward compatibility
Knowledge Check
Quick Quiz
Test what you have learned about C binary file operations