Input e Output su Stringhe in Linguaggio C

Nella libreria standard del linguaggio C sono presenti delle particolari funzioni di input e output che permettono di leggere e scrivere dati non su file, bensì su stringhe di caratteri in memoria trattandole come se fossero degli stream.

Le funzioni sprintf e snprintf permettono di scrivere dati formattati all'interno di una stringa di caratteri in memoria, in modo simile a quanto fa la funzione printf per l'output su console e la funzione fprintf per l'output su file.

Analogamente, la funzione sscanf permette di leggere dati formattati da una stringa di caratteri in memoria, in modo simile a quanto fa la funzione scanf per l'input da console e la funzione fscanf per l'input da file.

Il vantaggio di adoperare tali funzioni è che possiamo sfruttare tutta la potenza delle funzioni di formattazione del linguaggio C, inclusi gli specificatori di formato, i flag, la larghezza del campo e la precisione, senza dover necessariamente scrivere su file o su console. Questo è particolarmente utile quando si desidera costruire stringhe complesse in memoria o analizzare stringhe di input senza dover interagire con dispositivi esterni.

Concetti Chiave
  • Le funzioni sprintf e snprintf permettono di scrivere dati formattati su stringhe di caratteri in memoria.
  • La funzione sscanf permette di leggere dati formattati da stringhe di caratteri in memoria.
  • Queste funzioni sono utili per costruire o analizzare stringhe complesse senza dover interagire con file o console.
  • snprintf è una versione più sicura di sprintf, in quanto previene gli overflow del buffer specificando la dimensione massima del buffer di destinazione.

Funzioni di Output su Stringhe: sprintf e snprintf

La funzione sprintf è molto simile alla funzione printf, che stampa sullo standard output, e alla funzione fprintf che invece stampa su di un file o stream. La differenza è che l'output della funzione viene salvato su una stringa, ossia un array di caratteri in memoria, passata come primo argomento alla funzione.

La sua dichiarazione è la seguente:

int sprintf(char *str, const char *format, ...);

Come si può vedere, il primo argomento str è un puntatore alla stringa di caratteri in cui verrà salvato l'output formattato. Il secondo argomento format è una stringa di formato che specifica come i successivi argomenti variabili devono essere formattati e inseriti nella stringa di output.

Come le altre funzioni della famiglia printf, la dichiarazione di sprintf termina con i tre puntini ..., che indicano che la funzione può accettare un numero variabile di argomenti, a seconda degli specificatori di formato presenti nella stringa format.

Ad esempio, supponendo di voler creare una stringa che contenga una data formattata, possiamo utilizzare sprintf come segue:

int giorno = 15;
int mese = 8;
int anno = 2023;
char data[20];

sprintf(data, "Data: %02d/%02d/%04d", giorno, mese, anno);
printf("%s\n", data); // Output: Data: 15/08/2023

In questo esempio, la funzione sprintf formatta la data utilizzando gli specificatori di formato %02d per il giorno e il mese (con due cifre e zeri iniziali) e %04d per l'anno (con quattro cifre). L'output formattato viene salvato nella stringa data.

La funzione sprintf, inoltre, esegue due altre operazioni importanti:

  1. Aggiunge automaticamente il carattere di terminazione della stringa \0 alla fine della stringa di output.
  2. Restituisce il numero di caratteri effettivamente scritti nella stringa di output, escluso il carattere di terminazione \0.

Se si verifica un errore, ad esempio un problema di encoding, la funzione restituisce un valore negativo.

sprintf può essere adoperata in vari scenari. Ad esempio, si potrebbe voler prima costruire una stringa complessa in memoria prima di inviarla a una funzione che la memorizzi su un file o la invii su una connessione di rete. Inoltre, può essere utile nel convertire dati numerici in stringhe per scopi di visualizzazione o logging.

Tuttavia, è importante notare che sprintf non esegue alcun controllo sulla dimensione del buffer di destinazione. Se la stringa formattata supera la dimensione del buffer, si verificherà un overflow del buffer, che può portare a comportamenti imprevisti o vulnerabilità di sicurezza. Per questo motivo, si sconsiglia l'uso di sprintf in favore di snprintf, che consente di specificare la dimensione massima del buffer di destinazione.

La funzione snprintf ha una dichiarazione simile a quella di sprintf, ma con un argomento aggiuntivo che specifica la dimensione massima del buffer di destinazione:

int snprintf(char *str, size_t size, const char *format, ...);

Essa è sostanzialmente una versione più sicura di sprintf, in quanto previene gli overflow del buffer limitando il numero di caratteri scritti nella stringa di output a size - 1, riservando un carattere per il terminatore \0.

Anche la funzione snprintf restituisce un valore negativo in caso di errore. In caso di successo, invece, si comporta in maniera leggermente diversa rispetto a sprintf: essa restituisce il numero di caratteri che sarebbero stati scritti se il buffer fosse stato sufficientemente grande, escludendo il carattere di terminazione \0. Questo significa che se il valore restituito è maggiore o uguale a size, significa che l'output è stato troncato. Il che permette al programmatore di sapere se il buffer era troppo piccolo e di agire di conseguenza.

Ad esempio, ecco come utilizzare snprintf per creare una stringa formattata in modo sicuro:

int giorno = 15;
int mese = 8;
int anno = 2023;
char data[10];

int n = snprintf(data, sizeof(data), "Data: %02d/%02d/%04d", giorno, mese, anno);
if (n < 0) {
    // Gestione dell'errore
} else if (n >= sizeof(data)) {
    // L'output è stato troncato
    printf("Buffer troppo piccolo, output troncato.\n");
} else {
    printf("%s\n", data); // Output: Data: 15/08/2023
}

Funzione di Input da Stringhe: sscanf

La funzione sscanf è simile alla funzione scanf, che legge dallo standard input, e alla funzione fscanf, che legge da un file o stream. La differenza è che sscanf legge i dati da una stringa di caratteri in memoria, passata come primo argomento alla funzione.

La sua dichiarazione è la seguente:

int sscanf(const char *str, const char *format, ...);

Come si può vedere, il primo argomento str è un puntatore alla stringa di caratteri da cui verranno letti i dati formattati. Il secondo argomento format è una stringa di formato che specifica come i successivi argomenti variabili devono essere interpretati e letti dalla stringa di input.

Anche in questo caso, la dichiarazione di sscanf termina con i tre puntini ..., che indicano che la funzione può accettare un numero variabile di argomenti, a seconda degli specificatori di formato presenti nella stringa format.

La funzione sscanf è particolarmente utile per estrarre dati da una stringa che è stata precedentemente costruita o ricevuta, ad esempio da un file di testo o da una connessione di rete.

Per esempio, si potrebbe pensare di usare la funzione fgets per leggere una riga di testo da un file e poi utilizzare sscanf per estrarre i valori numerici da quella riga.

Supponiamo, ad esempio, di voler leggere dei dati numerici da un file CSV (Comma-Separated Values) e di voler estrarre i valori in variabili separate.

Il file CSV di testo potrebbe contenere righe come la seguente:

123,45.67
214,89.01

Ossia, un intero seguito da una virgola e da un numero in virgola mobile.

Potremmo combinare fgets e sscanf per leggere ogni riga del file e estrarre i valori come segue:

char riga[100];
fgets(riga, sizeof(riga), file); // Legge una riga dal file

int id;
float valore;
sscanf(riga, "%d,%f", &id, &valore); // Estrae i valori dalla riga

A prima vista, si potrebbe pensare che utilizzare fgets e sscanf in questo modo sia inutile, dato che esiste la funzione fscanf che permette di leggere direttamente da un file in modo formattato. Il vantaggio di questa combinazione, però, è che permette di leggere una riga intera in memoria e poi di analizzarla quante volte si desidera, senza dover rileggere dal file. Questo può essere utile in situazioni in cui si desidera eseguire più passaggi di elaborazione sui dati letti, o quando si vuole una gestione migliore degli errori di parsing.

Pensiamo ad un esempio. Supponiamo di voler leggere da un file di testo delle date. Tuttavia, il file può contenere date in formati diversi, ad esempio GG/MM/AAAA oppure GG-MM-AAAA. Utilizzando fgets e sscanf, possiamo leggere ogni riga del file e poi tentare di analizzare la data in entrambi i formati:

char riga[100];
fgets(riga, sizeof(riga), file); // Legge una riga dal file

int giorno, mese, anno;
if (sscanf(riga, "%d/%d/%d", &giorno, &mese, &anno) == 3 ||
    sscanf(riga, "%d-%d-%d", &giorno, &mese, &anno) == 3) {
    // Data letta con successo
} else {
    // Errore di parsing della data
}

Questo non sarebbe stato possibile utilizzando direttamente fscanf dato che quest'ultima avrebbe avanzato la posizione corrente del file ad ogni tentativo di lettura, rendendo difficoltoso il tentativo di leggere la stessa riga più volte. Infatti per rileggere la stessa riga con fscanf sarebbe stato necessario poi usare la funzione fseek per riposizionare il cursore del file all'inizio della riga, complicando notevolmente il codice.

Come la funzione scanf, anche sscanf restituisce il numero di elementi letti con successo dalla stringa di input. In caso di fine file, che nel caso di sscanf si verifica quando non ci sono più dati da leggere nella stringa, la funzione restituisce EOF. Analogamente, se alcuni elementi non possono essere letti a causa di un errore di formattazione, la funzione restituisce il numero di elementi letti con successo.