Input e Output a Linee in Linguaggio C

Dopo aver visto le funzioni di Input e Output formattato e a caratteri, vediamo ora come effettuare operazioni di Input e Output a linee in linguaggio C.

Quando si parla di Input e Output a linee, si fa riferimento alla lettura e scrittura di intere righe di testo, piuttosto che di singoli caratteri o valori formattati. In C, questo tipo di operazioni viene solitamente eseguito utilizzando le funzioni fgets() per l'input e fputs() per l'output.

A differenza dell'I/O a caratteri, che è in grado di leggere o scrivere un singolo byte alla volta e che si presta bene sia per file di testo che per file binari, l'I/O a linee è specificamente progettato per gestire righe di testo, rendendolo ideale per file di testo strutturati in righe.

Concetti Chiave
  • Le funzioni fgets() e fputs() sono utilizzate per leggere e scrivere intere righe di testo in linguaggio C.
  • L'I/O a linee è particolarmente utile per gestire file di testo strutturati in righe.
  • fgets() legge una riga di testo da un file o dallo standard input, mentre fputs() scrive una riga di testo su un file o sullo standard output.
  • Queste funzioni sono versioni più generali e sicure rispetto alle loro controparti gets() e puts(), che lavorano solo con lo standard input e output rispettivamente.

Funzioni di Output a linee: fputs e puts

Le funzioni fputs() e puts() sono utilizzate per scrivere stringhe di testo su un file o sullo standard output (console).

In particolare, la loro firma è la seguente:

int fputs(const char *str, FILE *stream);
int puts(const char *str);

Abbiamo già incontrato la funzione puts() in precedenza, per scrivere stringhe sulla console. Essa scrive la stringa str seguita da un carattere di nuova linea (\n) sullo standard output.

Ad esempio, il seguente codice utilizza puts() per stampare una stringa sulla console:

#include <stdio.h>

int main() {
    puts("Ciao, mondo!");
    return 0;
}

Un'importante caratteristica di puts() è che aggiunge automaticamente un carattere di nuova linea alla fine della stringa, quindi non è necessario includerlo manualmente.

La funzione fputs(), invece, è una versione più generale che consente di specificare il file di destinazione tramite il parametro stream. Ad esempio, il programma di sopra può essere riscritto utilizzando fputs() in questo modo:

#include <stdio.h>

int main() {
    fputs("Ciao, mondo!\n", stdout);
    return 0;
}

Esiste, però, un'importante differenza tra le due funzioni: fputs() non aggiunge automaticamente un carattere di nuova linea alla fine della stringa, quindi è necessario includerlo manualmente se si desidera che la stringa venga seguita da una nuova linea.

Entrambe le funzioni restituiscono un valore intero che indica il successo o il fallimento dell'operazione. In caso di successo, entrambe le funzioni restituiscono un valore non negativo, mentre in caso di errore restituiscono EOF.

Quindi, per verificare se l'operazione di scrittura è andata a buon fine, è possibile utilizzare un controllo come il seguente:

if (fputs("Ciao, mondo!\n", stdout) == EOF) {
    // Gestione dell'errore
}

Funzioni di Input a linee: fgets e gets

Le funzioni fgets() e gets() sono utilizzate per leggere stringhe di testo da un file o dallo standard input (console).

In particolare, la loro firma è la seguente:

char *fgets(char *str, int n, FILE *stream);
char *gets(char *str);

Anche la funzione gets() è stata già vista in precedenza, per leggere stringhe dalla console. Essa legge una riga di testo dallo standard input e la memorizza nella stringa str, terminando la lettura quando viene incontrato un carattere di nuova linea (\n) o quando viene raggiunta la fine del file (EOF).

Da notare che il carattere di nuova linea non viene incluso nella stringa memorizzata, ma viene sostituito dal carattere di terminazione della stringa (\0).

Un esempio di utilizzo di gets() è il seguente:

#include <stdio.h>

int main() {
    char buffer[100];
    printf("Inserisci una riga di testo: ");
    gets(buffer);
    printf("Hai inserito: %s\n", buffer);
    return 0;
}

La funzione gets ha, tuttavia, un grave difetto di sicurezza: non effettua alcun controllo sulla lunghezza della stringa letta, il che può portare a buffer overflow se l'input supera la capacità del buffer. Per questo motivo, l'uso di gets() è fortemente sconsigliato e la funzione è stata rimossa dallo standard C11.

Al suo posto, esiste fortunatamente la funzione fgets(), che può essere considerata come una versione più generale e sicura di gets(). La funzione fgets() legge fino a n-1 caratteri da un file specificato dal parametro stream e memorizza la stringa risultante in str. La lettura termina quando viene incontrato un carattere di nuova linea, quando viene raggiunta la fine del file (EOF) o quando sono stati letti n-1 caratteri.

L'esempio di sopra può essere riscritto utilizzando fgets() in questo modo:

#include <stdio.h>

int main() {
    char buffer[100];
    printf("Inserisci una riga di testo: ");
    fgets(buffer, sizeof(buffer), stdin);
    printf("Hai inserito: %s", buffer);
    return 0;
}

Un'altra importante differenza è che fgets() include il carattere di nuova linea nella stringa memorizzata, se presente, mentre gets() lo sostituisce con il carattere di terminazione della stringa.

Entrambe le funzioni restituiscono un puntatore alla stringa letta in caso di successo, quindi sostanzialmente restituiscono un puntatore al buffer str. In caso di errore o se viene raggiunta la fine del file prima di leggere qualsiasi carattere, entrambe le funzioni restituiscono NULL. A questo punto, si possono usare le funzioni feof() e ferror() per determinare se la lettura è terminata a causa della fine del file o a causa di un errore.

Esempio

Proviamo a mettere insieme quanto visto finora in un semplice programma che legge un file di testo una riga alla volta, inverte l'ordine delle righe e le scrive su un altro file di testo.

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

#define MAX_LINE_LENGTH 256

void inverti_riga(char *sorgente, char *destinazione) {
    int len = strlen(sorgente);
    for (int i = 0; i < len; i++) {
        destinazione[i] = sorgente[len - i - 1];
    }
    destinazione[len] = '\0';
}

void rimuovi_nuova_linea(char *str) {
    size_t len = strlen(str);
    if (len > 0 && str[len - 1] == '\n') {
        str[len - 1] = '\0';
    }
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Uso: %s <file_input> <file_output>\n", argv[0]);
        return 1;
    }

    FILE *file_input = fopen(argv[1], "r");
    if (file_input == NULL) {
        fprintf(stderr, "Errore nell'apertura del file di input");
        return 1;
    }

    FILE *file_output = fopen(argv[2], "w");
    if (file_output == NULL) {
        fprintf(stderr, "Errore nell'apertura del file di output");
        fclose(file_input);
        return 1;
    }

    char buffer[MAX_LINE_LENGTH];
    char riga_invertita[MAX_LINE_LENGTH];

    while (fgets(buffer, sizeof(buffer), file_input) != NULL) {
        rimuovi_nuova_linea(buffer);
        inverti_riga(buffer, riga_invertita);
        fputs(riga_invertita, file_output);
        fputc('\n', file_output);
    }

    fclose(file_input);
    fclose(file_output);

    return 0;
}

In questo esempio abbiamo creato due funzioni di supporto: inverti_riga() per invertire l'ordine dei caratteri in una riga e rimuovi_nuova_linea() per rimuovere il carattere di nuova linea alla fine della riga letta.

Il programma principale apre il file di input in modalità lettura e il file di output in modalità scrittura. Quindi, legge ogni riga dal file di input utilizzando fgets(), rimuove il carattere di nuova linea, inverte la riga e la scrive nel file di output utilizzando fputs(), aggiungendo manualmente un carattere di nuova linea con fputc().

Se abbiamo un file di input chiamato input.txt con il seguente contenuto:

Ciao come stai?
Tutto bene?
A presto!

Lanciando il programma con:

./inverti input.txt output.txt

Il file di output output.txt conterrà:

?iats emoc oaiC
?eneb ottuT
!otserp A