Puntatori di Puntatori in Linguaggio C

In questo articolo esploreremo il concetto di puntatori di puntatori nel linguaggio C.

I puntatori di puntatori sono strumenti potenti che ci permettono di gestire strutture dati complesse come matrici dinamiche, strutture con membri dinamici e array jagged.

Attraverso esempi pratici, vedremo come dichiarare, inizializzare e utilizzare i puntatori di puntatori per manipolare dati in modo efficiente e flessibile. Scopriremo anche come allocare e liberare memoria dinamicamente per queste strutture, garantendo una gestione ottimale delle risorse.

Cos'è un Puntatore di Puntatori?

In linguaggio C, abbiamo visto che un puntatore è in grado di memorizzare l'indirizzo di memoria di un oggetto qualunque in memoria purché se ne specifichi il tipo. Ad esempio possiamo creare un puntatore ad int, char, float, double, ecc.

Possiamo addirittura creare puntatori a strutture, unioni ed enumerazioni. Portando questo ragionamento avanti, possiamo creare un puntatore che punta ad un altro puntatore. Questo tipo di puntatore è chiamato puntatore di puntatori. Li abbiamo già studiati indirettamente quando abbiamo visto gli array di stringhe (gli array Jagged) e gli argomenti da riga di comando.

Un puntatore di puntatori è una variabile che contiene l'indirizzo di un altro puntatore. In C, la sintassi per dichiarare un puntatore di puntatori è simile a quella per dichiarare un puntatore singolo, ma con un asterisco aggiuntivo. Ad esempio, int **ptr dichiara una variabile ptr che è un puntatore a un puntatore a un intero. Per assegnare un valore a un puntatore di puntatori, è necessario prima avere un puntatore a un intero e poi assegnare l'indirizzo di questo puntatore al puntatore di puntatori. Ecco un esempio:

int a = 10;
int *p = &a;
int **pp = &p;

In questo esempio, a è una variabile intera, p è un puntatore a a, e pp è un puntatore a p. Utilizzando pp, è possibile accedere al valore di a indirettamente.

Graficamente, possiamo vedere l'esempio di sopra in questo modo:

Diagramma del Puntatore di Puntatore dell'esempio
Figura 1: Diagramma del Puntatore di Puntatore dell'esempio

Attraverso il puntatore pp possiamo sia accedere al puntatore p che accedere al valore di a. Ciò dipende dal numero di volte che adoperiamo l'operatore di dereferenziazione *.

/* Senza operatore di dereferenziazione
 * accediamo all'indirizzo di p */
printf("Indirizzo di p: %p\n", pp);

/* Applicando un operatore di dereferenziazione
 * accediamo al valore di p
 * ossia l'indirizzo di a */
printf("Indirizzo di a: %p\n", *pp);

/* Applicando due operatori di dereferenziazione
 * accediamo al valore di a */
printf("Valore di a: %d\n", **pp);

Come si può notare nell'esempio di sopra, nel primo caso accediamo all'indirizzo di p, ossia il valore che effettivamente è memorizzato in pp.

Applicando una sola volta, l'operatore di dereferenziazione *, accediamo al valore di p, ossia l'indirizzo di a.

Infine, applicando due volte l'operatore di dereferenziazione *, accediamo al valore di a.

Ricapitolando:

Definizione

Puntatore di Puntatori

Un Puntatore di Puntatori è una variabile puntatore che contiene l'indirizzo di un altro puntatore.

Per dichiarare un puntatore di puntatori, si utilizza la sintassi seguente:

tipo **nome_puntatore;

Per inizializzare un puntatore di puntatori, è necessario avere un puntatore a un tipo di dato e assegnare l'indirizzo di questo puntatore al puntatore di puntatori:

tipo *puntatore = &variabile;
tipo **puntatore_di_puntatori = &puntatore;

Per accedere al valore finale (ossia il valore della variabile puntata dal puntatore a cui punta il puntatore di puntatori), è necessario applicare l'operatore di dereferenziazione * tante volte quanti sono i livelli di indirezione:

**puntatore_di_puntatori;

Questo meccanismo può essere esteso a più livelli di indirezione.

Esempio: Matrici Dinamiche

Uno degli usi più comunie dei puntatori di puntatori è la creazione di matrici dinamiche. Una matrice dinamica è una matrice il cui numero di righe e colonne è definito a tempo di esecuzione. Per creare una matrice dinamica, possiamo utilizzare un puntatore di puntatori.

Prendiamo un esempio: supponiamo di voler creare una matrice di 3 righe e 4 colonne composte da numeri in virgola mobile. Possiamo dichiarare la matrice in questo modo:

int rows = 3;
int cols = 4;

double **matrix;

Per allocare la memoria per la matrice, dobbiamo prima allocare la memoria per le righe e poi per le colonne. Ecco un esempio di come possiamo fare:

/* Allochiamo dapprima la memoria per le righe */
matrix = (double **) malloc(rows * sizeof(double *));

Nel codice di sopra bisogna notare che abbiamo usato l'operatore sizeof per calcolare la dimensione di un puntatore a double e non la dimensione di un double. Questo perché matrix è un puntatore di puntatori a double.

Il risultato è rappresentato in figura:

Allocazione Dinamica di una Matrice. Parte 1: Allocazione dell'array dei puntatori alle righe
Figura 2: Allocazione Dinamica di una Matrice. Parte 1: Allocazione dell'array dei puntatori alle righe

Ora possiamo allocare la memoria per le colonne:

for (int i = 0; i < rows; i++) {
    matrix[i] = (double *) malloc(cols * sizeof(double));
}

Fatto questo, il risultato è quello riportato nella figura che segue:

Allocazione Dinamica di una Matrice. Parte 2: Allocazione delle colonne
Figura 3: Allocazione Dinamica di una Matrice. Parte 2: Allocazione delle colonne

Ora possiamo accedere agli elementi della matrice come segue:

matrix[0][0] = 1.0;
matrix[0][1] = 2.0;
matrix[0][2] = 3.0;
/* ... */

Dobbiamo prestare attenzione quando vogliamo liberare la memoria allocata per la matrice. Dobbiamo liberare dapprima la memoria per ogni singola riga e poi la memoria per la matrice. Ecco un esempio:

for (int i = 0; i < rows; i++) {
    free(matrix[i]);
}

free(matrix);

L'uso di puntatori di puntatori per creare matrici dinamiche è un possibile modo di gestirle. L'altro modo è, come abbiamo visto, quello di appiattire la matrice ed utilizzare un array monodimensionale dinamico. Questo secondo metodo è più efficiente in termini di accesso ai dati, ma è meno intuitivo.

Esempio: Strutture con Membri dinamici allocate dinamicamente

Un altro uso comune dei puntatori di puntatori è quello di creare strutture con membri dinamici allocate dinamicamente. Supponiamo di voler creare una struttura Person che contiene un nome e un cognome, entrambi allocati dinamicamente. Possiamo definire la struttura in questo modo:

typedef struct {
    char *name;
    char *surname;
} Person;

Per allocare la memoria per una variabile di tipo Person, dobbiamo allocare la memoria per i membri name e surname. Ecco un esempio di come possiamo fare:

Person *person = (Person *) malloc(sizeof(Person));

Ma questo non basta. Dobbiamo allocare la memoria per i membri name e surname:

person->name = (char *) malloc(100 * sizeof(char));
person->surname = (char *) malloc(100 * sizeof(char));

Ora possiamo assegnare i valori ai membri name e surname:

strcpy(person->name, "Mario");
strcpy(person->surname, "Rossi");

Infine, possiamo liberare la memoria allocata per la variabile person:

free(person->name);
free(person->surname);
free(person);

Tecnicamente, person non è un puntatore di puntatori bensì un puntatore ad una struct. Tuttavia, person->name e person->surname sono membri di person che sono a loro volta puntatori. Quindi abbiamo sfruttato la doppia indirezione in maniere implicita attraverso il puntatore person.

Esempio: Array Jagged Dinamici

Abbiamo già incontrato gli Array Jagged o Matrici Irregolari quando abbiamo parlato degli array di stringhe. Un array jagged è un array di array di dimensioni diverse. Sono molto simili alle matrici viste sopra ma con la differenza che ogni riga può essere un array di dimensioni differenti.

Vediamo un esempio. Supponiamo di voler rappresentare un insieme di classi di studenti. Ogni classe è composta da un insieme di studenti differenti, soprattutto le classi possono differire anche per il numero di studenti presenti. Possiamo rappresentare questo insieme di classi con un array jagged.

Partiamo con il definire la struttura dati che rappresenta uno studente, con il suo nome e la sua età:

typedef struct {
    char *name;
    int age;
} Student;

Ora possiamo definire l'insieme delle classi come un array di array di studenti:

int num_classes = 3;
int num_students[] = {2, 3, 4};

Student **classes = (Student **) malloc(num_classes * sizeof(Student *));

Una classe in questo caso è un array di studenti. Quindi dobbiamo allocare la memoria per ogni classe:

for (int i = 0; i < num_classes; i++) {
    classes[i] = (Student *) malloc(num_students[i] * sizeof(Student));
}

Ora possiamo assegnare i valori ai membri name e age di ogni studente:

strcpy(classes[0][0].name, "Mario");
classes[0][0].age = 20;

strcpy(classes[0][1].name, "Luigi");
classes[0][1].age = 21;

/* ... */

Infine, possiamo liberare la memoria allocata per le classi:

for (int i = 0; i < num_classes; i++) {
    free(classes[i]);
}

free(classes);

In questo esempio, abbiamo utilizzato un array jagged per rappresentare un insieme di classi di studenti. Ogni classe è un array di studenti, e il numero di studenti in ogni classe può variare.

In Sintesi

In questo articolo abbiamo aggiunto un altro tassello al nostro bagaglio di conoscenze sui puntatori in linguaggio C.

Abbiamo visto che:

  • Un puntatore di puntatori è una variabile che contiene l'indirizzo di un altro puntatore.
  • Per dichiarare un puntatore di puntatori, si utilizza la sintassi tipo **nome_puntatore.
  • Per inizializzare un puntatore di puntatori, è necessario avere un puntatore a un tipo di dato e assegnare l'indirizzo di questo puntatore al puntatore di puntatori.
  • Per accedere al valore finale (ossia il valore della variabile puntata dal puntatore a cui punta il puntatore di puntatori), è necessario applicare l'operatore di dereferenziazione * tante volte quanti sono i livelli di indirezione.

Abbiamo visto anche alcuni esempi pratici di come utilizzare i puntatori di puntatori per creare matrici dinamiche, strutture con membri dinamici allocate dinamicamente e array jagged.

Adesso che sappiamo creare ed usare gli array jagged dinamici, nella prossima lezione affronteremo gli array di stringhe dinamici.