Organização de Projetos e Convenções da Comunidade

[428] Organização de Projetos e Convenções da Comunidade

Um typedef que não revela os campos da struct obriga quem usa o módulo a passar pelas funções — é o encapsulamento que o C tem, e vem junto do prefixo que faz as vezes de namespace. Em volta dele, a árvore que separa fonte, cabeçalho e teste, e a regra de manter a estrutura proporcional ao tamanho.
Linguagem C

13 min de leitura

Aprendemos a escrever funções limpas e a protegê-las com contratos. Agora subimos um nível: da qualidade de uma função para a qualidade de um projeto inteiro. Um programa C real não é um arquivo solto — é uma estrutura de diretórios, cabeçalhos, bibliotecas e convenções que, juntos, tornam o código navegável, construível e colaborativo. Hoje aprendemos como projetos C profissionais se organizam: onde os arquivos ficam, como os módulos se separam, e quais convenções a comunidade adotou ao longo de décadas. É o momento em que a modularização da aula Compilação Separada: Cabeçalhos e Múltiplos Arquivos encontra a escala de um projeto de verdade.

Por que a organização importa

Quando você abre um projeto C bem organizado pela primeira vez, deveria conseguir entender sua estrutura em minutos: onde está o ponto de entrada, onde ficam os módulos, onde estão os testes, como se compila. Um projeto desorganizado — arquivos espalhados sem lógica, cabeçalhos misturados com fontes, nenhuma convenção clara — impõe uma barreira imediata a qualquer um que tente contribuir ou mantê-lo, inclusive você mesmo no futuro. A organização não é burocracia; é o que torna um projeto acessível. E, como programar em C quase sempre envolve colaboração ou continuidade ao longo do tempo, essa acessibilidade tem valor real.

Uma estrutura de diretórios convencional

Não há um padrão único imposto, mas a comunidade C convergiu para convenções amplamente reconhecíveis. Uma estrutura típica para um projeto de porte médio:

meu-projeto/
├── src/          # arquivos-fonte (.c) — a implementação
│   ├── main.c
│   ├── parser.c
│   └── util.c
├── include/      # cabeçalhos públicos (.h) — a interface
│   ├── parser.h
│   └── util.h
├── tests/        # testes automatizados
│   └── test_parser.c
├── build/        # arquivos gerados pela compilação (não versionado)
├── docs/         # documentação
├── Makefile      # ou CMakeLists.txt
├── README.md     # descrição do projeto e como usá-lo
└── LICENSE       # a licença do código

Cada diretório tem um papel claro. O src/ guarda a implementação (os .c), separada dos cabeçalhos em include/ (os .h que formam a interface pública) — a distinção interface/implementação da aula Compilação Separada: Cabeçalhos e Múltiplos Arquivos, agora refletida na própria organização de pastas. O tests/ isola os testes (aula Testes Automatizados em C). O build/ recebe os arquivos gerados (aula Projetos Maiores com CMake), mantendo-os fora do código-fonte. E arquivos como README.md, LICENSE e o Makefile/CMakeLists.txt vivem na raiz, onde qualquer um os encontra imediatamente. Essa separação por propósito é o que torna o projeto navegável: você sabe onde procurar cada tipo de coisa.

Um módulo bem formado: o par .h/.c

O tijolo fundamental da organização é o módulo — um par de arquivos .h e .c que juntos formam uma unidade coesa, com uma responsabilidade única. Reunindo tudo que aprendemos, um módulo bem formado tem uma estrutura reconhecível. O cabeçalho expõe apenas a interface:

// include/pilha.h — a INTERFACE PÚBLICA do módulo
#ifndef PILHA_H          // guarda de inclusão (aula O Pré-processador: #define, Macros e Inclusões)
#define PILHA_H

#include <stdbool.h>

// tipo opaco: o usuário não vê os detalhes internos
typedef struct Pilha Pilha;

// a interface: só o que o mundo externo precisa
Pilha *pilha_criar(void);
void   pilha_destruir(Pilha *p);
bool   pilha_push(Pilha *p, int valor);
bool   pilha_pop(Pilha *p, int *valor);
bool   pilha_vazia(const Pilha *p);

#endif // PILHA_H

E o .c guarda a implementação, incluindo os detalhes que o cabeçalho esconde:

// src/pilha.c — a IMPLEMENTAÇÃO
#include "pilha.h"
#include <stdlib.h>

// a definição REAL da struct fica aqui, escondida do usuário
struct Pilha {
    struct No *topo;
};

// ... implementações de pilha_criar, pilha_push, etc. ...

Note duas convenções profissionais importantes. Primeiro, o prefixo de módulo: todas as funções começam com pilha_ (pilha_criar, pilha_push). Como C não tem namespaces, esse prefixo evita colisões de nome entre módulos e deixa claro a que módulo cada função pertence — uma convenção universal em bibliotecas C. Segundo, o tipo opaco: o cabeçalho declara typedef struct Pilha Pilha; sem revelar os campos da struct (que só aparecem no .c). Assim, o usuário do módulo manipula a pilha apenas através das funções, sem acessar seus detalhes internos — um encapsulamento genuíno, que permite mudar a implementação sem afetar quem usa o módulo. Essas duas convenções são marcas de código C maduro.

Convenções de nomenclatura da comunidade

Ao longo do curso, seguimos consistentemente o snake_case (palavras em minúsculas separadas por underscore) — e não por acaso: é a convenção dominante na comunidade C. Vale consolidar as convenções mais difundidas. Funções e variáveis em snake_case (calcular_media, total_alunos). Constantes e macros em MAIÚSCULAS_COM_UNDERSCORE (MAX_TAMANHO, PI). Tipos criados com typedef frequentemente em PascalCase ou com sufixo _t (Pilha, ou pilha_t). E o prefixo de módulo nas funções públicas, como vimos. Não existe uma convenção "correta" absoluta — o kernel Linux, o projeto GNU e o Google têm variações —, mas existe a regra inegociável da consistência: escolha um conjunto de convenções e siga-o rigorosamente em todo o projeto. Um projeto onde metade das funções é snake_case e a outra metade camelCase confunde e sinaliza descuido.

Documentando o projeto: o README

Um projeto profissional se apresenta. O arquivo README.md, na raiz, é a porta de entrada — a primeira coisa que alguém lê ao encontrar seu projeto. Um bom README responde às perguntas essenciais: o que o projeto faz, como compilá-lo e rodá-lo, como usá-lo (exemplos), e talvez como contribuir. Não precisa ser extenso; precisa ser útil. Um projeto sem README é como uma loja sem placa — pode ter conteúdo excelente dentro, mas ninguém sabe do que se trata sem investigar. Documentar como construir (o comando make ou os passos do CMake) é especialmente valioso, pois é a primeira coisa que qualquer pessoa precisa fazer. Essa documentação mínima transforma um amontoado de arquivos num projeto que outros conseguem usar.

Aprendendo com projetos reais

Uma das melhores formas de internalizar boa organização é ler código de qualidade. Projetos C consagrados — como o SQLite, o Redis, o cURL, o próprio kernel Linux — são fontes riquíssimas de aprendizado sobre estrutura, convenções e estilo. Ao explorá-los, observe como separam interface de implementação, como nomeiam funções e arquivos, como organizam diretórios, como escrevem seus READMEs. Cada projeto maduro carrega décadas de sabedoria coletiva sobre o que funciona. Você não precisa copiar nenhum estilo específico, mas expor-se a vários amplia seu repertório e afina seu senso do que é código bem organizado. Ler bom código é tão formativo quanto escrevê-lo — talvez mais, no começo.

O equilíbrio: organização proporcional ao projeto

Uma ressalva de bom senso para fechar. A estrutura elaborada que descrevi — múltiplos diretórios, tipos opacos, README completo — é apropriada para projetos de porte médio a grande. Para um exercício de um único arquivo, ou um script pequeno, impor toda essa estrutura seria exagero, uma burocracia que atrapalha mais do que ajuda. A organização deve ser proporcional ao projeto: um programa de 50 linhas vive bem num arquivo só; um de 5.000 linhas precisa de módulos, diretórios e convenções claras. A maturidade está em reconhecer a escala do que você está construindo e aplicar o nível certo de estrutura — nem organização de menos, que gera caos, nem de mais, que gera peso desnecessário. Comece simples e adicione estrutura conforme o projeto cresce e a pede.

O que vem a seguir

Hoje aprendemos a organizar projetos C profissionalmente: uma estrutura de diretórios convencional que separa fontes, cabeçalhos, testes e artefatos; módulos bem formados com prefixos e tipos opacos para encapsulamento genuíno; as convenções de nomenclatura da comunidade e a regra inegociável da consistência; o valor do README; e o hábito de aprender lendo projetos reais — sempre com organização proporcional à escala. Na próxima aula, fechamos a Fase 7 com um tema que exige nuance e maturidade: a otimização consciente e as noções de desempenho — quando e como tornar o código mais rápido, e por que a pressa em otimizar é, frequentemente, o inimigo do bom código.

Fontes e leituras recomendadas

  • The Practice of Programming, Kernighan & Pike — sobre organização, interfaces e estilo
  • C Interfaces and Implementations, David Hanson — o livro definitivo sobre módulos e tipos opacos em C
  • SQLite, Redis, cURL — projetos C exemplares para estudar organização — https://github.com/curl/curl
  • pitchfork — uma proposta de layout de projeto para C/C++ — https://github.com/vector-of-bool/pitchfork
  • 21st Century C, Ben Klemens — estrutura de projetos modernos em C

Exercícios

Exercício 1

Projete (apenas a estrutura de diretórios e arquivos, sem implementar) a organização de um projeto C que seja uma calculadora com módulos separados para operações aritméticas, análise de expressões (parser) e a interface principal. Indique o que ficaria em cada diretório.

Ver resposta

✓ Resposta: Estrutura proposta para a calculadora modular:

calculadora/
├── src/
│   ├── main.c        # interface principal, lê entrada e orquestra
│   ├── aritmetica.c  # implementação das operações (somar, subtrair, etc.)
│   └── parser.c      # implementação da análise de expressões
├── include/
│   ├── aritmetica.h  # interface das operações aritméticas
│   └── parser.h      # interface do parser
├── tests/
│   ├── test_aritmetica.c
│   └── test_parser.c
├── build/            # artefatos gerados (não versionado)
├── Makefile
└── README.md

O src/ tem as implementações; o include/ tem as interfaces públicas de cada módulo; o tests/ tem os testes de cada módulo; o build/ recebe os artefatos; a raiz tem o sistema de build e a documentação. Cada módulo (aritmetica, parser) é um par .h/.c com responsabilidade única.

Exercício 2

Escreva o cabeçalho include/lista.h de um módulo de lista encadeada usando um tipo opaco (declarando typedef struct Lista Lista; sem expor os campos) e com prefixo de módulo (lista_) em todas as funções. Inclua a guarda de inclusão.

Ver resposta

✓ Resposta:

// include/lista.h
#ifndef LISTA_H
#define LISTA_H

#include <stdbool.h>

// tipo opaco: os campos internos não são visíveis ao usuário
typedef struct Lista Lista;

// interface pública, com prefixo de módulo 'lista_'
Lista *lista_criar(void);
void   lista_destruir(Lista *l);
bool   lista_inserir_inicio(Lista *l, int valor);
bool   lista_remover(Lista *l, int valor);
bool   lista_contem(const Lista *l, int valor);
int    lista_tamanho(const Lista *l);

#endif // LISTA_H

A struct Lista é apenas declarada (não definida) no cabeçalho; sua definição real, com os campos, ficaria em src/lista.c.

Exercício 3

Explique o que é um "tipo opaco" e qual vantagem de encapsulamento ele oferece. Por que declarar a struct completa no .h (em vez de escondê-la no .c) enfraqueceria esse encapsulamento?

Ver resposta

✓ Resposta: Um tipo opaco é um tipo cuja estrutura interna (os campos da struct) é escondida de quem usa o módulo: o cabeçalho apenas declara que o tipo existe (typedef struct Lista Lista;), sem revelar seus campos, que ficam definidos somente no .c. A vantagem de encapsulamento é que o usuário do módulo só pode manipular o tipo através das funções da interface (lista_inserir_inicio, lista_tamanho, etc.), nunca acessando ou modificando os campos diretamente. Isso traz duas grandes vantagens: (1) o usuário não pode corromper o estado interno mexendo em campos indevidamente — a integridade da estrutura é protegida; (2) você pode mudar completamente a implementação interna (trocar uma lista encadeada por um vetor dinâmico, adicionar campos, reorganizar) sem quebrar nenhum código que usa o módulo, pois esse código depende apenas da interface, não dos detalhes. Declarar a struct completa no .h enfraqueceria o encapsulamento porque, uma vez que os campos ficam visíveis, o código usuário pode (e frequentemente vai) acessá-los diretamente (minha_lista.cabeca), criando uma dependência dos detalhes internos. A partir daí, qualquer mudança nesses campos quebraria o código usuário, e não haveria mais garantia de que o estado interno só é modificado de formas válidas. O tipo opaco força o acesso pela interface, que é justamente o que dá liberdade ao implementador e segurança ao estado.

Exercício 4

Escreva um README.md mínimo para um projeto fictício (por exemplo, uma ferramenta de linha de comando que conta palavras em arquivos), cobrindo: o que faz, como compilar e como usar.

Ver resposta

✓ Resposta:

# contapalavras

Uma ferramenta de linha de comando que conta o número de palavras em
arquivos de texto.

## Compilação

    make

Isso gera o executável `contapalavras` no diretório do projeto.

## Uso

    ./contapalavras arquivo.txt

Imprime o número de palavras do arquivo informado. Também aceita
múltiplos arquivos:

    ./contapalavras a.txt b.txt c.txt

## Requisitos

- GCC ou Clang
- Make

O README cobre o essencial: o que a ferramenta faz, como construí-la e como usá-la, com exemplos concretos — tudo que alguém precisa para começar.

Exercício 5

Explique por que o prefixo de módulo (como pilha_, lista_) é uma convenção importante em C, considerando que a linguagem não tem namespaces. Que problema concreto ele previne quando um projeto usa vários módulos?

Ver resposta

✓ Resposta: O prefixo de módulo (como pilha_, lista_) é importante em C porque a linguagem não tem namespaces — não existe um mecanismo para agrupar nomes sob um "espaço" separado (como pilha::push em C++ ou pilha.push em outras linguagens). Todos os nomes de funções em um programa C compartilham um único espaço global: se dois arquivos definem uma função com o mesmo nome, há colisão. O problema concreto que o prefixo previne é justamente a colisão de nomes entre módulos. Imagine um projeto com um módulo de pilha e um módulo de fila, ambos com uma função natural chamada push e outra chamada vazia. Sem prefixo, ao ligar os dois módulos, o ligador encontraria duas definições de push e de vazia e emitiria um erro de "definição múltipla" (o mesmo problema que vimos na aula Compilação Separada: Cabeçalhos e Múltiplos Arquivos) — ou, pior, se as assinaturas coincidissem, uma poderia silenciosamente ser usada no lugar da outra. Com o prefixo, as funções viram pilha_push/pilha_vazia e fila_push/fila_vazia, nomes únicos que nunca colidem, e que ainda deixam claro a que módulo cada função pertence (melhorando a legibilidade). O prefixo é, portanto, a forma que a comunidade C encontrou de simular namespaces manualmente: uma convenção disciplinada de nomenclatura que substitui o recurso que a linguagem não oferece.

Comentários

Mais em Linguagem C

Funções, Escopo e Passagem por Valor
Funções, Escopo e Passagem por Valor

Por que dobrar um número dentro de uma função deixa o original intacto? A…

Quando o Programa Decide: if, else e switch
Quando o Programa Decide: if, else e switch

Trocar == por = dentro de um if compila, roda e faz a coisa errada em…

Ponteiros para Struct e o Operador Seta
Ponteiros para Struct e o Operador Seta

Os parênteses em (*ptr).x não são preciosismo: sem eles o ponto vence o…