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ória | uma variável guardando um endereço |
sizeof | 40 - o bloco inteiro | 8 - só o ponteiro |
| Atribuível | não: arr = x; é um erro | sim: p = x; está ok |
& dá | int (*)[10] | int ** |
| De onde vem a memória | declarada junto com o array | de 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.