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.
- 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 restaurante | No programa |
|---|---|
| A pilha de bandejas | A pilha (stack): uma "bandeja" por função em andamento, com as variáveis locais dela |
| Pegar e devolver bandeja | Automático: a bandeja aparece quando a função começa e some quando ela termina |
| O guarda-volumes | O heap (em português às vezes "monte"): memória pedida durante a execução |
| O número do armário | O endereço que o malloc devolve, guardado num ponteiro |
| Devolver a chave | free(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.
| Região | O que mora lá | Quem cria e apaga | Tempo de vida |
|---|---|---|---|
| Código (text) | As instruções das funções | O sistema, ao carregar o programa | O programa inteiro (só leitura) |
| Literais (rodata, "read-only data") | Textos entre aspas, como "Olá" | O sistema | O programa inteiro (só leitura) |
| Dados (data e bss) | Variáveis globais e variáveis static | O sistema; começam com o valor escrito ou com zero | O programa inteiro |
| Heap | O que você pede com malloc, calloc, realloc | Você: malloc cria, free apaga | Até o free (ou até o programa acabar) |
| Pilha | Variáveis locais e parâmetros | Automático: a função começa, elas nascem; a função termina, elas somem | Enquanto a função está rodando |
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 segunda | main: a = 1 | primeira: b = 2 | segunda: c = 3 ← topo |
segunda e primeira terminaram | main: a = 1 ← topo | (livre) | (livre) |
main chama segunda de novo | main: a = 1 | segunda: c = 3 ← topo | (livre) |
É 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
-Wallpegou: quandocriar_contadorterminar,contadornã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 oNULL). Assim o erro aparece logo, em vez de corromper a memória em silêncio. - Falha de segmentação
*c = 5tentou 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.
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 pilha | Como o heap resolve | Exemplo |
|---|---|---|
| É pequena (8 MB) | O heap pode usar quase toda a memória livre do computador | Os 3 milhões de notas |
| A variável morre quando a função termina | A memória do heap vive até o free, independentemente de qual função a criou | A função criar_contador |
| O tamanho de cada bandeja é decidido na compilação | O 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
mallocpode falhar (devolveNULL), então todo pedido precisa ser conferido. - Pedir e devolver memória do heap é mais lento que usar a pilha, porque o
mallocprecisa 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ção | Onde 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 termina | Heap (e quem recebe o ponteiro cuida do free) |
| Um valor que precisa existir o programa inteiro, com tamanho fixo | Variável global ou static (com moderação: globais deixam o código difícil de entender) |
| Um texto fixo que não muda | Literal com const char * |
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.
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ção | Região | Vive até | Valor inicial se você não der um |
|---|---|---|---|
int x; dentro de uma função | Pilha | O fim da função | Lixo |
| Parâmetros de função | Pilha | O fim da função | O valor passado |
int x; fora de qualquer função | Dados | O fim do programa | 0 |
static int x; dentro de uma função | Dados | O fim do programa | 0 |
"texto" | Literais (só leitura) | O fim do programa | As letras escritas |
malloc(n) | Heap | O free | Lixo (use calloc para zeros) |
ulimit -s | Mostra 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
malloce devolve comfree. - Globais e
staticvivem 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".