LIFO vs FIFO
LIFO significa Last In, First Out. Assim, o último item colocado na pilha é o primeiro item retirado da pilha.
O que você descreveu com a analogia de sua louça (na primeira revisão ) é uma fila ou FIFO, primeiro a entrar, primeiro a sair.
A principal diferença entre os dois é que o LIFO / pilha empurra (insere) e aparece (remove) do mesmo lado, e uma fila FIFO / faz isso de extremos opostos.
// Both:
Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]
// Stack // Queue
Pop() Pop()
-> [a, b] -> [b, c]
O ponteiro da pilha
Vamos dar uma olhada no que está acontecendo sob o capô da pilha. Aqui está um pouco de memória, cada caixa é um endereço:
...[ ][ ][ ][ ]... char* sp;
^- Stack Pointer (SP)
E há um ponteiro de pilha apontando para a parte inferior da pilha atualmente vazia (se a pilha cresce ou diminui não é particularmente relevante aqui, então ignoraremos isso, mas é claro que no mundo real, isso determina qual operação adiciona e que subtrai do SP).
Então, vamos empurrar a, b, and cnovamente. Gráficos à esquerda, operação de "alto nível" no meio, pseudo-código C-ish à direita:
...[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 você pode ver, cada vez que pushinserimos o argumento no local que o ponteiro da pilha está apontando no momento e ajusta o ponteiro da pilha para apontar para o próximo local.
Agora vamos aparecer:
...[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é o oposto de push, ajusta o ponteiro da pilha para apontar para o local anterior e remove o item que estava lá (geralmente para devolvê-lo a quem chamou pop).
Você provavelmente percebeu isso be cainda está na memória. Eu só quero garantir que esses não são erros de digitação. Voltaremos a isso em breve.
Vida sem ponteiro de pilha
Vamos ver o que acontece se não tivermos um ponteiro de pilha. Começando com o envio novamente:
...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]... Push(a) ? = 'a';
Hum, hum ... se não tivermos um ponteiro de pilha, não podemos mover algo para o endereço que ele aponta. Talvez possamos usar um ponteiro que aponte para a base e não para o topo.
...[ ][ ][ ][ ]... 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 não podemos alterar o valor fixo da base da pilha, apenas o substituímos apressionando bpara o mesmo local.
Bem, por que não acompanhamos quantas vezes pressionamos. E também precisamos acompanhar os horários em que aparecemos.
...[ ][ ][ ][ ]... 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
Bem, funciona, mas na verdade é bastante semelhante a antes, exceto que *pointeré mais barato do que pointer[offset](sem aritmética extra), sem mencionar que é menos digitado. Isso parece uma perda para mim.
Vamos tentar de novo. Em vez de usar o estilo de string Pascal para encontrar o final de uma coleção baseada em array (rastreando quantos itens há na coleção), vamos tentar o estilo de string C (varredura do começo ao fim):
...[ ][ ][ ][ ]... 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 '('
Você já deve ter adivinhado o problema aqui. Não é garantido que a memória não inicializada seja 0. Portanto, quando procuramos o topo para colocar a, acabamos pulando um monte de locais de memória não utilizados que contêm lixo aleatório. Da mesma forma, quando digitalizamos para o topo, acabamos pulando muito além do aque acabamos de empurrar até finalmente encontrarmos outro local de memória 0, e voltar e devolver o lixo aleatório antes disso.
Isso é fácil de corrigir, basta adicionar operações Pushe Popgarantir que o topo da pilha seja sempre atualizado para ser marcado com a 0, e precisamos inicializar a pilha com esse terminador. Claro que isso também significa que não podemos ter um 0(ou qualquer valor que escolhemos como terminador) como um valor realmente na pilha.
Além disso, também alteramos o que eram operações O (1) para operações O (n).
TL; DR
O ponteiro da pilha controla o topo da pilha, onde ocorre toda a ação. Existem maneiras de se livrar dele ( bp[count]e topainda são essencialmente o ponteiro da pilha), mas ambas acabam sendo mais complicadas e lentas do que simplesmente ter o ponteiro da pilha. E não saber onde está o topo da pilha significa que você não pode usá-la.
Nota: O ponteiro da pilha que aponta para a "parte inferior" da pilha de tempo de execução no x86 pode ser um equívoco relacionado a toda a pilha de tempo de execução de cabeça para baixo. Em outras palavras, a base da pilha é colocada em um endereço de memória alto e a ponta da pilha cresce em endereços de memória inferiores. O ponteiro da pilha faz ponto para a ponta da pilha onde toda a ação ocorre, assim que a ponta está em um endereço de memória menor do que a base da pilha.