[Estudos de C] Heap e Stack

Dois termos, frequentes, referentes à memória quando estamos estudando a linguagem C são: heap e stack.

Junto a eles temos a alocação de memória ou alocação dinâmica de memória que são assuntos um pouco abstratos e fazem-nos gastar alguns bons pares de neurônios para compreendê-los.

Neste escrito busco escrever algumas notas sobre o que é, a diferença e como é a utilização da heap e stack enquanto estamos programando linguagem C.

Em um primeiro memento, é preciso compreender o que é alocação estática e o que é alocação dinâmica de memória.

Alocação estática e dinâmica

Alocação estática

Quando um programa é executado, as declarações de variáveis de tamanhos fixos já são alocadas, em memória, previamente.

Exemplo:

int    main(void)
{
            int i;
            char ft[2];

            i = 42;
            ft[0] = "2";
            ft[1] = "A";

            return (0);
}

Essas variáveis do exemplo acima tem as suas necessidades de memória alocadas (reservadas) antes da execução do programa. Aliás, não se espera que elas requeiram mais memória. Caso isso ocorra, um erro ocorrer-se-á.

A memória alocada é persistida por enquanto durar o ciclo de execução do programa. Essa forma de alocar ou reservar memória é chamada de alocação estática.

Estática: por que o espaço definido inicialmente não será alterado durante a execução do programa.

Essa alocação de memória acontece na stack ou pilha. Explicarei mais na frente o que é a stack.

Do mesmo modo, quando declaramos uma variável em uma função; o espaço necessário é alocado, também, na pilha (The art and Science of C), bem como os argumentos passados a ela.

Um detalhe importante é que uma stack é Last In First Out (LIFO). O último dado ou função posicionado na pilha é o primeiro a ser retirado do fluxo de execução.

Alocação dinâmica

A alocação dinâmica é o meio pelo qual um programa pode obter memória em tempo de execução. Pois nem variáveis globais, nem locais podem ser acrescentadas durante o tempo de execução (SCHILDT, 1996).

Quando ocorre uma situação que requer a alocação de memória em tempo de execução, tal necessidade decorre pela natureza variável do fluxo de execução do programa, essa será obtida da heap. Qual é uma região da memória que está entre o seu programa e a área de armazenamento permanente da pilha (SCHILDT, 1996).

Um exemplo, simples, dessa situação é a criação de vetor em função do tamanho dado pelo usuário.

Na linguagem C, a alocação dinâmica de memória é realizada pelas funções fornecidas pela biblioteca stdlib.h. ****Tratarei delas em outro momento, mas, por ora se, pode citar as mais conhecidas: malloc, free, calloc, realloc.``

Aqui, diferentemente da alocação estática que faz uso da stack, na alocação dinâmica faz uso da heap.

Por fim, para diversão: https://drawings.jvns.ca/malloc/ .

Stack e Heap: anotações introdutórias

Quando um programa é carregado na memória, ocorre uma organização do local alocado em memória, para o programa, em áreas chamadas de segmentos, conforme ilustração abaixo:

High Addresses ---> .----------------------.
                    |      Environment     |
                    |----------------------|
                    |                      |   Functions and variable are declared
                    |         STACK        |   on the stack.
base pointer ->     | - - - - - - - - - - -|
                    |           |          |
                    |           v          |
                    :                      :
                    .                      .   The stack grows down into unused space
                    .         Empty        .   while the heap grows up. 
                    .                      .
                    .                      .   (other memory maps do occur here, such 
                    .                      .    as dynamic libraries, and different memory
                    :                      :    allocate)
                    |           ^          |
                    |           |          |
 brk point ->       | - - - - - - - - - - -|   Dynamic memory is declared on the heap
                    |          HEAP        |
                    |                      |
                    |----------------------|
                    |          BSS         |   Uninitialized data (BSS)
                    |----------------------|   
                    |          Data        |   Initialized data (DS)
                    |----------------------|
                    |          Text        |   Binary code
Low Addresses ----> '----------------------'

Fonte: https://aticleworld.com/memory-layout-of-c-program/

Explicando cada segmento (dos segmentos de baixo para os de cima):

Text: O segmento de texto ou segmento de código, contém os códigos responsáveis pela execução do programa. Em regra, este segmento é de apenas leitura e de tamanho fixo. Em outras palavras, contém os binários do programa compilado.

Data (Initialized Data Segment): Neste segmento contém as variáveis estáticas e globais inicializadas explicitamente. Os seus valores não são todos estáticos, podendo ser alterados em tempo de execução.

O seu tamanho é definido pelo tamanho dos valores definidos no código-fonte do programa e não é alterado em tempo de execução.

Exemplo de código:

int ft_1 = 10 ; // variável global salva no segmento de data

int main(void)
{
    static int ft_2 = 3;  // variável estática salva no segmento de data

    return (0);
}

BSS (Uninitialized data segment): Neste segmento contém as variáveis globais e estáticas não inicializadas. Todas a variáveis localizadas neste segmento são inicializadas por zero e ponteiros com NULL.

Exemplo de código:

int ft_1; // variável global salva no segmento de data

int main(void)
{
    static int ft_2;  // variável estática salva no segmento de data

    return (0);
}

HEAP: Quando uma quantidade de memória é alocada em tempo de execução, ela é alocada na heap. Na linguagem C, é feito por meio das funções calloc() e malloc().

STACK: Este segmento de memória é utilizada pelas variáveis locais e os argumentos das funções e as chamadas de execuções delas.

Utilitário size

Podemos conhecer o tamanho de cada segmento relativo a um executável por meio do utilitário de linha comando size. Segundo a sua documentação, ele é descrito como:

The GNU size utility lists the section sizes and the total size for each of the binary files objfile on its argument list. By default, one line of output is generated for each file or each module if the file is an archive.

Exemplo de sua utilização:

Stack

Quando um programa inicia a sua execução, uma secção de memória contínua é reservada para ele. Esse segmento é chamado de stack.

A stack ou pilha é um segmento de memória no qual são armazenados as variáveis locais e os dados das funções invocadas, em ordem de cada inicialização ou chamada, durante a execução do programa.

At a minimum, a thread’s stack is used to store the location of function calls in order to allow return statements to return to the correct location, but programmers may further choose to explicitly use the stack.

Fonte: https://cs.gmu.edu/~zduric/cs262/Slides/teoX.pdf

O seu tamanho é definido pelo sistema operacional quando o programa inicia a sua execução. A alocação de memória inicia no limite superior da stack e termina no seu limite inferior. Em outras palavras, começa-se a utilizar os endereços de memória mais altos até os endereços menores no âmbito do espaço de memória disponibilizado ao programa, pelo SO.

Sua estrutura de dados é do tipo LIFO (Last In First Out): o último elemento adicionado é o primeiro a ser retirado. Nesse contexto, os dados na pilha são estruturados e organizados. O que permite que os dados nela presente sejam armazenados e acessados de forma eficiente.

Fonte: https://open4tech.com/stacks-vs-queues/

Os dados são adicionados e retirados conforme o fluxo de execução do programa. No exemplo da imagem abaixo, as chamadas recursivas da função fibonacci são empilhadas conforme cada chamada e desempilhadas após os respectivos retornos.

Por fim, os dados alocados na pilha não podem ter os seus tamanhos redimensionados. Bem como são descartados ao fim da execução do programa. O programador não precisa liberar manualmente a memória alocada.

Heap

A heap é uma secção de memória utilizada para a alocação dinâmica, conforme for necessário. Sua estrutura de dados é linked list. O que faz com que o acesso para leitura e escrita seja mais lento, quando comparado à stack. Essa forma de organizar os dados permite que a manipulação da memória seja aleatória, e não sequencial conforme realizada na pilha.

Fonte: https://www.geeksforgeeks.org/heap-data-structure/

Para sua utilização, utilizamos as funções calloc() ou malloc() da biblioteca stdlib.h para alocar quantidades de memória necessária à execução do programa.

Neste segmento, as variáveis podem ter os seus tamanhos redimensionados por meio da função realloc(). O que é diferente da stack, cujo tamanho das variáveis são fixos e não podem serem redimensionados.

Para acessar o valor de algum dado presente na heap é necessário que um ponteiro aponte para esse dado. Diferente de stack no qual se pode acessar os dados das variáveis diretamente, neste segmento de memória não é possível. Por isso utilizarmos ponteiros.

A imagem abaixo exemplifica. Temos um ponteiro para uma cadeia de caracteres que aponta para o bloco de memória na heap onde os caracteres estão armazenados.

Fonte: https://craftofcoding.wordpress.com/2015/12/07/memory-in-c-the-stack-the-heap-and-static/

Outra imagem exemplificativa:

Fonte: https://vikashazrati.wordpress.com/2007/10/01/quicktip-java-basics-stack-and-heap/

Os dados alocados na heap não são liberados automaticamente ao fim da execução do programa. A memória utilizada requer que seja liberada manualmente. Para esse propósito é utilizada a função free() da biblioteca <stdlib.h>.

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

int     main(void)
{
    int     *ptr;

    ptr = (int*)malloc(sizeof(int));
    if (ptr == NULL)
        return (-1);

    *ptr = 42;
    printf("%d\\n", *ptr);
    free(ptr);

    return (0);
}

A quantidade de memória disponibilizada na heap não é fixa como para a stack. Ela tem um limite estipulado por um ponteiro chamado brk, que pode ser movimentado, caso for necessária mais memória, a função malloc() poderá movimentá-lo.

Heap memory space is limited by a pointer called “brk”. If your program accesses past the ‘brk’, it’ll be aborted. In order to malloc() more memory, simply shift ‘brk’ pointer away.

Fonte: https://www.sanfoundry.com/c-tutorials-different-segments-program-have/

Observações finais

Foram breves notas. Temos muitos outros assuntos que poderiam ser falados neste contexto. Porém, decidi escrever sobre eles em outro texto.

Tenho, também, as seguintes recomendações de leitura:

  1. https://courses.engr.illinois.edu/cs225/fa2022/resources/stack-heap/

  2. https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap

Referências

SCHILDT, H. C Completo e Total, Terceira Edição Revisada e Atualizada. 1996.

https://en.wikipedia.org/wiki/Data_segment

https://www.geeksforgeeks.org/memory-layout-of-c-program/

https://aticleworld.com/memory-layout-of-c-program/

https://cs.gmu.edu/~zduric/cs262/Slides/teoX.pdf

https://cs-fundamentals.com/tech-interview/c/difference-between-stack-and-heap-memory-segments

https://craftofcoding.wordpress.com/2015/12/07/memory-in-c-the-stack-the-heap-and-static/

https://www.sanfoundry.com/c-tutorials-different-segments-program-have/

https://web.archive.org/web/20130225162302/http://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Mips/stack.html

https://open4tech.com/stacks-vs-queues/

https://vikashazrati.wordpress.com/2007/10/01/quicktip-java-basics-stack-and-heap/