Código Limpo em C: Nomes, Funções e Legibilidade

[420] Código Limpo em C: Nomes, Funções e Legibilidade

f(a, b) e somar_intervalo(inicio, fim) fazem exatamente a mesma coisa; só a segunda dispensa quem lê de decifrar. Daqui saem a função que faz uma coisa só, o comentário que explica o porquê em vez do o quê, a consistência que faz o projeto parecer escrito por uma pessoa só, e a clareza vencendo a esperteza.
Linguagem C

13 min de leitura

Abrimos a Fase 7 com uma mudança de perspectiva. Até aqui, nossa preocupação foi fazer o código funcionar — e funcionar corretamente, com a ajuda das ferramentas. Agora voltamos a atenção para algo igualmente importante e muitas vezes negligenciado: fazer o código ser lido. Um programa é escrito uma vez, mas lido dezenas de vezes — por colegas, por mantenedores futuros, e principalmente por você mesmo, meses depois, quando já esqueceu por que fez o que fez. Código limpo não é um luxo estético; é o que torna um programa possível de entender, corrigir e evoluir. Hoje aprendemos os princípios que transformam código que funciona em código que se pode viver.

A verdade fundamental: código é lido mais do que escrito

Comece por internalizar esta ideia, porque ela justifica tudo o mais. Você passa muito mais tempo lendo código do que escrevendo — lendo para entender antes de modificar, para encontrar um bug, para reaproveitar uma função. Cada minuto investido em tornar o código claro na escrita economiza horas de decifração depois. O programador experiente escreve pensando no leitor futuro, que quase sempre não terá o contexto que você tem agora. Código limpo é um ato de generosidade com esse leitor — e, na maioria das vezes, esse leitor é você mesmo.

Nomes que revelam intenção

O nome de uma variável ou função é a primeira e mais poderosa forma de comunicar o que ela faz. Um bom nome torna o código autoexplicativo; um nome ruim exige que o leitor decifre. Compare:

// Ruim: nomes que não dizem nada
int f(int a, int b) {
    int r = 0;
    for (int i = a; i <= b; i++) {
        r += i;
    }
    return r;
}

// Bom: nomes que revelam a intenção
int somar_intervalo(int inicio, int fim) {
    int soma = 0;
    for (int atual = inicio; atual <= fim; atual++) {
        soma += atual;
    }
    return soma;
}

As duas funções fazem exatamente a mesma coisa, mas a segunda se explica sozinha. somar_intervalo(inicio, fim) diz o que faz e o que recebe; f(a, b) não diz nada. Os princípios de bons nomes: sejam descritivos (o nome revela o propósito), precisos (dizem exatamente o que a coisa é), e proporcionais ao escopo (um contador de laço curto pode ser i, mas uma variável importante merece um nome completo). Evite abreviações obscuras e nomes genéricos como dados, temp, valor quando um nome mais específico é possível. Um bom nome economiza um comentário.

Funções pequenas que fazem uma coisa

O segundo pilar do código limpo é a estrutura das funções. Uma boa função faz uma coisa, e a faz bem. Funções longas, que misturam várias responsabilidades, são difíceis de entender, testar e reutilizar. Veja o contraste:

// Ruim: uma função que faz tudo
void processar_alunos(void) {
    // lê dados
    // valida dados
    // calcula médias
    // ordena por média
    // imprime relatório
    // ... 80 linhas misturando tudo ...
}

// Bom: cada função com uma responsabilidade
double calcular_media(const double *notas, int n) { /* ... */ }
int    aprovado(double media)                     { /* ... */ }
void   imprimir_boletim(const char *nome, double media) { /* ... */ }

Quando cada função tem uma única responsabilidade, o código fica mais fácil de entender (você lê o nome e sabe o que ela faz), de testar (você testa cada peça isoladamente, como na aula Testes Automatizados em C), e de reaproveitar (uma função focada serve em vários contextos). A regra prática: se você precisa de um comentário para explicar o que uma seção de uma função faz, essa seção provavelmente deveria ser uma função separada, com um nome que substitua o comentário. Funções curtas, com um propósito claro, são a espinha dorsal do código limpo.

Comentários: explique o "porquê", não o "o quê"

Comentários têm seu lugar, mas são frequentemente mal usados. O erro comum é comentar o óbvio — o que o código já diz:

// Ruim: comentário redundante
i++; // incrementa i

// Ruim: comentário que repete o código
if (idade >= 18) { // se a idade for maior ou igual a 18
    // ...
}

Esses comentários não agregam nada — o código já os diz. Bons comentários explicam o que o código não consegue dizer: o porquê de uma decisão, o contexto, uma sutileza não óbvia:

// Bom: explica o PORQUÊ, o contexto que o código não revela
// Usamos <= aqui porque o intervalo é inclusivo em ambas as pontas,
// conforme a especificação do cliente.
for (int i = inicio; i <= fim; i++) { /* ... */ }

// Bom: alerta sobre uma sutileza
// strtok modifica a string original; trabalhamos numa cópia de propósito.
char copia[100];
strcpy(copia, original);

A diretriz: se você sente necessidade de comentar o que o código faz, talvez o código não esteja claro o suficiente — melhore os nomes primeiro. Reserve os comentários para o porquê, para decisões que surpreenderiam o leitor, para avisos sobre armadilhas. E lembre-se: comentários desatualizados são piores que nenhum comentário, pois enganam — então mantenha-os sincronizados com o código, ou não os escreva.

Consistência e formatação

Um código limpo é consistente. Escolha um estilo — como você nomeia (snake_case, como fizemos no curso todo), como indenta, onde põe as chaves — e siga-o rigorosamente em todo o projeto. A consistência reduz a carga mental do leitor: quando tudo segue o mesmo padrão, o olho encontra o que procura sem esforço. Uma inconsistência (uma função em snake_case, outra em camelCase) faz o leitor parar e se perguntar se a diferença significa algo. Muitos projetos adotam um guia de estilo (o do kernel Linux, o do Google, ou um próprio), e usam ferramentas de formatação automática como o clang-format para garantir uniformidade sem esforço manual. A regra de ouro: dentro de um projeto, o estilo deve parecer escrito por uma única pessoa, mesmo que muitas tenham contribuído.

Evitando a complexidade desnecessária

Código limpo também é código simples. Há uma tentação, especialmente ao aprender, de mostrar esperteza com truques compactos e engenhosos. Resista a ela. Compare:

// "Esperto" mas obscuro
int x = a ^ b ^ ((a ^ b) & -(a < b));

// Claro e direto
int x = (a < b) ? a : b; // o menor entre a e b

O código "esperto" pode até funcionar e ser marginalmente mais rápido, mas ninguém o entende sem esforço — e o que ninguém entende, ninguém consegue manter ou corrigir com segurança. A clareza quase sempre vence a esperteza. Prefira a solução direta e legível; recorra a truques obscuros apenas quando houver uma necessidade real e comprovada de desempenho (e, mesmo assim, comente por quê). Lembre-se do que vimos sobre otimização prematura: código legível primeiro; otimização, só quando medida e necessária — tema, aliás, de uma aula próxima.

A conexão com tudo que aprendemos

Vale notar que código limpo não é um tema isolado — ele amplifica tudo que construímos. Nomes claros tornam a depuração no GDB (aula Depurando de Verdade com o GDB) mais fácil, pois você reconhece as variáveis. Funções pequenas e focadas são muito mais simples de testar (aula Testes Automatizados em C), pois cada uma tem uma responsabilidade verificável. A organização modular (aula Compilação Separada: Cabeçalhos e Múltiplos Arquivos) é código limpo em escala de arquivos. E a disciplina defensiva que praticamos — verificar retornos, checar limites — é parte de escrever código confiável e legível. Código limpo é o fio que costura a qualidade em todos os níveis, do nome de uma variável à arquitetura de um projeto.

O que vem a seguir

Hoje aprendemos os princípios do código limpo em C: nomes que revelam intenção, funções pequenas com uma única responsabilidade, comentários que explicam o porquê e não o quê, consistência de estilo, e a preferência pela clareza sobre a esperteza. Escrever para o leitor futuro — muitas vezes você mesmo — é uma marca do programador maduro. Na próxima aula, aprofundamos um aspecto específico da qualidade que já vem nos acompanhando: a programação defensiva e os contratos — como escrever funções que declaram claramente o que esperam e garantem, validando pré-condições e falhando de forma previsível, para que os erros sejam pegos cedo e localizados, em vez de se propagarem silenciosamente.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Pegue esta função e reescreva-a com nomes que revelem a intenção: int c(int *a, int n, int x) { int k = 0; for (int i = 0; i < n; i++) if (a[i] == x) k++; return k; }. Explique cada nome que você escolheu.

Ver resposta

✓ Resposta:

int contar_ocorrencias(const int *vetor, int tamanho, int alvo) {
    int ocorrencias = 0;
    for (int i = 0; i < tamanho; i++) {
        if (vetor[i] == alvo) {
            ocorrencias++;
        }
    }
    return ocorrencias;
}

Escolhas de nome: contar_ocorrencias (a função conta quantas vezes algo aparece — diz o propósito); vetor e tamanho (o vetor e seu tamanho, o par idiomático); alvo (o valor procurado); ocorrencias (o contador, nomeado pelo que acumula). Acrescentei const ao vetor, pois a função só lê. O nome do laço i permanece curto por ser um índice de escopo mínimo e uso convencional. Cada nome torna a função legível sem comentários.

Exercício 2

A função abaixo faz três coisas. Divida-a em funções menores, cada uma com uma responsabilidade: uma função que lê valores, uma que calcula a soma e uma que imprime o resultado. (Descreva as assinaturas; o corpo pode ser simplificado.)

Ver resposta

✓ Resposta:

// cada função com uma única responsabilidade
int ler_valores(int *destino, int max);          // lê valores; retorna quantos leu
int somar(const int *valores, int n);            // calcula a soma
void imprimir_resultado(int soma);               // exibe o resultado

int main(void) {
    int valores[100];
    int n = ler_valores(valores, 100);
    int total = somar(valores, n);
    imprimir_resultado(total);
    return 0;
}

Cada função faz uma coisa: ler_valores cuida da entrada, somar do cálculo, imprimir_resultado da saída. O main orquestra as três, e cada uma pode ser testada isoladamente (aula Testes Automatizados em C). A separação torna o fluxo óbvio à leitura.

Exercício 3

Encontre e "conserte" os comentários ruins neste trecho, substituindo-os por comentários úteis (ou removendo-os): int total = 0; // declara total / total += preco * 1.1; // multiplica preco por 1.1 e soma. Que informação um bom comentário poderia acrescentar à segunda linha?

Ver resposta

✓ Resposta:

// Comentários ruins removidos; um comentário útil acrescentado onde há sutileza
int total = 0;
total += preco * 1.1; // acrescenta 10% de imposto ao preço, conforme regra fiscal

O // declara total era redundante — o código já declara total, o comentário não agrega nada, então foi removido. O // multiplica preco por 1.1 e soma apenas repetia a operação visível no código. Um bom comentário para a segunda linha explica o porquê do 1.1: que ele representa a adição de 10% de imposto (o número mágico 1.1 não é autoexplicativo). Ainda melhor seria extrair uma constante nomeada: const double COM_IMPOSTO = 1.1; ou #define TAXA_IMPOSTO 0.10, tornando a intenção clara sem depender do comentário.

Exercício 4

Explique a diferença entre um comentário que descreve "o quê" e um que descreve "o porquê". Por que o segundo tipo é geralmente mais valioso? Dê um exemplo de cada.

Ver resposta

✓ Resposta: Um comentário que descreve "o quê" apenas reafirma o que o código já faz, em linguagem natural — por exemplo, i++; // incrementa i ou soma += x; // adiciona x à soma. Ele é redundante porque o leitor que conhece C já vê isso no próprio código. Um comentário que descreve "o porquê" explica a razão, o contexto ou a decisão por trás do código — algo que o código sozinho não consegue revelar — por exemplo, // usamos <= porque o intervalo é inclusivo, conforme a especificação. O segundo tipo é mais valioso porque preenche a lacuna entre o como (que o código mostra) e o por que (que só existe na cabeça de quem escreveu). O leitor consegue deduzir o que o código faz lendo-o; o que ele não consegue deduzir é a intenção, a restrição externa, ou a armadilha que motivou aquela escolha. Comentar o "porquê" transmite justamente o conhecimento que se perderia. Exemplo de "o quê" (ruim): x = x * 2; // dobra x. Exemplo de "porquê" (bom): x = x * 2; // compensamos a resolução pela metade do sensor de baixa densidade.

Exercício 5

Explique por que a clareza geralmente deve ser preferida à esperteza, mesmo quando o código "esperto" é ligeiramente mais eficiente. Em que circunstância específica valeria a pena usar uma solução mais obscura, e o que você deveria fazer nesse caso para mitigar o problema de legibilidade?

Ver resposta

✓ Resposta: A clareza deve geralmente ser preferida à esperteza porque código é lido muito mais do que escrito, e código obscuro impõe um custo contínuo: cada pessoa que precisa entendê-lo (para corrigir, modificar ou reutilizar) gasta tempo e corre risco de interpretá-lo errado. Um ganho marginal de eficiência raramente compensa esse custo permanente de manutenção — e, pior, código que ninguém entende é código que ninguém consegue modificar com segurança, tornando-se uma fonte de bugs. na maioria do código, a diferença de desempenho é irrelevante (a otimização prematura é um erro clássico). A circunstância específica em que valeria a pena usar uma solução mais obscura é quando há uma necessidade real e comprovada de desempenho — um trecho identificado, por medição (com ferramentas de profiling), como um gargalo crítico, onde a otimização traz ganho significativo e necessário. Mesmo nesse caso, para mitigar o problema de legibilidade, você deveria: comentar claramente o porquê da abordagem obscura (explicando a otimização e por que ela é necessária), documentar o que o código faz em termos simples, e idealmente isolar o trecho otimizado numa função bem nomeada, de modo que quem lê o código ao redor veja uma chamada clara (menor_entre(a, b)) sem precisar decifrar o truque interno. Assim, a esperteza fica contida e justificada, não espalhada e misteriosa.

Comentários

Mais em Linguagem C

Comunicação em Rede com Sockets
Comunicação em Rede com Sockets

Depois de conectado, um socket se lê e se escreve como arquivo — e é essa…

Arquivos Binários e Posicionamento no Disco
Arquivos Binários e Posicionamento no Disco

O inteiro 12345 ocupa cinco bytes gravado como texto e quatro gravado como…

Ponteiros para Função: Passando Comportamento como Argumento
Ponteiros para Função: Passando Comportamento como Argumento

O nome de uma função, escrito sem os parênteses, é um endereço — e dessa…