Conseils de codage en C/Recherche des erreurs

Conseils de codage en C
Sommaire
Maintenabilité

Fiabilité

Qualité

Livre
Modifier le sommaire

L'application de ces conseils facilite la recherche des erreurs et permet d'en éviter certaines.

Des identificateurs plus parlants (c_rec_1)Modifier

Utilisez des identificateurs parlants pour les variables. Le nom d’une variable devra rappeler son rôle ou son utilité.

JustificationModifier

Des noms de variable trop courts et souvent sans signification (a, n, x...) ne permettent pas aux relecteurs de faire la relation entre l'identificateur informatique et la réalité représentée. La recherche des variables est alors difficile et les risques d'avoir deux identificateurs dont les portées se superposent sont augmentés.

ExempleModifier

Pour désigner un numéro de maille.

  • Mauvais : n
  • Bon : numMaille

Sortie de boucle (c_rec_2)Modifier

La sortie d’une boucle (do, while, for) doit se faire par la condition de test.

JustificationModifier

Un algorithme bien conçu ne doit pas nécessiter de sortie prématurée dans le corps de boucle. La programmation structurée permet d'éviter le style spaghetti des premiers programmeurs. Ce mode de programmation ancien rendait le suivi du déroulement difficile à l'aide d'un débugueur. Il fallait mettre des points d'arrêt sur chaque sortie de boucle potentiel. La logique du programme était très difficile à appréhender par la personne chargée d'effectuer des modifications.

Les instructions goto, continue, break (hors switch) sont donc vivement déconseillées.

ExempleModifier

for (int i = indiceMax; table[i] != NULL; i++)
{
   free(table[i]);
}

for pour contrôler les boucles (c_rec_3)Modifier

L’instruction d’une boucle for ne doit contenir aucune instruction vide. De plus, chacun de ses membres doit porter sur au moins une variable commune.

JustificationModifier

L’instruction for doit permettre le contrôle total d’une itération. Si une instruction est vide, cela veut dire que la boucle aurait pu être écrite sous une autre forme (do ou while).

Mettre des parenthèses dans les expressions (c_rec_4)Modifier

Les expressions arithmétiques et logiques doivent être placées entre parenthèses.

JustificationModifier

Cette règle évite les erreurs d’interprétation dues aux règles d’associativité du langage C. Son non respect peut conduire à des erreurs dans le codage des expressions mathématique ou de test.

ExempleModifier

On veut tester si n est pair : n & 1 effectue un ET logique de bit entre n et 1, mais l'opérateur relationnel == est prioritaire par rapport à &, l’expression ( n & 1 == 0 ) sera donc toujours fausse.

  • Mauvais : if ( n & 1 == 0 ) ...
  • Bon : if ( ( n & 1 ) == 0 ) ...

Mettre des parenthèses autour des paramètres des macros (c_rec_5)Modifier

Les macros doivent être écrites avec des parenthèses autour de leurs paramètres.

JustificationModifier

Lors du remplacement des paramètres formels par les paramètres effectifs, il peut y avoir des problèmes de priorité des opérateurs dans les expressions résultats.

ExempleModifier

Mauvais : #define double(a) 2 * a :
L’appel double(2+1) sera étendu en 2 * 2 +1 : vaudra 5 au lieu de 6.

Meilleur : #define double(a) 2 * (a) :
L’appel double(2+1) sera étendu en 2 * (2 +1) : vaudra 6

Ne pas écrire de macro de plus de 5 instructions (c_rec_6)Modifier

JustificationModifier

Au-delà de 5 instructions, le déroulement est assimilable à un traitement, et doit faire partie intégrante d’une fonction. De plus, les macros instructions ne permettent pas un contrôle strict de leurs paramètres.

Si l'utilisation d'une fonction entraîne une dégradation des performances, il faut recourir à l’inlining. Le mot clé C99 inline répond à ce problème.

Pas d'opérateurs unaire à l'appel des macros (c_rec_7)Modifier

Ne pas placer d’opérateurs unaire (++, --) dans les paramètres d’appel des macros.

JustificationModifier

Le résultat est bien souvent imprévisible par suite des effets de bord, surtout en cas de répétition du paramètre dans la définition de la macro.

Éviter les problèmes d'inclusion multiple (c_rec_8)Modifier

On protégera chaque fichier d’en-tête .h des inclusions multiples.

JustificationModifier

Lorsqu'un fichier d'entête (.h) est inclus plusieurs fois directement, un test en son début permet d'éviter les erreurs de compilation dues à des déclarations répétées :

ExempleModifier

Pour le fichier graphique.h:

#ifndef GRAPHIQUE_H
#define GRAPHIQUE_H
   // corps du fichier d’en-tête
   // ...
#endif

Mélange d'opérateur arithmétique et relationnel (c_rec_9)Modifier

Le calcul d’une expression complexe au sein d’une condition est à éviter.

JustificationModifier

Facilite l’analyse des sources, évite de superposer les erreurs de priorité des opérateurs arithmétiques à ceux des opérateurs relationnels.

Prototypes de fonction (c_rec_10)Modifier

Les fonctions doivent être prototypées, une fonction ou une variable ne doit pas avoir de type par défaut.

JustificationModifier

Facilite l’analyse. Permet un meilleur contrôle de la correspondance arguments - paramètres. La norme C99 interdit de transgresser cette règle.

OutilsModifier

Le contrôle du peut être effectué en utilisant le compilateur C avec options les plus strictes ou un outils qualité comme lint ou splint.

Les variables ne doivent pas se masquer (c_rec_11)Modifier

Une variable ne doit pas en masquer une autre. Cela se produit lorsque deux variables ont le même nom dans des blocs imbriqués.

JustificationModifier

Lorsque plusieurs entités sont désignées par le même nom et ne peuvent être distinguées que par leur position par rapport aux structures de contrôle, cela réduit la lisibilité du code et favorise l’apparition d’erreurs, en particulier lors de la maintenance du source.

Exemple à ne pas suivreModifier

Dans l'extrait de source suivant, les variables i se masquent.

// Première déclaration de i
int i = 0;
// ...
{
   // Deuxième déclaration de i
   int i = 0;
   // ...
   i++;
   // ...
}

Si la deuxième déclaration de i est supprimée, alors il y a un risque pour que l’incrémentation ne le soit pas, ce serait alors la variable déclarée hors du bloc qui serait utilisée, ce qui ne serait pas le comportement souhaité. Le code resterait alors valide et aucun problème ne serait détectée.

Pas de suppression de l'attribut const (c_rec_12)Modifier

JustificationModifier

Selon le compilateur, il est possible que les données const soient stockées dans une zone accessible en lecture, mais pas l’écriture.

Limiter l'utilisation des pointeurs (c_rec_13)Modifier

Éviter l’utilisation des pointeurs. Préférer l’utilisation des indices de tableau.

JustificationModifier

L’utilisation abusive de pointeurs est la source de graves dysfonctionnements très difficiles à détecter. Les programmeurs habitués à d’autres langages sont souvent perdus face à certaines subtilités du C. Pour les parcours de tableaux, il est préférable d’utiliser des indices (tab[i]) au lieu du déréférencement par pointeur.

Utiliser le mot-clé const (c_rec_14)Modifier

Utiliser le mot-clé ANSI const pour définir :

  • Une variable non modifiable.
  • Un argument non modifiable par une fonction.

JustificationModifier

Rejette, dès la phase de compilation, certaines modifications abusives.

Affectations et identificateurs de tableau (c_rec_15)Modifier

Être prudent lors de l'utilisation des identicateurs de tableau et des pointeurs sur chaîne constante.

JustificationModifier

Les types int a[] et int* a sont complètement différents, même si, une fois déclarés, leur usage parait identique. Le programmeur doit être particulièrement vigilant : par exemple, beaucoup d’éditeurs de liens confondraient un int* a et un int a[] définis dans deux modules (.o) différents. Ceci est susceptible de provoquer une erreur fatale.

ExempleModifier

Voici une illustration de la différence entre ces deux types [C FAQ] :

char a[] = "hello";
char* p = "hello";

La variable “ a ” occupe 6 octets dans l’espace de la mémoire dynamique. Cette zone sera désallouée lorsque la variable sortira de son espace de validité. La variable p occupe 4 octets (taille courante d’un pointeur). Elle est un pointeur qui référence une région de la mémoire non modifiable. Une nouvelle valeur peut être affectée à “p”, mais pas à “a”. En fait, un bon compilateur devrait imposer ici le type const char*.