Nesta aula, vamos mergulhar no modelo de memória dos programas escritos em C. Entender como a memória é organizada é crucial para escrever código eficiente, evitar vazamentos e compreender conceitos como alocação dinâmica, ponteiros e escopo de variáveis. A memória de um processo em C é dividida em regiões lógicas, cada uma com um propósito específico.

Vamos explorar as principais áreas: a pilha (stack), o monte (heap), e os segmentos estáticos (código, dados e BSS). Além disso, veremos como endereços de memória são representados e acessados. Ao final, você terá uma visão clara de onde suas variáveis e funções residem durante a execução.

Stack vs heap

A stack (pilha) é uma região de memória usada para armazenar variáveis locais, parâmetros de funções e endereços de retorno. Ela cresce para baixo (endereços decrescentes) e é gerenciada automaticamente pelo compilador: cada chamada de função aloca um novo quadro (frame) na pilha, que é liberado quando a função retorna. A stack é rápida e limitada em tamanho (tipicamente alguns MB), e o estouro dela causa o famoso stack overflow.

O heap (monte) é uma região de memória de alocação dinâmica, controlada pelo programador através de funções como malloc, calloc, realloc e free. O heap cresce para cima (endereços crescentes) e é mais lento que a stack, mas oferece flexibilidade para alocar memória em tempo de execução com tamanho variável. A memória alocada no heap persiste até ser explicitamente liberada, ou até o término do programa. Vazamentos de memória ocorrem quando o programador esquece de chamar free.

#include <stdlib.h>

void funcao() {
    int a = 10;           // alocado na stack
    int *p = malloc(sizeof(int)); // alocado no heap
    *p = 20;
    free(p);              // libera heap
}

int main() {
    funcao();
    return 0;
}

Segmentos (código, dados, BSS)

Um programa compilado em C é organizado em segmentos na memória. O segmento de código (text) contém as instruções executáveis do programa. Geralmente é somente leitura para evitar modificações acidentais. O segmento de dados (data) armazena variáveis globais e estáticas inicializadas com valores diferentes 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 são preenchidas com zeros antes da execução.

Além disso, há o segmento de constantes (rodata) que guarda literais como strings constantes. A organização típica de memória (endereços crescentes) é: segmento de código, segmento de dados, segmento BSS, heap (crescendo para cima), e stack (crescendo para baixo). Isso ajuda a evitar colisões entre heap e stack.

int global_inicializado = 42;    // segmento de dados
int global_nao_inicializado;     // segmento BSS

static int estatica_inicializada = 10; // dados
static int estatica_nao_inicializada;  // BSS

int main() {
    static int local_estatica = 5; // dados (inicializada uma vez)
    return 0;
}

Endereços

Endereços de memória são números que identificam localizações na memória RAM. Em C, você pode obter o endereço de uma variável usando o operador &. A impressão de endereços geralmente é feita com %p no printf. Os endereços são tipicamente hexadecimais e variam conforme a arquitetura e o sistema operacional.

Observação importante: endereços de variáveis na stack tendem a ser altos (próximos ao topo da memória virtual), enquanto endereços no heap são mais baixos. Segmentos de código e dados têm endereços fixos determinados pelo linker. A seguir, um exemplo que mostra endereços de diferentes regiões:

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

int global_data = 100;
int global_bss;

int main() {
    int local_stack = 1;
    int *heap_ptr = malloc(sizeof(int));
    static int static_data = 200;
    static int static_bss;

    printf("Codigo (funcao main): %p\n", main);
    printf("Dados globais: %p\n", &global_data);
    printf("BSS global: %p\n", &global_bss);
    printf("Stack local: %p\n", &local_stack);
    printf("Heap: %p\n", heap_ptr);
    printf("Dados estaticos: %p\n", &static_data);
    printf("BSS estatico: %p\n", &static_bss);

    free(heap_ptr);
    return 0;
}

Visão geral

A organização da memória de um programa C pode ser visualizada como um diagrama de segmentos. Do endereço mais baixo para o mais alto: segmento de código (text), segmento de dados inicializados (data), segmento BSS, heap (crescendo para cima), e stack (crescendo para baixo). O heap e a stack podem crescer um em direção ao outro, mas o sistema operacional impede que se encontrem.

Entender essa estrutura ajuda a depurar problemas como estouro de pilha, vazamentos de memória e corrupção de dados. Além disso, explica porque variáveis locais não persistem após a função retornar (são destruídas com o frame da stack), enquanto variáveis alocadas com malloc permanecem até free.

// Representação conceitual (não executável)
// Endereços baixos
// +------------------+
// | Código (text)    |
// +------------------+
// | Dados (data)     |
// +------------------+
// | BSS              |
// +------------------+
// | Heap             |
// |   (cresce p/ cima)|
// +------------------+
// | ...              |
// +------------------+
// | Stack            |
// |   (cresce p/ baixo)|
// +------------------+
// Endereços altos

Boas práticas

Sempre inicialize variáveis globais e estáticas explicitamente para evitar comportamentos indefinidos (embora o BSS garanta zero, é boa prática). Use free para toda memória alocada com malloc e evite acessar memória já liberada. Prefira alocação na stack quando possível, por ser mais rápida e segura. Para alocações grandes ou de tamanho dinâmico, use o heap.

Referências

Exercícios

  1. Explique a diferença entre stack e heap em termos de gerenciamento e velocidade.

    ✓ Resposta: A stack é gerenciada automaticamente pelo compilador, é rápida e de tamanho limitado. O heap é gerenciado manualmente pelo programador (malloc/free), é mais lento e permite alocação dinâmica de tamanho variável.
  2. O que acontece se você tentar alocar uma variável local muito grande na stack?

    ✓ Resposta: Ocorre um estouro de pilha (stack overflow), que geralmente causa um erro de segmentação (segfault) ou terminação do programa.
  3. Em qual segmento da memória são armazenadas variáveis globais não inicializadas?

    ✓ Resposta: No segmento BSS (Block Started by Symbol).
  4. Escreva um programa que imprima o endereço de uma variável local, uma global e uma alocada no heap.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int global = 0;
    
    int main() {
        int local = 0;
        int *heap = malloc(sizeof(int));
        printf("Local: %p\n", &local);
        printf("Global: %p\n", &global);
        printf("Heap: %p\n", heap);
        free(heap);
        return 0;
    }
  5. Por que o segmento de código é geralmente somente leitura?

    ✓ Resposta: Para evitar que o programa modifique suas próprias instruções acidentalmente ou maliciosamente, garantindo segurança e estabilidade.