Reading and Writing Files in C
Welcome to C-Language Series #65! In this installment, we delve into a fundamental aspect of many real-world applications: file input/output (I/O). The ability to read data from files and write data to files allows your C programs to interact with the persistent storage of a computer, making them capable of saving configurations, logging information, processing datasets, and much more.
Why File I/O?
When your C program runs, it primarily operates in memory. This memory is volatile, meaning any data stored there is lost once the program terminates. To make data persistent – available even after the program closes – we need to store it in files on a disk or other storage medium. File I/O in C provides the functions and mechanisms to perform these operations.
The FILE Pointer: Your Gateway to Files
In C, whenever you want to perform operations on a file, you first need to open it. The `fopen()` function (which we'll discuss next) returns a special pointer of type `FILE*`. This pointer, often called a file pointer or file handle, acts as a communication channel between your program and the file on the disk. All subsequent read or write operations use this pointer to specify which file to interact with.
You'll declare it like this:
FILE *file_pointer;
Opening Files: The fopen() Function
Before you can read from or write to a file, you must open it using the fopen() function. This function attempts to open the specified file and returns a `FILE*` pointer if successful, or `NULL` if it fails (e.g., file not found, permission denied).
Syntax
FILE *fopen(const char *filename, const char *mode);
filename: A string containing the name (and optionally path) of the file to open.mode: A string specifying the access mode.
File Modes
The mode argument is crucial as it dictates how the file will be accessed:
"r": **Read mode.** Opens an existing file for reading. If the file doesn't exist, `fopen()` returns `NULL`."w": **Write mode.** Opens a file for writing. If the file exists, its content is **truncated (erased)**. If the file doesn't exist, a new file is created."a": **Append mode.** Opens a file for writing. Data is added to the end of the file. If the file doesn't exist, a new file is created."r+": **Read and Write mode.** Opens an existing file for both reading and writing."w+": **Read and Write mode.** Opens a file for both reading and writing. If the file exists, its content is **truncated**. If the file doesn't exist, a new file is created."a+": **Read and Append mode.** Opens a file for both reading and writing (appending). Data is added to the end of the file. If the file doesn't exist, a new file is created.
For binary files, you add 'b' to the mode (e.g., "rb", "wb", "ab+"). We'll touch upon binary files briefly later.
Error Handling for fopen()
It's vital to always check the return value of fopen() to ensure the file was opened successfully.
#include <stdio.h>
int main() {
FILE *file_ptr;
// Open a file for writing
file_ptr = fopen("example.txt", "w");
if (file_ptr == NULL) {
printf("Error: Could not open file for writing.\n");
return 1; // Indicate an error
} else {
printf("File 'example.txt' opened successfully in write mode.\n");
// Don't forget to close the file!
fclose(file_ptr);
}
// Open a file for reading (which might not exist yet if only "w" was used)
file_ptr = fopen("nonexistent.txt", "r");
if (file_ptr == NULL) {
printf("Error: Could not open file 'nonexistent.txt' for reading (as expected).\n");
} else {
printf("File 'nonexistent.txt' opened successfully in read mode.\n");
fclose(file_ptr);
}
return 0;
}
Writing Data to Files
Once a file is opened in a write or append mode, you can use various functions to put data into it.
fprintf(): Formatted Output
Similar to printf(), but it writes formatted output to a specified file stream.
int fprintf(FILE *stream, const char *format, ...);
fputs(): Write a String
Writes a null-terminated string to the specified file stream. It does not append a newline character automatically.
int fputs(const char *str, FILE *stream);
fputc(): Write a Character
Writes a single character to the specified file stream.
int fputc(int character, FILE *stream);
Example: Writing to a File
#include <stdio.h>
#include <stdlib.h> // For exit()
int main() {
FILE *file_ptr;
char text[] = "This is a line of text.\n";
int number = 123;
char my_char = 'X';
file_ptr = fopen("output.txt", "w"); // Open in write mode, truncates if exists
if (file_ptr == NULL) {
printf("Error opening file for writing!\n");
exit(1);
}
fprintf(file_ptr, "Hello, C File I/O! Number: %d\n", number);
fputs(text, file_ptr); // This line already has a newline
fputc(my_char, file_ptr);
fputc('\n', file_ptr); // Add a newline after the character
printf("Data written to output.txt successfully.\n");
fclose(file_ptr); // Close the file
return 0;
}
After running this program, a file named output.txt will be created (or overwritten) in the same directory, containing:
Hello, C File I/O! Number: 123
This is a line of text.
X
Reading Data from Files
Once a file is opened in a read or read/write mode, you can extract data from it.
fscanf(): Formatted Input
Similar to scanf(), but it reads formatted input from a specified file stream.
int fscanf(FILE *stream, const char *format, ...);
fgets(): Read a String
Reads up to `size-1` characters from the stream and stores them into `str`. Reading stops if a newline character is encountered (which is then stored), or if the end-of-file is reached.
char *fgets(char *str, int size, FILE *stream);
fgetc(): Read a Character
Reads a single character from the specified file stream. Returns EOF (End Of File) if the end of the file is reached or an error occurs.
int fgetc(FILE *stream);
feof(): Check for End-Of-File
Checks if the end-of-file indicator is set for the specified stream.
int feof(FILE *stream);
Example: Reading from a File
#include <stdio.h>
#include <stdlib.h> // For exit()
int main() {
FILE *file_ptr;
char buffer[100]; // Buffer to store read lines
int num;
char ch;
file_ptr = fopen("output.txt", "r"); // Open the file created earlier
if (file_ptr == NULL) {
printf("Error opening file for reading!\n");
exit(1);
}
printf("Reading from output.txt:\n");
// Read a formatted string and an integer
if (fscanf(file_ptr, "Hello, C File I/O! Number: %d\n", &num) == 1) {
printf("Read formatted: Number = %d\n", num);
} else {
printf("Failed to read formatted line.\n");
rewind(file_ptr); // Reset file pointer if fscanf failed to try other methods
}
// Read line by line using fgets
printf("Reading remaining lines with fgets:\n");
while (fgets(buffer, sizeof(buffer), file_ptr) != NULL) {
printf("%s", buffer);
}
// You could also read character by character
// rewind(file_ptr); // Reset file pointer to the beginning
// printf("\nReading character by character:\n");
// while ((ch = fgetc(file_ptr)) != EOF) {
// putchar(ch);
// }
fclose(file_ptr); // Close the file
return 0;
}
Running this code after the writing example will output:
Reading from output.txt:
Read formatted: Number = 123
Reading remaining lines with fgets:
This is a line of text.
X
Note: Mixing `fscanf` and `fgets` can sometimes be tricky due to how `fscanf` leaves the newline character in the buffer. Be mindful of this when designing your parsing logic. Often, reading line by line with `fgets` and then parsing the line with `sscanf` (string scan format) is a safer approach for complex text files.
Closing Files: The fclose() Function
It is absolutely critical to close a file after you have finished reading from or writing to it. The fclose() function releases the file pointer, flushes any buffered data to the disk, and frees up system resources associated with the file.
Failing to close files can lead to:
- Data loss (especially if data is still in the buffer and not written to disk).
- Resource leaks.
- Corruption of the file.
Syntax
int fclose(FILE *stream);
It returns `0` on success, or `EOF` if an error occurs.
Working with Binary Files
The functions discussed so far (`fprintf`, `fscanf`, `fputs`, `fgets`, `fputc`, `fgetc`) are primarily designed for text files, where data is processed as characters and strings, and newline conversions might occur (e.g., `\n` to `\r\n` on Windows). For raw, unformatted data, such as images, executables, or structured data records, you should use binary file modes (`"rb"`, `"wb"`, etc.) and the functions `fread()` and `fwrite()`.
fread() and fwrite()
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
ptr: Pointer to the block of memory to read from/write to.size: Size in bytes of each item to be read/written.count: Number of items (each of `size` bytes) to read/write.stream: The file stream.
These functions are powerful for reading/writing structures or arrays of primitive types directly to/from files without any text interpretation.
Conclusion
File I/O is a cornerstone of robust C programming, allowing your applications to move beyond temporary memory and interact with the permanent storage of a system. By mastering functions like fopen(), fprintf(), fscanf(), fgets(), fputc(), fgetc(), and especially fclose(), you gain the ability to create programs that can truly store, retrieve, and manage data effectively. Always remember to handle errors and close your files diligently to ensure data integrity and prevent resource issues.
Experiment with different file modes and functions to solidify your understanding. Happy coding!