No post anterior, vimos o que é e para que serve a stack (e o que causa a StackOverflowException). Nesse post, vamos tratar de outros dois conceitos fundamentais: ponteiros e a heap.
O que são ponteiros
Ponteiros são variáveis que armazenam endereços de memória. Eles foram introduzidos, em linguagens de alto padrão, na antiga PL/I com o propósito de permitir a construção de listas (listas encadeadas, para ser mais preciso).
O tamanho de um ponteiro está associado a quantidade de bytes necessários para representar um endereço de memória. Modernamente, eles geralmente são de 32 bits ou 64 bits.
Ponteiros são pequenos o suficiente para ficarem armazenados em registradores da CPU, ou mesmo em variáveis locais, na stack.
Ponteiros possuem uma aritmética própria. A ideia é permitir o acesso a posições de memória adjacentes através de operações de adição e subtração.
O código abaixo utiliza ponteiros e aritimética de ponteiros:
public static unsafe string UnsafeReverseString(string input) { var result = new string(' ', input.Length); fixed (char* source = input) fixed (char* dest = result) { for ( int i = input.Length - 1, destIndex = 0; i >= 0; i--, destIndex++ ) { dest[destIndex] = source[i]; } } return result; }
Em C# (e Java), ponteiros não estão, explicitamente, disponíveis por padrão. Sua utilização fica restrita a códigos marcados como unsafe.
O que é a Heap (ou free store)
O heap é a área de memória onde ficam os objetos que alocamos dinamicamente em nossos programas. Ou seja, objetos com localização e tamanho variável e/ou não previstos durante processo de compilação.
Enquanto a Stack é uma estrutura organizada, o Heap (não confundir com a estrutura de dados) pode ser uma “bagunça” (não é o caso de .NET).
Como a localização de objetos na heap é variável, o acesso geralmente acontece através de um ponteiro (implicitamente, em .NET).
Quando um objeto é criado na heap, dizemos que estamos alocando memória. Quando um objeto é removido da heap, dizemos que estamos desalocando memória.
Em C++, o programador determina o momento da alocação e desalocação de objetos. Em C#, o programador determina apenas o momento da alocação, ficando a desalocação sob responsabilidade do Garbage Collector (a menos que se esteja utilizando um heap não-gerenciado).
O Garbage Collector é o artefato de software responsável por fazer a desalocação de objetos na heap, em .NET. Falaremos mais sobre ele em posts futuros.
Comparando a heap e a stack
Em nossos programas utilizamos invariavelmente a stack ou uma heap (geralmente gerenciada – tema para outro post).
Todos os valores que estão alocados na stack são automaticamente desalocados quando seu stack frame é removido (saída de contexto). Os objetos alocados na heap são desalocados de forma não-determinística, pelo GC, ou de forma determinística e não automática caso utilizemos heaps não-gerenciados.
Os objetos alocados na stack só podem ser utilizados na thread em que o método/função foi acionado. Os objetos alocados na heap podem ser acionados por qualquer código com um ponteiro para sua posição na memória.
O acesso de objetos na stack é mais rápido.
O volume de dados que pode ser armazenado na stack é limitado (cuidado com Stackoverflow!). O volume de dados alocados na heap está associado a quantidade de memória disponível, incluindo armazenamento temporário em disco.
Fechando … por enquanto
A heap é fundamental para nossos sistemas. Entretanto, em nossos clientes, a gestão ruim da heap é a terceira causa mais frequente de performance pobre.
Em posts futuros iremos falar mais sobre como a gestão da heap ocorre em .NET e em outras tecnologias.
No .NET, quando há passagem por referencia de tipos primitivos (no c# marcados com ref ou até mesmo out), como que funcionaria esse processo de controle dessas variáveis?