Publicidade

Projetos em C com múltiplos arquivos

Passe de um único arquivo .c para um layout realista de header/source/Makefile.

Por que dividir arquivos

Quando seu código ultrapassa cerca de 1.000 linhas, um único arquivo .c fica difícil de ler e lento para recompilar. Dividi-lo traz várias vantagens:

Exemplo de header + source

Aqui está uma biblioteca matemática bem pequena dividida em três arquivos:
mathutil.h (header)
// declarations only
#ifndef MATHUTIL_H
#define MATHUTIL_H

int add(int a, int b);
int mul(int a, int b);
int factorial(int n);

#endif
mathutil.c (implementação)
#include "mathutil.h"

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

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

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
main.c (usuário)
#include <stdio.h>
#include "mathutil.h"

int main(void) {
    printf("2+3 = %d\n", add(2, 3));
    printf("4*5 = %d\n", mul(4, 5));
    printf("5! = %d\n", factorial(5));
    return 0;
}
Os papéis: o .h lista o que este módulo exporta (declarações), enquanto o .c contém como ele funciona de fato (implementação). Um leitor deveria conseguir usar o módulo só com o header.
#include "..." vs. <...>: use aspas para headers do seu próprio projeto e colchetes angulares para headers de bibliotecas padrão ou externas.

Fluxo de build e opções do gcc

Internamente, o gcc executa quietamente quatro estágios: pré-processar → compilar → montar → linkar. Entender isso torna builds de vários arquivos bem mais claros.

Construa os três de uma vez

$ gcc main.c mathutil.c -o app $ ./app 2+3 = 5 4*5 = 20 5! = 120

Separando os estágios (a base do build incremental)

$ gcc -c main.c # só compila → main.o $ gcc -c mathutil.c $ gcc main.o mathutil.o -o app # link
Da próxima vez, se apenas main.c mudou, você só recompila ele e re-linka. É exatamente isso que o make automatiza.

Flags comuns do gcc

FlagSignificado
-cSó compila, não linka. Produz um arquivo .o.
-o nomeDefine o nome do arquivo de saída.
-Wall -WextraHabilita os avisos comuns. Sempre use.
-gInclui informações de depuração (para usar com gdb).
-O0 / -O2Sem otimização / otimização padrão para release.
-std=c11 / -std=c17Seleciona a versão do padrão C.
-I dirAdiciona um caminho de busca de headers.
-L dirAdiciona um caminho de busca de bibliotecas.
-l nomeLinka libnome — por exemplo, -lm para a biblioteca matemática.
-D MACRODefine uma macro do pré-processador.

include guards e extern

Por que você precisa de um include guard

Se o mesmo header é incluído duas vezes, seu conteúdo é expandido duas vezes e você acaba com erros de "redefinição". O padrão de macro abaixo garante que o header seja expandido no máximo uma vez por arquivo .c:
// top and bottom of mathutil.h
#ifndef MATHUTIL_H
#define MATHUTIL_H

/* header contents */

#endif
Alternativa moderna: tanto gcc quanto clang suportam #pragma once — uma única linha que faz o mesmo trabalho. Não está no padrão C, mas é amplamente portátil.

Compartilhando uma variável global com extern

Para usar a mesma variável global a partir de vários arquivos .c, coloque uma declaração extern no header (só nome e tipo) e a definição real em exatamente um arquivo .c.
config.h
#ifndef CONFIG_H
#define CONFIG_H

extern int g_verbose;   // declaration only

#endif
config.c
#include "config.h"

int g_verbose = 0;      // the one and only definition
Não coloque int g_verbose; no header: cada .c que incluísse ele teria sua própria definição, gerando um erro de "definição múltipla" no linker.

Noções básicas de Makefile

A ferramenta make lê um Makefile, descobre quais arquivos mudaram e reconstrói apenas o necessário.

Makefile mínimo

# Makefile — filename must start with capital M
# IMPORTANT: lines under a target must start with a TAB character

app: main.o mathutil.o
	gcc main.o mathutil.o -o app

main.o: main.c mathutil.h
	gcc -c main.c

mathutil.o: mathutil.c mathutil.h
	gcc -c mathutil.c

clean:
	rm -f *.o app
$ make gcc -c main.c gcc -c mathutil.c gcc main.o mathutil.o -o app $ make # nada mudou → nenhum trabalho make: 'app' is up to date. $ make clean

Usando variáveis e regras implícitas

CC      = gcc
CFLAGS  = -Wall -Wextra -O2 -g
OBJS    = main.o mathutil.o
TARGET  = app

$(TARGET): $(OBJS)
	$(CC) $(OBJS) -o $(TARGET)

# implicit rule handles .c → .o automatically
main.o: main.c mathutil.h
mathutil.o: mathutil.c mathutil.h

clean:
	rm -f $(OBJS) $(TARGET)

.PHONY: clean
.PHONY: declara alvos que não são arquivos reais. Sem isso, um arquivo chamado clean existindo no disco enganaria o make, fazendo-o achar que o alvo já está atualizado.

Dependências geradas automaticamente (avançado)

CFLAGS += -MMD
-include $(OBJS:.o=.d)
A flag -MMD do gcc escreve arquivos .d listando dependências de headers, então o make automaticamente reconstrói sempre que qualquer header incluído muda.

Desafios

Desafio 1: construa o projeto de três arquivos
Crie mathutil.h, mathutil.c, main.c e um Makefile, depois rode make e ./app.
Desafio 2: linke a biblioteca matemática
Adicione uma função em mathutil.c que use sqrt() de <math.h>. Adicione -lm em LDFLAGS e confirme que o link funciona.
Desafio 3: builds de debug e release
Adicione dois alvos: make debug com -g -O0 e make release com -O2.
Desafio 4: reproduza um erro de redefinição
Remova o include guard de um header, inclua-o a partir de dois lugares diferentes e leia o erro resultante. Depois, coloque o guard de volta e reconstrua.

Quiz de Revisão

Confira sua compreensão desta aula!

Q1. Qual é a forma padrão de compilar e linkar múltiplos arquivos .c de uma vez?

gcc main.c util.c -o app
gcc main.c + util.c
gcc -combine main.c util.c

Liste vários arquivos .c na linha de comando e o gcc vai compilar cada um e linkar todos juntos no final.

Q2. Qual é o idioma padrão para evitar inclusão dupla de um arquivo de cabeçalho?

Use include guards: #ifndef / #define / #endif
#pragma once_only
Definir uma macro #include_guard

Muitos compiladores também suportam #pragma once, mas para máxima portabilidade o guard clássico com #ifndef ainda é o padrão.

Q3. Qual é a forma básica de um alvo de Makefile e suas dependências?

alvo: dependências em uma linha, comandos na próxima linha com TAB
alvo = dependências => comando
[alvo] dependências: comando

Linhas de comando precisam começar com um caractere TAB literal — essa é uma regra rígida do make.