Conseils de codage en C/Lisibilité des sources

Un livre de Wikilivres.

Les conseils suivants rendent les logiciels plus maintenables.

Leur respect a pour but de :

  • faciliter la lecture des codes sources écrits en C
  • diminuer l'effort nécessaire pour diagnostiquer les déficiences ou causes de défaillance, ou pour identifier les parties à modifier.

Cartouche d'entête (c_lis_1)[modifier | modifier le wikicode]

Chaque fichier source (.c, .h) et chaque fonction C doivent être précédés d’un cartouche qui pourra contenir :

  • Le nom de l'unité ou de la fonction.
  • Son rôle et ses points de programmation critiques.
  • Pour une fonction, la description des arguments et des codes retours.
  • La date, l’auteur et le rôle de chaque modification.
  • Sa licence et droits d'utilisation
  • La mention de documents de conception ou de référence

Justification[modifier | modifier le wikicode]

Le cartouche de l’unité permet de faciliter sa compréhension d'un point de vue général. Les cartouches de fonctions permettent de comprendre leur rôle et conditions d'appel sans se plonger dans le code source . Ces entêtes sont indispensables à la gestion de la configuration et à la maintenance.

Exemple[modifier | modifier le wikicode]

/*******************************************************
Nom ......... : limites.c
Role ........ : Affiche les caractéristiques numériques
                pour les types du langage C
Auteur ...... : Thierry46
Version ..... : V1.1 du 10/3/2008
Licence ..... : GPL

Compilation :
gcc -Wall -pedantic -std=c99 -o limites.exe limites.c
Pour exécuter, tapez : ./limites.exe
********************************************************/

Outils[modifier | modifier le wikicode]

  • La plupart des environnements de développement bâtissent un squelette de cartouche d'entête lors de la création d'un nouveau fichier
  • Pour les projets importants, développés en équipe, les versions des fichiers sources pourront être gérées par un logiciel spécialisé comme CVS, subversion...
  • L'utilisation d'un outil de gestion de la documentation comme Doxygen permet, à l'aide d'une syntaxe particulière dans les commentaires, de générer à partir des sources la documentation de programmation du projet.

Indentation des instructions (c_lis_2)[modifier | modifier le wikicode]

Les instructions imbriquées sont indentées à l’aide de tabulations ou d'espaces de façon homogène dans tous les fichiers sources du projet.

Justification[modifier | modifier le wikicode]

La maintenance et la relecture des sources sont facilités par une présentation homogène.

Exemple (extrait)[modifier | modifier le wikicode]

        //...
        // Teste si les valeurs lues sont les valeurs attendues.
        for (i=0; i<(int)nbInt; i++)
        {
                (void)printf("%d, ", valIntLu[i]);
                if ( valIntLu[i] != valInt[i] )
                {
                        (void)printf("Au lieu de %d\n", valInt[i]);
                        exit(EXIT_FAILURE);
                }
        }

Outils[modifier | modifier le wikicode]

  • L'outil UNIX indent permet d'améliorer la présentation des fichiers sources écrits en C. De nombreuses options permettent de paramètrer le style de ses sorties. Elles peuvent être passées sur la ligne de commande ou mieux dans un fichier de configuration spécifique.

Indentation des commentaires (c_lis_3)[modifier | modifier le wikicode]

Les commentaires suivent l’indentation des instructions de code.

Justification[modifier | modifier le wikicode]

Augmente la lisibilité.

Une instruction par ligne (c_lis_4)[modifier | modifier le wikicode]

Ne pas écrire plus d'une instruction par ligne.

Justification[modifier | modifier le wikicode]

Augmente la lisibilité.

Décomposer les instructions longues (c_lis_5)[modifier | modifier le wikicode]

Coder des lignes simples.

Justification[modifier | modifier le wikicode]

Il est quelquefois tentant de coder des lignes longues :

  • Dans le domaine numérique, le programmeur écrit des formules compliquée juxtaposant de nombreuses fonctions et opérateurs.
  • Le langage C permet de mélanger tests, appel de fonctions, opérations.

Le code parait, à première vue, plus compact, plus efficace. Il faut cependant décomposer les lignes longues en plusieurs, en utilisant des variables intermédiaires. Les compilateurs récents se chargeront d'optimiser efficacement le code pour vous. Le résultat sera plus facile à comprendre par d'autres. Les problèmes de priorité pourraient être atténués.

Constantes symboliques et macros en majuscule (c_lis_6)[modifier | modifier le wikicode]

Les noms des constantes symboliques et des macros instructions doivent être écrits en majuscule.

Justification[modifier | modifier le wikicode]

Dans un fichier source, permet des distinguer rapidement les constantes des variables.

Exemple[modifier | modifier le wikicode]

#define LONGUEUR_NOM_FICHIER 15
#define STR(s) #s
#define XSTR(s) STR(s)

Pas de lignes trop longues (c_lis_7)[modifier | modifier le wikicode]

Les instructions ne doivent pas dépasser la colonne 80. Les instructions longues doivent être placées sur plusieurs lignes.

Justification[modifier | modifier le wikicode]

Les lignes courtes seront plus faciles à comprendre par une personnes chargée de maintenir le logiciel.

Exemples[modifier | modifier le wikicode]

Mauvais :

#define sonne(N) { int n; for (n=N; n>0; n--) fputc(0x7, stderr);}

   (void)printf("\nProgramme %s, fichier source "__FILE__"\nCompile le "__DATE__" a "__TIME__"\n", argv[0]);

Meilleur :

#define sonne(N) \
{ \
   int n; \
   for (n=N; n>0; n--) fputc(0x7, stderr); \
}
//...
   (void)printf("\nProgramme %s, fichier source "__FILE__
      "\nCompile le "__DATE__" a "__TIME__"\n",
      argv[0]);

Nommage des identificateurs (c_lis_8)[modifier | modifier le wikicode]

Vous devez donner des noms significatifs aux identificateurs.

  • Évitez les noms trop courts sans signification fonctionnelle : v, vv, vvv...
  • Ne différenciez pas deux identificateurs uniquement en changeant la casse de certains caractères : Fichier, fichier.
  • Ne différenciez pas deux identificateurs uniquement par un nombre trop restreint de lettres.
  • N'utilisez pas de caractère souligné en tête ou en fin d'identificateur.
  • Utiliser une règle de nommage cohérente dans le projet : mot séparés par des soulignés (fichier_parametres_calcul) ou mots collés commençant par des majuscules sauf première lettre (fichierParametresCalcul).

Justification[modifier | modifier le wikicode]

Facilite la lisibilité et la maintenabilité.

Cohérence des identificateurs (c_lis_9)[modifier | modifier le wikicode]

Dès lors qu’un identificateur de variable est défini pour une entité significative, il faut utiliser ce même identificateur quelle que soit l’unité de code considérée.

Justification[modifier | modifier le wikicode]

Facilite l'analyse du programme.

Arguments du programme principal (c_lis_10)[modifier | modifier le wikicode]

Les arguments du programme principal sont standards : int main(int argc, char *argv[])

Justification[modifier | modifier le wikicode]

Améliore la lisibilité du code. argv[0] représente le nom de lancement du programme et peut être utilisé pour les messages d'erreur. La récupération des arguments du programme pourra se faire avec les fonctions getopt(), getsubopt().

Limiter l'utilisation des opérateurs ++ et -- (c_lis_11)[modifier | modifier le wikicode]

Les opérateurs ++ et -- ne sont autorisés que pour les indices de boucles et les pointeurs, à condition qu’aucun autre opérateur n’apparaisse et que seule l’utilisation post-fixée soit utilisée.

Justification[modifier | modifier le wikicode]

Facilite l’analyse et la maintenance par des programmeurs habitués à d’autres langages.

Exemple[modifier | modifier le wikicode]

  • Correct : for (i=0; i<n; i++) {//....
  • Moins bon : t[i++]=f(i++);

Limiter l'utilisation de l'opérateur de test ternaire (c_lis_12)[modifier | modifier le wikicode]

L’utilisation de l’expression conditionnelle ? : est interdite en dehors des macros.

Justification[modifier | modifier le wikicode]

Facilite l’analyse et la maintenance par des programmeurs habitués à d’autres langages.

Ne pas économiser les accolades (c_lis_13)[modifier | modifier le wikicode]

Utilisez des accolades {} autour d'une ou des instructions d'un bloc. Ce bloc peut faire partie d’une structure de contrôle comme if - else - for - do - while.

Justification[modifier | modifier le wikicode]

Rend le code plus lisible et évite les erreurs lors de l'ajout d'une instruction dans un bloc qui n'en contient qu'une seule.

Exemple[modifier | modifier le wikicode]

Mauvais :

for (i=0; i<n; i++)
   table[i] = 1;

Meilleur :

for (i=0; i<n; i++)
{
   table[i] = 1;
}

Exemple d'erreur provoquée par la violation de ce conseil :

if (test == 2)
{
   for (i=0; i<n; i++)
      table[i] = 1;
      table2[i] = 2;
   }

/* Suite... */

L'instruction table2[i] = 2; donne faussement l'impression de faire partie de la boucle for, ce qui est renforcé par l'indentation et l'accolade fermante du if.

Si table2 était dimensionné à n, les indices acceptables iraient de 0 à n-1 inclus. Lors de l'exécution de l'instruction table2[i] = 2;, i vaudrait la valeur n, ce qui provoquerait l'écriture d'une valeur hors de l'espace mémoire réservé pour le tableau table2.

Il s'en suivra :

  • Le débordement hors de l'espace utilisateur qui provoquera une violation mémoire : Sanction rapide par arrêt du programme exception SIGSEGV.
  • L'écrasement du contenu d'un autre pointeur situé juste après l'espace table2 qui lorsqu'il sera utilisé plus tard (100 lignes plus bas ?) provoquera une autre catastrophe : Cette erreur peut être difficile à détecter sans l'utilisation d'un outil qualité d'analyse dynamique.
  • Ces erreurs peuvent arriver aléatoirement selon le système, le compilateur utilisé ou le chargement du programme en mémoire.

Bien aligner les accolades (c_lis_14)[modifier | modifier le wikicode]

Une accolade fermante se trouve toujours à la verticale de l’accolade ouvrante correspondante.

Justification[modifier | modifier le wikicode]

Permet de mieux repérer les blocs d'instructions conditionnelles, les corps de boucles surtout en cas de structures imbriquées.

Exemple[modifier | modifier le wikicode]

// Compilation : gcc -Wall -pedantic -std=c99 -o essai.exe essai.c
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
   const int nbLigne = 3;
   const int nbCol = 4;

   int table[nbLigne][nbCol];
   for(int ligne = 0; ligne < nbLigne; ligne++)
   {
      for(int col = 0; col < nbCol; col++)
      {
         table[ligne][col] = ligne + col;
      }
   }

   // Impression
   for(int ligne = 0; ligne < nbLigne; ligne++)
   {
      for(int col = 0; col < nbCol; col++)
      {
         (void)printf("table[%d][%d] = %d,",
                      ligne, col, table[ligne][col]);
      }
      (void)puts("");
   }
   return EXIT_SUCCESS;
}

Éviter d'utiliser les opérateurs d'affectation spécifique au C (c_lis_15)[modifier | modifier le wikicode]

L’utilisation de l’assignation composée (+=, -=, *=, %=, /=) et de l’affectation multiple (v1 = v2 = v3;) est déconseillée.

Justification[modifier | modifier le wikicode]

Facilite l’analyse et la maintenance par des programmeurs habitués à d’autres langages.

Ne pas déclarer plusieurs variables dans la même instruction (c_lis_16)[modifier | modifier le wikicode]

Il faut déclarer chaque variable séparément et non les unes à la suite des autres, séparées par des virgules.

Justification[modifier | modifier le wikicode]

La déclaration de plusieurs variables dans une même instruction peut provoquer des erreurs de type pour les variables.

Une déclaration séparée permet aussi de décrire dans un commentaire en bout de ligne le rôle de chaque variable.

Exemple[modifier | modifier le wikicode]

// Declaration incorrecte :
// nomFicSortie est de type char au lieu de char * et ne peut donc être initialisé à NULL
char *nomFicEntree = NULL, nomFicSortie = NULL;

// Declaration correcte mais pas lisible
char *nomFicEntree = NULL, *nomFicSortie = NULL;

// Meilleur
char *nomFicEntree = NULL; // Fichier de parametres du calcul
char *nomFicSortie = NULL; // Fichier pour les points calculés

Seuils pour les métriques déterminant la facilité d'analyse (c_lis_17)[modifier | modifier le wikicode]

Les métriques suivantes influent sur la facilité d'analyse d'un programme (ISO/IEC 9126) :

  • VG : nombre cyclomatique : nombre de chemins linéairement indépendants dans un graphe connexe g, V(g) = A - N + 1 avec A : nombre de graphe entre les nœuds du graphe et N : nombre de nœuds du graphe.
  • STMT : Nombre d'instructions exécutables entre les accolades de début et de fin de la fonction.
  • FCOM : Fréquence des commentaires : F_COM = (BCOM+BCOB) / STMT, avec BCOM = Nombre de blocs de commentaires dans la fonction, BCOB : nombre de blocs de commentaires avant la fonction.
  • AVGS : Taille moyenne des instructions, calculée à partir du nombre d'opérateur et d'opérande distincts.

Seuils pour les métriques :

  • VG : de 1 à 20
  • STMT : de 1 à 100
  • FCOM : de 0,2 à 1,2
  • AVGS : de 2 à 10

Justification[modifier | modifier le wikicode]

  • VG trop élevée montre qu'une fonction est trop complexe et mériterait d'être décomposée en plusieurs ou réanalysée.
  • STMT trop élevée indique que la fonction est trop longue. Il faudrait la diviser ou peut-être appeler des fonctions.
  • FCOM : Habituellement les programmes ne sont pas assez commenté : FCOM faible. Cependant un programme avec trop de commentaires peut aussi montrer des problèmes (trop de ruses d'un expert qui aurait dû écrire plus simplement ???).
  • AVGS : Les instructions trop longues sont difficiles à comprendre et peuvent provoquer des problèmes de priorité des opérateurs, de conversion de type involontaires.