Exploring File I/O in C📁
Tags: codenewbie, programming, c, beginners
Published: 2023-10-06 08:38:04

## Introduction
File handling is a fundamental aspect of programming that allows us to interact with data stored on our computers. Whether it's reading text from a file, writing data to it, or manipulating its contents, mastering Input/Output (IO) operations is essential for any developer.

In this article, we would cover some file handling concepts in C, low-level and high-level File I/O, file descriptors and more.

## Importance of File Handling
* When a program is terminated, the entire data is lost. Storing in a file will preserve data even if the program terminates.
* If you have to enter a large number of data, it'll take sometime. However, if you have a file containing all the data, you can easily access the contents with few commands.
* File handling enables the reading and writing of configuration files, allowing users to tailor software settings to their preferences.
* Through file handling, programs that need to exchange data with other programs or systems can share data in various formats with external entities.
* Regularly saving data to files ensures that valuable information can be restored in case of system failures or data loss

## Getting started with File Handling
At its core, file handling involves performing operations on files which include **_opening, reading, writing and closing_** files. In C programming, file handling is achieved through the following
* Streams
* File pointers
* File descriptors.

*Let's look at these more closely*

### Streams
These are a fundamental abstraction for file handling in C. They provide a high-level interface for reading and writing to files. In C, there are standard streams like `stdin`(standard input) `stdout` (standard output) and `stderr` (standard error) which are automatically available for input and output.

### File Pointers
A file pointer is a mechanism used to keep track of the current position within a file. It determines where the next read or write operation will occur. File pointers are *essential* for sequential file access and help navigate through the file's contents.

### File Descriptors
These are low-level integer identifiers that represent open files in C. Each descriptor corresponds to a particular stream as we found above.

*Below is a table to summarize the various descriptors and their corresponding streams.*

<table>
<tr>
<th>Integer Value</th>
<th>Name</th>
<th>Symbolic Constant</th>
<th>File Stream</th>
</tr>
<tr>
<td>0</td>
<td>Standard Input</td>
<td>STDIN_FILENO</td>
<td>stdin</td>
</tr>
<tr>
<td>1</td>
<td>Standard Output</td>
<td>STDOUT_FILENO</td>
<td>stdout</td>
</tr>
<tr>
<td>2</td>
<td>Standard Error</td>
<td>STDERR_FILENO</td>
<td>stderr</td>
</tr>
</table>

**_NOTE_**
`stdin`: It is used to read input from the *user* or another program.
`stdout`: Used to write output to the *user* or another program.
`stderr`: Used to write error messages and other diagnostics output to the *user*.

## Basic operations in File handling with C
There are **four (4)** basic operations in file handling with C. They are opening, reading, writing and closing. These operations must be followed **in order** when handling and manipulating files.

Aside the four (4) basic operations, there are generally two (2) approaches to handling them. They are low-level approach with system calls and high-level approach with standard library.

Another point to note is that files come in different formats such as csv, binary and text. This article would focus on the text files only.

Let us look at the basic operations in file handling. For each operation, we would look at its implementation in both the high-level and low-level approaches.

### Opening a File
* This is the first step in file handling. It establishes connection between your program and the file on disk.
* During file opening, you specify the following parameters
    * File's name
    * Location
    * Mode with which you want to open the file in. These modes specify what exactly you would like to do with the file you are opening. These includes: reading only, writing only, appending etc. [Man fopen](https://www.man7.org/linux/man-pages/man3/fopen.3.html)

#### High-Level Approach
```c
FILE *file = fopen("example.txt", "r");
```
*RETURN VALUE*: FILE pointer if successful, NULL if otherwise

#### Low-Level Approach
```c
int fd = open("example.txt", O_RDONLY);
```
[man open](https://linux.die.net/man/3/open)
*RETURN VALUE*: New file descriptor if successful, -1 if an error occurred.

Below is a table to understand various modes and how they correspond to each other in high-level and low-level approaches.

<table>
<tr>
<th>fopen() mode</th>
<th>open() flags</th>
<th>Usage</th>
</tr>
<tr>
<td>r</td><td>O_RDONLY</td><td>Opens file for reading</td>
</tr>
<tr>
<td>r+</td><td>O_RDWR</td><td>Opens file for reading and writing</td>
</tr>
<tr>
<td>w</td><td>O_WRONLY | O_CREAT | O_TRUNC</td><td>Writes to a file. It clears(truncates) everything if there is already text</td>
</tr>
<tr>
<td>w+</td><td>O_RDWR | O_CREAT | O_TRUNC</td><td>Opens for reading and writing. The file is created if it doesn't exist otherwise it's truncated.</td>
</tr>
<tr>
<td>a</td><td>O_WRONLY | O_CREAT | O_APPEND</td><td>Open for appending (writing at end of file). The file is created if it doesn't exist</td>
</tr>
</table>

### Reading from a File
* This involves retrieving data from existing file on disk.
* You can read data character by character, line by line or in larger chunks depending on your program's requirement.

#### High-Level Approach
There are two ways to read from files with this approach. They are:
**fgets()**: This reads texts line by line and stores in a buffer.
```c
FILE *file;
char buffer[1024];
while(fgets(buffer, sizeof(buffer), file) != NULL) {
     // Process each line in the file
}
```
*RETURN VALUE*: A string on success, NULL on error.
[man fgets](https://linux.die.net/man/3/fgets)
**fread()**: This reads specified number of bytes from a file or for reading binary files also into a buffer.
[man fread](https://www.man7.org/linux/man-pages/man3/fread.3.html)
```c
FILE *file;
char buffer[1024];
size_t bytes_to_read = sizeof(buffer);
size_t bytes_read = fread(buffer, 1, bytes_to_read, file);
```
*RETURN VALUE*: Number of items read
#### Low-Level Approach
This uses the `read()` function.
```c
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1)
    // handle error
else
    // Process the data in the buffer
```
NOTE: The `fd` is the return value of the `open` function
[man read](https://man7.org/linux/man-pages/man2/read.2.html)

### Writing to a file
* This adds or updates data in a file.
* You can write character by character, line by line or in larger blocks as well.
* Writing is essential for tasks like creating log files, saving program output or storing user-generated content.

#### High-Level Approach
This method uses `fprintf()` (Write formatted text data to a file) 
```c
FILE *file = fopen("example.txt", "w"); // Open for writing
fprintf(file, "Hello, %s!\n", "World");
```
[man fprintf](https://linux.die.net/man/3/fprintf)
`fwrite()` (Writes a specified number of bytes from the buffer to a file)
```c
FILE *file = fopen("data.bin", "wb"); // Open for binary writing
char data[] = {0x01, 0x02, 0x03};
size_t bytes_to_write = sizeof(data);
size_t bytes_written = fwrite(data, 1, bytes_to_write, file);
```
[man fwrite](https://linux.die.net/man/3/fwrite)
#### Low-Level Approach
`write()` is the function used here.
```c
int fd = open("example.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
const char *text = "Hello, World!\n";
ssize_t bytes_written = write(fd, text, strlen(text));
```
[man write](https://linux.die.net/man/2/write)

**NOTE**: Modify the various modes/flags of the write operation to get the desired results like append etc.

In the low-level approach, you have more control over the writing process and can directly manipulate the binary data. However, you need to manage buffering and handle text encoding yourself if you're working with text files.

### Closing a File
* This is the final step in the file handling and it's essential to release the system resources and ensure data integrity.
* Once you finish reading or writing, you should close the file to free up the file descriptors and ensure that all pending changes are saved.
* Failing to close a file property can result in resource leaks and data corruption.

#### High level approach
```c
FILE *file = fopen("example.txt", "w"); // Open for writing
// Write data to the file
fclose(file); // Close the file when done
```
[man fclose](https://linux.die.net/man/3/fclose)

#### Low level approach
```c
int fd = open("example.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644); // Open for writing
// Write data to the file
close(fd); // Close the file descriptor when done
```
[man close](https://linux.die.net/man/2/close)

Until now you might have realized that there are two approaches to handline files in C. The commonest one used is the high-level approach. However, let us look at some differences between the two. This would help inform our decisions as to which of them to use at what point in time.

<table>
<tr>
<th>Aspect</th><th>High level Approach</th><th>Low level Approach</th>
</tr>
<tr>
<td>File representation</td>
<td>Uses a file stream represented by *FILE**, for file operations</td>
<td>Uses file descriptors represented by *int* for file operations</td>
</tr>
<tr>
<td>Common functions</td>
<td>Common functions include fopen(), fclose(), fgets(), fprintf(), fwrite(). </td>
<td>Common functions include open(), close(), read(), write(). </td>
</tr>
<tr>
<td>Text vs. Binary Files</td>
<td>Suitable for both text and binary files, with functions handling text encoding and formatting. </td>
<td>Suitable for both text and binary files, but you need to handle text encoding and formatting manually if required. </td>
</tr>
<tr>
<td>Error Handling</td>
<td>Uses functions like ferror() and feof() to handle errors. </td>
<td>Relies on error codes returned by functions like read() and write() for error handling. </td>
</tr>
<tr>
<td>Resource Cleanup</td>
<td>Automatically flushes data and releases resources when you use fclose() </td>
<td>Requires manual closure of file descriptors using close() for resource cleanup. </td>
</tr>
</table>

*Now let's look at some examples with practical questions*

<u>**QUESTION 1**</u>
*Implement a program to read text from a file*

<u>**SOLUTION**</u>
#### High level approach
```c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s source_file\n", argv[0]);
        return 1;
    }

    FILE *source = fopen(argv[1], "r");

    if (!source) {
        perror("Error");
        return 1;
    }

    char buffer[1024];

    while (fgets(buffer, sizeof(buffer), source)) {
        printf("%s", buffer);
    }

    fclose(source);

    return 0;
}
```
#### Low level approach
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s source_file\n", argv[0]);
        return 1;
    }

    int source_fd = open(argv[1], O_RDONLY);

    if (source_fd == -1) {
        perror("Error");
        return 1;
    }

    char buffer[1024];
    ssize_t bytes_read;

    while ((bytes_read = read(source_fd, buffer, sizeof(buffer))) > 0) {
        if (write(STDOUT_FILENO, buffer, bytes_read) == -1) {
            perror("Error");
            return 1;
        }
    }

    close(source_fd);

    return 0;
}
```

<u>**QUESTION 2**</u>
*Implement a program to append some text to a file. This program should take the text from the user (stdin) and then append to the file*

<u>**SOLUTION**</u>
#### High level approach
```c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s destination_file\n", argv[0]);
        return 1;
    }

    FILE *destination = fopen(argv[1], "a");

    if (!destination) {
        perror("Error");
        return 1;
    }

    char input[1024];

    printf("Enter text (Ctrl-D to end):\n");

    while (fgets(input, sizeof(input), stdin)) {
        fputs(input, destination);
    }

    fclose(destination);

    return 0;
}
```
#### Low level approach
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s destination_file\n", argv[0]);
        return 1;
    }

    int destination_fd = open(argv[1], O_WRONLY | O_CREAT | O_APPEND, 0644);

    if (destination_fd == -1) {
        perror("Error");
        return 1;
    }

    char input[1024];

    printf("Enter text (Ctrl-D to end):\n");

    while (fgets(input, sizeof(input), stdin)) {
        if (write(destination_fd, input, strlen(input)) == -1) {
            perror("Error");
            return 1;
        }
    }

    close(destination_fd);

    return 0;
}
```
<u>**QUESTION 3**</u>
*Implement a File Copy Program (like cp command)*

<u>**SOLUTION**</u>
#### High level approach
```c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("Usage: %s source_file destination_file\n", argv[0]);
        return 1;
    }

    FILE *source = fopen(argv[1], "rb");
    FILE *destination = fopen(argv[2], "wb");

    if (!source || !destination) {
        perror("Error");
        return 1;
    }

    char buffer[1024];
    size_t bytes_read;

    while ((bytes_read = fread(buffer, 1, sizeof(buffer), source)) > 0) {
        fwrite(buffer, 1, bytes_read, destination);
    }

    fclose(source);
    fclose(destination);

    return 0;
}
```
#### Low level approach
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("Usage: %s source_file destination_file\n", argv[0]);
        return 1;
    }

    int source_fd = open(argv[1], O_RDONLY);
    int destination_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);

    if (source_fd == -1 || destination_fd == -1) {
        perror("Error");
        return 1;
    }

    char buffer[1024];
    ssize_t bytes_read;

    while ((bytes_read = read(source_fd, buffer, sizeof(buffer))) > 0) {
        if (write(destination_fd, buffer, bytes_read) == -1) {
            perror("Error");
            return 1;
        }
    }

    close(source_fd);
    close(destination_fd);

    return 0;
}
```
**NOTE**: In low-level implementation, the `open()` function has an optional argument known as *permissions*. These are file permissions that you can set to the file are read, write and execute permissions represented by their numerical values.
[More about file permissions here](https://linuxize.com/post/understanding-linux-file-permissions/)


## References
* [C File IO - Wikipedia](https://en.wikibooks.org/wiki/A_Little_C_Primer/C_File-IO_Through_System_Calls)
* [GeeksForGeeks](https://www.geeksforgeeks.org/input-output-system-calls-c-create-open-close-read-write/)
* [File Descriptor - Wikipedia](https://en.wikipedia.org/wiki/File_descriptor)

Congratulations on making it this far 👏🏾. As you have studied, file handling is a very useful programming concept. I would love to know what your experience is with File handling in the comment section.
 
Follow me on [Github](https://www.github.com/angelotheman), let's get interactive on [Twitter](https://www.twitter.com/_angelotheman) and form great connections on [LinkedIn](https://www.linkedin.com/in/angelotheman) 😊

Happy coding 🥂