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

Exercícios

  1. 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 com free.
  2. 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; ou static int z = 0;. Essas variáveis ocupam espaço apenas na memória durante a execução, não no arquivo executável.
  3. 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:
    #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;
    }
    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).
  4. 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 com malloc.
  5. 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: a está no segmento de dados (inicializada com 10). b está no BSS (não inicializada, será zero). c está na pilha (variável local automática).