Menu

Ponteiros e arrays em C: decaimento, arr[i] = *(arr+i) e a armadilha do sizeof

Em C um nome de array vira um ponteiro ao seu primeiro elemento em quase todo lugar onde você o usa. Esta página explica esse decaimento, por que arr[i] é literalmente *(arr+i), por que o tamanho do array precisa viajar em separado até as funções, e como um ponteiro para array difere de um array de ponteiros.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Arrays e ponteiros são coisas diferentes em C que se comportam do mesmo jeito na maior parte do tempo. Esse "na maior parte do tempo" é o que torna o assunto escorregadio: código que trata um array como um ponteiro normalmente funciona, até o dia em que cai em um dos dois lugares onde a diferença importa e a resposta está silenciosamente errada.

Esta página traça a linha com precisão.

Decaimento de array: a única regra por trás de tudo

Em quase toda expressão, um nome de array é automaticamente convertido para um ponteiro ao seu primeiro elemento. Isso se chama decaimento. arr vira &arr[0], com tipo int *.

O nome do array e o endereço do seu primeiro elemento são impressos de forma idêntica. Esse é o decaimento em ação, e é por isso que você pode atribuir um array a um ponteiro sem &.

Existem exatamente três lugares onde o decaimento não acontece: como operando de sizeof, como operando de &, e ao inicializar um array de caracteres a partir de um literal de string. Em todo o resto, presuma decaimento.

arr[i] é literalmente *(arr + i)

O padrão C define o operador de índice em termos de aritmética de ponteiros: a[b] significa *(a + b). Indexar não é um recurso separado - é abreviação.

Duas consequências saem daí. Primeira, um ponteiro pode ser indexado: p[2] é *(p + 2), e é por isso que funções que recebem um ponteiro ainda podem usar a sintaxe familiar arr[i]. Segunda, 2[arr] compila - *(2 + arr) é o mesmo endereço que *(arr + 2). Ninguém escreve isso fora de um teste de pegadinha, mas resolve a questão de se indexar é "de verdade" aritmética de ponteiros.

Passando um array para uma função

Por causa do decaimento, uma função nunca recebe um array. Ela recebe um ponteiro.

Três coisas que vale destacar desse exemplo.

O tamanho em int arr[100] é documentação, nada mais. O compilador reescreve o parâmetro para int *arr e nunca verifica se você passou 100 de qualquer coisa.

O comprimento precisa viajar em separado. A função tem um endereço e nenhuma ideia de até onde os dados se estendem. É por isso que praticamente toda função C que recebe um array recebe também uma contagem: memcpy(dst, src, n), fread(ptr, size, count, f), qsort(base, nmemb, size, cmp). Strings são a exceção apenas porque carregam seu próprio marcador de fim, o '\0'.

Passar um ponteiro é barato e os dados são compartilhados. Nenhuma cópia do array é feita, então a função pode modificar os elementos de quem a chamou - o que muitas vezes é exatamente o que você quer:

Marque parâmetros somente de leitura como const int *, como print_all faz. Isso documenta a intenção e deixa o compilador pegar uma escrita acidental.

A armadilha do sizeof

Eis o bug que pega todo programador C uma vez. sizeof é um dos dois lugares onde o decaimento não acontece - então ele dá o tamanho verdadeiro do array onde o array é declarado, e o tamanho de um ponteiro em qualquer lugar onde o array já decaiu.

Em main, sizeof data é 40 bytes e o tamanho dá 10. Dentro de inspect, sizeof arr é o tamanho de um ponteiro - 8 em uma máquina de 64 bits - e o "tamanho" sai como 2. O código parece idêntico e está silenciosamente errado.

A regra que decorre disso: calcule o comprimento de um array só no escopo onde o array foi declarado, e passe-o adiante a partir dali.

O & de um array não é do mesmo tipo

O outro lugar onde o decaimento não acontece é o operador &. &arr é um ponteiro para o array inteiro, não para o seu primeiro elemento. Os dois guardam o mesmo endereço; a diferença é o que "somar 1" significa.

Mesmo endereço inicial, passo diferente. arr tem tipo int * e anda um elemento; &arr tem tipo int (*)[5] e anda o array inteiro. Esse é o mecanismo que faz funcionar a passagem de uma linha de um array 2D, coberta em arrays multidimensionais.

Ponteiro para array contra array de ponteiros

Duas declarações que parecem iguais e significam coisas opostas:

int *p[5];     // array de 5 ponteiros para int
int (*q)[5];   // ponteiro para um array de 5 ints

[] tem precedência maior que *, então int *p[5] é lido como "p é um array, de ponteiros". Os parênteses em int (*q)[5] forçam o * a se ligar primeiro: "q é um ponteiro, para um array".

int *p[5]                     int (*q)[5]

 p[0] -> [ int ]               q -> [ int | int | int | int | int ]
 p[1] -> [ int ]                     um bloco contíguo de 5
 p[2] -> [ int ]
 p[3] -> [ int ]
 p[4] -> [ int ]
 cinco endereços separados

Um array de ponteiros é o mais comum, e você já usou um: char *argv[] é um array de ponteiros para strings, um por argumento de linha de comando. É também como você guarda uma lista de strings de comprimentos diferentes sem desperdiçar espaço.

Note o (*q)[0]: desreferencie o ponteiro para obter o array, depois indexe-o. Os parênteses são exigidos de novo, pela mesma razão de precedência.

Onde arrays e ponteiros genuinamente diferem

Tenha esta tabelinha em mente e a confusão desaparece:

Array int arr[10]Ponteiro int *p
O que é10 ints dispostos na memóriauma variável guardando um endereço
sizeof40 - o bloco inteiro8 - só o ponteiro
Atribuívelnão: arr = x; é um errosim: p = x; está ok
&int (*)[10]int **
De onde vem a memóriadeclarada junto com o arrayde onde quer que você o aponte

Um nome de array não é uma variável guardando um endereço - ele é o armazenamento, e o endereço é calculado sob demanda. É por isso que você não pode atribuir a ele.

Perguntas frequentes

Arrays e ponteiros são a mesma coisa em C?

Não, mas é fácil confundir porque um nome de array decai para um ponteiro ao seu primeiro elemento em quase toda expressão. O array em si é um bloco de elementos com um tamanho conhecido; um ponteiro é uma única variável guardando um endereço. sizeof e & são os dois lugares onde a diferença aparece.

Por que sizeof(arr) dá a resposta errada dentro de uma função?

Porque o parâmetro não é um array. void f(int arr[]) é silenciosamente reescrito para void f(int *arr), então sizeof(arr) mede um ponteiro (8 bytes na maioria das máquinas), não o array original. Passe o tamanho como um parâmetro separado.

Qual é a diferença entre int *p[5] e int (*p)[5]?

int *p[5] é um array de 5 ponteiros para int. int (*p)[5] é um único ponteiro para um array de 5 ints. Os parênteses ligam o * ao nome primeiro; sem eles o [] vence, porque tem precedência maior.

arr[i] é mesmo a mesma coisa que *(arr + i)?

Sim - o padrão define a[b] como *(a + b). É também por isso que o estranho i[arr] compila e funciona: a adição é comutativa, então *(i + arr) é o mesmo elemento. Nunca escreva isso em código real, mas isso prova a regra.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR