Ponteiros e arrays: a relação fundamental

1. O Nascimento da Relação: Arrays São Ponteiros (Quase)

Em C, a relação entre ponteiros e arrays é tão íntima que muitos programadores iniciantes confundem os dois conceitos. A verdade é que, na maioria dos contextos, o nome de um array se comporta como um ponteiro constante para o primeiro elemento.

int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;  // ptr aponta para arr[0]

printf("Valor de arr: %p\n", (void*)arr);
printf("Valor de ptr: %p\n", (void*)ptr);
printf("Valor de &arr[0]: %p\n", (void*)&arr[0]);

Os três endereços impressos serão idênticos. No entanto, existe uma diferença fundamental: sizeof se comporta de maneira distinta.

printf("sizeof(arr): %zu bytes\n", sizeof(arr));  // 20 bytes (5 * 4)
printf("sizeof(ptr): %zu bytes\n", sizeof(ptr));  // 8 bytes (tamanho do ponteiro)

Enquanto sizeof(arr) retorna o tamanho total do array (20 bytes para 5 inteiros), sizeof(ptr) retorna apenas o tamanho do ponteiro (8 bytes em sistemas 64-bit). Esta distinção é crucial e fonte frequente de bugs.

2. Indexação de Arrays: A Mão Invisível da Aritmética de Ponteiros

A notação arr[i] que usamos tão naturalmente é, na verdade, açúcar sintático para *(arr + i). O compilador traduz automaticamente a indexação para aritmética de ponteiros.

int arr[5] = {10, 20, 30, 40, 50};

// Forma tradicional com índice
for (int i = 0; i < 5; i++) {
    printf("arr[%d] = %d\n", i, arr[i]);
}

// Forma equivalente com aritmética de ponteiros
for (int i = 0; i < 5; i++) {
    printf("*(arr + %d) = %d\n", i, *(arr + i));
}

A consequência mais surpreendente dessa equivalência é que i[arr] também funciona:

printf("arr[2] = %d\n", arr[2]);     // 30
printf("2[arr] = %d\n", 2[arr]);     // 30 também!

Isso acontece porque arr[2] é traduzido para *(arr + 2), e 2[arr] para *(2 + arr). A adição é comutativa, então ambos produzem o mesmo resultado.

3. Passando Arrays para Funções: A Decaída (Decay) para Ponteiro

Quando passamos um array para uma função, ele "decai" (decay) para um ponteiro para o primeiro elemento. Isso significa que as seguintes declarações de função são idênticas:

void func1(int arr[]) {
    printf("sizeof(arr) dentro da funcao: %zu\n", sizeof(arr));
}

void func2(int *arr) {
    printf("sizeof(arr) dentro da funcao: %zu\n", sizeof(arr));
}

Em ambos os casos, sizeof(arr) retornará o tamanho de um ponteiro, não do array original. Esta é a armadilha clássica:

void imprime_array(int arr[]) {
    // ERRADO: sizeof(arr) é sizeof(int*), não o tamanho do array
    for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
        printf("%d ", arr[i]);
    }
}

A solução é passar o tamanho como parâmetro adicional:

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

4. Arrays Multidimensionais e Ponteiros: Desvendando a Matriz

Arrays bidimensionais como int mat[3][4] são armazenados como arrays de arrays na memória (linha a linha). O nome mat decai para um ponteiro para o primeiro elemento, que é um array de 4 inteiros.

int mat[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

// Ponteiro para array de 4 inteiros
int (*ptr)[4] = mat;

// Acessando elementos
printf("mat[1][2] = %d\n", mat[1][2]);           // 7
printf("*(*(mat + 1) + 2) = %d\n", *(*(mat + 1) + 2));  // 7
printf("*(ptr[1] + 2) = %d\n", *(ptr[1] + 2));          // 7

A expressão *(*(mat + i) + j) funciona assim: mat + i avança i linhas (cada linha tem 4 inteiros), *(mat + i) obtém o array da linha i, e + j avança j elementos dentro dessa linha.

5. Ponteiros para Arrays vs. Arrays de Ponteiros: Não Confunda!

A diferença entre int (*ptr)[5] e int *arr[5] é sutil mas crucial:

// Ponteiro para array de 5 inteiros
int (*ptr_para_array)[5];
int array[5] = {1, 2, 3, 4, 5};
ptr_para_array = &array;  // aponta para o array inteiro

// Array de 5 ponteiros para int
int *array_de_ponteiros[5];
int a = 10, b = 20, c = 30;
array_de_ponteiros[0] = &a;
array_de_ponteiros[1] = &b;
array_de_ponteiros[2] = &c;

Para criar uma matriz dinâmica, podemos usar um array de ponteiros:

int *matriz[3];  // array de 3 ponteiros

for (int i = 0; i < 3; i++) {
    matriz[i] = (int*)malloc(4 * sizeof(int));
    for (int j = 0; j < 4; j++) {
        matriz[i][j] = i * 4 + j + 1;
    }
}

// Acesso como matriz normal
printf("matriz[2][1] = %d\n", matriz[2][1]);  // 10

6. Aplicações Práticas: Quando Usar Ponteiro, Quando Usar Array

A iteração com ponteiros geralmente é mais eficiente:

int arr[100000];
int *ptr;

// Iteração com índice
for (int i = 0; i < 100000; i++) {
    arr[i] = i;
}

// Iteração com ponteiro (mais rápido)
for (ptr = arr; ptr < arr + 100000; ptr++) {
    *ptr = 0;
}

Para strings, a diferença entre array e ponteiro é importante:

char str1[] = "Hello";  // Array mutável no stack
char *str2 = "World";   // Ponteiro para string literal (imutável!)

str1[0] = 'h';  // OK
// str2[0] = 'w';  // Comportamento indefinido! String literal é imutável

Na alocação dinâmica, usamos ponteiros como arrays flexíveis:

int *arr_dinamico = (int*)malloc(10 * sizeof(int));
if (arr_dinamico != NULL) {
    for (int i = 0; i < 10; i++) {
        arr_dinamico[i] = i * 2;  // Usando notação de array
    }
    free(arr_dinamico);
}

Referências