Menu

Pilha vs heap em C: tempos de vida, ponteiros pendurados e quando usar cada um

Onde seus dados realmente moram: armazenamento automático na pilha, armazenamento dinâmico no heap e o armazenamento estático como terceira região - com o clássico bug do ponteiro pendurado ao devolver uma variável local, e uma regra para escolher.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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, calloc e realloc. O tamanho é decidido em tempo de execução; o tempo de vida só termina no free.
  • Armazenamento estático guarda globais e tudo declarado como static. Existe durante toda a execução do programa e é inicializado com zero antes de main começ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 do free. 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.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR