C Pilha e Heap

Quando um programa roda, a memória dele não é uma bagunça só: ela é dividida em regiões, cada uma com uma função e uma regra de quanto tempo as coisas vivem lá. As duas que mais importam para você são a pilha (onde moram as variáveis locais, que nascem e morrem sozinhas) e o heap (onde mora a memória que você pede com malloc e devolve com free). Entender essa divisão explica erros que parecem mágica, como um array que derruba o programa antes da primeira linha, e prepara o terreno para as próximas aulas de memória dinâmica.

Você vai aprender
  • O mapa da memória de um programa: código, literais, globais e static, heap e pilha.
  • O tempo de vida de cada tipo de variável: quem cria, quem apaga e quando.
  • Como a pilha funciona (as "bandejas" de cada função) e por que nunca se devolve o endereço de uma variável local.
  • Por que um array local enorme causa estouro de pilha (o limite de 8 MB).
  • Por que o heap existe e quando usá-lo.

Esta aula supõe que você já leu Memória e Endereços e Ponteiros. Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa.


A ideia: a pilha de bandejas e o guarda-volumes

Pense no restaurante por quilo onde a Ana almoça.

  • Na entrada há uma pilha de bandejas. Cada cliente que chega pega a bandeja de cima; quem sai devolve a sua, que volta para o topo. Ninguém precisa anotar nada: a pilha se organiza sozinha, é rapidíssima, mas tem altura limitada e cada bandeja tem um tamanho fixo.
  • No fundo do salão há um guarda-volumes. Você pede um armário do tamanho que precisar, recebe o número dele e ele fica seu até você devolver a chave. Cabe muita coisa e dura o quanto você quiser, mas você precisa lembrar de devolver; se esquecer, o armário fica ocupado para sempre.
No restauranteNo programa
A pilha de bandejasA pilha (stack): uma "bandeja" por função em andamento, com as variáveis locais dela
Pegar e devolver bandejaAutomático: a bandeja aparece quando a função começa e some quando ela termina
O guarda-volumesO heap (em português às vezes "monte"): memória pedida durante a execução
O número do armárioO endereço que o malloc devolve, guardado num ponteiro
Devolver a chavefree(ponteiro)

1. O mapa da memória

Este programa cria uma coisa em cada região e mostra onde cada uma mora. Não se preocupe com o malloc: ele tem aula própria, malloc e free. Aqui ele só serve para pegar um pedaço do heap.

#include <stdio.h>
#include <stdlib.h>

int recorde = 1200;              // global
const char *saudacao = "Olá";    // o ponteiro é global; o texto é um literal

int main(void) {
    int vidas = 3;                       // local: na pilha
    static int partidas = 0;             // static: junto das globais
    int *pontos = malloc(sizeof *pontos); // memória do heap
    if (pontos == NULL) return 1;
    *pontos = 0;

    printf("código  (main)     %p\n", (void *)main);
    printf("literal (\"Olá\")    %p\n", (void *)saudacao);
    printf("global  recorde    %p\n", (void *)&recorde);
    printf("static  partidas   %p\n", (void *)&partidas);
    printf("heap    *pontos    %p\n", (void *)pontos);
    printf("pilha   vidas      %p\n", (void *)&vidas);
    printf("pilha   pontos     %p\n", (void *)&pontos);

    free(pontos);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra mapa.c -o mapa
$ ./mapa
código  (main)     0x640e6c7cc1a9
literal ("Olá")    0x640e6c7cd004
global  recorde    0x640e6c7cf010
static  partidas   0x640e6c7cf024
heap    *pontos    0x640ea99d8010
pilha   vidas      0x7ffcfb4d886c
pilha   pontos     0x7ffcfb4d8870
$ ./mapa
código  (main)     0x57e7ee3541a9      « outra execução, outros números
literal ("Olá")    0x57e7ee355004
global  recorde    0x57e7ee357010
static  partidas   0x57e7ee357024
heap    *pontos    0x57e812bcf010
pilha   vidas      0x7fff892311dc
pilha   pontos     0x7fff892311e0

Os números mudam a cada execução (o Linux sorteia onde cada região começa, uma proteção chamada ASLR), mas a ordem é sempre a mesma: código, literais e globais lá embaixo, perto uns dos outros; o heap um pouco acima; e a pilha lá no alto, perto de 0x7ff…. Repare também que pontos (o ponteiro) mora na pilha, enquanto *pontos (a memória para onde ele aponta) mora no heap: são duas coisas diferentes em dois lugares diferentes.

endereços altos (0x7ff…) Pilha (stack) vidas, pontos (o ponteiro) uma bandeja por função cresce para baixo espaço livre cresce para cima Heap *pontos (o que o malloc deu) Dados recorde, partidas Literais "Olá" (só leitura) Código main (só leitura) 0x7ffcfb4d886c 0x640ea99d8010 0x640e6c7cf010 0x640e6c7cd004 0x640e6c7cc1a9 vive enquanto a função roda vive até o free vivem o programa inteiro
O mapa simplificado de um programa no Linux de 64 bits, com os endereços da primeira execução. Na figura, os endereços altos estão em cima. O espaço livre entre a pilha e o heap é enorme (bem maior do que o desenho sugere).
RegiãoO que mora láQuem cria e apagaTempo de vida
Código (text)As instruções das funçõesO sistema, ao carregar o programaO programa inteiro (só leitura)
Literais (rodata, "read-only data")Textos entre aspas, como "Olá"O sistemaO programa inteiro (só leitura)
Dados (data e bss)Variáveis globais e variáveis staticO sistema; começam com o valor escrito ou com zeroO programa inteiro
HeapO que você pede com malloc, calloc, reallocVocê: malloc cria, free apagaAté o free (ou até o programa acabar)
PilhaVariáveis locais e parâmetrosAutomático: a função começa, elas nascem; a função termina, elas somemEnquanto a função está rodando
No editor do guia, o mapa é outro

No editor, o programa roda em WebAssembly, dentro do navegador, e a memória é arrumada de outro jeito. O mesmo mapa.c mostrou lá:

código  (main)     0x1
literal ("Olá")    0x800000
global  recorde    0x800a30
static  partidas   0x800bb4
heap    *pontos    0x801700
pilha   vidas      0x7ffff8
pilha   pontos     0x7ffff4

A pilha fica embaixo (de 0x0 a 0x7fffff, exatamente 8 MB, crescendo em direção ao zero), os literais e as globais começam em 0x800000, e o heap vem logo depois deles. As funções nem moram na memória comum: o "endereço" 0x1 é a posição de main numa tabela de funções. As regiões são as mesmas; só a arrumação muda. E os números não mudam entre execuções, porque o editor não usa ASLR.


2. A pilha: uma bandeja por função

Cada vez que uma função começa, ela ganha uma bandeja (o nome técnico é stack frame, "quadro da pilha") com espaço para as suas variáveis locais e parâmetros. Quando ela termina, a bandeja é devolvida. Veja as bandejas se empilhando: main chama primeira, que chama segunda; depois main chama segunda de novo:

#include <stdio.h>

void segunda(void) {
    int c = 3;
    printf("c (em segunda)  mora em %p\n", (void *)&c);
}

void primeira(void) {
    int b = 2;
    printf("b (em primeira) mora em %p\n", (void *)&b);
    segunda();
}

int main(void) {
    int a = 1;
    printf("a (em main)     mora em %p\n", (void *)&a);
    primeira();
    segunda();
    return 0;
}
$ gcc -std=c23 -Wall -Wextra bandejas.c -o bandejas
$ ./bandejas
a (em main)     mora em 0x7ffd0dc459a4
b (em primeira) mora em 0x7ffd0dc45984     « 0x20 (32) bytes abaixo
c (em segunda)  mora em 0x7ffd0dc45964     « mais 32 bytes abaixo
c (em segunda)  mora em 0x7ffd0dc45984     « a 2ª chamada caiu onde estava a bandeja de primeira
Momento…9a4…984…964
Dentro de primeira, chamando segundamain: a = 1primeira: b = 2segunda: c = 3 ← topo
segunda e primeira terminarammain: a = 1 ← topo(livre)(livre)
main chama segunda de novomain: a = 1segunda: c = 3 ← topo(livre)
A pilha em três momentos. No computador, a pilha cresce para os endereços menores: cada bandeja nova fica abaixo da anterior. Quando uma função termina, a bandeja dela fica livre e a próxima chamada reaproveita o espaço.

É isso que torna a pilha tão boa: criar e apagar variáveis locais custa praticamente nada (o programa só move um marcador do "topo"), e ninguém precisa lembrar de devolver. Também é por isso que uma variável local sem valor inicial tem lixo: a bandeja reaproveitada ainda tem o que a função anterior deixou lá (aula Variáveis).

A variável local morre com a função

Como a bandeja é devolvida no fim da função, o endereço de uma variável local deixa de valer quando a função termina. A Ana tentou escrever uma função que cria um contador e devolve o endereço dele:

#include <stdio.h>

int *criar_contador(void) {
    int contador = 0;
    return &contador;
}

int main(void) {
    int *c = criar_contador();
    printf("Contador criado em %p\n", (void *)c);
    *c = 5;
    printf("Contador = %d\n", *c);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra local.c -o local
local.c: In function ‘criar_contador’:
local.c:5:12: warning: function returns address of local variable [-Wreturn-local-addr]
    5 |     return &contador;
      |            ^~~~~~~~~
$ ./local
Contador criado em (nil)
Falha de segmentação     (imagem do núcleo gravada) ./local
warning: function returns address of local variable
"A função devolve o endereço de uma variável local". O -Wall pegou: quando criar_contador terminar, contador não existe mais, e o endereço aponta para uma bandeja devolvida.
Contador criado em (nil)
Surpresa: o gcc, sabendo que esse endereço seria inútil, devolveu de propósito um ponteiro nulo ((nil) é como a glibc mostra o NULL). Assim o erro aparece logo, em vez de corromper a memória em silêncio.
Falha de segmentação
*c = 5 tentou escrever no endereço zero, e o sistema matou o programa.

No editor do guia, o Clang também avisa, com outras palavras (address of stack memory associated with local variable 'contador' returned [-Wreturn-stack-address], "endereço da memória da pilha da variável local contador devolvido"), mas o programa roda e mostra Contador = 5, como se estivesse tudo bem. Não está: o 5 foi escrito numa bandeja devolvida, que a próxima função chamada vai sobrescrever. É comportamento indefinido, e "funcionar" num compilador e quebrar no outro é exatamente o que se espera dele.

Missão: rode e leia o aviso. O conserto é guardar o contador no heap, que não some quando a função termina. Troque int contador = 0; por um ponteiro que recebe malloc(sizeof *contador) (confira se veio NULL), zere o valor com *contador = 0;, devolva o ponteiro e, no main, chame free(c); no fim. A saída deve terminar com Contador = 5, sem avisos.

#include <stdio.h>
#include <stdlib.h>

int *criar_contador(void) {
    int contador = 0;
    return &contador;
}

int main(void) {
    int *c = criar_contador();
    if (c == NULL) {
        printf("Faltou memória!\n");
        return 1;
    }
    *c = 5;
    printf("Contador = %d\n", *c);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

int *criar_contador(void) {
    int *contador = malloc(sizeof *contador);
    if (contador == NULL) {
        return NULL;
    }
    *contador = 0;
    return contador;
}

int main(void) {
    int *c = criar_contador();
    if (c == NULL) {
        printf("Faltou memória!\n");
        return 1;
    }
    *c = 5;
    printf("Contador = %d\n", *c);
    free(c);
    return 0;
}

3. A pilha tem limite: o estouro de pilha

A pilha é pequena. No Linux, o tamanho máximo vem de uma configuração do sistema, que o comando ulimit -s mostra em kilobytes:

$ ulimit -s
8192                     « 8192 KB = 8 MB para a pilha inteira

8 MB parece muito para variáveis como int vidas, mas um array local grande come isso rápido. A Ana quis guardar 3 milhões de notas num array local (3 000 000 × 4 bytes = 12 MB):

#include <stdio.h>

int main(void) {
    int notas[3000000];
    printf("Criei um array de %zu bytes na pilha.\n", sizeof notas);
    for (int i = 0; i < 3000000; i++) {
        notas[i] = i % 10;
    }
    printf("Última nota: %d\n", notas[3000000 - 1]);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra grande.c -o grande          « nenhum aviso
$ ./grande
Falha de segmentação     (imagem do núcleo gravada) ./grande
$ echo $?
139

Compilou sem aviso e morreu antes de mostrar a primeira linha. A bandeja do main precisava de 12 MB, a pilha só tem 8, e na primeira vez que o programa encostou na memória além do limite, o sistema o matou. Isso é um estouro de pilha (stack overflow, o mesmo nome do site famoso de perguntas de programação). O código 139 é 128 + 11, o número do sinal SIGSEGV, o mesmo de uma falha de segmentação comum: a mensagem não diz que foi a pilha.

Com 2 milhões de notas (8 000 000 bytes, um pouco menos que os 8 MB = 8 388 608 bytes), o mesmo programa roda:

$ ./grande2
Criei um array de 8000000 bytes na pilha.
Última nota: 9

Mas isso é andar na beirada: outras funções também precisam de bandejas, e em outro computador o limite pode ser outro (no Windows, a pilha padrão é de só 1 MB). No editor do guia a pilha também tem 8 MB: o array de 12 MB dá Falha de segmentação (imagem do núcleo gravada) e código 139, e o de 8 000 000 bytes roda.

A outra forma de estourar: recursão sem fim

Cada chamada de função pega uma bandeja. Uma função que chama a si mesma sem parar empilha bandejas até passar dos 8 MB, e o programa morre do mesmo jeito. Veja em Recursão.


4. O heap: para que ele existe

Se a pilha é tão rápida e automática, por que existe o heap? Porque a pilha tem três limitações, e o heap resolve as três:

Problema da pilhaComo o heap resolveExemplo
É pequena (8 MB)O heap pode usar quase toda a memória livre do computadorOs 3 milhões de notas
A variável morre quando a função terminaA memória do heap vive até o free, independentemente de qual função a criouA função criar_contador
O tamanho de cada bandeja é decidido na compilaçãoO tamanho do malloc pode ser decidido durante a execução"Quantos alunos tem a turma?" e só então reservar

Os 3 milhões de notas no heap funcionam sem problema:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t n = 3000000;
    int *notas = malloc(n * sizeof *notas);
    if (notas == NULL) {
        printf("Faltou memória!\n");
        return 1;
    }
    printf("Reservei %zu bytes no heap.\n", n * sizeof *notas);
    for (size_t i = 0; i < n; i++) {
        notas[i] = i % 10;
    }
    printf("Última nota: %d\n", notas[n - 1]);
    free(notas);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra heapgrande.c -o heapgrande
$ ./heapgrande
Reservei 12000000 bytes no heap.
Última nota: 9

Repare na diferença de uso: notas agora é um ponteiro (8 bytes, na pilha) que aponta para os 12 MB no heap. Os colchetes funcionam igual (notas[i]), graças à relação entre ponteiros e arrays (aula Ponteiros e Arrays).

O preço do heap:

  • Você tem de devolver a memória com free. Esquecer causa vazamentos de memória; devolver duas vezes ou usar depois de devolver causa erros graves (aula malloc e free).
  • O malloc pode falhar (devolve NULL), então todo pedido precisa ser conferido.
  • Pedir e devolver memória do heap é mais lento que usar a pilha, porque o malloc precisa procurar um espaço livre do tamanho certo.

5. Globais, static e literais

As variáveis globais e as static moram na região de dados. Elas nascem antes do main e só morrem quando o programa acaba, e começam com zero se você não der outro valor. Uma variável local static continua existindo entre uma chamada e outra da função (aula Escopo e Variáveis static). Por isso, outra forma de escapar do limite da pilha é static int notas[3000000];: o array vai para a região de dados, que não tem o limite de 8 MB. O defeito é que ele ocupa memória o programa inteiro e o tamanho continua fixo.

Os literais de texto ("Olá", "ana") moram numa região só de leitura. Um char * que aponta para um literal não pode ser usado para mudá-lo:

#include <stdio.h>

int main(void) {
    char *nome = "ana";
    printf("Antes: %s\n", nome);
    nome[0] = 'A';
    printf("Depois: %s\n", nome);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra literal.c -o literal       « nenhum aviso
$ ./literal
Antes: ana
Falha de segmentação     (imagem do núcleo gravada) ./literal

Nenhum aviso, e o programa morreu ao tentar escrever na região só de leitura. No editor do guia, ele mostra Depois: Ana e termina bem, porque o WebAssembly não tem memória só de leitura: mais um caso de comportamento indefinido que "funciona" num lugar e quebra no outro. O conserto é ter uma cópia própria, num array: char nome[] = "ana"; (o array é local, na pilha, e recebe uma cópia das letras), ou declarar const char *nome para o compilador barrar a escrita (aula Ponteiros e Strings).


Qual usar

SituaçãoOnde guardar
Variáveis pequenas usadas só dentro da função (contadores, somas, arrays pequenos)Pilha (variável local comum). É o padrão: rápido e automático.
Dados grandes (centenas de KB ou mais)Heap (malloc)
Tamanho só conhecido durante a execução (o usuário diz quantos)Heap
Dados criados numa função e usados depois que ela terminaHeap (e quem recebe o ponteiro cuida do free)
Um valor que precisa existir o programa inteiro, com tamanho fixoVariável global ou static (com moderação: globais deixam o código difícil de entender)
Um texto fixo que não mudaLiteral com const char *
E os VLAs?

Desde o C99 dá para criar um array local com tamanho decidido na execução: int notas[n]; (variable length array, VLA). Ele mora na pilha, então um n grande estoura a pilha do mesmo jeito, e sem nenhum jeito de conferir antes. No C11 eles viraram opcionais, e muitos projetos os proíbem. Para tamanhos vindos do usuário, prefira malloc, que devolve NULL quando não dá.


Quando dá errado

warning: function returns address of local variable [-Wreturn-local-addr]

O que significa: a função devolve o endereço de uma variável local, que deixa de existir quando ela termina. No gcc o ponteiro volta como NULL (e o programa cai ao usá-lo); no Clang ele volta apontando para uma bandeja devolvida, e o programa pode até parecer funcionar.

Como consertar: reserve a memória com malloc e devolva esse ponteiro (quem chama faz o free), ou deixe quem chama criar a variável e passar o endereço dela para a função (aula Passar por Ponteiro).

Falha de segmentação antes de qualquer saída, com um array local grande

O que significa: estouro de pilha. A soma das variáveis locais passou dos 8 MB (no Linux; 1 MB no Windows). Nenhum aviso de compilação ajuda aqui.

Como consertar: mova o array para o heap (int *v = malloc(n * sizeof *v);, conferindo NULL e com free no fim) ou, se o tamanho é fixo, use static. Em último caso, ulimit -s aumenta o limite só no seu terminal, mas o programa continua quebrando no computador dos outros.

Falha de segmentação ao mudar uma letra de um texto

O que significa: o char * aponta para um literal, que mora numa região só de leitura.

Como consertar: copie para um array (char nome[] = "ana";) antes de mudar. E declare ponteiros para literais como const char *: aí o próprio compilador recusa a escrita.

A variável local "esqueceu" o valor entre duas chamadas

O que significa: é o funcionamento normal da pilha: cada chamada ganha uma bandeja nova, e a variável nasce de novo.

Como consertar: se o valor precisa sobreviver entre chamadas, use static na variável local, ou guarde-o em quem chama e passe por ponteiro.


Referência rápida

DeclaraçãoRegiãoVive atéValor inicial se você não der um
int x; dentro de uma funçãoPilhaO fim da funçãoLixo
Parâmetros de funçãoPilhaO fim da funçãoO valor passado
int x; fora de qualquer funçãoDadosO fim do programa0
static int x; dentro de uma funçãoDadosO fim do programa0
"texto"Literais (só leitura)O fim do programaAs letras escritas
malloc(n)HeapO freeLixo (use calloc para zeros)
ulimit -sMostra o limite da pilha em KB (8192 no Ubuntu)

Exercícios

1. Onde mora?

Uma variável int total = 0; declarada dentro do main mora na…

2. O limite

Qual comando mostra o tamanho máximo da pilha no terminal do Linux?

3. Devolver a memória

Complete: a memória do heap vive até alguém chamar…

4. Por que int *f(void) { int x = 7; return &x; } é um erro?

5. Um programa com double medidas[2000000]; (16 MB) dentro do main morre com falha de segmentação antes de mostrar qualquer coisa. Qual é a causa mais provável?

6. Em int *notas = malloc(100 * sizeof *notas); dentro do main, onde moram o ponteiro notas e os 100 inteiros?

Missão final: tirar o array da pilha

Missão: rode: o array de 12 MB estoura a pilha e o programa morre sem mostrar nada (código 139). Mude notas para um ponteiro que recebe malloc(N * sizeof *notas), confira o NULL, troque o sizeof notas do primeiro printf por N * sizeof *notas e chame free(notas) antes do return 0;. A saída deve ser:

Reservei 12000000 bytes.
Última nota: 9
#include <stdio.h>
#include <stdlib.h>

#define N 3000000

int main(void) {
    int notas[N];
    printf("Reservei %zu bytes.\n", sizeof notas);
    for (int i = 0; i < N; i++) {
        notas[i] = i % 10;
    }
    printf("Última nota: %d\n", notas[N - 1]);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

#define N 3000000

int main(void) {
    int *notas = malloc(N * sizeof *notas);
    if (notas == NULL) {
        printf("Faltou memória!\n");
        return 1;
    }
    printf("Reservei %zu bytes.\n", N * sizeof *notas);
    for (int i = 0; i < N; i++) {
        notas[i] = i % 10;
    }
    printf("Última nota: %d\n", notas[N - 1]);
    free(notas);
    return 0;
}

O #define N 3000000 dá um nome ao número para não repeti-lo (aula Constantes). Experimente também a outra saída, static int notas[N];: funciona, mas o array passa a ocupar 12 MB do começo ao fim do programa.


Resumo

  • A memória de um programa tem regiões: código e literais (só leitura), dados (globais e static), heap e pilha.
  • A pilha guarda as variáveis locais e os parâmetros, uma bandeja por função; tudo nasce e morre sozinho. Nunca devolva o endereço de uma variável local (-Wreturn-local-addr).
  • A pilha é pequena: 8 MB no Linux (ulimit -s = 8192) e também no editor do guia. Um array local maior que isso causa estouro de pilha: falha de segmentação, código 139, sem aviso.
  • O heap existe para dados grandes, de tamanho decidido na execução, ou que precisam viver depois da função que os criou. Você pede com malloc e devolve com free.
  • Globais e static vivem o programa inteiro e começam com zero; literais de texto não podem ser mudados.
  • Comportamentos indefinidos como devolver endereço local e escrever num literal quebram no gcc e podem "funcionar" no editor: não confie no "funcionou".