Normes et conseils de programmation en C/C++

applicables en 1ère année dans le module "C++" et en 2ème année dans les modules "systèmes d'exploitation" et "réseaux".

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 20/12/1999 - Dernière mise à jour : 13/01/2000

Sommaire

Organisation des fichiers
Structure d'un fichier source
Structure d'un fichier inclus .h - Directives d'inclusion conditionnelle
Imbrication de fichiers inclus
Présentation
Indentation
Commentaires
Aération
Listings
Identificateurs
Identificateurs des variables et des fonctions
Identificateurs des classes
Identificateurs des paramètres formels des constructeurs, des modifieurs, ou d'autres fonctions membres de classes
Identificateurs des espaces de noms
Identificateurs des données membres
Identificateurs des constantes
Identificateurs des prédicats ou variables booléennes
Accolades
Présentation "K&R" (Kernighan and Richie)
Présentation "classique"
Terminaison des classes, des fonctions, et des espaces de noms
Déclarations et définitions
Déclarations et définitions des classes
Déclaration et définition des namespace's
Déclaration et définition des fonctions
Schéma alternatif
Rupture de séquence
Déséquilibre des blocs "alors" et "sinon"
Simplification de la condition
L'opérateur ? :
Divers
Variables intermédiaires - opérateurs spécifiques
Vérification des paramètres
Utilisation des constantes
Constante 0 ou NULL ?
Objets sur la pile, objets en mémoire dynamique
Constructeurs
Test d'égalité
Affectation

Organisation des fichiers

Structure d'un fichier source

    Afin de permettre une extraction automatique de documentation, tout fichier source doit obligatoirement commencer par des commentaires ayant rigoureusement le format suivant :
 
/**
*
* @File : tp_01.cxx
*
* @Authors : M. Laporte
*            D. Mathieu
*
* @Date : 28/12/99
*
* @Version : V1.0
*
* @Synopsis : Exemple d'entete de fichier
*             qui peut se continuer sur plusieurs lignes
*
* @SeeAlso : tp_02.cxx, cours C++
*
* @Bugs : indiquer les erreurs connues 
*         et non encore corrigees
*
* @ToDo : indiquer ce qui reste a faire
*
* @Modified :
*    @@Authors  : M. Laporte
*    @@Date     : 27/12/99
*    @@Synopsis : correction de la date de creation ...
*
* @Modified :
*    @@Authors  : D. Mathieu
*    @@Date     : 29/12/99
*    @@Synopsis : correction de la correction de M. Laporte ...
*
**/

    Les parties en gras sont obligatoires. Les parties en italiques sont à remplir par vous-même.

Sommaire

Structure d'un fichier inclus .h - Directives d'inclusion conditionnelle

    Tout fichier .h doit les contenir. Elles doivent inclure la totalité du code du fichier (donc commencer immédiatement après les commentaires de début de fichier). Elles doivent utiliser une constante symbolique construite à partir du nom de fichier en MAJUSCULES, précédé et suivi de deux '_' (caractère souligné).  Le '.' est également remplacé par un '_'.
 
/**
*
* @File : nsSysteme.h
*
* @Authors : M. Laporte
*            D. Mathieu
*
* @Date : 28/12/99
*
* @Version : V1.0
*
* @Synopsis : espace de noms nsSysteme contenant les fonctions systeme et ..
*
**/

#if !defined __NSSYSTEME_H__
#define __NSSYSTEME_H__

// totalite du code

#endif // __NSSYSTEME_H__

Sommaire

Imbrication de fichiers inclus

    Lorsque, dans un fichier source f.h ou f.cxx, une ligne de code utilise une déclaration contenue dans un fichier xxx.h, celui-ci doit obligatoirement être directement inclus. Considérons l'exemple suivant :     Dans le fichier exo_01.cxx, le fichier csignal est inclus deux fois : directement et indirectement par l'intermédiaire de trait.h. C'est la raison pour laquelle il est impératif que chaque fichier inclus ait les directives d'inclusion conditionnelles rappelées ci-dessus. Une autre solution serait de ne pas inclure directement <csignal> dans exo_01.cxx, sachant qu'il est inclus indirectement. L'expérience prouve que, à terme, on ne sait plus où on en est, car il y a parfois deux, trois ou même plus de niveaux d'imbrication des fichiers inclus et la moindre modification devient catastrophique.

Sommaire

Présentation

Indentation

    Elle doit être de 4 caractères :
 
void Fct (...)
{
    int i;
    {
        // bloc interne
        int j;
        //...
    }
} // Fct()

    Dans certains cas, on pourra utiliser une demi-indentation de 2 caractères : case dans une instruction switch,
 
switch (cond)
{
  case '1' :
    ...
    break;

  case '2' :
    ...
    break;

}

private , protected ou public dans une classe :
 
class CX
{
  private :
    ...

  protected :
    ...

  public :
    ...

};

Sommaire

Commentaires

    Ce paragraphe ne concerne que les commentaires destinés à la documentation interne au fichier (explications destinées à faciliter la relecture et la compréhention du code).

    Utilisez de préférence les commentaires //. Réservez les commentaires entre /* */ soit pour isoler une partie de ligne :
 
void Fct (const int n, const int Structure /* = 0 */)
...

ou une partie de programme (attention à ne pas les imbriquer) :
 
/*
    Plusieurs lignes,
    par exemple des explications
    en début de fichier
*/

ou
 
/*
    Risque d'erreur, par exemple :

    void Fct (const int n, const int Structure /* = 0 */)

*/

    Eviter si possible des commentaires en fin de ligne de code, aligner les commentaires sur le code pour respecter l'indentation, séparer les commentaires du code par une ligne vierge au-dessus et au-dessous.

    Limiter les commentaires :

    Pour le reste, essayer de choisir des identificateurs les plus parlants.

Sommaire

Aération

    Un listing doit pouvoir être parcouru en lecture rapide, pour situer rapidement une partie recherchée. N'hésitez pas à sauter une ligne de temps en temps pour séparer deux "paragraphes". De même, mettez des espaces sur une ligne, pour encadrer les opérateurs, après chaque virgule, avant une parenthèse ouverte, après une parenthèse fermée sauf si elle est suivie d'une ponctuation, etc ..., bref, suivez les règles habituellement utilisée en typographie :

cettephraseestaussidifficilealirequecellequivasuivrecarchaquemotoutokenestcolléauxautres!!!
 
if((n=waitpid(process_id,status_location,option))==-1)

qui est beaucoup moins agréable que :
 
if (-1 == (n = waitpid (ProcessID, StatusLocation, Option))) // voir aussi Tests d'égalité

Sommaire

Listings

    Lorsque des listings doivent être rendus sur papier, ils doivent être édités en utilisant une police de caractères non proportionnelle (par exemple MS LineDraw - attention : pas de caractères accentués - ou Courier New). Evitez absolument les polices proportionnelles ou étroites.

    Dans la mesure du possible, la définition d'une fonction doit tenir sur une page. Si elle dépasse une page, n'hésitez pas à mettre certaines parties sous forme de fonctions (inline par exemple). N'imprimez pas une fonction à cheval sur deux pages. Si c'est inévitable, choisissez l'endroit de la coupure astucieusement (pas en plein milieu d'une structure alternative par exemple).

    Lorsque vous saisissez les programmes à l'écran, pensez que les lignes longues risquent d'être coupées sur l'imprimante, terminées au début de la ligne suivante, en rompant ainsi l'indentation. Dès la saisie, ne dépassez pas les colonnes 70 ou 75.

Sommaire

Identificateurs

Identificateurs des variables et des fonctions

    Le choix des identificateurs est une opération primordiale pour la lisibilité: trop longs, ils ralentissent la lecture, sont fastidieux et prennent trop de place sur une ligne; trop courts, ils ne ressemblent plus qu'à des onomatopées impossibles à prononcer et dénuées de sens. C'est le bon choix d'un identificateur qui rend superflu un commentaire. Sommaire

Identificateurs des classes

    Faire précéder le nom de la classe (qui doit commencer par une majuscule) d'un C majuscule :
 
class CRationnel
{
    //

Sommaire

Identificateurs des paramètres formels des constructeurs, des modifieurs, ou d'autres fonctions membres de classes

    Lorsque des paramètres formels de fonctions membres servent à initialiser des données membres, utiliser le même identificateur que celui de la donnée membre correspondante, sans le m_ :
 
class CDate
{
    unsigned m_An;
    unsigned m_Mois;
    unsigned m_Jour;

   public :
    CDate (const unsigned An,
           const unsigned Mois,
           const unsigned Jour);

    void SetAn (const unsigned An);
    //
}; // CDate

Sommaire

Identificateurs des accesseurs et des modifieurs

    Les identificateurs des accesseurs et des modifieurs des données membres d'une classe doivent être construits à partir de l'identificateur de la donnée membre, préfixé par Get ou Set, sans le m_ :
 
class CDate
{
    // voir plus haut

    unsigned GetAn   (void) const;
    unsigned GetMois (void) const;
    unsigned GetJour (void) const;

    void SetAn   (const unsigned An);
    void SetMois (const unsigned Mois);
    void SetJour (const unsigned Jour);

    //

}; // CDate

Sommaire

Identificateurs des espaces de noms

    Faire précéder le nom de l'espace des noms des lettres ns :
 
namespace nsGraph 
{
    //

ou nsSysteme, nsImprim, etc...

Sommaire

Identificateurs des données membres

    Faire précéder le nom de la donnée membre des caractères m_ :
 
class CTimer
{
    CDelai m_Delai;

Sommaire

Identificateurs des constantes

    Afin de les reconnaître facilement, les identificateurs des constantes devraient commencer par le préfixe Cst :
 
const unsigned CstSizeMax = 100;
enum {CstNoError, CstPileVide, CstPilePleine, CstMemoireInsuffisante};

    Les littéraux numériques sont à éviter le plus possible dans les différents cas d'une instruction switch. Préférer des constantes comme par exemple :
 
enum {CasAdd = 'A', CasSubtr = 'S', CasModif = 'M'};
//...
switch (Choix)
{
  case CasAdd   :
    Add (...);
    break;

  case CasSubtr :
    Subtr (...);
    break;

}

    Cette convention est intéressante à plusieurs titres, en particulier pour les comparaisons d'égalité.

Sommaire

Identificateurs des prédicats ou variables booléennes

    Les identificateurs de prédicats sont souvent plus immédiatement lisibles s'ils sont préfixés de Est ou Is :
 
IsAlphabetic (c)

Sommaire

Accolades

    Deux arrangements d'accolades sont autorisés, choisissez celui qui vous convient le plus et tenez-vous à ce choix !

Présentation "K&R" (Kernighan and Richie)

if (...) {
    // suite des
    // instructions
}

ou
 
for (...) {
    // suite des
    // instructions
}

ou
 
class CClass {
    // suite des
    // instructions

}; // CClass

Sommaire

Présentation "classique"

    Les deux accolades sont alignées verticalement :
 
if (...)
{
    // suite des
    // instructions
}

ou
 
for (...)
{
    // suite des
    // instructions
}

ou
 
class CClass
{
    // suite des
    // instructions

}; // CClass

Sommaire

Terminaison des classes, des fonctions, et des espaces de noms

    Toujours rappeler en fin de classe, de fonction ou d'espace de noms, leur identificateur :
 
class CClass
{
    // suite des
    // instructions

}; // CClass

ou
 
void Projeter (...)
{
    int i;
    //...

} // Projeter()

ou
 
namespace nsGraph
{
    int i;
    //...

} // nsGraph

Exception : les fonctions qui tiennent sur une seule ligne :
 
void CDate::SetAn (const unsigned An) { m_An = An; }

void Derout (int) {}

Sommaire
 

Déclarations et définitions

Déclarations et définitions des classes

    Chaque classe doit être déclarée dans un fichier séparé, d'extension .h, et de nom identique à l'identificateur de la classe :
 
// fichier CElement.h

#if !defined __CELEMENT_H__
#define __CELEMENT_H__

class CElement
{
    // ...
    CElement (...);

}; // CElement

#endif // __CELEMENT_H__

    Cas particuliers : on regroupe parfois toute une hiérarchie de classes dans un même fichier, qui a le nom de la classe mère.

    Les définitions des fonctions membres et des opérateurs surchargés ne doivent pas apparaître dans la déclaration (trop lourd au fur et  mesure que la classe se développe). Si les définitions des méthodes sont courtes (moins de 3 ou 4 lignes par exemple), ou si la classe est générique, leur code doit être placé en inline dans un fichier de même nom et d'extension .hxx, lui-même inclus dans le fichier .h, et possédant les directives d'inclusion conditionnelle :
 
// fichier CElement.h

#if !defined __CELEMENT_H__
#define __CELEMENT_H__

class CElement
{
    // ...
    CElement (...);

}; // CElement

#include "CElement.hxx"

#endif // __CELEMENT_H__

et
 
// fichier CElement.hxx

#if !defined __CELEMENT_HXX__
#define __CELEMENT_HXX__

#include "CElement.h"

inline CElement::CElement (...)
{
    // ...

} // CElement ()
// ...

#endif  // __CELEMENT_HXX__

    Dans le cas contraire, leur code doit être placé dans un fichier de même nom et d'extension .cpp ou .cxx (selon le compilateur).

Sommaire

Déclaration et définition des namespace's

    Chaque espace de noms doit être déclarée dans un fichier séparé, d'extension .h, et de nom identique à l'identificateur de l'espace :
 
// fichier nsBAO.h 

#if !defined __NSBAO_H__
#define __NSBAO_H__

namespace nsBAO
{
    // ...
    void FctOutil (void);

} // nsBAO

#endif // __NSBAO_H__

    Les définitions des fonctions et des opérateurs surchargés ne doivent pas apparaître dans la déclaration (trop lourd au fur et  mesure que l'espace de noms se développe). Si les définitions des fonctions sont courtes (moins de 3 ou 4 lignes par exemple),  si elles sont génériques ou si elles sont très rarement appelées, leur code doit être placé en inline dans un fichier de même nom et d'extension .hxx, lui-même inclus dans le fichier .h, et possédant les directives d'inclusion conditionnelle :
 
// fichier nsBAO.h 

#if !defined __NSBAO_H__
#define __NSBAO_H__

namespace nsBAO
{
    // ...

} // nsBAO

#include "nsBAO.hxx"

#endif // __NSBAO_H__

et
 
// fichier nsBAO.hxx 

#if !defined __NSBAO_HXX__
#define __NSBAO_HXX__

#include "nsBAO.h"

namespace nsBAO
{
    // ...
    inline void FctOutil (void)
    {
        // ...

    } // FctOutil()

} // nsBAO

#endif // __NSBAO_HXX__

    Cette écriture a l'inconvénient de nécessiter une indentation de la totalité du code inclus dans l'espace des noms. Elle présente aussi le danger de surcharger involontairement une fonction de profil différent déclarée dans le fichier .h. On peut lui préférer :
 
// fichier nsBAO.hxx 

#if !defined __NSBAO_HXX__
#define __NSBAO_HXX__

#include "nsBAO.h"

inline void nsBAO::FctOutil (void)
{
        // ...

} // FctOutil()

#endif // __NSBAO_HXX__

    Le danger de cette seconde écriture est d'oublier le préfixage de la fonction par nsBAO::

Sommaire

Déclaration et définition des fonctions

    Tous les paramètres données doivent être précédés du mot réservé const, sauf si certaines fonctions C mal prototypées doivent elles-mêmes être appelées. Les éventuelles valeurs par défaut doivent apparaître dans la déclaration. En revanche elles sont interdites dans la définition. Cependant, vous devrez les indiquer en commentaires dans la définition (à la lecture du corps de la fonction, il est important de se souvenir des valeurs par défaut des paramètres). Bien qu'ils soient facultatifs, les identificateurs des paramètres formels devront être présents dans la déclaration des fonctions (seul le type est exigé par le compilateur). Tous les paramètres données d'un autre type qu'un type de base (int, short, etc...) doivent être passés par référence :
 
// fichier f.h
// ...
namespace nsPrive
{
    void Fct (const int n, const int Structure = 0);
    void TransfererImage (const CImage & Image, ...);

} // nsPrive

// fichier f.cxx

#include "f.h"

void nsPrive::Fct (const int n, const int Structure /* = 0 */)
{
   // ...

} // Fct()

Sommaire

Schéma alternatif

    Les indications ci-dessous peuvent parfois être contradictoires. Dans ce cas, il est conseillé d'appliquer les différentes règles dans l'ordre dans lequel elles sont présentées.

Rupture de séquence

    Lorsqu'un des deux blocs "alors" ou "sinon" se termine par une rupture de séquence (throw ...; ou return ...; ou exit (...);), placer ce bloc en tête (en inversant éventuellement la condition) et supprimer le bloc "sinon".  Par exemple :
 
if (EstValide (Param))
{
    cout << "paramètre valide"   << endl;
    TraiterSuite ();
}
else
{
    cout << "paramètre invalide" << endl;
    return false;
}

doit être remplacé par :
 
if (!EstValide (Param))
{
    cout << "paramètre invalide" << endl;
    return false;
}
cout << "paramètre valide"  << endl;
TraiterSuite ();

Sommaire

Déséquilibre des blocs "alors" et "sinon"

    Si le bloc "sinon" est beaucoup plus court que le bloc "alors" (1 ou 2 lignes contre une dizaine au moins), les permuter en inversant la condition.

Sommaire

Simplification de la condition

    Lorsqu'un schéma alternatif comporte les blocs "alors" et "sinon", choisir la condition de test la plus simple (par exemple en évitant une négation), quitte à inverser les contenus des blocs.  Par exemple :
 
if (!EstValide (Param))
    cout << "paramètre invalide" << endl;
else
    cout << "paramètre valide"   << endl;

devrait être remplacé par :
 
if (EstValide (Param))
    cout << "paramètre valide"   << endl;
else
    cout << "paramètre invalide" << endl;

Sommaire

L'opérateur ? :

    Lors de l'évaluation d'une expression, utiliser chaque fois que cela est possible l'opérateur ? : plutôt que l'instruction
if () ...; else par exemple :
 
cout << "résultat : x " << ((x > 0.0) ? "positif"
                                      : "négatif ou nul") << endl;

au lieu de :
 
cout << "résultat : ";
if (x > 0.0)
    cout << "x positif";
else
    cout << "x négatif ou nul";
cout << endl;

ou encore :
 
printf ("Valeur absolue de %d = %d\n", x, (x >= 0.0) ? x : -x);

au lieu de :
 
int xProv;
if (x < 0)
    xProv = -x;
else
    xProv = x;
printf ("Valeur absolue de %d = %d\n", x, xProv);

Sommaire

Divers

Variables intermédiaires - opérateurs spécifiques

    Eviter de déclarer des variables intermédiaires inutiles. On peut les utiliser pour rendre le programme lisible au moment de la mise au point, mais elles doivent être éliminées ultérieurement.  De la même façon, utiliser les possibilités du C/C++ au maximum : opérateurs ++ et --, (en préférant la pré-incrémentation/décrémentation ++i à la post-incrémentation/décrémentation i++),  variable entière non nulle équivalente à VRAI, etc... Par exemple, le programme suivant, qui affiche à l'envers les n valeurs d'un tableau Tab et réinitialise n à 0 :
 
for (int i = n; i > 0; i--) cout << Tab [i - 1];
n = 0;

sera avantageusement remplacé par :
 
for (; n; ) cout << Tab [--n];

De même :
 
i = i + 3;

sera avantageusement remplacé par :
 
i += 3;

Sommaire

Vérification des paramètres

    Lorsqu'une commande supporte ou nécessite des paramètres au lancement, la première chose que doit faire le programme est de vérifier leur validité :
 
int main (int argc, char * argv [])
{
    // vérifier argc et éventuellement argv
    // suite

} // main()

Sommaire

Utilisation des constantes

    Les constantes explicites (littéraux numériques) sont à éviter le plus possible : elles doivent être remplacées par des macros (c'est du C, à éviter en C++) ou mieux, par des constantes symboliques. Cette "paramétrisation" des programmes permet une maintenance beaucoup plus facile. Par exemple :
 
char Tab[12];

à remplacer par :
 
#define LONG_TAB 12
char Tab [LONG_TAB];

ou, encore mieux :
 
const int CstLgTab = 12;
char Tab [CstLgTab];

    En principe, seules les constantes 0, 1, -1 devraient apparaître en clair dans un programme.

Sommaire

Constante 0 ou NULL ?

    Contrairement à ce qui est demandé ci-dessus, la constante 0 doit être préférée à la macro NULL en C++, en raison de possibles définitions multiples de cette dernière dans de multiples fichiers. Il faut en effet rappeler que le C++ sait toujours transtyper et convertir la constante universelle entière 0, mais pas forcément la constante symbolique NULL, peut-être elle-même déjà transtypée.

Sommaire

Objets sur la pile, objets en mémoire dynamique

    Un objet créé sur la pile est présent en mémoire depuis le moment où il est défini jusqu'à la sortie du bloc qui le contient. Les raisons de créer un objet en mémoire dynamique plutôt que sur la pile d'exécution sont :     Dans tous les autres cas, il faut utiliser la pile d'exécution.

Exemples :
 
const int CstMaxTab = 128;
//    ...
void f (void)
{
    CX * Tab = new char [CstMaxTab]; // absurde !!!
    //...
    delete [] Tab;                       // risque d'être oublié !!!

} // f()

mais :
 
CX * f (void)
{
    CX Tab [CstMaxTab];
    CX * PtrTab = Tab;
    return PtrTab;       // une horreur : renvoie l'adresse d'une zone
                         // détruite lors du retour de la fonction
} // f()

et
 
void f (void)
{
    int Lg;
    cin >> Lg;
    CX * Tab = new CX [Lg];    //  incontournable   !!!
    // ...
    delete [] Tab;                 //  à ne pas oublier !!!
} // f()

    Pour libérer la mémoire le plus vite possible :
 
void f (void)
{
    CX * Tab = new CX [CstMaxTab];
    // ...
    delete [] Ligne;
    // suite de la fonction

} // f()

mais, plus efficace :
 
void f (void)
{
    {
        CX Tab [CstMaxTab];
        // ...
    }   // l'objet ligne est détruit ici
    // suite de la fonction
 

} // f()

Sommaire

Constructeurs

       Utiliser le plus possible la phase d'initialisation du constructeur plutôt que des affectations. Le code est beaucoup plus efficace. Par exemple, pour la classe suivante :
 
class CIndividu
{
    CDate m_DNaissance;
    CDate m_DEmbauche;
    int   m_Taille;

  public :
    CIndividu (const CDate & DNaissance,
               const CDate & DEmbauche,
               const int   Taille);

}; // CIndividu

   le constructeur :
 
inline CIndividu::CIndividu (const CDate & DNaissance,
                             const CDate & DEmbauche,
                             const int   Taille);
    : m_DNaissance (DNaissance),
      m_DEmbauche  (DEmbauche),
      m_Taille     (Taille) {}

}; // CIndividu

est à préférer à :
 
inline CIndividu::CIndividu (const CDate & DNaissance,
                             const CDate & DEmbauche,
                             const int   Taille)
{
     m_DNaissance = DNaissance;
     m_DEmbauche  = DEmbauche;
     m_Taille     = Taille;

} // CIndividu()

Sommaire

Test d'égalité

    Lors d'un test d'égalité, placer à gauche de l'opérateur l'opérande qui n'aurait pas le droit d'être à gauche d'une affectation, s'il y en a un. Par exemple
 
if (3 == x)

est à préférer à :
 
if (x == 3)

car la frappe malencontreuse de l'affectation au lieu de la comparaison provoque une erreur de syntaxe. Dans le sens contraire, l'erreur d'exécution est très difficilement détectable.

    Le nommage des constantes indiqué plus haut permet d'appliquer cette règle plus facilement :
 
if (CstNoError == CodeErreur)

Sommaire

Affectation

Le résultat de l'affectation est une valeur qui peut immédiatement être réutilisée
a = b = c = d = 0;

pour un test :
 
if (pid = fork())

à ne pas confondre avec la comparaison, qui n'a rien à voir :
 
if (pid == fork())

Sommaire
 

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique