Chiarimento da puntatore a puntatore


142

Stavo seguendo questo tutorial su come funziona un puntatore a un puntatore .

Vorrei citare il passaggio pertinente:


    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;

Ora possiamo impostare

    int **ipp = &ip1;

e ippindica a ip1quali punti i. *ippè ip1, ed **ippè i, o 5. Possiamo illustrare la situazione, con la nostra nota notazione con la casella e la freccia, in questo modo:

inserisci qui la descrizione dell'immagine

Se poi diciamo

    *ipp = ip2;

abbiamo cambiato il puntatore a cui punta ipp(cioè, ip1) per contenere una copia di ip2, in modo che ora ( ip1) punti a j:

inserisci qui la descrizione dell'immagine


La mia domanda è: perché nella seconda foto, ipppunta ancora ip1ma non ip2?


6
per favore, aggiungi il tipo di ippquando lo definisci, quindi la tua domanda è completa ;-)
zmo

1
Perché ippindicare ip1così *ipp = ip2è lo stesso diip1 = ip2
Grijesh Chauhan

1
Possiamo per favore smettere di scrivere asterisco così lontani dal tipo appuntito? int **ippè molto meno intuitivo di int** ipp, specialmente quando **ippsignifica una cosa completamente diversa al di fuori delle dichiarazioni di tipo.
Darkhogg,

4
@Darkhogg mi int **ippsembra abbastanza intuitivo. Significa che sto facendo **ippun int. Che è vero.
ithisa,

2
@ user54609 significati e l'intuitività personali a parte, considerare questo: il tipo di ippè int**, quindi basta scrivere int**al posto di magica " impdereference è un int" comprensioni .
Manu343726,

Risposte:


143

Dimentica un secondo dell'analogia di puntamento. Ciò che un puntatore contiene veramente è un indirizzo di memoria. L' &operatore è "indirizzo di", ovvero restituisce l'indirizzo in memoria di un oggetto. L' *operatore ti fornisce l'oggetto a cui fa riferimento un puntatore, ovvero dato un puntatore contenente un indirizzo, restituisce l'oggetto a quell'indirizzo di memoria. Quindi, quando lo fai *ipp = ip2, quello che stai facendo è *ippottenere l'oggetto all'indirizzo in ippcui si trova ip1e quindi assegnare al ip1valore memorizzato in ip2, che è l'indirizzo di j.

Semplicemente
& -> Indirizzo di
*-> Valore a


14
& e * non sono mai stati così facili
Ray

7
Credo che la principale fonte di confusione sia dovuta all'ambiguità dell'operatore *, che durante la dichiarazione delle variabili viene utilizzato per indicare che la variabile, in effetti, è un puntatore a un determinato tipo di dati. D'altra parte, viene anche usato nelle istruzioni per accedere al contenuto della variabile puntato da un puntatore (operatore di dereferenziazione).
Lucas A.

43

Perché hai cambiato il valore indicato ippnon dal valore di ipp. Quindi, ippindica ancora ip1(il valore di ipp), ip1il valore di ora è uguale al ip2valore di, quindi entrambi indicano j.

Questo:

*ipp = ip2;

equivale a:

ip1 = ip2;

11
Potrebbe valere la pena sottolineare la differenza tra int *ip1 = &ie *ipp = ip2;, ad esempio se si rimuove la intprima istruzione, le assegnazioni sembrano molto simili, ma *sta facendo qualcosa di molto diverso nei due casi.
Crowman,

22

Come la maggior parte delle domande per principianti nel tag C, a questa domanda si può rispondere tornando ai primi principi:

  • Un puntatore è un tipo di valore.
  • Una variabile contiene un valore.
  • L' &operatore trasforma una variabile in un puntatore.
  • L' *operatore trasforma un puntatore in una variabile.

(Tecnicamente dovrei dire "lvalue" invece di "variabile", ma sento che è più chiaro descrivere le posizioni di memoria mutabili come "variabili".)

Quindi abbiamo variabili:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

La variabile ip1 contiene un puntatore. L' &operatore si trasforma iin un puntatore e viene assegnato quel valore di puntatore ip1. Quindi ip1 contiene un puntatore a i.

La variabile ip2 contiene un puntatore. L' &operatore si trasforma jin un puntatore a cui è assegnato quel puntatore ip2. Quindi ip2 contiene un puntatore a j.

int **ipp = &ip1;

La variabile ippcontiene un puntatore. L' &operatore trasforma la variabile ip1in un puntatore e viene assegnato quel valore di puntatore ipp. Quindi ippcontiene un puntatore a ip1.

Riassumiamo la storia finora:

  • i contiene 5
  • j contiene 6
  • ip1contiene "puntatore a i"
  • ip2contiene "puntatore a j"
  • ippcontiene "puntatore a ip1"

Adesso diciamo

*ipp = ip2;

L' *operatore trasforma di nuovo un puntatore in una variabile. Prendiamo il valore di ipp, che è "puntatore a ip1e lo trasformiamo in una variabile. Quale variabile? ip1Ovviamente!

Quindi questo è semplicemente un altro modo di dire

ip1 = ip2;

Quindi prendiamo il valore di ip2. Che cos'è? "puntatore a j". Assegniamo quel valore di puntatore a ip1, quindi ip1ora è "puntatore a j"

Abbiamo cambiato solo una cosa: il valore di ip1:

  • i contiene 5
  • j contiene 6
  • ip1contiene "puntatore a j"
  • ip2contiene "puntatore a j"
  • ippcontiene "puntatore a ip1"

Perché indica ippancora ip1e no ip2?

Una variabile cambia quando le assegni. Conta i compiti; non ci possono essere più modifiche alle variabili di quante siano le assegnazioni! Si inizia assegnando a i, j, ip1, ip2e ipp. Quindi si assegna a *ipp, che come abbiamo visto significa lo stesso "assegnare a ip1". Dal momento che non hai assegnato ippuna seconda volta, non è cambiato!

Se vuoi cambiare, ippdovrai effettivamente assegnarti a ipp:

ipp = &ip2;

per esempio.


21

spero che questo pezzo di codice possa aiutare.

#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;
    int** ipp = &ip1;
    printf("address of value i: %p\n", &i);
    printf("address of value j: %p\n", &j);
    printf("value ip1: %p\n", ip1);
    printf("value ip2: %p\n", ip2);
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
    *ipp = ip2;
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
}

produce:

inserisci qui la descrizione dell'immagine


12

La mia opinione molto personale è che le immagini con le frecce che puntano in questo modo o che rendono più difficile la comprensione dei puntatori. Li fa sembrare delle entità astratte e misteriose. Non sono.

Come tutto il resto nel tuo computer, i puntatori sono numeri . Il nome "puntatore" è solo un modo elegante di dire "una variabile contenente un indirizzo".

Pertanto, lasciatemi mescolare le cose spiegando come funziona effettivamente un computer.

Abbiamo un int, ha il nome ie il valore 5. Questo è memorizzato. Come tutto ciò che è memorizzato, ha bisogno di un indirizzo, altrimenti non saremmo in grado di trovarlo. Diciamo che ifinisce all'indirizzo 0x12345678 e il suo amico jcon valore 6 finisce subito dopo. Supponendo una CPU a 32 bit in cui int è 4 byte e i puntatori sono 4 byte, quindi le variabili vengono archiviate nella memoria fisica in questo modo:

Address     Data           Meaning
0x12345678  00 00 00 05    // The variable i
0x1234567C  00 00 00 06    // The variable j

Ora vogliamo puntare a queste variabili. Creiamo un puntatore a int int* ip1, e uno int* ip2. Come ogni cosa nel computer, anche queste variabili puntatore vengono allocate da qualche parte nella memoria. Supponiamo che finiscano ai successivi indirizzi adiacenti in memoria, immediatamente dopo j. ip1=&i;Impostiamo i puntatori per contenere gli indirizzi delle variabili precedentemente allocate: ("copia l'indirizzo di i in ip1") e ip2=&j. Quello che succede tra le righe è:

Address     Data           Meaning
0x12345680  12 34 56 78    // The variable ip1(equal to address of i)
0x12345684  12 34 56 7C    // The variable ip2(equal to address of j)

Quindi quello che abbiamo ottenuto erano solo alcuni blocchi di memoria a 4 byte contenenti numeri. Non ci sono frecce mistiche o magiche da nessuna parte in vista.

In effetti, solo guardando un dump della memoria, non possiamo dire se l'indirizzo 0x12345680 contiene un into int*. La differenza sta nel modo in cui il nostro programma sceglie di utilizzare i contenuti memorizzati a questo indirizzo. (Il compito del nostro programma è in realtà solo quello di dire alla CPU cosa fare con questi numeri.)

Quindi aggiungiamo ancora un altro livello di riferimento indiretto con int** ipp = &ip1;. Ancora una volta, abbiamo solo un pezzo di memoria:

Address     Data           Meaning
0x12345688  12 34 56 80    // The variable ipp

Il modello sembra familiare. Ancora un altro pezzo di 4 byte contenente un numero.

Ora, se avessimo un dump della memoria della RAM fittizia sopra descritta, potremmo controllare manualmente dove puntano questi puntatori. Diamo un'occhiata a ciò che è memorizzato all'indirizzo della ippvariabile e troviamo il contenuto 0x12345680. Qual è ovviamente l'indirizzo in cui ip1è memorizzato. Possiamo andare a quell'indirizzo, controllare i contenuti lì, e trovare l'indirizzo di i, e infine possiamo andare a quell'indirizzo e trovare il numero 5.

Quindi, se prendiamo il contenuto di ipp, *ippotterremo l'indirizzo della variabile puntatore ip1. Scrivendo *ipp=ip2copiamo ip2 in ip1, è equivalente a ip1=ip2. In entrambi i casi avremmo

Address     Data           Meaning
0x12345680  12 34 56 7C    // The variable ip1
0x12345684  12 34 56 7C    // The variable ip2

(Questi esempi sono stati forniti per una CPU big endian)


5
Anche se prendo in considerazione il tuo punto, è utile pensare ai puntatori come entità astratte e misteriose. Ogni particolare implementazione di puntatori è solo un numero, ma la strategia di implementazione che disegni non è un requisito di un'implementazione, è solo una strategia comune. I puntatori non devono necessariamente avere le stesse dimensioni di un int, i puntatori non devono necessariamente essere indirizzi in un modello di memoria virtuale piatto e così via; questi sono solo dettagli di implementazione.
Eric Lippert,

@EricLippert Penso che uno possa rendere questo esempio più astratto non usando indirizzi di memoria o blocchi di dati reali. Se fosse una tabella in cui si affermava qualcosa di simile alla location, value, variableposizione 1,2,3,4,5e al valore della posizione A,1,B,C,3, l'idea corrispondente di puntatori potrebbe essere spiegata facilmente senza l'uso di frecce, che sono intrinsecamente confuse. Con qualsiasi implementazione si scelga, esiste un valore in qualche posizione, e questo è un pezzo del puzzle che viene offuscato quando si modella con le frecce.
MirroredFate

@EricLippert Nella mia esperienza, la maggior parte dei potenziali programmatori C che hanno problemi a comprendere i puntatori, sono quelli che sono stati nutriti con modelli astratti e artificiali. L'astrazione non è utile, perché l'intero scopo del linguaggio C oggi è che è vicino all'hardware. Se stai imparando C ma non intendi scrivere codice vicino all'hardware, stai perdendo tempo . Java ecc. È una scelta molto migliore se non vuoi sapere come funzionano i computer, ma fai solo programmi di alto livello.
Lundin,

@EricLippert E sì, possono esistere varie implementazioni oscure di puntatori, in cui i puntatori non corrispondono necessariamente agli indirizzi. Ma le frecce di disegno non ti aiuteranno nemmeno a capire come funzionano. Ad un certo punto devi abbandonare il pensiero astratto e scendere al livello hardware, altrimenti non dovresti usare C. Esistono molti linguaggi molto più adatti e moderni destinati alla programmazione di alto livello puramente astratta.
Lundin,

@Lundin: Neanche io sono un grande fan dei diagrammi a freccia; la nozione di una freccia come dati è difficile. Preferisco pensarci in modo astratto ma senza frecce. L' &operatore su una variabile ti dà una moneta che rappresenta quella variabile. L' *operatore su quella moneta ti restituisce la variabile. Non sono necessarie frecce!
Eric Lippert,

8

Notare gli incarichi:

ipp = &ip1;

risultati ippa cui puntare ip1.

quindi per ippindicare ip2, dovremmo cambiare allo stesso modo,

ipp = &ip2;

cosa che chiaramente non stiamo facendo. Invece stiamo cambiando il valore all'indirizzo indicato da ipp.
Facendo il folowing

*ipp = ip2;

stiamo solo sostituendo il valore memorizzato in ip1.

ipp = &ip1, Mezzi *ipp = ip1 = &i,
ora, *ipp = ip2 = &j.
Quindi, *ipp = ip2è essenzialmente lo stesso di ip1 = ip2.


5
ipp = &ip1;

Nessuna assegnazione successiva ha modificato il valore di ipp. Questo è il motivo per cui indica ancora ip1.

Quello che fai con *ipp, cioè con ip1, non cambia il fatto che ippindica ip1.


5

La mia domanda è: perché nella seconda immagine, ipp è ancora puntato su ip1 ma non su ip2?

hai messo delle belle foto, proverò a fare una bella arte ascii:

Come ha detto @ Robert-S-Barnes nella sua risposta: dimentica i puntatori e cosa indica cosa, ma pensa in termini di memoria. Fondamentalmente, int*significa che contiene l'indirizzo di una variabile e int**contiene l'indirizzo di una variabile che contiene l'indirizzo di una variabile. Quindi è possibile utilizzare l'algebra del puntatore per accedere ai valori o agli indirizzi: &foomedie address of fooe *foomedie value of the address contained in foo.

Quindi, poiché i puntatori riguardano la gestione della memoria, il modo migliore per rendere effettivamente "tangibile" è mostrare ciò che l'algebra dei puntatori fa alla memoria.

Quindi, ecco la memoria del tuo programma (semplificata ai fini dell'esempio):

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [   |   |   |   |   ]

quando fai il tuo codice iniziale:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

ecco come appare la tua memoria:

name:    i   j ip1 ip2
addr:    0   1   2   3
mem : [  5|  6|  0|  1]

lì puoi vedere ip1e ip2ottenere gli indirizzi di ie je ippancora non esiste. Non dimenticare che gli indirizzi sono semplicemente numeri interi memorizzati con un tipo speciale.

Quindi dichiari e definisci ippcome:

int **ipp = &ip1;

quindi ecco la tua memoria:

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  0|  1|  2]

e quindi, stai cambiando il valore indicato dall'indirizzo memorizzato ipp, che è l'indirizzo memorizzato in ip1:

*ipp = ip2;

la memoria del programma è

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  1|  1|  2]

NB: poiché int*è un tipo speciale, preferisco sempre evitare di dichiarare più puntatori sulla stessa riga, poiché penso che la notazione int *x;o la int *x, *y;notazione possa essere fuorviante. Preferisco scrivereint* x; int* y;

HTH


con il tuo esempio, il valore iniziale di ip2dovrebbe essere 3non 4.
Dipto,

1
oh, ho appena cambiato la memoria in modo che corrisponda all'ordine della dichiarazione. Immagino di averlo risolto nel farlo?
zmo

5

Perché quando dici

*ipp = ip2

stai dicendo 'oggetto puntato da ipp' per indicare la direzione della memoria che ip2sta puntando.

Non stai dicendo ippdi indicare ip2.


4

Se si aggiunge l'operatore dereference *al puntatore, si reindirizza dal puntatore all'oggetto puntato.

Esempi:

int i = 0;
int *p = &i; // <-- N.B. the pointer declaration also uses the `*`
             //     it's not the dereference operator in this context
*p;          // <-- this expression uses the pointed-to object, that is `i`
p;           // <-- this expression uses the pointer object itself, that is `p`

Perciò:

*ipp = ip2; // <-- you change the pointer `ipp` points to, not `ipp` itself
            //     therefore, `ipp` still points to `ip1` afterwards.

3

Se vuoi ippindicare ip2, dovresti dirlo ipp = &ip2;. Tuttavia, questo lascerebbe ip1ancora indicare i.


3

All'inizio hai impostato,

ipp = &ip1;

Ora dereference come,

*ipp = *&ip1 // Here *& becomes 1  
*ipp = ip1   // Hence proved 

3

Considere ogni variabile rappresentata in questo modo:

type  : (name, adress, value)

quindi le tue variabili dovrebbero essere rappresentate in questo modo

int   : ( i ,  &i , 5 ); ( j ,  &j ,  6); ( k ,  &k , 5 )

int*  : (ip1, &ip1, &i); (ip1, &ip1, &j)

int** : (ipp, &ipp, &ip1)

Poiché il valore di ippè &ip1così l'istruzione:

*ipp = ip2;

cambia il valore nell'addome &ip1con il valore di ip2, il che significa che ip1è cambiato:

(ip1, &ip1, &i) -> (ip1, &ip1, &j)

Ma ippancora:

(ipp, &ipp, &ip1)

Quindi il valore di ippstill &ip1significa che indica ancora ip1.


1

Perché stai cambiando il puntatore di *ipp. Significa

  1. ipp (nome variabile) ---- vai dentro.
  2. dentro ippc'è l'indirizzo di ip1.
  3. ora *ippvai a (indirizzo di dentro) ip1.

Ora siamo a ip1. *ipp(ie ip1) = ip2.
ip2contiene l'indirizzo di j.so il ip1contenuto sarà sostituito da contiene di ip2 (cioè l'indirizzo di j), NON CAMBIAMO ippCONTENUTO. QUESTO È TUTTO.


1

*ipp = ip2; implica:

Assegna ip2alla variabile indicata da ipp. Quindi questo equivale a:

ip1 = ip2;

Se si desidera ip2memorizzare l'indirizzo di ipp, fare semplicemente:

ipp = &ip2;

Ora ippindica ip2.


0

ipppuò contenere un valore (ovvero puntare a) un puntatore a un oggetto di tipo puntatore . Quando lo fai

ipp = &ip2;  

quindi ippcontiene l' indirizzo della variabile (puntatore)ip2 , che è ( &ip2) di tipo puntatore a puntatore . Ora la freccia ippnella seconda foto indicherà ip2.

Wiki dice:
L' *operatore è un operatore di dereference che opera sulla variabile puntatore e restituisce un valore l (variabile) equivalente al valore all'indirizzo del puntatore. Questo si chiama dereferenziare il puntatore.

Applicando l' *operatore su ippderefrence a un valore l di puntatore daint digitare. Il valore l dedotto *ippè di tipo puntatore aint , può contenere l'indirizzo di un inttipo di dati. Dopo la dichiarazione

ipp = &ip1;

ippè in possesso dell'indirizzo di ip1e *ippè in possesso dell'indirizzo di (indicando) i. Puoi dire che *ippè un alias di ip1. Entrambi **ippe *ip1sono alias per i.
Facendo

 *ipp = ip2;  

*ipped ip2entrambi puntano nella stessa posizione ma ipppuntano ancora a ip1.

Ciò *ipp = ip2;che effettivamente è che copia il contenuto di ip2(l'indirizzo di j) in ip1(come *ippè un alias per ip1), in effetti facendo entrambi i puntatori ip1e ip2puntando allo stesso oggetto ( j).
Quindi, nella seconda figura, la freccia di ip1e ip2sta indicando jwhileipp sta ancora puntando a ip1come nessuna modifica è stata fatta per cambiare il valore diipp .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.