Tout programmeur C rencontre la même courte liste d'erreurs, généralement la première semaine et parfois dix ans plus tard. Ce qui les rend dignes d'un catalogue, c'est que la plupart produisent des messages d'erreur qui ne les décrivent pas - ou, pire, aucun message du tout.
Chaque entrée ci-dessous est un symptôme, la cause qui se cache derrière, et la correction.
Point-virgule manquant et la cascade
Symptôme : un mur d'erreurs, toutes signalées sur des lignes qui semblent correctes.
program.c:6:5: error: expected ';' before 'printf'
program.c:7:5: error: expected declaration specifiers before 'return'
Cause : le C termine les instructions par ;. Omettez-en un et le compilateur colle la ligne suivante à la courante, puis signale la confusion à l'endroit où le texte combiné cesse d'avoir un sens - typiquement la ligne suivante.
int main(void) {
int x = 5 /* point-virgule manquant */
printf("%d\n", x);
return 0;
}
Correction : quand un paquet d'erreurs apparaît, ne corrigez que la première et recompilez. Tout ce qui suit peut n'être que des retombées. Et vérifiez toujours la ligne avant celle que le compilateur nomme.
La même cascade vient d'une accolade non fermée ou d'un commentaire /* non terminé, où les erreurs peuvent atterrir des dizaines de lignes plus loin.
Une note supplémentaire : pas de point-virgule après un en-tête if, for, while, ni après l'accolade fermante d'une définition de fonction. if (x > 0); compile très bien et ne fait rien - le ; est tout le corps.
Affectation au lieu de comparaison
Symptôme : une condition toujours vraie, ou une variable qui change mystérieusement.
int x = 5;
if (x = 10) { /* affecte 10, puis teste 10 -> vrai */
printf("x vaut dix\n"); /* s'affiche toujours ; x vaut maintenant 10 */
}
Cause : = affecte, == compare. La valeur d'une affectation est la valeur affectée, donc if (x = 10) teste 10, qui est non nul, donc vrai. Le compilateur l'accepte parce que c'est parfois ce que les gens veulent dire.
Correction : utilisez == dans toute condition, et activez -Wall pour que le compilateur avertisse :
warning: suggest parentheses around assignment used as truth value
Si vous voulez réellement une affectation dans une condition - courant avec while ((c = getchar()) != EOF) - les parenthèses supplémentaires le disent et font taire l'avertissement.
Déclaration implicite d'une fonction
Symptôme :
warning: implicit declaration of function 'printf'
warning: implicit declaration of function 'malloc'
...suivi parfois de résultats étranges à l'exécution ou d'une erreur d'édition de liens.
Cause : le compilateur a rencontré l'appel d'une fonction dont il n'a aucune déclaration. Dans les dialectes antérieurs à C99, il supposait que la fonction renvoyait un int et continuait ; sur les systèmes 64 bits, cette hypothèse tronque un pointeur renvoyé à 32 bits, et c'est ainsi qu'un <stdlib.h> manquant transforme malloc en plantage.
Correction : incluez le bon en-tête.
| Fonction | En-tête |
|---|---|
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> |
Pour vos propres fonctions, le même avertissement signifie que vous en avez appelé une avant de la définir. Mettez un prototype au-dessus de main :
scanf sans le &
Symptôme : le programme plante à la saisie, ou ne lit rien et laisse la variable inchangée.
int age;
scanf("%d", age); /* & manquant : passe la valeur, pas l'adresse */
Cause : scanf écrit dans votre variable, il lui faut donc l'adresse de celle-ci. Passer age livre le garbage qu'elle contenait et scanf le traite comme un endroit où écrire, ce qui est généralement une erreur de segmentation.
Correction : un & avant la variable - pour tous les types sauf un tableau, qui est déjà une adresse :
Deux pièges voisins de la même famille : utilisez %lf pour un double dans scanf (%f y désigne un float, et écrire la taille d'un float dans un double le laisse faux), et bornez toujours un %s par une largeur - %49s pour un tampon de 50 octets - sinon un mot long le déborde.
Mieux encore : lisez une ligne entière avec fgets et analysez-la, ce qui ne peut pas déborder et ne laisse pas de saisie traîner dans le tampon. Plus de détails dans scanf.
Comparer des chaînes avec ==
Symptôme : deux chaînes manifestement identiques se comparent comme différentes.
char a[] = "hello";
char b[] = "hello";
if (a == b) { /* compare deux adresses : faux */
printf("identiques\n");
}
Cause : une chaîne C n'est pas une valeur, c'est un pointeur vers le premier caractère. == compare les pointeurs. Deux tableaux contenant un texte identique vivent à des adresses différentes, donc le test est faux. (De façon déroutante, comparer deux littéraux identiques donne parfois vrai, car le compilateur peut n'en stocker qu'une copie - ce qui rend le bug intermittent.)
Correction : strcmp, et rappelez-vous qu'il renvoie 0 pour « égales » :
La lecture inversée piège aussi : if (strcmp(a, b)) est vrai quand les chaînes diffèrent, puisqu'un résultat non nul signifie « pas égales ». Écrivez toujours le == 0 explicitement.
La division entière
Symptôme : une moyenne de 0, un pourcentage toujours 0 ou 100, un rapport qui a perdu sa fraction.
int correct = 7, total = 10;
double score = correct / total; /* 0.0, pas 0.7 */
Cause : les deux opérandes sont des int, donc le C fait une division entière et tronque avant que le résultat ne soit affecté à un double. 7 / 10 vaut 0 ; convertir 0 en double donne 0.0.
Correction : rendez un opérande flottant avant la division :
Convertir un opérande promeut l'autre automatiquement. Convertir le résultat arrive trop tard - la troncature a déjà eu lieu. Voir transtypage.
Le même piège se cache dans des expressions comme (a + b) / 2 pour un milieu, et 1 / 2 * x, qui vaut toujours 0 quel que soit x.
return manquant ou erroné
Symptôme : une fonction renvoie un nombre faux mais plausible, différent à chaque exécution ou à chaque compilation.
int add(int a, int b) {
int sum = a + b;
/* pas d'instruction return */
}
Cause : atteindre la fin d'une fonction non-void sans retourner donne une valeur non spécifiée - en pratique, ce qui traînait dans le registre de retour. C'est un comportement indéfini si l'appelant l'utilise.
La forme plus sournoise retourne sur certains chemins et pas sur d'autres :
int classify(int n) {
if (n > 0) return 1;
if (n < 0) return -1;
/* n == 0 tombe au bout */
}
Correction : retournez sur chaque chemin, et compilez avec -Wall - le « control reaches end of non-void function » de GCC attrape les deux versions.
Variables non initialisées
Symptôme : une sortie aberrante, ou des résultats qui changent d'une exécution à l'autre et selon le niveau d'optimisation.
int total; /* contient ce qui trainait sur la pile */
for (int i = 1; i <= 5; i++) {
total += i; /* on ajoute a du garbage */
}
printf("%d\n", total); /* un nombre enorme */
Cause : les variables locales ne sont pas mises à zéro. Une variable globale ou static est mise à zéro automatiquement ; une locale démarre avec les octets déjà présents à cette adresse de pile.
Correction : initialisez au point de déclaration. Cela ne coûte rien et supprime toute la classe de bug :
-Wall -Wextra avertit sur beaucoup d'entre elles (« may be used uninitialized »), et -fsanitize=memory ou valgrind attrape le reste. Celle-ci est particulièrement vicieuse, car un pointeur non initialisé mène droit à une erreur de segmentation.
Décalage d'un
Symptôme : le dernier élément est manqué, ou un élément de trop est touché et le programme se comporte mal plus tard.
int arr[5];
for (int i = 0; i <= 5; i++) { /* touche arr[5], qui n'existe pas */
arr[i] = i;
}
Cause : un tableau de n éléments a des indices de 0 à n - 1. <= fait un passage de trop.
Correction : le motif i < n, et calculez n depuis le tableau plutôt que d'écrire le nombre deux fois :
La version chaîne - oublier la place pour '\0' - est la même erreur sous un autre chapeau, et char word[5] contenant "hello" est un débordement de tampon.
Deux plus petites à connaître
Point-virgule après un en-tête de boucle. for (int i = 0; i < 10; i++); suivi d'un bloc entre accolades exécute la boucle dix fois sans rien faire, puis le bloc une fois. Cela compile sans broncher.
sizeof sur un pointeur. Dans une fonction, un paramètre tableau est un pointeur, donc sizeof(arr) est la taille du pointeur (8 octets), pas celle du tableau. Passez la longueur en argument séparé :
L'habitude qui évite presque tout cela
Compilez avec les avertissements activés, dès le tout premier programme :
gcc -Wall -Wextra -g program.c -o program
-Wall -Wextra attrape l'affectation-dans-une-condition, le return manquant, la lecture non initialisée, la variable inutilisée et le spécificateur printf qui ne correspond pas à son argument. Ajouter -fsanitize=address,undefined pendant le développement attrape presque tout le reste au moment où cela se produit.
Traitez chaque avertissement comme une erreur que vous n'avez pas encore rencontrée. Un programme C qui compile sans avertissement n'est pas garanti correct - mais presque tout programme C qui plante avait d'abord averti de quelque chose.
Questions fréquentes
Que signifie « implicit declaration of function » en C ?
Le compilateur a rencontré l'appel d'une fonction dont il n'a jamais vu la déclaration. Presque toujours, vous avez oublié un #include - printf demande <stdio.h>, malloc demande <stdlib.h>, strlen demande <string.h>. Cela peut aussi signifier que vous avez appelé votre propre fonction avant de la définir, ce qu'un prototype au-dessus de main corrige.
Pourquoi mon programme C signale-t-il une erreur sur une ligne qui semble correcte ?
Généralement parce que la vraie faute est sur la ligne précédente. Un point-virgule manquant, une accolade non fermée ou un commentaire non terminé fait lire vos deux lignes comme une seule au compilateur, qui signale la confusion là où le texte devient enfin inanalysable. Vérifiez toujours d'abord la ligne au-dessus de celle signalée.
Pourquoi ne peut-on pas comparer des chaînes avec == en C ?
Parce qu'une chaîne C est un char * - un pointeur - donc == compare deux adresses, pas les caractères pointés. Deux chaînes identiques stockées à des endroits différents donnent false. Utilisez strcmp(a, b) == 0 de <string.h>, qui renvoie 0 quand les contenus correspondent.
Pourquoi 5 / 2 donne-t-il 2 en C ?
Parce que les deux opérandes sont des entiers, le C fait donc une division entière et jette la partie fractionnaire. Rendez un côté flottant pour obtenir 2,5 : 5.0 / 2, ou (double)a / b avec des variables. Convertir le résultat arrive trop tard - (double)(5 / 2) vaut 2.0.