Menu

Parâmetros de função em C: passagem por valor, ponteiros e arrays

C passa tudo por valor - funções recebem cópias, nunca os originais. O que isso significa na prática, como ponteiros simulam a passagem por referência, por que arrays se comportam de outro jeito, e como retornar vários resultados.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Parâmetros e argumentos

Duas palavras que são usadas como sinônimos, mas que vale a pena separar:

  • Um parâmetro é a variável na definição da função - o int n em int square(int n).
  • Um argumento é o valor que você fornece no ponto da chamada - o 4 em square(4).

Chamar uma função cria seus parâmetros como novas variáveis locais e copia os argumentos para dentro deles. Essa cópia é todo o assunto desta página.

C passa por valor - sempre

Essa é a regra, e C não tem exceção a ela: uma função recebe cópias de seus argumentos. Atribuir a um parâmetro muda apenas a cópia.

value continua sendo 42. A função recebeu uma cópia chamada x, zerou a cópia, e a cópia desapareceu quando a função retornou. Renomear o parâmetro para value não muda nada - o nome não os conecta; são duas variáveis diferentes.

O mesmo vale para todos os tipos: int, double, char, um ponteiro, até uma struct inteira (copiada campo a campo). Se você vem de Python, Java ou JavaScript, note que essas linguagens passam referências de objeto por valor, então mutar um objeto dentro de uma função é visível do lado de fora. C não tem isso - ela copia o próprio objeto.

Passar por valor é um recurso, não uma limitação. Uma função não pode corromper os dados de quem chamou por acidente, o que torna tudo bem mais fácil de raciocinar.

A falha clássica: swap

A demonstração de livro-texto é uma função que tenta trocar dois valores:

A lógica está correta - ela realmente troca a e b. Mas a e b são cópias, então o trabalho é jogado fora no instante em que a função retorna.

Simulando passagem por referência com ponteiros

Para deixar uma função mudar a variável de quem a chamou, dê a ela o endereço da variável em vez do valor. Esse endereço é ele próprio passado por valor - o ponteiro é copiado - mas a cópia continua apontando para o objeto original, então escrever através dele alcança a memória de quem chamou.

Três pedaços de sintaxe carregam a ideia inteira:

  • int *a na lista de parâmetros declara a como "um ponteiro para um int".
  • &x no ponto da chamada produz o endereço de x.
  • *a dentro da função significa "o int para o qual a aponta" - legível e atribuível.

É isso que programadores C querem dizer com "passagem por referência", e vale ser preciso: ainda é passagem por valor. O ponteiro é copiado. Atribuir ao próprio a (a = NULL;) mudaria apenas a cópia; atribuir a *a muda a variável de quem chamou. Ponteiros cobre o modelo por inteiro.

O custo é que o ponto da chamada deixa de dizer se uma variável pode mudar - que é exatamente por que o & é obrigatório. swap(x, y) não compila; você tem que escrever swap(&x, &y), e esse & é a marca visível de que esta chamada pode modificar x.

Retornando vários resultados

return produz um valor, então as saídas extras viajam por parâmetros ponteiro. O formato comum é um código de status como valor de retorno e os resultados de verdade escritos através dos ponteiros:

Repare que q e r mantêm seus valores anteriores quando a chamada falha - a função retornou antes de escrever qualquer coisa. Esse é o contrato que quem chama tem que respeitar, e é por isso que a verificação do status vem antes de usar as saídas.

A alternativa é retornar uma struct contendo os dois campos, o que evita ponteiros por completo e costuma ser o design mais agradável quando os valores realmente pertencem juntos.

Arrays são diferentes

Arrays não seguem a regra da cópia, e isso pega quase todo mundo. Quando você passa um array, ele decai para um ponteiro para seu primeiro elemento - então a função recebe um endereço, não uma cópia dos dados.

O array de quem chamou realmente mudou, sem nenhum & em lugar algum - porque dataé um endereço nesse contexto. Três consequências:

int a[] e int *a significam exatamente a mesma coisa em uma lista de parâmetros. Até int a[100] significa; o tamanho é ignorado. Escrever int a[] documenta a intenção, mas não leia isso como "um array está sendo copiado".

sizeof dentro da função está errado. Este é o bug:

Na main, sizeof(data) é 20 bytes e o comprimento dá 5. Dentro da função, sizeof(a) é o tamanho de um ponteiro - 8 em um sistema de 64 bits - então o "comprimento" é 2. O tamanho do array simplesmente não está disponível ali. Sempre passe o comprimento como um parâmetro separado. Compiladores modernos avisam sobre este com -Wall.

Use const quando você não for escrever. const int a[] diz que a função apenas lê, o que o compilador impõe e o leitor pode confiar:

int sum(const int a[], int n);        /* promete não modificar o array */

Strings também são arrays, então vale o mesmo: void greet(const char *name) é a assinatura padrão para uma função que lê uma string sem alterá-la.

E quanto ao custo?

Passar uma struct grande por valor copia cada byte dela a cada chamada. Para uma struct com um punhado de campos isso está ótimo e muitas vezes é mais rápido do que a indireção que um ponteiro acrescenta. Para uma grande, passe um ponteiro - e marque-o como const se a função só lê:

struct Config { char name[64]; int flags[32]; double weights[128]; };

void applyConfig(const struct Config *cfg);   /* sem cópia de 512 bytes por chamada */

const struct Config *cfg significa "um ponteiro para um Config que eu não vou modificar", o que dá a eficiência de um ponteiro com a segurança de uma cópia.

Erros comuns

  • Esperar que um parâmetro simples mude a variável de quem chamou. Ele não pode. Passe um ponteiro.
  • Esquecer o & no ponto da chamada. swap(x, y) onde swap quer ponteiros é um erro de tipo - o compilador pega, mas só se um protótipo estiver em escopo.
  • Esquecer o * dentro da função. a = b; em dois parâmetros ponteiro troca as cópias locais, não os valores. Você quer *a = *b;.
  • Usar sizeof em um parâmetro array. Sempre errado. Passe o comprimento.
  • Retornar um ponteiro para uma variável local. A local morre quando a função retorna, então quem chamou fica com um ponteiro pendurado, o que é comportamento indefinido. Retorne por valor, escreva em um buffer fornecido por quem chama, ou aloque com malloc.
  • Não verificar se um parâmetro ponteiro é NULL. Uma função que desreferencia o que quer que lhe entreguem vai travar com um argumento nulo.

Perguntas frequentes

C tem passagem por referência?

Não. C tem exatamente um modo: passagem por valor. Uma função sempre recebe uma cópia de cada argumento, então atribuir a um parâmetro nunca toca a variável de quem chamou. O que parece passagem por referência é passar um ponteiro por valor - o ponteiro é copiado, mas ele continua apontando para o objeto de quem chamou, então a função pode modificá-lo através do ponteiro.

Como se passa um array para uma função em C?

Escreva o parâmetro como int a[] ou int *a - significam a mesma coisa - e passe o comprimento como um parâmetro separado: int sum(const int a[], int n). O array não é copiado; a função recebe um ponteiro para o primeiro elemento, então mudanças em a[i] são visíveis para quem chamou.

Por que sizeof não funciona em um parâmetro array em C?

Porque o parâmetro é, na verdade, um ponteiro. sizeof(a) dentro da função dá o tamanho de um ponteiro (tipicamente 8 bytes), não o tamanho do array - então sizeof(a)/sizeof(a[0]) resulta em algo como 2 em vez da quantidade de elementos. Sempre passe o comprimento explicitamente.

Como uma função em C pode retornar mais de um valor?

Passe ponteiros para os resultados extras e escreva através deles: int divide(int a, int b, int *quotient, int *remainder) retorna um sinalizador de sucesso e preenche as duas saídas. A alternativa é retornar uma struct que agrupa os valores.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR