Le strutture

Una struttura é un tipo di dato derivato costituito da un insieme di elementi anche di diverso tipo.

La dichiarazione di un struttura avviene attraverso la seguente sintassi:

struct tag_struttura {
    tipo identificatore;
    tipo identificatore;
    tipo identificatore;
} identificatore_struttura;

Può essere dichiarato anche senza identificatore come schema tipo-struttura da applicare ad una successiva creazioni di una variabile di tipo struttura

struct tag_struttura identificatore_struttura;

Le strutture hanno uno spazio dei nomi riservato per cui si possono utilizzare nomi di tag e di membri anche dello stesso nome di quelli già utilizzati nel programma, avendo a tutti gli effetti uno scope separato per ogni struttura.

Inizializzazione

I membri di una struttura vengono inizializzati con una lista di inizializzatori sequenziale, corrispondente nell’ordine delle assegnazioni a quello contenuto nella lista dei membri indicata in fase di dichiarazione.

struct tag_struttura {
    tipo identificatore;
    tipo identificatore2;
    tipo identificatore3;
} identificatore_struttura = { valore1, valore2, valore3 };

In alternativa é possibile utilizzare gli inizializzatori designati, esplicitati nella seguente sintassi:

struct tag_struttura identificatore_struttura = {
    .identificatore = valore,
    .identificatore2 = valore
};

Facendo precedere l’operatore . al nome del membro della funzione da inizializzare. In questo caso l’inizializzazione può avvenire in modo selettivo. Tutto ciò che non viene espressamente inizializzato viene inizializzato a 0, o, nel caso dei puntatori, con un puntatore a NULL. Questo ci consente quindi operare sui singoli membri anche attraverso l’utilizzo dell’identificatore della struttura:

//Per la lettura
tipo identificatore_variabile = identificatore_struttura.identificatore_membro
//Per la scrittura
identificatore_struttura.identificatore_membro = valore;

Vale la pena ricordare il corretto modo di copiare una stringa all’interno di una struttura, servendoci della funzione strcpy (contenuta nell’header <string.h>):

struct tag_struttura {
    int variabile_intera;
    char variabile_stringa[MAX];
} identificatore_struttura;

strcpy( identificatore_struttura.variabile_stringa, “valore” );

E’ altresì possibile assegnare tutti i membri di una struttura ad un’altra struttura, purché siano uguali, cioè abbiano lo stesso tipo-struttura, oppure siano assegnate contestualmente alla dichiarazione di una struttura senza tag. Tra strutture non é possibile fare operazioni di confronto (==, >=).

La keyword typedef

Per rendere più agevole la dichiarazione di una struttura è possibile utilizzare la keyword typedef affinché si possa “invocare” una struttura-tipo in modo molto semplice:

typdef struct tag_struttura {
    int variabile_intera;
    char variabile_stringa[MAX];
} alias;

//Invocando con:
alias identificatore_struttura = { 150, “stringa\n” };

Dimensione in memoria delle strutture

Le strutture occupano una dimensione di memoria corrispondente a quella che sarebbe stata altrimenti necessaria per ognuna delle sue variabili, più la quantità di holes necessari all’allineamento dei dati. Gli holes sono quelle aree di memoria necessarie al riempimento degli spazi di memoria inutilizzati affinché il dato abbia una una dimensione e distribuzione uniforme nelle aree di memoria che utilizza.

Allo scopo di ottimizzare l’allineamento é preferibile ordinare le variabili nella dichiarazione della struttura in modo decrescente sulla base della quantità di memoria dedicata.

Strutture senza nome

Dallo standard C99 é possibile dichiarare strutture “senza nome” da utilizzare on-demand.

typedef struct {
    int variabile_intera;
    char variabile_stringa[MAX];
} alias;

alias identificatore_struttura = (tag_struttura){ 150, “stringa\n” };

Strutture annidate

Una struttura annidata é una struttura dove i suoi membri sono a loro volta delle strutture.

struct struttura_figlio identificatore_figlio = {
    tipo identificatore;
    tipo identificatore2;
};

struct struttura_padre identificatore_padre = {
    struct struttura_figlio identificatore_figlio;
    .identificatore1 = valore;
    .identificatore2 = valore;
};

//Inizializzazione
struct struttura_padre identificatore_padre = {
    { valore1, valore2 },
    150
};

//Accesso
printf(“%d”, identificatore_padre.identificatore_figlio.identificatore2 );

Array e strutture

Un array di strutture é un vettore all’interno del quale ogni elemento é a sua volta una struttura.

#define MAX 5
struct tag_struttura {
    tipo identificatore;
    tipo identificatore2;
};

struct tag_struttura identificatore_struttura[MAX] = {
    { valore01, valore02, valore03, ecc },
    //Sintassi alternative
    [1] = { valore11, valore12, valore13, ecc }, 
    {.identificatore = valore21, .identificatore2 = valore31 }
};

//Accesso
struct tag_struttura identificatore_variabile = identificatore_struttura[0]

Strutture e puntatori

Una struttura possiede un proprio indirizzo di memoria a partire dal quale vi sono memorizzati tutti i suoi dati. E’ possibile utilizzare questo indirizzo all’interno di un puntatore a struttura.

struct tag_struttura {
    tipo identificatore_variabile;
    tipo identificatore_variabile2;
} identificatore_struttura;

struct tag_struttura *puntatore_struttura = &identificatore_struttura

//Accesso
valore = puntatore_struttura->identificatore_variabile;

//Sintassi alternativa
valore = (*puntatore_struttura).identificatore_variabile;

//Caso array
struct *puntatore_struttura = &identificatore_struttura[0]

//Oppure interessante
puntatore_struttura += (MAX - 1) //Punta all’ultimo elemento

Strutture e funzioni

Le strutture possono essere utilizzate come argomenti di funzione, o come tipi di ritorno delle funzioni stesse. Se da un lato questo garantisce un certo grado di protezione dei dati della funzione passata, dall’altro strutture di una certa dimensione possono determinare un certo overhead di risorse, salvo che non si utilizzi un puntatore, ma in questo caso verrebbe meno il vantaggio della protezione. Vediamo la sintassi:

struct punto {
    int x;
    int y;
    int somma;
};

//Prototipi di funzione
struct punto nuovoPunto(struct punto p1, struct punto p2);

int main(void){
    /.../
}

struct point nuovoPunto{
    return (struct point){ .x = x, .y = y};
}

Strutture e VLA

Le strutture non possono avere VLA come membri, ma a partire dallo standard C99 sono stati introdotti i membri array flessibili, la cui dimensione può essere omessa, a patto che:

  • siamo dichiarati come ultimi membri della struttura
  • non siano gli unici membri della struttura
  • siano i soli array della struttura
  • non devono contenere alcun valore tra le parentesi quadre [ ]
struct punto {
    int x;
    int y;
    int array[];
};

Una volta dichiarato un membro array flessibile all’interno di una struttura, viene ad esso dedicato un indirizzo di memoria a partire dal quale saranno allocati i suoi elementi. Per compiere questa operazione viene utilizzata la funzione malloc:

int array_flessibile = 4;

struct punto {
    int x;
    int y;
    int array[];
};

struct punto *punto1 = malloc(sizeof(struct punto) + array_flessibile * sizeof(int));

Oltre a questa operazione é bene ricordare anche che i membri array flessibili:

  • non vengono copiate in altre strutture
  • non si devono inizializzare con una lista di inizializzatori
  • non si devono utilizzare più volte tra più strutture

Strutture anonime

A partire dallo standard C11 è possibile dichiarare anche strutture anonime, cioè prive di tag o di identificatore, quali membri di altre strutture:

struct punto {
    int x;
    int y;

    struct { //Struttura anonima
        int x1;
        int x2;
    }
};

L’inizializzazione avviene poi come se x1 ed x2 siano membri della struttura contenitrice:

struct punto {
    0,
    0,
    { 1 , 2 }
};

Campi di bit

E’ possibile operare sulle strutture anche bit-per-bit, attraverso i cosiddetti “campi di bit”, specificando per ogni membro della struttura il numero di bit necessari al contenimento del dato, in modo da evitare sprechi di memoria:

struct tag_struttura {
    tipo identificatore_membro : numero_bit;
    tipo identificatore_membro : numero_bit;
    tipo identificatore_membro : numero_bit;
};

A seconda del numero di bit utilizzati potremo così contenere variabili adatte ad ospitare dati più piccoli. Tale procedura é utilizzabile solo per le variabili di tipo int, signed int, unsigned int e Bool. La dimensione del dato viene esplicitata mediante l’utilizzo dell’operatore : seguito dal numero di bit necessari, ottenibile elevando 2 al numero_bit.

Per via delle sue dimensioni atipiche, l’area di memoria occupata da un campo di bit non é referenziabile attraverso l’operatore di indirizzamento &.

Luca Scandroglio

Sono un consulente tecnico informatico, un web designer e uno sviluppatore italiano. Aiuto le aziende a dotarsi degli strumenti tecnologici e digitali per superare le sfide del mercato di oggi.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *