Ponteiros — Básico

Ponteiros são como endereços de rua — o endereço em si não é a casa, mas através dele você encontra aquela casa exata. Domine endereços, e você dominará o recurso mais poderoso de C.

O que é um ponteiro?

Cada byte na memória tem um número único chamado endereço. Uma variável ponteiro é uma variável projetada especificamente para armazenar um endereço.

C
int x = 42;
int *p = &x;

Ponteiros também ocupam memória. Em um sistema de 32 bits, um ponteiro tem 4 bytes; em um sistema de 64 bits, tem 8 bytes, independentemente do tipo para o qual aponta.

O operador de endereço &

& é usado para obter o endereço de uma variável:

C
int a = 10;
printf("Value of a: %d\n", a);
printf("Address of a: %p\n", (void *)&a);

O especificador de formato %p imprime um endereço; o argumento deve ser convertido para void *.

Do que você pode obter o endereço:

Do que você não pode obter o endereço:

O operador de desreferência *

* é usado para acessar os dados que um ponteiro aponta:

C
int x = 42;
int *p = &x;

printf("%d\n", *p);
*p = 100;
printf("%d\n", x);

A primeira saída é 42, a segunda é 100. *p = 100 modifica x através do ponteiro.

💡 Dica: * em uma declaração significa "isto é um ponteiro"; em uma expressão significa "desreferenciar" (obter o valor apontado). O significado depende do contexto.

Desreferenciar um ponteiro não inicializado é um erro grave:

C
int *p;
*p = 10;

O valor de p é aleatório; escrever em um endereço aleatório pode corromper outros dados ou causar falha imediata.

O ponteiro NULL

NULL é um valor especial de ponteiro que significa "não aponta para lugar nenhum". Está definido em vários cabeçalhos incluindo <stddef.h>, e seu valor é tipicamente 0.

C
int *p = NULL;

if (p == NULL) {
    printf("Pointer does not point to a valid address\n");
}

Desreferenciar um ponteiro NULL causa uma falha de segmentação e encerra o programa. Portanto, sempre verifique se um ponteiro é NULL antes de usá-lo:

C
void safe_print(int *p) {
    if (p != NULL) {
        printf("%d\n", *p);
    }
}
⚠️ Atenção: Se um ponteiro não for atribuído imediatamente após a definição, sempre inicialize-o com NULL. Um "ponteiro selvagem" não inicializado tem valor indeterminado e é mais perigoso que um ponteiro NULL porque não pode ser detectado por uma verificação de NULL.

Tipos de ponteiro e passo

O tipo do ponteiro determina quantos bytes são lidos durante a desreferência, e quantos bytes o ponteiro se move durante aritmética (o passo).

C
int a = 10;
double b = 3.14;

int *pi = &a;
double *pd = &b;

O passo do ponteiro equivale ao tamanho do tipo apontado:

C
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;

printf("%d\n", *p);
printf("%d\n", *(p + 1));
printf("%d\n", *(p + 3));

Saída: 10, 20, 40. p+1 não adiciona 1 ao valor do endereço; adiciona sizeof(int) (4 bytes), apontando para o próximo elemento int.

A subtração de ponteiros também é medida em elementos:

C
int *q = &arr[4];
printf("%ld\n", (long)(q - p));

Saída: 4, significando que 4 elementos int separam os dois ponteiros.

💡 Dica: Ponteiros de tipos diferentes não podem ser diretamente atribuídos. int *p = &d; (onde d é double) produzirá um aviso do compilador porque a incompatibilidade de tipos faz com que o número errado de bytes seja lido na desreferência.

Exemplo

Troque os valores de duas variáveis usando ponteiros:

C
#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void) {
    int x = 10, y = 20;
    printf("Before swap: x=%d, y=%d\n", x, y);
    swap(&x, &y);
    printf("After swap: x=%d, y=%d\n", x, y);
    return 0;
}
▶ Experimente
TEXT
Before swap: x=10, y=20
After swap: x=20, y=10

Aqui, endereços são passados à função, que modifica as variáveis originais através de desreferência. Essa é a técnica clássica para "simular passagem por referência" — o parâmetro da função é um ponteiro int *, e a chamada passa &x.

Ponteiros e const

Combinar const com ponteiros tem dois significados:

Ponteiro para uma constante

C
const int *p = &x;
*p = 20;

Ilegal. Você não pode modificar os dados através de p, mas p pode apontar para uma variável diferente:

C
p = &y;

Legal.

Ponteiro constante

C
int * const p = &x;
p = &y;

Ilegal. p não pode mudar o que aponta, mas você pode modificar os dados através de p:

C
*p = 20;

Legal.

Truque de memorização: const à esquerda de * modifica os dados; const à direita de * modifica o ponteiro em si.

Exemplo

Some um vetor usando percurso com ponteiro:

C
#include <stdio.h>

int array_sum(const int *arr, int len) {
    int sum = 0;
    const int *end = arr + len;
    while (arr < end) {
        sum += *arr;
        arr++;
    }
    return sum;
}

int main(void) {
    int data[] = {3, 7, 1, 9, 5};
    int total = array_sum(data, 5);
    printf("Sum: %d\n", total);
    return 0;
}
▶ Experimente
TEXT
Sum: 25

O parâmetro const int *arr indica que a função não modificará o conteúdo do vetor. O ponteiro end marca a posição de parada, evitando a necessidade de índices.

❓ Perguntas Frequentes

P: Quantos bytes uma variável ponteiro ocupa? R: Depende da largura de endereço do sistema: 4 bytes em sistemas de 32 bits, 8 bytes em sistemas de 64 bits, independentemente do tipo apontado.

P: O * em int *p pertence ao tipo ou à variável? R: Pertence à variável. int* p, q; declara p como ponteiro para int e q como int simples. É recomendável declarar apenas um ponteiro por linha.

P: Qual a diferença entre NULL e um ponteiro não inicializado? R: NULL é um valor definido (0) que pode ser verificado; um ponteiro não inicializado tem um valor aleatório e não pode ser detectado de forma confiável, tornando-o mais perigoso.

P: Desreferenciar um ponteiro NULL sempre causa falha? R: Não garantido. O endereço 0 pode ser um endereço válido em alguns sistemas embarcados. No entanto, em sistemas operacionais mainstream, acessar o endereço 0 provoca uma falha de segmentação.

📖 Resumo

📝 Exercícios

  1. Escreva uma função void divide(int a, int b, int *quotient, int *remainder) que retorne o quociente e o resto através de parâmetros de ponteiro.
  2. Escreva um programa que defina uma variável int e uma double, aponte para elas com int * e double * respectivamente, imprima seus endereços e valores, e observe a diferença de passo.
  3. Escreva uma função int *find_max(int *arr, int len) que retorne um ponteiro para o elemento máximo no vetor.
100%