C Arquivos .h e .c
Até aqui, cada programa coube num arquivo só. Programas de verdade têm dezenas ou centenas de arquivos, e cada um cuida de um assunto: um calcula as notas, outro lê o arquivo de alunos, outro mostra o menu. Em C, cada assunto costuma ter dois arquivos: um .h, que diz o que existe, e um .c, que diz como funciona. Nesta aula você vai separar um programa assim, entender o que vai em cada arquivo e conhecer os erros que aparecem quando alguma coisa vai para o lugar errado.
- A diferença entre declarar e definir uma função.
- Como dividir um programa em
matematica.h,matematica.cemain.c, e compilar tudo junto. - O que vai no
.h(protótipos, structs, macros) e o que vai no.c(as definições). - Por que o
matematica.cinclui o própriomatematica.h. - Por que todo
.htem um include guard. - Os erros
implicit declaration,conflicting types,referência não definidaemúltiplas definições.
Todos os programas desta aula foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c/notas, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra. Você vai precisar de Funções, Protótipos e Pré-processador (o #include).
A ideia: o cardápio e a cozinha
Num restaurante, o cliente recebe o cardápio: o nome de cada prato, o que vem nele e o preço. Ele não precisa saber a receita. A receita fica na cozinha, e só o cozinheiro a conhece. Se a receita mudar (menos sal, outro tempero), o cardápio continua igual.
| Restaurante | Programa em C | Conteúdo |
|---|---|---|
| Cardápio | matematica.h (cabeçalho, header) | "Existe uma função media, que recebe as notas e devolve um double." |
| Cozinha | matematica.c (implementação) | O código que calcula a média, passo a passo. |
| Cliente | main.c | Lê o cardápio (#include "matematica.h") e pede o prato (media(notas, n)). |
Você já usa esse esquema desde o primeiro programa: o stdio.h é o cardápio do printf, e a cozinha (o código do printf) está pronta na biblioteca C do sistema.
1. Declarar × definir
Duas palavras que vão aparecer o tempo todo:
double media(const int valores[], int n); // declaração (protótipo)
double media(const int valores[], int n) { // definição
return (double) soma(valores, n) / n;
}
- declaração
- Só a primeira linha da função, terminada em
;. Diz o nome, o tipo do resultado e os parâmetros. É a promessa "esta função existe em algum lugar". Também se chama protótipo (aula Protótipos). - definição
- A função completa, com o corpo entre
{ }. É o código de verdade. Cada função tem uma definição no programa inteiro.
A regra que organiza tudo: declarações vão no .h; definições vão no .c. Uma declaração pode aparecer quantas vezes for preciso; uma definição, uma vez só.
2. Um projeto em três arquivos
A Ana dá aulas de reforço e quer calcular a soma, a média e a maior nota de uma turma. Ela separa as contas num "módulo" matematica, que outros programas dela também vão usar.
O cabeçalho: matematica.h
#ifndef MATEMATICA_H #define MATEMATICA_H #define NOTA_MAXIMA 10 int soma(const int valores[], int n); double media(const int valores[], int n); int maior(const int valores[], int n); #endif
- #ifndef MATEMATICA_H
- O include guard, com o
#defineda linha de baixo e o#endifdo fim: garante que o conteúdo entra uma vez só, mesmo que o arquivo seja incluído duas vezes. A aula Compilação Condicional mostra o erro que ele evita. - #define NOTA_MAXIMA 10
- Uma macro que quem usa o módulo precisa conhecer.
- int soma(…);
- Os três protótipos. Só a "linha do cardápio", com
;no fim. Oconstpromete que a função não muda o array (aula const e Ponteiros).
A implementação: matematica.c
#include "matematica.h"
int soma(const int valores[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += valores[i];
}
return total;
}
double media(const int valores[], int n) {
if (n == 0) {
return 0.0;
}
return (double) soma(valores, n) / n;
}
int maior(const int valores[], int n) {
int m = valores[0];
for (int i = 1; i < n; i++) {
if (valores[i] > m) {
m = valores[i];
}
}
return m;
}
As três definições. Repare na primeira linha: o matematica.c inclui o próprio matematica.h. Parece desnecessário, mas é uma proteção importante (seção 4). Ele não inclui stdio.h porque não usa nada de lá: cada arquivo inclui só o que usa.
Quem usa: main.c
#include <stdio.h>
#include "matematica.h"
int main(void) {
int notas[] = {7, 9, 6, 10};
int n = 4;
printf("Soma das notas: %d\n", soma(notas, n));
printf("Média: %.2f (de 0 a %d)\n", media(notas, n), NOTA_MAXIMA);
printf("Maior nota: %d\n", maior(notas, n));
return 0;
}
O main.c não sabe como a média é calculada. Ele só sabe, pelo cabeçalho, que media existe e devolve um double. Para compilar, passe os dois arquivos .c ao gcc. O .h não vai no comando: ele entra pelo #include.
$ ls main.c matematica.c matematica.h $ gcc -std=c23 -Wall -Wextra main.c matematica.c -o notas $ ./notas Soma das notas: 32 Média: 8.00 (de 0 a 10) Maior nota: 10
O gcc compilou cada .c separadamente e depois juntou os dois num programa só, o notas. Os detalhes dessa junção (e como compilar cada arquivo numa etapa própria) estão na próxima aula, Vários Arquivos.
Experimente: as abas são os três arquivos, e o editor compila todos os .c juntos (como o comando acima). Rode. Depois crie uma função int menor(const int valores[], int n);: o protótipo no matematica.h, a definição no matematica.c e um printf no main.c.
#include <stdio.h>
#include "matematica.h"
int main(void) {
int notas[] = {7, 9, 6, 10};
int n = 4;
printf("Soma das notas: %d\n", soma(notas, n));
printf("Média: %.2f (de 0 a %d)\n", media(notas, n), NOTA_MAXIMA);
printf("Maior nota: %d\n", maior(notas, n));
return 0;
}
#ifndef MATEMATICA_H #define MATEMATICA_H #define NOTA_MAXIMA 10 int soma(const int valores[], int n); double media(const int valores[], int n); int maior(const int valores[], int n); #endif
#include "matematica.h"
int soma(const int valores[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += valores[i];
}
return total;
}
double media(const int valores[], int n) {
if (n == 0) {
return 0.0;
}
return (double) soma(valores, n) / n;
}
int maior(const int valores[], int n) {
int m = valores[0];
for (int i = 1; i < n; i++) {
if (valores[i] > m) {
m = valores[i];
}
}
return m;
}
3. O que o compilador vê no main.c
Lembre: o #include cola o arquivo inteiro no lugar dele. O gcc -E -P mostra o main.c como o compilador o recebe (aqui, só as últimas 12 linhas, porque antes vem todo o stdio.h):
$ gcc -E -P main.c | tail -12
int soma(const int valores[], int n);
double media(const int valores[], int n);
int maior(const int valores[], int n);
int main(void) {
int notas[] = {7, 9, 6, 10};
int n = 4;
printf("Soma das notas: %d\n", soma(notas, n));
printf("Média: %.2f (de 0 a %d)\n", media(notas, n), 10);
printf("Maior nota: %d\n", maior(notas, n));
return 0;
}
Os três protótipos chegaram ao main.c, e o NOTA_MAXIMA virou 10. Para o compilador, é como se você tivesse escrito os protótipos à mão no topo do main.c. O .h só evita que você copie essas linhas em cada arquivo que usa o módulo (e esqueça de atualizar uma das cópias).
Sem o #include "matematica.h", o main.c não conhece nada disso:
$ gcc -std=c23 -Wall -Wextra sem_include.c matematica.c -o notas
sem_include.c: In function ‘main’:
sem_include.c:7:36: error: implicit declaration of function ‘soma’ [-Wimplicit-function-declaration]
7 | printf("Soma das notas: %d\n", soma(notas, n));
| ^~~~
sem_include.c:8:41: error: implicit declaration of function ‘media’ [-Wimplicit-function-declaration]
8 | printf("Média: %.2f (de 0 a %d)\n", media(notas, n), NOTA_MAXIMA);
| ^~~~~
sem_include.c:8:58: error: ‘NOTA_MAXIMA’ undeclared (first use in this function)
8 | printf("Média: %.2f (de 0 a %d)\n", media(notas, n), NOTA_MAXIMA);
| ^~~~~~~~~~~
« … e o mesmo para maior …
"Declaração implícita da função soma": você usou uma função que nunca foi declarada. Em C muito antigo (C89), o compilador "adivinhava" que ela devolvia int. Desde o C99 isso é proibido, e o gcc 14 em diante trata como erro (antes era só um aviso). Ainda bem: a adivinhação daria errado justamente na media, que devolve double.
4. Por que o .c inclui o próprio .h
Imagine que a Ana, distraída, escreveu a media no matematica.c devolvendo float, enquanto o matematica.h promete double. Como o matematica.c inclui o matematica.h, o compilador vê as duas coisas no mesmo arquivo e compara:
$ gcc -std=c23 -Wall -Wextra main.c matematica.c -o notas
matematica.c:11:7: error: conflicting types for ‘media’; have ‘float(const int *, int)’
11 | float media(const int valores[], int n) {
| ^~~~~
In file included from matematica.c:1:
matematica.h:7:8: note: previous declaration of ‘media’ with type ‘double(const int *, int)’
7 | double media(const int valores[], int n);
| ^~~~~
"Tipos conflitantes para media": a definição tem float, e a declaração anterior (no matematica.h, incluído pela linha 1 do matematica.c) tem double. Repare que o const int valores[] aparece como const int *: um array como parâmetro é, na verdade, um ponteiro (aula Ponteiros e Arrays).
Agora o mesmo engano, num matematica.c que não inclui o próprio .h (o arquivo sem_h.c):
$ gcc -std=c23 -Wall -Wextra main.c sem_h.c -o notas && ./notas Soma das notas: 32 Média: 0.00 (de 0 a 10) « deveria ser 8.00 Maior nota: 10
Nenhum erro, nenhum aviso, e uma média errada. Cada .c é compilado sozinho: o main.c acredita no .h e espera um double; o sem_h.c entrega um float, em outro formato. Ninguém compara os dois. O resultado é lixo (aqui, 0.00; em outra máquina, outro valor). É um bug difícil de achar, e a linha #include "matematica.h" no topo do matematica.c o transforma num erro de compilação claro.
x.c começa com #include "x.h". Muitos programadores põem essa linha como a primeira do arquivo, antes até dos cabeçalhos do sistema: assim, se o x.h esquecer de incluir algo de que precisa, o erro aparece logo.
Missão: provoque o erro. Na aba matematica.c, troque o double da definição de media por float e rode: o compilador deve reclamar de tipos conflitantes.
#include <stdio.h>
#include "matematica.h"
int main(void) {
int notas[] = {7, 9, 6, 10};
printf("Média: %.2f\n", media(notas, 4));
return 0;
}
#ifndef MATEMATICA_H #define MATEMATICA_H double media(const int valores[], int n); #endif
#include "matematica.h"
double media(const int valores[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += valores[i];
}
return (double) total / n;
}
#include "matematica.h"
float media(const int valores[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += valores[i];
}
return (double) total / n;
}
5. O que não vai no .h
Se o .h é colado em vários .c, tudo o que estiver nele aparece em vários .c. Declarações podem se repetir; definições, não. Veja o que acontece quando a Ana põe o corpo de soma no cabeçalho, e dois arquivos (main.c e relatorio.c) o incluem:
#ifndef MATEMATICA_H
#define MATEMATICA_H
int soma(const int valores[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += valores[i];
}
return total;
}
#endif
$ gcc -std=c23 -Wall -Wextra main.c relatorio.c -o notas /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccm4i2ba.o: na função "soma": relatorio.c:(.text+0x0): múltiplas definições de "soma"; /tmp/ccW5XBzu.o:main.c:(.text+0x0): definido primeiro aqui collect2: error: ld returned 1 exit status
Desta vez quem reclama não é o compilador, é o ligador (ld), o programa que junta os pedaços. Cada .c compilou sem problemas, mas os dois trouxeram uma soma completa, e o programa não pode ter duas. A mensagem do ligador sai em português no sistema da Ana: "múltiplas definições de soma ... definido primeiro aqui" (em inglês: multiple definition of `soma'; no editor do guia: duplicate symbol: soma).
.h entre duas vezes no mesmo .c. Aqui ele entrou uma vez no main.c e uma vez no relatorio.c: são compilações separadas, e cada uma começa do zero. O guard não ajuda nesse caso; o conserto é tirar a definição do .h.
O conserto: deixe no .h só o protótipo int soma(const int valores[], int n); e ponha o corpo num .c. Há uma exceção: funções pequenas marcadas como static inline podem ficar no .h, porque o static faz cada .c ter a sua cópia particular (aulas static e extern e Funções inline).
O mesmo vale para variáveis globais: int total_alunos = 0; no .h dá o mesmo erro. O jeito certo (com extern) está em static e extern.
E #include "matematica.c"?
Funciona num programa pequeno, e você vai ver isso em códigos de iniciante. Mas é um mau hábito: o .c inteiro é colado, e no dia em que dois arquivos fizerem isso, vêm as múltiplas definições. Inclua só .h, e passe os .c ao gcc.
O que vai em cada arquivo
No .h (o cardápio) | No .c (a cozinha) |
|---|---|
Include guard (ou #pragma once) | #include "proprio.h" na primeira linha |
| Protótipos das funções que outros arquivos usam | As definições dessas funções |
Definições de struct, enum, typedef que aparecem nos protótipos | Funções auxiliares que só este arquivo usa (marcadas static) |
Macros que quem usa precisa (#define NOTA_MAXIMA 10) | Variáveis globais (a definição, uma vez só) |
extern int x; (declaração de uma variável global) | Os #include que só a implementação usa |
#include só do que o próprio .h precisa | |
Nunca: corpo de função (exceto static inline), variável com valor | Nunca: #include de outro .c |
Boas práticas
- Um par por assunto:
notas.h+notas.c,arquivo.h+arquivo.c. O nome do.he do.cé o mesmo. - O
.hse basta: se um protótipo usasize_touFILE, o.hinclui<stddef.h>ou<stdio.h>. Quem inclui o seu.hnão deve precisar adivinhar o que incluir antes. - Mostre só o necessário: o
.hé a "vitrine" do módulo. Funções auxiliares ficam fora dele, estaticno.c. - Nomes com prefixo em projetos maiores:
mat_soma,mat_media. O C não tem "sobrenome" para funções, e duas bibliotecas com uma funçãosomanão podem ser usadas juntas.
Quando dá errado
error: implicit declaration of function ‘soma’
O que significa: a função foi usada sem declaração antes. Quase sempre falta o #include do .h, ou o protótipo não está no .h. No editor do guia, o Clang (no modo C23) diz use of undeclared identifier 'soma', "uso de um nome não declarado".
Como consertar: confira se o .c tem #include "matematica.h" e se o protótipo está lá, com o nome escrito igual.
error: conflicting types for ‘media’
O que significa: a definição no .c não bate com a declaração no .h: tipo de retorno ou parâmetros diferentes. A note: previous declaration mostra a linha do .h.
Como consertar: decida qual está certo e deixe os dois iguais. Copie a primeira linha da definição para o .h e troque o { por ;.
fatal error: matematica.h: Arquivo ou diretório inexistente
O que significa: o .h não está na pasta do .c, o nome está diferente, ou você usou <matematica.h> em vez de aspas.
Como consertar: veja com ls, use aspas, e se o .h está numa subpasta, use -I pasta (aula Pré-processador).
referência não definida para "soma" (undefined reference to `soma')
$ gcc -std=c23 -Wall -Wextra main.c -o notas /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccPkJjYs.o: na função "main": main.c:(.text+0x4b): referência não definida para "soma" /usr/bin/x86_64-linux-gnu-ld.bfd: main.c:(.text+0x74): referência não definida para "media" /usr/bin/x86_64-linux-gnu-ld.bfd: main.c:(.text+0xa8): referência não definida para "maior" collect2: error: ld returned 1 exit status
O que significa: o main.c compilou (o .h prometeu as funções), mas, na hora de juntar, o ligador não achou as definições. Aqui o matematica.c ficou de fora do comando. No editor: undefined symbol: soma.
Como consertar: passe todos os .c ao gcc: gcc … main.c matematica.c -o notas. Se o arquivo está no comando, confira se a função foi mesmo definida lá, com o mesmo nome. Mais em Vários Arquivos.
múltiplas definições de "soma" (multiple definition of `soma')
O que significa: o corpo de uma função (ou uma variável com valor) está num .h incluído por mais de um .c, ou a mesma função foi escrita em dois .c.
Como consertar: deixe só o protótipo no .h e a definição em um .c.
Referência
| Escrita | O que faz |
|---|---|
int soma(int a, int b); | Declaração (protótipo): vai no .h |
int soma(int a, int b) { … } | Definição: vai no .c, uma vez só |
#include "matematica.h" | Cola o cabeçalho (no main.c e no próprio matematica.c) |
#ifndef X_H / #define X_H / #endif | Include guard em todo .h |
gcc main.c matematica.c -o notas | Compila os .c e junta tudo (o .h não vai no comando) |
gcc -E -P main.c | Mostra o main.c com os .h já colados |
Exercícios
1. O protótipo
No .c está a definição int dobro(int x) { return 2 * x; }. Complete a linha que vai no .h:
2. Compilar
O projeto tem main.c, loja.c e loja.h. Complete o comando:
3. Qual destas linhas não deve ficar num .h incluído por vários .c?
4. Por que o matematica.c inclui o matematica.h?
5. O main.c compila, mas o comando termina com referência não definida para "maior". O que é mais provável?
Missão final: escreva o cabeçalho da lojinha
Missão: o loja.c e o main.c estão prontos, mas o loja.h está vazio, e nada compila. Escreva o loja.h: um include guard com o nome LOJA_H, a macro PRECO_CADERNO valendo 12 e os protótipos das duas funções do loja.c. Sem avisos.
3 cadernos: R$ 36 Com desconto: R$ 33
#include <stdio.h>
#include "loja.h"
int main(void) {
int total = total_compra(PRECO_CADERNO, 3);
printf("3 cadernos: R$ %d\n", total);
printf("Com desconto: R$ %d\n", aplicar_desconto(total, 10));
return 0;
}
// escreva aqui o cabeçalho da lojinha
#include "loja.h"
int total_compra(int preco, int quantidade) {
return preco * quantidade;
}
int aplicar_desconto(int valor, int porcento) {
return valor - valor * porcento / 100;
}
#ifndef LOJA_H #define LOJA_H #define PRECO_CADERNO 12 int total_compra(int preco, int quantidade); int aplicar_desconto(int valor, int porcento); #endif
Resumo
- Declarar (protótipo, com
;) diz que a função existe; definir (com o corpo) diz como ela funciona. Declarações podem se repetir; definições, não. - O
.hé o cardápio: include guard, protótipos,structs, macros. O.cé a cozinha: as definições. - Compile passando todos os
.c:gcc main.c matematica.c -o notas. O.hentra pelo#include, nunca no comando. - Todo
x.cinclui o própriox.h: o compilador confere se a definição bate com o protótipo (conflicting types). Sem isso, um tipo errado vira lixo em silêncio. - Nada de corpo de função (exceto
static inline) nem variável com valor no.h: dámúltiplas definiçõesno ligador, e o include guard não ajuda. - Sem o
#include:implicit declaration(erro no gcc 14+). Sem o.cno comando:referência não definida. A seguir: Vários Arquivos.