Toda variável de um programa em C mora em algum lugar, e onde determina duas coisas que você não pode mudar depois: por quanto tempo ela sobrevive e quanto dela você pode ter. O C lhe dá três regiões de armazenamento, e escolher errado produz ou um travamento ou um vazamento. Esta página as apresenta e mostra o bug clássico que vem de errar os tempos de vida.
As três regiões
enderecos altos
+---------------------------+
| pilha | locais, parametros, enderecos de retorno
| cresce para baixo | | liberada automaticamente no retorno
| v |
+---------------------------+
| (lacuna nao usada)|
+---------------------------+
| ^ |
| cresce para cima | |
| heap | blocos de malloc / calloc / realloc
+---------------------------+ liberados apenas por free()
| dados estaticos/globais | globais e statics, execucao inteira
+---------------------------+
| codigo (text) | o codigo de maquina, somente leitura
+---------------------------+
enderecos baixos
- Armazenamento automático (a pilha) guarda parâmetros de função e locais não estáticas. Um bloco da pilha é reservado quando se entra numa função e liberado quando ela retorna. O tamanho é fixo em tempo de compilação.
- Armazenamento dinâmico (o heap) guarda tudo que vem de
malloc,callocerealloc. O tamanho é decidido em tempo de execução; o tempo de vida só termina nofree. - Armazenamento estático guarda globais e tudo declarado como
static. Existe durante toda a execução do programa e é inicializado com zero antes demaincomeçar.
Os endereços do diagrama são o arranjo usual, não uma garantia - a norma descreve tempos de vida, não leiaute.
Armazenamento automático em ação
Cada chamada a demo ganha um local novo e um table novo. Nada é liberado à mão, nada pode vazar, e a alocação custa uma única instrução que move o ponteiro de pilha. É por isso que locais comuns devem ser o seu padrão: são o armazenamento mais rápido e mais seguro que o C tem.
O detalhe é a chave de fechamento. Assim que ela é executada, aquela memória sumiu.
O ponteiro pendurado
Este é o bug que todo programador C escreve uma vez:
/* QUEBRADO: devolve o endereco de memoria que nao existe mais */
int *make_counter(void) {
int count = 0;
return &count; /* count morre nesta chave */
}
int main(void) {
int *p = make_counter();
*p = 5; /* escrevendo num quadro de pilha morto */
return 0;
}
&count era um endereço perfeitamente válido enquanto make_counter estava rodando. No retorno, aquele espaço da pilha é entregue à próxima função chamada, então p agora aponta para as variáveis locais de outra pessoa. Ler dá lixo; escrever as corrompe. GCC e Clang avisam sobre exatamente essa forma (-Wreturn-local-addr), então compile com avisos ligados.
O mesmo bug se disfarça com arrays, e aí o aviso muitas vezes não dispara:
A versão quebrada dessa função montaria o texto num char buf[64] local e faria return buf; - devolvendo o endereço de um buffer que deixa de existir no mesmo instante.
Três formas de corrigir
1. O chamador fornece o buffer (mostrado acima). Sem alocação, sem questão de propriedade, e o estilo mais comum nas bibliotecas de C. A função recebe o tamanho para poder se manter dentro dele.
2. Devolva memória do heap, e diga quem a libera.
O bloco do heap sobrevive à função por projeto - esse é todo o sentido da memória dinâmica. O custo é o comentário sobre a propriedade e o free do chamador.
3. Use armazenamento estático, quando um único buffer compartilhado for aceitável:
O static dentro de uma função mantém o escopo da variável local enquanto lhe dá o tempo de vida do programa, então devolver o endereço dela é legal. A troca é que só existe uma dela: todo chamador a compartilha, o que torna esse padrão inutilizável em código com threads e surpreendente mesmo em código de thread única quando dois chamadores seguram o ponteiro ao mesmo tempo.
Tamanho: onde a pilha acaba
O espaço da pilha é pequeno e fixo. A thread principal costuma ganhar 1 MB no Windows e 8 MB no Linux; uma thread criada muitas vezes ganha bem menos. O heap é limitado pela memória disponível do sistema.
void bad(void) {
int huge[1000000]; /* ~4 MB de pilha - provavelmente trava na entrada */
huge[0] = 1;
}
Não há diagnóstico e não há NULL para verificar: o programa simplesmente morre, normalmente com uma falha de segmentação, antes da primeira linha do corpo rodar. A versão no heap reporta a falha direito:
Recursão profunda esgota a pilha do mesmo jeito, um quadro de cada vez - uma função recursiva desgovernada é, na prática, a causa mais comum de estouro de pilha.
Custo e localidade
A alocação na pilha é uma operação aritmética num registrador. A alocação no heap é uma chamada de biblioteca que procura um bloco adequado, pode adquirir um lock e ocasionalmente pede mais memória ao sistema operacional. Num laço quente essa diferença é mensurável.
Os dados da pilha também são compactos e foram tocados recentemente, então tendem a estar em cache. Os blocos do heap podem estar espalhados. Nenhum desses fatos deve guiar um projeto sozinho - a correção do tempo de vida vem primeiro - mas entre dois projetos ambos corretos, o da pilha costuma ser o mais rápido.
Enxergando as regiões
Imprimir endereços torna o leiaute concreto. Os valores exatos mudam a cada execução (sistemas modernos os aleatorizam), mas o agrupamento fica visível:
A global e a estática ficam lado a lado; o bloco do heap está em outro lugar; a local costuma estar longe das duas. Converta para void * no %p - é o que o especificador de formato exige.
Escolhendo
Use a pilha quando:
- o tamanho é conhecido em tempo de compilação,
- os dados só são necessários dentro desta função e das que ela chama,
- e é pequeno - alguns kilobytes, não megabytes.
Use o heap quando:
- o tamanho depende da entrada, de um arquivo ou de um cálculo,
- os dados precisam sobreviver à função que os criou,
- ou é grande o bastante para ameaçar o limite da pilha.
Use estático quando:
- exatamente uma instância deve existir para o programa inteiro,
- e compartilhá-la entre todos os chamadores é genuinamente correto.
O padrão é a pilha. Recorra ao heap quando uma das suas três razões se aplicar, e quando o fizer, siga as regras de propriedade em vazamentos de memória para que o bloco cujo tempo de vida você ganhou ainda seja liberado.
Dois bugs espelhados
Vale nomeá-los juntos, porque são a mesma pergunta de tempo de vida respondida de duas maneiras:
- Ponteiro pendurado - a memória morreu antes do ponteiro. Devolver
&local, ou usar um ponteiro depois dofree. O programa lê ou escreve num armazenamento que agora pertence a outra coisa. - Vazamento de memória - o ponteiro morreu antes da memória. Perder a última referência a um bloco do
malloc. Nada quebra imediatamente; o processo apenas cresce.
Os dois vêm de um descompasso entre por quanto tempo os dados precisam viver e em que região você os colocou. Decida o tempo de vida primeiro, e a região vem a reboque.
Perguntas frequentes
Qual a diferença entre a pilha e o heap em C?
A pilha guarda variáveis locais: o compilador define o tamanho delas, elas são criadas quando se entra numa função e destruídas quando ela retorna, e a alocação não custa nada. O heap guarda os blocos de malloc: você escolhe o tamanho em tempo de execução, o bloco sobrevive até você chamar free, e a alocação tem um custo real.
Por que não posso devolver um ponteiro para uma variável local em C?
Porque o armazenamento da local é liberado no instante em que a função retorna. O ponteiro ainda guarda aquele endereço, mas a memória agora pertence à próxima chamada de função - lê-la dá lixo, escrever nela corrompe dados sem relação. Isso é um ponteiro pendurado. Devolva um bloco do malloc, ou faça o chamador fornecer o buffer.
Qual o tamanho da pilha em C?
Tipicamente de 1 a 8 MB para a thread principal, e bem menos para threads adicionais - pequeno o bastante para que int big[1000000]; como variável local normalmente trave o programa logo na entrada. O heap é limitado pela memória disponível no sistema, então dados grandes ou de tamanho desconhecido pertencem a ele.
Quando devo usar o heap em vez da pilha em C?
Três casos: o tamanho só é conhecido em tempo de execução, os dados precisam sobreviver à função que os criou, ou o bloco é grande demais para a pilha (grosso modo, qualquer coisa acima de algumas centenas de kilobytes). Todo o resto deveria ser uma local comum - é mais rápido e não pode vazar.