Quali sono gli ostacoli alla comprensione dei puntatori e cosa si può fare per superarli?[Chiuso]

StackOverflow https://stackoverflow.com/questions/5727

  •  08-06-2019
  •  | 
  •  

Domanda

Perché i puntatori sono un fattore di confusione così importante per molti studenti nuovi e anche vecchi di livello universitario in C o C++?Esistono strumenti o processi mentali che ti hanno aiutato a capire come funzionano i puntatori a livello di variabile, funzione e oltre?

Quali sono alcune buone pratiche che si possono fare per portare qualcuno al livello di "Ah-hah, ho capito", senza impantanarlo nel concetto generale?Fondamentalmente, esegui scenari simili.

È stato utile?

Soluzione

I puntatori sono un concetto che all'inizio può creare confusione per molti, in particolare quando si tratta di copiare i valori dei puntatori e fare comunque riferimento allo stesso blocco di memoria.

Ho scoperto che l'analogia migliore è considerare il puntatore come un pezzo di carta con sopra l'indirizzo di una casa e il blocco di memoria a cui fa riferimento come la casa vera e propria.Tutti i tipi di operazioni possono quindi essere facilmente spiegati.

Ho aggiunto del codice Delphi in basso e alcuni commenti ove appropriato.Ho scelto Delphi poiché il mio altro linguaggio di programmazione principale, C#, non presenta cose come perdite di memoria allo stesso modo.

Se desideri solo apprendere il concetto di alto livello dei puntatori, dovresti ignorare le parti etichettate "Layout della memoria" nella spiegazione seguente.Hanno lo scopo di fornire esempi di come potrebbe apparire la memoria dopo le operazioni, ma sono di natura più bassa.Tuttavia, per spiegare con precisione come funzionano realmente i sovraccarichi del buffer, era importante aggiungere questi diagrammi.

Disclaimer:A tutti gli effetti, questa spiegazione e i layout di memoria di esempio sono notevolmente semplificati.Ci sono più spese generali e molti più dettagli che dovresti sapere se devi affrontare la memoria a basso livello.Tuttavia, per gli intenti di spiegare memoria e puntatori, è abbastanza accurato.


Supponiamo che la classe THouse utilizzata di seguito assomigli a questa:

type
    THouse = class
    private
        FName : array[0..9] of Char;
    public
        constructor Create(name: PChar);
    end;

Quando inizializzi l'oggetto casa, il nome dato al costruttore viene copiato nel campo privato FName.C'è una ragione per cui è definito come un array a dimensione fissa.

In memoria, ci sarà un sovraccarico associato all'allocazione della casa, lo illustrerò di seguito in questo modo:

---[ttttNNNNNNNNNN]---
     ^   ^
     |   |
     |   +- the FName array
     |
     +- overhead

L'area "tttt" è in alto, in genere ce ne sarà di più per vari tipi di runtime e linguaggi, come 8 o 12 byte.È fondamentale che qualunque valore sia memorizzato in quest'area non venga mai modificato da qualcosa di diverso dall'allocatore di memoria o dalle routine principali del sistema, altrimenti si rischia di mandare in crash il programma.


Allocare memoria

Chiedi a un imprenditore di costruire la tua casa e di darti l'indirizzo della casa.A differenza del mondo reale, non è possibile dire all'allocazione della memoria dove allocare, ma troverà un posto adatto con spazio sufficiente e riporterà l'indirizzo alla memoria allocata.

In altre parole, sarà l’imprenditore a scegliere il posto.

THouse.Create('My house');

Disposizione della memoria:

---[ttttNNNNNNNNNN]---
    1234My house

Mantieni una variabile con l'indirizzo

Scrivi l'indirizzo della tua nuova casa su un pezzo di carta.Questo documento servirà come riferimento per la tua casa.Senza questo pezzo di carta sei perso e non puoi trovare la casa, a meno che non ci sia già dentro.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...

Disposizione della memoria:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

Copia il valore del puntatore

Basta scrivere l'indirizzo su un nuovo pezzo di carta.Ora hai due pezzi di carta che ti porteranno alla stessa casa, non a due case separate.Qualsiasi tentativo di seguire l'indirizzo da un foglio e di riorganizzare i mobili di quella casa lo farà sembrare così l'altra casa è stato modificato nello stesso modo, a meno che tu non possa rilevare esplicitamente che in realtà si tratta di una sola casa.

Nota Questo di solito è il concetto che ho più problemi a spiegare alle persone, due puntatori non significano due oggetti o blocchi di memoria.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1
    v
---[ttttNNNNNNNNNN]---
    1234My house
    ^
    h2

Liberare la memoria

Demolire la casa.In seguito potrai riutilizzare il documento per un nuovo indirizzo, se lo desideri, oppure cancellarlo per dimenticare l'indirizzo della casa che non esiste più.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    h := nil;

Qui costruisco prima la casa e mi prendo il suo indirizzo.Poi faccio qualcosa alla casa (usala, il...codice, lasciato come esercizio per il lettore), e poi lo libero.Infine cancello l'indirizzo dalla mia variabile.

Disposizione della memoria:

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after free
----------------------          | (note, memory might still
    xx34My house             <--+  contain some data)

Puntatori penzolanti

Dici al tuo imprenditore di distruggere la casa, ma ti dimentichi di cancellare l'indirizzo dal tuo pezzo di carta.Quando poi guardi il pezzo di carta, hai dimenticato che la casa non c'è più, e vai a visitarla, con esito negativo (vedi anche la parte relativa al riferimento non valido più avanti).

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    ... // forgot to clear h here
    h.OpenFrontDoor; // will most likely fail

Utilizzando h dopo la chiamata a .Free Potrebbe lavoro, ma è solo pura fortuna.Molto probabilmente fallirà presso il cliente, nel bel mezzo di un'operazione critica.

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h                        <--+
    v                           +- after free
----------------------          |
    xx34My house             <--+

Come puoi vedere, H indica ancora i resti dei dati in memoria, ma poiché potrebbe non essere completo, usandolo come prima potrebbe fallire.


Perdita di memoria

Perdi il pezzo di carta e non riesci a trovare la casa.La casa però è ancora in piedi da qualche parte e quando in seguito vorrai costruire una nuova casa, non potrai riutilizzare quel punto.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    h := THouse.Create('My house'); // uh-oh, what happened to our first house?
    ...
    h.Free;
    h := nil;

Qui abbiamo sovrascritto il contenuto del file h variabile con l'indirizzo di una nuova casa, ma quella vecchia è ancora in piedi...in qualche luogo.Dopo questo codice, non c'è modo di raggiungere quella casa e rimarrà in piedi.In altre parole, la memoria allocata rimarrà allocata fino alla chiusura dell'applicazione, dopodiché il sistema operativo la distruggerà.

Disposizione della memoria dopo la prima allocazione:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

Layout della memoria dopo la seconda allocazione:

                       h
                       v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

Un modo più comune per ottenere questo metodo è semplicemente dimenticare di liberare qualcosa, invece di sovrascriverlo come sopra.In termini Delphi, ciò avverrà con il seguente metodo:

procedure OpenTheFrontDoorOfANewHouse;
var
    h: THouse;
begin
    h := THouse.Create('My house');
    h.OpenFrontDoor;
    // uh-oh, no .Free here, where does the address go?
end;

Dopo che questo metodo è stato eseguito, nelle nostre variabili non c'è spazio per l'esistenza dell'indirizzo della casa, ma la casa è ancora là fuori.

Disposizione della memoria:

    h                        <--+
    v                           +- before losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

Come puoi vedere, i vecchi dati vengono lasciati intatti in memoria e non saranno riutilizzati dall'allocatore di memoria.L'allocatore tiene traccia di quali aree di memoria sono state utilizzate e non le riutilizzerà a meno che non lo liberi.


Liberare la memoria ma mantenere un riferimento (ora non valido).

Demolisci la casa, cancella uno dei pezzi di carta ma hai anche un altro pezzo di carta con sopra il vecchio indirizzo, quando vai all'indirizzo non troverai una casa, ma potresti trovare qualcosa che assomiglia alle rovine di uno.

Forse troverai anche una casa, ma non è la casa a cui ti è stato originariamente dato l'indirizzo, e quindi qualsiasi tentativo di usarla come se ti appartenesse potrebbe fallire orribilmente.

A volte potresti anche scoprire che un indirizzo vicino ha una casa piuttosto grande che occupa tre indirizzi (Main Street 1-3) e il tuo indirizzo va al centro della casa.Anche qualsiasi tentativo di trattare quella parte della grande casa a 3 indirizzi come un'unica piccola casa potrebbe fallire orribilmente.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1.Free;
    h1 := nil;
    h2.OpenFrontDoor; // uh-oh, what happened to our house?

Qui la casa è stata demolita, attraverso il riferimento in h1, e mentre h1 è stato anch'esso cancellato, h2 ha ancora il vecchio indirizzo non aggiornato.L’accesso alla casa che non c’è più potrebbe funzionare o meno.

Questa è una variazione del puntatore pendente sopra.Vedi il suo layout di memoria.


Sovraccarico del buffer

Sposti più cose in casa di quante ne puoi contenere, riversandole nella casa o nel cortile dei vicini.Quando più tardi il proprietario della casa vicina tornerà a casa, troverà ogni genere di cose che considererà sue.

Questo è il motivo per cui ho scelto un array a dimensione fissa.Per impostare il palcoscenico, supponiamo che la seconda casa che assegniamo sarà, per qualche motivo, di essere collocata prima della prima in memoria.In altre parole, la seconda casa avrà un indirizzo inferiore rispetto al primo.Inoltre, sono assegnati uno accanto all'altro.

Pertanto, questo codice:

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := THouse.Create('My other house somewhere');
                         ^-----------------------^
                          longer than 10 characters
                         0123456789 <-- 10 characters

Disposizione della memoria dopo la prima allocazione:

                        h1
                        v
-----------------------[ttttNNNNNNNNNN]
                        5678My house

Layout della memoria dopo la seconda allocazione:

    h2                  h1
    v                   v
---[ttttNNNNNNNNNN]----[ttttNNNNNNNNNN]
    1234My other house somewhereouse
                        ^---+--^
                            |
                            +- overwritten

La parte che causerà più spesso un arresto anomalo è quando sovrascrivi parti importanti dei dati che hai archiviato che non dovrebbero essere cambiati in modo casuale.Ad esempio, potrebbe non essere un problema che alcune parti del nome della casa H1 siano state modificate, in termini di arresto del programma, ma sovrascrivere il sovraccarico dell'oggetto si crogiola molto probabilmente quando si tenta di utilizzare l'oggetto rotto, così come lo farà Sovrascrivere i collegamenti archiviati ad altri oggetti nell'oggetto.


Elenchi collegati

Quando segui un indirizzo su un pezzo di carta, arrivi a una casa, e in quella casa c'è un altro pezzo di carta con sopra un nuovo indirizzo, per la casa successiva nella catena, e così via.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;

Qui creiamo un collegamento dalla nostra casa alla nostra cabina.Possiamo seguire la catena finché una casa non ha n NextHouse riferimento, il che significa che è l'ultimo.Per visitare tutte le nostre case potremmo utilizzare il seguente codice:

var
    h1, h2: THouse;
    h: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;
    ...
    h := h1;
    while h <> nil do
    begin
        h.LockAllDoors;
        h.CloseAllWindows;
        h := h.NextHouse;
    end;

Layout di memoria (aggiunto Nexthouse come collegamento nell'oggetto, annotato con i quattro LLLL nel diagramma seguente):

    h1                      h2
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home       +        5678Cabin      +
                   |        ^              |
                   +--------+              * (no link)

In termini semplici, cos'è un indirizzo di memoria?

Un indirizzo di memoria è in termini basilari solo un numero.Se pensi alla memoria come a una grande serie di byte, il primo byte ha l'indirizzo 0, quello successivo l'indirizzo 1 e così via verso l'alto.Questo è semplificato, ma abbastanza buono.

Quindi questo layout di memoria:

    h1                 h2
    v                  v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

Potrebbero avere questi due indirizzi (quello più a sinistra è l'indirizzo 0):

  • h1 = 4
  • h2 = 23

Ciò significa che il nostro elenco collegato sopra potrebbe effettivamente assomigliare a questo:

    h1 (=4)                 h2 (=28)
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home      0028      5678Cabin     0000
                   |        ^              |
                   +--------+              * (no link)

È tipico memorizzare un indirizzo che "non punta da nessuna parte" come indirizzo zero.


In termini semplici, cos'è un puntatore?

Un puntatore è semplicemente una variabile che contiene un indirizzo di memoria.In genere puoi chiedere al linguaggio di programmazione di darti il ​​suo numero, ma la maggior parte dei linguaggi di programmazione e della corsa tentano di nascondere il fatto che esiste un numero sotto, solo perché il numero stesso non ha alcun significato per te.È meglio pensare a un puntatore come a una scatola nera, ad es.Non sai davvero o ti interessa come è effettivamente implementato, finché funziona.

Altri suggerimenti

Nella mia prima lezione di Comp Sci, abbiamo svolto il seguente esercizio.Certo, quella era un'aula magna con circa 200 studenti...

Il professore scrive alla lavagna: int john;

Giovanni si alza

Il professore scrive: int *sally = &john;

Sally si alza e indica John

Professore: int *bill = sally;

Bill si alza e indica John

Professore: int sam;

Sam si alza

Professore: bill = &sam;

Bill ora indica Sam.

Penso che tu abbia capito.Penso che abbiamo impiegato circa un'ora a farlo, finché non abbiamo esaminato le basi dell'assegnazione dei puntatori.

Un'analogia che ho trovato utile per spiegare i puntatori è quella dei collegamenti ipertestuali.La maggior parte delle persone può capire che un collegamento su una pagina Web "punta" a un'altra pagina su Internet e, se è possibile copiare e incollare quel collegamento ipertestuale, entrambi punteranno alla stessa pagina Web originale.Se vai e modifichi la pagina originale, quindi segui uno di questi collegamenti (puntatori) otterrai la nuova pagina aggiornata.

Il motivo per cui i puntatori sembrano confondere così tante persone è che nella maggior parte dei casi hanno poca o nessuna conoscenza dell'architettura dei computer.Poiché molti non sembrano avere un'idea di come siano effettivamente implementati i computer (la macchina), lavorare in C/C++ sembra estraneo.

Un esercizio consiste nel chiedere loro di implementare una semplice macchina virtuale basata su bytecode (in qualsiasi linguaggio scelgano, Python funziona benissimo per questo) con un set di istruzioni focalizzato sulle operazioni del puntatore (caricamento, archiviazione, indirizzamento diretto/indiretto).Quindi chiedi loro di scrivere semplici programmi per quel set di istruzioni.

Tutto ciò che richiede qualcosa di più di una semplice aggiunta coinvolgerà i puntatori e sicuramente lo otterranno.

Perché i puntatori sono un fattore di confusione così importante per molti studenti nuovi e anche vecchi di livello universitario nel linguaggio C/C++?

Il concetto di segnaposto per un valore - variabili - si associa a qualcosa che ci viene insegnato a scuola: l'algebra.Non esiste un parallelo esistente che puoi tracciare senza capire come la memoria è fisicamente disposta all'interno di un computer, e nessuno pensa a questo genere di cose finché non ha a che fare con cose di basso livello - a livello di comunicazione C/C++/byte .

Esistono strumenti o processi mentali che ti hanno aiutato a capire come funzionano i puntatori a livello di variabile, funzione e oltre?

Caselle di indirizzi.Ricordo che quando stavo imparando a programmare il BASIC nei microcomputer, c'erano questi graziosi libri con giochi al loro interno, e talvolta dovevi inserire valori in indirizzi particolari.Avevano l'immagine di un mucchio di scatole, etichettate in modo incrementale con 0, 1, 2...ed è stato spiegato che solo una piccola cosa (un byte) poteva stare in queste scatole, e ce n'erano molte: alcuni computer ne avevano fino a 65535!Erano uno accanto all'altro e avevano tutti un indirizzo.

Quali sono alcune buone pratiche che si possono fare per portare qualcuno al livello di "Ah-hah, ho capito", senza impantanarlo nel concetto generale?Fondamentalmente, esegui scenari simili.

Per un trapano?Crea una struttura:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

Stesso esempio di sopra, tranne che in C:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

Produzione:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

Forse questo spiega alcune delle nozioni di base attraverso l'esempio?

Il motivo per cui all'inizio ho avuto difficoltà a comprendere i puntatori è che molte spiegazioni includono un sacco di stronzate sul passaggio per riferimento.Tutto ciò non fa altro che confondere la questione.Quando usi un parametro puntatore, sei Ancora passando per valore;ma il valore sembra essere un indirizzo anziché, ad esempio, un int.

Qualcun altro ha già linkato questo tutorial, ma posso evidenziare il momento in cui ho iniziato a capire i puntatori:

Un tutorial su puntatori e array in C:Capitolo 3 - Puntatori e stringhe

int puts(const char *s);

Per il momento, ignora il const. Il parametro passato a puts() è un puntatore, questo è il valore di un puntatore (poiché tutti i parametri in C vengono passati per valore), e il valore di un puntatore è l'indirizzo a cui punta o, semplicemente, un indirizzo. Così quando scriviamo puts(strA); come abbiamo visto stiamo passando l'indirizzo di strA[0].

Nel momento in cui ho letto queste parole, le nuvole si sono aperte e un raggio di sole mi ha avvolto con una comprensione indicativa.

Anche se sei uno sviluppatore VB .NET o C# (come me) e non usi mai codice non sicuro, vale comunque la pena capire come funzionano i puntatori, altrimenti non capirai come funzionano i riferimenti agli oggetti.Allora avrai l'idea comune ma sbagliata che passare un riferimento a un oggetto a un metodo copia l'oggetto.

Ho trovato il "Tutorial on Pointers and Arrays in C" di Ted Jensen un'eccellente risorsa per conoscere i puntatori.È diviso in 10 lezioni, che iniziano con la spiegazione di cosa sono i puntatori (e a cosa servono) e finiscono con i puntatori a funzioni. http://home.netcom.com/~tjensen/ptr/cpoint.htm

Proseguendo da lì, la Guida alla programmazione di rete di Beej insegna l'API dei socket Unix, da cui puoi iniziare a fare cose davvero divertenti. http://beej.us/guide/bgnet/

La complessità dei puntatori va oltre ciò che possiamo facilmente insegnare.Fare in modo che gli studenti si indichino a vicenda e utilizzare pezzi di carta con gli indirizzi delle case sono entrambi ottimi strumenti di apprendimento.Fanno un ottimo lavoro nell'introdurre i concetti di base.In effetti, imparare i concetti di base lo è vitale per utilizzare con successo i puntatori.Tuttavia, nel codice di produzione, è comune entrare in scenari molto più complessi di quelli che queste semplici dimostrazioni possono incapsulare.

Sono stato coinvolto in sistemi in cui avevamo strutture che puntavano ad altre strutture che puntavano ad altre strutture.Alcune di queste strutture contenevano anche strutture incorporate (invece che puntatori a strutture aggiuntive).È qui che i puntatori diventano davvero confusi.Se hai più livelli di riferimento indiretto e inizi a finire con un codice come questo:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

può creare confusione molto rapidamente (immagina molte più linee e potenzialmente più livelli).Aggiungi matrici di puntatori e puntatori da nodo a nodo (alberi, elenchi collegati) e peggiora ancora.Ho visto alcuni sviluppatori davvero bravi perdersi una volta che hanno iniziato a lavorare su tali sistemi, anche sviluppatori che capivano molto bene le basi.

Anche le strutture complesse dei puntatori non indicano necessariamente una codifica scadente (anche se possono farlo).La composizione è un elemento vitale di una buona programmazione orientata agli oggetti e, nei linguaggi con puntatori grezzi, porterà inevitabilmente a un'indirizzamento indiretto a più livelli.Inoltre, i sistemi spesso necessitano di utilizzare librerie di terze parti con strutture che non corrispondono tra loro in termini di stile o tecnica.In situazioni del genere, la complessità emergerà naturalmente (anche se certamente dovremmo combatterla il più possibile).

Penso che la cosa migliore che le università possano fare per aiutare gli studenti ad apprendere i puntatori sia utilizzare buone dimostrazioni, combinate con progetti che richiedono l'uso dei puntatori.Un progetto difficile farà di più per la comprensione del puntatore di mille dimostrazioni.Le dimostrazioni possono darti una comprensione superficiale, ma per cogliere a fondo i suggerimenti, devi usarli davvero.

Ho pensato di aggiungere un'analogia a questo elenco che ho trovato molto utile quando spiegavo i puntatori (in passato) come tutor di informatica;per prima cosa, diciamo:


Allestire il palco:

Considera un parcheggio con 3 posti auto, questi posti sono numerati:

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

In un certo senso, è come le posizioni della memoria, sono sequenziali e contigue...una specie di array.In questo momento non ci sono auto, quindi è come un array vuoto (parking_lot[3] = {0}).


Aggiungi i dati

Un parcheggio non resta mai vuoto a lungo...se lo facesse sarebbe inutile e nessuno ne costruirebbe nessuno.Quindi diciamo che con il passare della giornata il parcheggio si riempie di 3 auto, un'auto blu, un'auto rossa e un'auto verde:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

Queste auto sono tutte dello stesso tipo (auto), quindi un modo di pensarci è che le nostre auto siano una sorta di dati (diciamo un int) ma hanno valori diversi (blue, red, green;potrebbe essere un colore enum)


Immettere il puntatore

Ora, se ti porto in questo parcheggio e ti chiedo di trovarmi un'auto blu, allunga un dito e usalo per indicare un'auto blu nel punto 1.È come prendere un puntatore e assegnarlo a un indirizzo di memoria (int *finger = parking_lot)

Il tuo dito (il puntatore) non è la risposta alla mia domanda.Guardare A il tuo dito non mi dice niente, ma se guardo dov'è il tuo dito puntando a (dereferenziando il puntatore), riesco a trovare l'auto (i dati) che stavo cercando.


Riassegnare il puntatore

Ora posso chiederti di trovare invece un'auto rossa e puoi reindirizzare il dito su una nuova auto.Ora il tuo puntatore (lo stesso di prima) mi mostra nuovi dati (il parcheggio dove si trova l'auto rossa) dello stesso tipo (l'auto).

Il puntatore non è cambiato fisicamente, è fermo tuo finger, sono cambiati solo i dati che mi mostrava.(l'indirizzo del "parcheggio")


Doppi puntatori (o un puntatore a un puntatore)

Funziona anche con più di un puntatore.Posso chiedere dov'è il puntatore, che punta alla macchina rossa e tu puoi usare l'altra mano e puntare con un dito verso l'indice.(questo è come int **finger_two = &finger)

Ora, se voglio sapere dov'è l'auto blu, posso seguire la direzione del primo dito fino al secondo dito, verso l'auto (i dati).


Il puntatore penzolante

Ora diciamo che ti senti proprio come una statua e vuoi tenere la mano puntata verso l'auto rossa per un tempo indefinito.E se quell'auto rossa se ne andasse?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

Il tuo puntatore sta ancora puntando verso la macchina rossa era ma non lo è più.Diciamo che arriva una macchina nuova...un'auto arancione.Ora, se ti chiedo di nuovo: "dov'è la macchina rossa", indichi ancora lì, ma ora ti sbagli.Quella non è un'auto rossa, è arancione.


Aritmetica dei puntatori

Ok, quindi stai ancora indicando il secondo parcheggio (ora occupato dall'auto Arancione)

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

Bene, ora ho una nuova domanda...Voglio sapere il colore dell'auto nel Prossimo parcheggio.Puoi vedere che stai puntando al punto 2, quindi aggiungi semplicemente 1 e indichi il punto successivo.(finger+1), ora poiché volevo sapere quali dati c'erano, devi controllare quel punto (non solo il dito) in modo da poter deferire il puntatore (*(finger+1)) per vedere che lì è presente un'auto verde (i dati in quella posizione)

Non penso che i puntatori come concetto siano particolarmente complicati: i modelli mentali della maggior parte degli studenti si associano a qualcosa di simile e alcuni schizzi rapidi possono aiutare.

La difficoltà, almeno quella che ho sperimentato in passato e che ho visto affrontare da altri, è che la gestione dei puntatori in C/C++ può essere inutilmente contorta.

Un esempio di tutorial con una buona serie di diagrammi aiuta molto nella comprensione dei puntatori.

Joel Spolsky nel suo articolo spiega alcuni punti utili sulla comprensione dei suggerimenti Guida alla guerriglia per le interviste articolo:

Per qualche ragione sembra che la maggior parte delle persone nasca senza la parte del cervello che comprende i puntatori.Questa è una questione di attitudine, non di abilità: richiede una forma complessa di pensiero doppiamente indiretto che alcune persone semplicemente non riescono a fare.

Penso che l’ostacolo principale alla comprensione delle indicazioni siano i cattivi insegnanti.

A quasi tutti vengono insegnate bugie sui puntatori:Quello lo sono nient'altro che indirizzi di memoria, o che ti permettono di puntare posizioni arbitrarie.

E ovviamente sono difficili da comprendere, pericolosi e semi-magici.

Niente di tutto ciò è vero.I puntatori sono in realtà concetti abbastanza semplici, purché ti attieni a ciò che il linguaggio C++ ha da dire su di essi e non impregnarli di attributi che "di solito" risultano funzionare nella pratica, ma che tuttavia non sono garantiti dal linguaggio, e quindi non fanno parte del concetto stesso di puntatore.

Ho provato a scrivere una spiegazione di questo qualche mese fa in questo post del blog - spero che possa aiutare qualcuno.

(Nota, prima che qualcuno diventi pedante con me, sì, lo standard C++ dice che i puntatori rappresentare indirizzi di memoria.Ma non dice che "i puntatori sono indirizzi di memoria e nient'altro che indirizzi di memoria e possono essere usati o pensati in modo intercambiabile con indirizzi di memoria".La distinzione è importante)

Il problema con i puntatori non è il concetto.È l'esecuzione e il linguaggio coinvolti.Ulteriore confusione risulta quando gli insegnanti presumono che sia il CONCETTO dei puntatori ad essere difficile, e non il gergo, o il pasticcio contorto che C e C++ creano del concetto.Sono stati sprecati così tanti sforzi per spiegare il concetto (come nella risposta accettata a questa domanda) ed è praticamente sprecato per qualcuno come me, perché capisco già tutto questo.Sta solo spiegando la parte sbagliata del problema.

Per darti un'idea delle mie idee, sono una persona che capisce perfettamente i puntatori e posso usarli con competenza nel linguaggio assembler.Perché nel linguaggio assembler non vengono definiti puntatori.Si chiamano indirizzi.Quando si tratta di programmare e usare i puntatori in C, faccio molti errori e mi confondo davvero.Non ho ancora risolto questo problema.Lasciate che vi faccia un esempio.

Quando un'API dice:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

cosa vuole?

potrebbe volere:

un numero che rappresenta un indirizzo in un buffer

(Per dirlo, dico doIt(mybuffer), O doIt(*myBuffer)?)

un numero che rappresenta l'indirizzo di un indirizzo di un buffer

(è questo doIt(&mybuffer) O doIt(mybuffer) O doIt(*mybuffer)?)

un numero che rappresenta l'indirizzo all'indirizzo all'indirizzo al buffer

(forse è così doIt(&mybuffer).o è doIt(&&mybuffer) ?o anche doIt(&&&mybuffer))

e così via, e il linguaggio coinvolto non lo rende così chiaro perché coinvolge le parole "puntatore" e "riferimento" che per me non hanno lo stesso significato e chiarezza di "x contiene l'indirizzo di y" e " questa funzione richiede un indirizzo per y".La risposta dipende inoltre da cosa diavolo è "mybuffer" e cosa intende fare con esso.Il linguaggio non supporta i livelli di nidificazione che si riscontrano nella pratica.Come quando devo consegnare un "puntatore" a una funzione che crea un nuovo buffer e modifica il puntatore in modo che punti alla nuova posizione del buffer.Vuole davvero il puntatore o un puntatore al puntatore, quindi sa dove andare per modificare il contenuto del puntatore.La maggior parte delle volte devo solo indovinare cosa si intende per "puntatore" e la maggior parte delle volte mi sbaglio, indipendentemente da quanta esperienza ho nell'indovinare.

"Pointer" è semplicemente troppo sovraccarico.Un puntatore è un indirizzo a un valore?oppure è una variabile che contiene un indirizzo per un valore.Quando una funzione vuole un puntatore, vuole l'indirizzo che contiene la variabile puntatore o vuole l'indirizzo della variabile puntatore?Non ho capito bene.

Penso che ciò che rende i puntatori difficili da imparare è che fino ai puntatori ti senti a tuo agio con l'idea che "in questa posizione di memoria c'è un insieme di bit che rappresentano un int, un double, un carattere, qualunque cosa".

Quando vedi per la prima volta un puntatore, non capisci veramente cosa c'è in quella posizione di memoria."Cosa vuoi dire con "ha un indirizzo?"

Non sono d'accordo con il concetto "o li ottieni o non li ottieni".

Diventano più facili da comprendere quando inizi a trovarne usi reali (come non trasformare grandi strutture in funzioni).

Il motivo per cui è così difficile da capire non è perché sia ​​un concetto difficile, ma perché la sintassi è incoerente.

   int *mypointer;

Si apprende innanzitutto che la parte più a sinistra della creazione di una variabile definisce il tipo della variabile.La dichiarazione del puntatore non funziona in questo modo in C e C++.Invece dicono che la variabile punta sul tipo a sinistra.In questo caso: *mypointer sta indicando su un int.

Non ho compreso appieno i puntatori finché non ho provato a usarli in C# (con unsafe), funzionano esattamente allo stesso modo ma con una sintassi logica e coerente.Il puntatore è un tipo esso stesso.Qui mypointer È un puntatore a un int.

  int* mypointer;

Non farmi nemmeno iniziare con i puntatori a funzione...

Potevo lavorare con i puntatori quando conoscevo solo il C++.In un certo senso sapevo cosa fare in alcuni casi e cosa non fare per tentativi/errori.Ma la cosa che mi ha dato una comprensione completa è il linguaggio assembly.Se esegui un serio debug a livello di istruzioni con un programma in linguaggio assembly che hai scritto, dovresti essere in grado di capire molte cose.

Mi piace l'analogia con l'indirizzo di casa, ma ho sempre pensato che l'indirizzo fosse quello della cassetta della posta stessa.In questo modo è possibile visualizzare il concetto di dereferenziazione del puntatore (apertura della casella di posta).

Ad esempio seguendo un elenco collegato:1) Inizia con il tuo documento con l'indirizzo 2) Vai all'indirizzo sulla carta 3) Apri la cassetta postale per trovare un nuovo pezzo di carta con l'indirizzo successivo su di esso

In un elenco collegato lineare, l'ultima casella di posta non contiene nulla (fine dell'elenco).In un elenco collegato circolare, l'ultima casella di posta contiene l'indirizzo della prima casella di posta.

Tieni presente che il passaggio 3 è il punto in cui si verifica il dereferenziamento e il punto in cui si verificherà un arresto anomalo o sbaglierai se l'indirizzo non è valido.Supponendo che tu possa avvicinarti alla casella di posta di un indirizzo non valido, immagina che ci sia un buco nero o qualcosa lì dentro che capovolge il mondo :)

Penso che il motivo principale per cui le persone hanno problemi con esso sia perché generalmente non viene insegnato in modo interessante e coinvolgente.Mi piacerebbe vedere un docente prendere 10 volontari dalla folla e dare loro un righello di 1 metro ciascuno, farli stare in piedi in una certa configurazione e usare i righelli per indicarsi l'un l'altro.Quindi mostra l'aritmetica del puntatore spostando le persone (e dove puntano i loro righelli).Sarebbe un modo semplice ma efficace (e soprattutto memorabile) di mostrare i concetti senza impantanarsi troppo nella meccanica.

Una volta arrivati ​​a C e C++ sembra che per alcune persone diventi più difficile.Non sono sicuro che ciò sia dovuto al fatto che stanno finalmente mettendo in pratica la teoria che non comprendono adeguatamente o perché la manipolazione del puntatore è intrinsecamente più difficile in quei linguaggi.Non riesco a ricordare così bene la mia transizione, ma io sapevo puntatori in Pascal e poi spostati in C e si sono persi completamente.

Non penso che gli indicatori stessi siano fonte di confusione.La maggior parte delle persone può comprendere il concetto.Ora a quanti suggerimenti puoi pensare o con quanti livelli di indiretto ti senti a tuo agio.Non ce ne vogliono troppi per portare le persone al limite.Il fatto che possano essere modificati accidentalmente da bug nel tuo programma può anche renderne molto difficile il debug quando le cose vanno male nel tuo codice.

Penso che potrebbe effettivamente essere un problema di sintassi.La sintassi C/C++ per i puntatori sembra incoerente e più complessa di quanto dovrebbe essere.

Ironicamente, ciò che mi ha aiutato a capire i puntatori è stato l'incontro con il concetto di iteratore in C++ Libreria di modelli standard.È ironico perché posso solo supporre che gli iteratori siano stati concepiti come una generalizzazione del puntatore.

A volte non riesci a vedere la foresta finché non impari a ignorare gli alberi.

La confusione deriva dai molteplici livelli di astrazione mescolati insieme nel concetto di "puntatore".I programmatori non vengono confusi dai riferimenti ordinari in Java/Python, ma i puntatori sono diversi in quanto espongono le caratteristiche dell'architettura di memoria sottostante.

È un buon principio separare in modo netto gli strati di astrazione e i puntatori non lo fanno.

Il modo in cui mi piaceva spiegarlo era in termini di array e indici: le persone potrebbero non avere familiarità con i puntatori, ma generalmente sanno cos'è un indice.

Quindi dico di immaginare che la RAM sia un array (e hai solo 10 byte di RAM):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

Quindi un puntatore a una variabile è in realtà solo l'indice (del primo byte) di quella variabile nella RAM.

Quindi se hai un puntatore/index unsigned char index = 2, allora il valore è ovviamente il terzo elemento, ovvero il numero 4.Un puntatore a un puntatore è dove prendi quel numero e lo usi come indice stesso, ad esempio RAM[RAM[index]].

Vorrei disegnare un array su un elenco di fogli e usarlo semplicemente per mostrare cose come molti puntatori che puntano alla stessa memoria, aritmetica dei puntatori, puntatore a puntatore e così via.

Numero della casella postale.

È un'informazione che ti consente di accedere a qualcos'altro.

(E se fai i calcoli sui numeri delle caselle postali, potresti avere un problema, perché la lettera finisce nella casella sbagliata.E se qualcuno si trasferisce in un altro stato, senza indirizzo di inoltro, allora hai un puntatore penzolante.D'altra parte, se l'ufficio postale inoltra la posta, allora hai un puntatore a un puntatore.)

Non è un brutto modo per coglierlo, tramite iteratori..ma continua a guardare e vedrai Alexandrescu iniziare a lamentarsi di loro.

Molti ex sviluppatori di C++ (che non hanno mai capito che gli iteratori sono un puntatore moderno prima di abbandonare il linguaggio) passano a C# e credono ancora di avere iteratori decenti.

Hmm, il problema è che tutto ciò che sono gli iteratori è completamente in contrasto con ciò che le piattaforme runtime (Java/CLR) stanno cercando di ottenere:utilizzo nuovo, semplice, adatto a tutti.Il che può andare bene, ma l'hanno detto una volta nel libro viola e l'hanno detto anche prima e prima di C:

Indiretto.

Un concetto molto potente ma mai così se lo fai fino in fondo..Gli iteratori sono utili in quanto aiutano con l'astrazione degli algoritmi, un altro esempio.E il tempo di compilazione è il luogo per un algoritmo, molto semplice.Conosci codice + dati o in quell'altra lingua C#:

IEnumerable + LINQ + Massive Framework = 300 MB di penalità di runtime indiretta per pessime app che trascinano tramite cumuli di istanze di tipi di riferimento.

"Le Pointer è economico."

Alcune risposte sopra hanno affermato che "i puntatori non sono davvero difficili", ma non hanno continuato a risolvere direttamente dove "il puntatore è difficile!" viene da.Alcuni anni fa ho fatto da tutor agli studenti di informatica del primo anno (solo per un anno, dato che chiaramente facevo schifo) e mi era chiaro che il idea del puntatore non è difficile.Ciò che è difficile è capire perché e quando vorresti un suggerimento.

Non penso che si possa separare questa domanda - perché e quando usare un puntatore - dalla spiegazione di problemi più ampi di ingegneria del software.Perché ogni variabile dovrebbe non essere una variabile globale e perché si dovrebbe fattorizzare codice simile in funzioni (che, prendi questo, usa puntatori per specializzare il proprio comportamento al luogo di chiamata).

Non vedo cosa ci sia di così confuso nei puntatori.Indicano una posizione nella memoria, ovvero memorizza l'indirizzo di memoria.In C/C++ puoi specificare il tipo a cui punta il puntatore.Per esempio:

int* my_int_pointer;

Dice che my_int_pointer contiene l'indirizzo di una posizione che contiene un int.

Il problema con i puntatori è che puntano a una posizione nella memoria, quindi è facile finire in una posizione in cui non dovresti trovarti.Come prova, si considerino le numerose falle di sicurezza nelle applicazioni C/C++ dovute all'overflow del buffer (incremento del puntatore oltre il limite allocato).

Giusto per confondere ancora un po' le cose, a volte devi lavorare con le maniglie invece che con i puntatori.Gli handle sono puntatori a puntatori, in modo che il back-end possa spostare elementi in memoria per deframmentare l'heap.Se il puntatore cambia nel corso della routine, i risultati sono imprevedibili, quindi devi prima bloccare la maniglia per assicurarti che nulla vada da nessuna parte.

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 ne parla in modo un po’ più coerente di me.:-)

Ogni principiante di C/C++ ha lo stesso problema e quel problema si verifica non perché "i puntatori sono difficili da imparare" ma "chi e come viene spiegato".Alcuni studenti lo raccolgono verbalmente, altri visivamente e il modo migliore per spiegarlo è usarlo Esempio "treno". (adatto per esempi verbali e visivi).

Dove "locomotiva" è un puntatore che non può tenere qualsiasi cosa e "carro" è ciò che la "locomotiva" cerca di tirare (o puntare).Successivamente, puoi classificare il "carro" stesso, può contenere animali, piante o persone (o un mix di essi).

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top