8  Livello 6

8.1 Le strutture

Come appena visto, gli array sono collezioni di elementi omogenei (caratterizzati dal medesimo tipo di dato). Attraverso le strutture (struct), possiamo invece definire tipi di dato nuovi e personalizzati. Le variabili dichiarate di tipo struttura possono aggregare diverse componenti (campi), ciascuna contraddistinta da uno specifico tipo di dato. Di seguito un esempio elementare di definizione di una struttura e della successiva dichiarazione di una variabile:

struct studente {
    int matricola;
    char nome[DIM_NOME];
    char cognome[DIM_NOME];
};
struct studente stud;

L’accesso ai singoli campi della struttura avviene mediante l’utilizzo dell’operatore punto (.), secondo la notazione: stud.matricola, stud.nome e stud.cognome. Per popolare una variabile di tipo struttura con dati acquisiti dallo standard input, si consideri il seguente frammento di codice:

#include <stdio.h>
#define DIM_NOME 20

struct studente {
    int matricola;
    char nome[DIM_NOME];
    char cognome[DIM_NOME];
};

int main(void) {
    struct studente stud;
    printf("\nNome: ");
    scanf("%s", stud.nome);    
    printf("\nCognome: ");
    scanf("%s", stud.cognome);    
    printf("\nMatricola: ");
    scanf("%d", &stud.matricola);    
    printf("\n\nDati studente: ");
    printf("%s %s, Matricola: %d\n", stud.nome, 
      stud.cognome, stud.matricola);    
    return 0;
}

Si noti che quando si utilizza la funzione scanf per l’acquisizione di una stringa (array di caratteri), non è necessario anteporre l’operatore & al nome del campo. Questo avviene perché la scanf richiede come argomento un indirizzo di memoria e l’identificatore di un array (e dunque di una stringa) rappresenta nativamente l’indirizzo al suo primo elemento. Di conseguenza, l’omissione dell’operatore & è corretta.

8.1.1 Gestione sicura delle stringhe: scanf e fgets

L’uso della scanf per la lettura di stringhe da tastiera è fortemente sconsigliato per due ragioni fondamentali:

  1. Non consente di verificare esplicitamente i limiti fisici dell’array di destinazione, esponendo il sistema a gravi vulnerabilità di buffer overflow qualora l’utente inserisca una stringa più lunga della capacità allocata.
  2. L’acquisizione del flusso viene interrotta non appena viene rilevato un carattere di spaziatura (spazio vuoto, tabulazione o ritorno a capo), rendendo impossibile l’acquisizione di stringhe composte da più parole.

Una soluzione consiste nell’impiego della funzione di libreria fgets. Si analizzi la versione aggiornata del codice:

#include <stdio.h>
#define DIM_NOME 20

struct studente {
    int matricola;
    char nome[DIM_NOME];
    char cognome[DIM_NOME];
};

int main(void) {
    struct studente stud;
    printf("\nNome: ");
    fgets(stud.nome, DIM_NOME, stdin);    
    printf("\nCognome: ");
    fgets(stud.cognome, DIM_NOME, stdin);    
    printf("\nMatricola: ");
    scanf("%d", &stud.matricola);
    printf("\n\nDati studente: ");
    printf("%s %s, Matricola: %d\n", stud.nome, 
      stud.cognome, stud.matricola);    
    return 0;
}

Come si può osservare, la funzione fgets richiede come secondo argomento la dimensione massima del buffer (DIM_NOME), garantendo che a runtime (durante l’esecuzione del programma) non scriva mai oltre il limite allocato. Il terzo parametro, stdin (standard input), specifica che il canale di comunicazione attivo è la tastiera. Nei capitoli successivi verrà mostrato come la medesima funzione possa essere applicata per la lettura di flussi di dati da file.

8.2 Strutture dati complesse: array di strutture

Oltre al caso appena esaminato in cui una struttura contiene degli array, il linguaggio C permette di elevare il livello di astrazione definendo array di strutture o strutture annidate (strutture che ospitano altre strutture al loro interno). Il seguente programma illustra la gestione di un elenco di record di studenti tramite un array di struct:

#include <stdio.h>
#define DIM_NOME 20
#define DIM_STUDI 100

struct studente {
    int matricola;
    char nome[DIM_NOME];
    char cognome[DIM_NOME];
};

int main(void) {
    struct studente elenco[DIM_STUDI];
    int n_studenti;
    char invio;
    printf("\nQuanti studenti si desidera inserire? ");
    scanf("%d", &n_studenti);    
    printf("\nInserimento dati:\n");
    for(int i = 0; i < n_studenti; ++i) {
        printf("\nStudente - %d: ", i + 1);        
        printf("\nNome: ");
        scanf("%c", &invio); // Consuma il carattere newline residuo
        fgets(elenco[i].nome, DIM_NOME, stdin);        
        printf("Cognome: ");
        fgets(elenco[i].cognome, DIM_NOME, stdin);        
        printf("Matricola: ");
        scanf("%d", &elenco[i].matricola);
    }
    printf("\n\nRegistro Studenti:\n");
    for(int i = 0; i < n_studenti; ++i) {
        printf("%s %s - Matricola: %d\n", elenco[i].nome, 
          elenco[i].cognome, elenco[i].matricola);
    }    
    return 0;
}
AvvisoLa Gestione del buffer di input

Si presti particolare attenzione all’inserimento dell’istruzione scanf("%c", &invio); antecedente alla fgets. Quando si acquisisce un valore numerico tramite scanf, il carattere di ritorno a capo (\n) generato dalla pressione del tasto Invio rimane memorizzato nel buffer dello standard input. Poiché la funzione fgets interrompe la lettura non appena incontra un newline, se il buffer non venisse preventivamente ripulito (o “consumato”), la fgets successiva leggerebbe immediatamente il carattere \n residuo, interpretandolo come una stringa vuota e saltando l’acquisizione del dato.

8.3 struct o typedef

Nel linguaggio C, la definizione e la successiva dichiarazione di una struttura possono essere affrontate con due paradigmi differenti: l’uso esplicito della parola chiave struct o l’impiego del costrutto typedef.

8.3.1 1. L’approccio standard (esplicito)

Nel primo caso, si definisce il modello della struttura e si dichiara la variabile ripetendo la parola chiave struct ad ogni istanza:

// Definizione
struct studente {
    int matricola;
    char nome[NOME];
    char cognome[NOME];
};

// Dichiarazione
struct studente mio_studente;

8.3.2 2. L’approccio con typedef (alias)

Nel secondo caso, si utilizza la parola chiave typedef per creare un vero e proprio alias (un nuovo identificatore di tipo) per la struttura. Questo approccio permette di omettere la parola struct durante la dichiarazione delle variabili, rendendo la sintassi più compatta:

// Definizione
typedef struct {
    int matricola;
    char nome[NOME];
    char cognome[NOME];
} studente;

// Dichiarazione
studente mio_studente;

8.3.3 Perché preferire struct?

I principali vantaggi dell’utilizzo diretto di struct includono:

  • Leggibilità e trasparenza: mantenere visibile la parola chiave struct impedisce di “mascherare” la natura del dato. Chi analizza il codice comprende immediatamente che sta manipolando una struttura.
  • Chiarezza del dominio utente: il rischio intrinseco del typedef è quello di creare un’astrazione opaca. Per comprendere questo rischio, si consideri la dichiarazione typedef int numero;. Essa permette di scrivere numero x = 10;, nascondendo tuttavia al lettore il fatto che x sia banalmente un intero.
  • Compatibilità storica: l’uso esplicito garantisce la massima aderenza e retrocompatibilità con i vecchi standard del linguaggio, facilitando l’integrazione e la manutenzione.