Ponteiros: O que É um Endereço de Memória

[346] Ponteiros: O que É um Endereço de Memória

A memória como uma rua de casas numeradas, e o ponteiro como a variável que guarda o número de uma delas. Daqui saem o operador de endereço, os dois papéis distintos do asterisco, a resposta para como fazer uma função alterar o original — e o motivo de o scanf sempre ter exigido aquele prefixo.
Linguagem C

10 min de leitura

Chegamos ao coração do C. Se você entender este artigo — de verdade, não só de passagem — todo o resto da linguagem se abre. Ponteiros têm fama de difíceis, mas a dificuldade não está no conceito; está em nunca ninguém ter explicado o que eles realmente são. Hoje vamos consertar isso. E, de quebra, vamos finalmente responder à pergunta que deixamos em aberto lá na aula sobre funções: "como faço uma função alterar o valor original que passei a ela?".

A memória é uma rua de casas numeradas

Imagine a memória do computador como uma rua enorme, com bilhões de casas enfileiradas. Cada casa guarda um byte de informação, e cada uma tem um número único — seu endereço. Quando você declara uma variável, o C reserva uma ou mais dessas casas e guarda seu valor lá dentro:

int idade = 30;

Essa linha faz duas coisas: reserva um espaço na memória (digamos, quatro casas, porque um int costuma ocupar quatro bytes) e escreve 30 ali. A variável idade é o nome que damos a esse espaço. Mas o espaço também tem um endereço — o número da primeira casa. E é esse endereço que um ponteiro guarda.

O operador &: "onde isto mora?"

Para descobrir o endereço de uma variável, usamos o operador &, chamado de "operador de endereço":

#include <stdio.h>

int main(void) {
    int idade = 30;
    printf("O valor de idade é: %d\n", idade);
    printf("O endereço de idade é: %p\n", (void *) &idade);
    return 0;
}

O %p é o especificador para imprimir endereços. A saída será algo como:

O valor de idade é: 30
O endereço de idade é: 0x7ffee3b4c8ac

Aquele número hexadecimal esquisito é o endereço — a "casa" onde idade vive. Ele muda a cada execução, e não faz mal nenhum não entender o número em si. O importante é a ideia: toda variável tem um endereço, e o & nos dá acesso a ele.

Declarando um ponteiro

Um ponteiro é uma variável como qualquer outra — só que, em vez de guardar um número comum, ela guarda um endereço. Declaramos um ponteiro com um asterisco * antes do nome:

#include <stdio.h>

int main(void) {
    int idade = 30;
    int *p;        // p é um "ponteiro para int"
    p = &idade;    // p agora guarda o endereço de idade

    printf("idade vale: %d\n", idade);
    printf("p aponta para o endereço: %p\n", (void *) p);
    return 0;
}

Leia int *p como "p é um ponteiro para int". O tipo importa: um int * só pode apontar para inteiros, um double * só para double, e assim por diante. O C precisa saber o tipo apontado para saber quantos bytes ler e como interpretá-los.

O operador *: seguindo a seta

Guardar um endereço é metade da história. A outra metade é usar esse endereço para chegar ao valor lá guardado. Isso se chama desreferenciar, e usamos o mesmo asterisco *, agora com outro sentido:

#include <stdio.h>

int main(void) {
    int idade = 30;
    int *p = &idade;

    printf("Valor via nome:      %d\n", idade); // 30
    printf("Valor via ponteiro:  %d\n", *p);    // 30 — segue a seta até idade

    *p = 42;  // escreve 42 no endereço apontado — ou seja, em idade!
    printf("idade agora vale:    %d\n", idade); // 42

    return 0;
}

Preste muita atenção nesta linha: *p = 42. Ela não muda o ponteiro; ela vai até o endereço que p guarda e escreve 42 lá. Como p aponta para idade, quem muda é idade. Alteramos uma variável sem mencioná-la pelo nome. Esse é o superpoder — e o perigo — dos ponteiros.

Note que o * tem dois papéis distintos, e isso confunde muita gente. Na declaração (int *p), ele diz "isto é um ponteiro". No uso (*p), ele diz "vá até o endereço e me dê o valor". São coisas diferentes escritas com o mesmo símbolo.

A recompensa: fazer uma função alterar o original

Lembra da aula Funções, Escopo e Passagem por Valor, quando tentamos dobrar um número dentro de uma função e o original não mudava? O C passa argumentos por valor, em cópia. Agora temos a chave para contornar isso: em vez de passar o valor, passamos o endereço dele. A função recebe uma cópia do endereço — mas o endereço aponta para o original.

#include <stdio.h>

void dobrar(int *x) {
    *x = *x * 2;  // vai até o endereço e dobra o valor que está lá
}

int main(void) {
    int numero = 10;
    dobrar(&numero);   // passamos o ENDEREÇO de numero
    printf("numero agora vale: %d\n", numero); // 20 — mudou de verdade!
    return 0;
}

Saída: numero agora vale: 20

Compare com a versão da aula Funções, Escopo e Passagem por Valor, que imprimia 10. A diferença é inteiramente o * e o &. Agora a função não recebe uma cópia do valor 10; recebe o endereço de numero, e por ele altera o original. Isso é o que chamamos de passagem por referência — que, no C, é feita explicitamente com ponteiros.

Esse padrão está em toda parte. O próprio scanf funciona assim: quando você escreve scanf("%d", &idade), passa o endereço de idade justamente para que a função consiga escrever o valor lido de volta na sua variável. Aquele & que você vinha copiando sem pensar agora faz sentido completo.

Devolver dois valores de uma vez

Uma função só pode ter um return. Mas com ponteiros, ela pode "devolver" quantos valores quisermos, escrevendo-os em endereços que recebeu:

#include <stdio.h>

void dividir(int a, int b, int *quociente, int *resto) {
    *quociente = a / b;
    *resto     = a % b;
}

int main(void) {
    int q, r;
    dividir(17, 5, &q, &r);
    printf("17 / 5 = %d, resto %d\n", q, r); // 3, resto 2
    return 0;
}

Passamos os endereços de q e r; a função preenche ambos. Esse é um idioma clássico do C para funções que precisam produzir mais de um resultado.

O ponteiro nulo: apontando para lugar nenhum

Às vezes precisamos de um ponteiro que ainda não aponta para nada. Para isso existe o NULL — um endereço especial que significa "nenhum". Um ponteiro nunca deve ser usado sem antes apontar para algo válido:

#include <stdio.h>

int main(void) {
    int *p = NULL; // honesto: p ainda não aponta para nada

    if (p != NULL) {
        printf("%d\n", *p); // só desreferencia se for seguro
    } else {
        printf("p não aponta para nada.\n");
    }
    return 0;
}

Aqui está a armadilha mais perigosa dos ponteiros: desreferenciar um ponteiro nulo ou inválido. Se você fizer *p quando p é NULL, o programa quase sempre trava na hora (o famoso segmentation fault). E pior que travar é o caso em que o ponteiro aponta para uma casa aleatória: aí você lê ou escreve num lugar imprevisível, e o desastre é silencioso. A regra de ouro: nunca desreferencie um ponteiro sem ter certeza de que ele aponta para algo válido. Aquele if (p != NULL) é um hábito que vai te salvar muitas vezes.

Ponteiros e o tipo const

Podemos prometer ao compilador que não vamos alterar o valor apontado, usando const. É uma trava de segurança útil quando uma função só precisa ler o dado:

#include <stdio.h>

void imprimir(const int *p) {
    printf("Valor: %d\n", *p);
    // *p = 99; // ERRO: o compilador impede — prometemos não alterar
}

int main(void) {
    int x = 7;
    imprimir(&x);
    return 0;
}

Assinar uma função com const int * comunica claramente a intenção — "só vou ler" — e deixa o compilador garantir que você cumpra a promessa. É a disciplina defensiva do C trabalhando a seu favor.

O que vem a seguir

Hoje desvendamos o conceito central da linguagem: um ponteiro guarda um endereço, o & obtém esse endereço e o * segue até o valor. Com isso, uma função finalmente pode alterar o que recebe — e entendemos por que o scanf sempre pediu aquele &. No próximo artigo, vamos ligar os ponteiros aos vetores e descobrir uma verdade que já vínhamos pressentindo: em C, o nome de um vetor é, na prática, um ponteiro para seu primeiro elemento. A aritmética de ponteiros vai transformar a forma como você percorre dados.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Declare um int x = 100; e um ponteiro int *p = &x;. Imprima, em três linhas: o valor de x, o valor de *p e o endereço guardado em p (com %p). Confirme que os dois primeiros são iguais.

Ver resposta

✓ Resposta:

#include <stdio.h>
int main(void) {
    int x = 100;
    int *p = &x;
    printf("x  = %d\n", x);
    printf("*p = %d\n", *p);              // igual a x
    printf("p  = %p\n", (void *) p);      // endereço de x
    return 0;
}

x e *p imprimem 100 porque p aponta para x — desreferenciar p chega ao mesmo espaço de memória.

Exercício 2

Escreva uma função void incrementar(int *n) que some 1 ao valor apontado. Chame-a três vezes sobre a mesma variável e imprima o resultado (deve terminar 3 unidades acima do inicial).

Ver resposta

✓ Resposta:

#include <stdio.h>
void incrementar(int *n) {
    *n = *n + 1;
}
int main(void) {
    int contador = 0;
    incrementar(&contador);
    incrementar(&contador);
    incrementar(&contador);
    printf("%d\n", contador); // 3
    return 0;
}

Como passamos o endereço, cada chamada altera o mesmo original.

Exercício 3

Escreva uma função void trocar(int *a, int *b) que troque os valores de duas variáveis. Teste com x = 5 e y = 9 e confirme que, após a chamada, x vale 9 e y vale 5.

Ver resposta

✓ Resposta: A troca clássica usa uma variável temporária:

#include <stdio.h>
void trocar(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
int main(void) {
    int x = 5, y = 9;
    trocar(&x, &y);
    printf("x = %d, y = %d\n", x, y); // x = 9, y = 5
    return 0;
}

Sem ponteiros, uma função trocar seria impossível em C: ela só receberia cópias, e o original nunca mudaria. Este exercício é a prova prática de por que ponteiros existem.

Exercício 4

Explique, com suas palavras, por que scanf("%d", &idade) precisa do &, enquanto printf("%d", idade) não precisa.

Ver resposta

✓ Resposta: O scanf precisa escrever o valor lido de volta na sua variável. Para isso, ele precisa do endereço dela — por isso o &idade. Já o printf só precisa ler o valor para exibi-lo; recebe uma cópia do valor e isso basta, então não há necessidade do &. quem vai alterar sua variável precisa do endereço; quem só vai lê-la, não.

Exercício 5

O que acontece se você executar int *p = NULL; printf("%d\n", *p);? Por que isso ocorre, e qual verificação evitaria o problema?

Ver resposta

✓ Resposta: O programa quase certamente trava com um segmentation fault. Isso ocorre porque *p tenta acessar o "endereço nulo" — um endereço que, por definição, não aponta para nenhuma memória válida do programa; o sistema operacional detecta a violação e encerra o processo. A verificação if (p != NULL) { printf("%d\n", *p); } evita o problema, garantindo que só desreferenciamos o ponteiro quando ele aponta para algo real.

Comentários

Mais em Linguagem C

A Arte da Repetição com while, do-while e for
A Arte da Repetição com while, do-while e for

Três laços e um critério para escolher entre eles: for quando o número de…

Structs: Criando Seus Próprios Tipos
Structs: Criando Seus Próprios Tipos

Três variáveis soltas dão conta de uma data; de cem, não. A struct cola o que…

Variáveis, Tipos e a Memória por Trás Deles
Variáveis, Tipos e a Memória por Trás Deles

Um int não tem 4 bytes garantidos: o padrão fixa apenas mínimos e uma ordem, e…