Pilha e heap: entendendo a memória do processo
1. Visão Geral da Memória de um Processo em C
Quando um programa em C é executado, o sistema operacional cria um espaço de endereçamento virtual dividido em segmentos distintos. Cada segmento tem uma finalidade específica e regras de acesso próprias. Os principais segmentos são:
- Segmento de texto (code): contém o código binário executável, geralmente em região somente leitura
- Segmento de dados: armazena variáveis globais e estáticas inicializadas
- Segmento BSS: contém variáveis globais e estáticas não inicializadas (zeradas pelo sistema)
- Pilha (stack): gerencia chamadas de função e variáveis locais
- Heap: região para alocação dinâmica de memória
O endereçamento virtual permite que cada processo tenha a ilusão de possuir um espaço contíguo de memória, isolado de outros processos. O sistema operacional, com auxílio da MMU (Memory Management Unit), traduz endereços virtuais para físicos.
Exemplo de mapa de memória de um programa simples:
#include <stdio.h>
#include <stdlib.h>
int global_inicializada = 42; // Segmento de dados
int global_nao_inicializada; // Segmento BSS
void funcao() {
int local = 10; // Pilha
printf("Endereco de local: %p\n", &local);
}
int main() {
int *ptr = malloc(sizeof(int)); // Heap
*ptr = 100;
printf("Texto (codigo): %p\n", (void*)main);
printf("Dados: %p\n", (void*)&global_inicializada);
printf("BSS: %p\n", (void*)&global_nao_inicializada);
printf("Heap: %p\n", (void*)ptr);
funcao();
printf("Pilha (main): %p\n", &ptr);
free(ptr);
return 0;
}
2. A Pilha (Stack): Alocação Automática e Controlada
A pilha opera no modelo LIFO (Last In, First Out). Cada chamada de função cria um stack frame que contém:
- Endereço de retorno
- Parâmetros da função
- Variáveis locais
- Estado dos registradores (quando necessário)
O ponteiro de pilha (SP - Stack Pointer) mantém o topo da pilha. Quando uma função é chamada, o SP decrementa; quando retorna, o SP incrementa, liberando automaticamente o espaço.
Exemplo de rastreamento de chamadas:
#include <stdio.h>
void funcao_a() {
int a = 10;
printf("funcao_a: a = %d, endereco = %p\n", a, (void*)&a);
}
void funcao_b() {
int b = 20;
printf("funcao_b: b = %d, endereco = %p\n", b, (void*)&b);
funcao_a();
}
int main() {
int m = 30;
printf("main: m = %d, endereco = %p\n", m, (void*)&m);
funcao_b();
return 0;
}
A saída mostrará endereços decrescentes, confirmando que cada novo frame é alocado em posição inferior na pilha.
3. A Pilha: Limitações e Comportamento
A pilha tem tamanho fixo (tipicamente 1MB a 8MB em sistemas modernos). Quando esse limite é excedido, ocorre stack overflow, geralmente resultando em falha de segmentação (segmentation fault).
A recursão profunda é a causa mais comum de estouro de pilha:
#include <stdio.h>
int fatorial_recursivo(int n) {
int temp;
printf("Chamada fatorial(%d) - pilha em: %p\n", n, &temp);
if (n <= 1) return 1;
return n * fatorial_recursivo(n - 1);
}
int main() {
int n = 100000; // Valor grande propositalmente
int resultado = fatorial_recursivo(n);
printf("Resultado: %d\n", resultado);
return 0;
}
Para valores muito grandes de n, esse programa causará stack overflow. A cada chamada recursiva, um novo frame é alocado na pilha sem que os anteriores sejam liberados, consumindo todo o espaço disponível.
4. O Heap (Monte): Alocação Dinâmica e Flexível
O heap permite alocação e liberação manual de memória em tempo de execução. As funções principais são:
- malloc: aloca memória não inicializada
- calloc: aloca e inicializa com zeros
- realloc: redimensiona bloco alocado
- free: libera memória
Exemplo de alocação de array com tamanho determinado em execução:
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("Quantos elementos? ");
scanf("%d", &n);
// Alocação dinâmica
int *array = (int*)malloc(n * sizeof(int));
if (array == NULL) {
fprintf(stderr, "Falha na alocacao\n");
return 1;
}
// Inicialização
for (int i = 0; i < n; i++) {
array[i] = i * 10;
}
// Exibição
for (int i = 0; i < n; i++) {
printf("array[%d] = %d\n", i, array[i]);
}
// Liberação
free(array);
return 0;
}
Diferente da pilha, o heap permite que o programador controle exatamente quando alocar e liberar memória, mas isso traz responsabilidades adicionais.
5. Heap: Fragmentação, Vazamentos e Boas Práticas
Fragmentação interna: ocorre quando blocos alocados são maiores que o necessário (ex: alocar 100 bytes num bloco de 128 bytes, desperdiçando 28).
Fragmentação externa: ocorre quando a memória livre está espalhada em pequenos fragmentos, impossibilitando alocações grandes mesmo havendo espaço total suficiente.
Vazamentos de memória (memory leaks): ocorrem quando memória alocada não é liberada.
Exemplo com vazamento intencional:
#include <stdio.h>
#include <stdlib.h>
void funcao_com_vazamento() {
int *ptr = (int*)malloc(100 * sizeof(int));
// Esqueceu de chamar free(ptr)
// A memória permanece alocada até o fim do programa
}
int main() {
for (int i = 0; i < 1000; i++) {
funcao_com_vazamento();
}
printf("Memoria vazada!\n");
return 0;
}
Para detectar vazamentos, use ferramentas como Valgrind:
valgrind --leak-check=full ./programa
Versão corrigida:
#include <stdio.h>
#include <stdlib.h>
void funcao_sem_vazamento() {
int *ptr = (int*)malloc(100 * sizeof(int));
// Usa o ponteiro...
free(ptr); // Libera a memória
}
int main() {
for (int i = 0; i < 1000; i++) {
funcao_sem_vazamento();
}
printf("Sem vazamento!\n");
return 0;
}
6. Comparação Prática: Pilha vs. Heap
| Característica | Pilha | Heap |
|---|---|---|
| Velocidade | Muito rápida (simples incremento/decremento de SP) | Mais lenta (busca por bloco livre) |
| Gerenciamento | Automático (entrada/saída de função) | Manual (malloc/free) |
| Tamanho | Fixo e limitado | Limitado pela memória disponível |
| Escopo | Apenas dentro da função | Persiste até free() ou fim do programa |
| Fragmentação | Não ocorre | Pode ocorrer |
Quando usar cada um:
Use pilha para:
- Variáveis locais com tamanho conhecido em compilação
- Dados pequenos e temporários
- Quando velocidade é crítica
Use heap para:
- Estruturas de dados dinâmicas (listas, árvores)
- Arrays com tamanho determinado em execução
- Dados que precisam persistir além do escopo da função
- Objetos grandes que poderiam estourar a pilha
Exemplo prático de decisão:
#include <stdio.h>
#include <stdlib.h>
// Uso correto da pilha para dados pequenos e temporários
void processa_temporario() {
int buffer[100]; // 400 bytes na pilha - OK
// processa...
}
// Uso correto do heap para dados grandes ou de tamanho variável
void processa_dinamico(int tamanho) {
int *buffer = (int*)malloc(tamanho * sizeof(int));
if (buffer == NULL) {
fprintf(stderr, "Erro de alocacao\n");
return;
}
// processa...
free(buffer);
}
int main() {
processa_temporario();
processa_dinamico(1000000); // 4MB no heap
return 0;
}
7. Depuração de Problemas de Memória
Erros comuns e como identificá-los:
Uso após free (use-after-free):
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int*)malloc(sizeof(int));
*ptr = 42;
free(ptr);
*ptr = 100; // ERRO: uso após liberar
return 0;
}
Double free:
#include <stdlib.h>
int main() {
int *ptr = (int*)malloc(sizeof(int));
free(ptr);
free(ptr); // ERRO: double free
return 0;
}
Buffer overflow no heap:
#include <stdlib.h>
#include <string.h>
int main() {
char *buffer = (char*)malloc(10);
strcpy(buffer, "Esta string e muito longa para o buffer"); // ERRO
free(buffer);
return 0;
}
Ferramentas essenciais:
- GDB: permite inspecionar pilha de chamadas com
backtrace, examinar variáveis locais, e identificar segmentation faults - Valgrind: detecta vazamentos, uso após free, buffer overflows, e acesso a memória não inicializada
Exemplo de debugging com GDB:
gcc -g -o programa programa.c
gdb ./programa
(gdb) run
(gdb) backtrace # Mostra pilha de chamadas no crash
(gdb) info locals # Mostra variáveis locais
(gdb) print variavel # Examina valor de variável
Referências
- The C Programming Language (K&R) - Capítulo 5: Pointers and Arrays — Referência clássica sobre ponteiros e alocação dinâmica em C
- GNU C Library Manual: Memory Allocation — Documentação oficial das funções malloc, calloc, realloc e free
- Valgrind User Manual — Guia completo da ferramenta de detecção de vazamentos e erros de memória
- GDB Documentation: Stack Inspection — Como usar o GDB para inspecionar pilha de chamadas e depurar problemas
- Understanding Memory Layout of C Programs — Artigo técnico detalhado sobre segmentos de memória em programas C
- Stack vs Heap: Know the Differences — Tutorial prático comparando alocação na pilha e no heap em C