Aritmética de ponteiros
A aritmética de ponteiros em C permite navegar por arrays e estruturas de dados manipulando endereços de memória. Esta aula aborda incremento, decremento, relação com tipos, comparação e cuidados essenciais para uso seguro e eficiente.
A aritmética de ponteiros é um dos recursos mais poderosos e ao mesmo tempo perigosos da linguagem C. Ela permite que você realize operações aritméticas diretamente sobre endereços de memória, possibilitando navegar por arrays, percorrer estruturas de dados e manipular memória de forma eficiente. No entanto, seu uso incorreto pode levar a erros sutis e crashes. Nesta aula, vamos explorar os conceitos fundamentais da aritmética de ponteiros, incluindo incremento, decremento, relação com tipos, comparação e os cuidados necessários para evitar armadilhas.
Incremento e decremento
Quando você incrementa um ponteiro (com ++), ele não simplesmente adiciona 1 ao endereço. Em vez disso, ele avança para o próximo objeto do tipo apontado. O deslocamento real em bytes é igual a sizeof(tipo_apontado). Por exemplo, se você tem um ponteiro para int (que geralmente ocupa 4 bytes), incrementá-lo adiciona 4 ao endereço. Isso permite navegar por 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); // 10
p++; // avança para o próximo int (4 bytes)
printf("Após incremento: %p, valor: %d\n", (void*)p, *p); // 20
p += 2; // avança 2 ints (8 bytes)
printf("Após +=2: %p, valor: %d\n", (void*)p, *p); // 40
p--; // volta um int
printf("Após decremento: %p, valor: %d\n", (void*)p, *p); // 30
return 0;
}
O operador de decremento (--) funciona de forma análoga, movendo o ponteiro para o elemento anterior. Tanto a pré-incremento/decremento quanto a pós-incremento/decremento são válidos, com a mesma semântica de precedência que em variáveis comuns.
Relação com tipos
O tamanho do passo na aritmética de ponteiros depende inteiramente do tipo base do ponteiro. Se você tem um ponteiro para char, incrementar adiciona 1 byte; para double, adiciona 8 bytes (em sistemas típicos). Isso é crucial para percorrer arrays de diferentes tipos corretamente.
#include <stdio.h>
int main() {
char c_arr[] = "abc";
char *cp = c_arr;
printf("char: %p -> %p (incremento %ld byte)\n", cp, cp+1, (long)((char*)(cp+1) - (char*)cp));
int i_arr[] = {1,2,3};
int *ip = i_arr;
printf("int: %p -> %p (incremento %ld bytes)\n", ip, ip+1, (long)((char*)(ip+1) - (char*)ip));
double d_arr[] = {1.0,2.0};
double *dp = d_arr;
printf("double: %p -> %p (incremento %ld bytes)\n", dp, dp+1, (long)((char*)(dp+1) - (char*)dp));
return 0;
}
Outro ponto importante: a aritmética de ponteiros só faz sentido quando o ponteiro aponta para um elemento de um array (ou para uma posição após o último elemento, para fins de comparação). Operar sobre ponteiros que apontam para variáveis isoladas pode resultar em comportamento indefinido.
Comparação
Ponteiros podem ser comparados usando operadores relacionais (==, !=, <, >, <=, >=). A comparação é feita com base nos endereços absolutos (como inteiros sem sinal). Isso é útil para verificar se um ponteiro está dentro dos limites de um array, por exemplo.
#include <stdio.h>
int main() {
int arr[5] = {0,1,2,3,4};
int *p = arr;
int *end = arr + 5; // um após o último
while (p < end) {
printf("%d ", *p);
p++;
}
printf("\n");
// Comparação de igualdade
int *q = &arr[2];
if (q == arr + 2) {
printf("q aponta para arr[2]\n");
}
return 0;
}
É importante notar que a comparação de ponteiros só é bem definida se ambos apontarem para o mesmo array (ou para uma posição após o último). Comparar ponteiros de arrays diferentes ou com variáveis isoladas pode levar a comportamento indefinido.
Cuidados
A aritmética de ponteiros oferece grande poder, mas também exige disciplina. Aqui estão os principais cuidados:
- Nunca desreferencie um ponteiro que não aponte para memória válida. Isso inclui ponteiros nulos, ponteiros que ultrapassaram os limites do array, ou ponteiros que foram liberados (dangling pointers).
- Evite ultrapassar os limites do array. Incrementar um ponteiro além do último elemento e depois desreferenciá-lo é comportamento indefinido. Você pode, no entanto, manter um ponteiro apontando para “um após o último” para fins de comparação, mas nunca desreferenciá-lo.
- Cuidado com a precedência de operadores. Por exemplo,
*p++é interpretado como*(p++), ou seja, desreferencia o valor atual e depois incrementa o ponteiro. Use parênteses quando necessário. - Não misture tipos de ponteiros diferentes em aritmética. A aritmética depende do tipo base; usar um ponteiro
void*não permite aritmética (em C padrão, não é permitido incrementarvoid*). - Lembre-se de que arrays decaem para ponteiros. O nome de um array é um ponteiro constante para o primeiro elemento. Você pode usar aritmética no nome do array (ex.:
arr + i), mas não pode atribuir a ele.
#include <stdio.h>
int main() {
int arr[3] = {1,2,3};
int *p = arr;
// Cuidado: p pode ultrapassar o array
p += 5; // agora p aponta para fora do array
// *p = 10; // ERRO: comportamento indefinido
// Precedência: *p++ vs (*p)++
p = arr;
int x = *p++; // x = 1, p agora aponta para arr[1]
printf("x = %d, *p = %d\n", x, *p);
p = arr;
(*p)++; // incrementa o valor apontado (arr[0] vira 2)
printf("arr[0] = %d\n", arr[0]);
return 0;
}
Seguindo essas boas práticas, você poderá usar aritmética de ponteiros com segurança e eficiência.
Referências
- cppreference.com: Operadores aritméticos
- GNU C Manual: Pointer Arithmetic
- TutorialsPoint: C Pointer Arithmetic
- GeeksforGeeks: Pointer Arithmetic
- Microsoft C++ Documentation: Pointer Arithmetic
- Wikipedia: Pointer arithmetic
Exercícios
- Escreva uma função que recebe um array de inteiros e seu tamanho, e imprime todos os elementos usando aritmética de ponteiros (sem usar índices).
- Explique o que acontece se você incrementar um ponteiro para
voidem C padrão. Isso é permitido? Justifique. - Dado o código a seguir, qual será a saída? Explique passo a passo.
- Escreva um programa que verifique se um ponteiro está dentro dos limites de um array (inclusive no elemento um após o último) usando comparação de ponteiros.
- Qual é a diferença entre
p++ep + 1? Dê um exemplo onde a diferença é importante.
#include <stdio.h>
void print_array(int *arr, int size) {
int *end = arr + size;
for (int *p = arr; p < end; p++) {
printf("%d ", *p);
}
printf("\n");
}
int main() {
int nums[] = {10, 20, 30, 40};
print_array(nums, 4);
return 0;
}
Em C padrão (C89, C99, C11, etc.), não é permitido realizar aritmética em ponteiros para void, pois o compilador não sabe o tamanho do tipo apontado. O operador ++ ou + com void* resulta em erro de compilação. Alguns compiladores (como GCC) permitem como extensão, tratando como tamanho 1 (como se fosse char*), mas isso não é portável.
#include <stdio.h>
int main() {
int arr[3] = {1, 2, 3};
int *p = arr;
printf("%d\n", *p++);
printf("%d\n", *p);
printf("%d\n", (*p)++);
printf("%d\n", *p);
return 0;
}
Saída:
1
2
2
3
Explicação:
1. *p++ é *(p++): desreferencia p (que aponta para arr[0] = 1) e depois incrementa p para apontar para arr[1]. Imprime 1.
2. *p agora imprime o valor em arr[1] = 2.
3. (*p)++ incrementa o valor apontado por p (arr[1] de 2 para 3), mas retorna o valor original (2). Imprime 2.
4. *p imprime o novo valor de arr[1] = 3.
#include <stdio.h>
#include <stdbool.h>
bool is_within(int *p, int *start, int *end) {
// start aponta para o primeiro, end para um após o último
return (p >= start && p < end);
}
int main() {
int arr[5] = {0,1,2,3,4};
int *first = arr;
int *last = arr + 5;
int *p = &arr[2];
printf("p dentro? %s\n", is_within(p, first, last) ? "sim" : "não");
p = arr + 5; // um após o último
printf("p (end) dentro? %s\n", is_within(p, first, last) ? "sim" : "não"); // não
p = arr - 1; // antes do início
printf("p (antes) dentro? %s\n", is_within(p, first, last) ? "sim" : "não"); // não
return 0;
}
p++ incrementa o ponteiro e retorna o valor original (efeito colateral). Já p + 1 apenas calcula um novo endereço sem modificar p. Exemplo:int arr[] = {10,20}; int *p = arr; int x = *p++; // x=10, p agora aponta para arr[1]
Se usássemos int x = *(p + 1);, p não seria alterado e x receberia 20.