LIFO vs FIFO
LIFO significa Last In, First Out. Como en, el último elemento puesto en la pila es el primer elemento sacado de la pila.
Lo que describiste con tu analogía de platos (en la primera revisión ), es una cola o FIFO, Primero en entrar, Primero en salir.
La principal diferencia entre los dos es que el LIFO / stack empuja (inserta) y hace estallar (elimina) desde el mismo extremo, y un FIFO / cola lo hace desde extremos opuestos.
// Both:
Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]
// Stack // Queue
Pop() Pop()
-> [a, b] -> [b, c]
El puntero de la pila
Echemos un vistazo a lo que sucede debajo del capó de la pila. Aquí hay un poco de memoria, cada cuadro es una dirección:
...[ ][ ][ ][ ]... char* sp;
^- Stack Pointer (SP)
Y hay un puntero de pila que apunta al final de la pila actualmente vacía (si la pila crece o disminuye no es particularmente relevante aquí, por lo que ignoraremos eso, pero, por supuesto, en el mundo real, eso determina qué operación agrega , y que resta del SP).
Así que empujemos de a, b, and c
nuevo. Gráficos a la izquierda, operación de "alto nivel" en el medio, pseudocódigo C-ish a la derecha:
...[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
Como puede ver, cada vez push
que insertamos el argumento en la ubicación a la que apunta actualmente el puntero de la pila, y ajusta el puntero de la pila para que apunte a la siguiente ubicación.
Ahora hagamos estallar:
...[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
es lo contrario de push
, ajusta el puntero de la pila para apuntar a la ubicación anterior y elimina el elemento que estaba allí (generalmente para devolverlo a quien llamó pop
).
Probablemente lo hayas notado b
y c
todavía estás en la memoria. Solo quiero asegurarte que esos no son errores tipográficos. Volveremos a eso en breve.
La vida sin un puntero de pila
Veamos qué sucede si no tenemos un puntero de pila. Comenzando con empujar nuevamente:
...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]... Push(a) ? = 'a';
Er, hmm ... si no tenemos un puntero de pila, entonces no podemos mover algo a la dirección a la que apunta. Quizás podamos usar un puntero que apunte a la base en lugar de a la parte superior.
...[ ][ ][ ][ ]... 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. Como no podemos cambiar el valor fijo de la base de la pila, simplemente sobrescribimos a
presionando b
a la misma ubicación.
Bueno, ¿por qué no hacemos un seguimiento de cuántas veces hemos presionado? Y también necesitaremos hacer un seguimiento de los tiempos que hemos aparecido.
...[ ][ ][ ][ ]... 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
Bueno, funciona, pero en realidad es bastante similar a antes, excepto que *pointer
es más barato que pointer[offset]
(sin aritmética adicional), sin mencionar que es menos para escribir. Esto me parece una pérdida.
Intentemoslo de nuevo. En lugar de usar el estilo de cadena Pascal para encontrar el final de una colección basada en una matriz (seguimiento de cuántos elementos hay en la colección), intentemos con el estilo de cadena C (escaneo desde el principio hasta el final):
...[ ][ ][ ][ ]... 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 '('
Puede que ya hayas adivinado el problema aquí. No se garantiza que la memoria no inicializada sea 0. Entonces, cuando buscamos la parte superior para colocar a
, terminamos saltando sobre un montón de ubicaciones de memoria no utilizadas que tienen basura aleatoria en ellas. De manera similar, cuando escaneamos hacia la parte superior, terminamos saltando mucho más allá de lo a
que acabamos de empujar hasta que finalmente encontramos otra ubicación de memoria que resulta ser 0
, y retrocedemos y devolvemos la basura aleatoria justo antes de eso.
Eso es bastante fácil de solucionar, solo tenemos que agregar operaciones Push
y Pop
asegurarnos de que la parte superior de la pila siempre se actualice para que se marque con un 0
, y tenemos que inicializar la pila con dicho terminador. Por supuesto, eso también significa que no podemos tener un 0
(o cualquier valor que elijamos como terminador) como un valor real en la pila.
Además de eso, también hemos cambiado lo que eran operaciones O (1) en operaciones O (n).
TL; DR
El puntero de la pila realiza un seguimiento de la parte superior de la pila, donde se produce toda la acción. Hay formas de deshacerse de él ( bp[count]
y top
siguen siendo esencialmente el puntero de la pila), pero ambos terminan siendo más complicados y más lentos que simplemente tener el puntero de la pila. Y no saber dónde está la parte superior de la pila significa que no puedes usar la pila.
Nota: El puntero de la pila que apunta al "fondo" de la pila de tiempo de ejecución en x86 podría ser una idea errónea relacionada con que toda la pila de tiempo de ejecución está al revés. En otras palabras, la base de la pila se coloca en una dirección de memoria alta, y la punta de la pila crece en direcciones de memoria más bajas. El puntero de la pila hace punto a la punta de la pila donde se produce toda la acción, sólo que la punta se encuentra en una dirección de memoria más baja que la base de la pila.