Articolo originale: Pointers in C Explained – They're Not as Difficult as You Think

I puntatori sono probabilmente la funzionalità di C più difficile da capire. Tuttavia sono una delle funzionalità che rendono C un linguaggio eccellente.

In questo articolo, partiremo dagli aspetti base dei puntatori fino ad arrivare al loro utilizzo con array, funzioni e strutture.

Quindi rilassati, prendi un caffè, e preparati per imparare tutto sui puntatori.

Sommario

A. Fondamenti
  1. Cosa sono esattamente i puntatori?
  2. Definizione e Notazione
  3. Alcuni Puntatori Speciali
  4. Puntatori Aritmetici
B. Array e Stringhe
  1. Perché puntatori e array?
  2. Array 1-D
  3. Array 2-D
  4. Stringhe
  5. Array di Puntatori
  6. Puntatori ad Array
C. Funzioni
  1. Chiamata per Valore vs. Chiamata per Riferimento
  2. Puntatori come Argomenti di Funzione
  3. Puntatori come Valori di Ritorno da Funzione
  4. Puntatore a Funzione
  5. Array di Puntatori a Funzioni
  6. Puntatore a Funzione come Argomento
D. Strutture
  1. Puntatore a Struttura
  2. Array di Strutture
  3. Puntatore a Struttura come Argomento
E. Puntatore a Puntatore
F. Conclusione

A. Fondamenti

1. Cosa sono esattamente i puntatori?

Prima di passare alla definizione di puntatori, cerchiamo di capire cosa avviene quando scriviamo il seguente codice:

int digit = 42;

Un blocco di memoria è riservato dal compilatore per conservare un valore int. Il nome del blocco è digit e il valore assegnato a questo blocco è 42.

Ora, per ricordare il blocco, a esso viene assegnato un indirizzo o un numero di posizione (diciamo 24650).

Il valore del numero di posizione non è importante per noi, visto che è un valore casuale. Tuttavia possiamo accedere a questo indirizzo usando il carattere & (e commerciale) o indirizzo dell'operatore.

printf("Indirizzo di digit = %d.",&digit);
 /* stampa "Indirizzo di digit = 24650. */

Possiamo ottenere il valore della variabile digit dal suo indirizzo usando un altro operatore, * (asterisco), chiamato operatore di riferimento indiretto o dereferenziazione o valore all'indirizzo.

printf("Valore di digit = %d.", *(&digit);
 /* stampa "Valore di digit = 42. */

2. Definizione e Notazione

L'indirizzo di una variabile può essere conservato in un'altra variabile nota come variabile puntatore. La sintassi per conservare l'indirizzo di una variabile in un puntatore è:

tipoDato *nomeVariabilePuntatore = &nomeVariabile;

Per la nostra variabile digit, ciò si può scrivere in questo modo:

int *addressOfDigit = &digit;

oppure:

int *addressOfDigit;
addressOfDigit= &digit;
Declaration and Definition

Questo si può leggere come  - Un puntatore a int (intero) addressOfDigit conserva l' indirizzo (addressOf(&)) della variabile digit.

Alcuni punti da capire:

tipoDato – Dobbiamo dire al computer quale tipo di dato conterrà la variabile il cui indirizzo andremo a conservare. In questo caso int era il tipo di dato di  digit.

Questo non vuol dire che addressOfDigit conserverà un valore di tipo int. Un puntatore a intero (come addressOfDigit) può solo conservare l'indirizzo di variabili di tipo int.

int variable1;
int variable2;
char variable3;
int *addressOfVariables;

* – Una variabile puntatore è una variabile speciale nel senso che viene usata per conservare un indirizzo di un'altra variabile. Per differenziarla dalle altre variabili che non conservano un indirizzo, usiamo * come simbolo nella dichiarazione.

Qui possiamo assegnare l'indirizzo di  variable1 e variable2 al puntatore di interi addressOfVariables ma non variable3 visto che è di tipo char. Ci serverebbe una variabile puntatore a carattere per contenere il suo indirizzo.

Possiamo usare la nostra variabile puntatore addressOfDigit per stampare l'indirizzo e il valore di  digit in questo modo:

printf("Indirizzo di digit = %d.", addressOfDigit);
 /* stampa "Indirizzo di digit = 24650." */
printf("Valore di digit = %d.", *addressOfDigit);
 /* stampa "Valore di digit = 42. */

Qui, *addressOfDigit può essere letto come il valore all'indirizzo conservato in addressOfDigit.

Nota che abbiamo usato %d come identificatore di formato per addressOfDigit. In effetti questo non è completamente corretto. L'identificatore corretto sarebbe %p.

Usando %p, l'indirizzo viene visualizzato come valore esadecimale. Un indirizzo di memoria può tuttavia essere visualizzato anche come intero oppure come valore ottale. Per questo, visto che non è il modo interamente corretto, viene mostrato un avvertimento.

int num = 5;
int *p = #
printf("Indirizzo usando %%p = %p",p);
printf("Indirizzo usando %%d = %d",p);
printf("Indirizzo usando %%o = %o",p);

Questo il risultato in base al compilatore che sto usando:

Indirizzo usando %p = 000000000061FE00
Indirizzo usando %d = 6422016
Indirizzo usando %o = 30377000
Questo è l'avvertimento mostrato quando usi  %d - " warning: format '%d' expects argument of type 'int', but argument 2 has type 'int *' " ("avvertimento: format %d si aspetta argomenti di tipo 'int', ma l'argomento 2 è di tipo 'int *').

3. Alcuni Puntatori Speciali

Il Puntatore Selvaggio

char *alphabetAddress; /* non inizializzato o puntatore selvaggio */
char alphabet = "a";
alphabetAddress = &alphabet; /* ora non più puntatore selvaggio */

Quando abbiamo definito il nostro puntatore a carattere alphabetAddress, non lo abbiamo inizializzato.

Tali puntatori sono noti come puntatori selvaggi (wild pointers). Conservano un valore spazzatura (vale a dire l'indirizzo di memoria) di un byte che non sappiamo se sia riservato o meno (ricorda int digit = 42;, abbiamo riservato un indirizzo di memoria quando l'abbiamo dichiarata).

Supponiamo di dereferenziare un puntatore selvaggio e di assegnare un valore all'indirizzo di memoria al quale sta puntando. Questo provocherà un comportamento inatteso visto che andremo a scrivere dati in un blocco di memoria che potrebbe essere libero oppure riservato.

Puntatore Null

Per assicurarci di non avere un puntatore selvaggio, possiamo inizializzare un puntatore con un valore NULL , rendendolo un puntatore null (null pointer).

char *alphabetAddress = NULL /* Puntatore null */ 

Un puntatore null punta al nulla, oppure a un indirizzo di memoria al quale gli utenti non possono accedere.

Puntatore Void

Un puntatore void può essere usato per puntare a una variabile associata a qualsiasi tipo di dato. Può essere riutilizzato per puntare a qualsiasi tipo di dato vogliamo. Viene dichiarato in questo modo:

void *puntatoreANomeVariabile = NULL;

Vista la loro natura molto generica, sono anche noti come puntatori generici (generic pointers).

Con la loro flessibilità, i puntatori void sono anche dotati di qualche vincolo. I puntatori void non possono essere dereferenziati come qualsiasi altro puntatore. È necessaria un'appropriata conversione di tipo (typecasting).

void *pointer = NULL;
int number = 54;
char alphabet = "z";
pointer = &number;
printf("Il valore di number = ", *pointer); /* Errore di compilazione */
/* Metodo corretto */
printf("Il valore di number = ", *(int *)pointer); /* stampa "Il valore di number = 54" */
pointer = &alphabet;
printf("Il valore di alphabet = ", *pointer); /* Errore di compilazione */
printf("Il valore di alphabet = ", *(char *)pointer); /* stampa "Il valore di alphabet = z */

Allo stessa stregua, i puntatori void richiedono una conversione di tipo per eseguire operazioni aritmetiche.

I puntatori void sono di grande uso in C. Funzioni di libreria come malloc() e calloc() che allocano dinamicamente memoria, ritornano puntatori void. qsort(), una funzione integrata di ordinamento in C, riceve una funzione come suo argomento che a sua volta riceve puntatori void come suoi argomenti.

Puntatori Pendenti

Un puntatore pendente (dangling pointer) punta a un indirizzo di memoria in uso per conservare una variabile. Visto che l'indirizzo a cui punta non è più riservato, l'uso potrebbe portare a risultati inaspettati.

main(){
  int *ptr;
  ptr = (int *)malloc(sizeof(int));
  *ptr = 1;
  printf("%d",*ptr); /* stampa 1 */
  free(ptr); /* deallocazione */
  *ptr = 5;
  printf("%d",*ptr); /* potrebbe o non potrebbe stampare 5 */
}

Anche se la memoria è stata deallocata da free(ptr), il puntatore a intero ptr punta ancora a quell'indirizzo di memoria non più riservato.

4. Puntatori Aritmetici

Ora sappiamo che i puntatori non sono come le altre variabili. Non conservano alcun valore, ma l'indirizzo di blocchi di memoria.

Pertanto dovrebbe essere piuttosto chiaro che non tutte le operazioni aritmetiche sarebbero valide con essi. Avrebbe senso moltiplicare o dividere due puntatori (che contengono indirizzi)?

I puntatori hanno poche ma immensamente utili operazioni valide:

  1. Si può assegnare il valore di un puntatore a un altro solo se sono dello stesso tipo (a meno che non siano oggetto di conversione di tipo oppure uno di essi sia void *).
int ManU = 1;
int *addressOfManU = &ManU;
int *anotherAddressOfManU = NULL;
anotherAddressOfManU = addressOfManU; /* Valido */
double *wrongAddressOfManU = addressOfManU; /* Non Valido */

2.   Puoi solo aggiungere o sottrarre interi ai puntatori.

int myArray = {3,6,9,12,15};
int *pointerToMyArray = &myArray[0];
pointerToMyArray += 3; /* Valido */
pointerToMyArray *= 3; /* Non Valido */

Quando aggiungi (o sottrai) un intero (diciamo n) a un puntatore, non stai effettivamente aggiungendo (o sottraendo) n byte al valore del puntatore. Stai in realtà aggiungendo (o sottraendo) n-volte in byte la dimensione del tipo di dato della variabile puntata.

int number = 5;
 /* Supponiamo che l'indirizzo di number sia 100 */
int *ptr = &number;
int newAddress = ptr + 3;
 /* Uguale a ptr + 3 * sizeof(int) */

Il valore conservato in newAddress non sarà 103, ma 112.

3. La sottrazione e il confronto di puntatori è valida solo se entrambi sono membri dello stesso array. La sottrazione tra puntatori fornisce il numero di elementi che li separano.

int myArray = {3,6,9,12,15};
int sixthMultiple = 18;
int *pointer1 = &myArray[0];
int *pointer2 = &myArray[1];
int *pointer6 = &sixthMuliple;
 /* Espressione valida */
if(pointer1 == pointer2)
pointer2 - pointer1;
 /* Espressione non valida */
if(pointer1 == pointer6)
pointer2 - pointer6

4.  Puoi assegnare o confrontare un puntatore con  NULL.

L'unica eccezione alle regole qui sopra è che l'indirizzo del primo blocco di memoria dopo l'ultimo elemento di un array segua un puntatore aritmetico.

Puntatori e array coesistono. Queste valide manipolazioni di puntatori sono immensamente utili con gli array, e le discuteremo nella prossima sezione.

B. Array e Stringhe

1. Perché puntatori e array?

In C, i puntatori e gli array hanno una relazione piuttosto forte.

La ragione per la quale dovrebbero essere discussi insieme è che quello che puoi ottenere con la notazione di array (nomeArray[indice]) può anche essere ottenuto con i puntatori, in genere più velocemente.

2. Array 1-D

Osserviamo cosa succede quando scriviamo int myArray[5];.

Cinque blocchi consecutivi di memoria a partire da myArray[0] fino a myArray[4] sono creati con valori spazzatura al loro interno. Ciascun blocco ha una dimensione di 4 byte.

Pertanto se l'indirizzo di myArray[0] è, diciamo, 100, l'indirizzo dei restanti blocchi sarebbe 104, 108, 112 e 116.

Dai un'occhiata al seguente codice:

int prime[5] = {2,3,5,7,11};
printf("Risultato usando &prime = %d\n",&prime);
printf("Risultato usando prime = %d\n",prime);
printf("Risultato usando &prime[0] = %d\n",&prime[0]);

/* Output */
Risultato usando &prime = 6422016
Risultato usando prime = 6422016
Risultato usando &prime[0] = 6422016

Quindi, &prime, prime, e &prime[0] danno tutti lo stesso indirizzo, giusto? Bene, aspetta e continua a leggere, perché avrai una sorpresa (e forse un po' di confusione).

Cerchiamo di incrementare  &prime, prime, e &prime[0] di 1.

printf("Risultato usando &prime = %d\n",&prime + 1);
printf("Risultato usando prime = %d\n",prime + 1);
printf("Risultato usando &prime[0] = %d\n",&prime[0] + 1);

/* Output */
Risultato usando &prime = 6422036
Risultato usando prime = 6422020
Risultato usando &prime[0] = 6422020

Aspetta! Come mai il risultato di &prime + 1 è diverso dagli altri due? Perché prime + 1 e &prime[0] + 1 sono ancora uguali? Rispondiamo a queste domande.

prime e &prime[0] puntano entrambi all'elemento 0 dell'array prime. Quindi il nome stesso di un array è a sua volta un puntatore all'elemento 0 dell' array.

Qui entrambi puntano al primo elemento di 4 byte di dimensione. Quando aggiungi 1 ad essi, ora puntano all'elemento con indice 1 dell'array. Ne consegue un aumento nell'indirizzo di 4.

&prime, d'altro canto, è un puntatore a un array int di dimensione 5. Conserva l'indirizzo base dell'array prime[5], che è uguale all'indirizzo del primo elemento. Pertanto un aumento di 1 risulta in un indirizzo con un aumento di 5 x 4 = 20 byte.

In breve, arrayName e &arrayName[0] puntano all'elemento 0 mentre  &arrayName punta all'intero array.

1-D Array

Possiamo accedere agli elementi dell'array usando variabili indicizzate come questa:

int prime[5] = {2,3,5,7,11};
for( int i = 0; i < 5; i++)
{
  printf("indice = %d, indirizzo = %d, valore = %d\n", i, &prime[i], prime[i]);
}

Possiamo fare la stessa cosa usando i puntatori che sono sempre più veloci rispetto all'uso dell'indicizzazione.

int prime[5] = {2,3,5,7,11};
for( int i = 0; i < 5; i++)
{
  printf("indice = %d, indirizzo = %d, valore = %d\n", i, prime + i, *(prime + i));
}

Entrambi i metodi danno questo risultato:

indice = 0, indirizzo = 6422016, valore = 2
indice = 1, indirizzo = 6422020, valore = 3
indice = 2, indirizzo = 6422024, valore = 5
indice = 3, indirizzo = 6422028, valore = 7
indice = 4, indirizzo = 6422032, valore = 11

Quindi &arrayName[i] e arrayName[i] sono lo stesso di  arrayName + i e  *(arrayName + i), rispettivamente.

3. Array 2-D

Gli array bidimensionali sono array di array.

int marks[5][3] = { { 98, 76, 89},
                    { 81, 96, 79},
                    { 88, 86, 89},
                    { 97, 94, 99},
                    { 92, 81, 59}
                  };

Si può pensare a marks come un array di 5 elementi, ognuno dei quali è un array monodimensionale che contiene 3 interi. Lavoreremo su una serie di programmi per comprendere le diverse espressioni di indicizzazione.

printf("L'indirizzo dell'intero array 2-D = %d\n", &marks);
printf("L'aggiunta di 1 risulta in %d\n", &marks +1);

/* Output */
L'indirizzo dell'intero array 2-D = 6421984
L'aggiunta di 1 risulta in 6422044

Come con gli array 1-D, &marks punta all'intero array 2-D, marks[5][3]. Pertanto incrementando di 1 ( = 5 array x 3 interi ciascuno x 4  byte = 60) risulta un aumento di 60 byte.

printf("Indirizzo dell'array 0 = %d\n", marks);
printf("L'aggiunta di 1 risulta in %d\n", marks +1);
printf("Indirizzo dell'array 0 =%d\n", &marks[0]);
printf("L'aggiunta di 1 risulta in %d\n", &marks[0] + 1);

/* Output */
Indirizzo dell'array 0 = 6421984
L'aggiunta di 1 risulta in 6421996
Indirizzo dell'array 0 = 6421984
L'aggiunta di 1 risulta in 6421996

Se marks fosse un array 1-D, marks e &marks[0] avrebbero puntato allo stesso elemento 0 . Per un array 2-D, gli elementi ora sono array 1-D. Di conseguenza, marks e &marks[0] puntano all'elemento dell'array con indice  0 , e l'aggiunta di 1 punta al primo array.

printf("Indirizzo dell'elemento 0 dell'array 0 = %d\n", marks[0]);
printf("L'aggiunta di 1 risulta in %d\n", marks[0] + 1);
printf("Indirizzo dell'elemento 0 del primo array = %d\n", marks[1]);
printf("L'aggiunta di 1 risulta in %d\n", marks[1] + 1);

 /* Output */
Indirizzo dell'elemento 0 dell'array 0 = 6421984
L'aggiunta di 1 risulta in 6421988
Indirizzo dell'elemento 0 del primo array = 6421996
L'aggiunta di 1 risulta in 6422000

Ora viene la differenza. Per un array 1-D, marks[0] darebbe il valore dell'elemento 0. Un incremento di 1 aumenterebbe il valore di 1.

Ma in un array 2-D , marks[0] punta all'elemento 0 dell'array 0. In modo simile,  marks[1] punta all'elemento 0 del primo array. Un incremento di 1 punterebbe al primo elemento del primo array.

printf("Valore dell'elemento 0 dell'array 0 = %d\n", marks[0][0]);
printf("L'aggiunta di 1 risulta in %d", marks[0][0] + 1);

/* Output */
Valore dell'elemento 0 dell'array 0 = 98
L'aggiunta di 1 risulta in 99

Questa è la parte nuova,  marks[i][j] fornisce il valore dell'elemento j dell'array i. Un incremento a esso cambia il valore conservato in marks[i][j]. Ora proviamo a scrivere marks[i][j] in termini di puntatori.

Sappiamo dalla nostra precedente discussione che marks[i] + j punterebbe all'elemento i dell'array j. Dereferenziandolo significherebbe ottenere il valore a quell'indirizzo. Pertanto marks[i][j] è uguale a *(marks[i] + j).

Dalla nostra discussione sugli array 1-D, marks[i] è uguale a *(marks + i). Quindi marks[i][j] può essere scritto come *(*(marks + i) + j) in termini di puntatori.

Ecco un riepilogo delle notazioni a confronto per gli array monodimensionali e bidimensionali.

ESPRESSIONE ARRAY 1-D ARRAY 2-D
&arrayName punta all'indirizzo dell'intero array
aggiungendo 1 l'indirizzo aumenta di 1 x sizeof(arrayName)
punta all'indirizzo dell'intero array
aggiungendo 1 l'indirizzo aumenta di 1 x sizeof(arrayName)
arrayName punta all'elemento 0
aggiungendo 1 aumenta l'indirizzo del primo elemento
punta all'elemento 0 (array)
aggiungendo 1 aumenta l'indirizzo del primo elemento (array)
&arrayName[i] punta all'elemento i
aggiungendo 1 aumenta l'indirizzo all'elemento (i+1)
punta all'elemento i (array)
aggiungendo 1 aumenta l'indirizzo dell'elemento (i+1) (array)
arrayName[i] fornisce il valore dell'elemento i
aggiungendo 1 aumenta dell'elemento i
punta all'elemento 0 dell'array i
aggiungento 1 aumenta l'indirizzo del primo elemento dell'array i
arrayName[i][j] Nulla fornisce il valore dell'elemento j dell'array i
aggiungendo 1 aumenta il valore dell'elemento j dell'array i
Espressione di Puntatore per Accedere agli Elementi *( arrayName + i) *( *( arrayName + i) + j)

4. Stringhe

Una stringa è un array monodimensionale di caratteri che finisce con un carattere null (\0). Quando scriviamo char name[] = "Srijan";, ciascun carattere occupa un byte di memoria con l'ultimo che è sempre \0.

In modo simile agli array visti in precedenza, name e &name[0] puntano al carattere a indice 0 nella stringa mentre &name punta all'intera stringa. Inoltre,  name[i] può essere scritto come *(name + i).

/* Array di Stringhe */
char top[6][15] = {
                    "Liverpool",
                    "Man City",
                    "Man United",
                    "Chelsea",
                    "Leicester",
                    "Tottenham"
                  };

printf("Puntatore a un array 2-D = %d\n", &top);
printf("L'aggiunta di 1 risulta in %d\n", &top + 1);

 /* Output */
// Puntatore a un array 2-D = 6421952
// L'aggiunta di 1 risulta in 6422042

printf("Puntatore all'indice 0 della stringa = %d\n", &top[0]);
printf("L'aggiunta di 1 risulta in %d\n", &top[0] + 1);

 /* Output */
// Puntatore all'indice 0 della stringa = 6421952
// L'aggiunta di 1 risulta in 6421967

printf("Puntatore all'indice 0 della stringa = %d\n", top);
printf("L'aggiunta di 1 risulta in %d\n", top + 1);

 /* Output */
// Puntatore all'indice 0 della stringa = 6421952
// L'aggiunta di 1 risulta in 6421967

printf("Puntatore all'elemento a indice 0 della quarta stringa = %d\n", top[4]);
printf("Puntatore al primo elemento della quarta stringa = %c\n", top[4] + 1);

 /* Output */
// Puntatore all'elemento a indice 0 della quarta stringa = 6422012
// Puntatore al primo elemento della quarta stringa = 6422013

printf("Valore del primo carattere della terza stringa = %c\n", top[3][1]);
printf("Lo stesso usando puntatori = %c\n", *(*(top + 3) + 1));

 /* Output */
// Valore del primo carattere della terza stringa = h
// Lo stesso usando puntatori = h

Si può accedere e manipolare anche un array di caratteri bidimensionale oppure un array di stringhe come discusso prima.

/* Array di Stringhe */
char top[6][15] = {
                    "Liverpool",
                    "Man City",
                    "Man United",
                    "Chelsea",
                    "Leicester",
                    "Tottenham"
                  };

printf("Puntatore a un array 2-D = %d\n", &top);
printf("L'aggiunta di 1 risulta in %d\n", &top + 1);

 /* Output */
// Puntatore a un array 2-D = 6421952
// L'aggiunta di 1 risulta in 6422042

printf("Puntatore all'indice 0 della stringa = %d\n", &top[0]);
printf("L'aggiunta di 1 risulta in %d\n", &top[0] + 1);

 /* Output */
// Puntatore all'indice 0 della stringa = 6421952
// L'aggiunta di 1 risulta in 6421967

printf("Puntatore all'indice 0 della stringa = %d\n", top);
printf("L'aggiunta di 1 risulta in %d\n", top + 1);

 /* Output */
// Puntatore all'indice 0 della stringa = 6421952
// L'aggiunta di 1 risulta in 6421967

printf("Puntatore all'elemento a indice 0 della quarta stringa = %d\n", top[4]);
printf("Puntatore al primo elemento della quarta stringa = %c\n", top[4] + 1);

 /* Output */
// Puntatore all'elemento a indice 0 della quarta stringa = 6422012
// Puntatore al primo elemento della quarta stringa = 6422013

printf("Valore del primo carattere della terza stringa = %c\n", top[3][1]);
printf("Lo stesso usando puntatori = %c\n", *(*(top + 3) + 1));

 /* Output */
// Valore del primo carattere della terza stringa = h
// Lo stesso usando puntatori = h

5. Array di Puntatori

Oltre agli array di tipo  int e  char, esiste anche un array di puntatori. Questo tipo di array sarebbe semplicemente una collezione di indirizzi. Questi indirizzi potrebbero puntare a variabili singole oppure a un altro array.

La sintassi per dichiarare un array di puntatori è la seguente:

tipoDato *nomeVariabile[dimensione];

/* Esempi */
int *example1[5];
char *example2[8];

Seguendo la precedenza degli operatori, il primo esempio potrebbe essere letto come -  example1 è un array([]) di 5 puntatori a int. In modo simile, example2 è un array di 8 puntatori a char.

Possiamo conservare un array di stringhe bidimensionale top usando un array di puntatori, risparmiando anche della memoria.

char *top[] = {
                    "Liverpool",
                    "Man City",
                    "Man United",
                    "Chelsea",
                    "Leicester",
                    "Tottenham"
                  };

top conterrà gli indirizzi base di tutti i rispettivi nomi. L'indirizzo base di  "Liverpool" sarà conservato in top[0], "Man City" in top[1], e così via.

Nella dichiarazione precedente, abbiamo richiesto 90 byte per conservare i nomi. Qui servono solo ( 58 (somma dei byte dei nomi) + 12 ( byte richiesti per conservare l'indirizzo nell'array) ) 70 byte.

La manipolazione di stringhe o interi diventa molto più facile quando si usa un array di puntatori.

Se proviamo a inserire "Leicester" davanti a "Chelsea", dobbiamo solo invertire i valori di  top[3] e top[4] in questo modo:

char *temporary;
temporary = top[3];
top[3] = top[4];
top[4] = temporary;

Senza puntatori, avremmo dovuto scambiare ogni carattere delle stringhe, il che avrebbe richiesto più tempo. Ecco perché le stringhe sono generalmente dichiarate usando i  puntatori.

6. Puntatori ad Array

Come i puntatori a int o i puntatori a char, abbiamo anche puntatori ad array. Questo puntatore punta all'intero array invece che ai suoi elementi.

Ricordi che abbiamo discusso di come &arrayName punti all'intero array? Ecco, si tratta di un puntatore ad array.

Un puntatore ad array può essere dichiarato in questo modo:

tipoDato (*nomeVariabile)[dimensione];

/* Esempi */
int (*ptr1)[5];
char (*ptr2)[15];

Nota le parentesi. Senza di esse, avremmo un array di puntatori. Il primo esempio può essere letto come  - ptr1 è un puntatore a un array di 5 int (interi).

int goals[] = { 85,102,66,69,67};
int (*pointerToGoals)[5] = &goals;
printf("Indirizzo conservato in pointerToGoals %d\n", pointerToGoals);
printf("Dereferenziandolo, otteniamo %d\n",*pointerToGoals);

/* Output */
Indirizzo conservato in pointerToGoals 6422016
Dereferenziandolo, otteniamo 6422016

Quando dereferenziamo un puntatore, otteniamo il valore a quell'indirizzo. Alla stessa stregua, dereferenziando un puntatore ad array, otteniamo l'array e il nome al quale punta l'array che punta all'indirizzo base. Possiamo confermare che *pointerToGoals fornisce l'array goals se troviamo la sua dimensione.

printf("Dimensione di goals[5] = %d, *pointerToGoals);

/* Output */
Dimensione di goals[5] = 20

Se lo dereferenziamo nuovamente, otterremo il valore conservato in quell'indirizzo. Possiamo stampare tutti gli elementi usando pointerToGoals.

for(int i = 0; i < 5; i++)
  printf("%d ", *(*pointerToGoals + i));

/* Output */
85 102 66 69 67

I puntatori e puntatori ad array sono piuttosto utili se accoppiati a funzioni. A seguire nella prossima sezione!

C. Funzioni

1. Chiamata per Valore vs. Chiamata per Riferimento

Dai un'occhiata al programma qui sotto:

#include <stdio.h>

int multiply(int x, int y){
  int z;
  z = x * y;
  return z;
}

main(){
  int x = 3, y = 5; 
  int product = multiply(x,y);
  printf("Prodotto = %d\n", product);
  /* stampa "Prodotto = 15" */
}

La funzione multiply() riceve due argomenti int e ritorna il loro prodotto come  int.

Nella chiamata a multiply(x,y), passiamo i valori di x e y (assegnati in main()), che sono gli effettivi argomenti, per multiply().

I valori degli effettivi argomenti sono passati o copiati negli argomenti formali x e y ( di multiply()).  x e y di multiply() sono diversi da quelli di main(). Questo si può verificare stampando i loro indirizzi.

#include <stdio.h>

int multiply(int x, int y){
  printf("Indirizzo di x in multiply() = %d\n", &x);
  printf("Indirizzo di y in multiply() = %d\n", &y);
  int z;
  z = x * y;
  return z;
}

main(){
  int x = 3, y = 5;
  printf("Indirizzo di x in main() = %d\n", &x);
  printf("Indirizzo di y in main() = %d\n", &y);
  int product = multiply(x,y);
  printf("Prodotto = %d\n", product);
}

/* Output */
Indirizzo di x in main() = 6422040
Indirizzo di y in main() = 6422036
Indirizzo di x in multiply() = 6422000
Indirizzo di y in multiply() = 6422008
Prodotto = 15

Visto che abbiamo creato valori conservati un una nuova posizione, ci costa memoria. Non sarebbe meglio se potessimo eseguire la stessa operazione senza sprecare spazio?

La chiamata per riferimento ci consente di farlo. Passiamo gli indirizzi o riferimenti delle variabili alla funzione, la quale non crea una copia. Usando l'operatore di dereferenziazione *, possiamo avere accesso al valore conservato in quegli indirizzi.

Possiamo anche riscrivere il programma qui sopra usando una chiamata per riferimento.

#include <stdio.h>

int multiply(int *x, int *y){
  int z;
  z = (*x) * (*y);
  return z;
}

main(){
  int x = 3, y = 5; 
  int product = multiply(&x,&y);
  printf("Prodotto = %d\n", product);
   /* Stampa "Prodotto = 15" */
}

2. Puntatori come Argomenti di Funzione

In questa sezione, esamineremo vari programmi nei quali passiamo valori di tipo int, char, array e stringhe come argomenti usando puntatori.

#include <stdio.h>

void add(float *a, float *b){
 float c = *a + *b;
 printf("L'addizione fornisce %.2f\n",c);
}

void subtract(float *a, float *b){
 float c = *a - *b;
 printf("La sottrazione fornisce %.2f\n",c);
}

void multiply(float *a, float *b){
 float c = *a * *b;
 printf("La moltiplicazione fornisce %.2f\n",c);
}

void divide(float *a, float *b){
 float c = *a / *b;
 printf("La divisione fornisce %.2f\n",c);
}

main(){
    printf("Digita due numeri :\n");
    float a,b;
    scanf("%f %f",&a,&b);
    printf("Cosa vuoi fare con questi numeri?\nAggiungi : a\nSottrai : s\nMoltiplica : m\nDividi : d\n");
    char operation = '0';
    scanf(" %c",&operation);
    printf("\nEseguo...\n\n");
    switch (operation) {
    case 'a':
        add(&a,&b);
        break;
    case 's':
        subtract(&a,&b);
        break;
    case 'm':
        multiply(&a,&b);
        break;
    case 'd':
        divide(&a,&b);
        break;
    default:
        printf("Input non valido!!!\n");

  }

}

Abbiamo creato quattro funzioni,  add(), subtract(), multiply() e divide() per eseguire operazioni aritmetiche su due numeri,  a e b.

Gli indirizzi di a e b sono passati alle funzioni. All'interno della funzione, usando  * abbiamo accesso ai valori e stampiamo il risultato.

In modo simile, possiamo passare array come argomenti usando un puntatore al loro primo elemento.

#include <stdio.h>

void greatestOfAll( int *p){
  int max = *p;
  for(int i=0; i < 5; i++){
    if(*(p+i) > max)
       max = *(p+i);
  }
  printf("L'elemento più grande è %d\n",max);
}
main(){
  int myNumbers[5] = { 34, 65, -456, 0, 3455};
  greatestOfAll(myNumbers);
   /* Stampa :L'elemento più grande è 3455" */
}

Visto che il nome dell'array è esso stesso un puntatore al primo elemento, lo passiamo come argomento alla funzione greatestOfAll(). Nella funzione qui sopra, iteriamo attraverso l'array tramite un ciclo e un puntatore.

#include <stdio.h>
#include <string.h>

void wish(char *p){
 printf("Buona giornata, %s",p);
}

main(){
 printf("Digita il tuo nome : \n");
 char name[20];
 gets(name);
 wish(name);
}

Qui sopra passiamo la stringa name a wish() tramite un puntatore e stampiamo il messaggio.

3. Puntatori come Valori di Ritorno da Funzione

#include <stdio.h>

int* multiply(int *a, int *b){
  int c = *a * *b;
  return &c;
}

main(){
  int a= 3, b = 5;
  int *c = multiply (&a,&b);
  printf("Prodotto = %d",*c);
}

La funzione multiply() riceve due parametri a  int. Anch'essa ritorna un puntatore a  int che conserva l'indirizzo nel quale il prodotto viene salvato.

È molto facile pensare che il risultato sia 15. Ma non lo è!

Quando viene chiamata multiply(), l'esecuzione di  main() viene messa in pausa e ora la memoria viene allocata per l'esecuzione di multiply(). Dopo che l'esecuzione è completata, la memoria allocata a  multiply() viene deallocata.

Pertanto, sebbene c ( locale a main()) conservi l'indirizzo del prodotto, non c'è garanzia che il dato sia ancora presente visto che la memoria è stata deallocata.

Quindi significa che i puntatori non possono essere ritornati da una funzione? No!

Possiamo fare due cose. Conservare l'indirizzo nello heap o sezione globale oppure dichiarare la variabile statica (static) in modo che il suo valore persista.

Le variabili statiche possono essere create semplicemente usando la parola chiave static prima del tipo di dato in fase di dichiarazione della variabile.

Per conservare indirizzi nello heap, possiamo usare le funzioni di libreria malloc() e calloc() che allocano memoria dinamicamente.

Il programma seguente spiega entrambi i metodi, che ritornano il risultato di 15.

#include <stdio.h>
#include <stdlib.h>

/* Usando malloc() */

int* multiply(int *a, int *b){
  int *c = malloc(sizeof(int));
  *c = *a * *b;
  return c;
}

main(){
  int a= 3, b = 5;
  int *c = multiply (&a,&b);
  printf("Prodotto = %d",*c);
}

/* Usando la parola chiave static */
#include <stdio.h>

int* multiply(int *a, int *b){
  static int c;
  c = *a * *b;
  return &c;
}

main(){
  int a= 3, b = 5;
  int *c = multiply (&a,&b);
  printf("Prodotto = %d",*c);
}

4. Puntatore a Funzione

Come un puntatore a diversi tipi di dati, abbiamo anche un puntatore a funzione.

Un puntatore a funzione (function pointer) conserva l'indirizzo di una funzione. Tuttavia non punta ad alcun dato. Punta alla prima istruzione nella funzione.

La sintassi per dichiarare un puntatore a funzione è la seguente:

 /* Dichiarazione di un funzione */
tipoRitornato nomeFunzione(tipoParametro1, tipoParametro2, ...);

 /* Dichiarazione di un puntatore a funzione */
tipoRitornato (*nomePuntatore)(tipoParametro1, tipoParametro2, ...);
nomePuntatore = &nomeFunzione; /* oppure nomePuntatore = nomefunzione; */

L'esempio che segue chiarisce:

int* multiply(int *a, int *b)
{
    int *c = malloc(sizeof(int));
    *c = *a * *b;
    return c;
}

main()
{ 
    int a=3,b=5;
    int* (*p)(int*, int*) = &multiply; /* oppure int* (*p)(int*, int*) = multiply; */
    int *c = (*p)(&a,&b); /* oppure int *c = p(&a,&b); */
    printf("Prodotto = %d",*c);
}

La dichiarazione per il puntatore  p alla funzione multiply() può essere letto come (seguendo la precedenza degli operatori) - p è un puntatore a funzione con due puntatori di tipo int( oppure due puntatori a int) come parametri che ritorna un puntatore a int.

Visto che il nome della funzione è anche un puntatore alla funzione, l'uso di & non è necessario. Inoltre l'eliminazione di * dalla chiamata alla funzione non influisce sul programma.

5. Array di Puntatori a Funzioni

Abbiamo visto come creare un array di puntatori a  int, char, e così via. Allo stesso modo, possiamo creare un array di puntatori a funzione.

In questo array, ogni elemento conserverà un indirizzo di una funzione, dove tutte le funzioni sono dello stesso tipo. Vale a dire che hanno lo stesso numero di parametri e tipo dato di ritorno.

Andremo a modificare un programma che abbiamo esaminato in precedenza in questa sezione. Conserveremo gli indirizzi di add(), subtract(), multiply() e divide() in un array facendo una chiamata a funzione tramite indicizzazione.

#include <stdio.h>

void add(float *a, float *b){
 float c = *a + *b;
 printf("L'addizione fornisce %.2f\n",c);
}

void subtract(float *a, float *b){
 float c = *a - *b;
 printf("La sottrazione fornisce %.2f\n",c);
}

void multiply(float *a, float *b){
 float c = *a * *b;
 printf("La moltiplicazione fornisce %.2f\n",c);
}

void divide(float *a, float *b){
 float c = *a / *b;
 printf("La divisione fornisce %.2f\n",c);
}

main(){
    printf("Digita due numeri :\n");
    float a,b;
    scanf("%f %f",&a,&b);
    printf("Cosa vuoi fare con questi numeri?\nAggiungi : a\nSottrai : s\nMoltiplica : m\nDividi : d\n");
    char operation = '0';
    scanf(" %c",&operation);
    void (*p[])(float* , float*) = {add,subtract,multiply,divide};
    printf("\nEseguo...\n\n");
    switch (operation) {
    case 'a':
        p[0](&a,&b);
        break;
    case 's':
        p[1](&a,&b);
        break;
    case 'm':
        p[2](&a,&b);
        break;
    case 'd':
        p[3](&a,&b);
        break;
    default:
        printf("Input non valido!!!\n");

  }

}

Qui la dichiarazione può essere letta come - p è un array di puntatori a funzione con due puntatori float come parametri e valore di ritorno void.

6. Puntatore a Funzione come Argomento

Come qualsiasi altro puntatore, i puntatori a funzione possono anche essere passati a un'altra funzione, nota come funzione callback. La funzione dalla quale viene passata è nota come funzione chiamante.

Un modo migliore per capire sarebbe dare uno sguardo a qsort(), che è una funzione integrata in C. Viene usata per ordinare un array di interi, stringhe, strutture e così via. La dichiarazione per qsort() è:

void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *));

qsort() riceve quattro argomenti:

  1. un puntatore void verso l'inizio dell'array da ordinare
  2. il numero di elementi
  3. la dimensione di ciascun elemento
  4. un puntatore a funzione che riceve due puntatori void come argomenti e ritorna un int

Il puntatore a funzione punta a una funzione di confronto che ritorna un intero maggiore di zero se il primo argomento è maggiore del secondo, un intero minore di zero se il secondo argomento è maggiore del primo e zero se sono uguali.

Il programma seguente mostra il suo utilizzo:

#include <stdio.h>
#include <stdlib.h>

int compareIntegers(const void *a, const void *b)
{
  const int *x = a;
  const int *y = b;
  return *x - *y;
}

main(){

  int myArray[] = {97,59,2,83,19,97};
  int numberOfElements = sizeof(myArray) / sizeof(int);

  printf("Prima dell'ordinamento - \n");
  for(int i = 0; i < numberOfElements; i++)
   printf("%d ", *(myArray + i));

  qsort(myArray, numberOfElements, sizeof(int), compareIntegers);

  printf("\n\nDopo l'ordinamento - \n");
  for(int i = 0; i < numberOfElements; i++)
    printf("%d ", *(myArray + i));
 }

/* Output */

Prima dell'ordinamento -
97 59 2 83 19 97

Dopo l'ordinamento -
2 19 59 83 97 97

Visto che il nome di una funzione è esso stesso un puntatore, possiamo passare compareIntegers come quarto argomento.

D. Strutture

1. Puntatore a Struttura

Come i puntatori a intero, ad array e a funzione, abbiamo anche puntatori a struttura.

struct records {
    char name[20];
    int roll;
    int marks[5];
    char gender;
};

struct records student = {"Alex", 43, {76, 98, 68, 87, 93}, 'M'};

struct records *ptrStudent = &student;

Qui abbiamo dichiarato un puntatore ptrStudent di tipo struct records e abbiamo assegnato l'indirizzo di student a ptrStudent.

ptrStudent conserva l'indirizzo base di  student, che è l'indirizzo base del primo membro della struttura. Incrementando di 1 si incrementerebbe l'indirizzo di sizeof(student) byte.

printf("Indirizzo della struttura = %d\n", ptrStudent);
printf("Indirizzo del membro `name` = %d\n", &student.name);
printf("L'incremento di 1 risulta in %d\n", ptrStudent + 1);

/* Output */
Indirizzo della struttura = 6421984
Indirizzo del membro `name` = 6421984
L'incremento di 1 risulta in 6422032

Possiamo accedere ai membri di student usando ptrStudent in due modi: con il nostro vecchio amico* oppure usando -> ( infix oppure operatore freccia).

Con *, continueremo a usare .(operatore punto) laddove con -> non avremo bisogno dell'operatore punto.

printf("Nome senza usare ptrStudent : %s\n", student.name);
printf("Nome usando ptrStudent e * : %s\n", ( *ptrStudent).name);
printf("Nome usando ptrStudent e -> : %s\n", ptrStudent->name);

/* Output */
Nome senza usare ptrStudent: Alex
Nome usando ptrStudent e *: Alex
Nome usando ptrStudent e ->: Alex

Allo stesso modo possiamo accedere e modificare anche altri membri. Nota che le parentesi sono necessarie quando si usa * visto che l'operatore punto (.) ha maggiore precedenza rispetto a *.

2. Array di Strutture

Possiamo creare un array di tipo struct records e usare un puntatore per accedere agli elementi e ai loro membri.

struct records students[10];

 /* Puntatore al primo elemento (struttura) dell'array */
struct records *ptrStudents1 = &students;

 /* Puntatore a un array di 10 struct records */
struct records (*ptrStudents2)[10] = &students;

Nota che ptrStudent1 è un puntatore a student[0] laddove ptrStudent2 è un puntatore all'intero array di 10 struct records. Aggiungendo 1 a ptrStudent1 punterebbe a student[1].

Possiamo usare ptrStudent1 con un ciclo per iterare sugli elementi e i loro membri.


for( int i = 0; i <  10; i++)
  printf("%s, %d\n", ( ptrStudents1 + i)->name, ( ptrStudents1 + i)->roll);

3. Puntatore a Struttura come Argomento

Possiamo anche passare l'indirizzo di una variabile di tipo struttura a una funzione.

#include <stdio.h>

struct records {
    char name[20];
    int roll;
    int marks[5];
    char gender;
};

main(){
 struct records students = {"Alex", 43, {76, 98, 68, 87, 93}, 
'M'};
 printRecords(&students);
}

void printRecords( struct records *ptr){
  printf("Name: %s\n", ptr->name);
  printf("Roll: %d\n", ptr->roll);
  printf("Gender: %c\n", ptr->gender);
  for( int i = 0; i < 5; i++)
   printf("Marks in %dth subject: %d\n", i, ptr->marks[i]);
}

 /* Output */
Name: Alex
Roll: 43
Gender: M
Marks in 0th subject: 76
Marks in 1th subject: 98
Marks in 2th subject: 68
Marks in 3th subject: 87
Marks in 4th subject: 93

Nota che la struttura struct records viene dichiarata all'esterno di main(). Questo per assicurarsi che sia disponibile globalmente e possa essere usata da printRecords().

Se la struttura venisse definita all'interno di main(), il suo ambito sarebbe limitato a main(). Inoltre una struttura deve essere dichiarata prima della dichiarazione della funzione.

Come per le strutture, possiamo avere puntatori a union e possiamo accedere ai membri tramite l'operatore freccia(->).

E. Puntatore a Puntatore

Finora abbiamo esaminato puntatori per diversi tipi di dato, array, stringhe, funzioni, strutture e unioni.

La domanda che sorge spontanea è - ma un puntatore a puntatore?

Ho buone notizie per te! Esistono anche quelli.

int var = 6;
int *ptr_var = &var;

printf("Indirizzo di var = %d\n", ptr_var);
printf("Indirizzo di ptr_var = %d\n", &ptr_var);

/* Output */
Indirizzo di var = 6422036
Indirizzo di ptr_var = 6422024

Per conservare l'indirizzo della variabile var di tipo int, abbiamo un puntatore a  int ptr_var. Avremmo bisogno di un altro puntatore per conservare l'indirizzo di ptr_var.

Visto che ptr_var è di tipo int *, per conservare il suo indirizzo avremmo dovuto creare un puntatore a int *. Il codice che segue mostra come deve essere fatto.

int * *ptr_ptrvar = &ptr_var; /* oppure int* *ppvar oppure int **ppvar */

Possiamo usare ptr_ptrvar per accedere all'indirizzo di ptr_var e usare la doppia dereferenziazione per accedere al valore di var.

printf("Indirizzo di ptr_var = %d\n", ptr_ptrvar);
printf("Indirizzo di var = %d\n", *ptr_ptrvar);
printf("Valore di var = %d\n", *(*ptr_ptrvar));

/* Output */
Indirizzo di ptr_var = 6422024
Indirizzo di var = 6422036
Valore di var = 6

Non sono richieste le parentesi quando si dereferenzia ptr_ptrvar, ma è buona pratica usarle. Possiamo creare un altro puntatore a ptr_ptrptrvar, che conserverà l'indirizzo di ptr_ptrvar.

Visto che ptr_ptrvar è di tipo int**, la dichiarazione di ptr_ptrptrvar sarà:

int** *ptr_ptrptrvar = &ptr_ptrvar;

Possiamo ancora accedere a  ptr_ptrvar, ptr_var e var usando ptr_ptrptrvar.

printf("Indirizzo di ptr_ptrvar = %d\n", ptr_ptrptrvar);
printf("Valore per ptr_ptrvar = %d\n",*ptr_ptrptrvar);
printf("Indirizzo di ptr_var = %d\n", *ptr_ptrptrvar);
printf("Valore per ptr_var = %d\n", *(*ptr_ptrptrvar));
printf("Indirizzo di var = %d\n", *(*ptr_ptrptrvar));
printf("Valore per var = %d\n", *(*(*ptr_ptrptrvar)));

/* Output */
Indirizzo di ptr_ptrvar = 6422016
Valore per ptr_ptrvar = 6422024
Indirizzo di ptr_var = 6422024
Valore per ptr_var = 6422036
Indirizzo di var = 6422036
Valore per var = 6
Pointer to Pointer

Se modifichiamo il valore di un qualsiasi puntatore usando ptr_ptrptrvar oppure ptr_ptrvar, il puntatore non punterebbe più alla variabile.

F. Conclusione

Bene! Sì, abbiamo finito. Siamo partiti dai puntatori e abbiamo finito con i puntatori (per così dire). Non dicono che la curva di apprendimento è un cerchio?

Cerca di ricapitolare tutti i sotto argomenti che hai letto. Se li ricordi, ben fatto! Rileggi quelli che non riesci a ricordare.

Questo articolo è terminato, ma non dovresti smettere con i puntatori, fai esperimenti. Successivamente, potresti dare un'occhiata alla Allocazione dinamica della memoria (Dynamic Memory Allocation) per conoscere meglio i puntatori.