Em projetos C maiores, é essencial separar a declaração das funções, estruturas e constantes da sua implementação. Essa separação é feita através de arquivos de cabeçalho (header files, extensão .h) e arquivos de código-fonte (source files, extensão .c). O cabeçalho contém as interfaces públicas — o que outros arquivos precisam saber para usar o código — enquanto o arquivo .c contém os detalhes de implementação. Nesta aula, vamos explorar como estruturar esses arquivos, o que deve ou não estar no cabeçalho, e como evitar problemas comuns como inclusões múltiplas.

Dominar essa organização é fundamental para manter o código legível, modular e reutilizável. Você verá como criar bibliotecas próprias, como separar responsabilidades e como facilitar a compilação de projetos grandes. Vamos começar!

Separando .h e .c

A separação entre arquivos de cabeçalho e de implementação é uma prática padrão em C. O arquivo .h (header) atua como um contrato: ele declara o que está disponível para uso, mas não fornece os detalhes de como funciona. O arquivo .c (source) fornece a implementação concreta dessas declarações.

Por exemplo, se você está criando uma biblioteca de operações matemáticas, pode ter um arquivo math_utils.h com as declarações das funções e um math_utils.c com as definições. Outros arquivos do projeto podem incluir apenas o cabeçalho (#include "math_utils.h") e, na hora da compilação, o linker une tudo.

Essa separação traz vários benefícios:

  • Encapsulamento: o usuário do código só conhece a interface, não os detalhes internos.
  • Compilação independente: mudanças na implementação não exigem recompilar todos os arquivos que usam a biblioteca — apenas o arquivo .c alterado e depois o linker.
  • Organização: facilita a navegação e manutenção do código.
  • Reutilização: você pode usar o mesmo cabeçalho em vários programas.

Vamos criar um exemplo prático. Suponha que queremos uma biblioteca de funções para lidar com números complexos. Teremos:

// complexo.h
#ifndef COMPLEXO_H
#define COMPLEXO_H

typedef struct {
    double real;
    double imag;
} Complexo;

Complexo complexo_soma(Complexo a, Complexo b);
Complexo complexo_produto(Complexo a, Complexo b);

#endif // COMPLEXO_H
// complexo.c
#include "complexo.h"

Complexo complexo_soma(Complexo a, Complexo b) {
    Complexo r;
    r.real = a.real + b.real;
    r.imag = a.imag + b.imag;
    return r;
}

Complexo complexo_produto(Complexo a, Complexo b) {
    Complexo r;
    r.real = a.real * b.real - a.imag * b.imag;
    r.imag = a.real * b.imag + a.imag * b.real;
    return r;
}

No arquivo principal, usamos apenas o cabeçalho:

// main.c
#include <stdio.h>
#include "complexo.h"

int main() {
    Complexo a = {1.0, 2.0};
    Complexo b = {3.0, 4.0};
    Complexo s = complexo_soma(a, b);
    printf("Soma: %.1f + %.1fi\n", s.real, s.imag);
    return 0;
}

Para compilar, você precisa compilar ambos os arquivos .c e depois linkar:

gcc -c complexo.c -o complexo.o
gcc -c main.c -o main.o
gcc main.o complexo.o -o programa

Ou, em um único comando:

gcc main.c complexo.c -o programa

O que vai no header

O arquivo de cabeçalho deve conter apenas declarações e definições que sejam necessárias para quem for usar o código. Isso inclui:

  • Protótipos de funções públicas;
  • Definições de tipos (structs, enums, typedefs) que o usuário precisa conhecer;
  • Constantes globais (via #define ou enum);
  • Variáveis globais declaradas como extern (embora seja geralmente desaconselhado usar variáveis globais);
  • Macros que fazem parte da interface.

É importante não colocar no cabeçalho:

  • Definições de funções (a menos que sejam static inline);
  • Variáveis globais definidas (apenas extern);
  • Implementações de funções (a menos que sejam static inline);
  • Inclusões de outros cabeçalhos que não são necessárias para o usuário.

Por exemplo, se a implementação usa uma estrutura auxiliar que não é exposta, ela não deve estar no cabeçalho. Em vez disso, você pode usar o conceito de struct opaca: declarar um tipo incompleto no cabeçalho e definir a struct apenas no arquivo .c.

// fila.h
#ifndef FILA_H
#define FILA_H

typedef struct Fila Fila;

Fila* fila_cria(void);
void fila_destroi(Fila* f);
void fila_insere(Fila* f, int valor);
int fila_remove(Fila* f);
int fila_vazia(const Fila* f);

#endif
// fila.c
#include "fila.h"
#include <stdlib.h>
#include <assert.h>

struct Fila {
    int* dados;
    int inicio;
    int fim;
    int capacidade;
};

// implementação das funções...

Assim, o usuário só pode usar as funções fornecidas, sem acessar diretamente os campos da estrutura.

Include guards e #pragma once

Quando você inclui um cabeçalho em vários arquivos, ou um cabeçalho inclui outro, pode acontecer de o mesmo conteúdo ser incluído múltiplas vezes em um mesmo arquivo de compilação. Isso causa erros de redefinição (por exemplo, de tipos ou funções). Para evitar isso, usamos include guards ou #pragma once.

O include guard tradicional usa preprocessador:

#ifndef NOME_DO_ARQUIVO_H
#define NOME_DO_ARQUIVO_H

// conteúdo do cabeçalho

#endif // NOME_DO_ARQUIVO_H

O nome deve ser único, geralmente baseado no nome do arquivo, como COMPLEXO_H ou FILA_H.

O #pragma once é uma diretiva não padronizada, mas suportada pela maioria dos compiladores (GCC, Clang, MSVC). Ela garante que o arquivo seja incluído apenas uma vez na unidade de tradução.

#pragma once

// conteúdo do cabeçalho

Ambos os métodos são eficazes. #pragma once é mais simples e menos propenso a erros de nome, mas não faz parte do padrão C. Em projetos portáveis, o include guard é preferível. Muitos projetos modernos usam #pragma once por conveniência.

Veja um exemplo completo com include guard:

// ponto.h
#ifndef PONTO_H
#define PONTO_H

typedef struct {
    int x;
    int y;
} Ponto;

Ponto ponto_cria(int x, int y);
void ponto_move(Ponto* p, int dx, int dy);

#endif // PONTO_H

Se você esquecer o guard, e incluir esse cabeçalho duas vezes em um mesmo arquivo, terá erro de redefinição da struct.

Organização

Em projetos maiores, a organização dos arquivos de cabeçalho é crucial. Algumas boas práticas:

  • Um par .h/.c para cada módulo: cada funcionalidade ou tipo abstrato de dados deve ter seu próprio par de arquivos.
  • Nomes coerentes: use nomes que reflitam o conteúdo, como math_utils.h, linked_list.h.
  • Diretórios: em projetos grandes, organize em pastas (ex.: src/, include/) e use caminhos relativos ou flags de compilação como -Iinclude.
  • Inclua apenas o necessário: evite incluir cabeçalhos que não são usados, para reduzir dependências e tempo de compilação.
  • Use static para funções internas: funções que são usadas apenas dentro do arquivo .c devem ser declaradas static para não expor símbolos.
  • Documente o cabeçalho: comentários sobre o propósito de cada função, parâmetros e retorno ajudam outros programadores.

Um exemplo de estrutura de projeto:

meu_projeto/
  include/
    matematica.h
    fila.h
  src/
    matematica.c
    fila.c
    main.c
  Makefile

No Makefile, você pode usar -Iinclude para que o compilador encontre os cabeçalhos.

Boas práticas e observações finais

Além do que já vimos, lembre-se:

  • Mantenha os cabeçalhos independentes: se um cabeçalho usa tipos de outro, inclua o outro explicitamente (ou use forward declarations quando possível).
  • Evite definições de variáveis globais no cabeçalho; use extern e defina em um único arquivo .c.
  • Teste seus cabeçalhos com gcc -Wall -Wextra para pegar problemas.
  • Considere usar ferramentas como include-what-you-use para otimizar inclusões.

Com essas práticas, seu código ficará mais modular, fácil de manter e pronto para crescer.

Referências

Exercícios

  1. Crie um par de arquivos (contador.h e contador.c) para um contador simples com funções contador_incrementa e contador_valor. O contador deve ser uma struct opaca (typedef struct Contador Contador).

    ✓ Resposta:
    // contador.h
    #ifndef CONTADOR_H
    #define CONTADOR_H
    
    typedef struct Contador Contador;
    
    Contador* contador_cria(void);
    void contador_destroi(Contador* c);
    void contador_incrementa(Contador* c);
    int contador_valor(const Contador* c);
    
    #endif // CONTADOR_H
    
    // contador.c
    #include "contador.h"
    #include <stdlib.h>
    
    struct Contador {
        int valor;
    };
    
    Contador* contador_cria(void) {
        Contador* c = malloc(sizeof(Contador));
        if (c) c->valor = 0;
        return c;
    }
    
    void contador_destroi(Contador* c) {
        free(c);
    }
    
    void contador_incrementa(Contador* c) {
        if (c) c->valor++;
    }
    
    int contador_valor(const Contador* c) {
        return c ? c->valor : 0;
    }
    
  2. Explique a diferença entre include guards e #pragma once e quando preferir cada um.

    ✓ Resposta: Include guards são baseados no pré-processador e usam macros únicas. São portáveis e padrão. #pragma once é uma diretiva não padrão, mas suportada pelos principais compiladores, e é mais simples e evita erros de nome. Prefira include guards em código que precisa ser portável para compiladores antigos ou não suportados; #pragma once é aceitável em projetos modernos.
  3. Escreva um cabeçalho para uma biblioteca de operações com vetores 2D (soma, subtração, produto escalar). Use include guard e defina um tipo Vetor2D.

    ✓ Resposta:
    // vetor2d.h
    #ifndef VETOR2D_H
    #define VETOR2D_H
    
    typedef struct {
        double x;
        double y;
    } Vetor2D;
    
    Vetor2D vetor_soma(Vetor2D a, Vetor2D b);
    Vetor2D vetor_subtracao(Vetor2D a, Vetor2D b);
    double vetor_produto_escalar(Vetor2D a, Vetor2D b);
    
    #endif // VETOR2D_H
    
  4. Dado o seguinte cabeçalho incompleto, complete-o com as diretivas de include guard e adicione uma função que retorna a área de um círculo, dado o raio. Explique sua escolha de nome do guard.
    // circulo.h
    ...
    #define CIRCULO_H
    
    double circulo_area(double raio);
    
    #endif // CIRCULO_H
    

    ✓ Resposta:
    #ifndef CIRCULO_H
    #define CIRCULO_H
    
    double circulo_area(double raio);
    
    #endif // CIRCULO_H
    
    O nome CIRCULO_H é baseado no nome do arquivo, em maiúsculas, para garantir unicidade.
  5. Em um projeto com muitos arquivos, qual a vantagem de usar static em funções auxiliares dentro do arquivo .c? Dê um exemplo de declaração.

    ✓ Resposta: A vantagem é que a função não é visível fora do arquivo .c, evitando conflitos de nomes e reduzindo o escopo. Exemplo: static int soma_interna(int a, int b) { return a + b; }