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 armazenamento — static, 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
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 4, Functions and Program Structure
- cppreference — Translation units e o modelo de compilação — https://en.cppreference.com/w/c/language/translation_phases
- Modern C, Jens Gustedt — capítulo sobre organização de código e unidades de tradução — https://gustedt.gitlabpages.inria.fr/modern-c/
- C Interfaces and Implementations, David Hanson — o livro de referência sobre modularização em C
- GCC — opções de compilação e ligação (
-c,-o) — https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html
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.