Todo programador C encontra a mesma listinha de erros, geralmente na primeira semana e ocasionalmente uma década depois. O que os torna dignos de catálogo é que a maioria produz mensagens de erro que não os descrevem - ou, pior, nenhuma mensagem.
Cada entrada abaixo é um sintoma, a causa por trás dele e a solução.
Ponto e vírgula faltando e a cascata
Sintoma: uma parede de erros, todos reportados em linhas que parecem corretas.
program.c:6:5: error: expected ';' before 'printf'
program.c:7:5: error: expected declaration specifiers before 'return'
Causa: C termina instruções com ;. Deixe um de fora e o compilador cola a próxima linha na atual, e depois reporta a confusão no ponto em que o texto combinado deixa de fazer sentido - tipicamente na linha seguinte.
int main(void) {
int x = 5 /* ponto e vírgula faltando */
printf("%d\n", x);
return 0;
}
Solução: quando aparecer um monte de erros, corrija só o primeiro e recompile. Tudo depois dele pode ser consequência. E confira sempre a linha anterior à que o compilador aponta.
A mesma cascata vem de uma chave não fechada ou de um comentário /* não encerrado, casos em que os erros podem cair dezenas de linhas adiante.
Uma observação extra: nada de ponto e vírgula depois do cabeçalho de um if, for ou while, nem depois da chave de fechamento da definição de uma função. if (x > 0); compila normalmente e não faz nada - o ; é o corpo inteiro.
Atribuição no lugar de comparação
Sintoma: uma condição sempre verdadeira, ou uma variável que muda misteriosamente.
int x = 5;
if (x = 10) { /* atribui 10 e depois testa 10 -> verdadeiro */
printf("x é dez\n"); /* imprime sempre; x agora é 10 */
}
Causa: = atribui, == compara. O valor da atribuição é o valor atribuído, então if (x = 10) testa 10, que é diferente de zero, o que é verdadeiro. O compilador aceita porque de vez em quando é isso que as pessoas querem.
Solução: use == em toda condição e ligue o -Wall para o compilador avisar:
warning: suggest parentheses around assignment used as truth value
Se você realmente quer uma atribuição dentro de uma condição - comum em while ((c = getchar()) != EOF) - os parênteses extras dizem isso e silenciam o aviso.
Declaração implícita de uma função
Sintoma:
warning: implicit declaration of function 'printf'
warning: implicit declaration of function 'malloc'
...seguido às vezes de resultados estranhos em tempo de execução ou de um erro de ligação.
Causa: o compilador encontrou uma chamada a uma função para a qual não tem declaração. Em dialetos anteriores ao C99, ele supunha que a função retornava int e seguia em frente; em sistemas de 64 bits essa suposição trunca um ponteiro retornado para 32 bits, que é como um <stdlib.h> esquecido transforma o malloc num travamento.
Solução: inclua o cabeçalho certo.
| Função | Cabeçalho |
|---|---|
printf, scanf, fopen | <stdio.h> |
malloc, free, atoi, rand, exit | <stdlib.h> |
strlen, strcpy, strcmp | <string.h> |
sqrt, pow, fabs | <math.h> |
isdigit, toupper | <ctype.h> |
time | <time.h> |
Para funções suas, o mesmo aviso significa que você chamou uma antes de defini-la. Coloque um protótipo acima de main:
scanf sem o &
Sintoma: o programa trava na entrada, ou não lê nada e deixa a variável intacta.
int age;
scanf("%d", age); /* falta o & : passa o valor, não o endereço */
Causa: scanf escreve dentro da sua variável, então precisa do endereço dela. Passar age entrega o lixo que estava lá dentro, e o scanf trata isso como um lugar para escrever, o que normalmente é uma falha de segmentação.
Solução: & antes da variável - para todo tipo, exceto um array, que já é um endereço:
Duas armadilhas vizinhas da mesma família: use %lf para um double no scanf (%f ali significa float, e escrever o tanto de bytes de um float dentro de um double o deixa errado) e sempre limite um %s com uma largura - %49s para um buffer de 50 bytes - ou uma palavra longa o estoura.
Melhor ainda: leia uma linha inteira com fgets e a analise, o que não pode transbordar e não deixa entrada perdida no buffer. Mais sobre os dois em scanf.
Comparar strings com ==
Sintoma: duas strings que claramente coincidem são comparadas como diferentes.
char a[] = "hello";
char b[] = "hello";
if (a == b) { /* comparando dois endereços: falso */
printf("iguais\n");
}
Causa: uma string em C não é um valor, é um ponteiro para o primeiro caractere. == compara os ponteiros. Dois arrays com o mesmo texto vivem em endereços diferentes, então o teste é falso. (De forma confusa, comparar dois literais idênticos às vezes dá verdadeiro, porque o compilador pode guardar só uma cópia - o que torna o bug intermitente.)
Solução: strcmp, e lembre que ele retorna 0 para igual:
A leitura invertida também pega gente: if (strcmp(a, b)) é verdadeiro quando as strings diferem, já que um resultado diferente de zero significa "não são iguais". Escreva sempre o == 0 explicitamente.
Divisão inteira
Sintoma: uma média de 0, uma porcentagem que é sempre 0 ou 100, uma razão que perdeu a fração.
int correct = 7, total = 10;
double score = correct / total; /* 0.0, e não 0.7 */
Causa: os dois operandos são int, então C faz divisão inteira e trunca antes de o resultado ser atribuído a um double. 7 / 10 é 0; converter 0 para double é 0.0.
Solução: torne um dos operandos de ponto flutuante antes da divisão:
Converter um operando promove o outro automaticamente. Converter o resultado é tarde demais - o truncamento já aconteceu. Veja conversão de tipos.
A mesma armadilha se esconde em expressões como (a + b) / 2 para um ponto médio e 1 / 2 * x, que é sempre 0 independentemente de x.
return ausente ou errado
Sintoma: uma função retorna um número errado com cara de plausível, diferente a cada execução ou a cada compilação.
int add(int a, int b) {
int sum = a + b;
/* nenhuma instrução return */
}
Causa: chegar ao fim de uma função não-void sem retornar dá um valor não especificado - na prática, o que por acaso estivesse no registrador de retorno. É comportamento indefinido se quem chamou usar esse valor.
A forma mais sorrateira retorna em alguns caminhos e em outros não:
int classify(int n) {
if (n > 0) return 1;
if (n < 0) return -1;
/* n == 0 escorrega até o fim */
}
Solução: retorne em todo caminho e compile com -Wall - o "control reaches end of non-void function" do GCC pega as duas versões.
Variáveis não inicializadas
Sintoma: saída com lixo, ou resultados que mudam entre execuções e entre níveis de otimização.
int total; /* contém o que estava na pilha */
for (int i = 1; i <= 5; i++) {
total += i; /* somando em cima de lixo */
}
printf("%d\n", total); /* algum número enorme */
Causa: variáveis locais não são zeradas. Uma variável global ou static é posta em zero automaticamente; uma local começa com os bytes que já estavam naquele endereço da pilha.
Solução: inicialize no ponto da declaração. Não custa nada e elimina a classe inteira de bug:
-Wall -Wextra avisa sobre muitos desses casos ("may be used uninitialized"), e -fsanitize=memory ou o valgrind pegam o resto. Esse é especialmente cruel porque um ponteiro não inicializado leva direto a uma falha de segmentação.
Deslocamento por um (off-by-one)
Sintoma: o último item é perdido, ou um elemento a mais é tocado e o programa se comporta mal depois.
int arr[5];
for (int i = 0; i <= 5; i++) { /* toca arr[5], que não existe */
arr[i] = i;
}
Causa: um array de n elementos tem índices de 0 a n - 1. <= executa uma passada a mais.
Solução: o padrão i < n, e calcule n a partir do array em vez de escrever o número duas vezes:
A versão com strings - esquecer espaço para o '\0' - é o mesmo erro com outra roupa, e char word[5] com "hello" dentro é um estouro de buffer.
Dois menores que vale conhecer
Ponto e vírgula depois do cabeçalho de um laço. for (int i = 0; i < 10; i++); seguido de um bloco entre chaves executa o laço dez vezes sem fazer nada e depois executa o bloco uma vez. Compila sem reclamar.
sizeof num ponteiro. Dentro de uma função, um parâmetro de array é um ponteiro, então sizeof(arr) é o tamanho do ponteiro (8 bytes), não o do array. Passe o comprimento como um argumento separado:
O hábito que evita a maior parte disso
Compile com os avisos ligados, desde o primeiro programa:
gcc -Wall -Wextra -g program.c -o program
-Wall -Wextra pega a atribuição dentro de condição, o return faltando, a leitura de variável não inicializada, a variável não usada e o especificador do printf que não bate com o argumento. Acrescentar -fsanitize=address,undefined durante o desenvolvimento pega quase todo o resto no momento em que acontece.
Trate todo aviso como um erro que você ainda não encontrou. Um programa em C que compila sem avisos não tem correção garantida - mas quase todo programa em C que trava estava avisando sobre alguma coisa antes.
Perguntas frequentes
O que significa 'implicit declaration of function' em C?
O compilador encontrou uma chamada a uma função que ele nunca viu declarada. Quase sempre você esqueceu um #include - printf precisa de <stdio.h>, malloc precisa de <stdlib.h>, strlen precisa de <string.h>. Também pode significar que você chamou uma função sua antes de defini-la, o que um protótipo acima de main resolve.
Por que meu programa em C acusa erro numa linha que parece correta?
Normalmente porque o erro de verdade está na linha anterior. Um ponto e vírgula faltando, uma chave não fechada ou um comentário não encerrado fazem o compilador ler suas duas linhas como uma só, então ele reporta a confusão onde o texto finalmente deixa de fazer sentido. Confira sempre a linha acima da que foi apontada.
Por que não posso comparar strings com == em C?
Porque uma string em C é um char * - um ponteiro - então == compara dois endereços, não os caracteres para os quais eles apontam. Duas strings idênticas guardadas em lugares diferentes dão false. Use strcmp(a, b) == 0, de <string.h>, que retorna 0 quando o conteúdo coincide.
Por que 5 / 2 dá 2 em C?
Porque os dois operandos são inteiros, então C faz divisão inteira e descarta a fração. Torne um dos lados um valor de ponto flutuante para obter 2.5: 5.0 / 2, ou (double)a / b quando estiver trabalhando com variáveis. Converter o resultado é tarde demais - (double)(5 / 2) é 2.0.