Un puntatore è una variabile o una costante il cui valore è l'indirizzo di un'altra variabile o costante. I puntatori sono una funzionalità potente e potenzialmente pericolosa del C. Capirli può essere una bella sfida.
int i = 5;
Quando assegniamo un valore a una variabile, possiamo poi porci due domande.
Chi sei?
Sono i.
Che cosa hai?
Ho il numero intero 5.
C'è anche una terza domanda che possiamo porci.
Dove sei?
Sono a 0x6dfed4.
Aspetta, come?
Le variabili e le costanti memorizzano il loro valore in una locazione di memoria. Quella locazione di memoria è nota come l'indirizzo della variabile o della costante.
Per ottenere l'indirizzo di una variabile usiamo l'operatore di indirizzo, che è &.
int i = 5;
int *i_address = &i;
Aspetta, che cosa significa quel * prima di i_address? Non vuol dire moltiplicare?
Un tipo seguito da un * seguito da un identificatore è il modo per dichiarare un puntatore a una variabile di quel tipo.
Il * è noto come operatore di indirezione perché fa riferimento in modo indiretto al valore di cui il puntatore detiene l'indirizzo.
Ok, ma perché creare una variabile solo per contenere l'indirizzo di un'altra variabile?
E poi, perché ci interessa sapere qual è l'indirizzo di una variabile?
Beh, ci interessa sapere dove si trovano le variabili quando sono in un array.
Quando creiamo un array così
int my_ints[] = {1, 2};
Il valore di my_ints in realtà è il suo indirizzo, che per di più coincide con l'indirizzo del suo primo elemento.
L'indirizzo del suo secondo elemento sarà l'indirizzo del primo elemento più la dimensione di un int sul sistema.
Per vederlo meglio, prova a eseguire quanto segue
#include <stdio.h>
int main() {
// my_int's value is its address
int my_ints[] = {1, 2};
// since my_ints is already an address
// we don't need the address operator to make a pointer from it.
// a pointer to my_ints holds the address of my_ints
int *my_ptr = my_ints;
// the address of my_ints and the value of my_ptr are the same
printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
// the address of the first element is the same as the values in the line above
printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
// the address of the second element has increased by the size of an int
printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); //
return 0;
}
L'esempio precedente mostra perché l'indicizzazione degli array inizia da 0 invece che da 1.
L'indice in un array moltiplica l'indice per la dimensione del tipo e lo somma all'indirizzo iniziale dell'array.
Dato che il primo elemento coincide con l'inizio dell'array, moltiplichiamo la dimensione del tipo per 0 e la sommiamo all'indirizzo iniziale per ottenere il primo elemento.
Per ottenere il secondo elemento moltiplichiamo la dimensione del tipo per 1 e la sommiamo all'indirizzo iniziale.
Se il tipo è lungo quattro byte, l'indirizzo del secondo elemento sarà più alto di quattro byte rispetto al primo.
È anche per questo che i puntatori vengono dichiarati di un certo tipo.
Se un puntatore non sapesse a che tipo punta, non saprebbe di quanti byte avanzare quando gli si somma 1.
Sommare 1 a un puntatore non significa necessariamente aggiungere un byte all'indirizzo.
Significa aggiungere 1 moltiplicato per la lunghezza del suo tipo in byte.
Ma non è finita qui! Non pensavi mica che avessimo finito con *, vero?
Ora che abbiamo capito come funzionano gli indirizzi con l'indicizzazione degli array, a cosa serve tutto questo se non riusciamo a ottenere il valore memorizzato a un indirizzo?
Come facciamo?
Per ottenere il valore memorizzato a un indirizzo usiamo l'operatore di indirezione, che, come ricorderai, è *.
In questa situazione l'operatore di indirezione è anche talvolta chiamato operatore di dereferenziazione, perché dereferenzia il valore di cui il puntatore ha l'indirizzo.
Chiamarlo operatore di dereferenziazione quando lo si usa per dereferenziare lo distingue dall'operatore di indirezione usato per definire un puntatore, ma tecnicamente il * in entrambi gli usi si chiama operatore di indirezione.
Dai!
Stai scherzando?
Quanti usi ha *?!
La moltiplicazione, la dichiarazione di un puntatore e ora anche la dereferenziazione!
È confuso!
E poi, che cos'è esattamente la dereferenziazione?
Un esempio mostra come usare l'operatore di indirezione per dereferenziare
#include <stdio.h>
int main() {
// define the variable
int my_ints[] = {1, 2};
// define the pointer from the address of the variable
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
// prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
return 0;
}
Usare le parentesi può aiutare a eliminare l'ambiguità delle intenzioni quando si usa l'aritmetica dei puntatori. Il seguente esempio potrebbe non stampare quello che il programmatore intendeva
#include <stdio.h>
int main() {
int my_ints[] = {1, 42};
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *++my_ptr is 42.
printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
// prints: my_ints[1] is 43. ++*my_ptr++ is 43.
printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);
return 0;
}
Sia il * sia il ++ hanno la stessa precedenza, ma vengono valutati da destra a sinistra.
Nella prima riga my_ptr viene prima incrementato fino al secondo elemento, che poi viene dereferenziato.
Nella seconda riga my_ptr, che nella prima riga era stato incrementato fino al secondo elemento, viene prima dereferenziato, e poi il valore dereferenziato viene incrementato.
Se aggiungiamo questa riga all'esempio precedente
// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);
In quella riga abbiamo incrementato il puntatore fino a superare l'array.
Abbiamo oltrepassato l'array e stiamo puntando ai quattro byte successivi in memoria, che il puntatore interpreta come un int, ma in realtà quei byte potrebbero appartenere a un'altra variabile o ad altre variabili.
Quei quattro byte potrebbero far parte di un double oppure essere quattro char.
Il problema è che non dovremmo nemmeno guardarli con il nostro puntatore.
Avremmo potuto usare
// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);
Dato che l'indicizzazione degli array è implementata con un puntatore, vale lo stesso pericolo di oltrepassare l'array.
E, dato che una stringa è un array di char, i puntatori si possono usare anche con le stringhe.
Spesso una stringa viene dichiarata usando la sintassi dei puntatori, così
char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";
Una domanda, però. Perché mettiamo l'asterisco accanto all'identificatore? Perché non metterlo accanto al tipo, dato che è un puntatore a quel tipo? Perché questo
int* my_ptr1, my_ptr2_that_is_really_an_int;
non dichiara due puntatori a un int, ma dichiara un puntatore a un int e un int.
Per dichiarare due puntatori a un int si farebbe così
int *my_ptr1, *my_ptr2;
Quindi è bene mettere il * vicino all'identificatore per ridurre al minimo l'ambiguità.
Oppure dichiarare un solo identificatore per riga.
E, se non ne hai ancora abbastanza di *, benvenuto a...
È comune vedere array di stringhe rappresentati così
#include <stdio.h>
void print_strings(char **passed_in_strings) {
printf("%s\n", *passed_in_strings);
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char *my_strings[] = {"One string", "Two string"};
print_strings(my_strings);
return 0;
}
Potrebbe non essere ovvio, ma l'array di stringhe è dichiarato e inizializzato come un puntatore a un array di array di char.
Quando l'array di array di char viene passato a print_strings(), la firma lo descrive come un puntatore a puntatore (o puntatori) a char.
Quindi come facciamo a sapere che char ** è davvero un puntatore a un array di puntatori ad array di char, e non un puntatore a un puntatore a un singolo char?
Non lo sappiamo.
Questa è una delle tante cose che possono rendere confuso l'uso dei puntatori. Quanto segue potrebbe compilare
#include <stdio.h>
void print_strings(char **passed_in_strings) {
// prints: A (followed by gibberish)
printf("%s\n", *passed_in_strings);
// prints gibberish
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char my_char = 'A';
char *my_ptr = &my_char;
char **my_ptr_to_ptr = &my_ptr;
print_strings(my_ptr_to_ptr);
return 0;
}
Ma print_strings() non viene usata come previsto.
Usare una firma diversa non aiuta.
Cambiando la firma in questa
void print_strings(char *passed_in_strings[]) {
// code snipped
}
potrebbe comunque compilare e stampare comunque cose senza senso.
Ma non c'è davvero alcun vantaggio nel passare un puntatore a un puntatore a un singolo char.
Con ogni probabilità, quando vedi char **, rappresenta un puntatore a un array di puntatori ad array di char.
Abbiamo visto un puntatore passato a una funzione. Ma perché passare un puntatore a una funzione?
In C, gli argomenti vengono passati alle funzioni per valore. Questo significa che il valore viene copiato dall'argomento e che quella copia è locale alla funzione. Quando la funzione termina, termina anche il valore copiato.
Guarda cosa succede quando proviamo a modificare i valori all'interno di una funzione
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
//prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", a, b);
}
int main() {
int a = 3, b = 4;
swap(a, b);
// prints: After 'swap', a is 3 and b is 4
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
Questo perché, anche se le variabili hanno gli stessi nomi, a e b in swap() non sono gli stessi a e b di main().
Sono copie dei valori di a e b.
Possiamo modificare swap per farla funzionare come vogliamo?
Sì, possiamo!
Possiamo passare gli indirizzi di a e b come puntatori.
Ma a cosa servirà, dato che la funzione copia i puntatori?
Serve, perché un indirizzo copiato è comunque lo stesso indirizzo.
Dereferenziando l'indirizzo possiamo accedere al valore originale così come era definito in main().
#include <stdio.h>
// pass in pointers to a and b
void swap(int *a, int *b) {
// set temp to value at a's dereferenced address
int temp = *a;
// set a's value to value at b's dereferenced address
*a = *b;
// set b's value to temp
*b = temp;
// prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}
int main() {
int a = 3, b = 4;
swap(&a, &b);
// prints: After 'swap', a is 4 and b is 3.
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
Ora ci sono un bel po' di * in swap, ma adesso funziona come voluto.
Un modo per leggere l'operatore di indirezione è «valore di».
Quindi *a è il valore di a.
Come funziona tutto questo con le stringhe?
Sappiamo che il valore di una variabile di tipo array è l'indirizzo in cui l'array inizia.
E sappiamo che possiamo definire una stringa sia come un array di char, char my_string[], sia come un puntatore a char, char *.
In che cosa sono diversi?
In questo codice passiamo un argomento di tipo char * a describe_me().
L'argomento adjective è definito come un array di char e viene accettato dalla funzione come un puntatore a char.
#include <stdio.h>
#include <string.h>
// takes pointer to char
void describe_me(char *adjective){
strcpy(adjective, "evil");
// prints: I am evil.
printf("I am %s.\n", adjective);
}
int main() {
// defined as a char array
char adjective[] = "good";
describe_me(adjective);
// prints: I am really evil.
printf("I am really %s.\n", adjective);
return 0;
}
Riusciamo a fare una modifica persistente a adjective, il che dimostra che l'array di char definito può essere accettato come un puntatore a char.
Ora proviamo a definire adjective direttamente come un puntatore a char.
#include <stdio.h>
#include <string.h>
void describe_me(char *adjective){
// segmentation fault on this line when program is executed
strcpy(adjective, "evil");
printf("I am %s.\n", adjective);
}
int main() {
char *adjective = "good";
describe_me(adjective);
printf("I am really %s.\n", adjective);
return 0;
}
Ricordi quanto i puntatori siano potenti, ma anche potenzialmente pericolosi?
Ma perché non funziona?
Un array è un puntatore e un puntatore funzionava con un array di char!
Il motivo è che, prima, adjective era definito come un array di char, che è una variabile appartenente a main().
Ora, adjective è definito come una stringa letterale, il che significa che i chars vengono scritti in una memoria statica di sola lettura che appartiene all'intero programma.
Sono entrambi puntatori, ma puntano a cose diverse.
Cercare di modificare una stringa letterale è come cercare di modificare il valore letterale di 5.
Non è consentito.
Quindi il problema non è davvero che si tratta di un puntatore.
Il problema è ciò a cui punta.