LIFO vs FIFO
LIFO sta per Last In, First Out. Come in, l'ultimo oggetto messo nella pila è il primo oggetto estratto dalla pila.
Quello che hai descritto con l'analogia dei tuoi piatti (nella prima revisione ), è una coda o FIFO, First In, First Out.
La differenza principale tra i due è che il LIFO / stack spinge (inserisce) e si apre (rimuove) dalla stessa estremità, e una FIFO / coda lo fa dalle estremità opposte.
// Both:
Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]
// Stack // Queue
Pop() Pop()
-> [a, b] -> [b, c]
Il puntatore dello stack
Diamo un'occhiata a ciò che sta accadendo sotto il cofano dello stack. Ecco un po 'di memoria, ogni casella è un indirizzo:
...[ ][ ][ ][ ]... char* sp;
^- Stack Pointer (SP)
E c'è un puntatore dello stack che punta nella parte inferiore dello stack attualmente vuoto (se lo stack cresce o diminuisce non è particolarmente rilevante qui, quindi lo ignoreremo, ma ovviamente nel mondo reale, che determina quale operazione aggiunge e quali sottrae dal SP).
Quindi spingiamo di a, b, and c
nuovo. Grafica a sinistra, operazione "alto livello" al centro, pseudo codice C-ish a destra:
...[a][ ][ ][ ]... Push('a') *sp = 'a';
^- SP
...[a][ ][ ][ ]... ++sp;
^- SP
...[a][b][ ][ ]... Push('b') *sp = 'b';
^- SP
...[a][b][ ][ ]... ++sp;
^- SP
...[a][b][c][ ]... Push('c') *sp = 'c';
^- SP
...[a][b][c][ ]... ++sp;
^- SP
Come puoi vedere, ogni volta che inseriamo push
l'argomento nella posizione attualmente puntata dal puntatore dello stack e regola il puntatore dello stack in modo che punti nella posizione successiva.
Ora pop:
...[a][b][c][ ]... Pop() --sp;
^- SP
...[a][b][c][ ]... return *sp; // returns 'c'
^- SP
...[a][b][c][ ]... Pop() --sp;
^- SP
...[a][b][c][ ]... return *sp; // returns 'b'
^- SP
Pop
è l'opposto di push
, regola il puntatore dello stack in modo che punti alla posizione precedente e rimuove l'elemento che era lì (di solito per restituirlo a chiunque abbia chiamato pop
).
Probabilmente l'hai notato b
e c
sono ancora nella memoria. Voglio solo assicurarti che quelli non sono errori di battitura. Torneremo su quello a breve.
La vita senza un puntatore pila
Vediamo cosa succede se non abbiamo un puntatore allo stack. A partire dalla spinta di nuovo:
...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]... Push(a) ? = 'a';
Ehm, hmm ... se non abbiamo un puntatore dello stack, allora non possiamo spostare qualcosa all'indirizzo a cui punta. Forse possiamo usare un puntatore che punta alla base anziché all'inizio.
...[ ][ ][ ][ ]... char* bp; // "base pointer"
^- bp bp = malloc(...);
...[a][ ][ ][ ]... Push(a) *bp = 'a';
^- bp
// No stack pointer, so no need to update it.
...[b][ ][ ][ ]... Push(b) *bp = 'b';
^- bp
Uh Oh. Poiché non possiamo modificare il valore fisso della base dello stack, abbiamo semplicemente sovrascritto a
spingendolo b
nella stessa posizione.
Bene, perché non teniamo traccia di quante volte abbiamo spinto. E dovremo anche tenere traccia dei tempi in cui siamo spuntati.
...[ ][ ][ ][ ]... char* bp; // "base pointer"
^- bp bp = malloc(...);
int count = 0;
...[a][ ][ ][ ]... Push(a) bp[count] = 'a';
^- bp
...[a][ ][ ][ ]... ++count;
^- bp
...[a][b][ ][ ]... Push(a) bp[count] = 'b';
^- bp
...[a][b][ ][ ]... ++count;
^- bp
...[a][b][ ][ ]... Pop() --count;
^- bp
...[a][b][ ][ ]... return bp[count]; //returns b
^- bp
Bene funziona, ma in realtà è abbastanza simile a prima, tranne che *pointer
è più economico di pointer[offset]
(senza aritmetica extra), per non parlare del fatto che è meno da digitare. Mi sembra una perdita.
Proviamo di nuovo. Invece di usare lo stile di stringa Pascal per trovare la fine di una raccolta basata su array (tenendo traccia di quanti elementi ci sono nella raccolta), proviamo lo stile di stringa C (scansiona dall'inizio alla fine):
...[ ][ ][ ][ ]... char* bp; // "base pointer"
^- bp bp = malloc(...);
...[ ][ ][ ][ ]... Push(a) char* top = bp;
^- bp, top
while(*top != 0) { ++top; }
...[ ][ ][ ][a]... *top = 'a';
^- bp ^- top
...[ ][ ][ ][ ]... Pop() char* top = bp;
^- bp, top
while(*top != 0) { ++top; }
...[ ][ ][ ][a]... --top;
^- bp ^- top return *top; // returns '('
Potresti aver già indovinato il problema qui. Non è garantito che la memoria non inizializzata sia 0. Quindi, quando cerchiamo la posizione più alta a
, finiamo per saltare un mucchio di posizioni di memoria inutilizzate che contengono spazzatura casuale. Allo stesso modo, quando eseguiamo la scansione verso l'alto, finiamo per saltare ben oltre ciò che a
abbiamo appena spinto fino a quando non troviamo finalmente un'altra posizione di memoria che sembra essere 0
, e torniamo indietro e restituiamo la spazzatura casuale appena prima.
È abbastanza facile da risolvere, dobbiamo solo aggiungere operazioni Push
e Pop
assicurarci che la parte superiore dello stack sia sempre aggiornata per essere contrassegnata con un 0
, e dobbiamo inizializzare lo stack con un tale terminatore. Ovviamente ciò significa anche che non possiamo avere un 0
(o qualunque valore scegliamo come terminatore) come valore effettivo nello stack.
Inoltre, abbiamo anche cambiato le operazioni O (1) in operazioni O (n).
TL; DR
Il puntatore dello stack tiene traccia della parte superiore dello stack, in cui si verifica tutta l'azione. Ci sono modi per sbarazzarsi di esso ( bp[count]
e top
sono essenzialmente ancora il puntatore dello stack), ma entrambi finiscono per essere più complicati e più lenti rispetto al semplice avere il puntatore dello stack. E non sapere dove si trova la parte superiore della pila significa che non puoi usare la pila.
Nota: il puntatore dello stack che punta alla "parte inferiore" dello stack di runtime in x86 potrebbe essere un malinteso relativo all'intero stack di runtime che è capovolto. In altre parole, la base dello stack viene posizionata in un indirizzo di memoria elevato e la punta dello stack si riduce in indirizzi di memoria inferiori. Il puntatore dello stack fa punto alla punta dello stack in cui si verifica l'opportunità, solo che punta è in un indirizzo di memoria inferiore alla base della pila.