Um placar guarda a pontuação de um jogador. Você cria uma função para somar 10 pontos, mas o placar original não muda.
Funções e exemplo de uso: as linhas de uso vão dentro do main
void adicionar(int pontos) {
pontos += 10; // altera apenas a cópia local
}
int placar = 20;
adicionar(placar); // placar continua 20
Se a função só recebe cópias, como ela pode alcançar a variável original e atualizá-la?
Pense em um endereço residencial. Você não entrega a casa a alguém: entrega o endereço, e a pessoa vai até lá.
Conceitos centrais
Toda variável ocupa um lugar na memória, e esse lugar tem um endereço. Um ponteiro é uma variável cujo valor é um endereço. Por meio dele, é possível ler e alterar o objeto que mora naquele endereço.
int *p
Declara p como ponteiro para int.
&x
Operador de endereço: o endereço da variável x.
*p
Desreferência (“seguir a seta”): o objeto que está no endereço guardado em p.
NULL
Valor especial que significa “não aponta para nada”.
Trecho: coloque dentro do main de um programa
int numero = 20;
int *p = № // p guarda o endereço de numero
*p = 30; // escreve 30 no endereço: numero agora vale 30
printf("%d %d\n", numero, *p); // 30 30
Um retrato possível da memória depois de executar o código acima (endereços ilustrativos)
Endereço
Nome
Valor guardado
0x1000
numero
30
0x2000
p
0x1000
Ideia-chave
Leia * de duas formas, conforme o contexto. Na declaração, int *p diz que p é um ponteiro para int. Em uma expressão, *p significa o valor apontado. & e * são inversos: *&x é o próprio x.
Passagem por valor, mesmo com ponteiros
C sempre passa argumentos por valor. No exemplo do placar, que aparece completo logo abaixo, em “Na prática”, quando você passa &placar, a função recebe uma cópia do endereço. Como as duas cópias apontam para o mesmo lugar, escrever em *pontos altera o placar original. É isso que costumamos chamar de “passagem por referência” em C.
Na prática
Programa completo: compile e execute
#include <stdio.h>
void adicionar(int *pontos, int bonus) {
*pontos += bonus;
}
int main(void) {
int placar = 20;
adicionar(&placar, 10);
printf("%d\n", placar);
return 0;
}
Saída: 30
Devolver mais de um resultado
Uma função em C retorna um único valor. Com ponteiros de saída, ela pode preencher quantos resultados forem necessários. scanf("%d", &x) usa esse mesmo padrão.
No exemplo abaixo, o vetor é recebido como const int v[]. O const avisa que a função só lê o vetor e não o altera. Ele é explicado com mais detalhes no fim da aula.
Funções e exemplo de uso: as linhas de uso vão dentro do main
// Pré-condições: n >= 1; menor e maior apontam para variáveis válidas,
// diferentes entre si e fora do vetor v.
void min_max(const int v[], int n, int *menor, int *maior) {
*menor = *maior = v[0];
for (int i = 1; i < n; i++) {
if (v[i] < *menor) *menor = v[i];
if (v[i] > *maior) *maior = v[i];
}
}
int w[] = {4, -2, 9, 0}, mn, mx;
min_max(w, 4, &mn, &mx); // mn = -2, mx = 9
Use . com a variável e -> com o ponteiro. Os parênteses em (*p).nota são necessários, porque . tem precedência maior que *.
Ponteiro para ponteiro Aprofundamento
Um ponteiro também é uma variável, e por isso também tem endereço. Um ponteiro para ponteiro guarda esse endereço. Este programa é independente dos exemplos anteriores:
Programa completo: compile e execute
#include <stdio.h>
int main(void) {
int x = 7, y = 9;
int *p = &x; // p aponta para x
int **pp = &p; // pp aponta para p
**pp = 8; // dois saltos: altera x
*pp = &y; // um salto: altera o próprio p, que passa a apontar para y
printf("%d %d %d\n", x, *p, **pp);
return 0;
}
Saída: 8 9 9
*pp é o próprio p; **pp é o inteiro para onde p aponta. Use isso quando uma função precisa alterar o ponteiro do chamador, por exemplo para fazê-lo apontar para outro lugar.
Ponteiros e vetores
Os elementos de um vetor ficam lado a lado na memória, sem espaços entre eles. Em quase todas as expressões, o nome do vetor é convertido em um ponteiro para o primeiro elemento. Por isso, int *p = v; é o mesmo que int *p = &v[0];, e uma função que recebe int v[] na verdade recebe int *v.
Compile e execute este programa:
Programa completo: compile e execute
#include <stdio.h>
int main(void) {
int v[] = {10, 20, 30, 40, 50};
int *p = v; // o mesmo que &v[0]
printf("*p = %d\n", *p); // primeiro elemento
printf("*(p + 2) = %d\n", *(p + 2)); // avança 2 elementos
printf("p[2] = %d\n", p[2]); // mesma coisa, outra notação
printf("&v[3] - &v[0] = %td elementos\n", &v[3] - &v[0]);
printf("p + 1 fica %td bytes depois de p\n",
(char *)(p + 1) - (char *)p);
return 0;
}
*p = 10
*(p + 2) = 30
p[2] = 30
&v[3] - &v[0] = 3 elementos
p + 1 fica 4 bytes depois de p
A última linha usa um truque, converter para (char *), só para medir a distância em bytes. É uma curiosidade, e não algo que você precise usar no dia a dia. A figura mostra o que acontece na memória. Os endereços são ilustrativos: os reais mudam a cada execução, mas a distância de 4 bytes entre vizinhos é sempre a mesma em plataformas com int de 4 bytes.
Somar 1 a um ponteiro avança um elemento, e não um byte. O compilador multiplica pelo tamanho do tipo, sizeof(int). Por isso, *(p + i) e p[i] chegam sempre ao mesmo lugar. O endereço logo após o fim (v + n) serve como marca de parada em laços, mas ler o conteúdo dele é comportamento indefinido.
Percorrendo um vetor com ponteiro
Em vez de um índice i que vai de 0 a n − 1, o laço pode usar um ponteiro que anda de v até v + n. A cada p++, ele avança para o próximo elemento:
Funções: copie para um programa que tenha main
int soma(const int *v, int n) {
int total = 0;
for (const int *p = v; p < v + n; p++) { // para ao chegar em "fim"
total += *p; // lê o elemento apontado
}
return total;
}
Exemplo real: leituras de um sensor
Uma estação meteorológica registra a temperatura de hora em hora. O programa precisa da média e do pico do dia, além de saber em qual leitura o pico aconteceu. A função maior devolve o endereço da maior leitura, e não o valor. Com o endereço, temos as duas coisas: *pico dá o valor, e pico - temp dá a posição.
Programa completo: compile e execute
#include <stdio.h>
// Soma as n leituras percorrendo o vetor com um ponteiro.
int soma(const int *v, int n) {
int total = 0;
for (const int *p = v; p < v + n; p++) {
total += *p;
}
return total;
}
// Devolve o ENDEREÇO da maior leitura. Pré-condição: n >= 1.
const int *maior(const int *v, int n) {
const int *pico = v;
for (const int *p = v + 1; p < v + n; p++) {
if (*p > *pico) {
pico = p;
}
}
return pico;
}
int main(void) {
int temp[] = {21, 23, 27, 25, 22}; // leituras de hora em hora
int n = sizeof temp / sizeof temp[0]; // 5: aqui sizeof enxerga o vetor
const int *pico = maior(temp, n);
printf("Leituras: %d\n", n);
printf("Media: %.1f graus\n", soma(temp, n) / (float)n);
printf("Pico: %d graus, na leitura %td\n", *pico, pico - temp);
return 0;
}
A expressão sizeof temp / sizeof temp[0] divide o tamanho do vetor inteiro pelo tamanho de um elemento. Em main, temp ainda é um vetor. Em uma plataforma com int de 4 bytes, sizeof temp vale 20 e sizeof temp[0] vale 4. Em qualquer plataforma, a divisão dá n = 5. Dentro de soma, isso não funcionaria, como mostra a primeira armadilha abaixo.
pico - temp
A diferença entre dois ponteiros do mesmo vetor conta elementos. O pico está em temp + 2, então a diferença é 2. O especificador %td imprime esse tipo de valor (ptrdiff_t).
(float)n
Sem a conversão, 118 / 5 seria uma divisão inteira e daria 23, em vez de 23.6.
Armadilhas clássicas
sizeof em parâmetro: dentro de void f(int v[]), sizeof v é o tamanho de um ponteiro (8 bytes em x86-64), e não o do vetor. Por isso, as funções sempre recebem n.
Ponteiro solto (dangling): nunca retorne &local. A variável local deixa de existir quando a função termina.
Esquecer o &:scanf("%d", x) passa o valor de x como se fosse um endereço.
Sair dos limites: você pode calcular v + n, mas não pode desreferenciá-lo.
Use const para documentar intenção
const int *p permite ler *p, mas não alterá-lo. Em uma assinatura como int soma(const int *v, int n), isso avisa a quem chama que o vetor não será modificado.
Exercícios progressivos
Cada exercício parte de uma situação de sistemas reais. Antes de abrir a dica, desenhe as variáveis como caixas e os ponteiros como setas, como na figura de ponteiros e vetores. A dica não entrega a resposta: ela indica a pergunta certa a fazer.
1. O contador de passos Básico
Um aplicativo de saúde guarda os passos do dia em passos. O módulo do relógio recebe um ponteiro para essa variável e soma os passos que detectou. Logo depois, o celular soma os seus:
Trecho: coloque dentro do main de um programa
int passos = 4000;
int *contador = &passos; // o relógio recebe o endereço
*contador = *contador + 500; // relógio detectou 500 passos
passos = passos + 100; // celular detectou 100 passos
printf("%d %d\n", passos, *contador);
O que o programa imprime? E se o relógio tivesse recebido uma cópia, com int copia = passos; e copia = copia + 500;, os 500 passos apareceriam no total?
Ver uma dica
Conte quantas variáveis int existem de verdade nesse código. contador é um segundo número de passos ou uma seta para o primeiro? Depois de responder, siga cada linha atualizando uma única caixa.
Conferir a resposta
Saída: 4600 4600.
Existe um único inteiro, passos. contador guarda o endereço dele, então *contador e passos são dois nomes para a mesma caixa: 4000 + 500 + 100 = 4600, lido pelos dois caminhos.
Linha
passos
*contador
início
4000
4000
*contador = *contador + 500
4500
4500
passos = passos + 100
4600
4600
Com uma cópia, os 500 passos seriam somados em outra caixa. O total continuaria sem eles: o relógio contaria passos que o aplicativo nunca veria. É exatamente o bug do placar no início da aula.
2. Esvaziar o carrinho Básico
Em uma loja virtual, o botão “Esvaziar carrinho” precisa zerar a quantidade de itens e o valor total, que são variáveis da função main. Um estagiário escreveu a função abaixo, mas o carrinho continua cheio depois da chamada:
Funções e exemplo de uso: as linhas de uso vão dentro do main
void esvaziar(int itens, float total) {
itens = 0;
total = 0.0f;
}
int itens = 3;
float total = 89.90f;
esvaziar(itens, total); // itens continua 3, total continua 89.90
Explique o motivo e corrija a função e a chamada. Depois da correção, printf("Itens: %d, total: %.2f\n", itens, total); deve imprimir Itens: 0, total: 0.00.
Ver uma dica
O que exatamente a função recebe quando você escreve esvaziar(itens, total): as variáveis de main ou os valores que estavam nelas? Para alterar duas variáveis do chamador, de que a função precisa receber? E o que muda na chamada?
Conferir a resposta
A versão do estagiário recebe cópias de 3 e 89.90 e zera essas cópias, que somem quando a função termina. As variáveis de main nunca são tocadas.
Funções e exemplo de uso: as linhas de uso vão dentro do main
void esvaziar(int *itens, float *total) {
*itens = 0; // escreve no endereço recebido
*total = 0.0f;
}
int itens = 3;
float total = 89.90f;
esvaziar(&itens, &total); // passa os ENDEREÇOS
printf("Itens: %d, total: %.2f\n", itens, total);
São duas mudanças: na função, os parâmetros viram ponteiros (int *, float *) e as atribuições passam a usar *. Na chamada, passamos &itens e &total.
Erro comum: mudar só a assinatura e esquecer o & na chamada. O compilador acusa o problema: para o int, costuma ser um aviso de que se passou um valor onde se esperava um endereço; para o float, o MSVC dá erro de compilação. Nunca ignore um aviso: ele quase sempre aponta um bug real.
3. Caixa eletrônico Intermediário
Um caixa eletrônico precisa de uma função de saque que informe duas coisas: se o saque foi autorizado e qual é o novo saldo. Escreva int sacar(float *saldo, float valor) com estas regras:
Se valor for menor ou igual a zero, ou maior que o saldo, recusa: retorna 0 e não altera o saldo.
Caso contrário, debita o valor e retorna 1.
Partindo de saldo 100, um saque de 30 e depois um saque de 100 devem produzir:
1 70.00
0 70.00
Ver uma dica
Uma função em C só retorna um valor. Qual das duas informações vai no return, e qual vai por ponteiro? Pense também na ordem: em que momento é seguro alterar *saldo? Antes ou depois de verificar as regras?
Conferir a resposta
Funções e exemplo de uso: as linhas de uso vão dentro do main
int sacar(float *saldo, float valor) {
if (valor <= 0.0f || valor > *saldo) {
return 0; // recusado: saldo intacto
}
*saldo -= valor; // só altera depois de validar
return 1;
}
float saldo = 100.0f;
int ok = sacar(&saldo, 30.0f);
printf("%d %.2f\n", ok, saldo); // 1 70.00
ok = sacar(&saldo, 100.0f);
printf("%d %.2f\n", ok, saldo); // 0 70.00
Esse é um padrão muito comum em C: o retorno indica sucesso ou erro, e os ponteiros carregam os resultados. A própria scanf funciona assim: retorna quantos valores conseguiu ler e grava os valores nos endereços recebidos.
Por que validar antes de alterar? Se a função debitasse primeiro e verificasse depois, um erro no meio deixaria o saldo em um estado inválido. Em sistemas financeiros, uma operação deve acontecer por completo ou não acontecer.
Para ir além: bancos reais não usam float para dinheiro, pela imprecisão comentada na aula de Struct. Eles guardam centavos em inteiros.
4. Fila de impressão Intermediário
A impressora do laboratório guarda os números dos trabalhos em uma fila: int fila[] = {101, 102, 103, 104};. O trabalho 103 é a prova que começa em 5 minutos, e o técnico quer trocá-lo de lugar com o primeiro da fila.
Escreva void trocar(int *a, int *b) e use-a para trocar fila[0] com fila[2]. A fila deve ficar 103 102 101 104. Depois, explique por que a versão void trocar(int a, int b) não funcionaria.
Ver uma dica
Faça a troca à mão com dois copos cheios de sucos diferentes. Você consegue trocar o conteúdo sem um terceiro copo? Na chamada, como passar o endereço de um elemento do vetor, e não do vetor inteiro?
Conferir a resposta
Funções e exemplo de uso: as linhas de uso vão dentro do main
void trocar(int *a, int *b) {
int temp = *a; // o "terceiro copo"
*a = *b;
*b = temp;
}
int fila[] = {101, 102, 103, 104};
trocar(&fila[0], &fila[2]); // endereços dos dois elementos
for (int i = 0; i < 4; i++) printf("%d ", fila[i]); // 103 102 101 104
Sem temp, a linha *a = *b apagaria o 101 antes de ele ser copiado para o outro lado. Com int a, int b, a função trocaria apenas suas cópias locais, e a fila não mudaria.
&fila[2] também pode ser escrito como fila + 2. Trocar um elemento com ele mesmo (trocar(&fila[1], &fila[1])) funciona corretamente nesta versão. O desafio hacker mostra uma versão “esperta” que falha exatamente nesse caso.
Guarde esta função: bubble sort e selection sort, nas próximas aulas, são basicamente uma sequência de chamadas a trocar.
5. Monitor cardíaco Intermediário
Um monitor cardíaco guarda as últimas leituras de batimentos por minuto (BPM) em um vetor. Para comparar a leitura atual com as vizinhas, o software mantém um ponteiro atual:
Trecho: coloque dentro do main de um programa
int bpm[] = {72, 75, 90, 110, 95};
int *atual = bpm + 2;
Sem executar, responda:
Quanto valem *atual, atual[-1] e *(atual + 1)? O que cada um representa para o médico?
Quanto vale (bpm + 4) - atual? O que esse número significa?
O programa pode ler atual[3]? Por quê?
Ver uma dica
Desenhe o vetor como na figura da seção de vetores e marque a casa para onde atual aponta. Lembre que atual[k] é o mesmo que *(atual + k), e que k pode ser negativo. Para o item 3, conte quantas casas existem depois de atual.
Conferir a resposta
*atual = 90, a leitura atual. atual[-1] = 75, a leitura anterior. *(atual + 1) = 110, a próxima. O monitor percebe que os batimentos subiram de 75 para 90 e depois para 110.
(bpm + 4) - atual = 2. A diferença entre ponteiros conta elementos: faltam 2 leituras até a última.
Não.atual + 3 é bpm + 5, a posição logo depois do fim do vetor. Esse endereço pode ser calculado e comparado, mas ler o conteúdo dele é comportamento indefinido. O programa pode imprimir lixo, travar ou funcionar por acaso.
atual[-1] só é válido porque atual não aponta para o início do vetor. Em código real, verifique os limites antes de olhar vizinhos: if (atual > bpm) antes de ler a anterior e if (atual + 1 < bpm + 5) antes de ler a próxima.
6. Quem pode devolver um endereço? Avançado
No sistema de uma loja, duas funções devolvem ponteiros. A primeira encontra o produto mais vendido e devolve o endereço dele, para que o chamador possa atualizar o estoque diretamente. A segunda foi escrita às pressas:
Funções: copie para um programa que tenha main
// Devolve o endereço do maior valor do vetor. Pré-condição: n >= 1.
int *maior(int *v, int n) {
int *m = v;
for (int *p = v + 1; p < v + n; p++) {
if (*p > *m) m = p;
}
return m;
}
// Calcula o dobro de x.
int *dobro_errado(int x) {
int resultado = 2 * x;
return &resultado;
}
Uma delas é segura, e a outra é um bug que pode passar nos testes e falhar em produção. Qual é qual? Explique a diferença usando a ideia de tempo de vida de uma variável e corrija a função errada.
Ver uma dica
Para cada return, pergunte: o objeto para onde esse endereço aponta ainda existe depois que a função termina? Onde o vetor v foi criado? E onde resultado foi criado?
Conferir a resposta
maior é segura. O endereço devolvido aponta para dentro do vetor do chamador, que continua existindo depois do retorno. O chamador pode até alterar o elemento: em {4, 9, 2}, *maior(v, 3) = 0; deixa o vetor como {4, 0, 2}. O endereço só continua válido enquanto o vetor do chamador existir.
dobro_errado é um ponteiro solto.resultado é uma variável local: ela nasce quando a função começa e deixa de existir quando ela retorna. O endereço devolvido aponta para uma área da pilha que será reaproveitada pela próxima chamada de função. Ler esse endereço é comportamento indefinido. Se o chamador fizer int *r = dobro_errado(21); printf("%d\n", *r);, às vezes ainda aparece 42, o que torna o bug traiçoeiro: ele passa nos testes simples.
Funções: copie para um programa que tenha main
int dobro(int x) {
return 2 * x; // devolve o VALOR, que é copiado para o chamador
}
Outras correções possíveis: receber um ponteiro de saída do chamador (void dobro(int x, int *saida)), ou alocar memória com malloc. Neste último caso, o chamador fica responsável pelo free.
Regra prática: só devolva endereços de objetos que vivem mais que a função: elementos de vetores do chamador, memória alocada com malloc ou variáveis globais. Os compiladores costumam avisar sobre esse erro. O MSVC emite o aviso C4172: returning address of local variable or temporary, e o GCC avisa com a opção -Wreturn-local-addr, que já vem ligada. Compile sempre com os avisos ligados.
./desafio --nivel=hacker
Desafio hacker: o swap mágico
Tipo: opcional
Dificuldade: ★★★
Tema: aliasing e ponteiros
Pré-requisito: XOR bit a bit
Um colega diz que é ninja: troca duas variáveis sem variável temporária, usando XOR (^).
Antes de começar: o que é XOR? O operador ^ compara dois números bit a bit e dá 1 onde os bits são diferentes e 0 onde são iguais. Por exemplo, 3 = 011 e 7 = 111 em binário, então 3 ^ 7 = 100 = 4. Duas propriedades vão ser úteis: x ^ x dá 0, e x ^ 0 dá o próprio x.
Os testes com x = 3, y = 7 passam. Depois, ele usa a função em um algoritmo de ordenação que às vezes chama xor_swap(&v[i], &v[j]) com i == j. Os dados começam a sumir.
Prove que a função troca os valores quando a e b apontam para lugares diferentes.
Explique o que acontece quando apontam para o mesmo lugar. Qual será o valor final?
Corrija a função.
Parte 2: escreva void inverter(int *ini, int *fim), que inverte o trecho de *ini até *fim (inclusive) usando só ponteiros, sem índices.
Para qualquer inteiro: x ^ x == 0, x ^ 0 == x, e XOR é comutativo e associativo.
Dica 2: aliasing
Quando dois ponteiros apontam para o mesmo objeto, dizemos que há aliasing. Nesse caso, escrever em *a também muda *b. Simule a primeira linha com a == b.
Solução comentada
1. Endereços diferentes. Sejam os valores iniciais A e B.
Linha
*a
*b
*a ^= *b
A^B
B
*b ^= *a
A^B
B^(A^B) = A
*a ^= *b
(A^B)^A = B
A
2. Mesmo endereço. A primeira linha faz *a = A ^ A = 0. Como *b é o mesmo objeto, também vale 0. As linhas seguintes fazem 0 ^ 0 = 0. O valor é apagado. Testando, xor_swap(&v[1], &v[1]) transforma 20 em 0.
3. Correção. Há duas saídas. A melhor é usar a variável temporária, ou seja, a função trocar do exercício 4. Se quiser manter o XOR, detecte o aliasing:
Funções: copie para um programa que tenha main
void xor_swap_seguro(int *a, int *b) {
if (a == b) return; // compara ENDEREÇOS, não valores
*a ^= *b;
*b ^= *a;
*a ^= *b;
}
Note que o teste é a == b, e não *a == *b. Com valores iguais em endereços diferentes, o XOR funciona. Na prática, compiladores modernos geram código melhor para a versão com temporária. O “truque” não traz ganho e introduz um bug.
4. Inverter com dois ponteiros. Um ponteiro vem da esquerda, o outro vem da direita, e eles se encontram no meio.
Funções: copie para um programa que tenha main
// Inverte [ini, fim], em que fim aponta para o ÚLTIMO elemento.
void inverter(int *ini, int *fim) {
while (ini < fim) {
int temp = *ini;
*ini = *fim;
*fim = temp;
ini++;
fim--;
}
}
// Para inverter o vetor inteiro: if (n > 0) inverter(v, v + n - 1);
// Com n == 0, v + n - 1 apontaria para antes do início do vetor.
O laço para quando os ponteiros se cruzam (ini >= fim). O elemento do meio, em tamanho ímpar, não precisa trocar de lugar. Por causa de ini < fim, a função nunca troca um elemento com ele mesmo. Tempo O(n), memória O(1). Essa técnica de dois ponteiros também resolve problemas como palíndromos e pares com soma alvo em vetores ordenados.
Revisão rápida
&x obtém o endereço. *p acessa o valor apontado.
C passa tudo por valor. Para alterar o chamador, passe o endereço.
v[i] é *(v + i), e a aritmética conta elementos, não bytes.
Nunca desreferencie ponteiro não inicializado, NULL ou solto.
Dois ponteiros podem apontar para o mesmo objeto (aliasing). Teste esse caso.