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 nemint square(int n). - Um argumento é o valor que você fornece no ponto da chamada - o
4emsquare(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 *ana lista de parâmetros declaraacomo "um ponteiro para umint".&xno ponto da chamada produz o endereço dex.*adentro da função significa "ointpara o qualaaponta" - 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 já é 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)ondeswapquer 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
sizeofem 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.