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


449

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

Quali sono alcune cose di buone pratiche che possono essere fatte per portare qualcuno al livello di "Ah-ah, l'ho capito" senza farli impantanare nel concetto generale? Fondamentalmente, esegui il drill-like degli scenari.


20
La tesi di questa domanda è che i puntatori sono difficili da capire. La domanda non offre prove del fatto che i puntatori siano più difficili da comprendere di qualsiasi altra cosa.
bmargulies,

14
Forse mi manca qualcosa (perché codice nei linguaggi GCC'd) ma ho sempre pensato se i puntatori in memoria fossero una struttura chiave-> valore. Poiché è costoso trasferire grandi quantità di dati in un programma, si crea la struttura (valore) e si passa al puntatore / riferimento (chiave) perché la chiave è una rappresentazione molto più piccola della struttura più grande. La parte difficile è quando devi confrontare due puntatori / riferimenti (stai confrontando le chiavi oi valori) che richiedono più lavoro per dividere i dati contenuti nella struttura (valore).
Evan Plaice,

2
@ Wolfpack'08 "Mi sembra che un ricordo in indirizzo sarà sempre un int." - Allora ti dovrebbe sembrare che nulla abbia un tipo, dato che sono tutti solo bit in memoria. "In realtà, il tipo di puntatore è il tipo di var a cui punta il puntatore" - No, il tipo di puntatore è puntatore al tipo di var a cui punta il puntatore - che è naturale e dovrebbe essere ovvio.
Jim Balter,

2
Mi sono sempre chiesto cosa c'è di così difficile da capire nel fatto che le variabili (e le funzioni) sono solo blocchi di memoria e i puntatori sono variabili che memorizzano gli indirizzi di memoria. Questo modello di pensiero forse troppo pratico potrebbe non impressionare tutti i fan dei concetti astratti, ma aiuta perfettamente a capire come funzionano i puntatori.
Christian Rau,

8
In poche parole, gli studenti probabilmente non capiscono perché non corretta comprensione, o affatto, come la memoria di un computer, in generale, e in particolare il "modello di memoria" C opere. Questo libro Programmazione da zero offre un'ottima lezione su questi argomenti.
Abbafei,

Risposte:


745

Puntatori è un concetto che all'inizio per molti può essere fonte di confusione, in particolare quando si tratta di copiare i valori dei puntatori e fare ancora riferimento allo stesso blocco di memoria.

Ho scoperto che la migliore analogia è considerare il puntatore come un pezzo di carta con un indirizzo di casa su di esso e il blocco di memoria a cui fa riferimento come casa reale. È quindi possibile spiegare facilmente ogni tipo di operazione.

Ho aggiunto un po 'di codice Delphi in basso e alcuni commenti, se del caso. Ho scelto Delphi poiché il mio altro linguaggio di programmazione principale, C #, non mostra cose come le perdite di memoria allo stesso modo.

Se desideri solo imparare il concetto di alto livello di puntatori, allora dovresti ignorare le parti etichettate "Layout di memoria" nella spiegazione sotto. Hanno lo scopo di fornire esempi di come potrebbe apparire la memoria dopo le operazioni, ma sono di natura più di basso livello. Tuttavia, al fine di spiegare con precisione come funzionano davvero i sovraccarichi del buffer, è stato importante aggiungere questi diagrammi.

Dichiarazione di non responsabilità: 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 hai bisogno di gestire la memoria a basso livello. Tuttavia, allo scopo di spiegare la memoria e i puntatori, è abbastanza accurato.


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

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

Quando si inizializza l'oggetto house, il nome assegnato al costruttore viene copiato nel campo privato FName. C'è un motivo per cui è definito come un array di dimensioni fisse.

In memoria, ci sarà un certo overhead associato con l'allocazione della casa, lo illustrerò di seguito in questo modo:

--- [ttttNNNNNNNNNN] ---
     ^ ^
     | |
     | + - l'array FName
     |
     + - spese generali

L'area "tttt" è sovraccarica, in genere ce ne sarà di più per vari tipi di runtime e lingue, come 8 o 12 byte. È indispensabile che i valori memorizzati in quest'area non vengano mai modificati da qualcosa di diverso dall'allocatore di memoria o dalle routine del sistema principale, oppure si rischia di arrestare il programma in modo anomalo.


Allocare memoria

Ottieni un imprenditore per costruire la tua casa e darti l'indirizzo della casa. Contrariamente al mondo reale, non è possibile dire dove allocare l'allocazione di memoria, ma troverà un posto adatto con spazio sufficiente e riporterà l'indirizzo alla memoria allocata.

In altre parole, l'imprenditore sceglierà il posto.

THouse.Create('My house');

Layout di memoria:

--- [ttttNNNNNNNNNN] ---
    1234 La mia casa

Mantieni una variabile con l'indirizzo

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

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

Layout di memoria:

    h
    v
--- [ttttNNNNNNNNNN] ---
    1234 La mia casa

Copia il valore del puntatore

Scrivi l'indirizzo su un nuovo pezzo di carta. Ora hai due pezzi di carta che ti porteranno nella stessa casa, non due case separate. Qualsiasi tentativo di seguire l'indirizzo da una carta e riorganizzare i mobili di quella casa sembrerà che l'altra casa sia stata modificata nello stesso modo, a meno che tu non riesca a rilevare esplicitamente che in realtà è solo una 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] ---
    1234 La mia casa
    ^
    h2

Liberare la memoria

Demolire la casa. Successivamente puoi riutilizzare la carta per un nuovo indirizzo, se lo desideri, o 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 prima costruisco la casa e ottengo il suo indirizzo. Quindi faccio qualcosa per la casa (lo uso, il ... codice, lasciato come esercizio per il lettore), e poi lo libero. Infine, desidero cancellare l'indirizzo dalla mia variabile.

Layout di memoria:

    h <- +
    v + - prima di libero
--- [ttttNNNNNNNNNN] --- |
    1234 La mia casa <- +

    h (ora non punta da nessuna parte) <- +
                                + - dopo libero
---------------------- | (nota, la memoria potrebbe ancora
    xx34La mia casa <- + contiene alcuni dati)

Puntatori ciondolanti

Dici al tuo imprenditore di distruggere la casa, ma ti dimentichi di cancellare l'indirizzo dal tuo pezzo di carta. Quando in seguito guardi il pezzo di carta, hai dimenticato che la casa non è più lì e va a visitarla, con risultati falliti (vedi anche la parte relativa a un riferimento non valido di seguito).

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

L'uso hdopo la chiamata .Free potrebbe funzionare, ma è solo pura fortuna. Molto probabilmente fallirà, nel luogo di un cliente, nel mezzo di un'operazione critica.

    h <- +
    v + - prima di libero
--- [ttttNNNNNNNNNN] --- |
    1234 La mia casa <- +

    h <- +
    v + - dopo libero
---------------------- |
    xx34La mia casa <- +

Come puoi vedere, h punta ancora ai resti dei dati in memoria, ma poiché potrebbe non essere completo, usarlo come prima potrebbe non riuscire.


Perdita di memoria

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

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 della hvariabile con l'indirizzo di una nuova casa, ma quella vecchia è ancora in piedi ... da qualche parte. 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, a quel punto il sistema operativo la distruggerà.

Layout di memoria dopo la prima allocazione:

    h
    v
--- [ttttNNNNNNNNNN] ---
    1234 La mia casa

Layout di memoria dopo la seconda allocazione:

                       h
                       v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
    1234 La mia casa 5678 La mia casa

Un modo più comune per ottenere questo metodo è semplicemente dimenticare di liberare qualcosa, invece di sovrascriverlo come sopra. In termini di Delphi, ciò si verificherà 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, non c'è posto nelle nostre variabili che l'indirizzo alla casa esiste, ma la casa è ancora là fuori.

Layout di memoria:

    h <- +
    v + - prima di perdere il puntatore
--- [ttttNNNNNNNNNN] --- |
    1234 La mia casa <- +

    h (ora non punta da nessuna parte) <- +
                                + - dopo aver perso il puntatore
--- [ttttNNNNNNNNNN] --- |
    1234 La mia casa <- +

Come puoi vedere, i vecchi dati vengono lasciati intatti in memoria e non verranno riutilizzati dall'allocatore di memoria. L'allocatore tiene traccia di quali aree della memoria sono state utilizzate e non le riutilizzerà se non lo si libera.


Liberare la memoria ma mantenendo 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 ricorda le rovine di uno.

Forse troverai persino una casa, ma non è la casa a cui ti è stato originariamente assegnato l'indirizzo, e quindi ogni tentativo di usarlo come se ti appartenesse potrebbe fallire in modo orribile.

A volte potresti persino scoprire che un indirizzo vicino ha una casa piuttosto grande allestita su di esso che occupa tre indirizzi (Main Street 1-3) e il tuo indirizzo va al centro della casa. Qualsiasi tentativo di trattare quella parte della grande casa a 3 indirizzi come una singola piccola casa potrebbe anche fallire in modo orribile.

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 h1e, anche se è h1stata cancellata, h2ha ancora il vecchio indirizzo, obsoleto. L'accesso alla casa che non è più in piedi potrebbe o potrebbe non funzionare.

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


Buffer sovraccarico

Sposta più cose nella casa di quante possiate inserire, versando nella casa o nel cortile dei vicini. Quando il proprietario di quella casa vicina tornerà a casa, troverà ogni sorta di cose che considererà sue.

Questo è il motivo per cui ho scelto un array di dimensioni fisse. Per preparare il terreno, supponiamo che la seconda casa che assegniamo verrà, per qualche motivo, posizionata prima della prima in memoria. In altre parole, la seconda casa avrà un indirizzo inferiore rispetto alla prima. Inoltre, sono allocati 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

Layout di memoria dopo la prima allocazione:

                        h1
                        v
----------------------- [ttttNNNNNNNNNN]
                        5678 La mia casa

Layout di memoria dopo la seconda allocazione:

    h2 h1
    vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
    1234 La mia altra casa da qualche parte
                        ^ --- + - ^
                            |
                            + - sovrascritto

La parte che più spesso causerà l'arresto anomalo è quando si sovrascrivono parti importanti dei dati archiviati che in realtà non devono essere cambiati casualmente. Ad esempio, potrebbe non essere un problema che parti del nome di h1-house siano state modificate, in termini di crash del programma, ma la sovrascrittura del sovraccarico dell'oggetto molto probabilmente si arresterà in modo anomalo quando si tenta di utilizzare l'oggetto rotto, come sovrascrivendo i collegamenti che sono memorizzati su 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 un nuovo indirizzo su di esso, 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 fino a quando una casa non ha NextHouseriferimenti, il che significa che è l'ultima. Per visitare tutte le nostre case, potremmo usare 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 le quattro LLLL nel diagramma seguente):

    h1 h2
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
    1234Home + 5678Cabin +
                   | ^ |
                   + -------- + * (nessun collegamento)

In termini di base, che cos'è un indirizzo di memoria?

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

Quindi questo layout di memoria:

    h1 h2
    vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
    1234 La mia casa 5678 La mia casa

Potrebbe avere questi due indirizzi (il più a sinistra - è l'indirizzo 0):

  • h1 = 4
  • h2 = 23

Ciò significa che il nostro elenco di link sopra riportato potrebbe apparire così:

    h1 (= 4) h2 (= 28)
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
    1234 Home 0028 5678 Cabina 0000
                   | ^ |
                   + -------- + * (nessun collegamento)

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


In termini di base, cos'è un puntatore?

Un puntatore è solo 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 dei runtime cerca di nascondere il fatto che c'è un numero sotto, solo perché il numero stesso non ha davvero alcun significato per te. È meglio pensare a un puntatore come a una scatola nera, cioè. non sai davvero o ti preoccupi di come è effettivamente implementato, purché funzioni.


59
Il sovraccarico del buffer è divertente. "Il vicino torna a casa, apre il cranio scivolando sulla tua spazzatura e ti fa causa nell'oblio."
gtd

12
Questa è una bella spiegazione del concetto, certo. Il concetto NON è ciò che trovo confuso riguardo ai puntatori, quindi l'intero saggio è stato un po 'sprecato.
Breton,

10
Ma solo per avere chiesto, che cosa si confonde su puntatori?
Lasse V. Karlsen,

11
Ho rivisitato questo post diverse volte da quando hai scritto la risposta. Il tuo chiarimento con il codice è eccellente e ti apprezzo rivederlo per aggiungere / perfezionare più pensieri. Bravo Lasse!
David McGraw,

3
Non è possibile che una singola pagina di testo (indipendentemente da quanto tempo) sia in grado di riassumere ogni sfumatura di gestione della memoria, riferimenti, puntatori, ecc. Dato che 465 persone lo hanno votato, direi che è abbastanza buono pagina iniziale sulle informazioni. C'è altro da imparare? Certo, quando non lo è?
Lasse V. Karlsen,

153

Nella mia prima lezione di Comp Sci, abbiamo fatto il seguente esercizio. Concesso, questa era una sala conferenze con circa 200 studenti ...

Il professore scrive alla lavagna: int john;

John si alza

Il professore scrive: int *sally = &john;

Sally si alza, indica John

Professoressa: int *bill = sally;

Bill si alza, indica John

Professoressa: int sam;

Sam si alza

Professoressa: bill = &sam;

Bill ora indica Sam.

Penso che tu abbia avuto l'idea. Penso che abbiamo trascorso circa un'ora a farlo, fino a quando non abbiamo approfondito le basi dell'assegnazione dei puntatori.


5
Non credo di aver sbagliato. La mia intenzione era di cambiare il valore della variabile puntata da John a Sam. È un po 'più difficile rappresentare con le persone, perché sembra che tu stia cambiando il valore di entrambi i puntatori.
Prova il

24
Ma il motivo per cui è confuso è che non è come se John si alzasse dal suo posto e poi Sam si sedesse, come potremmo immaginare. È più come se Sam si fosse avvicinato e avesse incastrato la sua mano in John e avesse clonato la programmazione di Sam nel corpo di John, come Hugo che si intrecciava in matrice ricaricata.
Breton,

59
Più come Sam prende il posto di John, e John fluttua nella stanza fino a quando non si imbatte in qualcosa di critico e provoca un segfault.
just_wes

2
Personalmente trovo questo esempio inutilmente complicato. Il mio prof mi ha detto di indicare una luce e ha detto "la tua mano è il puntatore all'oggetto luce".
Celeritas,

Il problema con questo tipo di esempi è che il puntatore a X e X non sono gli stessi. E questo non viene rappresentato con le persone.
Isaac Nequittepas,

124

Un'analogia che ho trovato utile per spiegare i puntatori è i 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 faranno riferimento alla stessa pagina Web originale. Se vai e modifichi quella pagina originale, segui uno di quei link (puntatori) otterrai quella nuova pagina aggiornata.


15
Mi piace molto questo. Non è difficile vedere che scrivere due volte un collegamento ipertestuale non fa apparire due siti Web (proprio come int *a = bnon ne fanno due copie *b).
Detly

4
Questo è in realtà molto intuitivo e qualcosa a cui tutti dovrebbero essere in grado di relazionarsi. Sebbene ci siano molti scenari in cui questa analogia cade a pezzi. Ottimo per una rapida introduzione però. +1
Brian Wigginton,

Un collegamento a una pagina che viene aperta due volte di solito crea due istanze quasi completamente indipendenti di quella pagina web. Penso che un collegamento ipertestuale potrebbe piuttosto essere una buona analogia con un costruttore forse, ma non con un puntatore.
Utkan Gezer,

@ThoAppelsin Non necessariamente vero, ad esempio se si accede a una pagina Web HTML statica, si accede a un singolo file sul server.
drammatico

5
Lo stai pensando troppo. I collegamenti ipertestuali puntano a file sul server, questa è l'estensione dell'analogia.
drammatico

48

Il motivo per cui gli indicatori sembrano confondere così tante persone è che per lo più arrivano con poco o nessun background nell'architettura del computer. Dal momento che molti non sembrano avere un'idea di come i computer (la macchina) siano effettivamente implementati - lavorare in C / C ++ sembra alieno.

Un esercizio è quello di chiedere loro di implementare una semplice macchina virtuale basata su bytecode (in qualsiasi lingua abbiano scelto, python funziona benissimo per questo) con un set di istruzioni focalizzato sulle operazioni del puntatore (caricamento, memorizzazione, indirizzamento diretto / indiretto). Quindi chiedi loro di scrivere semplici programmi per quel set di istruzioni.

Tutto ciò che richiede qualcosa di più della semplice aggiunta coinvolgerà i puntatori e sono sicuri di ottenerlo.


2
Interessante. Non ho idea di come iniziare a farlo però. Qualche risorsa da condividere?
Karolis,

1
Sono d'accordo. Ad esempio, ho imparato a programmare in assemblea prima di C e sapendo come funzionano i registri, apprendere i suggerimenti era facile. In effetti, non c'era molto apprendimento, tutto è diventato molto naturale.
Milan Babuškov,

Prendi una CPU di base, dì qualcosa che faccia funzionare rasaerba o lavastoviglie e implementala. O un sottoinsieme molto basilare di ARM o MIPS. Entrambi hanno un ISA molto semplice.
Daniel Goldberg,

1
Potrebbe valere la pena sottolineare che questo approccio educativo è stato sostenuto / praticato dallo stesso Donald Knuth. L'arte della programmazione al computer di Knuth descrive una semplice architettura ipotetica e chiede agli studenti di implementare soluzioni per mettere in pratica i problemi in un ipotetico linguaggio di assemblaggio per quell'architettura. Una volta che è diventato praticamente fattibile, alcuni studenti che leggono i libri di Knuth implementano effettivamente la sua architettura come una macchina virtuale (o usano un'implementazione esistente) ed eseguono effettivamente le loro soluzioni. IMO questo è un ottimo modo per imparare, se hai tempo.
WeirdlyCheezy,

4
@Luke Non penso sia facile capire persone che semplicemente non riescono a cogliere i puntatori (o, per essere più precisi, l'indirizzamento in generale). Fondamentalmente stai assumendo che le persone che non capiscono i puntatori in C sarebbero in grado di iniziare a imparare l'assemblaggio, capire l'architettura sottostante del computer e tornare a C con una comprensione dei puntatori. Questo può essere vero per molti, ma secondo alcuni studi sembra che alcune persone intrinsecamente non riescano a cogliere l'indirizzamento indiretto, anche in linea di principio (lo trovo ancora molto difficile da credere, ma forse sono stato solo fortunato con i miei "studenti ").
Luaan,

27

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

Il concetto di segnaposto per un valore - variabili - è mappato su qualcosa che ci viene insegnato a scuola - l'algebra. Non esiste un parallelo esistente che puoi disegnare senza capire come la memoria è fisicamente disposta all'interno di un computer e nessuno pensa a questo tipo di cose fino a quando non ha a che fare con cose di basso livello - a livello di comunicazione C / C ++ / byte .

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

Caselle indirizzi. Ricordo che quando stavo imparando a programmare BASIC in microcomputer, c'erano questi graziosi libri con giochi in essi, e talvolta dovevi inserire valori in indirizzi particolari. Avevano una foto 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 aveva ben 65535! Erano vicini l'uno all'altro e avevano tutti un indirizzo.

Quali sono alcune cose di buone pratiche che possono essere fatte per portare qualcuno al livello di "Ah-ah, l'ho capito" senza farli impantanare nel concetto generale? Fondamentalmente, esegui il drill-like degli scenari.

Per un trapano? Crea uno strutt:

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 cui sopra, tranne 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 basi attraverso l'esempio?


+1 per "senza capire come la memoria è posta fisicamente". Sono venuto a C da un background di linguaggio assembly e il concetto di puntatori era molto naturale e facile; e ho visto persone con un background linguistico di livello superiore che fatica a capirlo. A peggiorare la situazione, la sintassi è confusa (puntatori di funzione!), Quindi l'apprendimento del concetto e della sintassi allo stesso tempo è una ricetta per i problemi.
Brendan,

4
Conosco questo un vecchio post, ma sarebbe bello se l'output del codice fornito fosse aggiunto al post.
Josh

Sì, è simile all'algebra (sebbene l'algebra abbia un ulteriore punto di comprensibilità nell'avere immutabili le loro "variabili"). Ma circa la metà delle persone che conosco non hanno alcuna conoscenza dell'algebra in pratica. Semplicemente non calcola per loro. Conoscono tutte quelle "equazioni" e prescrizioni per arrivare al risultato, ma le applicano in modo un po 'casuale e goffamente. E non possono estenderli per il loro scopo - è solo una scatola nera immutabile, non sovrapponibile per loro. Se capisci l'algebra e sei in grado di usarlo in modo efficace, sei già molto avanti rispetto al pacchetto, anche tra i programmatori.
Luaan,

24

Il motivo per cui ho avuto difficoltà a comprendere i puntatori, all'inizio, è che molte spiegazioni includono molte cazzate sul passaggio per riferimento. Tutto ciò che fa è confondere il problema. Quando usi un parametro pointer, stai ancora passando per valore; ma il valore sembra essere un indirizzo piuttosto che, diciamo, un int.

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

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

int puts(const char *s);

Per il momento, ignora il const.parametro Passato a puts()è un puntatore, ovvero il valore di un puntatore (poiché tutti i parametri in C sono passati per valore) e il valore di un puntatore è l'indirizzo a cui punta o, semplicemente , un indirizzo. Quindi 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 separate e un raggio di sole mi ha avvolto con la comprensione del puntatore.

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


Mi lascia chiedermi quale sia il punto di avere un puntatore. Nella maggior parte dei blocchi di codice che incontro, un puntatore viene visualizzato solo nella sua dichiarazione.
Wolfpack'08,

@ Wolfpack'08 ... cosa ?? Che codice stai guardando ??
Kyle Strand,

@KyleStrand Fammi dare un'occhiata.
Wolfpack'08,

19

Ho trovato "Tutorial su puntatori e array in C" di Ted Jensen una risorsa eccellente per apprendere i puntatori. È diviso in 10 lezioni, a partire da una spiegazione di cosa sono (e a cosa servono) i puntatori e termina con i puntatori a funzione. http://home.netcom.com/~tjensen/ptr/cpoint.htm

Passando da lì, Beej's Guide to Network Programming insegna l'API dei socket Unix, da cui puoi iniziare a fare cose davvero divertenti. http://beej.us/guide/bgnet/


1
Secondo il tutorial di Ted Jensen. Suddivide i puntatori a un livello di dettaglio, che non è eccessivamente dettagliato, nessun libro che ho letto fa. Estremamente utile! :)
Dave Gallagher,

12

La complessità dei puntatori va oltre ciò che possiamo facilmente insegnare. Fare in modo che gli studenti indichino l'un l'altro e usare pezzi di carta con indirizzi di casa sono entrambi ottimi strumenti di apprendimento. Fanno un ottimo lavoro nell'introdurre i concetti di base. In effetti, apprendere i concetti di base è vitale per usare 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 con sistemi in cui avevamo strutture che puntavano ad altre strutture che puntavano ad altre strutture. Alcune di queste strutture contenevano anche strutture incorporate (piuttosto che puntatori a strutture aggiuntive). Questo è dove 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). Getta matrici di puntatori e puntatori da nodo a nodo (alberi, elenchi collegati) e peggiora ancora. Ho visto alcuni sviluppatori davvero bravi che si sono persi una volta che hanno iniziato a lavorare su tali sistemi, anche gli sviluppatori che hanno capito davvero le basi.

Le strutture complesse di puntatori non indicano necessariamente una codifica scadente (anche se possono). La composizione è un elemento vitale di una buona programmazione orientata agli oggetti e, nei linguaggi con puntatori non elaborati, porterà inevitabilmente a un'indirizzamento multilivello. Inoltre, i sistemi devono spesso utilizzare librerie di terze parti con strutture che non corrispondono l'una all'altra in termini di stile o tecnica. In situazioni del genere, sorgerà naturalmente la complessità (anche se certamente dovremmo combatterla il più possibile).

Penso che la cosa migliore che le università possano fare per aiutare gli studenti a imparare i puntatori è usare buone dimostrazioni, combinate con progetti che richiedono l'uso di puntatori. Un progetto difficile farà di più per la comprensione dei puntatori di mille dimostrazioni. Le dimostrazioni possono darti una comprensione superficiale, ma per afferrare profondamente i puntatori, devi davvero usarli.


10

Ho pensato di aggiungere un'analogia a questo elenco che ho trovato molto utile quando ho spiegato i puntatori (indietro nel tempo) come Tutor di Informatica; per prima cosa:


Imposta il palcoscenico :

Considera un parcheggio con 3 spazi, questi spazi sono numerati:

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

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


Aggiungi i dati

Un parcheggio non rimane mai vuoto a lungo ... se lo facesse sarebbe inutile e nessuno ne costruirebbe nessuno. Diciamo che mentre la giornata si sposta sul lotto si riempie di 3 macchine, una macchina blu, una macchina rossa e una macchina verde:

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

Queste vetture sono tutti dello stesso tipo (auto) così un modo di pensare di questo è che le nostre auto sono una sorta di dati (ad esempio una int) ma hanno valori diversi ( blue, red, green, che potrebbe essere un colore enum)


Inserisci il puntatore

Ora, se ti porto in questo parcheggio e ti chiedo di trovarmi una macchina blu, allunghi un dito e lo usi per indicare una macchina 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. Guardando a un dito mi dice nulla, ma se guardo dove stai dito che punta alla (dereferenziazione il puntatore), posso trovare l'auto (i dati) che stavo cercando.


Riassegnare il puntatore

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

Il puntatore non è cambiato fisicamente, è ancora il tuo dito, sono cambiati solo i dati che mi stava mostrando. (l'indirizzo del "parcheggio")


Puntatori doppi (o un puntatore a un puntatore)

Funziona anche con più di un puntatore. Posso chiederti dove si trova il puntatore, che punta verso la macchina rossa e puoi usare l'altra mano e puntare con un dito sul primo dito. (questo è come int **finger_two = &finger)

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


Il puntatore penzolante

Ora diciamo che ti senti molto simile a una statua e vuoi tenere la tua mano che punta indefinitamente alla macchina rossa. E se quell'auto rossa se ne andasse?

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

Il puntatore punta ancora al punto in cui l'auto rossa era ma non è più. Diciamo che una nuova auto tira lì ... un'auto Orange. Ora, se ti chiedo di nuovo "dov'è la macchina rossa", stai ancora indicando lì, ma ora ti sbagli. Quella non è una macchina rossa, è arancione.


Puntatore aritmetico

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

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

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


Basta non usare la parola "doppio puntatore". I puntatori possono puntare a qualsiasi cosa, quindi ovviamente puoi avere puntatori che puntano ad altri puntatori. Non sono doppi puntatori.
gnasher729,

Penso che questo non valga il fatto che le "dita" stesse, per continuare la tua analogia, ognuna "occupano un posto di parcheggio". Non sono sicuro che le persone abbiano qualche difficoltà a capire i puntatori ad alto livello di astrazione della tua analogia, è capire che i puntatori sono cose mutabili che occupano posizioni di memoria e come questo sia utile, che sembra eludere le persone.
Emmet

1
@Emmet - Non sono in disaccordo sul fatto che ci sia molto di più nei puntatori WRT, ma ho letto la domanda: "without getting them bogged down in the overall concept"come comprensione di alto livello. E al tuo punto: "I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"- sareste molto sorpresi da quante persone non capiscono i puntatori anche a questo livello
Mike

C'è qualche merito ad estendere l'analogia dito-macchina a una persona (con una o più dita - e un'anomalia genetica che può consentire a ciascuno di quelli di puntare in qualsiasi direzione!) Seduto in una delle macchine che punta verso un'altra macchina (o si chinò verso la terra desolata accanto al lotto come un "puntatore non inizializzato", o tutta una mano allargata che puntava verso una fila di spazi come un "array di puntatori a dimensione fissa [5]" o si accartocciò nel palmo "puntatore nullo" che punta a un posto dove si sa che non c'è una macchina) ... 8-)
SlySven

la tua spiegazione è stata apprezzabile e buona per un principiante.
Yatendra Rathore,

9

Non credo che i puntatori come concetto siano particolarmente complicati: la maggior parte dei modelli mentali degli studenti si associano a qualcosa del genere e alcuni schizzi a scatola possono aiutare.

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


9

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

Joel Spolsky fa alcuni buoni punti sulla comprensione dei suggerimenti nella sua Guerrilla Guide to Interviewing article:

Per qualche ragione la maggior parte delle persone sembra essere nata senza la parte del cervello che comprende i puntatori. Questa è una cosa attitudine, non una cosa di abilità - richiede una forma complessa di pensiero doppiamente indiretto che alcune persone semplicemente non possono fare.


8

Il problema con i puntatori non è il concetto. È l'esecuzione e il linguaggio coinvolti. Ulteriori risultati di confusione quando gli insegnanti presumono che sia CONCETTO di puntatori che è difficile, e non il gergo, o il disordine contorto che C e C ++ fanno del concetto. Così grandi sforzi sono spesi per spiegare il concetto (come nella risposta accettata per questa domanda) ed è praticamente sprecato per qualcuno come me, perché ho già capito tutto. Sta solo spiegando la parte sbagliata del problema.

Per darti un'idea di dove vengo, sono qualcuno che capisce perfettamente i puntatori e posso usarli con competenza nel linguaggio assemblatore. Perché nel linguaggio assembler non sono indicati come puntatori. Sono indicati come indirizzi. Quando si tratta di programmare e usare i puntatori in C, faccio molti errori e mi confondo molto. Non l'ho ancora risolto. Lasciate che vi faccia un esempio.

Quando un API dice:

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

cosa vuole?

potrebbe voler:

un numero che rappresenta un indirizzo in un buffer

(Per dirlo doIt(mybuffer), dico io o doIt(*myBuffer)?)

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

(è quello doIt(&mybuffer)o doIt(mybuffer)o doIt(*mybuffer)?)

un numero che rappresenta l'indirizzo per l'indirizzo per il buffer

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

e così via, e il linguaggio in questione non lo rende così chiaro perché comprende le parole "puntatore" e "riferimento" che non hanno così tanto significato e chiarezza per me come "x contiene l'indirizzo per y" e " questa funzione richiede un indirizzo per y ". La risposta dipende anche da cosa diavolo è il "mybuffer" e da cosa intende fare. La lingua non supporta i livelli di annidamento che si incontrano nella pratica. Come quando devo consegnare un "puntatore" a una funzione che crea un nuovo buffer e modifica il puntatore in modo che punti nella nuova posizione del buffer. Vuole davvero il puntatore o un puntatore al puntatore, quindi sa dove andare per modificare il contenuto del puntatore. Il più delle volte devo solo indovinare cosa si intende per "

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


L'ho visto spiegato in questo modo: se vedi una dichiarazione del puntatore simile double *(*(*fn)(int))(char), il risultato della valutazione *(*(*fn)(42))('x')sarà a double. È possibile eliminare i livelli di valutazione per comprendere quali devono essere i tipi intermedi.
Bernd Jendrissek,

@BerndJendrissek Non sono sicuro di seguire. Qual è il risultato della valutazione (*(*fn)(42))('x') allora?
Breton,

ottieni una cosa (chiamiamola x) dove, se valuti *x, ottieni una doppia.
Bernd Jendrissek,

@BerndJendrissek Questo dovrebbe spiegare qualcosa sui puntatori? Non capisco Qual è il tuo punto? Ho rimosso un livello e non ho ottenuto nuove informazioni su alcun tipo intermedio. Cosa spiega su ciò che una particolare funzione accetterà? Cosa c'entra con qualcosa?
Breton,

Forse il messaggio in questa spiegazione (e non è mio, vorrei poter trovare dove l'ho visto per la prima volta) è pensarlo meno in termini di ciò che fn è e più in termini di cosa puoi fare confn
Bernd Jendrissek

8

Penso che il principale ostacolo alla comprensione dei suggerimenti sia costituito dai cattivi insegnanti.

Quasi a tutti vengono insegnate bugie sui puntatori: che non sono altro che indirizzi di memoria o che consentono di puntare a posizioni arbitrarie .

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

Nessuno dei quali è vero. I puntatori sono in realtà concetti abbastanza semplici, a patto di attenersi a ciò che il linguaggio C ++ ha da dire su di essi e non li infondono di attributi che "di solito" risultano funzionare nella pratica, ma tuttavia non sono garantiti dal linguaggio e quindi non fanno parte del concetto reale di puntatore.

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

(Nota, prima che qualcuno diventi pedante con me, sì, lo standard C ++ afferma che i puntatori rappresentano 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 la memoria indirizzi ". La distinzione è importante)


Dopotutto, un puntatore null non punta a zero-address nella memoria, anche se il suo "valore" C è zero. È un concetto completamente separato e, se lo gestisci in modo errato, potresti finire per indirizzare (e dereferenziare) qualcosa che non ti aspettavi. In alcuni casi, potrebbe anche trattarsi di un indirizzo zero nella memoria (specialmente ora che lo spazio degli indirizzi è di solito piatto), ma in altri potrebbe essere omesso come comportamento indefinito da un compilatore di ottimizzazione o accedere ad altre parti della memoria associate con "zero" per il tipo di puntatore specificato. Ne deriva l'ilarità.
Luaan,

Non necessariamente. Devi essere in grado di modellare il computer nella tua testa affinché i puntatori abbiano un senso (e per eseguire il debug anche di altri programmi). Non tutti possono farlo.
Thorbjørn Ravn Andersen,

5

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 è un insieme di bit che rappresentano un int, un doppio, un carattere, qualunque cosa".

Quando vedi per la prima volta un puntatore, non ottieni davvero ciò che si trova in quella posizione di memoria. "Cosa intendi con un indirizzo ?"

Non sono d'accordo con l'idea che "o li capisci o no".

Diventano più facili da capire quando inizi a trovare usi reali per loro (come non passare grandi strutture in funzioni).


5

Il motivo per cui è così difficile da capire non è perché è 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 di 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 punta su un int.

Non ho afferrato completamente i puntatori fino a quando non ho provato a usarli in C # (con non sicuri), funzionano esattamente allo stesso modo ma con sintassi logica e coerente. Il puntatore è un tipo stesso. Qui mypointer è un puntatore a un int.

  int* mypointer;

Non iniziare nemmeno con i puntatori a funzione ...


2
In realtà, entrambi i tuoi frammenti sono validi C. È una questione di molti anni di stile C che il primo è più comune. Il secondo è un po 'più comune in C ++, per esempio.
RBerteig,

1
Il secondo frammento non funziona davvero bene con dichiarazioni più complesse. E la sintassi non è così "incoerente" quando ti rendi conto che la parte destra di una dichiarazione del puntatore ti mostra cosa devi fare al puntatore per ottenere qualcosa il cui tipo è l'identificatore del tipo atomico sulla sinistra.
Bernd Jendrissek,

2
int *p;ha un significato semplice: *pè un numero intero. int *p, **ppsignifica: *pe **ppsono numeri interi.
Miles Rout,

@MilesRout: Ma questo è esattamente il problema. *pe **ppsono non interi, perché non hai mai inizializzato po ppo *ppper punto a nulla. Capisco perché alcune persone preferiscono attenersi alla grammatica su questo, soprattutto perché alcuni casi limite e casi complessi richiedono che tu lo faccia (anche se, comunque, puoi banalmente aggirare il problema in tutti i casi di cui sono a conoscenza) ... ma non penso che questi casi siano più importanti del fatto che insegnare l'allineamento a destra sia fuorviante per i neofiti. Per non parlare del tipo di brutto! :)
Razze di leggerezza in orbita

@LightnessRacesinOrbit L'insegnamento dell'allineamento a destra è tutt'altro che fuorviante. È l'unico modo corretto di insegnarlo. NON insegnarlo è fuorviante.
Miles Rout

5

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


4

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

Ad esempio, seguendo un elenco collegato: 1) inizia con la tua carta con l'indirizzo 2) Vai all'indirizzo sulla carta 3) Apri la casella di posta per trovare un nuovo pezzo di carta con l'indirizzo successivo su di esso

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

Si noti che il passaggio 3 è il punto in cui si verifica la dereferenza e dove si verificano arresti anomali o errori quando l'indirizzo non è valido. Supponendo che potresti arrivare alla cassetta postale di un indirizzo non valido, immagina che ci sia un buco nero o qualcosa dentro che rovescia il mondo :)


Una brutta complicazione con l'analogia del numero di cassetta postale è che mentre il linguaggio inventato da Dennis Ritchie definisce il comportamento in termini di indirizzi di byte e valori memorizzati in quei byte, il linguaggio definito dallo Standard C invita le implementazioni "ottimizzate" per usare un comportamento modello che è più complicato ma definisce vari aspetti del modello in modo ambiguo, contraddittorio e incompleto.
supercat

3

Penso che il motivo principale per cui le persone hanno dei problemi è 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 da 1 metro ciascuno, farli stare in piedi in una certa configurazione e usare i righelli per indicare l'un l'altro. Quindi mostra l'aritmetica del puntatore spostando le persone (e dove indicano i loro sovrani). 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 afferrano correttamente nella pratica o perché la manipolazione del puntatore è intrinsecamente più difficile in quelle lingue. Non riesco a ricordare bene la mia transizione, ma conoscevo i suggerimenti di Pascal e poi mi sono trasferito in C e mi sono perso del tutto.


2

Non penso che i puntatori stessi siano confusi. Molte persone possono capire il concetto. Ora quanti puntatori riesci a pensare o con quanti livelli di riferimento indiretto sei a tuo agio. Non ci vogliono troppi per mettere le persone oltre il limite. Il fatto che possano essere modificati accidentalmente da bug nel tuo programma può anche renderli molto difficili da eseguire il debug quando le cose vanno male nel tuo codice.


2

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

Ironia della sorte, la cosa che in realtà mi ha aiutato a capire i puntatori era incontrare il concetto di iteratore nella libreria di modelli standard c ++ . È 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.


Il problema è principalmente nella sintassi della dichiarazione C. Ma l'uso del puntatore sarebbe sicuramente più semplice se (*p)lo fosse stato (p->), e quindi avremmo p->->xinvece l'ambiguo*p->x
MSalters

@MSalters Oh mio Dio stai scherzando, vero? Non ci sono incoerenze lì. a->bsignifica semplicemente (*a).b.
Miles Rout,

@Miles: In effetti, e con quella logica * p->xsignifica * ((*a).b)che *p -> xsignifica (*(*p)) -> x. La miscelazione di operatori prefisso e postfisso provoca un'analisi ambigua.
Salterio

@MSalters no, perché gli spazi bianchi sono irrilevanti. È come dire che 1+2 * 3dovrebbe essere 9.
Miles Rout

2

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

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


1
La cosa interessante è che i puntatori C in realtà non espongono alcun carattere charasteristico dell'architettura di memoria sottostante. Le uniche differenze tra riferimenti Java e puntatori C sono che puoi avere tipi complessi che coinvolgono puntatori (es. Int *** o char * ( ) (void * )), c'è l'aritmetica dei puntatori per le matrici e i puntatori per strutturare i membri, la presenza del vuoto * e dualità array / pointer. Oltre a ciò, funzionano allo stesso modo.
jpalecek,

Buon punto. È l'aritmetica del puntatore e la possibilità di buffer overflow - rottura dell'astrazione con rottura dell'area di memoria attualmente rilevante - che lo fa.
Joshua Fox,

@jpalecek: è abbastanza facile capire come funzionano i puntatori sulle implementazioni che documentano il loro comportamento in termini di architettura sottostante. Dire foo[i]significa andare in un determinato punto, avanzare di una certa distanza e vedere cosa c'è. Ciò che complica le cose è lo strato di astrazione extra molto più complicato che è stato aggiunto dallo Standard a puro vantaggio del compilatore, ma modella le cose in un modo che non è adatto alle esigenze del programmatore e alle esigenze del compilatore.
supercat

2

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

Quindi dico che la RAM è 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 di (il primo byte di) quella variabile nella RAM.

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

Disegnerei un array su un elenco di carta e lo uso solo per mostrare cose come molti puntatori che puntano alla stessa memoria, aritmetica puntatore, puntatore a puntatore e così via.


1

Numero casella postale.

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

(E se fai l'aritmetica sui numeri delle caselle postali, potresti avere un problema, perché la lettera va nella casella sbagliata. E se qualcuno si sposta 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.)


1

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

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

Hmm, il problema è che tutto ciò che gli iteratori sono in totale disaccordo con ciò che le piattaforme di runtime (Java / CLR) stanno cercando di ottenere: un nuovo, semplice utilizzo da parte di tutti. Che può essere buono, ma l'hanno detto una volta nel libro viola e l'hanno detto anche prima e prima di C:

Indirection.

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 posto 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 del pessimo, trascinando le app tramite un sacco di istanze di tipi di riferimento.

"Le Pointer è economico."


4
Cosa c'entra questo con qualcosa?
Neil Williams,

... cosa stai cercando di dire, a parte "il collegamento statico è la cosa migliore in assoluto" e "non capisco come funzioni qualcosa di diverso da quello che ho imparato in precedenza"?
Luaan,

Luaan, non potresti assolutamente sapere cosa si può imparare smontando la JIT nel 2000, vero? Che finisce in una tabella di salto, da una tabella di puntatori, come mostrato nel 2000 online in ASM, quindi non capire nulla di diverso può avere un altro significato: leggere attentamente è un'abilità essenziale, riprovare.
rama-jka toti,

1

Alcune risposte sopra hanno affermato che "i puntatori non sono davvero difficili", ma non hanno continuato a rivolgersi direttamente dove "i puntatori sono difficili!" viene da. Alcuni anni fa ho insegnato agli studenti CS del primo anno (solo per un anno, dal momento che l'ho succhiato chiaramente) ed era chiaro per me che l' idea del puntatore non è difficile. Ciò che è difficile è capire perché e quando vorresti un puntatore .

Non credo che tu possa divorziare da questa domanda - perché e quando usare un puntatore - dalla spiegazione di problemi di ingegneria del software più ampi. Perché ogni variabile non dovrebbe essere una variabile globale e perché si dovrebbe scomporre il codice simile in funzioni (che, a tale scopo, usano i puntatori per specializzare il loro comportamento nel loro sito di chiamata).


0

Non vedo cosa sia così confuso riguardo ai puntatori. Indicano una posizione in 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 in una posizione che contiene un int.

Il problema con i puntatori è che indicano una posizione in memoria, quindi è facile rintracciare in una posizione in cui non dovresti trovarti. Come prova guarda i numerosi buchi di sicurezza nelle applicazioni C / C ++ dall'overflow del buffer (incrementando il puntatore oltre il limite assegnato).


0

Solo per confondere un po 'di più le cose, a volte devi lavorare con le maniglie anziché con i puntatori. Le maniglie sono puntatori a puntatori, in modo che il back-end possa spostare le cose in memoria per deframmentare l'heap. Se il puntatore cambia a metà 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 un po 'più coerentemente di me. :-)


-1: le maniglie non sono puntatori a puntatori; non sono puntatori in alcun senso. Non confonderli.
Ian Goldby,

"Non sono in alcun senso puntatori" - um, mi permetto di dissentire.
SarekOfVulcan,

Un puntatore è una posizione di memoria. Un handle è un identificatore univoco. Potrebbe essere un puntatore, ma potrebbe anche essere un indice in un array o qualsiasi altra cosa. Il collegamento che hai fornito è solo un caso speciale in cui l'handle è un puntatore, ma non deve essere. Vedi anche parashift.com/c++-faq-lite/references.html#faq-8.8
Ian Goldby

Quel collegamento non supporta la tua affermazione che non sono in alcun modo puntatori - "Ad esempio, le maniglie potrebbero essere Fred **, dove i puntatori Fred * puntati ..." Non credo il -1 era giusto.
SarekOfVulcan,

0

Ogni principiante C / C ++ ha lo stesso problema e questo problema non si verifica perché "i puntatori sono difficili da imparare" ma "chi e come viene spiegato". Alcuni studenti lo raccolgono verbalmente in modo visivo e il modo migliore per spiegarlo è usare l' esempio "treno" (abiti per l'esempio verbale e visivo).

Dove "locomotiva" è un puntatore che non può contenere nulla e "carro" è ciò che "locomotiva" cerca di tirare (o indicare). Dopo, puoi classificare il "carro" stesso, può contenere animali, piante o persone (o un mix di essi).

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.