Hello! Welcome to your seventh lesson in our C programming module.
In our last lesson, we established a crucial skill for large projects: organizing code into multiple source (.c) and header (.h) files. This practice of creating distinct modules is the backbone of any scalable software, including the game engine we're aiming to build.
Today, we will apply that new skill by creating our first utility module. The learning outcome for this lesson is to read from and write to files using C standard library functions. This ability is fundamental for a game engine, as it's how we'll load game assets like 3D models and textures, read configuration settings, and implement save game systems.
Text vs. Binary Files
All data on a computer is stored as bytes (sequences of 0s and 1s), but we can interpret those bytes in different ways. In C file I/O, we primarily distinguish between two types of files:
- Text Files: These files store data as human-readable characters. When you save the number
123, it's stored as three characters: '1', '2', and '3'. These are great for configuration files or logs that a person might need to read or edit. - Binary Files: These files store data using the same byte representation that your C program uses in memory. The integer
123is stored directly as a 4-byte (on most systems) integer value. This is far more efficient in terms of space and speed for large or complex data, making it ideal for game assets and save data.

The Core of File Handling in C
Regardless of the file type, the process in C follows the same three steps:
- Open the file: You get a "file handle" from the operating system, which is a pointer to a
FILEstruct. - Read from or write to the file: You use various functions to transfer data between your program's memory and the file.
- Close the file: You release the file handle, which ensures any buffered data is written and resources are freed.
Let's start by looking at how to handle text files.
Working with Text Files
Text files are convenient for data that you or the player might want to edit by hand, like settings or simple level data.
The main functions for opening and closing files are fopen() and fclose(). fopen() takes a file path and a "mode" string as arguments. The mode specifies your intent (reading, writing, etc.).
For a handy reference on all the possible modes, please consult the table in the following resource.
C Files I/O: Opening, Reading, Writing and Closing a file
The Programiz article 'C Files I/O' has an excellent table summarizing the different file opening modes. You don't need to read the whole article right now, just familiarize yourself with the table.
Scroll down to the table under the section 'Opening Modes in Standard I/O'. Pay attention to the differences between r, w, and a (read, write, append).
A crucial safety check is to always verify that fopen() did not return NULL, which indicates an error (e.g., file not found, permissions error).
The following video will walk you through the entire process of writing to and reading from text files.
File Access Basics | C Programming Tutorial
The video 'File Access Basics' by Portfolio Courses provides a practical introduction to text file I/O in C.
Watch from the beginning until 19:46. The video covers: Opening and Writing (0:00 - 7:53): Using fopen() with 'w' (write) and 'a' (append) modes, and writing data with fputs() and fprintf(). Iterative Writing (7:53 - 10:48): Using a loop to write multiple pieces of data, a common pattern. Reading Formatted Data (10:48 - 19:46): Using fscanf() in a loop to read structured data until the end of the file (EOF) is reached.
Let's summarize the key functions from that video:
fprintf(FILE *stream, const char *format, ...): Works just likeprintf, but the first argument is the file pointer to write to. This is perfect for writing formatted, human-readable data.fscanf(FILE *stream, const char *format, ...): The file-based counterpart toscanf. It reads from a file, trying to match the format string. It's useful for simple, well-defined structures but can be brittle if the file format isn't perfect.fgets(char *str, int n, FILE *stream): A safer way to read from text files. It reads one line of text (up ton-1characters) into a character arraystr. It's generally preferred overfscanffor reading string data as it handles spaces and line breaks more predictably. The video also touches on this function near the end (not in the assigned segment, but good to know it exists).
Working with Binary Files
For game development, most of your file I/O will be binary. Storing a 3D model with thousands of vertices or a complex player save state in a text file would be incredibly slow and inefficient. Binary I/O treats data as a raw block of bytes, copying it directly between your program's memory and a file.
This has two major benefits:
- Size: Storing a number like 1,000,000,000 takes 4 bytes as a binary
int, but 10 bytes as a text string ("1000000000"). - Speed: No conversion between the internal data representation and a character string is needed. This is a significant performance win.
The next video explains these benefits and demonstrates how to use the primary functions for binary I/O: fwrite() and fread().
Binary File Access Introduction | C Programming Example
This video, also from Portfolio Courses, is an excellent introduction to binary file I/O. It clearly explains the 'why' and then shows the 'how'.
Watch the entire video. It covers: What Binary Files Are (0:00 - 2:42): Explains the concepts of size and performance benefits. Writing with fwrite (2:42 - 5:49): Demonstrates how to open a file in binary write mode (wb) and write an array of data. Reading with fread (5:49 - 7:48): Shows the corresponding process for reading the data back into memory. Performance Comparison (7:48 - End): A practical test showing how much faster binary I/O is than text I/O for large datasets. This really drives home why it's essential for games.
The syntax for fwrite and fread is a bit different from the text functions and is crucial to get right:
-
fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);ptr: A pointer to the data you want to write (e.g., the address of yourstructor array).size: The size in bytes of a single element you are writing (e.g.,sizeof(int)orsizeof(struct Player)).nmemb: The number of elements you are writing.stream: The file pointer.- The total bytes written will be
size * nmemb.
-
fread(void *ptr, size_t size, size_t nmemb, FILE *stream);- The arguments are identical, but
ptris now a pointer to the memory location where the read data will be stored.
- The arguments are identical, but
Your Turn: A Practical Exercise
Let's apply these concepts. Imagine we're creating a simple configuration system for our game. We want to save and load a player's settings.
Following the principles from our last lesson, you would create a config.h and config.c.
config.h
#ifndef CONFIG_H
#define CONFIG_H
// A struct to hold our game configuration
typedef struct {
char player_name[50];
int difficulty; // 0=Easy, 1=Normal, 2=Hard
float music_volume;
} GameConfig;
// Saves the config to a human-readable text file
int save_config_text(const char* filename, const GameConfig* config);
// Loads the config from a text file
int load_config_text(const char* filename, GameConfig* config);
// Saves the config to a compact binary file
int save_config_binary(const char* filename, const GameConfig* config);
// Loads the config from a binary file
int load_config_binary(const char* filename, GameConfig* config);
#endif // CONFIG_H
Your task is to think about how you would implement the four functions in config.c. You don't have to write and compile the full code, but sketch out the key lines inside each function.
save_config_text: Which function would you use to write theplayer_name,difficulty, andmusic_volumein a readable way?load_config_text: How would you read that data back? What function would be best suited for this structured data?save_config_binary: Which function would you use to write the entireGameConfigstruct to a file in one go? What would thesizeandnmembarguments be?load_config_binary: What would the corresponding read call look like?
Take a few minutes to think this through. The answer is below.
Click here to see a possible implementation
config.c
#include <stdio.h>
#include "config.h"
int save_config_text(const char* filename, const GameConfig* config) {
FILE* file = fopen(filename, "w"); // "w" for write text
if (file == NULL) { return -1; }
fprintf(file, "name: %s\n", config->player_name);
fprintf(file, "difficulty: %d\n", config->difficulty);
fprintf(file, "volume: %f\n", config->music_volume);
fclose(file);
return 0;
}
int load_config_text(const char* filename, GameConfig* config) {
FILE* file = fopen(filename, "r"); // "r" for read text
if (file == NULL) { return -1; }
// fscanf is okay here, but notice it's sensitive to the format.
// A more robust solution might use fgets and parse the lines.
fscanf(file, "name: %s\n", config->player_name);
fscanf(file, "difficulty: %d\n", &config->difficulty);
fscanf(file, "volume: %f\n", &config->music_volume);
fclose(file);
return 0;
}
int save_config_binary(const char* filename, const GameConfig* config) {
FILE* file = fopen(filename, "wb"); // "wb" for write binary
if (file == NULL) { return -1; }
// Write 1 element of size sizeof(GameConfig)
fwrite(config, sizeof(GameConfig), 1, file);
fclose(file);
return 0;
}
int load_config_binary(const char* filename, GameConfig* config) {
FILE* file = fopen(filename, "rb"); // "rb" for read binary
if (file == NULL) { return -1; }
// Read 1 element of size sizeof(GameConfig)
fread(config, sizeof(GameConfig), 1, file);
fclose(file);
return 0;
}
Conclusion
You have now learned one of the most essential I/O operations in C. Being able to persist data is the gateway to handling almost any external asset your game will need.
Key Takeaways:
- File I/O in C involves opening, reading/writing, and closing a file using a
FILEpointer. - Text files are human-readable and are best for configs or logs. You primarily use
fprintfandfscanf/fgets. - Binary files are machine-readable, smaller, and faster. They are essential for game assets and save data. You use
fwriteandfreadto copy blocks of memory directly. - Always check the return value of
fopen()forNULLto handle errors gracefully. - Always
fclose()a file when you are done with it to free up system resources.
In the last two lessons, you've learned to organize a C project and create a file I/O module. You may have noticed that compiling and linking multiple files by hand is getting tedious. In our next and final lesson of this module, we will address that by learning about the roles of a compiler, linker, and a build system, which automates this process for you.