Arrays e ponteiros são conceitos fundamentais em C, e sua relação é frequentemente fonte de confusão. Embora arrays e ponteiros sejam tipos distintos, o nome de um array pode ser tratado como um ponteiro para seu primeiro elemento na maioria dos contextos – isso é conhecido como decay. No entanto, existem diferenças importantes, especialmente quando usamos o operador sizeof. Nesta aula, vamos desmistificar esses conceitos com exemplos práticos.

Decay para ponteiro

Em C, quando um array é usado em uma expressão, ele automaticamente se converte (decai) para um ponteiro para seu primeiro elemento, exceto em três situações: como operando de sizeof, como operando do operador unário &, ou quando é uma string literal usada para inicializar um array. Esse comportamento é chamado de array decay.

Por exemplo, se declaramos int arr[5], então arr em uma expressão como int *p = arr; é equivalente a &arr[0]. Isso permite que usemos a sintaxe de ponteiros para acessar elementos do array.

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;  // arr decai para ponteiro para o primeiro elemento

    printf("arr[2] = %d\n", arr[2]);
    printf("*(p + 2) = %d\n", *(p + 2));
    printf("p[2] = %d\n", p[2]);  // sintaxe de array também funciona com ponteiros

    return 0;
}

Observe que p[2] é equivalente a *(p + 2). A sintaxe de subscrito ([]) é definida em termos de ponteiros: a[b] é equivalente a *(a + b). Isso significa que você pode usar a notação de array com ponteiros e vice-versa.

Quando diferem

Apesar do decay, arrays e ponteiros não são a mesma coisa. A diferença fundamental é que um array é um objeto que ocupa um bloco contíguo de memória, enquanto um ponteiro é uma variável que armazena um endereço. O array não pode ser atribuído a outro array, mas um ponteiro pode ser modificado para apontar para outro local.

Além disso, o operador & se comporta de forma diferente. Em um array, &arr retorna um ponteiro para o array inteiro (tipo int (*)[5]), não um ponteiro para o primeiro elemento. Já em um ponteiro, &p retorna o endereço da variável ponteiro.

#include <stdio.h>

int main() {
    int arr[5] = {1,2,3,4,5};
    int *p = arr;

    printf("arr = %p\n", (void*)arr);
    printf("&arr = %p\n", (void*)&arr);
    printf("arr + 1 = %p\n", (void*)(arr + 1));
    printf("&arr + 1 = %p\n", (void*)(&arr + 1));

    printf("p = %p\n", (void*)p);
    printf("&p = %p\n", (void*)&p);

    return 0;
}

Note que arr + 1 avança 4 bytes (tamanho de um int), enquanto &arr + 1 avança 20 bytes (tamanho do array inteiro). Já &p é o endereço da própria variável ponteiro, que é diferente do endereço armazenado em p.

sizeof em arrays vs ponteiros

Uma das diferenças mais importantes é o comportamento do operador sizeof. Quando aplicado a um array, sizeof retorna o tamanho total do array em bytes (número de elementos * tamanho de cada elemento). Quando aplicado a um ponteiro, retorna o tamanho do ponteiro (geralmente 4 ou 8 bytes), independentemente de para onde ele aponta.

Isso é crucial em funções que recebem arrays como parâmetros, pois na verdade recebem ponteiros. Dentro da função, sizeof no parâmetro dará o tamanho do ponteiro, não do array original. Por isso, é comum passar também o tamanho do array como argumento.

#include <stdio.h>

void print_size(int arr[]) {
    printf("Dentro da função: sizeof(arr) = %zu\n", sizeof(arr));
}

int main() {
    int arr[10];
    int *p = arr;

    printf("sizeof(arr) = %zu\n", sizeof(arr));
    printf("sizeof(p) = %zu\n", sizeof(p));

    print_size(arr);

    return 0;
}

A saída típica será algo como: sizeof(arr) = 40 (em máquina de 4 bytes por int), sizeof(p) = 8 (em 64 bits), e dentro da função sizeof(arr) = 8 (pois o parâmetro decaiu para ponteiro).

Exemplos

Vamos consolidar com mais exemplos práticos.

Exemplo 1: Percorrendo array com ponteiro

#include <stdio.h>

int main() {
    int arr[] = {100, 200, 300, 400};
    int *p = arr;
    int n = sizeof(arr) / sizeof(arr[0]);

    for (int i = 0; i < n; i++) {
        printf("Elemento %d: %d\n", i, *p);
        p++;
    }

    return 0;
}

Exemplo 2: Ponteiro para array multidimensional

#include <stdio.h>

int main() {
    int mat[2][3] = {{1,2,3}, {4,5,6}};
    int (*p)[3] = mat;  // ponteiro para array de 3 ints

    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", p[i][j]);
        }
        printf("\n");
    }

    return 0;
}

Exemplo 3: Cuidado com o decay em funções

#include <stdio.h>

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

int main() {
    int arr[] = {10, 20, 30};
    func(arr, sizeof(arr)/sizeof(arr[0]));
    return 0;
}

Boas práticas

  • Sempre que passar um array para uma função, passe também seu tamanho, pois sizeof dentro da função não funcionará como esperado.
  • Use const em parâmetros ponteiro quando a função não modificar o array.
  • Evite usar sizeof em parâmetros de array para obter o número de elementos – isso é um erro comum.
  • Preferir a notação de array (arr[i]) para legibilidade, a menos que precise de aritmética de ponteiros.

Referências

Exercícios

  1. Explique o que é o decay de array e cite as três exceções em que ele não ocorre.

    ✓ Resposta: O decay de array é a conversão automática do nome de um array para um ponteiro para seu primeiro elemento na maioria das expressões. As exceções são: quando o array é operando de sizeof, operando de &, ou quando é uma string literal usada para inicializar um array.
  2. Qual a diferença entre arr e &arr quando arr é um array de 10 inteiros? Mostre com um exemplo de aritmética de ponteiros.

    ✓ Resposta: arr decai para um ponteiro para o primeiro elemento (tipo int*), enquanto &arr é um ponteiro para o array inteiro (tipo int (*)[10]). Assim, arr + 1 avança 4 bytes (tamanho de um int), mas &arr + 1 avança 40 bytes (tamanho do array).
  3. O que será impresso pelo código a seguir? Explique.

    #include <stdio.h>
    int main() {
        int arr[5];
        int *p = arr;
        printf("%zu %zu\n", sizeof(arr), sizeof(p));
        return 0;
    }

    ✓ Resposta: Em uma máquina de 64 bits, sizeof(arr) será 20 (5 * 4 bytes) e sizeof(p) será 8 (tamanho do ponteiro). Em 32 bits, sizeof(p) seria 4.
  4. Escreva uma função que recebe um array de inteiros e seu tamanho, e retorna a soma dos elementos. Use aritmética de ponteiros para percorrer o array.

    ✓ Resposta:
    int soma_array(int *arr, int size) {
        int soma = 0;
        for (int *p = arr; p < arr + size; p++) {
            soma += *p;
        }
        return soma;
    }
  5. Por que o código abaixo está incorreto? Corrija-o.

    void imprime(int arr[]) {
        for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++) {
            printf("%d ", arr[i]);
        }
    }

    ✓ Resposta: O erro é que sizeof(arr) dentro da função retorna o tamanho do ponteiro, não do array. A correção é passar o tamanho como parâmetro: void imprime(int arr[], int size) { for (int i = 0; i < size; i++) ... }