Aritmética de ponteiros
A aritmética de ponteiros em C permite navegar por arrays e estruturas de dados de forma eficiente, usando operações como incremento, decremento e comparação. A aula aborda como essas operações se relacionam com os tipos dos ponteiros, os cuidados necessários para evitar erros comuns, e fornece exemplos práticos.
A aritmética de ponteiros é um dos recursos mais poderosos e, ao mesmo tempo, perigosos da linguagem C. Ela permite que você manipule endereços de memória diretamente, realizando operações como incremento, decremento, soma, subtração e comparação. Quando usada corretamente, a aritmética de ponteiros pode tornar o código mais eficiente, especialmente ao percorrer arrays ou gerenciar memória dinâmica. No entanto, erros podem levar a acessos inválidos e comportamento indefinido.
Nesta aula, vamos explorar as principais operações de aritmética de ponteiros, entender como o tipo do ponteiro afeta o resultado, aprender a comparar ponteiros e discutir os cuidados essenciais para evitar armadilhas. Ao final, você terá uma compreensão sólida para usar ponteiros de forma segura e eficiente em seus programas.
Incremento e decremento
Operadores de incremento (++) e decremento (--) podem ser aplicados a ponteiros. Quando você incrementa um ponteiro, ele passa a apontar para o próximo elemento do tipo apontado, não para o próximo byte. O deslocamento real em bytes é igual ao tamanho do tipo apontado (sizeof(tipo)).
Por exemplo, se um ponteiro int* p aponta para o endereço 1000, e sizeof(int) é 4, então p++ fará com que p aponte para o endereço 1004. Isso é fundamental para percorrer arrays de forma natural.
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // p aponta para arr[0]
printf("Endereço inicial: %p\n", (void*)p);
printf("Valor: %d\n", *p);
p++; // avança para o próximo int
printf("Após p++: endereço %p, valor %d\n", (void*)p, *p);
p += 2; // avança dois ints
printf("Após p+=2: endereço %p, valor %d\n", (void*)p, *p);
p--; // volta um int
printf("Após p--: endereço %p, valor %d\n", (void*)p, *p);
return 0;
}
No código acima, p++ move o ponteiro para o próximo elemento do array (arr[1]), e p += 2 move dois elementos adiante (arr[3]). O decremento funciona de forma análoga. É importante notar que a aritmética de ponteiros só é válida dentro dos limites de um array ou de um bloco de memória alocado. Incrementar além do último elemento ou decrementar antes do primeiro causa comportamento indefinido.
Relação com tipos
O tipo do ponteiro determina o incremento real em bytes. Um ponteiro char* avança de 1 em 1 byte, um int* avança de 4 em 4 (em sistemas comuns), e um double* avança de 8 em 8. Isso permite que a aritmética de ponteiros seja independente do tipo, bastando que o ponteiro seja corretamente tipado.
Considere o exemplo a seguir, que usa um ponteiro void* para ilustrar a necessidade de conversão:
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
void *vp = arr;
// Ilegal: aritmética com void* não é permitida (GCC aceita como extensão)
// vp++; // erro: incremento de ponteiro para void
// Correto: converter para o tipo apropriado
int *ip = (int*)vp;
ip++;
printf("Valor: %d\n", *ip); // imprime 2
return 0;
}
Em C padrão, não é permitido fazer aritmética com ponteiros para void, pois o compilador não sabe o tamanho do tipo apontado. Por isso, é necessário converter para um tipo completo antes de realizar operações. Além disso, a aritmética de ponteiros funciona com qualquer tipo, incluindo structs:
#include <stdio.h>
struct Point {
int x, y;
};
int main() {
struct Point pts[] = {{1,2}, {3,4}, {5,6}};
struct Point *p = pts;
printf("Ponto: (%d, %d)\n", p->x, p->y); // (1,2)
p++;
printf("Ponto: (%d, %d)\n", p->x, p->y); // (3,4)
return 0;
}
Neste caso, p++ avança o tamanho da struct (provavelmente 8 bytes, dependendo do alinhamento). Isso é útil para percorrer arrays de structs.
Comparação
Ponteiros podem ser comparados usando operadores relacionais (==, !=, <, >, <=, >=). A comparação é útil para verificar se dois ponteiros apontam para o mesmo local, ou para determinar a ordem relativa dentro de um array.
Quando dois ponteiros apontam para elementos do mesmo array, a comparação funciona como esperado: um ponteiro para um índice menor é considerado "menor" que um ponteiro para um índice maior. Comparar ponteiros de arrays diferentes é comportamento indefinido (embora em alguns sistemas funcione, não é portável).
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p1 = &arr[1]; // aponta para 20
int *p2 = &arr[3]; // aponta para 40
if (p1 < p2) {
printf("p1 está antes de p2\n");
}
if (p1 == &arr[1]) {
printf("p1 aponta para arr[1]\n");
}
// Comparação com NULL
int *p3 = NULL;
if (p3 == NULL) {
printf("p3 é nulo\n");
}
return 0;
}
A comparação com NULL é uma prática comum para verificar se um ponteiro foi inicializado ou se uma alocação falhou. Além disso, é possível subtrair dois ponteiros do mesmo array para obter a distância (número de elementos) entre eles:
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p1 = &arr[1];
int *p2 = &arr[4];
ptrdiff_t diff = p2 - p1; // resultado: 3
printf("Diferença: %td\n", diff); // imprime 3
return 0;
}
O tipo ptrdiff_t (definido em <stddef.h>) é o tipo adequado para armazenar a diferença entre dois ponteiros.
Cuidados
A aritmética de ponteiros oferece grande poder, mas também exige atenção redobrada para evitar erros. Aqui estão os principais cuidados:
- Nunca ultrapasse os limites do array: Incrementar um ponteiro além do último elemento ou decrementar antes do primeiro causa comportamento indefinido. Sempre verifique os limites.
- Cuidado com ponteiros não inicializados: Usar um ponteiro sem inicialização pode levar a acessos a locais aleatórios. Sempre inicialize ponteiros com um endereço válido ou
NULL. - Não misture ponteiros de diferentes arrays: Comparar ou subtrair ponteiros de arrays diferentes é indefinido. A aritmética só é válida dentro do mesmo bloco de memória.
- Evite aritmética com
void*: Em C padrão, não é permitido. Se precisar, converta para um tipo completo. - Prefira índices quando possível: Embora a aritmética de ponteiros seja eficiente, o uso de índices (
arr[i]) é mais legível e seguro. Use ponteiros quando o desempenho for crítico ou quando estiver manipulando alocação dinâmica.
Outro cuidado importante é com o operador sizeof aplicado a ponteiros. sizeof(p) retorna o tamanho do ponteiro (4 ou 8 bytes), não o tamanho do array. Para obter o número de elementos de um array, use sizeof(arr)/sizeof(arr[0]).
#include <stdio.h>
int main() {
int arr[] = {1,2,3,4,5};
int *p = arr;
printf("Tamanho do ponteiro: %zu\n", sizeof(p)); // 4 ou 8
printf("Número de elementos: %zu\n", sizeof(arr)/sizeof(arr[0])); // 5
return 0;
}
Boas práticas
Para usar aritmética de ponteiros de forma segura, siga estas recomendações:
- Sempre mantenha um ponteiro "sentinela" que aponte para o início do array, caso precise reiniciar a navegação.
- Use
constquando o ponteiro não deve modificar os dados apontados. - Documente o código que usa aritmética de ponteiros, explicando os limites e a lógica.
- Teste exaustivamente, especialmente com arrays de tamanho zero ou alocações dinâmicas.
Referências
- cppreference.com - Operadores aritméticos em C
- GNU C Library - Pointer Arithmetic
- learn-c.org - Tutorial sobre ponteiros
- GeeksforGeeks - Pointer Arithmetic in C
- Stack Overflow - Perguntas frequentes sobre aritmética de ponteiros
Exercícios
-
Escreva um programa que declare um array de inteiros de 5 elementos, inicialize-o com valores de sua escolha e use um ponteiro para imprimir todos os elementos usando aritmética de ponteiros (não use índices).
✓ Resposta:
#include <stdio.h> int main() { int arr[] = {10, 20, 30, 40, 50}; int *p = arr; for (int i = 0; i < 5; i++) { printf("%d ", *(p + i)); } printf("\n"); return 0; } -
Dado um array de doubles de 4 elementos, use um ponteiro para calcular a soma de todos os elementos. Imprima o resultado.
✓ Resposta:
#include <stdio.h> int main() { double arr[] = {1.5, 2.3, 4.7, 0.9}; double *p = arr; double soma = 0.0; for (int i = 0; i < 4; i++) { soma += *(p + i); } printf("Soma: %.2f\n", soma); return 0; } -
Escreva um programa que declare um array de caracteres (string) e use um ponteiro para percorrê-lo e contar quantas vogais (a, e, i, o, u) existem. Considere apenas minúsculas.
✓ Resposta:
#include <stdio.h> int main() { char str[] = "hello world"; char *p = str; int vogais = 0; while (*p != '\0') { char c = *p; if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') vogais++; p++; } printf("Vogais: %d\n", vogais); return 0; } -
Dado um array de inteiros, use dois ponteiros para inverter o array in-place (sem usar um array auxiliar). Dica: um ponteiro aponta para o início, outro para o fim; troque os valores e mova os ponteiros.
✓ Resposta:
#include <stdio.h> int main() { int arr[] = {1, 2, 3, 4, 5}; int *inicio = arr; int *fim = arr + 4; // aponta para o último elemento while (inicio < fim) { int temp = *inicio; *inicio = *fim; *fim = temp; inicio++; fim--; } // imprime array invertido for (int i = 0; i < 5; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; } -
Escreva uma função que recebe um array de inteiros e seu tamanho, e retorna a média dos valores usando aritmética de ponteiros (sem usar índices). A função deve ter protótipo:
double media(int *arr, int n);✓ Resposta:#include <stdio.h> double media(int *arr, int n) { int soma = 0; int *p = arr; for (int i = 0; i < n; i++) { soma += *(p + i); } return (double)soma / n; } int main() { int nums[] = {10, 20, 30, 40, 50}; printf("Média: %.2f\n", media(nums, 5)); return 0; }