Introduzione all'Input/Output e agli Stream in Linguaggio C

Una porzione consistente della libreria standard del C è dedicata alla gestione dell'input/output (I/O), che è fondamentale per qualsiasi programma.

A partire da questa lezione, inizieremo a esplorare come il C gestisce l'input e l'output, concentrandoci in particolare sui file e sul concetto di stream.

Il Concetto di Stream

In linguaggio C, il termine stream (in italiano flusso) si riferisce ad una qualsiasi sorgente di input o destinazione di output.

Molti programmi semplici, come quelli che abbiamo visto nelle lezioni precedenti, ottengono tutti i propri dati di input da un unico stream, ossia l'input associato alla tastiera. Allo stesso modo scrivono e destinano tutti i propri dati di output a un unico stream, ossia l'output associato alla console su schermo.

Programmi più grandi e complessi, invece, potrebbero avere bisogno di più stream. Tali flussi sono spesso associati a file su vari dispositivi di memorizzazione, come dischi rigidi, unità flash USB o altri supporti di archiviazione. Ma il concetto di stream non si limita ai file: può includere anche flussi di dati provenienti da reti, ad esempio per la lettura di dati da un server remoto, o flussi di dati provenienti da dispositivi hardware come stampanti o scanner.

Nel nostro studio sull'input/output in C ci concentreremo sui file per due motivi principali:

  1. Sono gli stream più comuni e semplici da gestire.
  2. Unix e i suoi sistemi operativi derivati (come Linux) trattano tutto come file, rendendo l'interazione con dispositivi hardware e risorse di sistema più uniforme. Per cui, conoscendo come lavorare con i file, si acquisisce una comprensione più ampia di come gestire vari tipi di input/output su tali sistemi.

Inoltre, spesso il termine stream e quello di file vengono usati in modo intercambiabile, poiché i file sono una delle forme più comuni di stream.

Puntatori a File

Per accedere ad uno stream in C si adopera un puntatore a file.

Questo tipo di dato è definito nella libreria standard del C e ha la forma:

FILE *nome_puntatore;

Il tipo FILE è definito nella libreria <stdio.h>, che deve essere inclusa all'inizio del programma per poter utilizzare le funzioni di input/output.

Alcuni stream sono rappresentati da puntatori a file predefiniti, che sono già pronti per l'uso. Li vedremo tra poco.

Possiamo, inoltre, dichiarare altri puntatori a file per gestire stream personalizzati, ad esempio per leggere o scrivere su file specifici. Ad esempio:

FILE *file_di_testo;

Sebbene un programma possa avere quanti puntatori a file desidera, è importante ricordare che il sistema operativo (come Linux e Windows) limita il numero di file su cui un programma può operare contemporaneamente. Questo limite varia a seconda del sistema e della configurazione, ma è generalmente abbastanza alto per la maggior parte delle applicazioni.

Stream Predefiniti e Redirezione

L'header della libreria standard <stdio.h> fornisce tre stream predefiniti. Questi stream hanno il vantaggio di essere predefiniti e pronti all'uso; non è necessario, infatti, aprirli o chiuderli.

Sono riportati nella tabella che segue:

Puntatore a FILE Stream Dispositivo di Default
stdin Standard Input Input da tastiera
stdout Standard Output Schermo (Terminale)
stderr Standard Error Schermo (Terminale)
Tabella 1: Stream predefiniti della libreria standard del C

Nelle precedenti lezioni abbiamo utilizzato varie funzioni per leggere l'input da tastiera e scrivere l'output sullo schermo:

  • printf() per scrivere stringhe formattate su stdout
  • scanf() per leggere dati da stdin
  • putchar() per scrivere caratteri su stdout
  • getchar() per leggere caratteri da stdin
  • puts() per scrivere stringhe su stdout
  • gets() per leggere stringhe da stdin

Tutte queste funzioni, in maniera implicita, utilizzano gli stream predefiniti stdin, stdout e stderr.

Di default, stdin è associato alla tastiera, stdout e stderr sono associati al terminale o alla console. Tuttavia, molti sistemi operativi, come Unix e Linux, permettono di redirigere questi stream verso file o altri dispositivi.

Ad esempio, è possibile eseguire un programma e redirigere il suo input da un file con questa sintassi:

./mio_programma < input.txt

Vediamo un esempio pratico di redirezione dell'output. Scriviamo un semplice programma che legge due numeri interi da stdin e stampa la loro somma su stdout:

#include <stdio.h>

int main() {
    int a, b;
    scanf("%d %d", &a, &b);
    printf("La somma è: %d\n", a + b);
    return 0;
}

Normalmente, quando eseguiamo questo programma, quello che accade è che il programma attende l'input da tastiera. Per cui, se lo eseguiamo normalmente, vedremo qualcosa del genere:

$ ./somma
3 5
La somma è: 8

Adesso, creiamo un semplice file di testo chiamato input.txt con il seguente contenuto:

3 5

Adesso possiamo eseguire il programma redirigendo l'input da questo file:

$ ./somma < input.txt
La somma è: 8

La bellezza della redirezione è che il processo non si accorge di nulla: il programma continua a funzionare come se stesse leggendo da tastiera, ma in realtà sta leggendo da un file.

Allo stesso modo, possiamo redirigere l'output su un file. La sintassi per farlo è:

./mio_programma > output.txt

Riprendende l'esempio precedente, se vogliamo salvare l'output della somma in un file chiamato output.txt, possiamo eseguire:

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

Il contenuto del file output.txt sarà:

La somma è: 7

In altre parole, tutto ciò che il programma avrebbe stampato sullo schermo viene ora scritto nel file output.txt.

Uno dei problemi della redirezione dell'output è che se qualcosa va storto, ad esempio se il programma genera un errore, l'errore verrà comunque salvato nel file di output. Per cui, non possiamo accorgerci immediatamente di eventuali errori.

Ecco perché esiste un altro stream predefinito, stderr, che è utilizzato per gli errori. Le funzioni che abbiamo visto sinora, come printf(), scrivono su stdout. Vedremo nelle prossime lezioni come possiamo scrivere su stderr per gestire gli errori in modo separato.

In ogni caso, anche stderr può essere rediretto verso un file, ma la sintassi è leggermente diversa. Per redirigere l'output di errore su un file, si utilizza:

./mio_programma 2> errori.txt

Da notare che il simbolo 2> rappresenta lo stream di errore standard (stderr), mentre > rappresenta lo stream di output standard (stdout). Quindi, se vogliamo redirigere sia l'output che gli errori in due file distinti, possiamo scrivere:

./mio_programma > output.txt 2> errori.txt

Ricordiamo che, se non specifichiamo diversamente, gli errori verranno comunque visualizzati sullo schermo, a meno che non li redirigiamo esplicitamente.

Ricapitolando:

Definizione

Redirezione dell'input o output

La redirezione è una tecnica che permette di cambiare la sorgente di input o la destinazione di output di un programma, ad esempio leggendo da un file invece che dalla tastiera o scrivendo su un file invece che sullo schermo.

In Linux e Unix, la sintassi per redirigere l'input di un programma da un file è:

./mio_programma < input.txt

La sintassi per redirigere l'output di un programma su un file è:

./mio_programma > output.txt

La sintassi per redirigere l'output di errore su un file è:

./mio_programma 2> errori.txt

Operazioni su File

La redirezione degli stream predefiniti è una funzionalità molto utile e semplice da utilizzare. Infatti, non richiede alcuna programmazione specifica: non è necessario scrivere codice per aprire, chiudere o gestire i file, poiché il sistema operativo gestisce tutto in modo trasparente.

Tuttavia, in molti casi, si tratta di una soluzione troppo limitante. Quando un programma sfrutta la redirezione, esso non ha il controllo su come vengono gestiti gli stream. Ad esempio, non può decidere quando aprire o chiudere un file, e non sa nemmeno su quale file sta leggendo o scrivendo. Inoltre, non si può adoperare la redirezione per leggere o scrivere su più file contemporaneamente.

Per questo motivo, quando la redirezione non è sufficiente, si ricorre a funzioni specifiche per gestire i file in modo più flessibile. In C, queste operazioni sono fornite dalla libreria standard <stdio.h> e comprendono:

  • Apertura di un file: per iniziare a leggere o scrivere su un file.
  • Chiusura di un file: per terminare l'uso di un file e liberare le risorse associate.
  • Lettura da un file: per ottenere dati da un file.
  • Scrittura su un file: per inviare dati a un file.
  • Posizionamento nel file: per spostare il cursore di lettura/scrittura all'interno del file.
  • Controllo dello stato del file: per verificare se un file è stato aperto correttamente o se ci sono errori.

A partire dalla prossima lezione, esploreremo queste operazioni in dettaglio, imparando come aprire, leggere e scrivere su file in C. Vedremo anche come gestire gli errori durante queste operazioni e come utilizzare i puntatori a file per accedere a file specifici.

Prima, però, dobbiamo chiarire un altro concetto fondamentale: la differenza tra file di testo e file binari. Questo è l'argomento della prossima lezione. Infatti, a seconda del tipo di file con cui lavoriamo, le operazioni di lettura e scrittura possono variare notevolmente. I file di testo sono formati in modo leggibile dall'uomo, mentre i file binari contengono dati in un formato che può essere interpretato solo da programmi specifici. Comprendere questa differenza è cruciale per lavorare efficacemente con i file in C.