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.

Você vai aprender
  • A diferença entre declarar e definir uma função.
  • Como dividir um programa em matematica.h, matematica.c e main.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.c inclui o próprio matematica.h.
  • Por que todo .h tem um include guard.
  • Os erros implicit declaration, conflicting types, referência não definida e mú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.

RestaurantePrograma em CConteúdo
Cardápiomatematica.h (cabeçalho, header)"Existe uma função media, que recebe as notas e devolve um double."
Cozinhamatematica.c (implementação)O código que calcula a média, passo a passo.
Clientemain.cLê 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 #define da linha de baixo e o #endif do 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. O const promete 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.

Regra: todo 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).

"Mas eu pus o include guard!" O include guard impede que o .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 usamAs definições dessas funções
Definições de struct, enum, typedef que aparecem nos protótiposFunçõ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 valorNunca: #include de outro .c

Boas práticas

  • Um par por assunto: notas.h + notas.c, arquivo.h + arquivo.c. O nome do .h e do .c é o mesmo.
  • O .h se basta: se um protótipo usa size_t ou FILE, o .h inclui <stddef.h> ou <stdio.h>. Quem inclui o seu .h nã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, e static no .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ção soma nã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

EscritaO 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 / #endifInclude guard em todo .h
gcc main.c matematica.c -o notasCompila os .c e junta tudo (o .h não vai no comando)
gcc -E -P main.cMostra 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:

gcc -std=c23 -Wall -Wextra -o loja

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 .h entra pelo #include, nunca no comando.
  • Todo x.c inclui o próprio x.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ções no ligador, e o include guard não ajuda.
  • Sem o #include: implicit declaration (erro no gcc 14+). Sem o .c no comando: referência não definida. A seguir: Vários Arquivos.