Modelo de memória
Esta aula explora o modelo de memória da linguagem C, detalhando a organização da memória em segmentos (código, dados, BSS, heap e stack), as diferenças entre alocação estática e dinâmica, e como os endereços são gerenciados. O aluno aprenderá a visualizar a memória de um programa em execução, essencial para depuração e otimização.
Nesta aula, vamos mergulhar no modelo de memória de programas escritos em C. Compreender como a memória é organizada e gerenciada é fundamental para escrever código eficiente, evitar vazamentos de memória e entender o comportamento de ponteiros e alocações dinâmicas.
O modelo de memória de um processo em C é dividido em segmentos: texto (código), dados, BSS, heap e pilha (stack). Cada segmento tem uma finalidade específica e regras de acesso. Vamos explorar cada um deles, entender as diferenças entre alocação estática e dinâmica, e ver como os endereços são organizados na memória virtual.
Stack vs heap
A pilha (stack) e o heap são as duas principais áreas de memória usadas durante a execução de um programa. A pilha é usada para alocação automática de variáveis locais, parâmetros de função e endereços de retorno. Ela cresce para baixo (endereços decrescentes) e é gerenciada automaticamente pelo compilador. A alocação na pilha é muito rápida, mas tem tamanho limitado (tipicamente alguns MB).
O heap, por outro lado, é usado para alocação dinâmica de memória, controlada pelo programador através de funções como malloc, calloc, realloc e free. Ele cresce para cima (endereços crescentes) e é muito maior que a pilha. A alocação no heap é mais lenta e requer gerenciamento explícito para evitar vazamentos de memória.
#include <stdlib.h>
#include <stdio.h>
void funcao() {
int local = 10; // alocado na pilha
int *ptr = malloc(sizeof(int)); // alocado no heap
if (ptr) {
*ptr = 20;
printf("local: %d, heap: %d\n", local, *ptr);
free(ptr);
}
}
int main() {
funcao();
return 0;
}No exemplo acima, a variável local é alocada na pilha, enquanto o ponteiro ptr aponta para uma região de memória no heap. Note que é necessário liberar a memória do heap com free.
Segmentos (código, dados, BSS)
Além da pilha e do heap, o programa é dividido em segmentos estáticos que são carregados na memória quando o processo é iniciado. O segmento de código (ou texto) contém as instruções do programa compilado. Normalmente é somente leitura e tem tamanho fixo. O segmento de dados contém variáveis globais e estáticas inicializadas com valor diferente de zero. O segmento BSS (Block Started by Symbol) contém variáveis globais e estáticas não inicializadas ou inicializadas com zero. Essas variáveis ocupam espaço somente na descrição do programa, mas não ocupam espaço no arquivo executável; elas são preenchidas com zeros na carga.
int global_inic = 5; // segmento de dados
int global_nao_inic; // segmento BSS (será zero)
static int estatica = 0; // BSS (inicializada com zero)
int main() {
static int local_estatica = 10; // segmento de dados
return 0;
}O layout típico da memória de um processo (endereços baixos para cima) é: segmento de código, segmento de dados, segmento BSS, heap (cresce para cima), pilha (cresce para baixo). As variáveis globais e estáticas têm endereços fixos durante toda a execução.
Endereços
Em C, podemos obter o endereço de variáveis usando o operador &. Os endereços são valores numéricos que representam posições na memória virtual do processo. É importante notar que endereços de variáveis na pilha são tipicamente altos (próximos ao topo da memória virtual), enquanto endereços de variáveis globais e estáticas são mais baixos. O heap ocupa endereços intermediários, entre BSS e a pilha.
#include <stdio.h>
int global = 1;
int nao_inic;
int main() {
int local = 2;
int *heap = malloc(sizeof(int));
printf("&global: %p\n", (void*)&global);
printf("&nao_inic: %p\n", (void*)&nao_inic);
printf("&local: %p\n", (void*)&local);
printf("heap: %p\n", (void*)heap);
printf("main: %p\n", (void*)main);
free(heap);
return 0;
}Observe que usamos o especificador de formato %p para imprimir endereços. Os endereços são mostrados em hexadecimal. A função main está no segmento de código, então seu endereço é baixo. Variável global está no segmento de dados, nao_inic no BSS, local na pilha e heap aponta para o heap.
Visão geral
O modelo de memória da linguagem C é uma abstração que ajuda a entender como os dados são armazenados e gerenciados. A pilha é ideal para variáveis temporárias de curta duração, enquanto o heap permite alocação dinâmica de tamanho variável. Os segmentos estáticos garantem que variáveis globais e estáticas tenham endereços fixos. Conhecer esse modelo é crucial para evitar erros comuns como estouro de pilha, vazamentos de memória e acesso a memória já liberada.
Boas práticas: sempre inicializar variáveis, especialmente as globais não inicializadas (que vão para BSS e são zero). Em programas com alocação dinâmica, pareie cada malloc com um free e evite acessar memória depois de liberada. Para funções recursivas profundas, considere aumentar o tamanho da pilha ou usar alocação no heap.
Referências
- cppreference: Memory model (C)
- GNU C Library: Memory Concepts
- Stack Overflow: Memory layout of a C program
- Wikipedia: Data segment
- IBM Documentation: Layout of a program in memory
Exercícios
Explique a diferença entre alocação na pilha e no heap em termos de gerenciamento de memória e desempenho.
✓ Resposta: A alocação na pilha é automática e muito rápida, mas o tamanho é limitado e a memória é liberada automaticamente quando a função retorna. A alocação no heap é controlada pelo programador, mais lenta, mas permite tamanhos variáveis e persistência além do escopo da função; requer liberação explícita comfree.O que são os segmentos de dados e BSS? Dê exemplos de variáveis que vão em cada um.
✓ Resposta: O segmento de dados contém variáveis globais e estáticas inicializadas com valor diferente de zero. Exemplo:int x = 5;. O segmento BSS contém variáveis globais e estáticas não inicializadas ou inicializadas com zero. Exemplo:int y;oustatic int z = 0;. Essas variáveis ocupam espaço apenas na memória durante a execução, não no arquivo executável.Escreva um programa que imprima os endereços de uma variável global, uma variável local e uma variável alocada no heap. O que você observa sobre a ordem dos endereços?
✓ Resposta:
Geralmente, o endereço global é o mais baixo, o heap vem em seguida (crescente), e o local é o mais alto (próximo ao topo da pilha).#include <stdio.h> #include <stdlib.h> int global = 1; int main() { int local = 2; int *heap = malloc(sizeof(int)); printf("Global: %p\n", (void*)&global); printf("Local: %p\n", (void*)&local); printf("Heap: %p\n", (void*)heap); free(heap); return 0; }O que acontece se você tentar alocar uma grande quantidade de memória na pilha (por exemplo, um array de 10 milhões de inteiros)? Explique.
✓ Resposta: Isso provavelmente causará um estouro de pilha (stack overflow), porque a pilha tem tamanho limitado (tipicamente 1-8 MB). O programa pode travar ou apresentar comportamento indefinido. Para grandes alocações, use o heap commalloc.Dado o código a seguir, identifique em qual segmento cada variável está alocada:
int a = 10;(global),static int b;(global),int c;(local dentro de main).✓ Resposta:aestá no segmento de dados (inicializada com 10).bestá no BSS (não inicializada, será zero).cestá na pilha (variável local automática).