C Boas Práticas
O C deixa você fazer quase tudo, inclusive muita coisa errada sem reclamar. Um programa pode compilar, rodar e mostrar um número absurdo, funcionar hoje e quebrar amanhã, ou estragar a memória em silêncio. As boas práticas são hábitos que evitam esses problemas antes que eles apareçam. Esta página junta num checklist comentado as práticas que o guia foi ensinando e termina com um programa "antes × depois": um código cheio de problemas e a versão consertada, mudança por mudança.
- Os itens do checklist de um bom código C, cada um com o porquê e a aula que o detalha.
- Como usar os avisos do compilador (
-Wall -Wextra) e o-Werrora seu favor. - Por que inicializar variáveis, conferir retornos e respeitar os limites dos arrays não é frescura.
- Como transformar um programa ruim num bom, passo a passo (refatorar).
Todos os programas foram compilados com o gcc 15 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c/boas. Cada item do checklist tem uma aula própria; aqui você os vê juntos e aprende a usá-los como rotina.
A ideia: o checklist do piloto
Antes de decolar, todo piloto, por mais experiente que seja, segue um checklist: combustível, portas, flaps. Não é porque esqueceu como se pilota — é porque, numa rotina repetida mil vezes, um item esquecido custa caro, e o checklist torna o esquecimento quase impossível.
Programar é parecido. Você sabe o que é uma variável sem valor inicial, mas num programa de 300 linhas, às 23h, ela passa. O checklist abaixo é a sua lista antes de "decolar" (entregar o programa):
| # | Prática | Evita |
|---|---|---|
| 1 | Compile com -Wall -Wextra (e -Werror em projetos) | Erros que o compilador já tinha visto |
| 2 | Inicialize toda variável | Números "lixo" |
| 3 | Confira o retorno de scanf, malloc e fopen | Usar um dado que nunca chegou |
| 4 | Nunca copie texto sem o tamanho | Estouro de buffer |
| 5 | Todo malloc tem o seu free | Vazamentos de memória |
| 6 | Respeite os limites dos arrays (i < N) | Ler e escrever fora do array |
| 7 | Números com nome (#define, const) | Números soltos que ninguém entende |
| 8 | int main(void), return 0 e protótipos | Surpresas na compilação |
| 9 | Funções pequenas, nomes claros | Código que ninguém entende |
| 10 | Sanitizers e FORTIFY nos testes | Erros de memória que só aparecem às vezes |
1. Compile com -Wall -Wextra
O compilador é o seu primeiro revisor, e de graça. As opções -Wall ("all warnings", todos os avisos) e -Wextra ("extra") ligam centenas de conferências que pegam os erros mais comuns antes de o programa rodar. Por isso o comando padrão do guia é sempre:
$ gcc -std=c23 -Wall -Wextra programa.c -o programa
Em um projeto que já está limpo, vá além com o -Werror ("warnings as errors", avisos como erros): ele transforma todo aviso em erro, e o programa só compila quando não sobra nenhum. Assim nenhum aviso é ignorado "para depois":
$ gcc -std=c23 -Wall -Wextra -Werror lixo.c -o lixo
lixo.c: In function ‘main’:
lixo.c:5:5: error: ‘contador’ is used uninitialized [-Werror=uninitialized]
5 | printf("Contador: %d\n", contador);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
cc1: all warnings being treated as errors
$ echo $?
1
O mesmo aviso que o compilador daria virou um error, e a compilação parou. Veja como ler essas mensagens em Lendo Erros do Compilador e o que cada aviso quer dizer em Avisos (-Wall -Wextra).
2. Inicialize toda variável
Uma variável local que você declara e não dá valor não vale zero: ela vale o que estava naquele pedaço de memória, um número qualquer (o "lixo"). Usar esse valor é comportamento indefinido. Com -Wall -Wextra, o gcc avisa:
$ gcc -std=c23 -Wall -Wextra lixo.c -o lixo
lixo.c: In function ‘main’:
lixo.c:5:5: warning: ‘contador’ is used uninitialized [-Wuninitialized]
5 | printf("Contador: %d\n", contador);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lixo.c:4:9: note: ‘contador’ was declared here
4 | int contador;
| ^~~~~~~~
‘contador’ is used uninitialized = "contador é usado sem valor inicial". O conserto é sempre o mesmo: dê um valor na hora de declarar — int contador = 0;, double soma = 0.0;, char nome[30] = "";. No editor do guia, o Clang diz a mesma coisa com outras palavras: warning: variable 'contador' is uninitialized when used here.
Missão: este programa soma três notas, mas soma começa sem valor inicial — o resultado pode ser qualquer coisa. Inicialize-a com double soma = 0.0;. A média certa de 7, 8 e 9 é 8.0.
#include <stdio.h>
#define QTD 3
int main(void) {
double soma;
double notas[QTD] = {7, 8, 9};
for (int i = 0; i < QTD; i++) {
soma += notas[i];
}
printf("Média: %.1f\n", soma / QTD);
return 0;
}
#include <stdio.h>
#define QTD 3
int main(void) {
double soma = 0.0;
double notas[QTD] = {7, 8, 9};
for (int i = 0; i < QTD; i++) {
soma += notas[i];
}
printf("Média: %.1f\n", soma / QTD);
return 0;
}
3. Confira o retorno de scanf, malloc e fopen
Muitas funções do C devolvem um valor que diz se deram certo, e é tentador ignorá-lo. Mas se você usa o resultado assim mesmo, pode estar usando um dado que nunca chegou:
| Função | Devolve | Confira |
|---|---|---|
scanf | quantos valores leu | if (scanf("%d", &x) != 1) { ... } |
malloc | o endereço, ou NULL se faltou memória | if (p == NULL) { ... } |
fopen | o arquivo, ou NULL se não abriu | if (arquivo == NULL) { perror(...); } |
O caso do scanf é o mais comum. Se a pessoa digita abc onde o programa espera um número, o scanf não lê nada e devolve 0; sem conferir, a variável fica com o valor que já tinha (quase sempre lixo), e o programa segue com um número inventado. Veja o "antes × depois" na missão do fim da página. A aula Validar a Entrada mostra os detalhes.
4. Nunca copie texto sem o tamanho
Copiar texto para um array sem dizer quanto cabe é a porta do estouro de buffer, o erro mais perigoso do C. Fuja de gets, strcpy, strcat, sprintf e scanf("%s") sem limite; prefira fgets, snprintf e %29s, que recebem o tamanho do destino. A aula Segurança (Buffer Overflow) é inteira sobre isso, com os estouros acontecendo de verdade.
5. Todo malloc tem o seu free
Toda memória que você reserva com malloc precisa ser devolvida com free quando não for mais usada. Esquecer é um vazamento de memória: o programa vai comendo memória até acabar. A regra prática é ler cada malloc e já pensar em onde vai o free. E, depois de liberar, faça p = NULL;: assim um free repetido por engano não vira um erro (veja Ponteiro Nulo).
free
Um programa com vazamento compila sem um aviso sequer. Quem pega vazamentos é o AddressSanitizer (ou o Valgrind), rodando os seus testes. Por isso o item 10.
6. Respeite os limites dos arrays
Um array de N posições vai de 0 a N - 1. O laço certo é for (int i = 0; i < N; i++) — com <, nunca <=. Trocar por <= faz o laço tocar em v[N], que não existe, e estragar a memória vizinha (o mesmo estouro da aula de Segurança; veja também o cenário do Boletim de Notas). Esse erro passa calado na compilação — por isso vale decorar: o índice vai até N - 1, o laço usa <.
7. Números com nome
Um 30 solto no meio do código não diz nada: é o tamanho da turma? a idade máxima? Dê um nome a ele com #define ou const. O código fica legível e, quando o número mudar, você muda num lugar só:
#define MAX_ALUNOS 30
#define MEDIA_APROVACAO 6.0
struct Aluno turma[MAX_ALUNOS];
if (nota >= MEDIA_APROVACAO) { /* aprovado */ }
Qualquer pessoa entende MAX_ALUNOS e MEDIA_APROVACAO; ninguém precisa adivinhar o que era o 30 ou o 6.0. No C23, para valores que são constantes de verdade, você pode usar também constexpr int MAX_ALUNOS = 30; C23.
8. int main(void), return 0 e protótipos
- Escreva
int main(void)— ovoiddiz "este programa não recebe argumentos aqui". Deixe oreturn 0;no fim para dizer ao sistema que deu tudo certo (o C99+ aceita omiti-lo só nomain, mas escrevê-lo é mais claro). - Declare cada função antes de usá-la, com um protótipo, ou ponha a função inteira acima de quem a chama. No C23, chamar uma função que o compilador ainda não viu é um erro, não um aviso.
9. Funções pequenas, nomes claros
Uma função deve fazer uma coisa e ter um nome que diga qual. double media(const double notas[], int n) se entende sozinha; um main de 200 linhas, não. Nomes claros valem para tudo: soma, maior, qtd_alunos — não x, a2, tmp3. O bom nome é um comentário que nunca fica desatualizado.
10. Sanitizers e FORTIFY nos testes
Os piores erros de C — estouros, vazamentos, uso de memória liberada — não aparecem na compilação e às vezes nem quando você roda o programa à mão. Para caçá-los, compile a versão de teste com os fiscais e rode os seus testes:
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address programa.c -o programa $ ./programa « rode os testes aqui
O AddressSanitizer aponta a linha de cada erro de memória; na versão final, -O2 -D_FORTIFY_SOURCE=2 protege de graça contra estouros (veja Segurança). Nenhum dos dois roda no editor do guia, só no seu Linux.
O programa antes × depois
Juntando tudo. Este programa calcula a média de duas notas que a pessoa digita. Ele até "funciona" num teste rápido, mas tem três dos problemas do checklist:
#include <stdio.h>
int main(void) {
double n1, n2;
double soma;
printf("Nota 1: ");
scanf("%lf", &n1);
printf("Nota 2: ");
scanf("%lf", &n2);
soma = n1 + n2;
printf("Média: %.1f\n", soma / 2);
return 0;
}
- Item 3: os
scanfnão são conferidos. Se a pessoa digitarabc,n1fica com lixo e a média sai sem sentido. - Item 7: o
2solto é um número mágico (a quantidade de notas). - Item 9: o cálculo e a leitura estão todos no
main.
A versão depois conserta os três:
#include <stdio.h>
#define QTD_NOTAS 2
int ler_nota(const char *rotulo, double *destino) {
printf("%s", rotulo);
return scanf("%lf", destino) == 1;
}
int main(void) {
double n1 = 0.0;
double n2 = 0.0;
if (!ler_nota("Nota 1: ", &n1) || !ler_nota("Nota 2: ", &n2)) {
printf("Nota inválida.\n");
return 1;
}
double media = (n1 + n2) / QTD_NOTAS;
printf("Média: %.1f\n", media);
return 0;
}
- int ler_nota(const char *rotulo, double *destino)
- Uma função pequena, com nome claro, que mostra o rótulo, lê a nota e devolve
1se conseguiu (item 9). Oconstno rótulo diz que a função não muda o texto (item do const). - double n1 = 0.0;
- Toda variável começa com valor (item 2).
- if (!ler_nota(...) || !ler_nota(...))
- Confere as duas leituras; se qualquer uma falhar, avisa e sai com código 1 (item 3).
- (n1 + n2) / QTD_NOTAS
- O
2virouQTD_NOTAS, com nome (item 7).
$ gcc -std=c23 -Wall -Wextra depois.c -o depois $ ./depois Nota 1: 7 Nota 2: 8 Média: 7.5 $ ./depois Nota 1: abc Nota inválida. $ echo $? 1
Agora o texto inválido é recusado em vez de virar uma média maluca, e o código se lê como uma frase.
Missão final: complete a versão "depois". O programa já inicializa as variáveis, mas ainda não confere o scanf. Troque cada scanf("%lf", &nX); solto por uma leitura conferida — por exemplo, if (scanf("%lf", &n1) != 1) { printf("Nota inválida.\n"); return 1; }. Com as notas 7 e 8, a média é 7.5.
#include <stdio.h>
#define QTD_NOTAS 2
int main(void) {
double n1 = 0.0;
double n2 = 0.0;
printf("Nota 1: ");
scanf("%lf", &n1);
printf("Nota 2: ");
scanf("%lf", &n2);
double media = (n1 + n2) / QTD_NOTAS;
printf("Média: %.1f\n", media);
return 0;
}
#include <stdio.h>
#define QTD_NOTAS 2
int main(void) {
double n1 = 0.0;
double n2 = 0.0;
printf("Nota 1: ");
if (scanf("%lf", &n1) != 1) {
printf("Nota inválida.\n");
return 1;
}
printf("Nota 2: ");
if (scanf("%lf", &n2) != 1) {
printf("Nota inválida.\n");
return 1;
}
double media = (n1 + n2) / QTD_NOTAS;
printf("Média: %.1f\n", media);
return 0;
}
Quando dá errado
O que aconteceu: quase sempre é uma variável sem valor inicial (item 2) ou um comportamento indefinido. O lixo que estava na memória era 0 na sua máquina e outro número na dele.
Como resolver: compile com -Wall -Wextra e rode com -fsanitize=address,undefined dos dois lados.
O que aconteceu: um scanf falhou (texto no lugar de número) e o retorno não foi conferido (item 3); a variável ficou com o valor de antes.
Como resolver: if (scanf(...) != N) { ... }, como na missão final.
O que aconteceu: os avisos foram se acumulando até ninguém mais olhar, e o de verdade importante sumiu no meio.
Como resolver: mantenha a compilação sem nenhum aviso, e ligue -Werror no projeto (item 1).
Exercícios
1. Tratar aviso como erro
Complete a opção que faz o gcc recusar o programa se houver qualquer aviso:
2. Variável com valor inicial
Complete a declaração de um contador que começa em zero:
3. Por que inicializar int soma; com = 0?
4. Qual é a forma segura de ler um número com o scanf?
5. O que o -fsanitize=address acha que o -Wall -Wextra não acha?
Resumo
- Compile sempre com
-Wall -Wextra, e-Werrorno projeto: o compilador é o seu primeiro revisor. - Inicialize toda variável; confira o retorno de
scanf,mallocefopen; nunca copie texto sem o tamanho. - Todo
malloctem o seufree; respeite os limites dos arrays (i < N); dê nome aos números. int main(void)comreturn 0, protótipos antes de usar, funções pequenas com nomes claros.- Rode os sanitizers nos testes e o FORTIFY na versão final: eles pegam o que o compilador não vê.
- Boa prática não é enfeite: cada item evita uma categoria inteira de bugs.
Próxima aula: Sobre os Projetos.