Operadores & e * — endereços, desreferência e a memória por dentro
Em Java, você nunca precisa saber em qual endereço de memória um objeto está — a JVM gerencia isso. Em C, a memória é sua responsabilidade. E às vezes você precisa exatamente saber onde um dado está, não apenas o que ele vale.
Três situações comuns que só se resolvem com ponteiros:
| Situação | Por que ponteiro é necessário |
|---|---|
| Modificar uma variável dentro de uma função | Parâmetros são cópias — a função precisa do endereço para acessar o original |
| Retornar múltiplos valores de uma função | return só devolve um valor; ponteiros permitem escrever em vários endereços |
| Alocar memória em tempo de execução | Arrays dinâmicos, listas encadeadas — tudo requer ponteiros para o bloco alocado |
Imagine a memória RAM como uma sequência enorme de caixas numeradas. Cada caixa tem um endereço (um número) e guarda um valor. Quando você declara int x = 42;, o compilador reserva 4 caixas consecutivas (pois int ocupa 4 bytes) e coloca o valor 42 lá.
| Endereço | Conteúdo | Variável |
|---|---|---|
0x1000 | 42 (byte 0) | int x = 42 |
0x1001 | 0 (byte 1) | |
0x1002 | 0 (byte 2) | |
0x1003 | 0 (byte 3) | |
0x1004 | 0x1000 | int *p = &x |
O ponteiro p guarda o valor 0x1000 — o endereço onde x começa. Ele aponta para x.
int * e um double * têm o mesmo tamanho de 8 bytes; o tipo informa apenas como interpretar o que está no endereço.
& — Endereço-de& (lê-se "endereço de") aplicado a uma variável retorna o endereço de memória onde ela está armazenada. O resultado é um ponteiro para aquela variável.
#include <stdio.h>
int main() {
int x = 42;
float f = 3.14f;
char c = 'A';
/* %p imprime o endereço em hexadecimal */
printf("Valor de x: %d\n", x);
printf("Endereço de x: %p\n", &x); // ex: 0x7ffd2a1b
printf("Valor de f: %.2f\n", f);
printf("Endereço de f: %p\n", &f);
printf("Valor de c: %c\n", c);
printf("Endereço de c: %p\n", &c);
/* Endereços são consecutivos na stack — observe a diferença */
printf("\nDiferença x→f: %ld bytes\n",
(char*)&x - (char*)&f);
return 0;
}
& antes! Toda vez que escreveu scanf("%d", &variavel), estava passando o endereço da variável para que scanf pudesse escrever o valor digitado diretamente naquele endereço de memória. Agora você sabe exatamente por quê.
Um ponteiro é declarado com um asterisco * entre o tipo e o nome:
int *p; // ponteiro para int float *pf; // ponteiro para float char *pc; // ponteiro para char double *pd; // ponteiro para double
&variavel ou com NULL.
int *p; // ❌ ponteiro "selvagem" — aponta para lugar desconhecido *p = 10; // ❌ comportamento indefinido — pode destruir dados! int *q = NULL; // ✅ ponteiro nulo — seguro, indica "não aponta para nada" int x = 5; int *r = &x; // ✅ ponteiro válido — aponta para x
#include <stdio.h>
int main() {
int x = 10; // variável comum: armazena o valor 10
int *p = &x; // p armazena o ENDEREÇO de x
printf("x = %d\n", x); // valor de x: 10
printf("&x = %p\n", &x); // endereço de x: ex. 0x7ffd...
printf("p = %p\n", p); // valor de p (= endereço de x)
printf("&p = %p\n", &p); // endereço do próprio ponteiro p
printf("*p = %d\n", *p); // valor no endereço apontado: 10
return 0;
}
Repare: &x e p têm o mesmo valor (o endereço de x), mas &p é diferente — é o endereço do próprio ponteiro, que também precisa de espaço na memória.
* — Desreferência* aplicado a um ponteiro (fora da declaração) é chamado de desreferência (ou indireção). Ele acessa o valor armazenado no endereço apontado — como se você "seguisse a seta" do ponteiro até o dado real.
O * tem três significados distintos em C — é crucial não confundi-los:
| Contexto | Significado | Exemplo |
|---|---|---|
| Na declaração | "Esta variável é um ponteiro" | int *p; |
| Em expressão (leitura) | "Leia o valor no endereço apontado" | printf("%d", *p); |
| Em expressão (escrita) | "Escreva no endereço apontado" | *p = 100; |
#include <stdio.h>
int main() {
int x = 5;
int *p = &x;
printf("Antes: x = %d\n", x); // 5
*p = 100; // escreve 100 no endereço de x
printf("Depois: x = %d\n", x); // 100 — x foi alterado via ponteiro!
printf("Depois: *p = %d\n", *p); // 100 — *p e x são a mesma posição
(*p)++; // incrementa x através do ponteiro
printf("Após ++: x = %d\n", x); // 101
return 0;
}
*p++ e (*p)++ são completamente diferentes. O operador ++ tem precedência maior que *, então *p++ incrementa o ponteiro p (faz ele apontar para o próximo endereço), e só depois desreferencia. (*p)++ incrementa o valor apontado. Use parênteses para deixar a intenção clara.
O uso mais imediato de ponteiros é permitir que funções modifiquem variáveis do chamador — o que parâmetros por valor nunca conseguem:
#include <stdio.h> /* Troca os valores de duas variáveis — impossível sem ponteiros */ void trocar(int *a, int *b) { int temp = *a; // guarda o valor em *a *a = *b; // copia *b para *a *b = temp; // copia temp para *b } /* Divide e retorna quociente + resto via ponteiros */ void dividir(int dividendo, int divisor, int *quociente, int *resto) { *quociente = dividendo / divisor; *resto = dividendo % divisor; } int main() { int x = 10, y = 20; printf("Antes: x=%d y=%d\n", x, y); trocar(&x, &y); printf("Depois: x=%d y=%d\n", x, y); int q, r; dividir(17, 5, &q, &r); printf("\n17 ÷ 5 = %d resto %d\n", q, r); return 0; }
NULLNULL é uma constante especial (geralmente definida como 0 ou (void*)0) que representa um ponteiro que não aponta para nada. É o valor seguro para ponteiros não inicializados ou para indicar ausência de dado válido.
#include <stdio.h>
int *buscar(int v[], int tam, int alvo) {
for (int i = 0; i < tam; i++)
if (v[i] == alvo)
return &v[i]; // retorna endereço do elemento encontrado
return NULL; // não encontrado — retorna NULL
}
int main() {
int notas[] = {7, 3, 9, 5, 8};
int *p = buscar(notas, 5, 9);
if (p != NULL) // SEMPRE verifique antes de desreferenciar
printf("Encontrado: %d (endereço %p)\n", *p, p);
else
printf("Não encontrado.\n");
int *q = buscar(notas, 5, 42);
if (q != NULL)
printf("Encontrado: %d\n", *q);
else
printf("42 não encontrado.\n");
return 0;
}
*NULL causa segmentation fault imediato — o programa trava. Sempre verifique if (p != NULL) antes de usar *p, especialmente quando o ponteiro veio de uma função que pode retornar NULL.
O tipo antes do * não afeta o tamanho do ponteiro (sempre 8 bytes em 64-bit), mas determina como o compilador interpreta os bytes no endereço e quanto avança em aritmética de ponteiros.
#include <stdio.h>
int main() {
int vi = 42;
float vf = 3.14f;
double vd = 2.718;
char vc = 'Z';
int *pi = &vi;
float *pf = &vf;
double *pd = &vd;
char *pc = &vc;
printf("sizeof(int*) = %zu bytes\n", sizeof(pi));
printf("sizeof(float*) = %zu bytes\n", sizeof(pf));
printf("sizeof(double*) = %zu bytes\n", sizeof(pd));
printf("sizeof(char*) = %zu bytes\n", sizeof(pc));
printf("\nValores via desreferência:\n");
printf("*pi = %d\n", *pi);
printf("*pf = %.2f\n", *pf);
printf("*pd = %.3f\n", *pd);
printf("*pc = %c\n", *pc);
return 0;
}
void *): É um ponteiro genérico — pode armazenar qualquer endereço, mas não pode ser desreferenciado diretamente (o compilador não sabe quantos bytes ler). É usado em funções genéricas como malloc(), que retorna void * e precisa ser convertido para o tipo adequado.
Até agora, toda a memória foi alocada em tempo de compilação — o compilador reserva espaço fixo para cada variável. Com ponteiros, é possível pedir memória em tempo de execução, na quantidade exata que o programa precisar.
malloc(), calloc() e realloc() da biblioteca <stdlib.h>, e a memória deve ser liberada com free() quando não for mais necessária.
| Região | O que armazena | Gerenciamento |
|---|---|---|
| Stack (Pilha) | Variáveis locais, parâmetros, frames de função | Automático — criadas e destruídas com cada chamada |
| Heap | Memória alocada dinamicamente com malloc |
Manual — você aloca e você libera com free |
| Segmento de dados | Variáveis globais e static |
Automático — existem durante toda a execução |
| Segmento de código | Código compilado das funções | Fixo — carregado uma vez pelo SO |
malloc() — alocando um bloco de memória#include <stdio.h> #include <stdlib.h> // necessário para malloc e free int main() { int n; printf("Quantos valores? "); scanf("%d", &n); /* Aloca n inteiros no heap — malloc retorna void* */ int *vetor = (int *) malloc(n * sizeof(int)); /* SEMPRE verifique se a alocação teve sucesso */ if (vetor == NULL) { printf("Erro: memória insuficiente!\n"); return 1; } /* Usa o vetor normalmente */ for (int i = 0; i < n; i++) { printf("Valor %d: ", i + 1); scanf("%d", &vetor[i]); } printf("\nVocê digitou: "); for (int i = 0; i < n; i++) printf("%d ", vetor[i]); printf("\n"); /* Libera a memória — obrigatório para evitar vazamento */ free(vetor); vetor = NULL; // boa prática: evita uso acidental após free return 0; }
| Função | Sintaxe | Inicializa com zero? | Uso típico |
|---|---|---|---|
malloc(n) |
malloc(n * sizeof(tipo)) |
❌ Contém lixo | Alocação simples — quando o programa vai preencher imediatamente |
calloc(qtd, tam) |
calloc(n, sizeof(tipo)) |
✅ Zeros | Quando os valores iniciais precisam ser zero (contadores, flags) |
realloc(p, novo_n) |
realloc(p, novo_n * sizeof(tipo)) |
— | Redimensionar um bloco já alocado (crescimento dinâmico) |
calloc
#include <stdio.h>
#include <stdlib.h>
int main() {
int n = 5;
/* calloc: aloca 5 ints e inicializa todos com 0 */
int *contadores = (int *) calloc(n, sizeof(int));
if (contadores == NULL) { printf("Sem memória!\n"); return 1; }
printf("Valores iniciais (calloc): ");
for (int i = 0; i < n; i++)
printf("%d ", contadores[i]); // todos serão 0
printf("\n");
contadores[2] = 10;
contadores[4] = 25;
printf("Após modificação: ");
for (int i = 0; i < n; i++)
printf("%d ", contadores[i]);
printf("\n");
free(contadores);
return 0;
}
malloc/calloc e não liberar com free, essa memória permanece ocupada até o programa terminar — mesmo que nenhuma variável mais a referencie. Em programas longos isso consome memória progressivamente. Regra: para cada malloc, deve haver um free correspondente.
| Operador | Nome | Onde aparece | O que faz | Exemplo |
|---|---|---|---|---|
& |
Endereço-de | Expressão | Retorna o endereço de memória de uma variável | p = &x |
*tipo |
Declaração de ponteiro | Declaração | Declara que a variável é um ponteiro | int *p; |
*p |
Desreferência (leitura) | Expressão | Lê o valor no endereço apontado | x = *p; |
*p = val |
Desreferência (escrita) | Expressão | Escreve no endereço apontado | *p = 10; |
-> |
Seta | Expressão | Acessa campo de struct via ponteiro (= (*p).campo) |
p->x |
NULL |
Ponteiro nulo | Declaração / teste | Ponteiro que não aponta para nada — valor seguro | p = NULL; |
| Aspecto | C | Java |
|---|---|---|
| Ponteiros explícitos | ✅ Sim — você manipula endereços diretamente | ❌ Não — Java esconde ponteiros; usa referências implícitas |
| Gerenciamento de memória | Manual — malloc e free são sua responsabilidade |
Automático — Garbage Collector libera memória não usada |
| Ponteiro nulo | NULL (definido em <stddef.h>) |
null (palavra-chave da linguagem) |
| Desreferenciar nulo | Segmentation fault (falha silenciosa) | NullPointerException (exceção capturável) |
| Memory leak | ⚠️ Possível e silencioso — responsabilidade do programador | ✅ Improvável — GC coleta memória inacessível |
| Acesso à memória fora dos limites | ⚠️ Comportamento indefinido — corrupção silenciosa | ✅ Lança ArrayIndexOutOfBoundsException |
void dobrar(int *n) {
*n = *n * 2;
}
int main() {
int x = 5;
dobrar(&x);
printf("%d\n", x); // 10
}
static void dobrar(int n) {
n = n * 2; // apenas a cópia
}
// Java não permite modificar
// primitivos pelo chamador.
// Solução: retornar o valor ou
// usar Integer[] de um elemento.