Um programa real em C não vive num único ficheiro. A separação em múltiplos
ficheiros .c com os seus headers .h é a forma
como C organiza código em unidades de compilação independentes — cada
ficheiro compila separadamente, e o linker une os pedaços no executável final.
Perceber esta separação é perceber a diferença entre declaração e definição,
porque include guards existem, o que o linker faz com símbolos, e como
os mesmos conceitos se manifestam em Java via packages e o classpath.
Em C, cada ficheiro .c é uma unidade de compilação
independente — o compilador processa-o isoladamente e produz um ficheiro
objecto .o. Isto significa que um ficheiro .c
não "vê" o conteúdo de outro ficheiro .c directamente:
precisa de uma declaração que diga ao compilador o que existe no exterior.
// Estrutura típica de um projecto C multi-ficheiro:
// projecto/
// ├── main.c ← entry point — usa account e database
// ├── account.h ← declarações públicas de account
// ├── account.c ← implementação de account
// ├── database.h ← declarações públicas de database
// ├── database.c ← implementação de database
// └── Makefile ← instruções de compilação
// Compilação — cada .c compila independentemente:
gcc -c main.c -o main.o // compila main.c → main.o
gcc -c account.c -o account.o // compila account.c → account.o
gcc -c database.c -o database.o // compila database.c → database.o
// Linking — une todos os .o no executável:
gcc main.o account.o database.o -o programa
// A ordem de compilação dos .c não importa — cada um é independente.
// A ordem no linking pode importar para bibliotecas estáticas (ver artigo seguinte).
Esta é a distinção mais importante em C multi-ficheiro. Uma declaração informa o compilador que algo existe — o seu tipo e assinatura — sem fornecer a implementação. Uma definição fornece a implementação e aloca memória. Cada símbolo pode ser declarado múltiplas vezes mas definido exactamente uma vez — a One Definition Rule.
// ── Declarações — dizem ao compilador "isto existe em algum lado" ─────────────
// Declaração de função (prototype):
int calcular_saldo(int account_id, int owner_id);
// O compilador sabe: a função existe, recebe dois int, retorna int
// Não sabe: onde está o código — o linker resolve isto depois
// Declaração de variável externa:
extern int max_connections;
// O compilador sabe: existe um int chamado max_connections noutro ficheiro
// extern = "está definido externamente"
// Declaração de struct (tipo sem membros — forward declaration):
typedef struct Account Account;
// O compilador sabe: existe um tipo Account
// Não sabe: os seus membros — não pode calcular sizeof(Account) ainda
// Útil para ponteiros: Account* p; — o compilador só precisa de saber que Account existe
// ── Definições — fornecem o corpo e alocam memória ───────────────────────────
// Definição de função:
int calcular_saldo(int account_id, int owner_id) {
// ... implementação
return saldo;
}
// Definição de variável global (aloca memória):
int max_connections = 100;
// Sem extern — esta é a definição, aloca 4 bytes na secção .data
// Definição completa de struct:
typedef struct Account {
int id;
int owner_id;
double balance;
char status[16];
} Account;
// ── A One Definition Rule ─────────────────────────────────────────────────────
// CORRECTO: declarar múltiplas vezes, definir uma vez
// account.h — declaração (incluída por múltiplos ficheiros):
int calcular_saldo(int account_id, int owner_id); // declaração — OK incluir muitas vezes
// account.c — definição (apenas num ficheiro):
int calcular_saldo(int account_id, int owner_id) { ... } // definição — apenas uma vez
// ERRO: definir a mesma função em dois ficheiros .c diferentes
// account.c: int calcular_saldo(...) { return 1; }
// database.c: int calcular_saldo(...) { return 2; } // ← linker error: multiple definition
Um ficheiro .h é o contrato público de uma unidade de compilação —
declara o que está disponível para outros ficheiros usarem.
O ficheiro .c correspondente é a implementação privada.
Esta separação é o equivalente em C do conceito de interface/implementação
em Java (public vs implementação interna de uma classe).
// account.h — o contrato público:
#ifndef ACCOUNT_H // include guard — evita inclusão dupla
#define ACCOUNT_H // se ACCOUNT_H já está definido, todo o conteúdo é ignorado
#include <stdbool.h> // tipos necessários para as declarações
// Tipos públicos — qualquer ficheiro que inclua account.h pode usar Account
typedef struct Account {
int id;
int owner_id;
double balance;
char status[16];
} Account;
// Constantes públicas
#define ACCOUNT_STATUS_ACTIVE "ACTIVE"
#define ACCOUNT_STATUS_INACTIVE "INACTIVE"
#define MAX_ACCOUNTS_PER_USER 50
// Declarações de funções públicas — a interface disponível para outros ficheiros
Account* account_create(int owner_id, double initial_balance);
void account_destroy(Account* account);
bool account_deposit(Account* account, double amount);
bool account_withdraw(Account* account, double amount);
double account_get_balance(const Account* account);
Account* account_find_by_id_and_owner(int id, int owner_id);
#endif // ACCOUNT_H ← fim do include guard
// account.c — a implementação privada:
#include "account.h" // include com aspas = ficheiro local
#include <stdlib.h> // malloc, free
#include <string.h> // strcpy, memset
#include <stdio.h> // printf (para logging)
// Variável estática — visível apenas neste ficheiro (static = file scope)
static Account accounts[MAX_ACCOUNTS_PER_USER * 1000];
static int account_count = 0;
// Função estática — privada, não visível fora deste ficheiro
static bool validar_valor(double valor) {
return valor > 0.0 && valor < 1e9;
}
// Implementação das funções públicas declaradas no header:
Account* account_create(int owner_id, double initial_balance) {
if (!validar_valor(initial_balance)) return NULL;
Account* acc = malloc(sizeof(Account));
if (acc == NULL) return NULL;
acc->id = ++account_count;
acc->owner_id = owner_id;
acc->balance = initial_balance;
strcpy(acc->status, ACCOUNT_STATUS_ACTIVE);
return acc;
}
void account_destroy(Account* account) {
if (account == NULL) return;
free(account);
}
bool account_deposit(Account* account, double amount) {
if (account == NULL || !validar_valor(amount)) return false;
account->balance += amount;
return true;
}
// ... resto das implementações
// main.c — usa account sem saber da implementação:
#include "account.h" // inclui o contrato público
#include "database.h" // inclui outro módulo
#include <stdio.h>
int main() {
// main.c apenas vê o que está declarado em account.h
// não tem acesso a validar_valor() — é estática em account.c
// não tem acesso a accounts[] — é estático em account.c
Account* acc = account_create(99, 1500.0);
if (acc == NULL) {
fprintf(stderr, "Erro ao criar conta\n");
return 1;
}
account_deposit(acc, 500.0);
printf("Saldo: %.2f\n", account_get_balance(acc));
account_destroy(acc);
return 0;
}
Se main.c inclui tanto account.h como
database.h, e ambos incluem types.h,
o preprocessador inseriria o conteúdo de types.h duas vezes —
causando erros de redefinição de tipos e structs.
Include guards resolvem isto.
// O problema sem include guards:
// types.h (sem guard):
typedef struct { int x; int y; } Point;
// account.h:
#include "types.h" // insere a definição de Point
// database.h:
#include "types.h" // insere a definição de Point novamente
// main.c:
#include "account.h" // → insere types.h → define Point
#include "database.h" // → insere types.h → define Point NOVAMENTE
// Erro: redefinition of typedef 'Point'
// Solução 1 — Include Guard (standard ISO C):
// types.h:
#ifndef TYPES_H
#define TYPES_H
typedef struct { int x; int y; } Point;
#endif // TYPES_H
// Na segunda inclusão:
// #ifndef TYPES_H → TYPES_H já está definido → todo o bloco é ignorado ✅
// Solução 2 — #pragma once (suportado por todos os compiladores modernos):
// types.h:
#pragma once
typedef struct { int x; int y; } Point;
// O compilador garante que este ficheiro só é incluído uma vez por unidade de compilação
// Mais simples, mas não é standard ISO — é uma extensão universalmente suportada
// Regra prática:
// - Projectos novos: usar #pragma once — mais simples, sem risco de erro no nome da macro
// - Projectos legacy ou com requisito de portabilidade ISO: usar include guards
// - Nunca misturar os dois no mesmo ficheiro
O linker recebe múltiplos ficheiros .o e resolve as referências
cruzadas entre eles — substitui cada referência a um símbolo externo pelo
endereço real onde esse símbolo está definido. Quando o linker não encontra
um símbolo, o erro é imediato e claro.
// O linker em acção — erros comuns e o que significam:
// ── Erro 1: símbolo não definido ─────────────────────────────────────────────
// main.c chama account_find_by_id_and_owner() mas account.c não foi compilado:
gcc main.o database.o -o programa // faltou account.o
// Erro: undefined reference to 'account_find_by_id_and_owner'
// undefined reference to 'account_create'
// Causa: main.o tem referências ao símbolo, mas nenhum .o fornece a definição
// ── Erro 2: símbolo definido múltiplas vezes ──────────────────────────────────
// account.c e utils.c definem ambos uma função global com o mesmo nome:
// account.c: double calcular_taxa(double valor) { return valor * 0.01; }
// utils.c: double calcular_taxa(double valor) { return valor * 0.02; }
gcc main.o account.o utils.o -o programa
// Erro: multiple definition of 'calcular_taxa'
// Solução: tornar uma delas estática (static) — visível apenas no seu ficheiro
// ── Símbolo estático — visibilidade restrita ao ficheiro ─────────────────────
// account.c:
static double calcular_taxa(double valor) { return valor * 0.01; }
// A keyword static em funções/variáveis globais = "não exportar este símbolo"
// O linker não vê este símbolo — não pode causar conflito com outros ficheiros
// Boa prática: todas as funções auxiliares internas devem ser static
// ── Inspecionar símbolos com nm ───────────────────────────────────────────────
nm account.o
// T account_create ← T = Text section, exported (definido e visível)
// T account_deposit ← T = exportado
// t calcular_taxa ← t = Text section, local (static — não exportado)
// U malloc ← U = Undefined (referência externa — será resolvida pelo linker)
// U free ← U = Undefined
//
// Após linking:
nm programa
// Todos os U devem estar resolvidos — se algum U persistir: erro de linking
Com múltiplos ficheiros, recompilar tudo de raiz a cada alteração é
ineficiente. O make rastreia dependências e recompila
apenas os ficheiros que mudaram — se só account.c mudou,
apenas account.o é recompilado e o linking repete.
# Makefile — sintaxe: target: dependências
# [TAB] comando
# ATENÇÃO: a indentação DEVE ser TAB, não espaços
CC = gcc
CFLAGS = -O2 -Wall -Wextra -Wshadow -Wformat=2
LDFLAGS = # flags do linker (bibliotecas, etc.)
TARGET = programa
SRCS = main.c account.c database.c
OBJS = $(SRCS:.c=.o) # substitui .c por .o: main.o account.o database.o
# Target principal — depende de todos os .o
$(TARGET): $(OBJS)
$(CC) $(OBJS) $(LDFLAGS) -o $(TARGET)
# Regra genérica: como compilar qualquer .c para .o
# $< = primeira dependência (.c), $@ = target (.o)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Dependências explícitas — quando um .c inclui um .h
# se account.h mudar, account.o e main.o precisam de ser recompilados
main.o: main.c account.h database.h
account.o: account.c account.h
database.o: database.c database.h account.h
# Target especial — limpar ficheiros gerados
.PHONY: clean
clean:
rm -f $(OBJS) $(TARGET)
# Target de debugging
.PHONY: debug
debug: CFLAGS = -O0 -g3 -fsanitize=address,undefined -Wall -Wextra
debug: $(TARGET)
# Uso:
# make → compila apenas o que mudou
# make clean → remove todos os .o e o executável
# make debug → compila com ASan e símbolos de debugging
# Geração automática de dependências — evitar manter as dependências manualmente
CC = gcc
CFLAGS = -O2 -Wall -Wextra -MMD -MP
# -MMD = gera ficheiros .d com dependências automaticamente
# -MP = adiciona targets phony para headers (evita erro se header for apagado)
SRCS = main.c account.c database.c
OBJS = $(SRCS:.c=.o)
DEPS = $(SRCS:.c=.d) # main.d account.d database.d
TARGET = programa
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Incluir os ficheiros de dependência gerados automaticamente
# -include ignora o erro se os .d ainda não existem (primeira compilação)
-include $(DEPS)
.PHONY: clean
clean:
rm -f $(OBJS) $(DEPS) $(TARGET)
# O GCC gera automaticamente main.d com conteúdo como:
# main.o: main.c account.h database.h /usr/include/stdio.h ...
# Quando account.h muda, make sabe que main.o precisa de ser recompilado
Os mesmos conceitos de separação de interface e implementação, visibilidade de símbolos, e resolução de dependências existem em Java — com uma camada de abstracção por cima que esconde os detalhes.
// Correspondência C ↔ Java:
// C: ficheiro .h (declarações públicas)
// Java: interface ou classe pública com métodos public
//
// account.h: Account.java (interface):
// Account* account_create(...); ↔ Account create(int ownerId, double balance);
// bool account_deposit(...); ↔ boolean deposit(double amount);
// C: static em funções/variáveis globais (visibilidade limitada ao ficheiro)
// Java: private em métodos e campos (visibilidade limitada à classe)
//
// static bool validar_valor(double v) ↔ private boolean validarValor(double v)
// C: #include "account.h" (declara o que está disponível)
// Java: import com.randomt.account.Account (declara o que está disponível)
// C: unidade de compilação (.c) compila independentemente
// Java: classe (.java) compila independentemente para .class
// C: linker une os .o e resolve símbolos externos
// Java: ClassLoader carrega .class e resolve referências em runtime
// (linking dinâmico — em Java é sempre em runtime, nunca em compile time)
// C: undefined reference (erro de linking em compile time)
// Java: ClassNotFoundException / NoClassDefFoundError (erro em runtime)
// → Java descobre dependências em falta MUITO mais tarde que C
// C: nm programa — listar símbolos exportados
// Java: javap -p Account.class — listar membros da classe (incluindo privados)
// C: Makefile — compilação incremental manual
// Java: Maven/Gradle — compilação incremental automática + gestão de dependências
// (ver Módulo 2: Maven & Dependências)
.c é uma unidade de compilação independente — não vê o conteúdo de outros .c directamente..h contém declarações públicas; ficheiro .c contém a implementação privada.#ifndef/#define/#endif) ou #pragma once em todos os headers — evitam inclusão dupla.static em funções e variáveis globais restringe a visibilidade ao ficheiro — usar em todas as funções auxiliares internas.undefined reference = faltou um .o no linking ou a função nunca foi definida.multiple definition = dois ficheiros definem o mesmo símbolo global — tornar um static.-MMD -MP para geração automática de dependências — evita manter dependências manualmente.