Chapter 9: File I/O: Interacting with Files
Most programs need to interact with the outside world, and often this means reading data from or writing data to files on a storage device (like an SSD or hard drive). This allows your programs to store persistent data, process large datasets, or communicate with other applications.
In C, file input/output (I/O) is handled through a set of standard library functions declared in <stdio.h>. This chapter will cover:
- Opening and closing files.
- Reading and writing data in text mode.
- Reading and writing data in binary mode.
- Handling file errors.
- Positioning within a file.
9.1 The FILE Pointer and File Operations
All file operations in C revolve around a special type: the FILE pointer. When you open a file, the fopen() function returns a pointer to a FILE structure, which contains all the necessary information about the file (its name, mode, buffer, current position, etc.). You use this FILE * to refer to the file in subsequent operations.
The basic workflow for file I/O is:
- Open the file using
fopen(). - Perform read/write operations.
- Close the file using
fclose().
9.1.1 fopen(): Opening a File
fopen() is used to open a file in a specified mode.
Syntax:
FILE *fopen(const char *filename, const char *mode);
filename: A string (path) specifying the name of the file to open.mode: A string specifying how the file should be opened.
Common File Modes:
| Mode | Description |
|---|---|
"r" | Read mode. Opens an existing file for reading. Returns NULL if the file doesn’t exist. |
"w" | Write mode. Opens a file for writing. If the file exists, its content is truncated (deleted). If it doesn’t exist, a new one is created. |
"a" | Append mode. Opens a file for writing at the end of the file. If the file doesn’t exist, a new one is created. |
"r+" | Read and Write. Opens an existing file for both reading and writing. |
"w+" | Read and Write. Creates a new file for reading and writing. If the file exists, its content is truncated. |
"a+" | Read and Write. Opens for reading and appending. If the file doesn’t exist, a new one is created. |
Binary Modes: To open a file in binary mode, append b to the mode string (e.g., "rb", "wb", "ab"). Binary mode handles data as raw bytes, without any special translation of characters like newline characters (which can be \n or \r\n depending on the OS in text mode).
Return Value: fopen() returns a FILE * on success, or NULL if the file could not be opened (e.g., file not found, permission denied). Always check for NULL!
9.1.2 fclose(): Closing a File
fclose() closes the file associated with the FILE * pointer, flushing any buffered data to disk and releasing system resources.
Syntax:
int fclose(FILE *stream);
stream: TheFILE *pointer returned byfopen().- Returns
0on success,EOF(End Of File, usually -1) on error.
9.2 Text File I/O
For human-readable data, text file I/O uses functions that interpret data as characters and perform conversions (e.g., between integer values and their string representations).
9.2.1 fprintf(): Formatted Output to File
Similar to printf(), but writes formatted data to a specified FILE *.
Syntax:
int fprintf(FILE *stream, const char *format, ...);
Example:
#include <stdio.h>
int main() {
FILE *file_ptr;
char name[] = "Alice";
int age = 30;
float height = 1.65f;
// Open file "person.txt" in write mode ("w")
file_ptr = fopen("person.txt", "w");
// Check for errors
if (file_ptr == NULL) {
perror("Error opening file for writing"); // Prints "Error opening file for writing: <system error message>"
return 1;
}
// Write formatted data to the file
fprintf(file_ptr, "Name: %s\n", name);
fprintf(file_ptr, "Age: %d\n", age);
fprintf(file_ptr, "Height: %.2f meters\n", height);
printf("Data written to person.txt successfully.\n");
fclose(file_ptr); // Close the file
return 0;
}
9.2.2 fscanf(): Formatted Input from File
Similar to scanf(), but reads formatted data from a specified FILE *.
Syntax:
int fscanf(FILE *stream, const char *format, ...);
Example (reading person.txt created above):
#include <stdio.h>
int main() {
FILE *file_ptr;
char name[50];
int age;
float height;
char buffer[100]; // To read and discard labels like "Name:", "Age:"
// Open file "person.txt" in read mode ("r")
file_ptr = fopen("person.txt", "r");
if (file_ptr == NULL) {
perror("Error opening file for reading");
return 1;
}
// Read formatted data from the file
// Note: We read the label ("Name:") into buffer, then the actual name.
// The space in format string allows skipping whitespace.
fscanf(file_ptr, "%s %s\n", buffer, name); // Reads "Name:" then "Alice"
fscanf(file_ptr, "%s %d\n", buffer, &age); // Reads "Age:" then 30
fscanf(file_ptr, "%s %f %s\n", buffer, &height, buffer); // Reads "Height:", 1.65, "meters"
printf("Data read from person.txt:\n");
printf("Name: %s\n", name);
printf("Age: %d\n", age);
printf("Height: %.2f meters\n", height);
fclose(file_ptr);
return 0;
}
Caution with fscanf(): It can be tricky to use fscanf() reliably for arbitrary text files, especially if the format isn’t strictly controlled. It’s often safer to read entire lines and then parse them.
9.2.3 fgets(): Reading a Line from File
Reads a whole line (up to n-1 characters or until a newline \n or EOF) into a buffer.
Syntax:
char *fgets(char *buffer, int n, FILE *stream);
buffer: Pointer to the character array where the line will be stored.n: The maximum number of characters to read (including the null terminator\0).stream: TheFILE *pointer.- Returns
bufferon success, orNULLon error or EOF.
Example:
#include <stdio.h>
#include <stdlib.h> // For EXIT_SUCCESS, EXIT_FAILURE
int main() {
FILE *file_ptr;
char line[100]; // Buffer to store each line
file_ptr = fopen("sample.txt", "w"); // Create a sample file first
if (file_ptr == NULL) { perror("Error creating sample.txt"); return EXIT_FAILURE; }
fprintf(file_ptr, "First line of text.\n");
fprintf(file_ptr, "Second line here.\n");
fprintf(file_ptr, "Third and final line.\n");
fclose(file_ptr);
file_ptr = fopen("sample.txt", "r");
if (file_ptr == NULL) {
perror("Error opening sample.txt for reading");
return EXIT_FAILURE;
}
printf("Content of sample.txt:\n");
while (fgets(line, sizeof(line), file_ptr) != NULL) {
printf("%s", line); // Print the line (it already contains \n)
}
// Check if loop terminated due to error or EOF
if (ferror(file_ptr)) { // Check if an error occurred
perror("Error reading file");
}
fclose(file_ptr);
return EXIT_SUCCESS;
}
9.2.4 fputs(): Writing a String to File
Writes a string to a file. It does not append a newline character automatically.
Syntax:
int fputs(const char *str, FILE *stream);
str: The string to write.stream: TheFILE *pointer.- Returns a non-negative value on success,
EOFon error.
Example:
#include <stdio.h>
int main() {
FILE *file_ptr;
char text1[] = "Hello C File I/O!\n";
char text2[] = "This is another line.\n";
file_ptr = fopen("output.txt", "w");
if (file_ptr == NULL) { perror("Error opening output.txt"); return 1; }
fputs(text1, file_ptr); // Write text1 (includes newline)
fputs(text2, file_ptr); // Write text2 (includes newline)
fputs("A final message.", file_ptr); // This line won't have a newline unless added
fclose(file_ptr);
printf("Data written to output.txt.\n");
return 0;
}
9.3 Binary File I/O
Binary files store data exactly as it’s represented in memory, byte by byte. This is efficient for numerical data, custom data structures, images, audio, etc., and avoids text conversions.
9.3.1 fread(): Reading Binary Data
Reads count items, each of size bytes, from stream into ptr.
Syntax:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
ptr: Pointer to the memory block where the data will be stored.size: Size of each item to read (in bytes). Usesizeof().count: Number of items to read.stream: TheFILE *pointer (opened in binary read mode, e.g.,"rb").- Returns the number of items successfully read. Can be less than
countif EOF or error.
9.3.2 fwrite(): Writing Binary Data
Writes count items, each of size bytes, from ptr to stream.
Syntax:
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
ptr: Pointer to the memory block from where the data will be read.size: Size of each item to write (in bytes). Usesizeof().count: Number of items to write.stream: TheFILE *pointer (opened in binary write mode, e.g.,"wb").- Returns the number of items successfully written. Can be less than
countif error.
Example: Writing and Reading a struct in Binary Mode
#include <stdio.h>
#include <stdlib.h> // For malloc, free
#include <string.h> // For strcpy
// Define a struct to store data
typedef struct {
int id;
char name[50];
float value;
} Item;
int main() {
FILE *file_ptr;
Item item1 = {101, "Laptop", 1200.50f};
Item item2 = {102, "Mouse", 25.99f};
Item read_item; // To store read data
// --- Write to binary file ---
file_ptr = fopen("items.bin", "wb"); // Open in binary write mode
if (file_ptr == NULL) { perror("Error opening items.bin for writing"); return 1; }
fwrite(&item1, sizeof(Item), 1, file_ptr); // Write item1
fwrite(&item2, sizeof(Item), 1, file_ptr); // Write item2
printf("Wrote 2 items to items.bin.\n");
fclose(file_ptr);
// --- Read from binary file ---
file_ptr = fopen("items.bin", "rb"); // Open in binary read mode
if (file_ptr == NULL) { perror("Error opening items.bin for reading"); return 1; }
printf("Reading items from items.bin:\n");
while (fread(&read_item, sizeof(Item), 1, file_ptr) == 1) {
printf("ID: %d, Name: %s, Value: %.2f\n",
read_item.id, read_item.name, read_item.value);
}
// Check for read errors other than EOF
if (ferror(file_ptr)) {
perror("Error reading from items.bin");
}
fclose(file_ptr);
return 0;
}
Portability Note: Writing structures directly to binary files can sometimes lead to portability issues across different systems or compilers due to variations in structure padding and byte order (endianness). For maximum portability, consider writing individual members one by one or using standardized serialization formats.
9.4 File Positioning: fseek(), ftell(), rewind()
Sometimes you need to move to a specific location within a file to read or write.
fseek(): Sets the file position indicator for the stream. Syntax:int fseek(FILE *stream, long offset, int origin);stream: TheFILE *pointer.offset: Number of bytes to move from theorigin. Can be positive (forward) or negative (backward).origin: Position from whichoffsetis measured:SEEK_SET(or0): Beginning of the file.SEEK_CUR(or1): Current file position.SEEK_END(or2): End of the file.
- Returns
0on success, non-zero on error.
ftell(): Returns the current file position indicator (offset from the beginning of the file). Syntax:long ftell(FILE *stream);- Returns the current position, or
-1Lon error.
- Returns the current position, or
rewind(): Sets the file position indicator to the beginning of the file. Syntax:void rewind(FILE *stream);- Equivalent to
fseek(stream, 0L, SEEK_SET);but also clears the error indicator.
- Equivalent to
Example:
#include <stdio.h>
int main() {
FILE *file_ptr;
char buffer[100];
file_ptr = fopen("sample_text.txt", "w+"); // Open for read/write, create if not exist
if (file_ptr == NULL) { perror("Error opening sample_text.txt"); return 1; }
fprintf(file_ptr, "Hello, World!\nThis is line two.\n");
// After writing, the file position is at the end.
long current_pos = ftell(file_ptr);
printf("Current position after writing: %ld bytes\n", current_pos); // Approx 30
rewind(file_ptr); // Go back to the beginning of the file
// Read the first line
fgets(buffer, sizeof(buffer), file_ptr);
printf("First line after rewind: %s", buffer);
// Seek to a specific position (e.g., 7 bytes from the beginning)
fseek(file_ptr, 7, SEEK_SET); // Go to 'W' in "Hello, World!"
fgets(buffer, sizeof(buffer), file_ptr); // Read from there
printf("After seeking 7 bytes from start: %s", buffer); // Output: World!\nThis is line two.\n
// Seek backwards from current position
fseek(file_ptr, -10, SEEK_CUR); // Go back 10 bytes from current position
current_pos = ftell(file_ptr);
printf("Position after seeking backwards: %ld\n", current_pos);
fgets(buffer, sizeof(buffer), file_ptr);
printf("Read after seeking backwards: %s", buffer); // Prints part of the remaining line
fclose(file_ptr);
return 0;
}
9.5 Error Handling with Files
ferror(FILE *stream): Returns non-zero if an error occurred on the stream.feof(FILE *stream): Returns non-zero if the end-of-file indicator is set for the stream.clearerr(FILE *stream): Clears the error and EOF indicators for the given stream.perror(const char *s): Prints a system-dependent error message tostderr, prefixed bys. Use this afterfopen()fails orferror()indicates an error.
Always include robust error checking, especially when dealing with external resources like files.
Exercise 9.1: Log File Generator
Write a C program that simulates writing log messages to a file.
- Open a file named
application.login append mode ("a"). - If the file cannot be opened, print an error and exit.
- Write 5 log entries to the file. Each entry should include:
- A timestamp (you can use
time()from<time.h>andctime()to get a simple string, or just a counter for simplicity). - A log level (e.g.,
INFO,WARNING,ERROR). - A message (e.g., “Application started.”, “User login failed.”, “Data processed successfully.”).
- A timestamp (you can use
- After writing, close the file.
- Then, open the same
application.logfile in read mode ("r"), read all its content line by line usingfgets(), and print it to the console. - Close the file.
Example Log Entry Format: [2025-11-03 10:30:00] [INFO] Application started.
Hint for simple timestamp (or use a counter):
#include <time.h>
// ...
time_t current_time;
time(¤t_time);
fprintf(file_ptr, "[%s] [INFO] Application started.\n", ctime(¤t_time));
Note: ctime() adds a newline itself, so adjust fprintf format string.
Exercise 9.2: Simple Inventory Persistence (Mini-Challenge with Structs and Binary I/O)
Building on Exercise 8.2 (Product struct):
- Define the
Productstruct andProductCategoryenum as in Exercise 8.2 (or copy them here). - Create a function
void save_products(const Product products[], int count, const char *filename)that takes an array ofProductstructs, their count, and a filename. This function should:- Open the file in binary write mode (
"wb"). - Write the
countof products as anintat the beginning of the file. - Then, use
fwrite()to write the entire array ofProductstructs to the file. - Handle file opening errors.
- Open the file in binary write mode (
- Create a function
Product* load_products(int *count, const char *filename)that takes a pointer to anint(to store the loaded count) and a filename. This function should:- Open the file in binary read mode (
"rb"). - Read the
countof products from the beginning of the file. - Dynamically allocate memory for an array of
Productstructs usingmallocbased on the loaded count. - Read the entire array of
Productstructs from the file usingfread(). - Handle file opening errors and memory allocation errors.
- Return a pointer to the dynamically allocated array of products.
- Open the file in binary read mode (
- In your
mainfunction:- Create an array of a few
Productstructs and initialize them. - Call
save_products()to save them toinventory.bin. - Call
load_products()to load them back into a new dynamically allocated array. - Print the details of the loaded products to verify.
- Remember to
free()the dynamically allocated memory after you’re done.
- Create an array of a few
You now have the knowledge to enable your C programs to interact with external files, a crucial capability for almost any practical application. Whether storing configuration, logging events, or managing data, file I/O is a fundamental skill. In the next chapter, we’ll explore the C preprocessor, which allows you to modify your source code before it even gets to the compiler.