C Linker Arquitectura
C

Compilação Multi-ficheiro & Linking

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.

Unidades de Compilação

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).

Declaração vs Definição

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

Headers: o Contrato Público

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;
}

Include Guards e #pragma once

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 e a Resolução de Símbolos

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

Makefile: Compilação Incremental

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

A Analogia com Java

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)

Checklist Multi-ficheiro & Linking