Nesta aula, vamos mergulhar em padrões e organização de código em C, um tópico essencial para projetos de médio e grande porte. Embora C seja uma linguagem flexível, sem uma estruturação cuidadosa, o código pode se tornar rapidamente difícil de manter, entender e estender. Vamos explorar técnicas como encapsulamento com ponteiros opacos, modularização, convenções de nomenclatura e gerenciamento claro de ownership (posse de recursos).

Esses conceitos não apenas melhoram a qualidade do código, mas também facilitam o trabalho em equipe, a reutilização de código e a depuração. Ao final, você terá um repertório de padrões que pode aplicar imediatamente em seus próprios projetos.

Encapsulamento (opaque pointers)

Encapsulamento é o princípio de esconder detalhes internos de uma estrutura de dados, expondo apenas uma interface pública. Em C, uma técnica comum é usar ponteiros opacos: declaramos a estrutura completa em um arquivo de implementação (.c) e, no cabeçalho (.h), apenas declaramos um tipo opaco (por exemplo, struct MyStruct;). O usuário da API só manipula ponteiros para essa estrutura, sem conhecer seus campos.

Isso traz várias vantagens: o código do usuário não depende da representação interna, permitindo que você mude a implementação sem quebrar o código cliente; protege os dados de modificações acidentais; e reduz o acoplamento. É um padrão usado em muitas bibliotecas C, como FILE do stdio.h.

Vejamos um exemplo:

// point.h
#ifndef POINT_H
#define POINT_H

typedef struct Point Point;

Point* point_create(double x, double y);
void point_destroy(Point* p);
double point_get_x(const Point* p);
double point_get_y(const Point* p);
void point_set(Point* p, double x, double y);

#endif
// point.c
#include "point.h"
#include <stdlib.h>

struct Point {
    double x;
    double y;
};

Point* point_create(double x, double y) {
    Point* p = malloc(sizeof(Point));
    if (p != NULL) {
        p->x = x;
        p->y = y;
    }
    return p;
}

void point_destroy(Point* p) {
    free(p);
}

double point_get_x(const Point* p) {
    return p->x;
}

double point_get_y(const Point* p) {
    return p->y;
}

void point_set(Point* p, double x, double y) {
    p->x = x;
    p->y = y;
}

Note que o usuário nunca vê os campos x e y; ele só usa as funções. Isso permite que você, por exemplo, adicione um campo de cor sem afetar o código cliente.

Módulos

Modularização é a prática de dividir o código em unidades independentes (módulos), cada uma com uma responsabilidade bem definida. Em C, um módulo geralmente consiste em um par de arquivos: um cabeçalho (.h) que declara a interface pública e um arquivo de implementação (.c) que contém o código. Essa separação permite compilar módulos separadamente e reutilizá-los em vários programas.

Boas práticas para módulos:

  • Use include guards (ou #pragma once) para evitar inclusões múltiplas.
  • Exponha apenas o necessário: funções e tipos que fazem parte da API.
  • Mantenha funções internas como static para que não sejam visíveis fora do arquivo.
  • Escolha nomes de prefixo consistentes para evitar conflitos (ex.: point_).

Exemplo de um módulo de fila:

// queue.h
#ifndef QUEUE_H
#define QUEUE_H

#include <stdbool.h>
#include <stddef.h>

typedef struct Queue Queue;

Queue* queue_create(void);
void queue_destroy(Queue* q);
bool queue_enqueue(Queue* q, int value);
bool queue_dequeue(Queue* q, int* out_value);
bool queue_is_empty(const Queue* q);
size_t queue_size(const Queue* q);

#endif
// queue.c
#include "queue.h"
#include <stdlib.h>

typedef struct Node {
    int value;
    struct Node* next;
} Node;

struct Queue {
    Node* head;
    Node* tail;
    size_t size;
};

Queue* queue_create(void) {
    Queue* q = malloc(sizeof(Queue));
    if (q != NULL) {
        q->head = NULL;
        q->tail = NULL;
        q->size = 0;
    }
    return q;
}

void queue_destroy(Queue* q) {
    Node* cur = q->head;
    while (cur != NULL) {
        Node* next = cur->next;
        free(cur);
        cur = next;
    }
    free(q);
}

bool queue_enqueue(Queue* q, int value) {
    Node* n = malloc(sizeof(Node));
    if (n == NULL) return false;
    n->value = value;
    n->next = NULL;
    if (q->tail != NULL) {
        q->tail->next = n;
    } else {
        q->head = n;
    }
    q->tail = n;
    q->size++;
    return true;
}

bool queue_dequeue(Queue* q, int* out_value) {
    if (q->head == NULL) return false;
    Node* n = q->head;
    *out_value = n->value;
    q->head = n->next;
    if (q->head == NULL) q->tail = NULL;
    free(n);
    q->size--;
    return true;
}

bool queue_is_empty(const Queue* q) {
    return q->head == NULL;
}

size_t queue_size(const Queue* q) {
    return q->size;
}

Note que a estrutura Node é definida apenas no .c, não sendo exposta. Isso reforça o encapsulamento.

Convenções

Convenções de código são um conjunto de diretrizes que padronizam a aparência e a estrutura do código. Elas variam entre projetos e empresas, mas algumas são amplamente adotadas na comunidade C.

Principais convenções:

  • Nomenclatura: use nomes claros e descritivos. Funções e variáveis em minúsculas com underscores (ex.: calculate_average), constantes em MAIÚSCULAS (ex.: MAX_SIZE), tipos com prefixo de módulo (ex.: point_t ou Point).
  • Indentação: use 4 espaços ou tabs, consistentemente.
  • Chaves: estilo Allman (chaves na próxima linha) ou K&R (chaves na mesma linha). Escolha um e mantenha.
  • Comentários: explique o porquê, não o quê. Use comentários para documentar APIs em cabeçalhos.
  • Funções: funções devem ser pequenas e fazer uma única coisa. Se uma função tem mais de 30-40 linhas, considere dividir.

Exemplo de um cabeçalho bem documentado:

/**
 * @file math_utils.h
 * Funções utilitárias para operações matemáticas.
 */

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

/**
 * Calcula o fatorial de um número não negativo.
 * @param n Número inteiro não negativo.
 * @return O fatorial de n, ou -1 se n for negativo.
 */
long long factorial(int n);

/**
 * Verifica se um número é primo.
 * @param n Número a verificar.
 * @return true se n for primo, false caso contrário.
 */
bool is_prime(int n);

#endif

Adotar convenções facilita a leitura e a colaboração, pois todos seguem o mesmo estilo.

Ownership claro

Ownership (posse) refere-se a quem é responsável por alocar e liberar recursos (memória, arquivos, etc.). Em C, não há coletor de lixo, então é crucial definir claramente quem é o dono de um recurso e quando ele deve ser liberado. Isso evita vazamentos de memória e acessos inválidos.

Padrões comuns:

  • Funções de criação e destruição: como point_create e point_destroy vistos antes. O criador é o dono e deve destruir.
  • Transferência de ownership: uma função pode retornar um recurso alocado, transferindo a posse ao chamador. Documente isso claramente.
  • Empréstimo (borrowing): uma função recebe um ponteiro e não deve liberá-lo; ela apenas usa. Use const para indicar que não modifica.
  • Referências a recursos internos: se uma função retorna um ponteiro para dados internos de uma estrutura, o chamador não deve liberá-lo. Isso deve ser documentado.

Exemplo de documentação de ownership:

// buffer.h
#ifndef BUFFER_H
#define BUFFER_H

#include <stddef.h>

typedef struct Buffer Buffer;

// Cria um buffer com capacidade inicial.
// O chamador é dono do buffer e deve chamar buffer_destroy.
Buffer* buffer_create(size_t capacity);

// Libera todos os recursos associados ao buffer.
void buffer_destroy(Buffer* b);

// Retorna um ponteiro para os dados internos.
// O ponteiro é válido até a próxima operação que modifique o buffer.
// O chamador NÃO deve liberar esse ponteiro.
char* buffer_data(Buffer* b);

#endif

Além disso, sempre verifique o retorno de malloc e outros alocadores. Se falhar, trate o erro adequadamente.

Boas práticas e observações

Além dos padrões acima, aqui estão algumas boas práticas gerais:

  • Use const sempre que possível para parâmetros que não serão modificados, e para funções que não alteram o estado.
  • Inicialize sempre as variáveis para evitar valores indefinidos.
  • Evite variáveis globais; prefira passar o contexto explicitamente.
  • Teste seus módulos com funções de teste simples (ex.: assert).
  • Documente o código com comentários úteis, especialmente em interfaces públicas.

Essas práticas, combinadas com os padrões anteriores, tornam o código mais robusto e sustentável.

Exercícios

  1. Crie um módulo counter com um tipo opaco Counter que encapsula um contador inteiro. Implemente funções para criar, destruir, incrementar, obter o valor e resetar. Escreva um programa de teste.
  2. Explique por que usar ponteiros opacos melhora o encapsulamento. Dê um exemplo de mudança na implementação que não afete o código do usuário.
  3. Considere o seguinte código que usa malloc. Identifique problemas de ownership e corrija-os: char* get_message(void) { char* s = malloc(20); strcpy(s, "Hello"); return s; } (suponha que strcpy seja usada corretamente).
  4. Escreva uma função int* find_max(int* arr, size_t n) que retorna um ponteiro para o maior elemento do array. Documente o ownership: quem possui o ponteiro retornado?
  5. Refatore o código abaixo para usar um módulo com encapsulamento e convenções claras. Considere que struct Pessoa deve ser opaco.

✓ Resposta:
// counter.h
#ifndef COUNTER_H
#define COUNTER_H

typedef struct Counter Counter;

Counter* counter_create(void);
void counter_destroy(Counter* c);
void counter_increment(Counter* c);
int counter_value(const Counter* c);
void counter_reset(Counter* c);

#endif
// counter.c
#include "counter.h"
#include <stdlib.h>

struct Counter {
    int value;
};

Counter* counter_create(void) {
    Counter* c = malloc(sizeof(Counter));
    if (c != NULL) c->value = 0;
    return c;
}

void counter_destroy(Counter* c) { free(c); }

void counter_increment(Counter* c) { c->value++; }

int counter_value(const Counter* c) { return c->value; }

void counter_reset(Counter* c) { c->value = 0; }

✓ Resposta: Ponteiros opacos escondem a definição da estrutura, permitindo que você altere os campos internos (como adicionar um novo atributo) sem que o código do usuário precise ser recompilado ou modificado, desde que a interface (funções) permaneça a mesma. Por exemplo, se em um Point você quiser mudar de coordenadas cartesianas para polares, pode ajustar a implementação sem que o usuário perceba.

✓ Resposta: O problema é que a função get_message aloca memória e retorna o ponteiro, mas o chamador pode não saber que precisa liberar. Isso pode causar vazamento de memória. A correção é documentar que o chamador é dono do ponteiro e deve chamar free quando não precisar mais. Uma alternativa é usar um buffer fornecido pelo chamador.

✓ Resposta: A função retorna um ponteiro para um elemento dentro do array fornecido pelo chamador. Portanto, o chamador é dono do array, e o ponteiro retornado é um empréstimo: ele não deve ser liberado pelo chamador, e é válido enquanto o array existir. A documentação deve deixar isso claro.

✓ Resposta: Crie um módulo pessoa.h e pessoa.c com funções pessoa_create, pessoa_destroy, pessoa_get_nome, etc., usando um tipo opaco. O código original que usava a struct diretamente deve ser alterado para usar apenas as funções da API.

Referências