Règles de programmation en langage C

DESS-CCI

UJF UFR -IMA

 

 

1 Définition d'un fichier C
Une structuration systématique des fichiers sources apporte une bonne lisibilité des programmes pour l'auteur ainsi que pour un intervenant extérieur. Elle doit aussi être partagée par un groupe travaillant à un même développement, permettant ainsi au membre du groupe d'intervenir facilement sur des parties qu'il n'a pas développées. Nous donnons ci-après un exemple (fichier Somme) d'une telle structuration pour le fichier d'interface (Somme.h décrit en 1.1) et le fichier d'implantation (Somme.c décrit en 1.2). Il est important de bien commenter un programme pour en garantir réellement la lisibilité. Les commentaires en C sont définis par la syntaxe /* commentaires ... */.

 

1.1 Structuration du fichier d'interface
Un fichier d'interface contient toutes les définitions exportées par un fichier contenant un programme C. Ces définitions peuvent être des constantes, des types, des fonctions ou des variables.

 

1.2 Structuration du fichier d'implantation

Un fichier d'implantation contenant un programme C contient d'une part les définitions locales au fichier (notamment les variables déclarées comme static), c'est à dire celles dont la connaissance n'est nécessaire qu'à l'intérieur de ce fichier. Par ailleurs, il inclut systématiquement les définitions exportées de façon à ne pas les définir à deux endroits différents. D'autre part, il contient l'implantation des fonctions qu'il définit : les fonctions locales (déclarées comme static) ainsi que les fonctions exportées.

 

2 Format des définitions

Nous décrivons ici les règles à suivre lors des définitions de constantes, de macros, de types ou de variables, que ce soit dans la façon d'indenter ces définitions ou dans la manière de les nommer.

 

2.1 Les constantes

L'utilisation de constante dans le code doit toujours s'effectuer à travers des noms symboliques. Il y a plusieurs manières de définir des constantes symboliques. La première consiste à utiliser des "define".

#define CARACTERE_R ((char)'R')

Avec C ANSI, il est possible de définir les constantes d'une autre façon (économie de données dans les fichiers objets).

const char CARACTERE_R = 'R';

Toutes les constantes, qu'elles soient introduites par un "define" ou un "const", doivent être définies en majuscule. Lorsqu'il s'agit de constantes exportées (déclarées dans un fichier d'interface), le mot clé const doit être précédé par extern comme pour les variables ou les fonctions.
 

2.2 Les macros

Les macros sont introduites par des "define" comme les constantes. Néanmoins, nous n'allons pas leur faire suivre les mêmes règles typographiques. Un nom de macro sera toujours préfixé par "m_" de manière à les distinguer clairement dans le code, notamment vis-à-vis des fonctions.

L'utilisation de macros doit être réduite autant que possible. En effet, si nous avons un empilage important de macros (qui exprime malgré tout une certaine sémantique), en cas d'erreur, la phase de deboguage devient cauchemardesque car il est souvent impossible de suivre le code déroulé.

Par ailleurs, à l'utilisation d'une macro, certaines précautions doivent être prises pour éviter certains problèmes classiques. Prenons par exemple la définition suivante qui élève au carré une variable "X" :

#define m_Carre(X) (X * X)
 

Une telle macro suivant son utilisation n'a pas le même comportement que si c'était une fonction C alors qu'on considère facilement les macros comme telles. En effet, l'expression suivante n'a pas la sémantique d'un carré :

y = m_Carre(++x);

En fait, on affecte ici à "y" l'expression "(x + 1) * (x + 2)" (x étant la valeur de départ de cette variable). Pour cette raison, il faut éviter d'utiliser des expressions ayant des effets de bord comme paramètres dans des macros. Enfin, lorsqu'une macro définit une expression, il faut que cette expression soit parenthésée de manière à éviter les problèmes de priorité d'opérateurs.

 

2.3 Les types

Pour les définitions de types, les conventions de nommage utilisent le préfixe "t_", c'est à dire que le nom d'un type défini par typedef contient toujours le préfixe "t_". On privilégiera des définitions à base de "typedef" notamment pour tous les types exportés. Pour les types structurés ou les unions, les règles d'indentation des blocs s'appliquent :

typedef struct ListNode {

     char *Donnee;

     struct ListNode *Precedant, *Suivant;

} t_ListNode;
 

Le bloc définissant les éléments de la structure est décalé d'une tabulation vers la droite et les éléments définis dans le bloc sont alignés sur les accolades le définissant.

 

2.4 Les variables

Nous pouvons distinguer deux types de variables : les variables exportées et les variables locales à un fichier. Les premières sont déclarées dans le fichier d'interface associé au fichier d'implantation tandis que les secondes le sont dans le fichier d'implantation.

Une variable globale se définit de la façon suivante :

EXTERN char MaChaine[TAILLE];

 
La définition est précédée de la macro EXTERN définie systématiquement en tête d'un fichier d'interface. Il s'agit ensuite d'une définition normale de variable. Lorsqu'il s'agit d'une variable locale à un module, la définition a la forme suivante :

static int TailleChaine;

Le format de définition est donc le même que pour les deux types de variables sauf que celles qui sont locales à un module doivent toujours être déclarées static. Il est intéressant d'utiliser, comme précédemment, des majuscules dans les noms de variables exportées ou locales à un fichier d'implantation de manière à les distinguer des paramètres d'une fonction ou des variables locales à une fonction.

 

2.5 Les signatures

Comme pour les variables, nous distinguons deux types de fonctions : les fonctions exportées et les fonctions locales. Pour toutes les fonctions, leur signature est définie avant leur implantation. Dans le cas de fonctions exportées qui sont accessibles depuis d'autres fichiers, leur signature est définie dans le fichier d'interface de la manière suivante :

EXTERN long SommeEnt(long, long);

La signature commence par la macro EXTERN qui permet comme pour les variables de spécifier le caractère "extern" de la fonction suivant le contexte d'inclusion. Nous définissons ensuite la signature avec le type du résultat de la fonction, le nom de la fonction et enfin la définition du type des paramètres.

Pour les fonctions locales, comme les variables locales, elles sont toujours définies comme static, aussi bien au niveau de leur signature que de leur implantation dans le module. Toutes les signatures de ces fonctions apparaissent dans la partie qui leur est réservée dans le fichier d'implantation (section SIGNATURES DES FONCTIONS LOCALES) :

static char *FonctionLocale(t1, t2, t3);

 
Le format de définition est le même que pour les signatures de fonctions exportées et ne diffère que par le premier mot de la signature : la macro EXTERN est remplacée par le mot clé static.

 
3 Format du code

Nous définissons dans cette partie le format du code C utilisé pour l'implantation des fonctions. Ces règles sont essentiellement tirées de la norme ANSI du langage C. Le but est ici de garantir une certaine homogénéïté de ce code de manière à ce qu'un programmeur puisse lire aussi facilement son code que celui d'un autre.

 
3.1 Les fonctions
L'implantation d'une fonction est précédée d'une entête qui fournit divers renseignements sur cette dernière. Le format d'une telle entête est la suivant :

/*

*-----------------------------------------------------

*

* Fonction    : MaFonction

* Resultat    : int         Code d'error.

* Parametres  :

* Nom          Type         Role

* p1           t1           (D)Role de p1.

* p2           t2           (DR)Role de p2.

*

* Description : Role de la fonction.

*

*-----------------------------------------------------

*/
 

Deux parties peuvent être distinguées dans l'implantation d'une fonction : la signature de la fonction et le bloc définissant le code qui lui est associé. Prenons par exemple l'implantation de la fonction MaFonction :

int

M_MaFonction(

    t1 p1,

     t2 p2) {

     int i;

     for (i = 0; i < TAILLE; i++)

{

          ...

     }

 
La signature a toujours le même format et les lignes composant sa définition sont indentées comme le bloc définissant le code de la fonction. Le type de résultat de la fonction apparait sur la première ligne suivi, sur la ligne suivante, du nom de la fonction implantée. Ces deux lignes sont précédées d'une ligne contenant le mot clé static dans le cas d'une fonction locale. Les lignes suivant celle contenant le nom de la fonction définissent la liste des paramètres de la fonction au format ANSI.

Le bloc définissant le code de la fonction suit les règles d'indentation définies pour n'importe quel bloc. Il est décalé d'une tabulation vers la droite par rapport au bord. Il est important pour des raisons de lisibilité que le code associé à une fonction ne soit pas trop complexe. Cette complexité correspond soit à de trop nombreux niveaux d'imbrication, soit à un code trop long. Une longueur se situant aux environs de la taille d'une page parait être une bonne limite.

 

3.2 Les blocs

Un bloc contient deux parties consécutives. La première partie est composée d'un ensemble de définitions de variables locales au bloc. La deuxième correspond à un ensemble d'instructions.

{

long c_cour, nc = 0;

FILE *f_desc;

/* Ouverture du fichier "MonFichier" */

f_desc = fopen("MonFichier", "r");

if (f_desc == NULL)

      return;

c_cour = fgetc(f_desc);

/* ... */

}

 
Les deux parties sont toujours séparées par une ligne vide. Chaque ligne de définition de variable ne contient que des définitions de variables d'un même type. Toutes les définitions de ces lignes sont alignées sur les accolades ouvrant et fermant le bloc. Ces règles sont aussi valides pour les instructions de même que pour les commentaires qui doivent suivre le même alignement horizontal.
 

3.3 Les instructions
Pour que les instructions soient le plus lisibles possible, nous définissons leur indentation. Ainsi, la règle générale est que pour chaque instruction C qui introduit un nouveau bloc (le bloc pouvant se limiter à une seule instruction), ce bloc est en retrait d'une tabulation par rapport au positionnement horizontal de l'instruction concernée.
 

Structure du "if"

if (expression)              if (expression)

     bloc/instruction;            bloc/instruction;

                              else

                                  bloc/instruction;

Exemple :

if (a == 1)

     x = y;

if (a == b)

{

     fprintf(f_desc, "Hello world !");

     n_errs++;

}

else

continue;

 
Pour la structure if, il est possible de faire une exception à la règle d'indentation lorsqu'il s'agit d'instructions if cascadées du type "si condition1 B1 sinon si condition2 B2 sinon si condition3 B3 ... sinon Bn". Dans ce cas, les nouveaux if cascadés sont alignés sur le premier et figurent sur la même ligne que le else précédent.

Exemple :

/* str est une chaine dont on veut tester trois valeurs particulières ... */

if (strcmp(str, "Cas1") == 0)

     printf("C'est le premier cas.");

else if (strcmp(str, "Cas2") == 0)

     printf("C'est le deuxieme cas.");

else if (strcmp(str, "Cas3") == 0)

     printf("C'est le troisieme cas.");

else

{

     printf("Cas non considere : erreur.");

     exit(1);

}
 

Structure du "while"

while (expression)

bloc/instruction;

 

Exemple :

while ((c = getchar()) != EOF)

     putchar(c);

while ((c = getchar()) != EOF)

{

     putchar(c);

     --nbchar;

}
 

Structure du "do-while"

do

     bloc/instruction;

while (expression);

 

Exemple :

do

{

     b += Compute(a, MAX);

     ++a;

} while (a < 100);

 

Structure du "for"

for (expression; expression; expression)

     bloc/instruction;

 

Exemple :

for (p = Tete; p; p = p->Suiv)

{

     Install(p);

     syms++;

}

 

Structure du "switch"

switch (expression)

     bloc

 

Exemple :

switch e

{

case '\n' :

case '\r' :

     echo("....");

     break;

case '\t' :

     echo("....");

     break;

default :

     echo("....");

     break;

}

Lorsqu'un cas correspondant à l'expression du switch est sélectionné, toutes les instructions qui suivent ce cas dans le bloc du switch sont exécutées jusqu'à la fin du bloc ou jusqu'à une instruction de rupture de séquence du bloc (break ou return par exemple).
 

3.4 Les commentaires

Les commentaire doivent être écrits autant que possible en anglais. Les commentaires dans le code sont définis avant l'instruction ou la suite d'instructions qu'ils renseignent et respecte le même alignement que celles-ci. Ils sont donnés sur une ou plusieurs lignes qui leur sont réservées.

Exemples :

/* commentaire court */

instruction;

instruction; /* commentaire court */

/*

* Commentaire long decrivant un bloc de lignes.

* Le commentaire est encadre par les delimiteurs

* de commentaire et chaque ligne est précédée de " *".

*/

instruction;

instruction;

instruction;

 

3.5 Les opérateurs

Nous définissons, dans cette section, les règles d'écriture des opérateurs.

Opérateurs primaires "->", "." et "[ ]"

Ils ne sont ni précédés ni suivis d'espaces.

ptree->leftnode          s.m          tab[ind]

Les parenthèses

Les parenthèses après un nom de fonction ne sont pas précédées d'espaces. Les expressions parenthésées sont écrites sans espace après la parenthèse ouvrante et sans espace avant la parenthèse fermante.

func(2, x)          (x * (y - 3.87))

Les opérateurs unaires

Ils sont écrits sans espace entre eux et leur opérande.

++i       !p       -n       (long)m       *p       &x

Les opérateurs d'assignation et de comparaison

Ils sont précédés et suivis d'un espace.

c1 = c2      i += j      a == b      n > 0 ? n : -n

Les virgules et point virgules

Ils sont toujours suivis d'un espace ou d'un saut à la ligne.

strncat(t, s, n)          for (i = 0; i < n; ++i)

Tous les autres opérateurs sont en général précédés et suivis par un espace.

x - y          a < b && b < c          m % 10
 

3.6 Les expressions

Les expressions ne doivent pas dépendre de l'ordre d'évaluation d'un compilateur C. Il faut donc éviter les expressions du type :

a[i] = i++;

++i + i

Dans tous les cas, il faut parenthéser les expressions au maximum, notamment lorsque cela facilite la compréhension d'une expression complexe.