Introduction to Input/Output and Streams in C

A consistent portion of the C standard library is dedicated to input/output (I/O) management, which is fundamental for any program.

Starting from this lesson, we will begin to explore how C handles input and output, focusing particularly on files and the concept of streams.

The Concept of Stream

In C language, the term stream (in Italian flusso) refers to any source of input or destination of output.

Many simple programs, like those we have seen in previous lessons, obtain all their input data from a single stream, namely the input associated with the keyboard. Similarly, they write and direct all their output data to a single stream, namely the output associated with the console on screen.

Larger and more complex programs, however, might need multiple streams. Such flows are often associated with files on various storage devices, such as hard drives, USB flash drives, or other storage media. But the concept of stream is not limited to files: it can also include data streams from networks, for example for reading data from a remote server, or data streams from hardware devices such as printers or scanners.

In our study of input/output in C we will focus on files for two main reasons:

  1. They are the most common and simple streams to handle.
  2. Unix and its derivative operating systems (such as Linux) treat everything as a file, making interaction with hardware devices and system resources more uniform. Therefore, by knowing how to work with files, one acquires a broader understanding of how to handle various types of input/output on such systems.

Furthermore, often the term stream and that of file are used interchangeably, since files are one of the most common forms of streams.

File Pointers

To access a stream in C, a file pointer is used.

This data type is defined in the C standard library and has the form:

FILE *pointer_name;

The FILE type is defined in the <stdio.h> library, which must be included at the beginning of the program to be able to use input/output functions.

Some streams are represented by predefined file pointers, which are already ready for use. We will see them shortly.

We can also declare other file pointers to manage custom streams, for example to read or write to specific files. For example:

FILE *text_file;

Although a program can have as many file pointers as it desires, it is important to remember that the operating system (such as Linux and Windows) limits the number of files on which a program can operate simultaneously. This limit varies depending on the system and configuration, but is generally high enough for most applications.

Predefined Streams and Redirection

The standard library header <stdio.h> provides three predefined streams. These streams have the advantage of being predefined and ready to use; it is not necessary, in fact, to open or close them.

They are listed in the following table:

FILE Pointer Stream Default Device
stdin Standard Input Keyboard Input
stdout Standard Output Screen (Terminal)
stderr Standard Error Screen (Terminal)
Table 1: Predefined streams of the C standard library

In previous lessons we have used various functions to read input from the keyboard and write output to the screen:

  • printf() to write formatted strings to stdout
  • scanf() to read data from stdin
  • putchar() to write characters to stdout
  • getchar() to read characters from stdin
  • puts() to write strings to stdout
  • gets() to read strings from stdin

All these functions, implicitly, use the predefined streams stdin, stdout, and stderr.

By default, stdin is associated with the keyboard, stdout and stderr are associated with the terminal or console. However, many operating systems, such as Unix and Linux, allow redirecting these streams to files or other devices.

For example, it is possible to execute a program and redirect its input from a file with this syntax:

./my_program < input.txt

Let's see a practical example of output redirection. Let's write a simple program that reads two integer numbers from stdin and prints their sum to stdout:

#include <stdio.h>

int main() {
    int a, b;
    scanf("%d %d", &a, &b);
    printf("The sum is: %d\n", a + b);
    return 0;
}

Normally, when we execute this program, what happens is that the program waits for input from the keyboard. So, if we execute it normally, we will see something like this:

$ ./sum
3 5
The sum is: 8

Now, let's create a simple text file called input.txt with the following content:

3 5

Now we can execute the program by redirecting the input from this file:

$ ./sum < input.txt
The sum is: 8

The beauty of redirection is that the process doesn't notice anything: the program continues to work as if it were reading from the keyboard, but in reality it is reading from a file.

Similarly, we can redirect the output to a file. The syntax to do this is:

./my_program > output.txt

Taking the previous example, if we want to save the sum output to a file called output.txt, we can execute:

$ ./sum < input.txt > output.txt

The content of the output.txt file will be:

The sum is: 7

In other words, everything that the program would have printed on the screen is now written to the output.txt file.

One of the problems with output redirection is that if something goes wrong, for example if the program generates an error, the error will still be saved in the output file. Therefore, we cannot immediately notice any errors.

This is why there exists another predefined stream, stderr, which is used for errors. The functions we have seen so far, such as printf(), write to stdout. We will see in the next lessons how we can write to stderr to handle errors separately.

In any case, stderr can also be redirected to a file, but the syntax is slightly different. To redirect the error output to a file, one uses:

./my_program 2> errors.txt

Note that the 2> symbol represents the standard error stream (stderr), while > represents the standard output stream (stdout). So, if we want to redirect both the output and the errors to two distinct files, we can write:

./my_program > output.txt 2> errors.txt

Let's remember that, if we don't specify otherwise, errors will still be displayed on the screen, unless we explicitly redirect them.

Recapping:

Definition

Input or output redirection

Redirection is a technique that allows changing the input source or output destination of a program, for example reading from a file instead of from the keyboard or writing to a file instead of to the screen.

In Linux and Unix, the syntax to redirect a program's input from a file is:

./my_program < input.txt

The syntax to redirect a program's output to a file is:

./my_program > output.txt

The syntax to redirect error output to a file is:

./my_program 2> errors.txt

File Operations

Redirection of predefined streams is a very useful and simple feature to use. In fact, it does not require any specific programming: it is not necessary to write code to open, close, or manage files, since the operating system handles everything transparently.

However, in many cases, it is a too limiting solution. When a program exploits redirection, it does not have control over how streams are managed. For example, it cannot decide when to open or close a file, and it doesn't even know which file it is reading from or writing to. Furthermore, one cannot use redirection to read from or write to multiple files simultaneously.

For this reason, when redirection is not sufficient, one resorts to specific functions to manage files in a more flexible way. In C, these operations are provided by the standard library <stdio.h> and include:

  • Opening a file: to start reading from or writing to a file.
  • Closing a file: to terminate the use of a file and free the associated resources.
  • Reading from a file: to obtain data from a file.
  • Writing to a file: to send data to a file.
  • Positioning in the file: to move the read/write cursor within the file.
  • Checking file status: to verify if a file has been opened correctly or if there are errors.

Starting from the next lesson, we will explore these operations in detail, learning how to open, read, and write to files in C. We will also see how to handle errors during these operations and how to use file pointers to access specific files.

First, however, we must clarify another fundamental concept: the difference between text files and binary files. This is the topic of the next lesson. In fact, depending on the type of file we are working with, read and write operations can vary considerably. Text files are formatted in a human-readable way, while binary files contain data in a format that can only be interpreted by specific programs. Understanding this difference is crucial for working effectively with files in C.