Come funzionano i puntatori ai puntatori in C? Quando li useresti?
Come funzionano i puntatori ai puntatori in C? Quando li useresti?
Risposte:
Supponiamo che un computer a 8 bit con indirizzi a 8 bit (e quindi solo 256 byte di memoria). Questo fa parte di quella memoria (i numeri in alto sono gli indirizzi):
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| | 58 | | | 63 | | 55 | | | h | e | l | l | o | \0 | |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
Quello che puoi vedere qui, è che all'indirizzo 63 inizia la stringa "ciao". Quindi, in questo caso, se questa è l'unica occorrenza di "ciao" in memoria,
const char *c = "hello";
... definisce cdi essere un puntatore alla stringa (sola lettura) "ciao", e quindi contiene il valore 63. cdeve essere archiviato da qualche parte: nell'esempio sopra nella posizione 58. Naturalmente non possiamo solo puntare a caratteri , ma anche ad altri puntatori. Per esempio:
const char **cp = &c;
Ora cpindica c, cioè, contiene l'indirizzo di c(che è 58). Possiamo andare ancora oltre. Tener conto di:
const char ***cpp = &cp;
Ora cppmemorizza l'indirizzo di cp. Quindi ha un valore 55 (basato sull'esempio sopra), e hai indovinato: è esso stesso memorizzato all'indirizzo 60.
Per quanto riguarda il motivo per cui si usano i puntatori ai puntatori:
t, un riferimento all'array ha tipo t *. Consideriamo ora un array di array di tipo t: naturalmente un riferimento a questo array 2D avrà type (t *)*= t **, ed è quindi un puntatore a un puntatore.char **.fdovrà accettare un argomento di tipo t **se si desidera modificare una variabile di tipo t *.Come funzionano i puntatori ai puntatori in C?
Innanzitutto un puntatore è una variabile, come qualsiasi altra variabile, ma che contiene l'indirizzo di una variabile.
Un puntatore a un puntatore è una variabile, come qualsiasi altra variabile, ma che contiene l'indirizzo di una variabile. Quella variabile sembra essere solo un puntatore.
Quando li useresti?
È possibile utilizzarli quando è necessario restituire un puntatore a un po 'di memoria sull'heap, ma non utilizzare il valore restituito.
Esempio:
int getValueOf5(int *p)
{
*p = 5;
return 1;//success
}
int get1024HeapMemory(int **p)
{
*p = malloc(1024);
if(*p == 0)
return -1;//error
else
return 0;//success
}
E lo chiami così:
int x;
getValueOf5(&x);//I want to fill the int varaible, so I pass it's address in
//At this point x holds 5
int *p;
get1024HeapMemory(&p);//I want to fill the int* variable, so I pass it's address in
//At this point p holds a memory address where 1024 bytes of memory is allocated on the heap
Ci sono anche altri usi, come l'argomento main () di ogni programma C ha un puntatore a un puntatore per argv, dove ogni elemento contiene un array di caratteri che sono le opzioni della riga di comando. È necessario fare attenzione, tuttavia, quando si utilizzano puntatori di puntatori per puntare su array bidimensionali, è preferibile utilizzare un puntatore a un array bidimensionale.
Perché è pericoloso?
void test()
{
double **a;
int i1 = sizeof(a[0]);//i1 == 4 == sizeof(double*)
double matrix[ROWS][COLUMNS];
int i2 = sizeof(matrix[0]);//i2 == 240 == COLUMNS * sizeof(double)
}
Ecco un esempio di puntatore a un array bidimensionale eseguito correttamente:
int (*myPointerTo2DimArray)[ROWS][COLUMNS]
Non è possibile utilizzare un puntatore a un array bidimensionale se si desidera supportare un numero variabile di elementi per ROWS e COLUMNS. Ma quando sai prima, useresti un array bidimensionale.
Mi piace questo esempio di codice "mondo reale" di utilizzo da puntatore a puntatore, in Git 2.0, commetto 7b1004b :
Linus una volta disse:
In realtà vorrei che più persone capissero il tipo di codifica di basso livello davvero fondamentale. Non cose grandi e complesse come la ricerca dei nomi senza lucchetto, ma semplicemente un buon uso di puntatori a puntatori ecc.
Ad esempio, ho visto troppe persone che eliminano una voce di elenco collegata singolarmente tenendo traccia della voce "precedente" e quindi per eliminare la voce, facendo qualcosa di simile
if (prev)
prev->next = entry->next;
else
list_head = entry->next;
e ogni volta che vedo codice del genere, vado semplicemente "Questa persona non capisce i puntatori". Ed è purtroppo abbastanza comune.
Le persone che comprendono i puntatori usano semplicemente un " puntatore al puntatore voce " e lo inizializzano con l'indirizzo di list_head. E poi mentre attraversano l'elenco, possono rimuovere la voce senza usare alcun condizionale, semplicemente facendo un
*pp = entry->next

Applicando questa semplificazione ci consente di perdere 7 righe da questa funzione anche quando si aggiungono 2 righe di commento.
- struct combine_diff_path *p, *pprev, *ptmp;
+ struct combine_diff_path *p, **tail = &curr;
Chris sottolinea nei commenti al video del 2016 " Linus Torvalds's Double Pointer Problem " di Philip Buuck .
kumar sottolinea nei commenti il post sul blog " Linus on Understanding Pointers ", in cui Grisha Trubetskoy spiega:
Immagina di avere un elenco collegato definito come:
typedef struct list_entry {
int val;
struct list_entry *next;
} list_entry;
Devi iterarlo dall'inizio alla fine e rimuovere un elemento specifico il cui valore è uguale al valore di to_remove.
Il modo più ovvio per farlo sarebbe:
list_entry *entry = head; /* assuming head exists and is the first entry of the list */
list_entry *prev = NULL;
while (entry) { /* line 4 */
if (entry->val == to_remove) /* this is the one to remove ; line 5 */
if (prev)
prev->next = entry->next; /* remove the entry ; line 7 */
else
head = entry->next; /* special case - first entry ; line 9 */
/* move on to the next entry */
prev = entry;
entry = entry->next;
}
Quello che stiamo facendo sopra è:
- iterando sulla lista fino a quando la voce è
NULL, il che significa che abbiamo raggiunto la fine della lista (riga 4).- Quando troviamo una voce che vogliamo rimuovere (riga 5),
- assegniamo il valore del puntatore successivo corrente a quello precedente,
- eliminando così l'elemento corrente (linea 7).
C'è un caso speciale sopra - all'inizio dell'iterazione non c'è una voce precedente (
prevèNULL), e quindi per rimuovere la prima voce nella lista devi modificare la testa stessa (riga 9).Quello che Linus stava dicendo è che il codice sopra potrebbe essere semplificato rendendo l'elemento precedente un puntatore a un puntatore anziché solo un puntatore .
Il codice quindi appare così:
list_entry **pp = &head; /* pointer to a pointer */
list_entry *entry = head;
while (entry) {
if (entry->val == to_remove)
*pp = entry->next;
pp = &entry->next;
entry = entry->next;
}
Il codice sopra è molto simile alla variante precedente, ma nota come non abbiamo più bisogno di guardare per il caso speciale del primo elemento dell'elenco, poiché
ppnon èNULLall'inizio. Semplice e intelligente.Inoltre, qualcuno in quel thread ha commentato che il motivo per cui è meglio è perché
*pp = entry->nextè atomico. Certamente NON è atomico .
L'espressione sopra contiene due operatori di dereference (*e->) e un incarico, e nessuna di queste tre cose è atomica.
Questo è un malinteso comune, ma purtroppo praticamente nulla in C dovrebbe mai essere considerato atomico (inclusi gli operatori++e--)!
Nel fornire indicazioni su un corso di programmazione all'università, ci hanno dato due suggerimenti su come iniziare a conoscerli. Il primo è stato vedere Pointer Fun With Binky . Il secondo è stato quello di pensare al passaggio degli Haddocks 'Eyes da Through the Looking-Glass di Lewis Carroll
"Sei triste", disse il Cavaliere con tono ansioso: "Lascia che ti canti una canzone per confortarti."
"È molto lungo?" Chiese Alice, poiché quel giorno aveva ascoltato molte poesie.
"È lungo", disse il Cavaliere, "ma è molto, molto bello. Tutti quelli che mi ascoltano cantare - o porta le lacrime agli occhi, oppure -
"Oppure cosa?" disse Alice, perché il Cavaliere aveva fatto una pausa improvvisa.
“Oppure no, lo sai. Il nome della canzone si chiama 'Haddocks' Eyes. '”
"Oh, questo è il nome della canzone, vero?" Disse Alice, cercando di sentirsi interessata.
"No, non capisci," disse il Cavaliere, un po 'seccato. “Ecco come si chiama il nome. Il nome è davvero "The Aged Aged Man". "
"Allora avrei dovuto dire 'Ecco come si chiama la canzone'?" Si corresse Alice.
“No, non dovresti: è un'altra cosa! La canzone si chiama 'Ways And Means': ma è solo quello che si chiama, lo sai! ”
"Bene, qual è la canzone, allora?" disse Alice, che ormai era completamente sconcertata.
"Stavo arrivando a quello", ha detto il cavaliere. "La canzone è davvero 'A-sitting On A Gate': e la melodia è la mia invenzione."
Potresti voler leggere questo: da puntatori a puntatori
Spero che questo aiuti a chiarire alcuni dubbi di base.
Quando è richiesto un riferimento a un puntatore. Ad esempio, quando si desidera modificare il valore (indirizzo puntato a) di una variabile puntatore dichiarata nell'ambito di una funzione chiamante all'interno di una funzione chiamata.
Se si passa un singolo puntatore come argomento, si modificheranno le copie locali del puntatore, non il puntatore originale nell'ambito di chiamata. Con un puntatore a un puntatore, si modifica quest'ultimo.
Un puntatore a un puntatore è anche chiamato handle . Un utilizzo è spesso quando un oggetto può essere spostato in memoria o rimosso. Uno è spesso responsabile di bloccare e sbloccare l'utilizzo dell'oggetto in modo che non venga spostato durante l'accesso.
Viene spesso utilizzato in ambienti con restrizioni di memoria, ad esempio Palm OS.
Considera la figura e il programma di seguito per comprendere meglio questo concetto .
Come da figura, ptr1 è un singolo puntatore che ha l'indirizzo della variabile num .
ptr1 = #
Allo stesso modo ptr2 è un puntatore al puntatore (doppio puntatore) che ha l'indirizzo del puntatore ptr1 .
ptr2 = &ptr1;
Un puntatore che punta a un altro puntatore è noto come doppio puntatore. In questo esempio ptr2 è un doppio puntatore.
Valori dal diagramma sopra:
Address of variable num has : 1000
Address of Pointer ptr1 is: 2000
Address of Pointer ptr2 is: 3000
Esempio:
#include <stdio.h>
int main ()
{
int num = 10;
int *ptr1;
int **ptr2;
// Take the address of var
ptr1 = #
// Take the address of ptr1 using address of operator &
ptr2 = &ptr1;
// Print the value
printf("Value of num = %d\n", num );
printf("Value available at *ptr1 = %d\n", *ptr1 );
printf("Value available at **ptr2 = %d\n", **ptr2);
}
Produzione:
Value of num = 10
Value available at *ptr1 = 10
Value available at **ptr2 = 10
è un puntatore al valore dell'indirizzo del puntatore. (è terribile lo so)
in sostanza, ti consente di passare un puntatore al valore dell'indirizzo di un altro puntatore, in modo da poter modificare il punto in cui un altro puntatore punta da una funzione secondaria, come:
void changeptr(int** pp)
{
*pp=&someval;
}
Un puntatore a puntatore è, bene, un puntatore a puntatore.
Un esempio significativo di someType ** è un array bidimensionale: hai un array, pieno di puntatori ad altri array, quindi quando scrivi
DPointer [5] [6]
si accede alla matrice che contiene puntatori ad altre matrici nella sua 5a posizione, si ottiene il puntatore (lasciare fpointer il suo nome) e quindi si accede al sesto elemento della matrice a cui fa riferimento quella matrice (quindi, fpointer [6]).
Come funziona: è una variabile che può memorizzare un altro puntatore.
Quando li useresti: molti usano uno di questi è se la tua funzione vuole costruire un array e restituirlo al chiamante.
//returns the array of roll nos {11, 12} through paramater
// return value is total number of students
int fun( int **i )
{
int *j;
*i = (int*)malloc ( 2*sizeof(int) );
**i = 11; // e.g., newly allocated memory 0x2000 store 11
j = *i;
j++;
*j = 12; ; // e.g., newly allocated memory 0x2004 store 12
return 2;
}
int main()
{
int *i;
int n = fun( &i ); // hey I don't know how many students are in your class please send all of their roll numbers.
for ( int j=0; j<n; j++ )
printf( "roll no = %d \n", i[j] );
return 0;
}
Ho creato un video di 5 minuti che spiega come funzionano i puntatori:
Ci sono così tante spiegazioni utili, ma non ho trovato solo una breve descrizione, quindi ..
Fondamentalmente il puntatore è l'indirizzo della variabile. Codice sintetico breve:
int a, *p_a;//declaration of normal variable and int pointer variable
a = 56; //simply assign value
p_a = &a; //save address of "a" to pointer variable
*p_a = 15; //override the value of the variable
//print 0xfoo and 15
//- first is address, 2nd is value stored at this address (that is called dereference)
printf("pointer p_a is having value %d and targeting at variable value %d", p_a, *p_a);
Altre informazioni utili sono disponibili nell'argomento Cosa significa riferimento e dereferenza
E non sono così sicuro, quando possono essere utili i puntatori, ma in comune è necessario usarli quando si esegue una allocazione manuale / dinamica della memoria: malloc, calloc, ecc.
Quindi spero che aiuterà anche a chiarire il problema :)