Menu

Arquivos de cabeçalho em C: .h vs .c, guardas de inclusão e programas multiarquivo

Como dividir um programa em C entre vários arquivos: o que pertence a um .h, o que pertence a um .c, guardas de inclusão que impedem a inclusão dupla, compilar vários arquivos juntos, e compartilhar globais com extern.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Um único arquivo .c está ótimo até ele ter 2.000 linhas. Dividir um programa entre arquivos permite que cada parte seja compilada separadamente, reutilizada em outros programas e lida por conta própria - mas C não tem um sistema de import. O que ela tem é o pré-processador colando texto, mais um ligador que junta as peças no fim.

Um arquivo de cabeçalho (.h) é o contrato compartilhado entre essas peças: ele conta a cada arquivo-fonte o que existe em outro lugar, sem conter a implementação.

Declarações vs definições

Todo o projeto se apoia em uma distinção.

Uma declaração diz isto existe em algum lugar e este é o seu formato. Ela não gera código e pode aparecer quantas vezes for:

int add(int a, int b);        /* declaração de função (protótipo) */
extern int error_count;       /* declaração de variável */
struct Point { int x, y; };   /* definição de tipo - seguro repetir por arquivo */

Uma definição cria a coisa. Ela precisa aparecer exatamente uma vez em todo o programa:

int add(int a, int b) { return a + b; }   /* definição de função */
int error_count = 0;                      /* definição de variável */

Cabeçalhos guardam declarações. Arquivos-fonte guardam definições. Inverta isso e o ligador reclama de "multiple definition of ..." - a única mensagem de erro que de forma confiável significa que uma definição foi parar em um cabeçalho.

Um programa de dois arquivos

Aqui está a menor divisão útil. Um cabeçalho declarando duas funções:

/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int max_of(int a, int b);

#endif

O arquivo-fonte que as implementa - note que ele inclui o próprio cabeçalho:

/* math_utils.c */
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

int max_of(int a, int b) {
    return (a > b) ? a : b;
}

E o programa que as usa:

/* main.c */
#include <stdio.h>
#include "math_utils.h"

int main(void) {
    printf("add(3, 4)    = %d\n", add(3, 4));
    printf("max_of(3, 4) = %d\n", max_of(3, 4));
    return 0;
}

Compile os dois arquivos-fonte juntos:

gcc main.c math_utils.c -o app
./app

Dois detalhes que valem atenção. Primeiro, math_utils.c inclui o próprio cabeçalho - isso não é redundante. Isso faz o compilador conferir que cada definição combina com sua declaração, então se você mudar o protótipo no cabeçalho e esquecer o arquivo .c, você recebe um erro na hora em vez de uma divergência na ligação.

Segundo, math_utils.h não está na linha de comando do gcc. Cabeçalhos nunca são compilados; eles são colados dentro dos arquivos .c pelo #include. Passar um .h ao compilador produz um arquivo de cabeçalho pré-compilado solto e nenhum código ligável.

O mesmo programa espremido em um arquivo só, para você poder rodá-lo aqui:

As declarações antes da main são o que o cabeçalho fornece na versão real - que é exatamente por que protótipos de função e cabeçalhos são a mesma ideia em duas escalas.

Guardas de inclusão

#include cola texto, e texto colado duas vezes é texto duplicado. Isso é inofensivo para um protótipo de função e fatal para uma struct:

/* shapes.h SEM uma guarda */
struct Point { int x, y; };

Se main.c inclui tanto shapes.h quanto canvas.h, e canvas.h também inclui shapes.h, o compilador vê struct Point definida duas vezes em uma unidade de tradução e para com "redefinition of 'struct Point'". Em um projeto real essas cadeias ficam fundas o bastante para você não conseguir rastreá-las à mão.

A solução é uma guarda de inclusão: uma macro que registra "este cabeçalho já foi colado".

/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H

struct Point { int x, y; };
struct Point origin_point(void);

#endif /* SHAPES_H */

A primeira inclusão encontra SHAPES_H indefinida, então o corpo é mantido - e define SHAPES_H de passagem. Toda inclusão posterior no mesmo arquivo a encontra definida e pula direto para o #endif. O nome da macro precisa ser único no projeto; NOMEDOARQUIVO_H derivado do caminho é a convenção usual.

A alternativa de uma linha é suportada por todo compilador de peso:

/* shapes.h */
#pragma once

struct Point { int x, y; };

#pragma once não pode sofrer colisão de nomes e não pode ser quebrado por um erro de digitação no #endif. Sua única desvantagem é não estar no padrão C, então um projeto que precisa compilar em compiladores incomuns deve preferir a forma #ifndef. De um jeito ou de outro, todo cabeçalho recebe uma - sem exceções, inclusive cabeçalhos que você acha que nada mais vai incluir.

O que pertence a um cabeçalho

Coloque em um .h:

  • Protótipos de função
  • Definições de struct, union e enum
  • Declarações typedef
  • Macros destinadas a serem compartilhadas
  • Declarações extern de variáveis globais compartilhadas
  • Os #includes de que o próprio cabeçalho precisa para ser autossuficiente

Mantenha fora de um .h:

  • Corpos de função (a menos que sejam deliberadamente static inline)
  • Definições de variáveis - int counter; em um cabeçalho define uma variável separada em cada arquivo que o inclui, ou um erro de ligação, dependendo do compilador
  • #include de cabeçalhos de que o cabeçalho não precisa - isso empurra essa dependência para todo mundo rio abaixo

"Autossuficiente" vale como regra: um cabeçalho deve compilar quando incluído primeiro, antes de qualquer outra coisa. Se shapes.h usa size_t, ele mesmo inclui <stddef.h> em vez de torcer para que o arquivo que o inclui tenha feito isso.

Um cabeçalho completo e bem formado:

/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H

#include <stddef.h>   /* para size_t, usado abaixo */

#define MAX_NAME 64

typedef struct {
    char   name[MAX_NAME];
    int    quantity;
    double price;
} Item;

/* compartilhada em todo o programa, definida uma vez em inventory.c */
extern int item_count;

void   inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);

#endif /* INVENTORY_H */

Compartilhando uma global com extern

Uma variável global precisa ser definida em exatamente um arquivo .c e declarada em todos os outros. extern é o que faz a declaração:

/* inventory.h  - declaração, sem armazenamento */
extern int item_count;
/* inventory.c  - a única definição */
#include "inventory.h"
int item_count = 0;
/* main.c - usa a variável, via o cabeçalho */
#include <stdio.h>
#include "inventory.h"

int main(void) {
    printf("%d itens\n", item_count);
    return 0;
}

Tire o extern do cabeçalho e cada arquivo que o inclui define o próprio item_count, o que é um erro de ligação de "multiple definition" na melhor das hipóteses e dois contadores independentes na pior.

A necessidade oposta é igualmente comum: uma variável ou função auxiliar que deve ficar privada a um arquivo .c. static em escopo de arquivo faz isso - dá ao nome ligação interna, invisível para o ligador e portanto para todos os outros arquivos:

/* inventory.c */
static Item storage[256];          /* privado a este arquivo */
static int  find_slot(const char *name);   /* auxiliar privado */

Dois arquivos podem cada um ter um static int counter; sem colisão. Esta é a versão C de um membro privado, e é o padrão que você deve procurar primeiro - só o que outros arquivos genuinamente precisam vai no cabeçalho.

Compilando programas maiores

Listar cada arquivo funciona e é lento, porque todo arquivo é recompilado toda vez:

gcc main.c inventory.c report.c -o app

A forma escalável compila cada fonte para um arquivo-objeto e os liga:

gcc -c main.c        # produz main.o
gcc -c inventory.c   # produz inventory.o
gcc -c report.c      # produz report.o
gcc main.o inventory.o report.o -o app

Agora mudar report.c só precisa de gcc -c report.c e de uma nova ligação. É precisamente essa contabilidade que o make automatiza:

app: main.o inventory.o report.o
	gcc main.o inventory.o report.o -o app

%.o: %.c
	gcc -Wall -Wextra -c $< -o $@

Se seus cabeçalhos ficam em um subdiretório, -Iinclude o adiciona ao caminho de busca dos colchetes angulares.

Erros e o que eles significam

Os dois modos de falha são fáceis de distinguir assim que você sabe qual etapa os produziu.

"undefined reference to 'add'" - um erro do ligador. A declaração foi encontrada, a definição não. Ou você esqueceu de listar o arquivo .c na linha de comando, ou a função é static, ou o nome está escrito errado em um dos dois lugares.

"multiple definition of 'item_count'" - também um erro do ligador, a imagem espelhada: uma definição foi parar em um cabeçalho, ou em dois arquivos-fonte. Mova-a para um .c e deixe uma declaração extern no cabeçalho.

"redefinition of 'struct Item'" - um erro do compilador, significando que um cabeçalho foi colado duas vezes em um arquivo. Adicione a guarda de inclusão.

"implicit declaration of function 'add'" - um aviso do compilador (um erro nos modos C99 e posteriores) significando que o protótipo nunca foi visto. Você esqueceu o #include, ou o cabeçalho não declara a função.

Assim que um programa se espalha por arquivos, a próxima coisa que você normalmente quer é variar o que é compilado por plataforma ou por tipo de build - que é a compilação condicional.

Perguntas frequentes

O que vai em um arquivo .h e o que vai em um arquivo .c?

O cabeçalho guarda declarações - protótipos de função, definições de struct e typedef, enums, macros e declarações extern de globais compartilhadas. O arquivo .c guarda definições - os corpos das funções e as variáveis de fato. A regra prática: um cabeçalho diz o que existe, um arquivo-fonte diz o que aquilo faz.

O que é uma guarda de inclusão e por que eu preciso de uma?

#include cola texto, então incluir um cabeçalho duas vezes cola o conteúdo dele duas vezes - o que redefine cada struct e typedef ali dentro e não compila. Uma guarda de inclusão envolve o cabeçalho em #ifndef MEUCABECALHO_H / #define MEUCABECALHO_H / #endif, de modo que a segunda inclusão vê a macro já definida e pula o corpo.

Como compilo um programa em C com vários arquivos?

Liste cada arquivo .c na linha de comando: gcc main.c math_utils.c -o app. Nunca coloque um arquivo .h ali - cabeçalhos são colados pelo #include, não compilados por conta própria. Para projetos maiores, compile para arquivos-objeto (gcc -c main.c) e ligue-os, que é o que um Makefile automatiza.

Devo usar #pragma once ou guardas de inclusão com #ifndef?

Os dois funcionam. #pragma once é uma linha só e não pode ter colisão de nomes, e todo compilador de peso o suporta - mas ele não está no padrão C. A forma #ifndef/#define/#endif é padrão e funciona em qualquer lugar. Escolha uma e use-a de forma consistente em um projeto; para portabilidade máxima escolha a forma #ifndef.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR