Cosa c'è di così difficile in puntatori / ricorsione? [chiuso]


20

Nei pericoli delle scuole di java, Joel discute della sua esperienza in Penn e della difficoltà dei "difetti di segmentazione". Lui dice

[i segfault sono difficili fino a quando] "fai un respiro profondo e provi davvero a forzare la tua mente a lavorare simultaneamente a due diversi livelli di astrazione".

Dato un elenco di cause comuni per i segfault, non capisco come dobbiamo lavorare a 2 livelli di astrazione.

Per qualche ragione, Joel considera questi concetti fondamentali per l'abilità di un programmatore di astrarre. Non voglio assumere troppo. Quindi, cosa c'è di così difficile in puntatori / ricorsione? Gli esempi sarebbero belli.


31
Smetti di preoccuparti di ciò che Joel potrebbe pensare di te. Se trovi facile la ricorsione, va bene. Non tutti gli altri.
FrustratedWithFormsDesigner,

6
La ricorsione è facile per definizione (funzione che chiama sé), ma sapere quando usarla e come farlo funzionare è la parte difficile.
JeffO,

9
Candidati per un lavoro presso Fog Creek e facci sapere come va. Siamo tutti molto interessati alla tua auto promozione.
Joel Etherton,

4
@ P.Brian.Mackey: non siamo fraintendimenti. La domanda non fa davvero nulla. È palese autopromozione. Se vuoi sapere cosa chiede Joel riguardo a puntatori / ricorsione, chiedigli: team@stackoverflow.com
Joel Etherton,

19
Duplica di questa domanda ?
ozz,

Risposte:


38

Ho notato per la prima volta che gli indicatori e la ricorsione erano difficili al college. Avevo seguito un paio di corsi tipici del primo anno (uno era C e Assembler, l'altro era in Schema). Entrambi i corsi sono iniziati con centinaia di studenti, molti dei quali hanno avuto anni di esperienza di programmazione a livello di scuola superiore (in genere BASIC e Pascal, a quei tempi). Ma non appena i puntatori furono introdotti nel corso C e fu introdotta la ricorsione nel corso Scheme, un numero enorme di studenti - forse anche la maggioranza - furono completamente confusi. Questi erano bambini che avevano scritto MOLTO codice prima e non avevano avuto alcun problema, ma quando colpivano puntatori e ricorsione, colpivano anche un muro in termini di capacità cognitiva.

La mia ipotesi è che i puntatori e la ricorsione siano gli stessi in quanto richiedono di mantenere due livelli di astrazione contemporaneamente nella testa. C'è qualcosa in più livelli di astrazione che richiede un tipo di attitudine mentale che è molto probabile che alcune persone non avranno mai.

  • Con i puntatori, i "due livelli di astrazione" sono "dati, indirizzo di dati, indirizzo di indirizzo di dati, ecc." O ciò che tradizionalmente chiamiamo "valore vs. riferimento". Per lo studente non addestrato, è molto difficile vedere la differenza tra l'indirizzo di x e X stesso .
  • Con la ricorsione, i "due livelli di astrazione" stanno capendo come è possibile che una funzione si chiami da sola. Un algoritmo ricorsivo è talvolta ciò che le persone chiamano "programmazione per pio illusione" ed è molto, molto innaturale pensare a un algoritmo in termini di "caso base + caso induttivo" invece dell'elenco più naturale di passaggi che segui per risolvere un problema ". Per lo studente non addestrato che guarda un algoritmo ricorsivo, l'algoritmo sembra porre la domanda .

Sarei anche perfettamente disposto ad accettare che è possibile insegnare suggerimenti e / o ricorsioni a chiunque ... Non ho prove in un modo o nell'altro. So che empiricamente, essere in grado di comprendere davvero questi due concetti è un ottimo predittore della capacità di programmazione generale e che nel normale corso di formazione CS universitaria, questi due concetti rappresentano alcuni dei maggiori ostacoli.


4
"molto, molto innaturale pensare a un algoritmo in termini di" caso base + caso induttivo "" - Penso che non sia affatto innaturale, è solo che i bambini non vengono formati di conseguenza.
Ingo,

14
se fosse naturale, non avresti bisogno di essere allenato. : P
Joel Spolsky

1
Buon punto :), ma non abbiamo bisogno di formazione in matematica, logica, fisica, ecc., Tutti in un senso più ampio e naturale. È interessante notare che pochi programmatori hanno problemi con la sintassi delle lingue, eppure è pieno di ricorsività.
Ingo,

1
Nella mia università, il primo corso è iniziato con la programmazione funzionale e la ricorsione quasi immediatamente, ben prima di introdurre la mutazione e simili. Ho scoperto che alcuni studenti senza esperienza capivano la ricorsione meglio di quelli con qualche esperienza. Detto questo, il vertice della classe era composto da persone con molta esperienza.
Tikhon Jelvis,

2
Penso che l'incapacità di comprendere i puntatori e la ricorsione sia collegata a a) livello generale di QI eb) cattiva educazione matematica.
quant_dev,

23

La ricorsione non è solo "una funzione che si chiama da sola". Non apprezzerai davvero il motivo per cui la ricorsione è difficile finché non ti ritrovi a disegnare stack-frame per capire cosa è andato storto con il tuo parser ricorsivo di discesa. Spesso avrai funzioni reciprocamente ricorsive (la funzione A chiama la funzione B, che chiama la funzione C, che può chiamare la funzione A). Può essere molto difficile capire cosa è andato storto quando sei N stackframe in profondità in una serie di funzioni reciprocamente ricorsive.

Per quanto riguarda i puntatori, ancora una volta, il concetto di puntatori è piuttosto semplice: una variabile che memorizza un indirizzo di memoria. Ma ancora una volta, quando qualcosa va storto con la complicata struttura dei dati dei void**puntatori che puntano a nodi diversi, vedrai perché può diventare complicato mentre fai fatica a capire perché uno dei tuoi puntatori punta a un indirizzo di immondizia.


1
L'implementazione di un parser decente ricorsivo è stata quando ho davvero sentito di avere un po 'di controllo sulla ricorsione. I puntatori sono facili da capire ad alto livello come hai detto; non è fino a quando non si entra nei dettagli delle implementazioni che trattano i puntatori che si vede perché sono complicati.
Chris,

La ricorsione reciproca tra molte funzioni è essenzialmente la stessa goto.
Starblue,

2
@starblue, non proprio, poiché ogni stackframe crea nuove istanze di variabili locali.
Charles Salvia,

Hai ragione, solo la ricorsione della coda è la stessa di goto.
Starblue,

3
@wnoise int a() { return b(); }può essere ricorsivo, ma dipende dalla definizione di b. Quindi non è così semplice come sembra ...
alternativa il

14

Java supporta i puntatori (sono chiamati riferimenti) e supporta la ricorsione. Quindi in superficie, la sua argomentazione appare inutile.

Quello di cui sta davvero parlando è la capacità di eseguire il debug. Un puntatore Java (err, riferimento) è garantito per puntare a un oggetto valido. AC pointer no. E il trucco nella programmazione in C, supponendo che tu non usi strumenti come valgrind , è scoprire esattamente dove hai rovinato un puntatore (raramente si trova nel punto trovato in uno stacktrace).


5
I puntatori di per sé sono un dettaglio. L'uso dei riferimenti in Java non è più complicato dell'uso delle variabili locali in C. Anche mescolarle come fanno le implementazioni di Lisp (un atomo può essere un numero intero di dimensioni limitate, o un carattere o un puntatore) non è difficile. Diventa più difficile quando la lingua consente allo stesso tipo di dati di essere locali o referenziati, con sintassi diversa e molto pelosa quando la lingua consente l'aritmetica del puntatore.
David Thornley,

@ David - um, cosa c'entra questo con la mia risposta?
Anon,

1
Il tuo commento su Java che supporta i puntatori.
David Thornley,

"dove hai rovinato un puntatore (raramente si trova nel punto trovato in uno stacktrace)." Se sei abbastanza fortunato da ottenere uno stacktrace.
Omega Centauri,

5
Sono d'accordo con David Thornley; Java non supporta i puntatori a meno che non sia possibile creare un puntatore a un puntatore a un puntatore a un puntatore a un int. Che forse suppongo di poter fare facendo come 4-5 classi che ciascuna faccia riferimento a qualcos'altro, ma sono davvero dei puntatori o è una brutta soluzione?
alternativa il

12

Il problema con i puntatori e la ricorsione non è che sono necessariamente difficili da capire, ma che sono insegnati male, specialmente per quanto riguarda le lingue come C o C ++ (principalmente perché le lingue stesse vengono insegnate male). Ogni volta che sento (o leggo) qualcuno dice "un array è solo un puntatore" muoio un po 'dentro.

Allo stesso modo, ogni volta che qualcuno usa la funzione Fibonacci per illustrare la ricorsione, voglio urlare. È un cattivo esempio perché la versione iterativa non è più difficile da scrivere e si comporta almeno altrettanto bene o meglio di quella ricorsiva e non ti dà una reale comprensione del perché una soluzione ricorsiva sarebbe utile o desiderabile. Quicksort, traversal tree, ecc., Sono esempi molto migliori per il perché e come della ricorsione.

Dover confondere con i puntatori è un artefatto di lavorare in un linguaggio di programmazione che li espone. Generazioni di programmatori Fortran stavano costruendo elenchi, alberi, pile e code senza bisogno di un tipo di puntatore dedicato (o allocazione dinamica della memoria), e non ho mai sentito nessuno accusare Fortran di essere un linguaggio giocattolo.


Sono d'accordo, avevo avuto anni / decenni di Fortran prima di vedere dei veri e propri indicatori, quindi avevo già usato il mio modo di fare la stessa cosa, prima di avere la possibilità di lasciare che il lanquage / compilatore lo facesse per me. Penso anche che la sintassi C relativa ai puntatori / indirizzi sia molto confusa, anche se il concetto di un valore, memorizzato in un indirizzo è molto semplice.
Omega Centauri,

se hai un link a Quicksort implementato in Fortran IV, mi piacerebbe vederlo. Non dicendo che non si può fare - in effetti, l'ho implementato in BASIC circa 30 anni fa - ma sarei interessato a vederlo.
Anon,

Non ho mai lavorato in Fortran IV, ma ho implementato alcuni algoritmi ricorsivi nell'implementazione VAX / VMS di Fortran 77 (c'era un gancio per consentire di salvare l'obiettivo di un goto come un tipo speciale di variabile, in modo da poter scrivere GOTO target) . Penso che abbiamo dovuto costruire le nostre pile di runtime, però. È stato tanto tempo fa che non ricordo più i dettagli.
John Bode,

8

Esistono diverse difficoltà con i puntatori:

  1. Aliasing La possibilità di modificare il valore di un oggetto utilizzando nomi / variabili diversi.
  2. Non località La possibilità di modificare un valore di oggetti in un contesto diverso da quello in cui viene dichiarato (ciò accade anche con argomenti passati per riferimento).
  3. Mancata corrispondenza della durata La durata di un puntatore potrebbe essere diversa dalla durata dell'oggetto a cui punta e ciò potrebbe portare a riferimenti non validi (SEGFAULTS) o immondizia.
  4. Puntatore aritmetico . Alcuni linguaggi di programmazione consentono la manipolazione di puntatori come numeri interi e ciò significa che i puntatori possono puntare ovunque (compresi i punti più imprevisti quando è presente un bug). Per usare correttamente l'aritmetica del puntatore, un programmatore deve essere consapevole delle dimensioni della memoria degli oggetti indicati e questo è qualcosa in più a cui pensare.
  5. Cast di tipi La possibilità di lanciare un puntatore da un tipo a un altro consente di sovrascrivere la memoria di un oggetto diverso da quello previsto.

Ecco perché un programmatore deve pensare in modo più approfondito quando utilizza i puntatori (non conosco i due livelli di astrazione ). Questo è un esempio dei tipici errori commessi da un principiante:

Pair* make_pair(int a, int b)
{
    Pair p;
    p.a = a;
    p.b = b;
    return &p;
}

Si noti che codice come quello sopra è perfettamente ragionevole in linguaggi che non hanno un concetto di puntatori ma piuttosto uno di nomi (riferimenti), oggetti e valori, come fanno i linguaggi di programmazione funzionale e i linguaggi con Garbage Collection (Java, Python) .

La difficoltà con le funzioni ricorsive si verifica quando le persone senza sufficiente background matematico (in cui la ricorsività è comune e conoscenza richiesta) cercano di avvicinarsi a loro pensando che la funzione si comporterà in modo diverso a seconda di quante volte è stata chiamata in precedenza . Questo problema è aggravato perché le funzioni ricorsive possono davvero essere create in modi in cui devi pensare in quel modo per capirle.

Pensa alle funzioni ricorsive con i puntatori passati, come in un'implementazione procedurale di un albero rosso-nero in cui la struttura dei dati viene modificata sul posto; è qualcosa di più difficile da pensare rispetto a una controparte funzionale .

Non è menzionato nella domanda, ma l'altro importante problema con cui i principianti hanno difficoltà è la concorrenza .

Come altri hanno già detto, esiste un ulteriore problema non concettuale con alcuni costrutti del linguaggio di programmazione: è anche se capiamo che errori semplici e onesti con tali costrutti possono essere estremamente difficili da debug.


L'uso di quella funzione restituirà un puntatore valido ma la variabile si trova in un ambito superiore all'ambito che ha chiamato la funzione, quindi il puntatore potrebbe (supporre sarà) invalidato quando si utilizza malloc.
bendaggio

4
@Radek S: No, non lo farà. Restituirà un puntatore non valido che in alcuni ambienti funziona per un po 'fino a quando qualcos'altro lo sovrascrive. (In pratica, questo sarà lo stack, non l'heap. malloc()Non è più probabile che qualsiasi altra funzione lo faccia.)
wnoise

1
@Radeck Nella funzione di esempio, il puntatore indica alla memoria che il linguaggio di programmazione (in questo caso C garantisce) verrà liberato una volta che la funzione tornerà. Pertanto, il puntatore restituito punta all'immondizia . Le lingue con Garbage Collection mantengono in vita l'oggetto fintanto che viene fatto riferimento in qualsiasi contesto.
Apalala,

A proposito, Rust ha puntatori ma senza questi problemi. (quando non in un contesto pericoloso)
Sarge Borsch,

2

Puntatori e ricorsione sono due bestie separate e ci sono diverse ragioni che qualificano ciascuna come "difficile".

In generale, i puntatori richiedono un modello mentale diverso rispetto alla pura assegnazione di variabili. Quando ho una variabile puntatore, è proprio questo: un puntatore a un altro oggetto, gli unici dati che contiene sono gli indirizzi di memoria a cui punta. Quindi, ad esempio, se ho un puntatore int32 e assegno direttamente un valore, non sto cambiando il valore di int, sto puntando a un nuovo indirizzo di memoria (ci sono molti trucchi accurati che puoi fare con questo ). Ancora più interessante è avere un puntatore a un puntatore (questo è ciò che accade quando si passa una variabile Ref come parametro di funzione in C #, la funzione può assegnare un oggetto completamente diverso al parametro e quel valore sarà ancora nell'ambito quando la funzione uscite.

La ricorsione fa un leggero salto mentale quando apprendi per la prima volta perché stai definendo una funzione in termini di se stessa. È un concetto selvaggio quando ci si imbatte per la prima volta, ma una volta afferrata l'idea, diventa una seconda natura.

Ma torniamo all'argomento in questione. L'argomento di Joel non riguarda i puntatori o la ricorsione in sé e per sé, ma piuttosto il fatto che gli studenti vengono rimossi ulteriormente dal modo in cui i computer funzionano davvero. Questa è la scienza in informatica. C'è una netta differenza tra imparare a programmare e imparare come funzionano i programmi. Non penso che si tratti tanto di "l'ho imparato in questo modo, quindi tutti dovrebbero impararlo in questo modo" mentre sostiene che molti programmi di CS stanno diventando scuole commerciali glorificate.


1

Do a P. Brian un +1, perché mi sento come lui: la ricorsione è un concetto talmente fondamentale che chi ha le minime difficoltà dovrebbe prendere in considerazione l'idea di cercare un lavoro presso Mac Donalds, ma poi c'è anche la ricorsione:

make a burger:
   put a cold burger on the grill
   wait
   flip
   wait
   hand the fried burger over to the service personel
   unless its end of shift: make a burger

Sicuramente, la mancanza di comprensione ha anche a che fare con le nostre scuole. Qui si dovrebbero introdurre numeri naturali come hanno fatto Peano, Dedekind e Frege, quindi non avremmo avuto così tante difficoltà in seguito.


6
Quella è la ricaduta della coda, che è probabilmente in loop.
Michael K,

6
Mi dispiace, per me il looping è probabilmente una ricorsione della coda :)
Ingo,

3
@Ingo: :) Funzionale fanatico!
Michael K,

1
@Michael - hehe, davvero !, ma penso che si possa sostenere che la ricorsione è il concetto più fondamentale.
Ingo,

@Ingo: Potresti, davvero (il tuo esempio lo dimostra bene). Tuttavia, per qualche ragione gli umani hanno difficoltà con questo nella programmazione - sembriamo volere quel extra goto topper qualche ragione IME.
Michael K,

1

Non sono d'accordo con Joel sul fatto che il problema sia quello di pensare a più livelli di astrazione di per sé, penso che sia più che i puntatori e la ricorsione siano due buoni esempi di problemi che richiedono un cambiamento nel modello mentale che le persone hanno del funzionamento dei programmi.

I puntatori sono, credo, il caso più semplice da illustrare. La gestione dei puntatori richiede un modello mentale di esecuzione del programma che spieghi il modo in cui i programmi funzionano effettivamente con indirizzi e dati di memoria. La mia esperienza è stata che spesso i programmatori non ci hanno nemmeno pensato prima di conoscere i puntatori. Anche se lo conoscono in senso astratto, non l'hanno adottato nel loro modello cognitivo di come funziona un programma. Quando vengono introdotti i puntatori, è necessario un cambiamento fondamentale nel modo in cui pensano al funzionamento del codice.

La ricorsione è problematica perché ci sono due blocchi concettuali da comprendere. Il primo è a livello di macchina e, proprio come i puntatori, può essere superato sviluppando una buona comprensione di come i programmi vengono effettivamente archiviati ed eseguiti. L'altro problema con la ricorsione è, penso, che le persone hanno una tendenza naturale a cercare di decostruire un problema ricorsivo in un problema non ricorsivo, che confonde la comprensione di una funzione ricorsiva come gestalt. Questo è un problema con le persone che hanno un background matematico insufficiente o un modello mentale che non lega la teoria matematica allo sviluppo di programmi.

Il fatto è che non credo che i puntatori e la ricorsione siano le uniche due aree problematiche per le persone bloccate in un modello mentale insufficiente. Il parallelismo sembra essere un'altra area in cui alcune persone rimangono semplicemente bloccate e hanno difficoltà ad adattare il loro modello mentale per renderne conto, è solo che spesso i puntatori e la ricorsione sono facili da testare in un'intervista.


1
  DATA    |     CODE
          |
 pointer  |   recursion    SELF REFERENTIAL
----------+---------------------------------
 objects  |   macro        SELF MODIFYING
          |
          |

Il concetto di dati e codice autoreferenziali è alla base rispettivamente della definizione di puntatori e ricorsione. Sfortunatamente, la diffusa esposizione ai linguaggi di programmazione imperativa ha portato gli studenti di informatica a credere di dover capire l'implementazione attraverso il comportamento operativo dei loro runtime quando devono fidarsi di questo mistero per l'aspetto funzionale del linguaggio. Sommare tutti i numeri fino a cento sembra una semplice questione di iniziare con uno e aggiungerlo al successivo nella sequenza e farlo all'indietro con l'aiuto di funzioni circolari autoreferenziali sembra perverso e persino pericoloso per molti non abituati alla sicurezza di funzioni pure.

Il concetto di auto-modifica dei dati e del codice è alla base rispettivamente della definizione di oggetti (ovvero dati intelligenti) e macro. Le menziono poiché sono ancora più difficili da comprendere, specialmente quando si prevede una comprensione operativa del runtime da una combinazione di tutti e quattro i concetti, ad esempio una macro che genera un insieme di oggetti che implementa un parser decorsivo ricorsivo con l'aiuto di un albero di puntatori . Invece di tracciare passo per passo l'intera operazione dello stato del programma attraverso ogni strato di astrazione contemporaneamente, i programmatori imperativi devono imparare a fidarsi che le loro variabili sono assegnate una sola volta all'interno delle funzioni pure e che ripetute invocazioni della stessa funzione pura con gli stessi argomenti producono sempre lo stesso risultato (ovvero trasparenza referenziale), anche in un linguaggio che supporta anche funzioni impure, come Java. Correre in tondo dopo il runtime è uno sforzo inutile. L'astrazione dovrebbe semplificare.


-1

Molto simile alla risposta di Anon.
A parte le difficoltà cognitive per i neofiti, sia i puntatori che la ricorsione sono molto potenti e possono essere usati in modo criptico.

Il rovescio della medaglia di grande potenza, è che ti danno grande potenza per rovinare il tuo programma in modi sottili.
Memorizzare un valore fasullo in una variabile normale è abbastanza male, ma la memorizzazione di qualcosa di fasullo in un puntatore può far accadere ogni sorta di cose catastrofiche ritardate.
E peggio ancora, tali effetti possono cambiare mentre si tenta di diagnosticare / eseguire il debug della causa del bizzarro comportamento del programma.

Allo stesso modo con la ricorsione. Può essere un modo molto potente per organizzare cose complicate, inserendo la difficoltà nella struttura di dati nascosti (stack).
Ma, se qualcosa viene fatto in modo sottilmente errato, può essere difficile capire cosa sta succedendo.

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.