Arrays (ou vetores) são estruturas de dados fundamentais na linguagem C, que permitem armazenar uma sequência de elementos do mesmo tipo em posições contíguas de memória. Dominar arrays é essencial para desenvolver programas eficientes e compreender como a memória é manipulada em C.

Nesta aula, vamos explorar desde a declaração e o acesso aos elementos, passando pela inicialização, até a estreita relação entre arrays e ponteiros, que é uma das características mais poderosas (e às vezes confusas) da linguagem.

Declaração e acesso

Em C, um array é declarado especificando o tipo dos elementos, o nome do array e o tamanho entre colchetes. O tamanho deve ser uma constante inteira (ou uma expressão constante) e é definido em tempo de compilação. Por exemplo, int numeros[5]; declara um array de 5 inteiros.

O acesso aos elementos é feito através do índice, que começa em 0. Assim, o primeiro elemento é numeros[0], o segundo numeros[1], e assim por diante. O último elemento válido é numeros[4] no exemplo acima. Tentar acessar um índice fora dos limites (como numeros[5]) é um erro de programação que pode causar comportamento indefinido, como corromper memória ou causar falhas de segmentação.

#include <stdio.h>

int main() {
    int numeros[5]; // declara array de 5 inteiros

    // Atribuindo valores
    numeros[0] = 10;
    numeros[1] = 20;
    numeros[2] = 30;
    numeros[3] = 40;
    numeros[4] = 50;

    // Acessando e imprimindo
    for (int i = 0; i < 5; i++) {
        printf("numeros[%d] = %d\n", i, numeros[i]);
    }

    return 0;
}

É importante notar que o índice pode ser qualquer expressão inteira, inclusive variáveis e expressões aritméticas. Isso permite percorrer o array com loops, como mostrado acima.

Inicialização

Arrays podem ser inicializados no momento da declaração, fornecendo uma lista de valores entre chaves. Se você fornecer menos valores do que o tamanho do array, os elementos restantes serão inicializados com zero (para tipos numéricos). Se você fornecer mais valores do que o tamanho declarado, o compilador emitirá um erro.

int vetor[5] = {1, 2, 3}; // vetor[3] e vetor[4] serão 0
int outro[] = {10, 20, 30}; // o tamanho é inferido como 3

Também é possível inicializar arrays de caracteres (strings) com literais de string. Nesse caso, o array inclui automaticamente o caractere nulo '\0' no final, a menos que o tamanho seja especificado exatamente sem espaço para ele (o que não é recomendado).

char palavra[] = "Olá"; // tamanho 4 (O, l, á, \0)
char nome[10] = "João"; // os caracteres restantes são zerados

Para arrays multidimensionais, a inicialização é feita com chaves aninhadas. Por exemplo:

int matriz[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

Quando você inicializa um array, pode omitir o primeiro tamanho (o das linhas) e o compilador o deduz a partir da lista de inicializadores, mas os tamanhos subsequentes devem ser especificados.

Tamanho fixo

Uma característica importante dos arrays em C é que seu tamanho é fixo durante todo o tempo de execução. O tamanho é definido em tempo de compilação (a menos que você use alocação dinâmica, que veremos em aulas futuras). Isso significa que você não pode aumentar ou diminuir o tamanho de um array após a sua criação.

Consequentemente, ao declarar um array, você precisa saber exatamente quantos elementos ele deve armazenar. Isso pode ser um desafio quando o número de elementos só é conhecido em tempo de execução. Nesses casos, é comum alocar um array com um tamanho máximo possível ou usar alocação dinâmica com malloc e realloc.

Outra consequência é que você deve ter cuidado para não ultrapassar os limites do array. O compilador não verifica automaticamente os limites, então o programador é responsável por garantir que os índices sejam válidos.

#define TAMANHO 10

int main() {
    int dados[TAMANHO];

    // Preenchendo com os quadrados dos índices
    for (int i = 0; i < TAMANHO; i++) {
        dados[i] = i * i;
    }

    // Exibindo
    for (int i = 0; i < TAMANHO; i++) {
        printf("%d ", dados[i]);
    }
    return 0;
}

É possível obter o número de elementos de um array em tempo de compilação usando o operador sizeof. Por exemplo, sizeof(dados) / sizeof(dados[0]) retorna o número de elementos, pois sizeof(dados) é o tamanho total em bytes e sizeof(dados[0]) é o tamanho de um elemento. Essa técnica é útil para evitar hard-coded de tamanhos em loops.

Relação com ponteiros

Em C, arrays e ponteiros estão intimamente relacionados. Na maioria dos contextos, o nome de um array é convertido em um ponteiro para o seu primeiro elemento. Por exemplo, se você tem int arr[5], o nome arr pode ser usado como um ponteiro para int apontando para arr[0].

Isso significa que você pode usar aritmética de ponteiros para acessar os elementos do array. Por exemplo, *(arr + i) é equivalente a arr[i]. Na verdade, a expressão arr[i] é traduzida pelo compilador para *(arr + i). Essa equivalência é tão forte que você pode escrever i[arr], embora isso seja incomum e confuso.

#include <stdio.h>

int main() {
    int vetor[4] = {10, 20, 30, 40};
    int *ptr = vetor; // ptr aponta para o primeiro elemento

    printf("Primeiro elemento: %d\n", *ptr);        // 10
    printf("Segundo elemento: %d\n", *(ptr + 1));   // 20
    printf("Endereço do primeiro: %p\n", (void*)ptr);
    printf("Endereço do segundo: %p\n", (void*)(ptr + 1));

    return 0;
}

É importante notar que, apesar dessa relação, arrays e ponteiros não são a mesma coisa. Um array é um bloco de memória contíguo com tamanho fixo, enquanto um ponteiro é uma variável que armazena um endereço. Por exemplo, você não pode atribuir um array a outro array diretamente (arr1 = arr2 é inválido), mas pode atribuir ponteiros.

Quando você passa um array para uma função, na verdade está passando um ponteiro para o primeiro elemento. Portanto, a função não conhece o tamanho do array, a menos que você o informe explicitamente.

void imprimir(int *arr, int tamanho) {
    for (int i = 0; i < tamanho; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int dados[5] = {1, 2, 3, 4, 5};
    imprimir(dados, 5); // passa o array como ponteiro
    return 0;
}

Boas práticas e observações finais

Ao trabalhar com arrays, sempre verifique os limites para evitar estouro de buffer, que pode levar a vulnerabilidades de segurança. Prefira usar constantes simbólicas (como #define ou const int) para definir o tamanho, em vez de números mágicos espalhados pelo código.

Lembre-se de que o tamanho de um array é fixo; se precisar de uma coleção dinâmica, estude alocação dinâmica de memória (malloc, calloc, realloc) e estruturas de dados como listas ligadas.

Quando usar sizeof com arrays, tenha cuidado: se o array for passado para uma função, ele decai para um ponteiro, e sizeof retornará o tamanho do ponteiro, não do array. Portanto, sempre passe o tamanho como parâmetro.

Exercícios

  1. Declare um array de 10 inteiros e preencha-o com os números de 1 a 10. Em seguida, imprima o array na ordem inversa.
  2. Escreva um programa que declare um array de 5 floats, inicialize-o com valores à sua escolha e calcule a média dos elementos. Imprima a média com duas casas decimais.
  3. Dado o array int arr[4] = {5, 10, 15, 20}, use aritmética de ponteiros para imprimir cada elemento e seu endereço de memória.
  4. Crie uma função que receba um array de inteiros e seu tamanho, e retorne o maior elemento. No main, teste a função com um array de 7 elementos.
  5. Explique a diferença entre int *ptr; e int arr[5];. Em particular, o que acontece quando você usa sizeof em cada um?

✓ Resposta:
#include <stdio.h>

int main() {
    int numeros[10];
    for (int i = 0; i < 10; i++) {
        numeros[i] = i + 1;
    }
    for (int i = 9; i >= 0; i--) {
        printf("%d ", numeros[i]);
    }
    return 0;
}

✓ Resposta:
#include <stdio.h>

int main() {
    float valores[5] = {1.5, 2.5, 3.5, 4.5, 5.5};
    float soma = 0.0;
    for (int i = 0; i < 5; i++) {
        soma += valores[i];
    }
    float media = soma / 5;
    printf("Média: %.2f\n", media);
    return 0;
}

✓ Resposta:
#include <stdio.h>

int main() {
    int arr[4] = {5, 10, 15, 20};
    int *ptr = arr;
    for (int i = 0; i < 4; i++) {
        printf("Elemento %d: %d, endereço: %p\n", i, *(ptr + i), (void*)(ptr + i));
    }
    return 0;
}

✓ Resposta:
#include <stdio.h>

int maior_elemento(int *arr, int tamanho) {
    int maior = arr[0];
    for (int i = 1; i < tamanho; i++) {
        if (arr[i] > maior) {
            maior = arr[i];
        }
    }
    return maior;
}

int main() {
    int dados[7] = {3, 5, 7, 2, 8, -1, 4};
    int maior = maior_elemento(dados, 7);
    printf("Maior elemento: %d\n", maior);
    return 0;
}

✓ Resposta:

int *ptr; declara um ponteiro para inteiro. Ele pode apontar para qualquer local de memória e seu tamanho é o tamanho de um ponteiro (geralmente 8 bytes em sistemas 64-bit). int arr[5]; declara um array de 5 inteiros. O nome arr é um endereço constante (não pode ser modificado) para o primeiro elemento, e sizeof(arr) retorna o tamanho total em bytes (5 * sizeof(int)). Já sizeof(ptr) retorna o tamanho do ponteiro, não do bloco para o qual aponta.

Referências