C Protótipos
Nas aulas anteriores, toda função era escrita acima de quem a chamava. Com três ou quatro funções isso é fácil; com vinte, vira um quebra-cabeça de ordem. O C tem uma saída: o protótipo, uma linha no topo do arquivo que avisa ao compilador "existe uma função com este nome, que recebe isto e devolve aquilo; o corpo vem depois". Nesta aula você vai ver o erro que aparece sem protótipo (e por que ele já foi só um aviso), escrever protótipos, entender a diferença entre declarar e definir e descobrir que o #include <stdio.h> que você usa desde a primeira aula é, no fundo, um arquivo cheio de protótipos.
- Por que o compilador precisa conhecer a função antes da chamada, e o erro
implicit declaration of function. - O que mudou entre o C89, o C99, o gcc 14 e o C23 nesse assunto.
- Como escrever um protótipo, e a diferença entre declaração e definição.
- Que os nomes dos parâmetros são opcionais no protótipo (e por que vale a pena escrevê-los).
- Os erros de protótipo que não bate (
conflicting types) e de protótipo sem função (referência não definida). - Que os arquivos
.h, como ostdio.h, são coleções de protótipos.
Todos os programas desta aula foram compilados com o gcc 15.2 na máquina fictícia da Ana, na pasta ~/c, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa.
A ideia: a lista de convidados na portaria
Numa festa com portaria, o porteiro só deixa entrar quem está na lista de convidados. Não importa se a pessoa chega cedo ou tarde: se o nome dela está na lista desde o começo da noite, ela entra.
O compilador é esse porteiro. Ele lê o seu arquivo uma vez, de cima para baixo, e só aceita uma chamada de função se já souber que ela existe. O protótipo é o nome na lista: escrito no topo, ele avisa que a função vai chegar, e a ordem das definições deixa de importar.
1. O problema: chamar antes de definir
Aqui o main vem primeiro e a função triplo, depois:
#include <stdio.h>
int main(void) {
int t = triplo(5);
printf("Triplo de 5: %d\n", t);
return 0;
}
int triplo(int n) {
return n * 3;
}
$ gcc -std=c23 -Wall -Wextra depois.c -o depois
depois.c: In function ‘main’:
depois.c:4:13: error: implicit declaration of function ‘triplo’ [-Wimplicit-function-declaration]
4 | int t = triplo(5);
| ^~~~~~
- In function ‘main’
- O problema está dentro do
main. - implicit declaration of function ‘triplo’
- "Declaração implícita da função
triplo". Na linha 4, ninguém tinha declarado (apresentado) essa função ao compilador. Usá-la assim seria declará-la "implicitamente", sem dizer nada sobre ela. - [-Wimplicit-function-declaration]
- O nome da regra. Começa com
-Wporque, durante décadas, foi só um aviso. No gcc 14 em diante, é erro: o programa não é gerado.
No editor do guia, o Clang diz use of undeclared identifier 'triplo' ("uso de identificador não declarado"). Com -std=c17, ele escreve call to undeclared function 'triplo'; ISO C99 and later do not support implicit function declarations: "chamada a função não declarada; o C99 e posteriores não aceitam declarações implícitas de função".
Por que já foi só um aviso: a adivinhação do C antigo
No C de 1989 (C89), o compilador não reclamava: quando via uma função desconhecida, ele adivinhava que ela devolvia int e aceitava qualquer argumento. O gcc 15 ainda imita esse comportamento se você pedir o padrão antigo:
$ gcc -std=c89 -Wall -Wextra depois.c -o depois
depois.c: In function ‘main’:
depois.c:4:13: warning: implicit declaration of function ‘triplo’ [-Wimplicit-function-declaration]
4 | int t = triplo(5);
| ^~~~~~
$ ./depois
Triplo de 5: 15 « deu certo por sorte: o palpite "devolve int" estava certo
Agora veja o palpite errar. A função metade devolve double:
#include <stdio.h>
int main(void) {
printf("Metade de 7: %f\n", metade(7.0));
return 0;
}
double metade(double x) {
return x / 2;
}
$ gcc -std=c89 -Wall -Wextra adivinha.c -o adivinha
adivinha.c: In function ‘main’:
adivinha.c:4:33: warning: implicit declaration of function ‘metade’ [-Wimplicit-function-declaration]
4 | printf("Metade de 7: %f\n", metade(7.0));
| ^~~~~~
adivinha.c:4:27: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
4 | printf("Metade de 7: %f\n", metade(7.0));
| ~^ ~~~~~~~~~~~
| | |
| | int
| double
| %d
adivinha.c: At top level:
adivinha.c:8:8: error: conflicting types for ‘metade’; have ‘double(double)’
8 | double metade(double x) {
| ^~~~~~
adivinha.c:4:33: note: previous implicit declaration of ‘metade’ with type ‘int()’
4 | printf("Metade de 7: %f\n", metade(7.0));
| ^~~~~~
A última nota conta a história toda: previous implicit declaration of ‘metade’ with type ‘int()’, "a declaração implícita anterior de metade tinha o tipo int()". Na linha 4, o compilador chutou "devolve int" (e até reclamou do %f por causa disso); na linha 8, descobriu que era double. Aqui o erro foi pego porque tudo estava no mesmo arquivo. Com a função em outro arquivo, o compilador não teria como comparar, e o programa podia ser gerado e mostrar números sem sentido. Por isso:
| Versão | Chamar uma função não declarada |
|---|---|
| C89/C90 | Permitido: o compilador supõe que ela devolve int |
| C99 a C17 | Proibido pela norma, mas o gcc ainda aceitava com um aviso |
| gcc 14 em diante (C99 ou mais novo) | Erro (só volta a ser aviso com -fpermissive ou -std=c89) |
| C23 | A ideia de "declaração implícita" foi removida da linguagem |
Se você encontrar na internet um conselho do tipo "é só um aviso, pode ignorar", ele é de antes de 2024. Hoje, a resposta certa é declarar a função.
2. A solução: o protótipo
Um protótipo é o cabeçalho da função, terminado em ;, sem o corpo. Escrito no topo do arquivo, ele apresenta a função ao compilador:
#include <stdio.h>
int triplo(int n);
int main(void) {
int t = triplo(5);
printf("Triplo de 5: %d\n", t);
return 0;
}
int triplo(int n) {
return n * 3;
}
$ gcc -std=c23 -Wall -Wextra proto.c -o proto $ ./proto Triplo de 5: 15
- int triplo(int n);
- O protótipo: o mesmo cabeçalho da função, com
;no lugar do corpo. Ele diz ao compilador o nome, o que a função recebe (umint) e o que devolve (umint). - triplo(5) no main
- Quando chega aqui, o compilador já conhece
triplo: confere se o número e o tipo dos argumentos batem e sabe que o resultado éint. - int triplo(int n) { … }
- A definição, com o corpo, pode vir em qualquer lugar depois. Ela tem de bater com o protótipo.
Declaração × definição
| Declaração (protótipo) | Definição | |
|---|---|---|
| Cara | int triplo(int n); | int triplo(int n) { return n * 3; } |
| Termina com | ; | o corpo { … } |
| Diz | "esta função existe e tem este formato" | "é isto que ela faz" |
| Quantas pode haver | Várias, desde que iguais | Uma só no programa inteiro |
| Comparação | O nome na lista de convidados | O convidado em pessoa |
Toda definição também serve de declaração: por isso, nas aulas anteriores, escrever a função acima do main bastava.
3. Os nomes dos parâmetros são opcionais no protótipo
O compilador só precisa dos tipos no protótipo. Os nomes podem ficar de fora:
#include <stdio.h>
double preco_final(double, int);
int main(void) {
printf("Total: %.2f\n", preco_final(2.5, 4));
return 0;
}
double preco_final(double preco, int quantidade) {
return preco * quantidade;
}
$ gcc -std=c23 -Wall -Wextra nomes.c -o nomes $ ./nomes Total: 10.00
Compila e funciona. Mas compare as duas linhas, do ponto de vista de quem lê:
double preco_final(double, int);— odoubleé o preço ou a quantidade? E oint?double preco_final(double preco, int quantidade);— dá para usar a função sem abrir a definição.
O guia sempre escreve os nomes. Eles não precisam ser iguais aos da definição (o compilador os ignora no protótipo), mas é uma boa ideia que sejam. E, para uma função sem parâmetros, o protótipo é void linha(void);; até o C17, void linha(); não era um protótipo de verdade (veja Funções).
Experimente: rode o programa (ele tem protótipos no topo e as funções depois do main). Depois apague a linha do protótipo de mostrar_menu e rode de novo para ver o erro do Clang. Por fim, devolva a linha e troque a ordem das duas definições lá embaixo: com os protótipos no topo, a ordem não importa mais.
#include <stdio.h>
void mostrar_menu(void);
double preco_com_desconto(double preco, int porcentagem);
int main(void) {
mostrar_menu();
printf("Caderno com 10%% de desconto: R$ %.2f\n", preco_com_desconto(12.50, 10));
return 0;
}
void mostrar_menu(void) {
printf("=== Lojinha da Ana ===\n");
}
double preco_com_desconto(double preco, int porcentagem) {
return preco - preco * porcentagem / 100;
}
No laboratório, esse programa mostra:
$ gcc -std=c23 -Wall -Wextra menu.c -o menu $ ./menu === Lojinha da Ana === Caderno com 10% de desconto: R$ 11.25
4. Protótipo e definição têm de combinar
O protótipo é uma promessa. Se a definição for diferente, o compilador acusa:
#include <stdio.h>
int triplo(int n);
int main(void) {
printf("Triplo de 5: %d\n", triplo(5));
return 0;
}
double triplo(double n) {
return n * 3;
}
$ gcc -std=c23 -Wall -Wextra conflito.c -o conflito
conflito.c:10:8: error: conflicting types for ‘triplo’; have ‘double(double)’
10 | double triplo(double n) {
| ^~~~~~
conflito.c:3:5: note: previous declaration of ‘triplo’ with type ‘int(int)’
3 | int triplo(int n);
| ^~~~~~
- conflicting types for ‘triplo’
- "Tipos em conflito para
triplo". - have ‘double(double)’
- O formato da definição: devolve
double, recebe umdouble. - previous declaration … with type ‘int(int)’
- O formato do protótipo, com a linha onde ele está. Para consertar, deixe as duas linhas iguais (mudando a que estiver errada).
É o mesmo erro da aula Parâmetros, quando duas funções tinham o mesmo nome: para o C, um nome tem um formato só. No editor: conflicting types for 'triplo' e note: previous declaration is here.
Missão: o main vem antes da função dobro, e o programa não compila. Sem mudar a ordem das funções, acrescente o protótipo de dobro no topo do arquivo, logo abaixo do #include.
#include <stdio.h>
int main(void) {
printf("Dobro de 21: %d\n", dobro(21));
return 0;
}
int dobro(int n) {
return n * 2;
}
#include <stdio.h>
int dobro(int n);
int main(void) {
printf("Dobro de 21: %d\n", dobro(21));
return 0;
}
int dobro(int n) {
return n * 2;
}
5. Protótipo sem função: o erro vem do ligador
E se você escreve o protótipo e esquece a definição?
#include <stdio.h>
int triplo(int n);
int main(void) {
printf("Triplo de 5: %d\n", triplo(5));
return 0;
}
$ gcc -std=c23 -Wall -Wextra semdef.c -o semdef /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/cccrQsBI.o: na função "main": semdef.c:(.text+0xe): referência não definida para "triplo" collect2: error: ld returned 1 exit status
A mensagem tem outra cara, e é em português. É que ela não vem do compilador, e sim do ligador (ld, de linker), a última etapa da construção do programa (aula Como o C Vira Programa):
- O compilador leu o arquivo. Graças ao protótipo, a chamada
triplo(5)estava certinha: ele deixou anotado "aqui vai a funçãotriplo, que alguém vai fornecer". - O ligador juntou as peças para montar o executável e procurou a função
triplo. Não achou em lugar nenhum.
- /tmp/cccrQsBI.o: na função "main"
- O arquivo intermediário (temporário, com nome sorteado a cada compilação) e a função onde está a chamada.
- referência não definida para "triplo"
- "Existe uma chamada para
triplo, mas nenhuma definição". Em inglês, que é o que você vai achar nos fóruns:undefined reference to `triplo'. - ld returned 1 exit status
- "O
ldterminou com erro" (ocollect2é um ajudante do gcc que chama o ligador).
No editor, o ligador é o wasm-ld, e a mensagem sai assim: wasm-ld: error: … undefined symbol: triplo ("símbolo não definido"). Conserto: escreva a definição (ou confira se o nome dela é idêntico ao do protótipo: Triplo e triplo são funções diferentes).
6. Quando o protótipo é obrigatório: funções que se chamam
Às vezes não existe uma ordem que funcione: a função A chama a B, e a B chama a A. Este exemplo (um jeito engraçado de descobrir se um número é par, usando Recursão) só compila com protótipo:
#include <stdio.h>
bool eh_impar(int n);
bool eh_par(int n) {
if (n == 0) {
return true;
}
return eh_impar(n - 1);
}
bool eh_impar(int n) {
if (n == 0) {
return false;
}
return eh_par(n - 1);
}
int main(void) {
for (int i = 0; i <= 3; i++) {
printf("%d é %s\n", i, eh_par(i) ? "par" : "ímpar");
}
return 0;
}
$ gcc -std=c23 -Wall -Wextra parimpar.c -o parimpar $ ./parimpar 0 é par 1 é ímpar 2 é par 3 é ímpar
eh_par usa eh_impar, que ainda não foi definida: o protótipo na linha 3 resolve. Sem ele, qualquer ordem dá implicit declaration em uma das duas.
7. Os arquivos .h são listas de protótipos
Desde a primeira aula você escreve #include <stdio.h>. Veja o que acontece sem ele:
int main(void) {
printf("Olá!\n");
return 0;
}
$ gcc -std=c23 -Wall -Wextra seminclude.c -o seminclude
seminclude.c: In function ‘main’:
seminclude.c:2:5: error: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
2 | printf("Olá!\n");
| ^~~~~~
seminclude.c:1:1: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
+++ |+#include <stdio.h>
1 | int main(void) {
seminclude.c:2:5: warning: incompatible implicit declaration of built-in function ‘printf’ [-Wbuiltin-declaration-mismatch]
2 | printf("Olá!\n");
| ^~~~~~
seminclude.c:2:5: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
O mesmo erro de antes, agora com o printf. E o gcc sugere o conserto: include ‘<stdio.h>’ or provide a declaration of ‘printf’, "inclua o <stdio.h> ou forneça uma declaração de printf". A linha com +++ |+ mostra onde a linha nova entraria. Ou seja: o stdio.h fornece a declaração do printf. Dá para conferir no laboratório, procurando dentro do arquivo:
$ grep -n "printf (const" /usr/include/stdio.h | head -3 370:extern int printf (const char *__restrict __format, ...); 385:extern int vprintf (const char *__restrict __format, __gnuc_va_list __arg);
Lá está, na linha 370: int printf (const char *…, ...);, um protótipo terminado em ;. O extern quer dizer "definida em outro lugar" (aula static e extern), os ... dizem que ela aceita quantos argumentos você quiser (aula Funções Variádicas) e os nomes estranhos com __ são só nomes de parâmetros. A definição do printf, com o código de verdade, já vem compilada na biblioteca do C, que o ligador junta ao seu programa automaticamente.
Quando os seus programas crescerem, você vai fazer o mesmo: colocar os protótipos num arquivo .h seu (loja.h), as definições num .c (loja.c) e escrever #include "loja.h" onde precisar. Esse é o assunto de Arquivos .h e .c e Vários Arquivos.
Formas diferentes de organizar + qual usar
| Organização | Quando usar |
|---|---|
Funções acima do main, na ordem certa, sem protótipos | Programas curtos, de um arquivo só (como a maioria dos exercícios) |
Protótipos no topo, main logo depois, definições no fim | Programas de um arquivo com muitas funções: o main fica no começo, como um resumo, e a ordem das definições não importa |
Protótipos num .h, definições num .c | Programas com vários arquivos, ou funções que você quer reaproveitar (Arquivos .h e .c) |
| Protótipo obrigatório | Funções que se chamam mutuamente, e funções definidas em outro arquivo |
Quando dá errado
error: implicit declaration of function ‘f’ [-Wimplicit-function-declaration]
O que significa: a chamada aparece antes de qualquer declaração de f. Causas comuns: a função está definida mais abaixo; o nome está escrito diferente; faltou o #include da biblioteca (stdio.h para printf, string.h para strlen, math.h para sqrt). No editor: use of undeclared identifier 'f' ou call to undeclared library function.
Como consertar: leia a note do gcc, que às vezes já diz o #include que falta. Para funções suas, escreva o protótipo no topo ou mova a definição para cima.
error: conflicting types for ‘f’
O que significa: o protótipo e a definição têm tipos diferentes (retorno ou parâmetros). Também aparece quando a função foi usada antes de declarada e o compilador "chutou" um tipo (previous implicit declaration).
Como consertar: copie o cabeçalho da definição, cole no lugar do protótipo e acrescente o ;. Assim os dois ficam idênticos.
referência não definida para "f" (undefined reference to `f')
O que significa: o compilador aceitou a chamada (havia protótipo), mas o ligador não achou a definição. A função não foi escrita, tem outro nome, ou está em outro .c que não entrou no comando do gcc.
Como consertar: escreva a definição, confira o nome letra por letra, ou compile os arquivos juntos (gcc main.c loja.c -o loja, aula Vários Arquivos). Com funções de math.h, o motivo pode ser outro: veja Matemática.
error: expected identifier or ‘(’ before ‘{’ token
O que significa: você copiou o protótipo (com ;) para escrever a definição e esqueceu de tirar o ;: int triplo(int n); {.
Como consertar: na definição, nada de ; antes da {.
Referência rápida
| Escrita | O que é |
|---|---|
int triplo(int n); | Protótipo (declaração): avisa que a função existe |
int triplo(int); | Protótipo sem o nome do parâmetro (válido, mas menos claro) |
void linha(void); | Protótipo de função sem parâmetros |
int triplo(int n) { … } | Definição: o corpo da função (uma só no programa) |
#include <stdio.h> | Traz os protótipos das funções de entrada e saída (printf, scanf…) |
#include "loja.h" | Traz os protótipos que você mesmo escreveu (Arquivos .h e .c) |
Exercícios
1. O protótipo
A definição é double media(double a, double b) { return (a + b) / 2; }. Complete o protótipo:
2. O que termina o protótipo
Que caractere termina um protótipo, no lugar do corpo?
3. O cabeçalho do printf
Complete o #include que traz o protótipo do printf:
4. No gcc 15, com o padrão C23, o que acontece ao chamar uma função definida só mais abaixo, sem protótipo?
5. O arquivo tem int triplo(int n);, chama triplo(5) e não tem a definição. Quem reclama?
6. Qual destes é uma definição (e não só uma declaração)?
Missão final: o termômetro com o main no topo
Missão: a Ana quer o main logo no começo do arquivo, como um resumo do programa. Complete o arquivo:
- no topo, os protótipos de
double para_fahrenheit(double celsius)evoid mostrar(double celsius); - depois do
main, as definições:para_fahrenheitdevolvecelsius * 9 / 5 + 32;mostrarescreve a linha25.0 °C = 77.0 °F(com%.1f), usandopara_fahrenheit.
25.0 °C = 77.0 °F -10.0 °C = 14.0 °F
#include <stdio.h>
// protótipos aqui
int main(void) {
mostrar(25.0);
mostrar(-10.0);
return 0;
}
// definições aqui
#include <stdio.h>
double para_fahrenheit(double celsius);
void mostrar(double celsius);
int main(void) {
mostrar(25.0);
mostrar(-10.0);
return 0;
}
double para_fahrenheit(double celsius) {
return celsius * 9 / 5 + 32;
}
void mostrar(double celsius) {
printf("%.1f °C = %.1f °F\n", celsius, para_fahrenheit(celsius));
}
Resumo
- O compilador lê de cima para baixo e precisa conhecer a função antes da chamada.
- Sem isso:
error: implicit declaration of function. Era só aviso até o gcc 13 (o C89 "chutava" que a função devolviaint); é erro desde o gcc 14, e o C23 removeu a ideia da linguagem. - Protótipo = cabeçalho +
;(int triplo(int n);). É uma declaração; a definição tem o corpo e existe uma vez só. - Os nomes dos parâmetros são opcionais no protótipo, mas ajudam quem lê. Sem parâmetros:
(void). - Protótipo e definição diferentes:
conflicting types. Protótipo sem definição: o ligador dizreferência não definida(undefined reference). - Os
.h, como ostdio.h, são coleções de protótipos; as definições vêm prontas na biblioteca.