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:
- Sono gli stream più comuni e semplici da gestire.
- 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) |
Nelle precedenti lezioni abbiamo utilizzato varie funzioni per leggere l'input da tastiera e scrivere l'output sullo schermo:
printf()
per scrivere stringhe formattate sustdout
scanf()
per leggere dati dastdin
putchar()
per scrivere caratteri sustdout
getchar()
per leggere caratteri dastdin
puts()
per scrivere stringhe sustdout
gets()
per leggere stringhe dastdin
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:
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.