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.
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:
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:
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 membronome
; - il secondo valore
"Rossi"
viene assegnato al membrocognome
; - il terzo valore
25
viene assegnato al membroeta
; - il quarto valore
"1234567890"
viene assegnato al membronumero_di_telefono
; - il quinto valore
"mario.rossi@prova.com"
viene assegnato al membroindirizzo_email
.
Il risultato finale sarà il seguente:
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.
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
.
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.
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.
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;
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.
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
.