Input e Output a Caratteri in Linguaggio C

La libreria standard del linguaggio C fornisce una serie di funzioni per eseguire operazioni di input e output a caratteri su file. Quando si parla di I/O a caratteri, in realtà si fa riferimento a operazioni di lettura e scrittura byte per byte, in cui ogni carattere, char, è rappresentato da un singolo byte.

Pertanto, l'I/O a caratteri è particolarmente adatto per manipolare qualunque tipo di file, inclusi file di testo e file binari, poiché consente di leggere e scrivere dati in modo preciso e controllato.

Concetti Chiave
  • La libreria standard del linguaggio C fornisce funzioni per eseguire operazioni di input e output a caratteri su file.
  • Le principali funzioni di scrittura a caratteri sono fputc(), putc(), e putchar().
  • Le principali funzioni di lettura a caratteri sono fgetc(), getc(), e getchar().
  • La funzione ungetc() consente di restituire un carattere precedentemente letto allo stream.
  • Le funzioni di I/O a caratteri lavorano con il tipo int per gestire correttamente il valore speciale EOF.
  • Lavorare a caratteri permette di manipolare qualsiasi tipo di file, inclusi file binari in quanto consente di leggere e scrivere byte esatti.

Funzioni di Scrittura a Caratteri

La libreria standard del linguaggio C fornisce diverse funzioni per scrivere caratteri su file. Le principali funzioni di scrittura a caratteri sono:

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);

La prima osservazione da fare è che tutte queste funzioni accettano come parametro un intero int c anziché un carattere char. Questo perché, sebbene il valore venga trattato come un carattere, tali funzioni possono anche gestire il valore speciale EOF (End Of File), che è definito come un intero negativo. Quindi per gestire correttamente questo valore speciale, è necessario utilizzare il tipo int.

La funzione fputc() scrive il carattere specificato dal parametro c sul file associato allo stream indicato dal puntatore FILE *stream. Essa restituisce il carattere scritto in caso di successo, oppure EOF in caso di errore.

La funzione putc() è simile a fputc(), ma essa è implementata come una macro in libreria, il che può renderla leggermente più efficiente in alcune situazioni. Anche questa funzione scrive il carattere specificato sul file associato allo stream e restituisce il carattere scritto o EOF in caso di errore.

Per usare queste funzioni, si può seguire il seguente schema di programma:

#include <stdio.h>

// Scrive ch sul file specificato
putc(ch, file);
fputc(ch, file);

La funzione putchar() scrive il carattere specificato dal parametro c sullo standard output (solitamente il terminale). Anche questa funzione restituisce il carattere scritto in caso di successo, oppure EOF in caso di errore.

Ecco un esempio di utilizzo di putchar():

#include <stdio.h>

putchar('A'); // Scrive il carattere 'A' sullo standard output

Nella pratica, spesso la funzione putchar() è implementata come una macro, il che può renderla più efficiente in alcune situazioni:

#define putchar(c) putc(c, stdout)

Tutte e tre le funzioni restituiscono EOF in caso di errore, quindi è buona pratica controllare il valore di ritorno per gestire eventuali errori di scrittura. In caso di successo, invece, restituiscono il carattere scritto.

La preferenza tra fputc() e putc() dipende dalle esigenze specifiche del programma, ma in generale, putc() può essere leggermente più efficiente grazie alla sua implementazione come macro.

L'ultima osservazione riguarda il fatto che queste funzioni scrivono i caratteri senza alcuna formattazione. Ciò significa che esse scrivono esattamente il byte corrispondente al carattere specificato, senza aggiungere spazi, nuove righe o altri caratteri speciali. Questo è il motivo per cui queste funzioni possono essere usate per scrivere sia file di testo che file binari.

Funzioni di Lettura a Caratteri

Le principali funzioni di lettura a caratteri fornite dalla libreria standard del linguaggio C sono:

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

La funzione getchar() legge un carattere dallo standard input (solitamente la tastiera) e restituisce il carattere letto come un intero. In caso di errore o se si raggiunge la fine del file, restituisce EOF. Ad esempio:

#include <stdio.h>

int ch = getchar(); // Legge un carattere dallo standard input

Le funzioni fgetc() e getc() sono simili a getchar(), ma leggono un carattere dal file associato allo stream specificato dal puntatore FILE *stream. Entrambe le funzioni restituiscono il carattere letto come un intero, o EOF in caso di errore o fine del file. Anche qui, getc() è spesso implementata come una macro per motivi di efficienza. Ecco un esempio di utilizzo:

#include <stdio.h>

int ch;
ch = fgetc(file); // Legge un carattere dal file specificato
ch = getc(file);  // Legge un carattere dal file specificato

Tutte e tre le funzioni lavorano in questo modo:

  1. Leggono un singolo byte dal file o dallo standard input.
  2. Trattano il byte come un unsigned char e lo promuovono a int.
  3. In caso di successo, restituiscono il valore intero del carattere letto.
  4. In caso di errore o fine del file, restituiscono EOF.

Da ciò si evince che l'unico valore negativo possibile restituito da queste funzioni è EOF, mentre tutti gli altri valori interi rappresentano caratteri validi (da 0 a 255) e quindi dei byte validi.

Anche in questo caso, spesso getchar è implementata come una macro:

#define getchar() getc(stdin)

Le tre funzioni di lettura a caratteri si comportano allo stesso modo in presenza di problemi:

  • Se viene raggiunta la fine del file (EOF), restituiscono EOF e impostano il flag di fine file sullo stream.
  • In caso di errore di lettura, restituiscono EOF e impostano il flag di errore sullo stream.

Come abbiamo visto, per differenziare tra questi due casi, è possibile utilizzare le funzioni feof() e ferror().

L'utilizzo classico di tali funzioni di lettura a caratteri può essere rappresentato dal seguente schema di programma:

int ch;

while ((ch = fgetc(file)) != EOF) {
    // Elaborare il carattere letto
}

In pratica, questo ciclo legge carattere per carattere dal file fino a quando non viene raggiunta la fine del file, elaborando ogni carattere letto all'interno del ciclo. Solo quando fgetc() restituisce EOF, il ciclo termina.

Nota

Usare sempre il tipo int per i caratteri letti

Bisogna sempre ricordare di dichiarare la variabile che riceve il valore restituito dalle funzioni di lettura a caratteri come int, e non come char. Questo perché il valore EOF è un intero negativo e non può essere rappresentato correttamente da una variabile di tipo char. Usare int permette di distinguere tra un carattere valido e il valore speciale EOF.

Confrontare un char con EOF può portare a risultati errati, specialmente se char è un tipo senza segno (unsigned), poiché in tal caso EOF verrebbe convertito in un valore positivo molto grande, causando un confronto sempre falso.

La funzione ungetc

Oltre alle funzioni di lettura a caratteri viste sopra, ne esiste un'altra chiamata ungetc(), che consente di restituire un carattere precedentemente letto dallo stream e ripulisce il flag di fine file se era stato impostato. La sua sintassi è la seguente:

int ungetc(int c, FILE *stream);

L'utilizzo di ungetc() è particolarmente utile quando si desidera vedere un carattere in anticipo senza rimuoverlo dallo stream, ossia senza leggerlo, per poi poterlo leggere nuovamente in un secondo momento.

Ad esempio, supponiamo di voler leggere una serie di cifre da un file, ma di voler interrompere la lettura non appena incontriamo un carattere non numerico. In questo caso, potremmo leggere il carattere, verificare se è una cifra attraverso la funzione isdigit() (definita in <ctype.h>), e se non lo è, utilizzare ungetc() per restituirlo allo stream in modo che possa essere letto nuovamente in seguito.

Ecco un esempio di utilizzo di ungetc():

#include <stdio.h>
#include <ctype.h>

while (isdigit(ch = fgetc(file))) {
    // Elaborare la cifra letta
}
// Restituire il carattere non numerico allo stream
ungetc(ch, file);

Esiste una limitazione importante, però, nell'uso di ungetc(): il numero di chiamate consecutive a ungetc() senza una lettura intermedia dipende dall'implementazione della libreria standard del C. In generale, è garantito che solo la prima chiamata a ungetc() dopo una lettura avrà successo. Chiamate successive potrebbero fallire se non c'è spazio sufficiente nel buffer dello stream per memorizzare i caratteri restituiti.

La funzione, inoltre, restituisce il carattere c in caso di successo, oppure EOF in caso di errore (ad esempio, se il carattere non può essere restituito allo stream).

In ogni caso, esistono delle funzioni più avanzate per il posizionamento all'interno di un file, che vedremo in una lezione successiva. Inoltre, una chiamata ad una di tali funzioni di posizionamento (come fseek(), fsetpos(), o rewind()) causa la cancellazione di tutti i caratteri restituiti con ungetc().

Esempio: Copia di un File

Proviamo a mettere insieme quanto visto finora con un semplice esempio di programma che copia il contenuto di un file di input in un file di output, utilizzando le funzioni di lettura e scrittura a caratteri fgetc() e fputc().

Questo programma prende in input dalla riga di comando il nome del file di origine e il nome del file di destinazione, apre entrambi i file, legge il contenuto del file di origine carattere per carattere, e lo scrive nel file di destinazione. Infine, chiude entrambi i file.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    FILE *sourceFile, *destFile;
    int ch;

    // Controllare il numero di argomenti
    if (argc != 3) {
        fprintf(stderr, "Uso: %s <file_sorgente> <file_destinazione>\n", argv[0]);
        return EXIT_FAILURE;
    }

    // Aprire il file di origine in modalità lettura
    sourceFile = fopen(argv[1], "rb");
    if (sourceFile == NULL) {
        fprintf(stderr, "Errore nell'apertura del file di origine\n");
        return EXIT_FAILURE;
    }

    // Aprire il file di destinazione in modalità scrittura
    destFile = fopen(argv[2], "wb");
    if (destFile == NULL) {
        fprintf(stderr, "Errore nell'apertura del file di destinazione\n");
        fclose(sourceFile);
        return EXIT_FAILURE;
    }

    // Copiare il contenuto del file di origine nel file di destinazione
    while ((ch = getc(sourceFile)) != EOF)
        putc(ch, destFile);

    // Chiudere entrambi i file
    fclose(sourceFile);
    fclose(destFile);

    return EXIT_SUCCESS;
}

Un'importante osservazione riguarda l'uso delle modalità di apertura dei file: in questo esempio, abbiamo utilizzato "rb" per aprire il file di origine in modalità lettura binaria e "wb" per aprire il file di destinazione in modalità scrittura binaria. In questo modo, sebbene stiamo utilizzando funzioni di I/O a caratteri, possiamo adoperare il programma per copiare qualsiasi tipo di file, inclusi file binari, senza rischiare di alterarne il contenuto a causa di conversioni di fine linea o altri problemi legati alla modalità testo.

Il modo di utilizzo del programma è il seguente:

./copia_file file_sorgente file_destinazione