Variabili Struct in Linguaggio C

Una struttura dati è un insieme di elementi che sono logicamente correlati tra di loro. Tali elementi non necessariamente sono dello stesso tipo.

In linguaggio C è possibile dichiarare una variabile come una struttura dati utilizzando la parola chiave struct. Gli elementi che compongono la struttura prendono il nome di membri o campi della struttura.

In questa lezione vediamo come dichiarare una variabile di tipo struct e come accedere ai campi che la compongono.

struct

Finora, nelle lezioni sul linguaggio C abbiamo osservato esclusivamente un unico tipo di struttura dati: gli array. Gli array hanno la proprietà che tutti gli elementi che li compongono sono omogenei, ossia sono dello stesso tipo. Inoltre, per accedere agli elementi di un array abbiamo utilizzato l'operatore di indicizzazione: [], attraverso il quale specifichiamo la posizione dell'elemento a cui accedere. Questo è possibile perché gli array sono sequenze di elementi.

Adesso incominciamo lo studio delle strutture chiamate principalmente struct in linguaggio C. Le proprietà di una struct sono molto differenti da quelle di un array. Per prima cosa non vi è un vincolo sul tipo degli elementi che compongono una struttura. In gergo tecnico, gli elementi che compongono una struct prendono il nome di campi (in inglese fields) o più spesso membri (in inglese members). La principale proprietà dei campi di una struct è che essi possono avere tipi differenti.

La seconda proprietà degli elementi di una struct è che tali campi hanno un nome. Per accedere ad un membro di una struttura è necessario usare il nome del campo.

Dichiarazione di una variabile struct

Quando dobbiamo memorizzare un insieme di elementi dal tipo differente ma logicamente correlati tra di loro, una struttura dati è il mezzo ideale.

Per esempio, supponiamo di voler memorizzare le informazioni di un contatto telefonico. Questo perché magari stiamo implementando un programma che funge da rubrica.

In questo caso, un contatto telefonico conterrà varie informazioni:

  • il nome e il cognome del contatto;
  • l'età del contatto;
  • il numero di telefono del contatto;
  • l'indirizzo email del contatto.

Ovviamente, questi dati sono di tipi differenti. Il nome e il cognome sono stringhe, l'età è un numero intero, il numero di telefono è una stringa e l'indirizzo email è una stringa.

Per poter memorizzare insieme tutte queste informazioni logicamente correlate possiamo creare una variabile di tipo struct, in questo modo:

struct {
    char nome[20];
    char cognome[20];
    int eta;
    char numero_di_telefono[20];
    char indirizzo_email[20];
} contatto1, contatto2;

Facendo così, abbiamo dichiarato due variabili: contatto1 e contatto2. Ciascuna di esse avrà ben 5 membri: nome, cognome, eta, numero_di_telefono e indirizzo_email.

Bisogna notare che questa dichiarazione ha la stessa forma di una normale dichiarazione di una variabile. Infatti struct { ... } specifica un tipo, mentre contatto1 e contatto2 sono variabili di quel tipo.

Definizione

Dichiarazione di una variabile struttura dati

In linguaggio C la sintassi per dichiarare una variabile di tipo struttura dati è la seguente:

struct {
    tipo1 nome_membro1;
    tipo2 nome_membro2;
    /* ... */
    tipoN nome_membroN;
} nome_variabile1, nome_variabile2, ..., nome_variabileN;

Organizzazione in memoria di una struct

Tipicamente, in memoria i membri di una struttura sono disposti nell'ordine in cui essi sono dichiarati.

Ad esempio, supponiamo che la variabile contatto1 sia stata allocata all'indirizzo 4000. A questo punto il primo membro nome sarà memorizzato all'indirizzo 4000 e occuperà 20 byte; per cui nome occuperà i byte da 4000 a 4019. Il secondo membro cognome sarà memorizzato all'indirizzo 4020 e occuperà 20 byte; per cui cognome occuperà i byte da 4020 a 4039. Il terzo membro eta sarà memorizzato all'indirizzo 4040 ed essendo un int occuperà 4 byte: da 4040 a 4043. Stesso ragionamento vale per i membri numero_di_telefono e indirizzo_email. Alla fine la disposizione in memoria della struct avrà l'aspetto seguente:

Disposizione in memoria della struttura Contatto Telefonico
Figura 1: Disposizione in memoria della struttura Contatto Telefonico

Rappresentare strutture in questo modo può risultare tedioso, in quanto non abbiamo necessità di rappresentare tutti questi dettagli. Per tal motivo nel resto di queste lezioni rappresenteremo le strutture in questo modo più semplice:

Visualizzazione semplificata della struttura Contatto Telefonico
Figura 2: Visualizzazione semplificata della struttura Contatto Telefonico

Ambito di visibilità dei membri di una struct

Ogniqualvolta dichiariamo una variabile di tipo struct, stiamo definendo a tutti gli effetti un nuovo ambito o scope. Ciascun nome dichiarato all'interno di questo scope non può andare in conflitto con un nome dichiarato all'esterno di esso.

Per esempio, prendiamo questo stralcio di codice:

char nome[20];
int eta;

struct {
    char nome[20];
    char cognome[20];
    int eta;
    char numero_di_telefono[20];
    char indirizzo_email[20];
} contatto1, contatto2;

Le due variabili nome e eta dichiarate al di fuori della struct non confliggono con le variabili nome e eta dichiarate all'interno della struct. Questo perché i membri dichiarati all'interno della struct sono visibili solo all'interno di essa.

Inizializzazione di una struct

Come nel caso di un array una variabile struct può essere inizializzata nello stesso momento in cui viene dichiarata.

Per farlo è necessario fornire una lista di valori che devono essere memorizzati all'interno dei singoli membri della struttura e racchiuderli tra parentesi graffe.

Tornando all'esempio del contatto telefonico, possiamo inizializzare la variabile contatto1 nel modo seguente:

struct {
    char nome[20];
    char cognome[20];
    int eta;
    char numero_di_telefono[20];
    char indirizzo_email[20];
} contatto1 = {
    "Mario",
    "Rossi",
    25,
    "1234567890",
    "mario.rossi@prova.com" }

I valori nella lista di inizializzazione devono apparire nello stesso ordine con cui sono stati dichiarati i membri della struttura. Nell'esempio:

  • il primo valore "Mario" viene assegnato al membro nome;
  • il secondo valore "Rossi" viene assegnato al membro cognome;
  • il terzo valore 25 viene assegnato al membro eta;
  • il quarto valore "1234567890" viene assegnato al membro numero_di_telefono;
  • il quinto valore "mario.rossi@prova.com" viene assegnato al membro indirizzo_email.

Il risultato finale sarà il seguente:

Struttura Contatto Telefonico Inizializzata
Figura 3: Struttura Contatto Telefonico Inizializzata

L'inizializzazione di una struct segue approssimativamente le stesse regole di inizializzazione di un array:

  • Le espressioni di una lista di inizializzazione devono essere costanti; infatti non possiamo utilizzare variabili all'interno della lista (anche se nello standard C99 questo vincolo è rilassato);
  • Se il numero di elementi nella lista di inizializzazione è inferiore al numero di membri della struct, i membri rimanenti saranno inizializzati con valori di default.

Per quanto riguarda l'ultimo punto prendiamo l'esempio seguente:

struct {
    char nome[20];
    char cognome[20];
    int eta;
    char numero_di_telefono[20];
    char indirizzo_email[20];
} contatto1 = {
    "Mario",
    "Rossi",
    25,
    "1234567890" }

In questo caso il membro indirizzo_email non è stato inizializzato. Il compilatore provvederà a inizializzarlo con un valore di default, che dipende dal tipo di dato del membro. In questo caso il membro è di tipo array di char, quindi il compilatore inizializzerà il membro con una sequenza di zeri che corrisponde ad una stringa vuota.

Definizione

Inizializzazione di una variabile struttura dati

In linguaggio C per inizializzare una variabile di tipo struttura dati bisogna usare una lista di inizializzazione.

La sintassi è la seguente:

struct {
    tipo nome_membro1;
    tipo nome_membro2;
    ...
    tipo nome_membroN;
} nome_variabile = {
    valore_membro1,
    valore_membro2,
    ...
    valore_membroN
};

In caso la lista di inizializzazione non contenga tutti i membri della struct, i membri rimanenti saranno inizializzati con valori di default.

Inizializzatori Designati di una struct in C99

Così come per gli array, anche per le struct è possibile usare i cosiddetti inizializzatori designati. Questi inizializzatori ci permettono di inizializzare solo alcuni membri della struct e di lasciare gli altri membri inizializzati con valori di default.

Riprendiamo l'esempio del contatto telefonico. Normalmente per inizializzare la struct avremmo usato la seguente sintassi:

struct {
    char nome[20];
    char cognome[20];
    int eta;
    char numero_di_telefono[20];
    char indirizzo_email[20];
} contatto1 = {
    "Mario",
    "Rossi",
    25,
    "1234567890",
    "mario.rossi@prova.com" }

Utilizzando, nello standard C99, gli inizializzatori designati possiamo inizializzare la struct in questo modo:

struct {
    char nome[20];
    char cognome[20];
    int eta;
    char numero_di_telefono[20];
    char indirizzo_email[20];
} contatto1 = {
    .nome = "Mario",
    .cognome = "Rossi",
    .eta = 25,
    .numero_di_telefono = "1234567890",
    .indirizzo_email = "mario.rossi@prova.com"
}

L'inizializzatore designato per le struct ha una sintassi differente rispetto al caso degli array. Infatti, è necessario usare un punto . seguito dal nome del membro.

Gli inizializzatori designati hanno un grosso vantaggio rispetto alle semplici liste di inizializzazione. Infatti non è necessario rispettare l'ordine di dichiarazione dei membri. Possiamo inizializzare i membri in qualsiasi ordine; ad esempio:

struct {
    char nome[20];
    char cognome[20];
    int eta;
    char numero_di_telefono[20];
    char indirizzo_email[20];
} contatto1 = {
    .indirizzo_email = "mario.rossi@prova.com",
    .eta = 25,
    .cognome = "Rossi",
    .nome = "Mario",
    .numero_di_telefono = "1234567890"
}

Questo comporta due vantaggi. In primo luogo, il programmatore non deve ricordarsi dell'ordine in cui sono stati dichiarati i campi. Secondo, se dovessimo cambiare l'ordine dei campi o aggiungere campi intermedi non è necessario modificare il codice di inizializzazione.

Inoltre, non è necessario usare gli inizializzatori designati per tutti i membri della struttura. Possiamo mescolare gli inizializzatori designati con le liste di inizializzazione. Ad esempio:

struct {
    char nome[20];
    char cognome[20];
    int eta;
    char numero_di_telefono[20];
    char indirizzo_email[20];
} contatto1 = {
    .nome = "Mario",
    "Rossi",
    25,
    .numero_di_telefono = "1234567890",
    "mario.rossi@prova.com"
}

In tal caso, quando il compilatore incontra un valore di inizializzazione che non ha associato un inizializzatore designato, lo associa al primo membro della struct che non è stato ancora inizializzato. Per tal motivo non possiamo invertire le stringhe "Rossi" e "mario.rossi@prova.com". Questo perché, raggiunto il punto in cui il compilatore incontra la stringa "Rossi" il primo membro non inizializzato risulta essere cognome e non indirizzo_email.

Definizione

Inizializzazione di una variabile struttura dati con gli inizializzatori designati in C99

Nello standard C99 è possibile inizializzare una variabile di tipo struttura dati utilizzando gli inizializzatori designati.

La sintassi è la seguente:

struct {
    tipo nome_membro1;
    tipo nome_membro2;
    ...
    tipo nome_membroN;
} nome_variabile = {
    .nome_membro1 = valore_membro1,
    .nome_membro2 = valore_membro2,
    ...
    .nome_membroN = valore_membroN
};

L'ordine dei membri non è importante.

Accesso ai membri di una struct

Abbiamo visto che l'operazione più comune che si effettua sugli array è l'accesso ai singoli elementi. Nel caso delle struct l'operazione più comune risulta essere l'accesso ai singoli membri.

L'accesso ai membri di una struct avviene utilizzando il nome del membro e non l'indice come avviene, invece, per gli array.

Per accedere ad un membro di una struct si usa l'operatore punto .. La sintassi è la seguente:

nome_variabile_struct.nome_membro

Pertanto, ritornando all'esempio del contatto telefonico, supponiamo di voler stampare a schermo il nome, il cognome e l'indirizzo email. Possiamo farlo in questo modo:

printf("Nome: %s\n", contatto1.nome);
printf("Cognome: %s\n", contatto1.cognome);
printf("Indirizzo email: %s\n", contatto1.indirizzo_email);

I membri di una struttura sono sempre l-value e quindi possono essere usati anche come operandi di assegnamento.

contatto1.eta = 30;

strncpy(contatto1.nome, "Giovanni", 19);

L'operatore di accesso ai membri di una struttura, il punto ., ha la stessa priorità degli operatori di incremento e decremento postfissi, ++ e --. Pertanto, la sua priorità è la massima rispetto agli altri. Ad esempio, prendiamo il pezzo di codice seguente:

printf("Inserisci l'età: ");
scanf("%d", &contatto1.eta);

Il compilatore interpreta questo codice come:

printf("Inserisci l'età: ");
scanf("%d", &(contatto1.eta));

In altre parole, il punto ha la precedenza sull'operatore indirizzo &. Per cui, l'operatore indirizzo prenderà l'indirizzo del campo eta della struttura.

Definizione

Operatore di accesso ad un membro di una struttura

In linguaggio C, per accedere ad un membro di una struttura dati si utilizza l'opertore di accesso ..

La sintassi è la seguente:

nome_variabile_struct.nome_membro

Operatore di assegnamento e struct

Una grande differenza tra gli array e le struct è che l'operatore di assegnamento = non può essere usato per copiare due array. Infatti il codice seguente è illegale:

int a[10];
int b[10];

/* ERRORE */
a = b;

Viceversa, nel caso delle struct l'operatore di assegnamento = può essere usato per copiare due struct dello stesso tipo. Ad esempio:

struct {
    char nome[20];
    char cognome[20];
    int eta;
    char numero_di_telefono[20];
    char indirizzo_email[20];
} contatto1 = {
    .nome = "Mario",
    .cognome = "Rossi",
    .eta = 25,
    .numero_di_telefono = "1234567890",
    .indirizzo_email = "mario.rossi@prova.com"
},
contatto2 = {
    .nome = "Giovanni",
    .cognome = "Bianchi",
    .eta = 30,
    .numero_di_telefono = "0987654321",
    .indirizzo_email = "giovanni.bianchi@prova.com"
};

contatto1 = contatto2;

In questo caso, il compilatore copia tutti i membri di contatto2 in contatto1. Questo comportamento è simile a quello dell'operatore di assegnamento = per le variabili.

Il vincolo è che le due struct devono essere dello stesso tipo. Ad esempio, il seguente codice è illegale:

struct {
    int campo1;
    float campo2;
    char campo3[10];
} s1 = {
    5,
    3.14,
    "prova"
};

struct {
    char campo1[10];
    int campo2;
    float campo3;
} s2 = {
    "prova2",
    10,
    2.71
};

/* ERRORE */
s1 = s2;

In questo caso, il compilatore non può copiare tutti i membri di s2 in s1 perché i tipi dei membri non sono compatibili.

Definizione

Operatore di assegnamento e strutture dati

In linguaggio C è possibile assegnare il contenuto di una variabile di tipo struct ad un'altra variabile purché le due variabili abbiano lo stesso tipo.

La sintassi è la seguente:

struct {
    tipo1 campo1;
    tipo2 campo2;
    /* ... */
    tipoN campoN;
} variabile1, variabile2;

/* ... */

variabile1 = variabile2;
Consiglio

Usare le struct come array

Dal momento che in linguaggio C è possibile copiare strutture dati compatibili tra di loro, è possibile usare l'operatore di assegnamento per implementare una sorta di copia tra array.

Ad esempio, possiamo definire una struct così formata:

struct { int a[10]; } s1, s2;

In questo modo, s1 e s2 sono due strutture che contengono soltanto un array di 10 elementi. Possiamo quindi copiare tutti gli elementi di s1 in s2 usando l'operatore di assegnamento =.

s2 = s1;

Operazioni vietate su struct

In linguaggio C le uniche operazioni ammesse sulle variabili struct sono l'accesso ai membri, attraverso l'operatore di accesso . e l'operatore di assegnamento =.

Non esistono altre operazioni globali che è possibile effettuare su una struct. Ad esempio, non è possibile effettuare operazioni di confronto tra due struct:

struct {
    int campo1;
    float campo2;
    char campo3[10];
} s1 = {
    5,
    3.14,
    "prova"
},
s2 = {
    10,
    2.71,
    "prova2"
};

/* ERRORE */
if (s1 == s2) {
    printf("Le strutture sono uguali\n");
}

Per poter effettuare un confronto dobbiamo modificare il codice di sopra in questo modo:

struct {
    int campo1;
    float campo2;
    char campo3[10];
} s1 = {
    5,
    3.14,
    "prova"
},
s2 = {
    10,
    2.71,
    "prova2"
};

if (s1.campo1 == s2.campo1 && s1.campo2 == s2.campo2 && strcmp(s1.campo3, s2.campo3) == 0) {
    printf("Le strutture sono uguali\n");
}

In altre parole abbiamo dovuto effettuare un confronto tra tutti i membri delle due struct per poter stabilire se sono uguali o meno.

Definizione

Confronto tra strutture dati

In linguaggio C, non è possibile confrontare due variabili struct utilizzando un operatore di confronto. Per poter effettuare un confronto, è necessario confrontare tutti i membri delle due struct.

struct {
    tipo1 campo1;
    tipo2 campo2;
    /* ... */
    tipoN campoN;
} variabile1, variabile2;

/* ERRORE */
if (variabile1 == variabile2) {
    /* ... */
}

/* CORRETTO */
if (variabile1.campo1 == variabile2.campo1 &&
    variabile1.campo2 == variabile2.campo2 &&
    /* ... */ &&
    variabile1.campoN == variabile2.campoN) {
    /* ... */
}

In Sintesi

In questa lezione abbiamo visto come definire una struct e come inizializzarla. Abbiamo visto come accedere ai membri di una struct e come copiare due struct dello stesso tipo.

Inoltre, abbiamo visto che le uniche operazioni ammesse sulle variabili struct sono l'accesso ai membri, attraverso l'operatore di accesso . e l'operatore di assegnamento =.

L'unico problema è che, ogniqualvolta dobbiamo dichiarare una variabile struct dobbiamo specificare il tipo di tutti i membri. Questo può essere un problema se dobbiamo dichiarare molte variabili struct dello stesso tipo. Per questo motivo, nella prossima lezione vedremo come definire un nuovo tipo di dato, utilizzando le struct.