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,unioneenum - Declarações
typedef - Macros destinadas a serem compartilhadas
- Declarações
externde 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 #includede 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.