Compilação Separada: Cabeçalhos e Múltiplos Arquivos

[367] Compilação Separada: Cabeçalhos e Múltiplos Arquivos

Incluir um .c em vez de ligar os objetos é o erro que rende definição múltipla e confunde meio mundo. Aqui está a divisão certa: protótipos e tipos no .h, corpo no .c, cada um compilado sozinho e unido pelo ligador — mais o static que torna função e variável privadas ao arquivo, o encapsulamento que o C tem.
Linguagem C

11 min de leitura

Todos os nossos programas, até agora, couberam num único arquivo. Isso é ótimo para aprender, mas insustentável para software real — imagine o kernel do Linux, com seus milhões de linhas, num só .c. Programas de verdade se dividem em muitos arquivos, cada um cuidando de uma parte do problema. Hoje aprendemos a fazer essa divisão do jeito certo, usando cabeçalhos (.h) e arquivos de implementação (.c). É a aula que transforma você de alguém que escreve programas em alguém que organiza projetos — e ela reúne quase tudo que vimos nesta fase.

A ideia central: interface versus implementação

A divisão de código em C gira em torno de uma distinção fundamental. De um lado, a interface: o que um módulo oferece — quais funções e tipos ele expõe para os outros usarem. Do outro, a implementação: como essas funções fazem o que prometem — o código de fato. A interface vai num arquivo de cabeçalho (.h); a implementação, num arquivo de código (.c). Quem quer usar o módulo lê o .h (a "vitrine") e não precisa conhecer o .c (a "cozinha"). Essa separação é o alicerce de todo software organizado.

Um exemplo concreto: um módulo de matemática

Vamos construir um pequeno módulo com duas funções e ver como os arquivos se relacionam. Começamos pela interface, o cabeçalho mymath.h:

// mymath.h — a INTERFACE: o que o módulo oferece
#ifndef MYMATH_H
#define MYMATH_H

// Apenas protótipos: anunciam as funções, sem implementá-las
int somar(int a, int b);
int multiplicar(int a, int b);

#endif // MYMATH_H

Note três coisas. O cabeçalho contém apenas protótipos — as assinaturas das funções, sem corpo, exatamente o conceito que aprendemos lá na aula Funções, Escopo e Passagem por Valor. Ele é cercado por uma guarda de inclusão, a técnica da aula anterior, para não causar redefinições. E ele não tem nenhuma linha de código executável — é pura declaração, a "vitrine" do módulo.

Agora a implementação, no arquivo mymath.c:

// mymath.c — a IMPLEMENTAÇÃO: como o módulo faz o que promete
#include "mymath.h"   // inclui a própria interface, para conferência

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

int multiplicar(int a, int b) {
    return a * b;
}

O mymath.c inclui o próprio mymath.h. Isso não é redundância: permite que o compilador verifique se as implementações batem com os protótipos anunciados. Se você declarar int somar(int, int) no .h mas escrever double somar(...) no .c, o compilador acusa a divergência. É uma rede de segurança automática.

E, por fim, quem usa o módulo — o main.c:

// main.c — o USUÁRIO do módulo
#include <stdio.h>
#include "mymath.h"   // basta incluir a interface

int main(void) {
    printf("3 + 4 = %d\n", somar(3, 4));
    printf("3 * 4 = %d\n", multiplicar(3, 4));
    return 0;
}

O main.c inclui apenas o mymath.h. Ele não inclui o mymath.c — e isso é crucial. Ele conhece só a interface (os protótipos); a implementação será conectada depois, na ligação. Este é o ponto central de toda a aula.

Como compilar vários arquivos

Aqui reencontramos, na prática, as etapas de compilação que estudamos na primeira aula. Cada .c é compilado separadamente em um arquivo-objeto (.o), e depois todos os objetos são ligados num único executável. A forma manual, passo a passo:

# Etapa 1: compilar cada .c em um .o (a flag -c compila sem ligar)
gcc -c mymath.c -o mymath.o
gcc -c main.c   -o main.o

# Etapa 2: ligar os objetos num executável
gcc mymath.o main.o -o programa

./programa

A flag -c diz "compile, mas não ligue" — produz o .o e para. Cada arquivo vira um objeto independente. Só no comando final os objetos são unidos: é aí que a chamada a somar no main.o (que estava "pendente") encontra a implementação de somar no mymath.o. Este é exatamente o trabalho do ligador que descrevemos na aula Por que C existe e o que ele quer de você, agora visível diante dos seus olhos.

Para projetos pequenos, você pode fazer tudo de uma vez, deixando o GCC cuidar das etapas:

gcc main.c mymath.c -o programa

Mas a compilação separada tem uma vantagem que fica óbvia em projetos grandes: se você altera só o mymath.c, precisa recompilar apenas ele, reaproveitando o main.o já pronto. Num projeto com centenas de arquivos, isso é a diferença entre recompilar por segundos ou por minutos. Automatizar esse processo é o tema de aulas futuras (Make e CMake).

O que vai no .h e o que vai no .c

A regra prática que orienta a divisão: no cabeçalho (.h) vão as coisas que outros arquivos precisam conhecer — protótipos de funções públicas, definições de tipos (structs, enums, typedefs) compartilhados, e macros públicas. No arquivo de código (.c) vão as coisas que são detalhes internos — os corpos das funções, e quaisquer funções ou variáveis auxiliares que só o próprio módulo usa. O princípio orientador: exponha o mínimo necessário no .h. Quanto menos um módulo revela sobre suas entranhas, mais livre você fica para mudá-las sem quebrar quem o usa.

Escondendo o que é privado: static em escopo de arquivo

E se um módulo tem uma função auxiliar que só ele deveria usar, e que não quer expor? A palavra-chave static, aplicada a uma função ou variável global, a torna privada ao arquivo — invisível para os outros:

// contador.c
#include "contador.h"

static int total = 0;   // privada: só este arquivo enxerga

static void validar(int n) {  // função auxiliar interna
    // ... só usada dentro deste arquivo
}

void incrementar(int n) {   // pública: anunciada no contador.h
    validar(n);
    total += n;
}

int obter_total(void) {     // pública
    return total;
}

Aqui, total e validar são static: existem só dentro de contador.c e não podem ser acessados de outros arquivos, nem por engano. Já incrementar e obter_total (que estariam declaradas no contador.h) são a interface pública. Esse uso de static é a forma que o C oferece de criar encapsulamento — esconder detalhes internos e expor só o essencial. Voltaremos a explorar o static e suas várias facetas na próxima aula, que trata justamente das classes de armazenamento.

Um cuidado importante: nunca inclua um .c

Um erro comum de iniciantes é tentar #include "mymath.c" em vez de ligar os objetos. Nunca faça isso. Incluir um .c cola o código-fonte inteiro no arquivo, e se dois arquivos fizerem isso, você terá duas cópias das mesmas funções no programa — o ligador reclamará de "definição múltipla". A regra é firme: #include serve para cabeçalhos (.h), que contêm apenas declarações; os arquivos de implementação (.c) são compilados separadamente e unidos pelo ligador. Declarações podem se repetir sem problema; definições, não.

O que vem a seguir

Hoje demos o salto que separa exercícios de projetos: aprendemos a dividir código em módulos, com a interface no .h (protótipos e tipos, protegidos por guarda de inclusão) e a implementação no .c, compilados separadamente e unidos pelo ligador. Vimos como static cria membros privados a um arquivo, dando ao C uma forma de encapsulamento. Na próxima aula, fechamos a Fase 3 examinando a fundo as classes de armazenamentostatic, extern, auto, register —, que controlam onde suas variáveis vivem e por quanto tempo, amarrando conceitos de escopo e tempo de vida que vêm nos acompanhando desde o início.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Crie um módulo saudacao dividido em saudacao.h (com o protótipo de void saudar(const char *nome); e guarda de inclusão) e saudacao.c (com a implementação). Escreva um main.c que use o módulo. Compile os três com um único comando gcc.

Ver resposta

✓ Resposta:

// saudacao.h
#ifndef SAUDACAO_H
#define SAUDACAO_H
void saudar(const char *nome);
#endif
// saudacao.c
#include <stdio.h>
#include "saudacao.h"
void saudar(const char *nome) {
    printf("Olá, %s!\n", nome);
}
// main.c
#include "saudacao.h"
int main(void) {
    saudar("Marcelo");
    return 0;
}

Compilação em um comando: gcc main.c saudacao.c -o programa.

Exercício 2

Recompile o projeto do exercício 1 usando compilação separada: gere saudacao.o e main.o com -c e depois ligue-os. Confirme que o executável funciona igual.

Ver resposta

✓ Resposta: Compilação separada:

gcc -c saudacao.c -o saudacao.o
gcc -c main.c     -o main.o
gcc saudacao.o main.o -o programa
./programa   # Olá, Marcelo!

O resultado é idêntico ao do exercício 1 — a diferença é apenas o processo (dois objetos intermediários unidos no fim), não o comportamento.

Exercício 3

Crie um módulo calculadora com quatro funções (somar, subtrair, multiplicar, dividir), com a interface no .h e a implementação no .c. No .c, adicione uma função auxiliar static que não apareça no .h. Explique por que ela é invisível ao main.c.

Ver resposta

✓ Resposta:

// calculadora.h
#ifndef CALCULADORA_H
#define CALCULADORA_H
double somar(double a, double b);
double subtrair(double a, double b);
double multiplicar(double a, double b);
double dividir(double a, double b);
#endif
// calculadora.c
#include "calculadora.h"

static int chamadas = 0;              // privada ao arquivo
static void registrar(void) {         // auxiliar interna, não exposta
    chamadas++;
}

double somar(double a, double b)       { registrar(); return a + b; }
double subtrair(double a, double b)    { registrar(); return a - b; }
double multiplicar(double a, double b) { registrar(); return a * b; }
double dividir(double a, double b)     { registrar(); return (b != 0.0) ? a / b : 0.0; }

A função registrar é static, o que a limita ao escopo de calculadora.c. Como ela não aparece no calculadora.h (a única coisa que o main.c inclui) e o static impede que o ligador a exponha a outros arquivos, o main.c não tem como conhecê-la nem chamá-la: ela é um detalhe interno do módulo.

Exercício 4

Explique, com suas palavras, por que o main.c inclui mymath.h mas não inclui mymath.c. Como a chamada a somar em main.c acaba encontrando a implementação em mymath.c?

Ver resposta

✓ Resposta: O main.c inclui mymath.h porque precisa apenas conhecer as funções — seus nomes, tipos de parâmetro e de retorno — para poder chamá-las corretamente. Isso é o que o protótipo fornece. Ele não inclui mymath.c porque não precisa saber como as funções são implementadas, e porque incluir o .c colaria o código e causaria duplicação. A ligação entre a chamada e a implementação acontece na etapa de ligação: quando main.c é compilado, a chamada a somar fica como uma referência "pendente" (o compilador confia no protótipo). Ao ligar main.o com mymath.o, o ligador encontra a definição real de somar no mymath.o e conecta a chamada à implementação. É o mecanismo dos protótipos, da aula Funções, Escopo e Passagem por Valor, operando entre arquivos.

Exercício 5

O que acontece se você tentar compilar o projeto fazendo #include "mymath.c" dentro do main.c e depois ligar também o mymath.o? Que erro o ligador emite, e por quê?

Ver resposta

✓ Resposta: Ao fazer #include "mymath.c" no main.c, o código-fonte inteiro de mymath.c (incluindo os corpos de somar e multiplicar) é colado dentro de main.c. Quando você também liga o mymath.o (que contém as mesmas funções compiladas), o programa passa a ter duas definições de somar e de multiplicar. O ligador emite um erro de definição múltipla (algo como "multiple definition of 'somar'"), porque uma função só pode ter uma definição em todo o programa. É por isso que nunca se inclui um .c: declarações (nos .h) podem se repetir, mas definições (nos .c) precisam ser únicas, e cabe ao ligador uni-las, não ao pré-processador colá-las.

Comentários

Mais em Linguagem C

O Mapa da Jornada: O que Você Vai Dominar em 52 Aulas
O Mapa da Jornada: O que Você Vai Dominar em 52 Aulas

Nove fases, da primeira linha impressa na tela ao programa que cria processos…

Classes de Armazenamento: static, extern e o Tempo de Vida
Classes de Armazenamento: static, extern e o Tempo de Vida

A mesma palavra static faz duas coisas diferentes: dentro de uma função…

Aritmética de Ponteiros e a Ligação Profunda com Vetores
Aritmética de Ponteiros e a Ligação Profunda com Vetores

Somar 1 a um ponteiro não soma 1 ao endereço: avança um elemento inteiro…