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.

Você vai aprender
  • 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 scanf precisa 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
3773a (main)b (main)a (trocar)b (trocar)
Por valor: a função troca as cópias dela (em cinza). As variáveis do 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. (O a e o b daqui 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 onde a aponta", ou seja, o a do main. O temp é um int comum, que guarda o valor 3 durante a troca.
*a = *b; *b = temp;
Escreve nas gavetas do main: na de a, o valor da de b; na de b, o que estava guardado no temp.
trocar(&a, &b);
Na chamada, &: o main entrega os endereços das suas variáveis.
73&a&ba (main)b (main)a (trocar)b (trocar)
Por ponteiro: os parâmetros da função (em verde) guardam os endereços das variáveis do main. Escrever em *a e *b é escrever nas gavetas do main.

Passo a passo, com as gavetas:

Linhaa (main)b (main)temp
trocar(&a, &b); (entrando)37–
int temp = *a;373
*a = *b;773
*b = temp;733
de volta ao main73(sumiu)
A receita Para uma função mudar uma variável de quem a chama: (1) o parâmetro é 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.

E o 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âmetroChamada
só ler um número, um caractere, um boolint x (por valor)f(x)
mudar uma variável de quem chamaint *xf(&x)
devolver mais de um resultadoint *saida1, int *saida2f(&r1, &r2)
trabalhar com um arrayint *v, int nf(v, n) (o nome já é endereço)
só ler um array ou uma struct grandeconst int *vf(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

A troca "não troca" (sem ponteiro)
$ ./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.

A troca "não troca" (com ponteiro, mas sem *)
#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;.

"makes pointer from integer without a cast": esqueceu o & 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);.

"function returns address of local variable"
#include <stdio.h>

int *criar_nota(void) {
    int nota = 9;
    return &nota;
}

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 &nota;
      |            ^~~~~
$ ./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ódigoO 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:

void zerar() { *n = 0; }

2. A chamada

Complete a chamada para zerar a variável vidas:

zerar();

3. Dentro da função

Complete a função para somar 1 à variável apontada por n:

void incrementar(int *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 &nota; (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, chamada f(&x).
  • A troca clássica: void trocar(int *a, int *b) { int t = *a; *a = *b; *b = t; }.
  • O scanf precisa 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 "return diz 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.