C Passar por Ponteiro
Na aula Parâmetros você viu que uma função recebe cópias dos valores que você passa. Isso é ótimo para proteger as suas variáveis, mas às vezes você quer que a função mude uma variável sua: trocar dois valores, ler um número do teclado, devolver dois resultados de uma vez. Em C, o jeito é entregar à função o endereço da variável. Esta é a aula em que os ponteiros começam a ser úteis de verdade.
- Por que uma função comum não consegue mudar as variáveis de quem a chamou.
- Como escrever
trocar(int *a, int *b), a função clássica de troca, e chamá-la com&. - Por que o
scanfprecisa do&, e o que acontece sem ele. - Como uma função pode devolver mais de um resultado.
- Os erros clássicos: trocar sem ponteiro, esquecer o
&e devolver o endereço de uma variável local.
Os programas foram compilados na máquina da Ana, na pasta ~/c, com gcc -std=c23 -Wall -Wextra arquivo.c -o programa.
A ideia: a fotocópia e o endereço
A Ana precisa que o contador corrija um documento. Ela tem duas opções:
- Mandar uma fotocópia. O contador rabisca a cópia à vontade, mas o original da Ana continua igual. É assim que o C passa argumentos normalmente: por valor, uma cópia.
- Mandar o endereço de onde o original está guardado ("está na gaveta 1000 do meu arquivo"). Aí o contador vai até lá e corrige o original. É a passagem por ponteiro.
O C só tem a primeira forma. A segunda é um truque em cima dela: você manda uma cópia... do endereço. A cópia de um endereço continua levando à mesma gaveta.
1. O problema: a função recebe cópias
O exemplo clássico: uma função que troca dois valores. Escrita do jeito mais natural, ela não funciona:
#include <stdio.h>
void trocar(int a, int b) {
int temp = a;
a = b;
b = temp;
printf("Dentro de trocar: a = %d, b = %d\n", a, b);
}
int main(void) {
int a = 3;
int b = 7;
trocar(a, b);
printf("No main: a = %d, b = %d\n", a, b);
return 0;
}
$ gcc -std=c23 -Wall -Wextra trocar-errado.c -o trocar-errado $ ./trocar-errado Dentro de trocar: a = 7, b = 3 « as cópias foram trocadas No main: a = 3, b = 7 « os originais, não
Os nomes a e b se repetem, mas são quatro variáveis diferentes: duas do main e duas da função trocar, que nasceram com cópias dos valores e morreram no } da função. Dá para ver que moram em gavetas diferentes:
#include <stdio.h>
void mostrar(int x) {
printf("na função: x mora em %p\n", (void *) &x);
}
int main(void) {
int x = 3;
printf("no main: x mora em %p\n", (void *) &x);
mostrar(x);
return 0;
}
$ gcc -std=c23 -Wall -Wextra copias.c -o copias $ ./copias no main: x mora em 0x7ffd5cc7f8d4 na função: x mora em 0x7ffd5cc7f8bc « outra gaveta: é uma cópia
main nem ficam sabendo. Quando trocar termina, as cópias somem.2. A solução: mandar o endereço
Agora a função recebe ponteiros, e quem chama entrega os endereços:
#include <stdio.h>
void trocar(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main(void) {
int a = 3;
int b = 7;
trocar(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
$ gcc -std=c23 -Wall -Wextra trocar.c -o trocar $ ./trocar a = 7, b = 3 « agora os originais trocaram
São três mudanças, todas pequenas:
- void trocar(int *a, int *b)
- Os parâmetros agora são ponteiros para
int: a função recebe dois endereços. (Oae obdaqui ainda são cópias, mas cópias de endereços.) - int temp = *a;
- Dentro da função, todo acesso aos valores leva
*:*aé "a variável para ondeaaponta", ou seja, oadomain. Otempé umintcomum, que guarda o valor 3 durante a troca. - *a = *b; *b = temp;
- Escreve nas gavetas do
main: na dea, o valor da deb; na deb, o que estava guardado notemp. - trocar(&a, &b);
- Na chamada,
&: omainentrega os endereços das suas variáveis.
main. Escrever em *a e *b é escrever nas gavetas do main.Passo a passo, com as gavetas:
| Linha | a (main) | b (main) | temp |
|---|---|---|---|
trocar(&a, &b); (entrando) | 3 | 7 | – |
int temp = *a; | 3 | 7 | 3 |
*a = *b; | 7 | 7 | 3 |
*b = temp; | 7 | 3 | 3 |
de volta ao main | 7 | 3 | (sumiu) |
tipo *nome; (2) dentro da função, use *nome; (3) na chamada, passe &variavel.
Missão: complete a função dobrar, que recebe o endereço de um int e dobra o valor guardado lá. O main já chama dobrar(&pontos).
pontos = 20
#include <stdio.h>
void dobrar(int *n) {
// dobre o valor da variável para onde n aponta
}
int main(void) {
int pontos = 10;
dobrar(&pontos);
printf("pontos = %d\n", pontos);
return 0;
}
#include <stdio.h>
void dobrar(int *n) {
*n = *n * 2;
}
int main(void) {
int pontos = 10;
dobrar(&pontos);
printf("pontos = %d\n", pontos);
return 0;
}
3. Por isso o scanf precisa do &
Agora o mistério da aula Entrada com scanf está resolvido. O scanf é uma função como a trocar: para guardar o número digitado na sua variável, ela precisa do endereço. scanf("%d", &idade) quer dizer "leia um inteiro e guarde na gaveta de idade".
Sem o &, o scanf recebe o valor de idade (uma cópia, 0) e o trata como se fosse um endereço. O gcc avisa:
#include <stdio.h>
int main(void) {
int idade = 0;
printf("Quantos anos você tem? ");
scanf("%d", idade);
printf("Você tem %d anos\n", idade);
return 0;
}
$ gcc -std=c23 -Wall -Wextra scanf-sem.c -o scanf-sem
scanf-sem.c: In function ‘main’:
scanf-sem.c:7:13: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]
7 | scanf("%d", idade);
| ~^ ~~~~~
| | |
| | int
| int *
$ echo 29 | ./scanf-sem
Falha de segmentação (imagem do núcleo gravada) echo 29 | ./scanf-sem
"O formato %d espera um argumento do tipo int * (endereço de int), mas o argumento 2 é do tipo int". É só um aviso, então o programa foi gerado. Rodando, o scanf tentou guardar o 29 no "endereço 0" (o valor de idade), que é o ponteiro nulo, e o programa morreu. (A pergunta sumiu da tela por causa do balde do printf, explicado em Ponteiro Nulo; o echo 29 | faz o papel da pessoa digitando.)
No editor do guia, o resultado é outro, e mais traiçoeiro: o Clang avisa (format specifies type 'int *' but the argument has type 'int'), o programa não morre e mostra Você tem 0 anos. O 29 foi parar numa gaveta qualquer e o programa seguiu em frente com o valor errado. É mais um exemplo de por que um aviso do -Wall deve ser tratado como erro.
scanf("%s", nome), sem &?
Com um array de char, o & não vai, porque o nome do array já vira o endereço do primeiro item (veja Ponteiros e Arrays). A regra é a mesma: o scanf sempre recebe um endereço. Com int, double e char sozinho, você precisa do & para obtê-lo; com array, o nome já é um.
4. Devolver mais de um resultado
Uma função só tem um return, que devolve um valor só. Quando a resposta tem duas partes, os ponteiros resolvem: a função recebe os endereços de onde guardar cada parte. A Ana quer dividir balas entre crianças e saber quanto cada uma ganha e quantas sobram:
#include <stdio.h>
void dividir(int total, int pessoas, int *cada, int *sobra) {
*cada = total / pessoas;
*sobra = total % pessoas;
}
int main(void) {
int balas = 17;
int cada = 0;
int sobra = 0;
dividir(balas, 5, &cada, &sobra);
printf("%d balas para 5 crianças: %d para cada, sobram %d\n", balas, cada, sobra);
return 0;
}
$ gcc -std=c23 -Wall -Wextra dividir.c -o dividir $ ./dividir 17 balas para 5 crianças: 3 para cada, sobram 2
Repare na mistura: total e pessoas são de entrada (a função só lê, então vão por valor, sem ponteiro); cada e sobra são de saída (a função escreve, então vão por ponteiro). Parâmetros de saída costumam ficar no fim da lista.
O padrão "deu certo?" + resultado
Uma combinação muito comum: o return diz se deu certo, e o resultado vai por ponteiro. É exatamente o que o próprio scanf faz (devolve quantos itens leu, e guarda os valores pelos ponteiros):
#include <stdio.h>
bool ler_idade(int *idade) {
printf("Quantos anos você tem? ");
if (scanf("%d", idade) != 1) {
return false;
}
return *idade >= 0 && *idade <= 130;
}
int main(void) {
int idade = 0;
if (ler_idade(&idade)) {
printf("Daqui a 10 anos você terá %d.\n", idade + 10);
} else {
printf("Idade inválida.\n");
}
return 0;
}
$ gcc -std=c23 -Wall -Wextra ler.c -o ler $ ./ler Quantos anos você tem? 29 Daqui a 10 anos você terá 39. $ ./ler Quantos anos você tem? abc Idade inválida. $ ./ler Quantos anos você tem? 200 Idade inválida.
Detalhe importante: dentro de ler_idade, o scanf recebe idade sem &. Aqui, idade já é um ponteiro (o endereço que veio do main); colocar & daria o endereço do ponteiro, que não é o que o scanf quer. O bool, o true e o false não precisam de #include no C23 (veja Booleanos).
Por valor ou por ponteiro: qual usar
| A função precisa… | Parâmetro | Chamada |
|---|---|---|
só ler um número, um caractere, um bool | int x (por valor) | f(x) |
| mudar uma variável de quem chama | int *x | f(&x) |
| devolver mais de um resultado | int *saida1, int *saida2 | f(&r1, &r2) |
| trabalhar com um array | int *v, int n | f(v, n) (o nome já é endereço) |
| só ler um array ou uma struct grande | const int *v | f(v, n), veja const e Ponteiros |
Prefira por valor sempre que a função só precisar ler um valor simples: é mais seguro (a função não consegue estragar nada) e mais fácil de entender. Use ponteiro quando a mudança for o objetivo. Com structs, o ponteiro também evita copiar muitos bytes.
Quando dá errado
$ ./trocar-errado Dentro de trocar: a = 7, b = 3 No main: a = 3, b = 7
O que aconteceu: a função recebeu cópias e trocou as cópias. O compilador não avisa nada: o código é válido, só não faz o que você queria.
Como consertar: parâmetros int *a, int *b, *a e *b dentro, trocar(&a, &b) na chamada.
*)
#include <stdio.h>
void trocar(int *a, int *b) {
int *temp = a;
a = b;
b = temp;
}
int main(void) {
int a = 3;
int b = 7;
trocar(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
$ gcc -std=c23 -Wall -Wextra ponteiro-local.c -o ponteiro-local $ ./ponteiro-local a = 3, b = 7
O que aconteceu: sem o *, a função trocou os próprios ponteiros (os bilhetes), que são cópias locais. Os dois bilhetes trocaram de mão, mas nenhuma gaveta do main foi aberta. Nenhum aviso.
Como consertar: mexa nos valores apontados: int temp = *a; *a = *b; *b = temp;.
& na chamada
$ gcc -std=c23 -Wall -Wextra semecomercial.c -o semecomercial
semecomercial.c: In function ‘main’:
semecomercial.c:13:12: error: passing argument 1 of ‘trocar’ makes pointer from integer without a cast [-Wint-conversion]
13 | trocar(a, b);
| ^
| |
| int
semecomercial.c:13:12: note: possible fix: take the address with ‘&’
13 | trocar(a, b);
| ^
| &
semecomercial.c:3:18: note: expected ‘int *’ but argument is of type ‘int’
3 | void trocar(int *a, int *b) {
| ~~~~~^
semecomercial.c:13:15: error: passing argument 2 of ‘trocar’ makes pointer from integer without a cast [-Wint-conversion]
13 | trocar(a, b);
| ^
| |
| int
semecomercial.c:13:15: note: possible fix: take the address with ‘&’
13 | trocar(a, b);
| ^
| &
semecomercial.c:3:26: note: expected ‘int *’ but argument is of type ‘int’
3 | void trocar(int *a, int *b) {
| ~~~~~^
O que significa: "passar o argumento 1 de trocar transforma inteiro em ponteiro sem conversão". A função espera int * e recebeu int. O gcc até sugere o conserto: "possible fix: take the address with ‘&’", pegue o endereço com &. No gcc 15 isso é erro; em compiladores antigos era só aviso, e o programa gerado podia morrer ao rodar.
Como consertar: trocar(&a, &b);.
#include <stdio.h>
int *criar_nota(void) {
int nota = 9;
return ¬a;
}
int main(void) {
int *p = criar_nota();
printf("p = %p\n", (void *) p);
printf("nota = %d\n", *p);
return 0;
}
$ gcc -std=c23 -Wall -Wextra local.c -o local
local.c: In function ‘criar_nota’:
local.c:5:12: warning: function returns address of local variable [-Wreturn-local-addr]
5 | return ¬a;
| ^~~~~
$ ./local
p = (nil)
Falha de segmentação (imagem do núcleo gravada) ./local
O que significa: "a função devolve o endereço de uma variável local". A nota mora na gaveta da função criar_nota, que é desmontada quando a função termina. O endereço devolvido aponta para um lugar que não é mais de ninguém (um "ponteiro pendurado"). O gcc é tão desconfiado disso que, além de avisar, troca o endereço por NULL de propósito, para o programa morrer logo em vez de ler lixo: por isso p = (nil). No editor do guia, o Clang avisa com outras palavras (address of stack memory associated with local variable 'nota' returned), o programa não morre e mostra um número sem sentido no lugar do 9.
Como consertar: devolva o valor (int criar_nota(void) { return 9; }), ou receba um ponteiro para onde guardar (void criar_nota(int *nota)), ou reserve a memória com malloc, que não some quando a função termina.
Referência rápida
| Código | O que faz |
|---|---|
void f(int x) | Por valor: a função recebe uma cópia e não muda o original. |
void f(int *x) | Por ponteiro: a função recebe um endereço. |
*x = 5; (dentro de f) | Muda a variável de quem chamou. |
f(&variavel); | Chama entregando o endereço. |
void trocar(int *a, int *b) | A troca clássica: int t = *a; *a = *b; *b = t; |
void f(int entrada, int *saida1, int *saida2) | Devolve vários resultados pelos ponteiros. |
bool f(int *resultado) | O return diz se deu certo; o valor vai pelo ponteiro. |
scanf("%d", &x) | O scanf é uma função com parâmetro ponteiro. |
return &local; | Nunca: a variável local some quando a função termina. |
Exercícios
1. O parâmetro
Complete o cabeçalho de uma função que zera uma variável int de quem a chama:
2. A chamada
Complete a chamada para zerar a variável vidas:
3. Dentro da função
Complete a função para somar 1 à variável apontada por n:
4. Uma função void f(int x) { x = 10; } é chamada com f(idade), e idade valia 5. Quanto vale idade depois?
5. Dentro de bool ler(int *n), qual chamada do scanf está certa?
6. Por que return ¬a; (com nota local) é um erro?
Missão final: o menor e o maior
Missão: escreva a função min_max, que recebe as notas (int *v e a quantidade n) e guarda a menor nota em *menor e a maior em *maior. Dica: comece com *menor = v[0]; e *maior = v[0]; e percorra o resto.
Menor: 3 Maior: 9
#include <stdio.h>
void min_max(int *v, int n, int *menor, int *maior) {
// guarde a menor nota em *menor e a maior em *maior
}
int main(void) {
int notas[6] = {7, 3, 9, 6, 8, 5};
int menor = 0;
int maior = 0;
min_max(notas, 6, &menor, &maior);
printf("Menor: %d\n", menor);
printf("Maior: %d\n", maior);
return 0;
}
#include <stdio.h>
void min_max(int *v, int n, int *menor, int *maior) {
*menor = v[0];
*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 main(void) {
int notas[6] = {7, 3, 9, 6, 8, 5};
int menor = 0;
int maior = 0;
min_max(notas, 6, &menor, &maior);
printf("Menor: %d\n", menor);
printf("Maior: %d\n", maior);
return 0;
}
Resumo
- Em C, todo argumento vai por valor: a função recebe uma cópia. Mudar a cópia não muda o original.
- Para uma função mudar uma variável sua, passe o endereço: parâmetro
int *x, uso*x, chamadaf(&x). - A troca clássica:
void trocar(int *a, int *b) { int t = *a; *a = *b; *b = t; }. - O
scanfprecisa do&pelo mesmo motivo. Sem ele, o gcc avisa e o programa pode morrer (ou, no editor, seguir com o valor errado). - Ponteiros permitem devolver vários resultados, e o padrão "
returndiz se deu certo + resultado por ponteiro" é comum. - Nunca devolva o endereço de uma variável local: ela some quando a função termina.
- Próxima aula: Ponteiros e Strings.