Memória é um conceito razoavelmente complexo. Em primeiro lugar, não existe uma única memória, mas uma hierarquia de várias camadas; desde registradores do processador, cache L1, L2 e L3, memória primária, memória secundária e, às vezes, até memória terciária. A gestão destes recursos é de responsabilidade do SO, e seu funcionamento é transparente, bastando ao programador fazer chamadas a uma API.
Seu programa usa a memória primária para armazenar dados em runtime, mas não acessa diretamente o endereçamento real da máquina, mas um espaço virtual segmentado, que isola as aplicações, e que a depender da carga de trabalho do sistema, pode sofrer paginação. O sistema operacional escalona tempo e recursos de forma que todas as aplicações "pensem" ter disponibilidade total da máquina a todo momento.
De forma resumida e simplificada, podemos entender a memória de um programa de forma análoga a um grande array de bytes, em que o endereçamento é composto de 2 áreas principais: o espaço de usuário, e de kernel. O espaço de usuário por sua vez é subdividido em 4 outras áreas, respectivamente ordenadas do endereço mais alto para o mais baixo.
- stack: Uma estrutura LIFO (last in, first out), que comporta duas operação básicas, push e pop. É onde dados necessários à chamada de funções, tais como contexto de retorno, argumentos e variáveis locais, são armazenados. Possui um tamanho pré-definido, mas que depende de arquitetura. Quando não se faz uso de otimizações para eliminação de chamadas em cauda, e se excede a profundidade permitida pelo espaço de memória, tem-se um estouro de pilha, o que leva à finalização do programa;
- heap: Começa imediatamente antes da área de dados, e cresce em direção à stack. É onde se obtém memória dinamicamente alocada, destinada a variáveis cuja existência e, ou, tamanho não pode ser determinada em tempo de compilação;
- dados: Variáveis estáticas, isto é, cuja existência é conhecida em tempo de compilação e o tempo de vida equivale ao do programa. Tem tamanho fixo pré-determinado, seu conteúdo em geral pode ser alterado;
- texto: Não só os dados são armazenados em memória, mas também as instruções, funções e procedimentos, justamente por isso podem ser referenciadas por ponteiros. Contudo, diferentemente do segmento de dados, a área de texto é apenas de leitura, e uma tentativa de escrita acarreta a finalização do programa.
Quando se trabalha a nível de linguagem de máquina, ou mesmo em linguagens de nível intermediário como C, outra consideração importante é o alinhamento de memória, ou seja, a forma como você vai organizar os dados dentro da memória para simplificar o acesso via aritmética de endereços. As instruções do processador operam com blocos de dados de tamanhos padronizados (normalmente 1, 2, 4 e 8 bytes).
Sempre que possível, opte por usar um buffer estático, contudo, atenção ao risco de um estouro de buffer! O uso de memória dinâmica é custoso, especialmente para aplicações de longa duração de uptime, devido à fragmentação da memória. Além disso, impõe a disciplina de liberar o recurso após o uso, do contrário acarreta em vazamentos. No entanto, nem sempre é possível evitar seu uso. Deve ser usada em estruturas de dados dinâmicas em memória primária (como listas e árvores binárias), simulação de comportamento orientado a objetos e eventos de input de tamanho variável que não possam ser paginados e, ou, reduzidos (isto é, consolidados a um valor final por meio de uma operação de agregação acumuladora).