Padrões e organização em C
LidoEsta aula explora padrões e organização em C, focando em encapsulamento com ponteiros opacos, estruturação de módulos, convenções de código e gerenciamento claro de ownership. O conteúdo inclui exemplos práticos, boas práticas e exercícios para consolidar o aprendizado.
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
staticpara 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_touPoint). - 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_createepoint_destroyvistos 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
constpara 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
constsempre 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
- Crie um módulo
countercom um tipo opacoCounterque encapsula um contador inteiro. Implemente funções para criar, destruir, incrementar, obter o valor e resetar. Escreva um programa de teste. - 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.
- 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 questrcpyseja usada corretamente). - 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? - Refatore o código abaixo para usar um módulo com encapsulamento e convenções claras. Considere que
struct Pessoadeve ser opaco.
// 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; }
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.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.