Em C, arrays e ponteiros estão intimamente relacionados, mas não são a mesma coisa. Entender essa relação é crucial para escrever código correto e eficiente. Nesta aula, vamos explorar o conceito de decay de arrays para ponteiros, situações onde arrays e ponteiros se comportam de forma diferente, e como o operador sizeof se comporta com cada um.

O nome de um array pode ser usado como um ponteiro para seu primeiro elemento na maioria dos contextos, mas essa equivalência não é absoluta. Vamos detalhar cada aspecto com exemplos práticos.

Decay para ponteiro

Em C, o nome de um array decai para um ponteiro para seu primeiro elemento na maioria das expressões. Isso significa que, ao passar um array para uma função, o que é passado é um ponteiro, não uma cópia do array. Esse comportamento é chamado de decay.

Por exemplo, ao declarar int arr[5] = {1,2,3,4,5};, o nome arr em uma expressão como arr + 1 se torna um ponteiro para arr[0]. A aritmética de ponteiros então avança para o próximo elemento. Esse decay permite que arrays sejam manipulados como ponteiros, mas também pode causar confusão.

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;  // arr decai para ponteiro
    printf("Primeiro elemento: %d\n", *p);  // 10
    printf("Segundo elemento: %d\n", *(p+1)); // 20
    return 0;
}

Quando diferem

Embora arrays e ponteiros sejam frequentemente intercambiáveis, existem situações onde eles se comportam de forma diferente. A diferença mais notável ocorre com o operador sizeof e com o operador de endereço &.

Quando aplicamos sizeof a um array, obtemos o tamanho total do array em bytes. Já em um ponteiro, obtemos o tamanho do ponteiro (geralmente 8 bytes em sistemas 64 bits). Além disso, o operador & aplicado a um array retorna um ponteiro para o array inteiro (tipo int (*)[5]), enquanto aplicado a um ponteiro 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("sizeof(arr) = %zu\n", sizeof(arr));  // 20 (5 * 4)
    printf("sizeof(p) = %zu\n", sizeof(p));      // 8 (ou 4 em 32 bits)

    printf("&arr = %p\n", (void*)&arr);
    printf("&p = %p\n", (void*)&p);
    return 0;
}

sizeof em arrays vs ponteiros

O operador sizeof é uma das principais maneiras de distinguir arrays de ponteiros. Quando aplicado a um array declarado localmente, sizeof retorna o número total de bytes ocupados pelo array. Isso é útil para calcular o número de elementos: sizeof(arr) / sizeof(arr[0]).

No entanto, quando um array é passado para uma função, ele decai para um ponteiro, e sizeof dentro da função retornará o tamanho do ponteiro, não do array original. Isso é uma fonte comum de erros. Por isso, ao passar arrays para funções, geralmente passamos também o tamanho como parâmetro.

#include <stdio.h>

void printArray(int arr[]) {
    // arr aqui é um ponteiro, não um array
    printf("sizeof(arr) na função: %zu\n", sizeof(arr));  // 8
}

int main() {
    int arr[10] = {0};
    printf("sizeof(arr) no main: %zu\n", sizeof(arr));  // 40
    printArray(arr);
    return 0;
}

Exemplos

Vamos consolidar com exemplos práticos que mostram a diferença entre arrays e ponteiros, especialmente em relação ao decay e sizeof.

#include <stdio.h>

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

    // Acessando elementos via array e ponteiro
    printf("arr[2] = %d\n", arr[2]);      // 3
    printf("*(ptr+2) = %d\n", *(ptr+2));  // 3

    // Diferença com &
    printf("&arr = %p\n", (void*)&arr);   // endereço do array
    printf("&ptr = %p\n", (void*)&ptr);   // endereço do ponteiro

    // Aritmética de ponteiros
    printf("ptr+1 aponta para: %d\n", *(ptr+1));  // 2

    // Array bidimensional
    int matriz[2][3] = {{1,2,3},{4,5,6}};
    int (*pmat)[3] = matriz;  // ponteiro para array de 3 ints
    printf("matriz[1][2] = %d\n", matriz[1][2]);  // 6
    printf("*(*(pmat+1)+2) = %d\n", *(*(pmat+1)+2)); // 6

    return 0;
}

Boas práticas

Ao trabalhar com arrays e ponteiros, lembre-se de: (1) usar sizeof apenas no escopo onde o array foi declarado; (2) passar o tamanho como parâmetro em funções; (3) preferir a notação de array para clareza quando possível; (4) cuidado com aritmética de ponteiros fora dos limites.

Referências

Exercícios

  1. Explique o que é o decay de array para ponteiro em C. Dê um exemplo.

    ✓ Resposta: Decay é o fenômeno pelo qual o nome de um array é convertido em um ponteiro para seu primeiro elemento na maioria das expressões. Exemplo: int arr[5]; int *p = arr;arr decai para &arr[0].
  2. Qual a diferença entre sizeof(arr) e sizeof(ptr) quando arr é um array de 10 inteiros e ptr é um ponteiro para int?

    ✓ Resposta: sizeof(arr) retorna 40 (10 * 4 bytes, supondo int de 4 bytes), enquanto sizeof(ptr) retorna o tamanho do ponteiro (8 bytes em sistemas 64 bits).
  3. Por que ao passar um array para uma função, devemos também passar o seu tamanho?

    ✓ Resposta: Porque o array decai para um ponteiro, e sizeof dentro da função retorna o tamanho do ponteiro, não do array original. Sem o tamanho, a função não sabe quantos elementos existem.
  4. O que retorna &arr e como isso difere de arr?

    ✓ Resposta: &arr retorna um ponteiro para o array inteiro (tipo int (*)[5]), enquanto arr decai para um ponteiro para o primeiro elemento (int *). Ambos têm o mesmo valor de endereço, mas tipos diferentes.
  5. Escreva um programa que declare um array de 5 inteiros e um ponteiro para int, e mostre a diferença entre sizeof do array e do ponteiro.

    ✓ Resposta:
    #include <stdio.h>
    int main() {
        int arr[5] = {1,2,3,4,5};
        int *ptr = arr;
        printf("sizeof(arr) = %zu\n", sizeof(arr));
        printf("sizeof(ptr) = %zu\n", sizeof(ptr));
        return 0;
    }