DEV Community

Angel Oduro-Temeng Twumasi
Angel Oduro-Temeng Twumasi

Posted on • Edited on

Exploring File I/O in CπŸ“

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.

Integer Value Name Symbolic Constant File Stream
0 Standard Input STDIN_FILENO stdin
1 Standard Output STDOUT_FILENO stdout
2 Standard Error STDERR_FILENO stderr

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

High-Level Approach

FILE *file = fopen("example.txt", "r");
Enter fullscreen mode Exit fullscreen mode

RETURN VALUE: FILE pointer if successful, NULL if otherwise

Low-Level Approach

int fd = open("example.txt", O_RDONLY);
Enter fullscreen mode Exit fullscreen mode

man 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.

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

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.

FILE *file;
char buffer[1024];
while(fgets(buffer, sizeof(buffer), file) != NULL) {
     // Process each line in the file
}
Enter fullscreen mode Exit fullscreen mode

RETURN VALUE: A string on success, NULL on error.
man fgets
fread(): This reads specified number of bytes from a file or for reading binary files also into a buffer.
man fread

FILE *file;
char buffer[1024];
size_t bytes_to_read = sizeof(buffer);
size_t bytes_read = fread(buffer, 1, bytes_to_read, file);
Enter fullscreen mode Exit fullscreen mode

RETURN VALUE: Number of items read

Low-Level Approach

This uses the read() function.

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
Enter fullscreen mode Exit fullscreen mode

NOTE: The fd is the return value of the open function
man read

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)

FILE *file = fopen("example.txt", "w"); // Open for writing
fprintf(file, "Hello, %s!\n", "World");
Enter fullscreen mode Exit fullscreen mode

man fprintf
fwrite() (Writes a specified number of bytes from the buffer to a file)

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);
Enter fullscreen mode Exit fullscreen mode

man fwrite

Low-Level Approach

write() is the function used here.

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));
Enter fullscreen mode Exit fullscreen mode

man 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

FILE *file = fopen("example.txt", "w"); // Open for writing
// Write data to the file
fclose(file); // Close the file when done
Enter fullscreen mode Exit fullscreen mode

man fclose

Low level approach

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
Enter fullscreen mode Exit fullscreen mode

man 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.

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

Now let's look at some examples with practical questions

QUESTION 1
Implement a program to read text from a file

SOLUTION

High level approach

#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;
}
Enter fullscreen mode Exit fullscreen mode

Low level approach

#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;
}
Enter fullscreen mode Exit fullscreen mode

QUESTION 2
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

SOLUTION

High level approach

#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;
}
Enter fullscreen mode Exit fullscreen mode

Low level approach

#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;
}
Enter fullscreen mode Exit fullscreen mode

QUESTION 3
Implement a File Copy Program (like cp command)

SOLUTION

High level approach

#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;
}
Enter fullscreen mode Exit fullscreen mode

Low level approach

#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;
}
Enter fullscreen mode Exit fullscreen mode

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

References

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, let's get interactive on Twitter and form great connections on LinkedIn 😊

Happy coding πŸ₯‚

Top comments (6)

Collapse
 
pauljlucas profile image
Paul J. Lucas
  • A CSV file is a text file.
  • In Unix-like environments, there is no distinction between a text and binary file.
  • You don't ever say why you'd use the "high level" vs. the "low level".
  • You never mention buffering.
  • You never mention line-ending differences in operating systems, specifically Unix vs. Windows.
  • You never mention that stdout is buffered (by default) and stderr is unbuffered.
Collapse
 
angelotheman profile image
Angel Oduro-Temeng Twumasi

Hi Paul,

Your comments show that you have really scrutinized my article and I'm glad you did that.
I would have wished to add as much information, but I provided helpful links so that it would clear up some of the loopholes.

And yeah, a CSV file is also a text file πŸ˜€. Concerning the differences in OS with respect to the line-endings, thank you for calling me out on that.

Overall, I'll take your comments seriously as I write my next article on Building a shell

Thank you

Collapse
 
johsef profile image
Oluwayemi Joseph

Nice article. Thanks for sharing

Collapse
 
angelotheman profile image
Angel Oduro-Temeng Twumasi

My pleasure

Collapse
 
0xdvc profile image
N E I L O H E N E

Interesting read

Collapse
 
angelotheman profile image
Angel Oduro-Temeng Twumasi

I'm glad you loved it πŸ‘πŸΎ