Publicidade

Aula 27: Noções Básicas de Ponteiros

Aprenda ponteiros em C por meio de visualização de memória. Endereços e desreferenciação explicados com diagramas.

📖 O que aprender nesta página
✅ Essencial que você precisa saber
  • &x pega o endereço, *p desreferencia
  • Declare: int *p = &x;
  • NULL é um marcador explícito de "não aponta para nada"
⭐ Leia se tiver tempo
  • Aritmética de ponteiros com arrays
  • Ponteiro não inicializado vs. ponteiro pendurado
  • Passe um ponteiro para modificar a variável de quem chama
💪 É normal não pegar na primeira tentativa
Ponteiros são o maior obstáculo em C. Quase ninguém entende tudo na primeira leitura, e isso está totalmente bem.
Como voltar a isso
  1. Revise arrays para reconstruir sua intuição sobre memória contígua
  2. Releia o diagrama de memória desta página (a analogia da casa e do endereço) algumas vezes
  3. Comece com o exemplo concreto da função swap
  4. Desenhe a memória e os endereços no papel, à mão
  5. Tudo bem se não fizer sentido hoje. Siga em frente e volte daqui a alguns dias.
💡 Dica: a maior parte da confusão vem de misturar & (pegar o endereço) com * (desreferenciar). Em uma declaração, * marca "tipo ponteiro". Em uma expressão, *p significa "o que p aponta". Fixe essa distinção.

O que é um ponteiro?

Uma variável é uma caixa na memória, e cada caixa tem um endereço. Um ponteiro é simplesmente uma variável que guarda um endereço.

A memória é como um depósito numerado

Pense na memória (RAM) do seu computador como uma longa fileira de prateleiras, em que cada byte (8 bits) tem o seu próprio número sequencial — um endereço. Quando você declara uma variável, o sistema operacional pega uma vaga livre e atribui um endereço a ela.
💾 Memória (RAM) — cada célula é 1 byte
int n = 10; (4 bytes)
int m = 20; (4 bytes)
char c = 'A'; (1 byte)
Não utilizado
Ponto-chave: n começa no endereço 0x1000 / m começa no endereço 0x1004 / c está no endereço 0x1008
Como um int ocupa 4 bytes, a próxima variável cai 4 endereços depois. A memória é alocada de forma contígua, com o tamanho determinado por cada tipo de variável.
Observação: este diagrama assume um sistema típico de 32 ou 64 bits em que int tem 4 bytes. Os tamanhos dos tipos são definidos pela implementação, então verifique com sizeof(int) no seu próprio sistema. Endereços reais também variam entre sistemas operacionais e entre execuções, e as variáveis nem sempre são organizadas de forma tão certinha.
A analogia da casa e do endereço: imagine uma variável como uma casa, com seu endereço sendo o número na rua. Um ponteiro é um papelzinho com esse endereço anotado. Sabendo o endereço, você pode chegar até a casa e ler ou alterar o que está dentro.
Use o operador & para pegar um endereço: escrever &n te dá o endereço da variável n (algo como 0x1000). Você até consegue imprimir com printf("%p\n", &n);.

Variável comum vs. ponteiro

Variável comum
int n = 10;
Guarda o valor (10) diretamente dentro da caixa.
Variável ponteiro
int *p = &n;
Guarda o endereço de n dentro da caixa.
A sintaxe de declaração é tipo *nome;, em que o tipo é o tipo da coisa apontada. Então int *p significa "p é um ponteiro para uma variável int".

Os operadores & e *

Dois operadores são essenciais quando você trabalha com ponteiros.
& (operador de endereço)
&n → endereço de n
Pega o endereço de uma variável.
É o mesmo & que você vem usando em scanf.
* (operador de desreferenciação)
*p → valor no endereço apontado por p
Acessa o valor para o qual o ponteiro aponta.
Você pode usar tanto para ler quanto para escrever.
int n = 10;
int *p = &n; // atribui o endereço de n a p
printf("%d", *p); // → 10 (valor para o qual p aponta)
*p = 99; // reescreve n!
printf("%d", n); // → 99

Endereço e valor — visualizador

Clique nos botões para ver como a variável n e o ponteiro p se relacionam.
Endereço 0x1000
int n
Endereço 0x2000
int *p
Clique nos botões na ordem...

Passo a passo pela memória, linha por linha

Veja como o ponteiro se move pela memória, uma linha de cada vez. O ponteiro p vai alternar entre apontar para n e apontar para m.
Programa
int n = 10;
int m = 20;
int *p;
p = &n;
*p = 99;
p = &m;
*p = 77;
💾 Memória (RAM)
0x1000
int n
0x1004
int m
0x2000
int *p
Clique em "Executar próxima linha" para começar...
O que observar:

A verdade sobre o & no scanf

Você vinha escrevendo scanf("%d", &a); o tempo todo. Esse & é exatamente o mesmo operador — ele está passando um endereço.
Por que o & é necessário aqui?
O scanf precisa saber onde escrever o valor de entrada, então você passa o endereço da variável.
Internamente, o scanf usa um ponteiro para escrever o valor de entrada naquele endereço.
int a;
scanf("%d", &a); // passa o endereço de a

// dentro do scanf, mais ou menos...
// void scanf(char *fmt, int *p){ *p = valor_de_entrada; }
⚠️ Erro comum: se você esquecer o & e escrever scanf("%d", a);, o scanf vai tratar o conteúdo (indeterminado) de a como um endereço e escrever em algum local aleatório da memória — uma forma garantida de quebrar o seu programa.

A função swap — exemplo clássico de ponteiros

Uma função swap que troca duas variáveis é o caso de uso de livro-texto para ponteiros.
A passagem por valor não consegue trocar!
void swap(int a, int b){ ... } não afeta as variáveis de quem chamou — apenas as cópias locais dentro da função são trocadas.
// recebe ponteiros e troca os valores para os quais eles apontam
void swap(int *x, int *y){
  int t = *x;
  *x = *y;
  *y = t;
}

int main(void){
  int a = 5, b = 10;
  swap(&a, &b); // passa os endereços
  printf("%d %d", a, b); // → 10 5
}
A ideia-chave: passar um endereço para uma função é efetivamente passagem por referência. Arrays passados para funções funcionam do mesmo jeito pelo mesmo motivo.

Aritmética de ponteiros (soma e subtração de ponteiros)

Somar ou subtrair um inteiro a um ponteiro move o endereço pelo tamanho do tipo apontado. Essa é a chave para entender a relação entre arrays e ponteiros.
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;        // points to arr[0]

printf("%d\n", *p);       // 10  (arr[0])
printf("%d\n", *(p + 1)); // 20  (arr[1])
printf("%d\n", *(p + 3)); // 40  (arr[3])

p += 2;               // p now points to arr[2]
printf("%d\n", *p);       // 30
p + 1 significa "o próximo elemento", não "o próximo byte".
Com um ponteiro para int (4 bytes), p + 1 avança o endereço em 4 bytes.
10
p+0
20
p+1
30
p+2
40
p+3
50
p+4

Equivalência entre array e ponteiro

Em C, as formas a seguir são todas equivalentes:
Notação de arrayNotação de ponteiroSignificado
arr[0]*arrPrimeiro elemento
arr[i]*(arr + i)i-ésimo elemento
&arr[i]arr + iEndereço do i-ésimo elemento

🎚 Arraste para ver a equivalência em ação

arr[i] e *(arr+i) referem-se exatamente ao mesmo elemento. Arraste o controle e veja as duas notações acendendo a mesma célula.
📝 Notação de array
arr[2]
Acesso por índice (legível)
= 30
🎯 Notação de ponteiro
*(arr + 2)
Avance i elementos a partir do início
= 30
// Aritmética de endereço (int = 4 bytes, base de arr = 0x1000)
arr = 0x1000
arr + 2 = 0x1000 + 2 × 4 = 0x1008
*(arr + 2) = arr[2] = 30
💡 O que o compilador realmente faz: arr[i] é só açúcar sintático para *(arr + i). Engraçado que isso significa que i[arr] (!) também compila — é *(i + arr) = *(arr + i). Não escreva isso de verdade, mas mostra bem que acesso a array é, na raiz, aritmética de ponteiros.

Subtração de ponteiros

int arr[5] = {10, 20, 30, 40, 50};
int *p1 = &arr[1];
int *p2 = &arr[4];
printf("%ld\n", p2 - p1);  // 3 (difference in number of elements)
Subtrair dois ponteiros dá a quantidade de elementos entre eles — não o número de bytes.

O qualificador const — marque valores como inalteráveis

const avisa ao compilador e aos colegas desenvolvedores que uma variável não vai ser modificada. Isso evita bugs e torna sua intenção explícita.

Básico: variáveis const

const int MAX = 100;
printf("%d\n", MAX);  // OK: read freely

MAX = 200;             // ❌ Compile error: cannot modify const
Quando usar: para valores que nunca mudam em tempo de execução — pi, tamanhos de array, constantes de configuração. A vantagem sobre #define é que você ganha checagem de tipo.

Ponteiros e const — três padrões

Combinar ponteiros com const é notoriamente confuso. O significado depende de qual lado do asterisco o const está.
DeclaraçãoO que pode mudarSignificado
const int *p
ou int const *p
p pode mudar
*p não pode mudar
Um ponteiro para um valor constante.
int * const p p não pode mudar
*p pode mudar
Um ponteiro constante — não pode apontar para outro lugar.
const int * const p p não pode mudar
*p não pode mudar
Totalmente travado — nada pode mudar.

Exemplos concretos

int a = 10, b = 20;

// ① const int *p : pointed-to value is read-only
const int *p1 = &a;
p1 = &b;        // ✅ OK: pointer itself can change
*p1 = 30;       // ❌ Error: pointed-to value is immutable

// ② int * const p : pointer itself is read-only
int * const p2 = &a;
*p2 = 30;       // ✅ OK: can modify 'a' through p2
p2 = &b;        // ❌ Error: p2 can't be redirected

// ③ const int * const p : everything locked
const int * const p3 = &a;
*p3 = 30;       // ❌ Error
p3 = &b;        // ❌ Error
Truque de leitura: leia a declaração da direita para a esquerda.
const int *p = "p é um ponteiro para um int const"
int * const p = "p é um ponteiro const para int"

const em parâmetros de função

Quando você passa arrays ou strings para uma função que não precisa modificá-los, use const para deixar essa intenção clara.
// Just reads the string → mark as const
int my_strlen(const char *s) {
    int n = 0;
    while (*s) { s++; n++; }
    return n;
}

// Sums array elements, doesn't modify them → const
int sum(const int arr[], int n) {
    int total = 0;
    for (int i = 0; i < n; i++) total += arr[i];
    return total;
}
Boa prática: sempre marque parâmetros como const quando a função não for modificá-los.
・Sinaliza a intenção: "esta função não vai mexer nos seus dados".
・Modificações acidentais são pegas em tempo de compilação.
・Funções da biblioteca padrão como strlen e strcmp seguem essa convenção.

Literais de string e const

char *s1 = "Hello";        // ⚠️ Discouraged (some compilers warn)
s1[0] = 'J';                // ❌ Undefined behavior!

const char *s2 = "Hello";  // ✅ Correct way
// s2[0] = 'J';  ← caught at compile time

char s3[] = "Hello";         // ✅ Array: modification OK
s3[0] = 'J';                // → "Jello"
Importante: literais de string (strings escritas direto no código-fonte, como "Hello") vivem em memória somente leitura. Sempre receba-os por meio de const char *.
Resumo: const funciona como uma barreira de prevenção contra bugs. Seu código ainda roda sem ele, mas usá-lo pega erros em tempo de compilação e deixa sua intenção cristalina.

Experimente você mesmo — ponteiros

Aqui está um programa que reescreve um valor através de um ponteiro. Vá em frente e rode.
pointer.c
Saída
Pressione "Executar"...
💡 Tente também estas ideias
Observe como *p = 99; reescreve o valor de n. Isso é o que significa alterar um valor indiretamente, por meio de um ponteiro.
Publicidade

Aulas relacionadas

Funções
Aula 26: Arrays como argumentos de função
Como passar um array para uma função em C, e sua conexão com ponteiros.
Avançado
Aula 30: Memória dinâmica (malloc/free)
Alocação dinâmica em C. malloc, free, causas e correções de vazamento de memória.
Avançado
Aula 28: Estruturas (struct)
Definindo uma struct em C, acessando membros e combinando com arrays.
← Aula anterior
Aula 26: Arrays como argumentos
Próxima aula →
Aula 28: Estruturas (struct)

Perguntas Frequentes (FAQ)

P. Por que usar ponteiros? Variáveis comuns não bastam?

R. Ponteiros permitem fazer coisas que simplesmente são impossíveis de outra forma: 1. modificar uma variável de quem chama de dentro de uma função (passagem por referência), 2. alocar memória dinamicamente, 3. construir estruturas de dados complexas como listas e árvores, e 4. trabalhar com strings. Muitos recursos de C não funcionam sem eles.

P. Vivo confundindo * e &. Qual é qual?

R. & (endereço-de) te dá o endereço — "onde a variável mora". * (desreferência) vai até esse endereço e olha "o que está guardado lá". Em uma declaração de ponteiro como int *p;, o * só significa "p é um ponteiro para int".

P. O que é um ponteiro NULL? Qual a diferença para um ponteiro pendurado?

R. Ajuda distinguir três estados:
① Ponteiro NULL — um ponteiro intencionalmente definido como NULL (o valor 0), significando "não aponta para nada". Desreferenciá-lo quebra de forma confiável, o que o torna uma sentinela segura e detectável.
② Ponteiro não inicializado — declarado mas nunca atribuído. Seu valor é indeterminado (pode estar em qualquer lugar da memória), e desreferenciá-lo é comportamento indefinido.
③ Ponteiro pendurado (dangling)um ponteiro que um dia se referia a um objeto válido, mas esse objeto não é mais válido (foi liberado, saiu do escopo, etc.). Os bits ainda podem parecer válidos, mas qualquer acesso é comportamento indefinido.
Bons hábitos defensivos: inicialize ponteiros na declaração (int *p = NULL;), defina p = NULL; logo após free(p) e nunca retorne o endereço de uma variável local.

P. O que significa "um ponteiro para um ponteiro"?

R. Um ponteiro também é só uma variável, então a própria variável ponteiro vive na memória — o que significa que você pode fazer "um ponteiro para um ponteiro": int **pp = &p;. Ponteiros de múltiplos níveis parecem complicados, mas você só está aplicando o mesmo princípio repetidamente. Na prática, raramente são necessários.

Quiz de Revisão

Confira sua compreensão desta aula!

Q1. Em int *p;, o que p guarda?

Um valor inteiro
O endereço de uma variável int
Uma string

int *p é um ponteiro para int, então guarda o endereço de memória de uma variável int.

Q2. Com int x=10; int *p=&x;, qual é o valor de *p?

O endereço de x
10
O endereço do ponteiro p

*p desreferencia o ponteiro. Como p aponta para x, *p é 10.

Q3. O que acontece quando você desreferencia (*p) um ponteiro NULL?

Retorna 0
Erro de execução (falha de segmentação)
Erro de compilação

Desreferenciar um ponteiro NULL causa um erro de execução como uma falha de segmentação. Sempre verifique se é NULL antes de usar um ponteiro.

Compartilhe este artigo
Compartilhar no X (Twitter) Compartilhar no Facebook Compartilhar no LinkedIn Compartilhar no Reddit