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.

Você vai aprender
  • 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 -Werror a 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áticaEvita
1Compile com -Wall -Wextra (e -Werror em projetos)Erros que o compilador já tinha visto
2Inicialize toda variávelNúmeros "lixo"
3Confira o retorno de scanf, malloc e fopenUsar um dado que nunca chegou
4Nunca copie texto sem o tamanhoEstouro de buffer
5Todo malloc tem o seu freeVazamentos de memória
6Respeite os limites dos arrays (i < N)Ler e escrever fora do array
7Números com nome (#define, const)Números soltos que ninguém entende
8int main(void), return 0 e protótiposSurpresas na compilação
9Funções pequenas, nomes clarosCódigo que ninguém entende
10Sanitizers e FORTIFY nos testesErros 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çãoDevolveConfira
scanfquantos valores leuif (scanf("%d", &x) != 1) { ... }
malloco endereço, ou NULL se faltou memóriaif (p == NULL) { ... }
fopeno arquivo, ou NULL se não abriuif (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).

O compilador não cobra o 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) — o void diz "este programa não recebe argumentos aqui". Deixe o return 0; no fim para dizer ao sistema que deu tudo certo (o C99+ aceita omiti-lo só no main, 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 scanf não são conferidos. Se a pessoa digitar abc, n1 fica com lixo e a média sai sem sentido.
  • Item 7: o 2 solto é 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 1 se conseguiu (item 9). O const no 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 2 virou QTD_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

"No meu computador funciona, no do professor não"

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 programa ignora o que a pessoa digita e usa outro valor

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.

Muitos avisos "para arrumar depois", e nunca arrumados

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:

gcc -std=c23 -Wall -Wextra prog.c -o prog

2. Variável com valor inicial

Complete a declaração de um contador que começa em zero:

int total ;

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 -Werror no projeto: o compilador é o seu primeiro revisor.
  • Inicialize toda variável; confira o retorno de scanf, malloc e fopen; nunca copie texto sem o tamanho.
  • Todo malloc tem o seu free; respeite os limites dos arrays (i < N); dê nome aos números.
  • int main(void) com return 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.