O compilador lê de cima para baixo
Um compilador C processa um arquivo-fonte em uma passagem, da primeira linha até a última. Quando chega a uma chamada de função, ele precisa já saber três coisas: o que a função retorna, quantos argumentos ela recebe, e de quais tipos eles são. Sem isso, ele não consegue gerar código correto nem verificar a chamada.
Então isto é um problema:
#include <stdio.h>
int main(void) {
printf("%d\n", add(2, 3)); /* add ainda não foi vista */
return 0;
}
int add(int a, int b) {
return a + b;
}
A solução é um protótipo: a assinatura da função, escrita antes da chamada, com um ponto e vírgula onde ficaria o corpo.
Agora a main pode vir primeiro, que é como a maioria dos arquivos C é organizada: os protótipos no topo dizem o que o arquivo oferece, a main se lê como o esboço, e os detalhes vêm abaixo.
Declaração vs definição
Duas palavras fáceis de confundir e que vale a pena manter separadas:
- Uma declaração diz que uma função existe e informa seu tipo. Ela termina em ponto e vírgula e não tem corpo. Você pode escrevê-la quantas vezes quiser.
- Uma definição fornece o corpo. Ela precisa aparecer exatamente uma vez em todo o programa - duas definições da mesma função é um erro de ligação ("multiple definition of").
int add(int a, int b); /* declaração (protótipo) */
int add(int a, int b) { return a + b; } /* definição - também uma declaração */
Os nomes dos parâmetros são opcionais em um protótipo; só os tipos importam para o compilador:
int add(int, int); /* legal, e equivalente */
int add(int a, int b); /* melhor: os nomes documentam a ordem */
Use os nomes. void drawRect(int, int, int, int); não diz nada a um leitor, enquanto void drawRect(int x, int y, int width, int height); diz tudo.
O que o protótipo te dá
Não é só a capacidade de reordenar definições - é o que faz o compilador verificar suas chamadas.
Com o protótipo em escopo, o 3 e o 2 são convertidos para double antes da chamada. Sem ele, seriam empilhados como ints e scale os leria como doubles - lixo, e nenhum diagnóstico em dialetos antigos de C.
O protótipo também pega quantidades erradas de argumentos e tipos incompatíveis em tempo de compilação:
scale(3.0); /* erro: argumentos de menos */
scale(3.0, "dois"); /* erro: passando char * onde se espera double */
Esses são os erros que você quer - barulhentos, em tempo de compilação, apontando para a linha.
Erros de declaração implícita
Chame uma função de que o compilador nunca ouviu falar e você recebe uma das mensagens mais comuns de C:
warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]
No C89, o compilador chutava: assumia que a função retornava int e aceitava quaisquer argumentos que você passasse. Esse chute está errado mais vezes do que certo, e quando a função real retorna um double ou um ponteiro o resultado é besteira. O C99 removeu as declarações implícitas da linguagem, então isso é um erro em C99 e posteriores - GCC e clang modernos o rejeitam por padrão nas versões recentes.
Duas causas, duas soluções:
Sua própria função, ainda não declarada. Adicione o protótipo acima da chamada, ou mova a definição para mais cedo.
Uma função de biblioteca cujo cabeçalho você esqueceu. Os protótipos da biblioteca vivem no cabeçalho dela, então você precisa do #include:
printf, scanf, fopen -> #include <stdio.h>
malloc, free, exit, atoi -> #include <stdlib.h>
strlen, strcpy, strcmp -> #include <string.h>
sqrt, pow, sin, fabs -> #include <math.h>
isdigit, toupper -> #include <ctype.h>
bool, true, false -> #include <stdbool.h>
Uma mensagem relacionada, conflicting types for 'add', significa que o protótipo e a definição discordam - o tipo de um parâmetro difere, ou o tipo de retorno. Corrija o que estiver errado; eles têm que coincidir exatamente.
Recursão mútua precisa de um protótipo
Às vezes reordenar não resolve: duas funções que chamam uma à outra não podem ambas vir primeiro.
Um protótipo quebra o círculo. Esse é o mesmo mecanismo de que uma função recursiva comum depende - o próprio nome de uma função está em escopo dentro do corpo dela, então a recursão direta não precisa de protótipo, mas a recursão mútua precisa.
Protótipos em arquivos de cabeçalho
Assim que um programa se espalha por vários arquivos .c, os protótipos se mudam para um cabeçalho, para que todos os arquivos vejam as mesmas declarações a partir de um único lugar.
mathutils.h - a interface:
#ifndef MATHUTILS_H
#define MATHUTILS_H
int add(int a, int b);
int multiply(int a, int b);
double average(const int values[], int count);
#endif
mathutils.c - a implementação:
#include "mathutils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
double average(const int values[], int count) {
if (count == 0) return 0.0;
int total = 0;
for (int i = 0; i < count; i++) total += values[i];
return (double) total / count;
}
main.c - um usuário:
#include <stdio.h>
#include "mathutils.h"
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}
Então:
gcc main.c mathutils.c -o program
Dois hábitos que valem a pena adotar aqui. O invólucro #ifndef / #define / #endif é uma guarda de inclusão: ela impede que as declarações sejam processadas duas vezes se o cabeçalho for incluído por dois caminhos. E mathutils.c inclui o próprio cabeçalho - o que parece redundante mas não é, porque faz o compilador conferir cada definição contra a declaração que os outros arquivos vão usar. Se um dia elas divergirem, você descobre na hora em vez de na ligação. Arquivos de cabeçalho cobre o resto.
Colchetes angulares (<stdio.h>) procuram nos caminhos de inclusão do sistema; aspas ("mathutils.h") procuram primeiro no diretório do seu projeto. Use aspas para seus próprios cabeçalhos.
f(void) vs f()
Este é genuinamente surpreendente, e é a razão pela qual todos os exemplos nestes docs escrevem int main(void).
void ping(void); /* NÃO recebe argumentos - chamadas com argumentos são rejeitadas */
void pong(); /* NÃO diz NADA sobre os parâmetros */
void ping(void); é um protótipo: ele declara que a função não recebe parâmetros, então ping(1, 2, 3) é um erro de compilação.
void pong(); é uma declaração no estilo antigo, herdada do C pré-padrão. Ela declara o tipo de retorno e absolutamente nada sobre os parâmetros, então o compilador não consegue checar as chamadas - pong(1, 2, 3) compila em silêncio e faz algo indefinido.
int main(void) { /* certo: main não recebe argumentos */
int main() { /* legal, mas a verificação de argumentos está desligada */
Sempre escreva (void) para uma função sem parâmetros. O C23 muda () para significar o mesmo que (void), o que finalmente remove a armadilha - mas muito código e muitos compiladores ainda não chegaram lá, e (void) está correto em todo padrão C.
A mesma distinção vale para a definição. void ping(void) { } é uma definição em estilo de protótipo; void ping() { } não é, e não habilita a verificação de chamadas que aparecem antes dela.
Erros comuns
- Ponto e vírgula na definição.
int add(int a, int b); { return a + b; }declaraadde depois deixa um bloco solto. A mensagem de erro é confusa; a causa é um caractere. - Ponto e vírgula faltando no protótipo. O compilador segue lendo o que vier a seguir e relata algo desconcertante várias linhas abaixo.
- Protótipo e definição discordando.
conflicting types for .... Deixe-os idênticos - ou melhor, inclua o cabeçalho no arquivo de implementação para que a checagem seja automática. - Declarar uma função dentro de outra função. Legal (
int add(int, int);dentro damain), mas a declaração fica então com escopo apenas naquela função. Coloque os protótipos em escopo de arquivo. - Definir uma função em um cabeçalho. Inclua-o a partir de dois arquivos
.ce o ligador relata uma definição duplicada. Cabeçalhos guardam declarações; definições vão em um arquivo.c. - Confiar em
()para a verificação. Ele não verifica. Escreva(void).
Perguntas frequentes
O que é um protótipo de função em C?
É a declaração da assinatura de uma função - tipo de retorno, nome e tipos dos parâmetros - terminando em ponto e vírgula no lugar de um corpo: int add(int a, int b);. Ele diz ao compilador como a função é chamada, para que as chamadas possam ser verificadas, sem dizer o que ela faz.
Qual é a diferença entre uma declaração e uma definição em C?
Uma declaração introduz o nome e o tipo (int add(int, int);) e pode aparecer muitas vezes. Uma definição fornece o corpo (int add(int a, int b) { return a + b; }) e precisa aparecer exatamente uma vez em todo o programa. Toda definição também é uma declaração.
Como corrijo "implicit declaration of function" em C?
Adicione um protótipo antes da chamada. Para suas próprias funções, coloque tipoDeRetorno nome(tiposDosParametros); perto do topo do arquivo ou em um cabeçalho; para funções de biblioteca, inclua o cabeçalho certo - #include <stdio.h> para printf, <stdlib.h> para malloc, <string.h> para strlen, <math.h> para sqrt.
Qual é a diferença entre f(void) e f() em C?
void f(void); declara uma função que não recebe argumentos, e o compilador rejeita qualquer chamada que passe algum. void f(); é uma declaração no estilo antigo que não diz nada sobre os parâmetros, então a verificação de argumentos fica desligada. Sempre escreva (void); o C23 finalmente faz os dois significarem a mesma coisa, mas código e compiladores mais antigos ainda diferem.