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.

Você vai aprender
  • 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 o stdio.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 -W porque, 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ãoChamar uma função não declarada
C89/C90Permitido: o compilador supõe que ela devolve int
C99 a C17Proibido 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)
C23A 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 (um int) e o que devolve (um int).
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
Caraint 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 haverVárias, desde que iguaisUma só no programa inteiro
ComparaçãoO nome na lista de convidadosO 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); — o double é o preço ou a quantidade? E o int?
  • 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 um double.
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):

  1. O compilador leu o arquivo. Graças ao protótipo, a chamada triplo(5) estava certinha: ele deixou anotado "aqui vai a função triplo, que alguém vai fornecer".
  2. 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 ld terminou com erro" (o collect2 é 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çãoQuando usar
Funções acima do main, na ordem certa, sem protótiposProgramas curtos, de um arquivo só (como a maioria dos exercícios)
Protótipos no topo, main logo depois, definições no fimProgramas 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 .cProgramas com vários arquivos, ou funções que você quer reaproveitar (Arquivos .h e .c)
Protótipo obrigatórioFunçõ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

EscritaO 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:

#include <>

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) e void mostrar(double celsius);
  • depois do main, as definições: para_fahrenheit devolve celsius * 9 / 5 + 32; mostrar escreve a linha 25.0 °C = 77.0 °F (com %.1f), usando para_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 devolvia int); é 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 diz referência não definida (undefined reference).
  • Os .h, como o stdio.h, são coleções de protótipos; as definições vêm prontas na biblioteca.