Arrays e ponteiros na prática
Nesta aula, exploramos a relação entre arrays e ponteiros em C, focando no decay de arrays para ponteiros, as diferenças entre eles e como o operador sizeof se comporta em cada caso. Através de exemplos práticos, você entenderá quando arrays e ponteiros são equivalentes e quando não são.
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
sizeofdentro da função não funcionará como esperado. - Use
constem parâmetros ponteiro quando a função não modificar o array. - Evite usar
sizeofem 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
- cppreference: Array declaration
- cppreference: Pointer declaration
- cppreference: Member access operators
- cppreference: sizeof operator
- GNU C Manual: Pointer Arithmetic
Exercícios
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 desizeof, operando de&, ou quando é uma string literal usada para inicializar um array.Qual a diferença entre
arre&arrquandoarré um array de 10 inteiros? Mostre com um exemplo de aritmética de ponteiros.✓ Resposta:arrdecai para um ponteiro para o primeiro elemento (tipoint*), enquanto&arré um ponteiro para o array inteiro (tipoint (*)[10]). Assim,arr + 1avança 4 bytes (tamanho de um int), mas&arr + 1avança 40 bytes (tamanho do array).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) esizeof(p)será 8 (tamanho do ponteiro). Em 32 bits,sizeof(p)seria 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; }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 é quesizeof(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++) ... }