Entendendo Arrays

Para entender o array de forma precisa, é interessante compreender um pouco sobre memória, tipos de dados e endereçamento, mesmo que de forma superficial. Digo isso porque o array é uma das formas mais básicas de dados estruturados. Podemos considerar o array uma base para vários tipos de estruturas de dados mais complexas, como por exemplo Vetor Dinâmico, Fila circular, pilha e por aí vai. Então eu resolvi criar esse artigo para explicar o array de uma forma que, pelo menos para mim, ajudou para que eu compreendesse de vez, e não esquecesse mais. Espero que também ajude quem está aprendendo agora.

Array por baixo do capô

Vamos resumir o array da forma que mais lemos e escutamos por aí só para ter a base do raciocínio.

O array é uma sequência de elementos, todos do mesmo tipo armazenados de forma contígua na memória. Cada elemento do array é acessível por um índice, que geralmente começa em 0. Cada índice em um array é uma referência (ou ponteiro) para uma posição específica na memória onde o valor correspondente está armazenado. O array tem um tamanho fixo, que não pode ser alterado.

Maravilha! Agora você sabe que o array é uma sequência de elementos do mesmo tipo, que você pode acessar esses elementos pelo índice, cada índice faz uma referência (aponta) para o endereço de memória onde o elemento (a informação) está guardada e que o tamanho é fixo.

Mas, por que somente elementos do mesmo tipo? E por que o tamanho de um array não pode ser dinâmico? (obs. estamos falando de arrays homogêneos aqui, e não as listas de python e JavaScript que aceitam tipos diferentes, esses são Heterogêneos).

Vamos entender isso:

Tipos de dados: os dados são uma representação da informação. Cada tipo de dado serve para representar uma informação específica e ocupa uma quantidade de memória necessária para isso.

Por exemplo:

Inteiros e números de ponto flutuante possuem tamanhos diferentes porque representam informações distintas, isso exige alocações de memória específicas para cada tipo.

Um inteiro em Java, por exemplo, ocupa um espaço de 32 bits para ser representado, pode conter valores de -2.147.483.648 a 2.147.483.647.

Já um double ocupa um espaço de 64 bits para ser representado, pode conter valores de ponto flutuante de IEEE 754 ±4,94065645841246544e-324 a 81,79769313486231570e+308.

A memória é organizada em células, elas contém um endereço único que serve para localizar os dados armazenados naquele espaço específico

Por isso quando criamos um array precisamos informar qual será o tipo de dado e a quantidade a ser armazenada. Isso permite que o sistema aloque espaço fixo suficiente na memória para guardar esses dados. O sistema calcula esse espaço multiplicando o tamanho do tipo de dado pela quantidade de elementos. Por exemplo, um array de inteiros com 10 elementos ocupará um espaço de memória ao tamanho de um inteiro multiplicado por 10.

No exemplo com Java seria 32 bits (4 bytes) x 10 = 40 bytes. Essa alocação fixa torna o uso eficiente, pois o sistema sabe exatamente quanto espaço precisa para armazenar os dados.

Acesso direto Para ter a referência de cada valor armazenado nesse espaço contíguo de memória, cada índice do array aponta para cada endereço naquela memória alocada, permitindo acessar cada elemento diretamente, sem ter que percorrer cada endereço um por um. Isso é conhecido como acesso direto.

Para ter o acesso direto é preciso realizar o cálculo de endereço, que só é possível de ser realizado tendo a informação do tipo de dado e quantidade (que é passada ao criar o array).

A partir da fórmula: valor do endereço n = endereço base + ( n x tamanho do tipo de dado) podemos encontrar qualquer valor no array.

onde:

Endereço base é o endereço de memória do primeiro elemento do array (índice 0). n é o índice do elemento que queremos acessar tamanho do tipo de dado é a quantidade de bytes que cada elemento ocupa na memória. tamanho do tipo de dado é a quantidade de bytes que cada elemento ocupa na memória. Para um array de inteiros em Java:

Onde o endereço base seja 1000 (em bytes). Cada int ocupa 4 bytes. Se quisermos o endereço do elemento no índice 3, a fórmula seria: Endereço do elemento 3 = 1000 +(3×4) =1 000+12 = 1012.

Ou seja, o valor do elemento no índice 3 está no endereço de memória 1012.

Arrays são ideais quando se precisa de uma estrutura de dados compacta, de tamanho fixo, acesso rápido e previsível. São muito úteis para armazenar dados quando o tamanho e tipo dos elementos são conhecidos previamente. Também são base para alguma outras estruturas de dados mais elaboradas, como vetores dinâmicos, filas e listas ligadas.

Desvantagens

Uma das limitações signiificativas dos arrays é seu tamanho fixo. Isso significa que, uma vez declarado não pode aumentar ou diminuir seu tamanho, o que torna essa estrutura inadequada para situações em que a quantidade de dados é variável ou desconhecida.

Em um array, inserir ou remover elementos especialmente em posições intermediárias, é uma operação complexa. Para inserir um elemento no meio de um array, é necessário deslocar todos os elementos posteriores para abrir espaço, para remover, fechar a lacuna deixada.

Para finalizar

Ao entender as limitações e pontos fortes dos arrays, conseguimos ver que cada estrutura tem seu lugar no design de software. A estrutura certa para cada situação e o que torna o código mais eficiente e fácil de manter.

Entender esses conceitos de alocação de memória, endereçamento e tipos de dados nos ajuda a compreender como um array funciona, porque devemos declarar seus tipos e quantidades de elementos na sua criação, o que é o acesso direto e porque é possível realizá-lo. Motivos que o torna eficiente em acesso de elementos porém ineficiente em flexibilidade e inserções e remoções no meio do array.

Espero que este artigo tenha ajudado a entender melhor como funciona o array, e com certeza correções e melhores esclarecimentos são muito bem vindos.

ThiagoPompeu, gostaria de deixar um comentário sobre algo que é bem simpes para os experts aqui no Tabnews.

Passei a ter certa consideração por esse tipo composto de dado quando via a importância para armazenar look up tables (LUT). Em determinados processos em que velocidade é preferida em vez da precisão, os vetores armazenam tabelas. O valor é rapidamente recuperado por meio de um índice dispensando cálculos para quantidades que podem ser constantes dentro de uma aplicação. Por exemplo:

  1. armazenar as words mágicas utilizadas pela função SHA256 em vez de calcular as raizes quadradas e cúbicas, truncar resultados, multiplicá-los por 2^32, truncar novamente, operações que sempre produzirão as mesmas constantes.

  2. armazenar valores "estratégicos" que representem razoavelmente, por exemplo, a função trigonométrica arco-tangente em vez calculá-la enviando uma instrução para o processador (FPU). Sendo mais específico, operações com DSP (Digital Signal Processor) e FPGA (Field Programmable Gate Array) tiram proveito desta estratégia, já que uma porta lógica pode ser facilmente "mapeada" por uma LUT armazenada num vetor ou array como chamou em sua postagem.


Para quem lê sua postagem em tela escura, as imagens com fundo claro podem causar certo desconforto. Nestes casos, tenho preferido ocultá-las com um recurso que aprendi com o rafael. Deixo o código para você copiar e colar, experimentar:


<details>
    <summary>Imagem com fundo claro</summary>
    
<img src="https://media.licdn.com/dms/image/v2/D4D12AQHh7m2h__cNcA/article-inline_image-shrink_1000_1488/article-inline_image-shrink_1000_1488/0/1730849841506?e=1736380800&v=beta&t=Im73H9YPXpk2NTBTw8Kniu2AoSRWPoGBDJm37pidv3Q" height="500">
    
</details>

Eis o código sugerido funcionando (imagem com redimensionamento da altura para height="500".

E aqui com redimensionamento da imagem em largura com width="500".

É possível incluir ou omitir as propriedades width e height conforme necessário, sendo desejável que mantenha a mesma taxa de aspecto da imagem original para evitar distorções como no exemplo a seguir.

Obrigado, gpoleszuk! Fiquei realmente interessado em aprender mais sobre o uso de Lookup Tables (LUTs) e como elas podem acelerar o desempenho em situações em que a velocidade é mais importante que a precisão. Achei muito interessante o exemplo do SHA-256, onde armazenar constantes evita cálculos repetitivos complexos. Eu nunca tinha pensado nos arrays dessa forma mais avançada, como em aproximações para funções trigonométricas. Obrigado pela dica de acessibilidade nas imagens! Vou testar o código e aplicar nas próximas postagens também. Se tiver mais sugestões ou conteúdo sobre LUTs e essas otimizações ficaria feliz em dar uma olhada!

Vale lembrar que essas definições variam conforme a linguagem.

Por exemplo, a alocação em um bloco de memória contínuo é verdade em C e C++, só para citar duas (mas também vale para Java e outras).

Porém, em outras linguagens como PHP e JavaScript, o "array" é uma estrutura que pode mudar de tamanho, e por isso não é alocado de forma contínua (em PHP, na verdade o "array" é implementado como uma hashtable).

Já em Python, o nome da estrutura básica é "lista", que também pode mudar de tamanho e não é alocada de forma contínua. Embora a sintaxe seja bem similar a de um array, como o uso de [] para acessar índices, etc, a implementação é diferente de linguagens que alocam de forma contínua, como C.

E mesmo a regra de ter todos os elementos do mesmo tipo também não é universal. Em JavaScript, PHP, Python e várias outras, é perfeitamente possível ter um array como [1, "abc", objeto_de_qualquer_tipo].

Se forçarmos a barra, até mesmo em Java podemos ter algo como:

// Em Java, um array de Object pode ter "qualquer coisa"
Object[] array = {1, "abc", null, new FileInputStream("/tmp/arquivo")};

Embora eu não recomende, é um código perfeitamente válido. Isso porque um array declarado com um tipo T pode ter elementos que sejam subtipos de T, por isso um array de Object pode ter "qualquer coisa" (no caso do 1, é feito o auto-boxing para Integer).

Outra diferença é que algumas linguagens permitem índices negativos ou intervalos. Por exemplo, em Python lista[-1] retorna o último elemento da lista, lista[-2] o penúltimo, e assim por diante. E lista[2:5] retorna outra lista, contendo os elementos dos índices 2, 3 e 4 (o intervalo é "start inclusive, end exclusive", ou seja, o primeiro índice é incluso, o último não).

Em C# tem algo similar: array[^1] retorna o último elemento do array, array[^2] o penúltimo, etc, e array[2..5] retorna outro array contendo os elementos dos índices 2, 3 e 4.


Cada índice em um array é uma referência (ou ponteiro) para uma posição específica na memória onde o valor correspondente está armazenado.

Depende. Em Java, arrays de tipos primitivos não usam ponteiros e nem referências, os próprios valores são colocados diretamente no array.


Claro que como "guia geral" ou introdução ao conceito, está ótimo. Mas é sempre bom se atentar a essas diferenças, pois cada linguagem possui seus detalhes de implementação. Não dá pra achar que o array sempre vai ser igual em todas elas.

Perfeito kht, obrigado pelo esclarecimento! > Cada índice em um array é uma referência (ou ponteiro) para uma posição específica na memória onde o valor correspondente está armazenado. Neste caso o que eu tentei dizer, mas talvez eu nao tenha explicado da melhor forma, é que quando indicamos um índice como por exemplo **int num = numeros[5];** esse 5 se refere a uma posicao na memoria, onde o valor esta armazenado.

Adorava as aulas sobre estruturas de dados na faculdade, eram super interessantes, aprender cada estrutura nova, vetor, matriz, pilha, fila,lista, arvore, etc....

Depois que se aprende mais sobre como esses conceitos funcionam, muita coisa nova fica mais simples para entender e até utilizar

Exatamente, muitas vezes a dificuldade para entender alguns conceitos mais avançados simplismente são por conta de algumas falhas nos fundamentos.