xlgh gh 6w\oh gx /dqjdjh

Transcription

xlgh gh 6w\oh gx /dqjdjh
*XLGHGHVW\OHGXODQJDJH&
*XLGHGH6W\OH
GX/DQJDJH&
&\ULO&DPELHQ
F\ULO#FDPELHQFRP
ZZZFDPELHQFRP
*XLGHGH6W\OHGX/DQJDJH&
SOMMAIRE
1
2
Introduction ..................................................................................... 1
Mise en page du code ...................................................................... 3
Règles d’indentation............................................................................................................... 3
Privilégier les petites fonctions .............................................................................................. 4
Eviter les fonctions trop larges............................................................................................... 5
Aérer le code .......................................................................................................................... 6
Alléger le code........................................................................................................................ 7
3
Règles de nommage......................................................................... 8
Nommage en général.............................................................................................................. 8
Nommage des fonctions ......................................................................................................... 9
Nommage des macros, des types et des constantes................................................................ 9
Nommage des variables, paramètres et champs ................................................................... 10
4
Déclarations ................................................................................... 12
Déclarations des constantes.................................................................................................. 12
Déclarations des types avec typedef................................................................................ 12
typedef pour les types standards.................................................................................. 12
typedef pour les structures, les unions et les types énumérés...................................... 12
typedef pour les pointeurs de fonctions....................................................................... 12
Ne jamais utiliser #define à la place de typedef ..................................................... 13
Déclarations des fonctions.................................................................................................... 13
Déclarations des variables .................................................................................................... 15
Déclaration des variables.................................................................................................. 15
Initialisation des variables ................................................................................................ 15
Déclaration des tableaux .................................................................................................. 15
Variables globales ............................................................................................................ 16
Utilisation d’une variable pour deux buts différents ........................................................ 16
Utilisation de const ........................................................................................................... 16
5
Structures de contrôle .................................................................... 18
Attention aux points-virgules ............................................................................................... 18
Le else dans les if imbriqués........................................................................................... 18
Les tests monstrueux ............................................................................................................ 19
Le switch .......................................................................................................................... 20
La boucle infinie................................................................................................................... 21
Les modifications intempestives des indices de boucles...................................................... 22
Utilisation du goto ............................................................................................................. 22
Les goto inutiles................................................................................................................. 25
6
Fichiers d’en-tête (includes).......................................................... 27
Factoriser les définitions ...................................................................................................... 27
Pas de chemins absolus ........................................................................................................ 27
Guillemets ou <> ? ............................................................................................................... 27
Utiliser les prototypes définis dans les includes................................................................... 27
Prévenir la double inclusion et prévoir C++ ....................................................................... 27
Des includes « autonomes » ................................................................................................. 28
ii
Sommaire
7
Gestion des erreurs en C................................................................ 29
Détection et traitement des erreurs .......................................................................................29
Utilisation de errno............................................................................................................32
Cast des appels de fonctions en void..................................................................................33
8
Stratégies pour aider à la mise au point ........................................ 34
Les assertions ........................................................................................................................34
Utiliser les warnings .............................................................................................................35
Libération des ressources......................................................................................................36
Utilisation d’outils de vérification ........................................................................................36
9
Problèmes de portabilité................................................................ 37
NULL....................................................................................................................................37
Opérateurs de décalage .........................................................................................................37
Commentaires à la C++ ........................................................................................................37
Taille des types entiers..........................................................................................................37
Ordre des octets à l’intérieur d’un entier ..............................................................................38
Préférez les fonctions standards............................................................................................38
Alignements dans les structures............................................................................................39
10 Bugs Classiques............................................................................. 40
Les macros (1).......................................................................................................................40
Les macros (2).......................................................................................................................41
Les macros (3).......................................................................................................................41
La vérité est ailleurs ..............................................................................................................42
= n’est pas ==........................................................................................................................42
Commentaire mal fermé .......................................................................................................43
C est gourmand .....................................................................................................................43
a[i] = i++; ne fonctionne pas !.....................................................................................44
Ordre d’évaluation des sous expressions ..............................................................................44
Ordre d’évaluation des paramètres .......................................................................................45
int a = 1000, b = 1000; long c = a * b; ne fonctionne pas ! ..................................................45
Gérer les dépassements de capacité ......................................................................................45
Ne pas confondre *p++ et (*p)++ ....................................................................................46
Tableau de pointeurs ou pointeur de tableaux ? ...................................................................46
Pointeur constant ou constante pointée ? ..............................................................................46
Distinction pointeurs/tableaux ..............................................................................................47
Précision du calcul flottant....................................................................................................47
Fonction retournant une valeur allouée sur la pile................................................................48
11 Optimisations................................................................................. 50
Optimisation vs Lisibilité......................................................................................................50
Cibler les optimisations ........................................................................................................50
Valider les optimisations ......................................................................................................50
Quelques techniques .............................................................................................................50
Sortir les calculs des boucles ............................................................................................50
Inversion boucle/test .........................................................................................................51
Optimisation de la condition de sortie de boucle..............................................................51
Fusion de boucle ...............................................................................................................51
Déroulage de boucles (1) ..................................................................................................52
Déroulage de boucles (2) ..................................................................................................52
Inversion de boucles imbriquées.......................................................................................52
iii
*XLGHGH6W\OHGX/DQJDJH&
Tester en premier les cas les plus courants....................................................................... 53
Tester en premier les cas les moins coûteux à tester........................................................ 54
Utiliser un switch à la place d’un if ........................................................................... 54
Utilisation des pointeurs pour accéder aux tableaux ........................................................ 55
Précalcul dans des tables .................................................................................................. 56
Utilisation de memset, memcpy et memmove avec les tableaux .................................. 57
Enlever les register .................................................................................................... 58
12 Réalisation de Composants Logiciels ........................................... 59
Qu’est ce qu’un composant logiciel ? .................................................................................. 59
Nommage des fonctions et types exportés par une librairie................................................. 59
Le principe de la "boîte noire".............................................................................................. 59
Comment cacher les structures de données .......................................................................... 60
Hétérogénéité des runtimes C entre les modules.................................................................. 61
Implémentation différentes des structures........................................................................ 62
L'allocateur mémoire........................................................................................................ 63
Le futur des composants logiciels ........................................................................................ 64
13 Curiosités du C .............................................................................. 65
a[b] équivalent à b[a]............................................................................................................ 65
Le dispositif de Duff (Duff’s device) ................................................................................... 65
Les séquences Trigraph ........................................................................................................ 66
14 Bibliographie ................................................................................. 67
iv
*XLGHGHVW\OHGX/DQJDJH&
1
INTRODUCTION
Le langage C est très « proche de la machine » et peut être vu comme une sorte de langage
assembleur universel : les notions de mémoire globale, de pile, de pointeurs sont essentiels
pour développer en C. Ceci fait de C est langage très efficace et aussi très puissant et ce n’est
pas un hasard si les systèmes d’exploitation sont généralement écrits en langage C. D’un autre
coté, comme le langage C n’est pas un langage de très haut niveau, ce n’est pas un langage
facile car le développeur est responsable de beaucoup de chose. Par exemple, en langage Java,
un développeur n’a jamais à se préoccuper de libérer de la mémoire allouée devenue inutile,
car Java prévoit un dispositif, le « ramasseur de miettes » (garbage collector) qui va récupérer
cette mémoire automatiquement. En C, c’est le développeur qui est responsable de cette
mémoire et qui doit la libérer explicitement et, s’il ne le fait pas, cela a une répercussion sur
les performances du programme.
Q : Alors pourquoi développer en langage C si des langages comme Java peuvent
m’éviter d’avoir à gérer les problèmes de gestion mémoire ?
R : Cette gestion plus perfectionnée de la mémoire à un coût non négligeable en
temps de calcul puisque le système doit déterminer de lui-même quelle est la mémoire
qui n’est plus utilisée par le programme. C’est une des raisons pour lesquelles un
programme Java ne sera jamais aussi efficace qu’un programme C.
L’efficacité du C se fait aussi au prix d’un manque total de contrôle de la validité des
opérations effectuées lors de l’exécution d’un programme. Par exemple, dans le code suivant :
void ModifieCaractere(char *string,char value, int index)
{
string[index]=value ;
}
void Bug()
{
char toto[]= "bonjour" ;
ModifieCaractere(toto, ‘a’, 2000) ;
}
aucun mécanisme n’avertit que l’on accède au 2001ième élément d’un tableau ne comportant
que 8 éléments. Par conséquent, le comportement du programme est ici absolument
imprévisible : il peut « planter » ou avoir un comportement aberrant.
Q : Je développe un programme interactif en PowerBuilder. Certes, C est très
efficace, mais pourquoi m’embêter à faire du C alors que l’utilisateur est bien plus
lent que son ordinateur ?
R : Sans doute ce type d’application peut être avantageusement développé en
PowerBuilder, mais n’y a-t-il pas dans cette application un traitement qui fait
attendre l’utilisateur ? Dans ce cas, seul ce traitement pourra être développé avec
des fonctions C qui seront appelées depuis PowerBuilder.
1
*XLGHGH6W\OHGX/DQJDJH&
Afin d’obtenir un bon niveau de qualité logicielle, le développeur C doit redoubler de rigueur
et déjouer tous les pièges. Ce guide propose des méthodes pour développer :
•
•
•
•
un code plus robuste dans lequel les erreurs sont évitées d’entrée de jeu ou, au pire,
détectées le plus rapidement possible
un code plus lisible car un code clair permet de localiser les problèmes plus facilement
un code plus portable pour éviter de devoir (trop) réviser un programme lors d’un
changement de machine ou de système d’exploitation.
un code plus optimisé pour profiter au mieux de l’efficacité du C
Ce guide de style n’est pas un cours ni un manuel de référence sur le langage C, mais il
s’adresse au contraire aux développeurs C, y compris aux experts, afin qu’ils améliorent la
qualité de leurs applications.
2
*XLGHGHVW\OHGX/DQJDJH&
2
MISE EN PAGE DU CODE
Règles d’indentation
Il existe plusieurs types d’indentation du code C et les partisans de chaque méthode
s’affrontent lors de terribles guerres de religion. En fait, le plus important est d’adopter un
style unique au sein d’un même groupe de travail et de le respecter.
Si vous n’avez pas encore choisi un style particulier, vous pouvez utiliser les règles
suivantes :
-
Les accolades du corps d’une définition de fonction sont toujours isolées sur la colonne de
gauche.
-
Les autres accolades ouvrantes, situées après les mots-clés if, while, for, do, else
ou switch, sont toujours en fin de ligne, sur la même ligne que le mot-clé.
-
Les accolades fermantes correspondantes sont alignées en dessous de la première lettre du
mot-clé correspondant. Elles sont isolées sur leur ligne sauf :
- dans le cas d’une fin de bloc if suivi par un else.
- dans le cas d’une fin de bloc do suivi par son while.
-
Des if/else en cascade peuvent se mettre les un sous les autres
Quelques exemples :
3
*XLGHGH6W\OHGX/DQJDJH&
int ExempleIndentation()
{
if (<test>) {
while (<test>) {
instruction;
for (i=0 ;i<n ;i++) {
instruction;
instruction;
}
instruction;
}
} else if (<test> {
do {
switch (<valeur>) {
case 1 :
instruction;
instruction;
break;
case 2 :
instruction;
break;
default :
instruction;
}
} while (<test>);
} else {
instruction;
instruction;
}
}
Q : J’ai appris à programmer en langage Pascal et je n’aime pas les accolades ;
alors comme je suis nostalgique, je me suis défini ces deux macros :
#define begin {
#define end }
Sympa non ?
R : NON ! Pensez aux développeurs C qui travaillent avec vous s’ils doivent un jour
reprendre votre code à la sauce Pascal…
Privilégier les petites fonctions
Eviter les fonctions trop longues (qui comporte trop de lignes) ou trop larges (trop de
colonnes).
Idéalement une fonction doit tenir sur 25 lignes ; après il faut songer à découper la fonction en
plusieurs fonctions.
Q : Ma fonction fait 5 pages, mais elle fonctionne bien. Alors pourquoi la découper ?
R : Pensez au jour où, dans quelques années, vous ou l’un de vos collègues devra
modifier ou corriger votre fonction. Il faudra alors relire dans le détail ces 5 pages de
code… Ne vaut-il mieux pas prendre 5 minutes aujourd’hui pour découper la
fonction ?
4
*XLGHGHVW\OHGX/DQJDJH&
Eviter les fonctions trop larges
Eviter les fonctions trop larges (qui comporte trop de colonnes).
Idéalement une fonction doit tenir sur 80 colonnes ; après il faut « couper » les lignes
astucieusement.
Exemple :
ans = (exp(ax)/sqrt(ax)) *
(0.39 +
y * (0.13 +
y * (0.22e-2 +
y * (-0.15 +
y * (0.91 +
y * (-0.20 + y * 0.26) ))))));
plutôt que :
ans = (exp(ax)/sqrt(ax))*(0.39+y*(0.13+y*(0.22e-2+
y*(-0.15+y*(0.91+y*(-0.20+y*0.26)))))));
Les prototypes de fonctions avec de nombreux paramètres peuvent être coupés après le type
de retour et après chaque virgule :
Exemple :
static AP_STRING ApplyFilter(AP_CSTRING term, AP_LPINT termsize, AP_CSTRING
deb, AP_CSTRING filter, int sizein, int sizeout) ;
Peut s’écrire :
static AP_STRING
ApplyFilter(AP_CSTRING term, AP_LPINT termsize,
AP_CSTRING deb, AP_CSTRING filter,
int sizein, int sizeout) ;
Ou encore mieux, en commentant les paramètres :
static AP_STRING
ApplyFilter(AP_CSTRING term,
AP_LPINT termsize,
AP_CSTRING deb,
AP_CSTRING filter,
int sizein,
int sizeout) ;
/*
/*
/*
/*
/*
/*
Le terme */
La taille du terme */
De’but de ma modif */
Le filtre */
taille filtre d’entrée */
taille filtre de sortie */
Les chaînes de caractères trop longues peuvent facilement être réparties sur plusieurs lignes.
Exemple :
printf ( "Salut les amis, je suis un gentil programme, "
"bien que certains pensent que je suis un peu bavard") ;
5
*XLGHGH6W\OHGX/DQJDJH&
Aérer le code
Utilisez des espaces et des lignes blanches pour aérer le code afin d’augmenter la lisibilité :
-
Mettre un espace après chaque mot-clé
Ne pas coller les accolades avec le reste du code
Mettre des espaces autour d’une sous-expression pour la détacher
Mettre des espaces après les virgules
Mettre des espaces après les points virgules au sein d’un for
Comparez les deux versions :
float chebev(float a,float b,float c[],int m,float x)
{
float d=0.0,dd=0.0,sv,y,y2;
int j;
if((x-a)*(x-b)>0.0)
return 0.0;
y2=2.0*(y=(2.0*x-a-b)/(b-a));
for(j=m-1;j>=1;j--){
sv=d;
d=y2*d-dd+c[j];
dd=sv;
}
return y*d-dd+0.5*c[0];
}
float chebev(float a, float b, float c[], int m, float x)
{
float d=0.0, dd=0.0, sv, y, y2;
int j;
if ( (x-a)*(x-b) > 0.0)
return 0.0;
y2 = 2.0 * ( y = (2.0*x-a-b)/(b-a) );
for (j=m-1; j>=1; j--) {
sv = d;
d = y2*d – dd + c[j];
dd = sv;
}
return y*d – dd + 0.5*c[0];
}
Vous pouvez également insérer des espaces pour aligner des éléments semblables de lignes en
lignes. Exemple :
prixHT=prixUnitaire*quantite; /* Calcul du prix HT des articles */
prixTTC=prixHT*(1+tauxTVA); /* Calcul du prix TTC */
est moins lisible que :
6
*XLGHGHVW\OHGX/DQJDJH&
prixHT = prixUnitaire * quantite ; /* Calcul du prix HT des articles * /
prixTTC = prixHT * (1 + tauxTVA) ; /* Calcul du prix TTC */
Alléger le code
Souvent, un code concis est plus lisible :
• Supprimez les paramètres et accolades inutiles
• Profitez des possibilités du langage
• Utilisez les règles de simplifications mathématiques et logiques
Version « Grasse »
return ( <valeur> ) ;
if (<test>) {
<une intruction >;
}
a = (b*c) + (d/e);
if ((a*b) > (c+d))
if (a>b)
c=a;
else
c=b;
a=i;
i=i+1;
a = a + b;
!a && !b
!a || !b
BOOL EstTropGrand(int index)
{
if (index > MAX_INDEX)
return TRUE;
else return FALSE;
}
Version « Light »
return <valeur> ;
if (<test>)
<une intruction >;
a = b*c + d/e;
if (a*b > c+d)
c= (a>b) ? a : b;
a=i++;
a+=b;
!(a || b)
!(a && b)
BOOL EstTropGrand(int index)
{
return index > MAX_INDEX;
}
Evitez toutefois les raccourcis saisissants qui rendent le code abscons . Comme :
z=x && y++ ; /* Grosse astuce */
au lieu de :
if (x==0)
z=0 ;
else
z=y++ ;
Si vous hésitez entre plusieurs types d’écriture, choisissez toujours ce qui vous semble le plus
lisible.
7
*XLGHGH6W\OHGX/DQJDJH&
3
RÈGLES DE NOMMAGE
De même que pour l’indentation du code C, il n’y a pas de règles absolues concernant le
nommage des identificateurs en C. Encore une fois, le plus important est d’adopter un style
unique au sein d’un même groupe de travail et de le respecter.
Nommage en général
Les noms peuvent être en anglais ou en français à condition d’éviter les mélanges de langue
au sein d’une même application.
Q : Je suis martiniquais et mon île ensoleillée est loin ; alors pour me consoler et me
rappeler le pays, j’aime bien choisir des identificateurs en créole.
R : Le jour où vous ne serez plus là pour maintenir votre code vos collègues auront la
joie d’apprendre le créole.
Les noms choisis doivent être explicites, ni trop longs, ni trop courts. L’emploi d’abréviations
est acceptable pour éviter les identificateurs trop longs :
Trop court ou pas clair
tot
nbf
AfficheResultat
Tri
VE
flag
Trop long
totalDesVentesDeLAnnee
nombreDeFenetresOuvertes
AfficheNombreDeFichiersTrouves
TriClientsParOrdreAlphabetique
TYPE_VECTEUR_D_ENTIERS
etat_Fichier_Bien_Ouvert
Bon
totalVenteAn
nbFenetresOuvertes
AfficheNbFicTrouves
TriClientsAlpha
VECTEUR_INT
ficOuvert
En général on choisira les noms longs et explicites pour les identificateurs qui ont une portée
globale tandis que des noms courts sont acceptables pour des variables locales et des
compteurs de boucles.
Evitez :
• les noms mal orthographiés ou phonétisés pour gagner de la place
clientCouran
AficheMoyene
ipotez
•
d’avoir des noms similaires pour deux variables différentes
numClient,noClient
coord2,coordZ
•
d’avoir des chiffres dans les noms
total1, total2, total3…
•
d’abréger différemment les mêmes mots
nbFenetresOuvertes, nbreFenetresFermees
ficClients, fcrFournisseurs
8
*XLGHGHVW\OHGX/DQJDJH&
Nommage des fonctions
Les noms de fonctions sont toujours en lettres minuscules mais capitalisés. C’est à dire que
les mots à l’intérieur d’un nom commencent par une majuscule.
Par exemple :
Initialiser
DessinerObjet
ChargeFichierClients
CalculeDateDePaques
CreateFunnyButton
On notera que les verbes peuvent être à l’infinitif ou à l’impératif. Toutefois, au sein d’une
même application, on choisira l’infinitif ou l’impératif, mais sans mélanger les deux.
Pour une fonction effectuant un test, le nom reflète le sens de la proposition :
EstUnJourFerie
EstVisible
IsAnEmptyFile
IsValidWindow
Certains développeurs aiment préfixer les fonctions locales à un fichier (déclarées en
static) avec un underscore :
_CalculeTotIntermediaire
_CompareNbOfItems
Nommage des macros, des types et des constantes
Les macros, les types et les constantes sont toujours en lettres majuscules. Les mots à
l’intérieur d’un nom sont séparés par des underscores.
Exemples :
TAILLE_BUFFER
MAX_LINE_SIZE
MEMORY_HANDLE
WINDOW
Certaines personnes préfèrent choisir des noms en minuscules pour les types. Dans ce cas, ils
utilisent le suffixe « _t » pour indiquer qu’il s’agit d’un type.
Exemples :
typedef float coordonnee_t ;
typedef int booleen_t;
9
*XLGHGH6W\OHGX/DQJDJH&
Nommage des variables, paramètres et champs
Les variables et les paramètres sont en minuscules. Les mots à l’intérieur d’un nom
commencent par des majuscules, mais la première lettre est toujours une minuscule.
Exemples :
age
adresseClient
numSecuriteSociale
On rencontre parfois aussi des développeurs préférant n’utiliser que des minuscules en
utilisant l’underscore comme séparateur :
adresse_client
num_securite_sociale
Cette pratique allonge les identificateurs sans vraiment les rendre plus lisibles. En tout état de
cause éviter les identificateurs tout en minuscules sans séparateur:
adresseclient
numsecuritesociale
Certains développeurs préfixent les noms avec un code rappelant son type ou son usage. C’est
la « notation hongroise », inventée par Charles Simonyi. Par exemple « i » préfixe les entiers,
« ch » les caractères, « c » les compteurs, « f » les flags… Le « p » préfixe tous les pointeurs
et se compose avec le préfixe du type pointé. Par exemple :
char chDernierLu;
int cValeurCourante;
int *piLongeur
BOOL *ppfFichierLu
/*
/*
/*
/*
dernier caractère lu */
un compteur */
pointeur sur un entier contenant la longueur */
pointeur sur un pointeur sur un flag
indiquant si le fichier est lu */
Les règles de nommage sont les mêmes pour les noms de champs des struct et union.
Certains développeurs les préfixent toutefois avec « m_ » (le « m » étant l’initiale de
« member ») ou avec un autre préfixe rappelant le nom de la structure.
Exemples :
struct CLIENT {
char
*m_nom ;
char
*m_prenom ;
int
m_age ;
} ;
10
*XLGHGHVW\OHGX/DQJDJH&
typedef struct tagBITMAPINFOHEADER {
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER;
11
*XLGHGH6W\OHGX/DQJDJH&
4
DÉCLARATIONS
Déclarations des constantes
Ne jamais utiliser de constantes en dur dans le code, mais utilisez #define ou const pour
les définir en un endroit unique (généralement dans un fichier d’en-tête).
Déclarations des types avec typedef
typedef pour les types standards
Ne pas hésiter à construire des types plutôt que d’utiliser directement les types standards.
Par exemple, pour une température, plutôt que d’utiliser le type float, définissez :
typedef float temperature_t ;
Ainsi, si vous vous apercevez que le choix de « float » était mauvais (par exemple, pas
assez précis) et que vous voulez utiliser le type « double », il suffit de modifier la
déclaration de type plutôt que de retrouver et de modifier toutes les déclarations de variables
et de paramètres correspondant à des températures.
typedef pour les structures, les unions et les types énumérés
Plutôt que de déclarer chaque variable de type structure en faisant :
struct stToto *monToto ;
Définissez un type pour la structure :
typedef struct stToto TOTO ;
Toutes les déclarations seront plus lisibles :
TOTO *monToto ;
Le même raisonnement s’applique aux unions et aux types énumérés.
typedef pour les pointeurs de fonctions
Lors de manipulation de pointeurs de fonctions, définir avec typedef un type pour chaque
pointeur de fonctions. Ceci permet de clarifier la lecture du code.
Par exemple, la fonction système signal, définie dans signal.h peut se déclarer ainsi :
12
*XLGHGHVW\OHGX/DQJDJH&
void (*signal)(int,void(*)(int)))(int) ;
Ou en déclarant un type fonction , ce qui donne :
typedef void (*SIGNAL_HANDLER)(int) ;
SIGNAL_HANDLER signal(int, SIGNAL_HANDLER) ;
Ne jamais utiliser #define à la place de typedef
Certaines personnes pensent que typedef n’est pas très utile et qu’il est tout à fait valable
de déclarer un type en utilisant #define. Ainsi, à première vue :
typedef char * STRING ;
et :
#define STRING char *
sont équivalents. Mais imaginons maintenant la déclaration suivante :
STRING str1,str2 ;
Si le type a été défini avec une macro, la déclaration va s’expanser ainsi :
char *str1,str2 ;
Au lieu de l’effet recherché :
char *str1,*str2 ;
Déclarations des fonctions
Avant d’être appelée, une fonction doit avoir été déclarée :
•
soit parce qu’elle a été définie plus haut :
double CalculeDelta(double a, double b, double c)
{
return b*b-4*a*c ;
}
int NbreSolutions(double a, double b, double c)
{
double delta=CalculeDelta(a,b,c) ;
if (delta>0.0) return 2 ;
if (delta<0.0) return 0 ;
return 1 ;
}
•
soit parce qu’elle a été déclarée à l’aide d’un prototype
13
*XLGHGH6W\OHGX/DQJDJH&
double CalculeDelta(double a, double b, double c) ;
int NbreSolutions(double a, double b, double c)
{
double delta=CalculeDelta(a,b,c) ;
if (delta>0.0) return 2 ;
if (delta<0.0) return 0 ;
return 1 ;
}
L’ancienne notation :
int NbreSolutions(a, b, c)
double a,b,c;
{
...
}
n’existe plus que pour des raisons de compatibilité ; il faut donc ne plus l’utiliser.
Toujours spécifier le type retourné par une fonction. Si une fonction ne retourne rien, utilisez
« void » :
void AfficheObjet(OBJECT *unObjet) ;
Si vous voulez bien mettre en évidence qu’une fonction n’a pas de paramètre, vous pouvez
aussi utiliser « void » dans la liste des arguments :
DATE *Aujourdhui(void) ;
Si une fonction n’est utilisée que dans le fichier où elle est définie, utilisez static pour la
déclarer.
/* cette fonction est locale au fichier courant */
static int _Square(int x)
{
return x*x ;
}
Eviter qu’une fonction ait un nombre très important de paramètres. Dans ce cas, à la fois pour
des raisons de lisibilité et d’efficacité, il est souhaitable de définir une structure qui va
regrouper les « arguments ».
14
*XLGHGHVW\OHGX/DQJDJH&
Déclarations des variables
Déclaration des variables
Déclarez une variable par ligne de code et commentez la variable à droite de la déclaration,
sur la même ligne.
Initialisation des variables
Initialisez toutes les variables, ne pas compter sur le fait qu’une variable sera initialisée par
défaut à zéro.
Déclaration des tableaux
Lors de la déclaration d’un tableau, n’utilisez pas une taille arbitraire, donnée en dur. Allouez
le nombre exact d’éléments dans un tableau sans ajouter quelques éléments pour tenter de
minimiser le risque d’un débordement de tableau.
char message[500];
/* mauvais */
strcpy(message, "Bonjour ");
strcat(message, nomUtilisateur) ;
AfficheMessage(message);
Faites plutôt :
#define TAILLE_BONJOUR 8
#define MAX_TAILLE_NOM 50
char message[TAILLE_BONJOUR+MAX_TAILLE_NOM+1];
strcpy(message, "Bonjour ");
strcat(message, nomUtilisateur) ;
AfficheMessage(message);
De cette manière, on sait immédiatement comment ajuster la taille d’un tableau si nécessaire.
On peut encore rendre le code de l’exemple plus robuste en ajoutant des assertions (cf. Les
assertions, page 34)
#define MSG_BONJOUR “Bonjour “
#define TAILLE_BONJOUR (sizeof(MSG_BONJOUR)-1)
#define MAX_TAILLE_NOM 50
char message[TAILLE_BONJOUR+MAX_TAILLE_NOM+1];
assert(strlen(MSG_BONJOUR)==TAILLE_BONJOUR);
assert(strlen(nomUtilisateur)<=MAX_TAILLE_NOM);
strcpy(message, MSG_BONJOUR);
strcat(message, nomUtilisateur) ;
AfficheMessage (message);
15
*XLGHGH6W\OHGX/DQJDJH&
Variables globales
Minimisez l’utilisation des variables globales. Utiliser static pour déclarer une variable
globale locale à un fichier.
Si une fonction utilise une variable globale uniquement pour elle, définissez la variable en
static à l’intérieur de la fonction. Par exemple :
int Compteur()
{
static int valeurCompteur=1 ;
return valeurCompteur++ ;
}
Utilisation d’une variable pour deux buts différents
Il est parfois tentant d’utiliser la même variable plusieurs fois pour des usages différents car
cela évite des déclarations multiples et permet d’économiser un peu de mémoire :
/* Calcule le nombre de pages ne’cessaires pour imprimer
le fichier clients */
nb = NbEnregistrements(clients) ;
nb *=tailleLigne ;
if (nb%taillePage == 0)
nb/=taillePage ;
else
nb=nb/taillePage+1 ;
Dans cet exemple la variable au nom vague de « nb » change plusieurs fois de signification.
Ce code aurait été plus clair ainsi :
/* Calcule le nombre de pages ne’cessaires pour imprimer
le fichier clients */
nbLignes = NbEnregistrements(clients) ;
totalTaillesLignes = nbLignes * tailleLigne ;
if (totalTaillesLignes%taillePage == 0)
nbPages = totalTaillesLignes / taillePage ;
else
nbPages = totalTaillesLignes / taillePage + 1;
Utilisation de const
Utilisez const pour déclarer un pointeur sur un objet qui ne sera pas modifié. Ceci est
particulièrement intéressant lorsque l’on définit les paramètres d’une fonction. Par exemple,
considérons le prototype de la fonction standard strcpy qui copie une chaîne de caractères
dans une autre :
char *strcpy( char *s1, const char *s2 ) ;
16
*XLGHGHVW\OHGX/DQJDJH&
Même si on a oublié comment fonctionne cette fonction, un simple coup d’œil au prototype
permet de voir que c’est s2 que l’on copie dans s1 puisque la chaîne s2 n’est pas modifiable.
De plus imaginons le code suivant :
#include <string.h>
#define MAX_STRING 100
typedef struct tagCLIENT {
char
m_nom[MAX_STRING] ;
char
m_prenom[MAX_STRING] ;
int
m_age ;
} CLIENT;
void ChangeNomClient(CLIENT *unClient,const char *nouveauNom)
{
strcpy(nouveauNom,unClient->m_nom) ; /* bug ! ! ! ! */
}
Le développeur a permuté par erreur les arguments du strcpy mais il a eu la sagesse :
• de définir un prototype pour sa fonction ChangeNomClient qui précise que le nouveau
nom passé en paramètre ne peut pas être modifié
• de ne pas utiliser strcpy sans que le prototype de la fonction soit connu grâce à
l’inclusion du fichier string.h
Ainsi, le compilateur signale une erreur qui aurait été bien plus difficile à trouver autrement.
Q : Mon code peut très bien fonctionner sans const, alors pourquoi se casser la
tête ?
R : Un bon développeur fait tout pour détecter les problèmes potentiels le plus tôt
possible (lors de la compilation) et const est l’un des outils fournis par C pour y
parvenir. Il vaut mieux passer un peu de temps à mettre des consts que de passer
une semaine à trouver un bug avec le debugger.
17
*XLGHGH6W\OHGX/DQJDJH&
5
STRUCTURES DE CONTRÔLE
Attention aux points-virgules
Un point virgule après un if, un while ou un for est considéré comme une instruction
vide.
Ainsi :
if (x[i] > big);
big=x[i];
est équivalent à :
if (x[i] > big) {};
big=x[i];
et donc équivalent à :
big=x[i];
Le else dans les if imbriqués
Considérons le code suivant :
if (x==0)
if (y ==0) error();
else {
z = x + y;
f(&z);
}
Contrairement à ce que le développeur a voulu faire, le compilateur rattache le else au
deuxième if et le code, signifie :
if (x==0) {
if (y ==0)
error();
else {
z = x + y;
f(&z);
}
}
En effet, il aurait fallu écrire :
18
*XLGHGHVW\OHGX/DQJDJH&
if (x==0) {
if (y ==0) error();
} else {
z = x + y;
f(&z);
}
Les tests monstrueux
Par exemple, supposons une fonction qui détermine le taux d’assurance d’une personne en
fonction de sa situation :
typedef
typedef
typedef
typedef
typedef
enum {Fumeur,NonFumeur} fumeur_t;
enum {Homme,Femme} sexe_t;
enum {Marie,Celibataire} statut_marital_t;
int age_t ;
double taux_t ;
/* Calcule le taux d’assurance */
taux_t
CalculeTaux(fumeur_t fume, sexe_t sexe, statut_marital_t statut, age_t age)
{
if (fume==Fumeur) {
if (age<18) return 10.0 ;
else
if (age>60 && sexe==Homme) return 40.0 ;
else {
if (status_marital==Celibataire) return 20.0 ;
else {
if (age<40) . . .
etc… etc… etc…
Notre fonction CalculeTaux fait 10 pages et on espère que tous les cas sont bien traités
(comment en être certain ?). Le jour où tout les taux changent, une bonne journée de travail
est nécessaire pour mettre la fonction à jour.
Ce test aurait pu être remplacé par une table :
typedef
#define
typedef
#define
typedef
#define
typedef
typedef
#define
typedef
enum {Fumeur,NonFumeur} fumeur_t;
NB_ETATS_FUMEURS 2
enum {Homme,Femme} sexe_t;
NB_SEXES 2
enum {Marie,Celibataire} statut_marital_t;
NB_STATUTS 2
int age_t ;
enum {enfant=8,ado=18,jeune=39,quadra=59,papy} classe_age_t;
NB_CLASSE_AGE 5
double taux_t ;
19
*XLGHGH6W\OHGX/DQJDJH&
taux_t TableDesTaux[NB_ETATS_FUMEURS][NB_SEXES][NB_STATUTS][NB_CLASSE_AGE];
/* Charge la table des taux depuis un fichier */
int ChargeTableDesTaux()
{
…
}
/* Determine la classe d’age */
classe_age_t DetermineClasseAge(age_t age)
{
if (age<=enfant) return enfant;
if (age<=ado)
return ado;
if (age<=jeune) return jeune;
if (age<=quadra) return quadra;
return papy;
}
/* Calcule le taux d’assurance */
taux_t
CalculeTaux(fumeur_t fume, sexe_t sexe, statut_marital_t statut, age_t age)
{
return TableDesTaux[fume][sexe][statut][DetermineClasseAge(age)];
}
Avec cette version, le calcul est simple et très efficace et de plus tous les taux sont stockés
dans un fichier qui pourra être édité lorsque les taux changeront sans avoir à modifier le code.
Le switch
Toujours avoir une clause « default » dans un switch, même si on suppose que tout les
cas possibles sont traités. Au pire, utiliser assert (cf. Les assertions, page 34) :
switch (codeSexe) {
case ‘M’ :
printf("Masculin”);
break;
case ‘F’ :
printf("Feminin”);
break;
default:
assert(0); /* Cas Impossible */
}
L’oubli d’un break dans une clause case conduit à l’exécution du code de la clause
suivante :
20
*XLGHGHVW\OHGX/DQJDJH&
switch (codeSexe) {
case ‘M’ :
printf("Masculin”);
case ‘F’ :
printf("Feminin”);
default:
assert(0); /* Cas Impossible */
}
Par exemple, l’exécution de ce code avec codeSexe égal à ‘M’ afficherait :
MasculinFeminin
assertion failed : 0, file toto.c, line 153
Si on veut omettre le break intentionnellement, il est souhaitable de mettre un commentaire
pour montrer qu’il ne s’agit pas d’un oubli :
switch (codeSexe) {
case ‘F’ :
printf("Mes hommages, Madame.\n”);
/* On continue... */
case ‘M’ :
/* traitement commun homme/femme*/
printf("Comment-allez vous ?\n”);
default:
assert(0); /* Cas Impossible */
}
Il est possible de déclarer des variables à l’intérieur d’un bloc switch et même de mettre des
instructions avant le premier case :
switch (expr) {
int i=4;
f(i);
case 0:
i=17;
/* On continue... */
default:
printf(“%d\n”,i);
}
Mais le code avant le premier case n’est jamais exécuté. Par conséquent, dans l’exemple, la
variable i n’est pas initialisée à 4 et la fonction f n’est pas appelée.
La boucle infinie
Pour une boucle infinie, utilisez for( ; ;), plus élégant que while(1)
21
*XLGHGH6W\OHGX/DQJDJH&
Les modifications intempestives des indices de boucles
Il faut éviter le modifier les compteurs de boucles à l’intérieur des boucles et également éviter
d’utiliser la valeur d’un compteur en sortie de boucles.
Exemple :
for(i=0;i<NB_CLIENTS;i++) {
if (<condition d’erreur>) {
i=NB_CLIENTS+1; /* pour sortir de la boucle */
} else {
/* traitement */
…
}
}
if (i==NB_CLIENTS+1) /* pour traiter l’erreur */
Dans ce cas, il aurait été plus clair d’avoir :
erreurSurClient=FALSE;
for(i=0;i<NB_CLIENTS;i++) {
if (<condition d’erreur>) {
erreurSurClient=TRUE;
break;
}
/* traitement */
…
}
if (erreurSurClient) /* pour traiter l’erreur */
Utilisation du goto
Utilisez le goto avec parcimonie. Beaucoup de développeurs sont des adversaires résolus du
goto pour des raisons idéologiques alors que dans certains cas, l’utilisation du goto permet
d’avoir un code nettement plus concis et lisible. Le cas typique est l’utilisation du goto pour
sortir de plusieurs structures de contrôle très imbriquées lorsqu’une erreur a été détectée.
Par exemple :
22
*XLGHGHVW\OHGX/DQJDJH&
int UnCalcul()
{
int i,j,k ;
int errorcode =0;
char *buffer ;
buffer=(char *) malloc(MY_BUF_SIZE) ;
if (buffer==NULL)
return –1 ;
for(i=0 ;i<MAXHEIGHT ;i++) {
for(j=0 ;j<MAXWIDTH ;j++) {
for(k=0 ;k<MAXSAMPLE ;k++) {
if (<détection d’erreur …>) {
errorcode=-2 ;
goto error ;
}
…
…
}
…
}
…
}
error :
/* on libe`re le buffer, me^me en cas d’erreur */
free(buffer) ;
return errorcode ;
}
est plus clair (et plus efficace !) que :
23
*XLGHGH6W\OHGX/DQJDJH&
int UnCalcul()
{
int i,j,k ;
int errorcode =0;
char *buffer ;
buffer=(char *) malloc(MY_BUF_SIZE) ;
if (buffer==NULL)
errorcode=-1;
else {
for(i=0 ;i<MAXHEIGHT && !errorcode;i++) {
for(j=0 ;j<MAXWIDTH && !errorcode;j++) {
for(k=0 ;k<MAXSAMPLE && !errorcode;k++) {
if (<détection d’erreur …>) {
errorcode=-2 ;
} else {
…
}
}
if (!errorcode) {
…
}
}
if (!errorcode) {
..
}
}
free(buffer) ;
}
return errorcode ;
}
Lorsque c’est possible, on préférera utiliser continue ou break. Par exemple :
for ( i = 0; i < LENGTH; i++ ) {
for ( j = 0; j < WIDTH; j++) {
if ( lines[i][j] == ’\0’ )
goto stoploop ;
else lengths[i] = j;
}
stoploop : ;
}
est avantageusement replacé par
for ( i = 0; i < LENGTH; i++ ) {
for ( j = 0; j < WIDTH; j++) {
if ( lines[i][j] == ’\0’ )
break ;
else lengths[i] = j;
}
}
Toutes les techniques exposées ici :
• utilisation du goto
• utilisation de break ou continue
• return anticipé (un return qui n’est pas la dernière instruction d’une fonction)
24
*XLGHGHVW\OHGX/DQJDJH&
sont en contradiction avec les principes de la programmation structurée et il faut donc
toujours les utiliser avec précaution et uniquement lorsqu’elles permettent un gain substantiel
concernant la lisibilité du code.
Les goto inutiles
Même si le goto est parfois utile, il faut proscrire les utilisations du type :
goto onyva ;
while (expression) {
/* traitement 1 */
…
onyva :
/* traitement 2 */
…
}
On peut obtenir le même résultat avec un code plus lisible et sans goto :
for(;;) {
/* traitement 2 */
…
if (!expression) break;
/* traitement 1 */
…
}
Autre exemple abominable :
if (fileOpened) {
if(dataReady) {
nbPage++;
goto goprint ;
}
else goto errorData;
} else {
OpenTheFile() ;
PrepareData () ;
goprint :
/* gros traitement */
…
…
}
return OK;
errorData:
return ERROR;
Ce type d’écriture qui tente de réutiliser un bout de code grâce à un goto est inacceptable.
Typiquement, il aurait été nécessaire de créer une fonction :
25
*XLGHGH6W\OHGX/DQJDJH&
void PrintData()
{
/* notre gros traitement */
…
…
}
Afin de pouvoir réutiliser le traitement sans faire appel à un goto. Le code modifié serait
alors :
if (fileOpened) {
if(dataReady) {
nbPage++;
PrintData();
}
else return ERROR;
} else {
OpenTheFile() ;
prepareData () ;
PrintData();
}
return OK;
26
*XLGHGHVW\OHGX/DQJDJH&
6
FICHIERS D’EN-TÊTE (INCLUDES)
Factoriser les définitions
Ne jamais dupliquer une information (constante, macro…) dans plusieurs fichiers .c. Utilisez
les fichiers d’en-tête .h
Pas de chemins absolus
Dans un #include, ne pas utiliser des noms de chemins absolus du type :
#include "d:\MonProjet\Sources\Foo\toto.h"
Car si vous déplacez vos fichiers, il faudra modifier vos sources.
Utilisez pour ceci l’option –I du compilateur.
Guillemets ou <> ?
Utilisez #include "…" pour les en-têtes de l’application et #include <…> pour les entêtes des librairies, cela accélérera la recherche des fichiers en-têtes lors de la compilation.
Utiliser les prototypes définis dans les includes
Ajoutez les #include correspondant aux fonctions utilisées afin de bénéficier des
prototypes qui y sont définis.
Prévenir la double inclusion et prévoir C++
Les fichiers d’en-tête doivent avoir un mécanisme prévenant la double inclusion et prévoyant
l’utilisation de ces fonctions depuis le C++.
Exemple pour un fichier foo.h :
27
*XLGHGH6W\OHGX/DQJDJH&
#if !defined ( _FOO_H )
#define _FOO_H
#if defined( __cplusplus )
extern "C" {
#endif
/* les définitions */
int FOO_ComputeValue(int source);
int FOO_ConvertValue(int value,int factor);
/* Fin des de’finitions */
#if defined( __cplusplus )
}
#endif
#endif /* !defined( _FOO_H ) */
Des includes « autonomes »
Un fichier include doit se suffire à lui-même en faisant lui-même les includes nécessaires à
son fonctionnement. En effet l’utilisateur d’un include ne doit pas devoir faire d’autres
includes préalables pour obtenir des définitions nécessaires qu’il n’a pas à connaître.
Q : Oui, mais ne risque-t-on pas d’inclure plusieurs fois le même fichier si les fichiers
d’en-tête font des inclusions que l’on ne maîtrise pas ?
R : C’est pour cette raison qu’il faut un mécanisme pour prévenir la double inclusion
dans les fichiers d’en-tête.
28
*XLGHGHVW\OHGX/DQJDJH&
7
GESTION DES ERREURS EN C
Détection et traitement des erreurs
Un bon programme doit être capable de gérer toutes les erreurs sans planter. Au pire, en cas
de problème grave (plus de mémoire disponible, par exemple) le programme doit
s’interrompre après avoir affiché un message indiquant la cause de l’arrêt.
Il faut en particulier, lorsque l’on appelle une fonction, tester au retour qu’une erreur n’a pas
eu lieu.
Exemple :
/* Copie un fichier en supprimant les lignes commençant par # */
void SuppressComments(const char *filenameIn, const char *filenameOut)
{
#define MAX_LINE_SIZE 132
FILE *streamIn, *streamOut;
char *line;
streamIn=fopen(filenameIn);
streamOut=fopen(filenameOut);
line=(char *)malloc(MAX_LINE_SIZE+1);
while(fgets(line, MAX_LINE_SIZE, streamIn)) {
if (line[0]!=’#’)
fprintf(streamOut,”%s”,line);
}
free(line);
fclose(streamIn);
fclose(streamOut);
}
Dès qu’une erreur va se produire, le comportement de la fonction sera imprévisible car rien
n’est fait pour gérer les erreurs. Pourtant, les causes d’erreurs sont ici multiples :
- Erreur d’ouverture du fichier d’entrée filenameIn
- Erreur d’ouverture du fichier de sortie filenameOut
- Erreur d’allocation du buffer line
- Erreur lors de la lecture du fichier d’entrée
- Erreur lors de l’écriture du fichier de sortie
- Erreur lors de la fermeture du fichier d’entrée
- Erreur lors de la fermeture du fichier de sortie
Il faut donc détecter et traiter au mieux ces erreurs. De plus notre fonction va être appelée par
d’autres fonctions et elle doit elle-même reporter les erreurs qu’elle a détectées. On a donc :
29
*XLGHGH6W\OHGX/DQJDJH&
int SuppressComments(const char *filenameIn, const char *filenameOut)
{
#define MAX_LINE_SIZE 132
FILE *streamIn, *streamOut;
char *line;
streamIn=fopen(filenameIn);
if (streamIn==NULL) return –1;
streamOut=fopen(filenameOut);
if (streamOut==NULL) {
fclose(streamIn);
return –2;
}
line=(char *)malloc(MAX_LINE_SIZE+1);
if (line==NULL) {
fclose(streamIn);
fclose(streamOut);
return –3;
}
while(fgets(line, MAX_LINE_SIZE, streamIn)) {
if (line[0]!=’#’) {
if (fprintf(streamOut,”%s”,line)<0) {
free(line);
fclose(streamIn);
fclose(streamOut);
return –4;
}
}
}
free(line);
if (ferror(streamIn)) {
fclose(streamIn);
fclose(streamOut);
return –5;
}
if (fclose(streamIn)) {
fclose(streamOut);
return –6;
}
if (fclose(streamOut))
return –7;
return 0;
}
Notre fonction est maintenant robuste mais considérablement alourdie à cause de la nécessité
de libérer les ressources avant de sortir. De plus, il est facile d’oublier de libérer une ressource
dans un cas particulier. L’utilisation de goto peut nous permettre de centraliser la libération
des ressources à la fin de la fonction. De plus, cette fonction retourne des codes d’erreurs sous
forme de nombre négatifs (une tradition en C) mais il serait souhaitable d’avoir un vrai type
pour les codes d’erreur sous la forme d’un type énuméré :
typedef enum {
ERROR_OK = 0 ,
ERROR_CANT_OPEN_FILE_FOR_WRITING = -1,
ERROR_CANT_OPEN_FILE_FOR_READING = -2,
etc etc…
} ERROR_CODE ;
30
*XLGHGHVW\OHGX/DQJDJH&
ERROR_CODE
SuppressComments(const char *filenameIn, const char *filenameOut)
{
#define MAX_LINE_SIZE 132
FILE *streamIn=NULL, *streamOut=NULL;
char *line=NULL;
ERROR_CODE code=ERROR_OK;
streamIn=fopen(filenameIn,”r”);
if (streamIn==NULL) {
code=ERROR_CANT_OPEN_FILE_FOR_READING;
goto error;
}
streamOut=fopen(filenameOut,”w”);
if (streamOut==NULL) {
code=ERROR_CANT_OPEN_FILE_FOR_WRITING;
goto error;
}
line=(char *)malloc(MAX_LINE_SIZE+1);
if (line==NULL) {
code=ERROR_MEMORY;
goto error;
}
while(fgets(line, MAX_LINE_SIZE, streamIn)) {
if (line[0]!=’#’) {
if (fprintf(streamOut,”%s”,line)<0) {
code=ERROR_WRITING;
goto error;
}
}
}
if (ferror(streamIn)) code=ERROR_READING;
error:
if (line) free(line);
if (streamIn) {
if (fclose(streamIn)) {
fclose(streamOut);
if (code==ERROR_OK) code=ERROR_CLOSING;
}
}
if (streamOut) {
if (fclose(streamOut)) {
if (code==ERROR_OK) code=ERROR_CLOSING;
}
}
return code;
}
La fonction renvoie maintenant des codes d’erreurs exploitables et la libération des ressources
est la même, que l’on sorte naturellement de la fonction ou qu’une erreur ait été détectée. Il
est clair que le code est plus lourd que sans traitement d’erreurs, mais c’est le prix à payer. Par
exemple, si la fonction retourne l’erreur ERROR_WRITING l’application pourra exploiter ce
code pour afficher :
Erreur lors de l’écriture du fichier xxxx .
Vérifiez que votre disque n’est pas saturé.
Alors que sans traitement d’erreur le comportement serait :
31
*XLGHGH6W\OHGX/DQJDJH&
-
plantage pur et simple de l’application ou
l’application n’a pas traité le fichier mais ne signale rien
Q : Quand une fonction retourne déjà une valeur qui n’est pas un entier positif,
comment peut-elle retourner un code d’erreur ?
R : Deux techniques sont possibles :
1- ajouter un paramètre qui est un pointeur sur un code d’erreur
Exemple :
float
CalculeVitesse(int temps,float vitesseInitiale,ERROR_CODE *erreur) ;
2- avoir une variable globale dans laquelle on stocke le code d’erreur
C’est la solution retenue par la librairie C avec la variable globale errno
Utilisation de errno
La librairie C standard défini le symbole errno qui permet de consulter un code d’erreur
affecté par les fonctions de la librairie.
Au démarrage de l’application, la valeur de errno est 0, mais aucun mécanisme ne permet
de remettre errno à 0 entre chaque appel. Il est donc souhaitable d’initialiser errno à 0 avant
l’appel de la fonction pour laquelle on gère les erreurs. Exemple :
#include <errno.h>
...
errno=0 ;
f=fopen(“toto”,”r”);
La valeur de errno peut être modifiée par la librairie qu’il y ait ou qu’il n’y ait pas eu
d’erreur. Par conséquent, ne pas faire :
errno=0 ;
f=fopen(“toto”,”r”);
if (errno) {
/* traitement de l’erreur */
car errno peut être non nul après l’appel à fopen même si fopen a fonctionné
correctement. Il faut faire :
errno=0 ;
f=fopen(“toto”,”r”);
if (f==NULL) { /* echec de l’ouverture */
/* consultation du code d’erreur */
switch (errno) {
case ...
}
Le problème c’est que la norme ISO ne défini que deux codes d’erreurs :
EDOM (argument mathématique illégal) et ERANGE (dépassement de capacité). Tous les
autres codes d’erreur dépendent de l’implémentation. L’utilisation de errno implique donc des
problèmes de portabilité.
32
*XLGHGHVW\OHGX/DQJDJH&
Q : Je récupère un code d’erreur errno en cas d’erreur, mais j’aimerais afficher un
message d’erreur plus sympathique que « Erreur n°-142 »
R : Les fonctions standards perror et strerror permettent d’afficher ou
d’obtenir un message d’erreur à partir d’un code errno. Toutefois, ces messages
sont la plupart du temps en anglais.
Cast des appels de fonctions en void
Lorsque qu’un développeur choisi d’ignorer délibérément la valeur retournée par un appel de
fonction, et donc de ne pas gérer une erreur éventuelle, il est préférable de clairement
l’exprimer dans le code ainsi :
(void) fclose(f) ;
D’ailleurs, certains compilateurs ou certains outils de vérification de code comme « lint »
signalent systématiquement toutes les valeurs de retour non exploitées. Dans ce cas, il peut
être souhaitable d’écrire :
(void) printf(”Salut la Foule !\n”) ;
(void) strcpy(s1,s2) ;
même si printf est en pratique peu susceptible de retourner une erreur et strcpy
incapable de le faire.
33
*XLGHGH6W\OHGX/DQJDJH&
8
STRATÉGIES POUR AIDER À LA MISE AU POINT
Les assertions
Lors de l’exécution, un programme C ne vérifie rien quant à la validité des opérations
effectuées. Par exemple :
/* met une lettre d’une chai^ne en majuscule */
void CapitalizeLetter(char *str,int index)
{
str[index]=toupper(str[index]) ;
}
void Foo()
{
char *toto[]= "Hello" ;
CapitalizeLetter(toto,10) ; /* bug ! ! */
}
Lors de l’exécution de la fonction CapitalizeLetter appelée depuis la fonction Foo, le
programme modifiera le 11ième élément d’une chaîne de caractères ne comportant que 5
lettres. Au mieux le programme va planter, mais sans indiquer la raison du plantage qui ne
risque d’être découverte qu’après une longue séance sous debugger. Au pire le comportement
de programme va être aléatoire et le problème sera encore plus difficile à résoudre.
C’est donc le devoir mais aussi l’intérêt du développeur d’écrire du code qui va vérifier, dans
la mesure du possible, que le code est valide. Le langage C fourni un outil pour ceci : le
fichier en-tête assert.h défini une macro de nom assert qui vérifie qu’une condition que
l’on suppose vraie est bien vérifiée :
#include <assert.h>
/* met une lettre d’une chai^ne en majuscule */
void CapitalizeLetter(char *str,int index)
{
assert(index >=0);
assert(index<strlen(str));
str[index]=toupper(str[index]) ;
assert(!islower(str[index]));
}
Avec ce nouveau code plus robuste, lors de l’exécution de la fonction Foo, l’exécution
s’interrompra après que le programme ait affiché un message de diagnostique du type:
assertion failed : index<strlen(str), file foo.c, line 12
Q : Toutes vérifications avec assert, ça va ralentir mon code ! Ne vais-je pas
perdre tout le bénéfice de l’efficacité du langage C ?
34
*XLGHGHVW\OHGX/DQJDJH&
R : assert n’est pas une fonction mais une macro qui, lorsque l’on compile la
version finale que l’on va livrer à l’utilisateur, s’expanse en rien afin que les tests ne
soient plus effectués
Lorsque l’on développe, on génère un exécutable dit de « mise au point » (debug) qui n’est
pas optimisé et qui comporte tous les éléments aidant à la mise au point. Dans ce cas la
définition de assert, qui est une macro est du type :
#define assert(exp) (void)((exp) || (_assert(#exp, __FILE__, __LINE__), 0))
avec _assert qui est une fonction affichant le message de diagnostique et sortant du
programme.
Lorsque l’on livre l’application au client, on génère un exécutable dit de « livraison »
(release) qui lui est optimisé et ne comporte pas les éléments aidant à la mise au point. Dans
ce cas la définition de assert est :
#define assert(exp)
((void)0)
ce qui fait que dans la version de livraison, les tests ne sont plus effectués et ne pénalisent
donc pas les performances .
Q : assert est génial, maintenant je gère toutes mes erreurs avec ça. Par exemple :
FILE *f=fopen("toto", "r") ;
assert(f !=NULL) ;
R : assert ne sert pas à gérer les erreurs mais écrire du code de vérification lors
de la phase de mise au point. Dans la version de livraison, les asserts ne sont plus
pris en compte et dans cet exemple, on ne vérifierait plus que le fichier se soit
correctement ouvert.
Q : La définition d’assert me semble bien complexe ! Pourquoi ne pas écrire
#define assert(exp) if(exp) _assert(#exp, __FILE__, __LINE__)
R : voir « bug avec les macros (3) » page 41
Utiliser les warnings
Configurer le compilateur pour obtenir le maximum de message de warning. Les warnings
signalent toujours des problèmes potentiels qu’il faut analyser.
Q : Tous ces messages de warning m’énervent en m’empêchent de travailler
correctement. Alors j’ai configuré le compilateur pour qu’il n’affiche plus les
warnings. Malin, non ?
R : Hérétique ! Comme toujours, il faut détecter les problèmes potentiels le plus tôt
possible et les warnings sont des outils précieux dont on ne peut se priver
impunément. Il faut au contraire configurer le compilateur pour obtenir d’avantage
de message de warning.
Q : J’ai ce code qui me donnait un warning à propos d’un problème de conversion :
double b ;
…
35
*XLGHGH6W\OHGX/DQJDJH&
int a=2*sqrt(b) ;
Heureusement, je n’ai pas perdu de temps et j’ai fait :
int a=2*(int) sqrt(b) ;
J’ai plus de warning ! Suis-je fort ?
R : Le but du jeu n’est pas de trouver le moyen le plus rapide de ne plus voir le
message. Dans cet exemple, la bonne correction aurait sans doute été :
int a=(int) (2*sqrt(b)+0.5) ;
Un warning doit inciter à réfléchir !.
Libération des ressources
Toujours libérer les ressources allouées :
• La mémoire allouée par malloc doit toujours être libérée par free
• Un fichier ouvert par fopen doit être fermé par fclose
• …
L’oubli d’une libération n’est malheureusement pas immédiatement visible puisque le
programme fonctionne bien (tant qu’il reste des ressources disponibles, bien sur). Toutefois,
ceci à une répercussion sur l’efficacité du programme mais peut aussi dégrader les
performances des autres applications fonctionnant en parallèle.
Q : J’ai fait un programme qui alloue des ressources (mémoire dynamique), fait un
traitement et sort juste derrière. N’est ce pas inutile de se fatiguer à libérer les
ressources alors que le système les libère de lui-même à la sortie du programme ?
R : Rien ne garantit que ces ressources seront effectivement libérées par le système.
C’est a priori vrai sous Unix mais complètement faux avec Window 3.x et pas
toujours vrai avec Windows 95 et NT.
Utilisation d’outils de vérification
Il existe des outils comme Purify, BoundChecker ou CodeGuard qui permettent de vérifier au
cours d’une exécution que l’application n’a pas effectué d’opération illégale, que toutes les
ressources ont été correctement libérées et que les fonctions systèmes ont été correctement
utilisées. Il est clair que ces outils sont une aide précieuse lors de la mise au point d’une
application.
36
*XLGHGHVW\OHGX/DQJDJH&
9
PROBLÈMES DE PORTABILITÉ
NULL
Ne pas supposer que NULL est codé avec (void *) 0. Par conséquent, dans les tests ne faites
pas :
if ( !(f=fopen("foo.txt", "r") ) {
/* Erreur d’ouverture du fichier */
}
mais :
if ((f=fopen("foo.txt", "r") == NULL) {
/* Erreur d’ouverture du fichier */
}
Opérateurs de décalage
N’utiliser les décalages binaires (opérateurs >> << >>= et <<=) que sur des entiers non
signés. Le comportement sur des entiers signés est imprévisible et dépend des machines et des
compilateurs.
Commentaires à la C++
Les commentaires « à la C++ » du type :
// Un commentaire
ne font pas parti de la norme bien qu’ils soient acceptés par certains compilateurs. Il vaut
mieux donc s’en tenir aux commentaires C traditionnels.
Taille des types entiers
Le C possède trois types d’entiers : short, int et long. La norme précise que le type
short prend au moins 16 bits et que le type long prend au moins 32 bits. Quant au type
int, on peut juste supposer qu’il est possible d’affecter un short à un int sans effectuer
de troncation. Un programme portable doit donc supposer qu’un int peut ne faire que 16
bits. Un programme peut utiliser les constantes définies dans le fichier en-tête « limits.h »
pour connaître les valeurs minimales et maximales qu’un type donné peut contenir.
37
*XLGHGH6W\OHGX/DQJDJH&
Ordre des octets à l’intérieur d’un entier
Dans un entier codé sur plusieurs octets, rien ne suppose que les octets les plus significatifs
seront codés en premier ou en dernier et cela dépend des architectures. Par exemple :
unsigned short mot=0x80;
/* re’cupe’ration de l’octet de poid faible et de l’octet de poid fort*/
poidFaible = *( (unsigned char *) &mot) ;
poidFort
= *(((unsigned char *) &mot)+1) ;
n’est absolument pas portable. La bonne version est :
poidFaible = mot&0xff
poidFort
= (mot>>8) & 0xff;
Préférez les fonctions standards
Lorsque c’est possible utilisez toujours les fonctions standards de la librairie.
Les fonctions d'entrée/sortie de bas niveau « à la Unix » peuvent couramment être remplacées
par des appels de haut niveau qui sont normalisés.
Appel de bas niveau (propriétaire)
open
create
close
eof
read
write
tell
lseek
dup
dup2
Appel de haut niveau (standard ISO)
fopen
fopen
fclose
feof
fread
fwrite
ftell
fseek
freopen
freopen
Si la bufférisation induite par les fonctions de haut niveau est gênante, elle peut être
supprimée par un appel à la fonction setvbuf.
Certaines fonctions opérant sur les blocs mémoires qui existent sur certains systèmes Unix
doivent être abandonnées au profit des fonctions définies par la norme. Voici une table
d'équivalence:
Propriétaire
bcopy(src,dest,len)
bzero(dest,len)
bcmp(s1,s2,len)
De plus évitez le code du type :
38
Standard ISO
memcpy(dest,src,len)
memset(dest,val,len)
memcmp(s1,s2,len)
*XLGHGHVW\OHGX/DQJDJH&
if (car>=’a’ && car<=’z’) car=car+’A’-‘a’;
Qui repose sur les propriétés du code ASCII. Les fonctions de la librairie font la même chose
de manière portable :
if (islower(car)) car=toupper(car) ;
Alignements dans les structures
Lorsque le compilateur implémente une structure en mémoire, les différents membres de la
structure ne sont pas forcément contigus en mémoire. Ainsi dans la structure :
struct FOO {
char ch ;
int i ;
} ;
l’implémentation en mémoire peut être (par exemple):
ch
Inutilisé
Inutilisé
Inutilisé
i (bits 0-7)
i (bits 8-15)
i (bits 16-23)
i (bits 24-31)
En général, le compilateur possède des options permettant de modifier la façon dont les
membres des structures sont alignés en mémoire.
La macro offsetof(type, membre) permet de connaître à quelle adresse mémoire
relative un membre donné est implémenté. Dans notre exemple, offsetof(FOO,i) vaut 4.
C permet de définir des champs de bits à l’intérieur d’une structure. Par exemple :
struct {
unsigned int color : 4;
unsigned int underline : 1;
unsigned int italic: 1;
unsigned int bold : 1;
} screen[25][80];
L’ordre d’implémentation des bits à l’intérieur d’un mot mémoire dépend de l’implémentation
et rien ne permet de le déterminer.
39
*XLGHGH6W\OHGX/DQJDJH&
10
BUGS CLASSIQUES
Les macros (1)
Lors de la déclaration d’une macro, Pensez aux parenthèses autour des paramètres
Par exemple, veut définir une macro qui multiplie par 2 et que l’on écrit :
#define MULPAR2(x) 2*x
l’expression
r=MULPAR2(a) ;
Donne bien
r=2*a ;
Mais
r=MULPAR2(a+b) ;
Donne
r=2*a+b ; /* Argh !*/
Il fallait définir :
#define MULPAR2(x) 2*(x)
le résultat aurait alors été :
r=2*(a+b); /* Correct !*/
Maintenant considérons la macro suivante :
#define PLUS10(x) 10+(x)
On a bien mis les parenthèses autour du paramètre mais si on a :
r=2*PLUS10(a);
Cela va être expansé ainsi :
r=2*10+(a) ; /* Encore faux ! ! */
Il fallait définir :
#define PLUS10(x) (10+(x))
40
*XLGHGHVW\OHGX/DQJDJH&
Pour avoir :
r=2*(10+(a)) ; /* Cette fois c’est juste! */
Les macros (2)
Lorsque l’on utilise plusieurs fois le même paramètre dans la définition d’une macro, il faut
être très méfiant. Par exemple, avec :
#define max(a,b) (((a)>(b)) ? (a) : (b))
L’expression :
r=max(2*a+foo(b),exp(c)+log(d)) ;
est expansé en :
r=(((2*a+foo(b))>(exp(c)+log(d)) ? (2*a+foo(b)) : (exp(c)+log(d))) ;
Par conséquent, un des deux paramètres sera calculé deux fois, ce qui n’est pas très efficace.
De plus, si deux appels consécutifs de la fonction « foo » avec la même valeur ne rendent pas
le même résultat, le deuxième calcul du premier paramètre de l’exemple peut renvoyer une
valeur différente du premier calcul !
Les macros (3)
Une macro n’est pas une instruction. En effet, considérons la macro suivante :
#define PRINTIFPOSITIVE(x) if ((x)>=0) printf("%d est > 0\n ",(x))
Le code :
PRINTIFPOSITIVE(a+b) ;
fonctionne parfaitement, à part le fait que l’expression a+b est calculée deux fois lorsqu’elle
est positive. Mais le code :
if (z)
PRINTIFPOSITIVE(a+b) ;
else
z=0;
est équivalent à :
if (z)
if ((a+b)>=0) printf(”%d est > 0\n”,(a+b));
else
z=0;
41
*XLGHGH6W\OHGX/DQJDJH&
équivalent lui-même à :
if (z) {
if ((a+b)>=0)
printf(”%d est > 0\n”,(a+b));
else
z=0;
}
La seule définition correcte de cette macro n’est malheureusement pas très lisible :
#define PRINTIFPOSITIVE(x) (void) ((x) >= 0 && printf("%d est > 0\n ",(x)))
La vérité est ailleurs
Le langage C ne possède pas de type booléen, alors on défini souvent :
typedef int
#define FALSE
#define TRUE
BOOL;
0
1
Le problème avec ces définitions, c’est que tout autre valeur que 0, et pas seulement 1 est
considérée comme « vraie ». Par conséquent, ne jamais faire un test du type :
if (FichierOuvert() == TRUE) {
Naturellement, il suffisait d’écrire :
if (FichierOuvert()) {
= n’est pas ==
if (x = y) {….
Ne compare pas x et y mais affecte x avec y.
Dans le cas où vous voudriez vraiment faire une affectation, privilégiez l’écriture
if ((x = y) != 0) {…
Plus claire.
De même, attention aux confusions entre & (et binaire) et && (et logique) ainsi qu’entre | (ou
binaire) et || (ou logique).
Certaines personnes, lors d’une comparaison avec une constante préfère écrire :
if
(0 == x)
au lieu de
42
*XLGHGHVW\OHGX/DQJDJH&
if
(x == 0)
car, en cas d’oubli du deuxième =, on aurait
if
(0 = x)
Ce qui provoquerait une erreur de compilation. Toutefois, cette astuce nuit quelque peu à la
lisibilité du code.
Commentaire mal fermé
Considérons le code suivant :
prixHT = prixUnitaire * quantite ; /* Calcul du prix HT des articles * /
prixTTC = prixHT * (1 + tauxTVA) ; /* Calcul du prix TTC */
A la suite d’une faute de frappe, un espace s’est inséré entre le « * » et le « / » de la fin du
commentaire sur la première ligne. Le commentaire de la première ligne commence au
premier « /* » et se termine au prochain « */ » qui est en fait la fin du deuxième
commentaire. Le compilateur considère donc que la deuxième ligne de code est commentée !
C est gourmand
On dit que C est gourmand (greedy) car il recherche le mot-clé le plus grand possible lors de
la compilation. Ainsi l’expression :
i+++j
est interprété comme :
i++ + j
et non comme :
i+ ++j
Q : Comment est interprété x+++++y ?
R : Comme x++ ++ + y ce qui provoque une erreur de syntaxe puisque x++ ++
est illégal. L’interprétation légale x++ + ++y n’est pas considérée par le
compilateur .
Cette caractéristique peut causer des bugs particulièrement vicieux, comme le piège de la
division par une valeur pointée :
Dans :
y= x/*p ;
43
*XLGHGH6W\OHGX/DQJDJH&
Le compilateur interprète /* comme un début de commentaire. Utilisez des espaces ou des
paramètres pour résoudre le problème :
y= x / *p ;
y= x/(*p) ;
Dans l’exemple précédent, on peut supposer que l’erreur soit détectée au moment de la
compilation. Mais pas toujours, car imaginons maintenant que l’on ait :
if (x/*p
> facteur)
/* si la division de x par la valeur pointée par p */
/* est supe’rieure au facteur */
Le commentaire commence après le « x » et fini au prochain « */ », donc le compilateur
comprendra :
if (x>facteur)
a[i] = i++; ne fonctionne pas !
Dans l’expression
a[i] = i++;
La sous expression i++ provoque un effet de bord qui modifie la valeur de i. Le standard C
spécifie que le comportement de cette expression est indéfini. Le problème serait similaire
pour :
a[i++] = i;
En général, on écrit ce type d’expression parce que l’on a utilisé une boucle while à la place
d’une boucle for. Ainsi, plutôt que :
i=0 ;
while ( i<n )
y[i++]=x[i] ;
/* bug */
Il faut écrire :
for(i=0 ;i<n ;i++)
y[i]=x[i] ;
Ordre d’évaluation des sous expressions
Dans une expression, l’ordre d’évaluation des sous expressions n’est pas garanti. Ainsi, le
résultat de :
( i++ + 1 ) * ( 2 + i )
est indéfini. Seuls quatre opérations garantissent l’ordre d’évaluation : « && », « || », le
couple « ? : » et l’opérateur de continuation « , ».
44
*XLGHGHVW\OHGX/DQJDJH&
Ainsi, il est parfaitement légal d’écrire :
if (b !=0 && a/b>limite)
car a/b ne sera évalué que si b est bien différent de 0 et il n’y a donc pas de risque d’erreur
causée par une division par 0.
Ordre d’évaluation des paramètres
De même que pour les sous expressions, l’ordre d’évaluation des paramètres n’est pas défini
lors de l’appel d’une fonction. Par conséquent le comportement de :
add( i + 1, i = j + 2 );
est indéfini.
int a = 1000, b = 1000; long c = a * b; ne fonctionne pas !
Dans l’expression :
int a = 1000, b = 1000; long c = a * b;
a et b sont des entiers « int » et leur produit est toujours de type « int ». Le produit est ensuite
promu en type « long » pour affecter c. Si la valeur 1000*1000 dépasse la capacité du type
int, le résultat escompté sera faux.
Pour avoir le bon résultat, il faut convertir un des deux arguments avant la multiplication à
l’aide d’un « cast » :
long c= (long) a *b ;
Gérer les dépassements de capacité
C a deux types d’arithmétique entière : signée et non signée. Il n’existe pas de notion de
dépassement pour l’arithmétique non signé car tous les calculs sont effectués modulo 2n , n
étant le nombre de bits du type résultat. Si l’un des opérandes est signé et l’autre non signé,
l’opérande signé est converti en non signé et il n’y a donc toujours pas de dépassement
possible.
Par contre un dépassement peut survenir lors d’une opération dont les deux arguments sont
signés. Le résultat d’un dépassement est indéfini et il est sain de ne rien supposer concernant
ce résultat. En C, rien ne permet de savoir si un dépassement a eu lieu. Donc, si un
dépassement est possible il faut tester cette possibilité avant d’effectuer l’opération. Par
exemple, si
45
*XLGHGH6W\OHGX/DQJDJH&
c= a + b ;
risque de provoquer un dépassement, on peut s’en assurer ainsi :
if (a > INT_MAX – b) {
/* on gère le de’passement */
} else
c = a+b ;
NB : INT_MAX est l’entier le plus grand représentable avec le type int. Cette constante est
définie dans le fichier d’en-tête limits.h.
Ne pas confondre *p++ et (*p)++
*p++
Incrémente le pointeur p et retourne la valeur pointée par p avant l’incrémentation. Alors que :
(*p)++
Incrémente la valeur pointée par p
Tableau de pointeurs ou pointeur de tableaux ?
int *pointeurs[10];
est un tableau de 10 pointeurs sur des entiers, tandis que :
int (*pointeur)[10];
est un pointeur sur un tableau de 10 entiers…
Pointeur constant ou constante pointée ?
const char *p
ou
char const *p
déclarent un pointeur sur un caractère constant. (on ne peut pas modifier le caractère pointé
mais on peut modifier le pointeur)
Tandis que :
char * const p
Déclare un pointeur constant sur un caractère (on peut modifier le caractère pointé mais pas la
valeur du pointeur)
46
*XLGHGHVW\OHGX/DQJDJH&
const char * const p
ou
char const * const p
déclare un pointeur constant sur un caractère constant (on ne peut ni modifier la valeur du
pointeur ni la valeur du caractère pointé)
Distinction pointeurs/tableaux
On a coutume de dire que les tableaux et les pointeurs sont équivalents en C. Il existe
quelques différences, en particulier :
char s[]= “abc“;
défini un tableau de 4 caractères et les initialise avec la chaîne « abc ». Tandis que :
char *p= “abc“;
Défini un pointeur qui pointe sur un tableau de 4 caractères contenant la chaîne « abc ». Dans
ce dernier cas, le système n’assure pas que le tableau est modifiable ou qu’il soit à l’usage du
seul pointeur p. Par conséquent, le comportement de :
*p= ‘a‘ ;
est indéfini.
Précision du calcul flottant
Le calcul flottant se fait avec une certaine précision et ne reflète pas toujours certaines vérités
mathématiques. Par exemple :
void foo (double a)
{
double i ;
double step=a/100 ;
for(i=a ; i!=0.0 ;i-=step) {
…
}
}
Très probablement, la boucle ne s’arrêtera jamais car les calculs ne seront pas assez précis : la
variable i va passer par une valeur « proche » de 0 mais ne sera jamais nulle.
Généralement, à cause de ces problèmes de précision, on utilise rarement les opérateurs ==
ou != en calcul flottant, mais on fait des comparaisons du type :
if(fabs(a - b) <= epsilon * a)
Avec une valeur « habilement choisie » pour epsilon.
47
*XLGHGH6W\OHGX/DQJDJH&
Fonction retournant une valeur allouée sur la pile
Considérons une fonction qui permet de convertir un prix stocké sous forme de valeur
flottante en une chaîne de caractères justifiée à droite :
typedef float prix_t ;
char *PrixVersChaine(prix_t prix)
{
char chainePrix[11];
sprintf(chainePrix,”%10.2f”,prix);
return chainePrix;
/* bug */
}
Cette fonction retourne un buffer qui est une variable locale allouée sur la pile. A la sortie de
la fonction, l’espace mémoire du buffer est donc libéré et le pointeur retourné par notre
fonction est invalide. On devrait avoir :
char *PrixVersChaine(prix_t prix)
{
static char chainePrix[11];
sprintf(chainePrix,”%10.2f”,prix);
return chainePrix;
}
Dans cette version le buffer est alloué en mémoire globale grâce à l’emploi du mot clé
static pour la déclaration. La valeur retournée par la fonction est maintenant valide, mais
comme le buffer a été alloué en mémoire globale, son contenu sera écrasé au prochain appel
de la fonction. La fonction, de ce fait, n’est pas réentrante, ce qui serait particulièrement
gênant dans le cas d’une application multi-thread. La meilleure solution étant donc d’allouer
le buffer en mémoire dynamique avec malloc :
char *PrixVersChaine(prix_t prix)
{
char *chainePrix=(char *) malloc(11);
if (chainePrix==NULL) return NULL;
sprintf(chainePrix,”%10.2f”,prix);
return chainePrix;
}
Dans ce cas la fonction appelante devra libérer la chaîne de caractères retournée avec un appel
à la fonction free lorsque la chaîne deviendra inutile .
Notre fonction a encore un petit problème… On alloue 11 octets dans le buffer car avec la
chaîne de formatage du printf (« %10.2 »), on sait que la sortie devra faire 10 caractères
(donc 11 avec le ‘\0’ terminal). Outre le fait qu’il est toujours désagréable d’avoir des
constantes en dur, il existe un cas pour lequel la sortie va excéder les 10 caractères : printf
ne fait jamais de troncation et si le prix dépasse 9999999 (sept ‘9’ car 3 caractères sont pris
48
*XLGHGHVW\OHGX/DQJDJH&
par le point décimal et les centimes), la sortie sera plus large que 10 caractères et il y aura un
débordement dans le buffer chainePrix. Voici une version robuste :
#define LARGEUR_CHAINE_PRIX 10
char *PrixVersChaine(prix_t prix)
{
/* nombre d’octets ne’ce’ssaires pour afficher un prix */
int nbOctetsNecessaires;
char *chainePrix;
/* calcul nombre d’octets ne’cessaires pour afficher le prix
log base 10 du prix + 1 pour les unite’s +
1 pour le point de’cimal + 2 pour les centimes */
nbOctetsNecessaires=((int) log10(prix)) + 4;
/* On essaie de formater à droite sur LARGEUR_CHAINE_PRIX
caracte`res sinon, on de’borde */
nbOctetsNecessaires=max(nbOctetsNecessaires, LARGEUR_CHAINE_PRIX);
chainePrix=(char *) malloc(nbOctetsNecessaires+1);
if (chainePrix==NULL) return NULL;
sprintf(chainePrix,”%*.2f”,LARGEUR_CHAINE_PRIX,prix);
return chainePrix;
}
49
*XLGHGH6W\OHGX/DQJDJH&
11
OPTIMISATIONS
Optimisation vs Lisibilité
C est un langage très efficace mais on cherche toujours à optimiser son programme pour avoir
les meilleurs temps de calcul. Malheureusement les optimisations nuisent souvent à la
lisibilité du code ; il faut donc bien savoir ce qui est important pour votre projet, l’efficacité
ou la lisibilité. Il faut évaluer si une optimisation vaut le prix payé en terme de perte de
maintenabilité du programme et essayer d’en minimiser le coût à l’aide de commentaires.
Cibler les optimisations
L’expérience montre que toutes les fonctions ne sont pas égales en terme de temps
d’exécution. On constate en général que 20% des fonctions consomment 80% du temps
d’exécution. Il ne sert donc à rien de tout optimiser sans discernement mais il faut déterminer
quelles sont les fonctions sur lesquelles les efforts devront porter. Des outils (profilers)
permettent de calculer le temps passé dans chaque fonction lors d’une exécution.
Valider les optimisations
On doit toujours être capable de savoir ce qu’apporte chaque optimisation. Pour ceci il suffit
de chronométrer la portion de code avant et après l’optimisation. Les exemples ne sont pas
rares où certaines « optimisations » ne font rien gagner ou même font perdre de l’efficacité.
En effet les compilateurs modernes optimisent votre code parfois mieux que vous-même.
Quelques techniques
Sortir les calculs des boucles
Au sein d’une boucle, il ne faut pas calculer la même chose à chaque itération. Si une
expression est constante, il faut la calculer avant la boucle. Par exemple :
for(i=0;i<nbventes;i++)
prix[i]=prix[i]*remise*(1+tauxtva);
devient :
facteurRemiseTVA=remise*(1+tauxtva);
for(i=0;i<nbventes;i++)
prix[i]*= facteurRemiseTVA;
50
*XLGHGHVW\OHGX/DQJDJH&
Inversion boucle/test
Si un test au sein d’une boucle se fait sur une expression qui toujours fausse ou vraie pour
toutes les itérations, il est souhaitable de tester l’expression avant de boucler. Par exemple :
for(i=0 ;i<nbOperations; i++) {
if(operation==Credit)
montant+=operation[i];
else
montant-=operation[i];
}
s’optimise en
if (operation==Credit)
for(i=0 ;i<nbOperations; i++) montant+=operation[i];
else
for(i=0 ;i<nbOperations; i++) montant-=operation[i];
Optimisation de la condition de sortie de boucle
Pour chaque itération dans une boucle, la condition de sortie de la boucle est testée. Il est donc
souhaitable d’optimiser ce test.
Par exemple, si le processeur de la machine compare plus rapidement une valeur avec 0
qu’avec une variable :
for(i=0 ;i<nbOperations; i++)
montant+=operation[i];
s’optimise en :
for(i=NbOperations-1 ;i>=0; i--)
montant+=operation[i];
Fusion de boucle
Deux boucles consécutives avec les mêmes bornes peuvent parfois être fusionnées en une
seule boucle :
for(i=0;i<nbOperations;i++)
operation[i]*=tauxTVA;
for(i=0;i<nbOperations;i++)
libelleOperation[i]=strdup(“Taxe”);
s’optimise en :
51
*XLGHGH6W\OHGX/DQJDJH&
for(i=0;i<nbOperations;i++) {
operation[i]*=tauxTVA;
libelleOperation[i]=strdup(“Taxe”);
}
Déroulage de boucles (1)
Pour les boucles ou le nombre d’itérations est faible et constant, il est possible de supprimer la
boucle. Par exemple :
for(somme=0,i=0;i<8;i++)
somme+=operation[i];
s’optimise en:
somme=operation[0] + operation[1] + operation[2] + operation[3] +
operation[4] + operation[5] + operation[6] + operation[7];
Déroulage de boucles (2)
Afin de minimiser le coût de gestion d’une boucle (test de la condition d’arrêt, incrémentation
du compteur), on peut transformer la boucle de manière à diviser le nombre d’itérations par
deux, comme dans cet exemple :
for(somme=0,i=0;i<nboperations;i++)
somme+=operation[i];
s’optimise en:
somme=0;
nboperations--;
i=0;
while (i<nboperations) {
/* deux ope’rations par ite’rations !! */
somme+=operation[i++];
somme+=operation[i++];
}
/* traite dernie`re valeur si nboperations impair */
if (i != (++nboperations))
somme+=operation[i];
Q : Dans cet exemple, on fait deux opérations par itération, mais pourquoi par 3, 4
ou même plus
R : C’est effectivement possible, mais cela fait un code de plus en plus volumineux
Les amateurs de bizarrerie pourront examiner une technique exotique d’optimisation par
déroulage de boucle : le dispositif de Duff (cf . page 65).
Inversion de boucles imbriquées
Pour deux boucles imbriquées, il faut essayer d’avoir la boucle la plus active « à l’intérieur ».
Exemple:
52
*XLGHGHVW\OHGX/DQJDJH&
#define MAT_W 2000
#define MAT_H 10
double matrix[MAT_H][MAT_W];
for(somme=0.0,i=0;i<MAT_W;i++) /* 2000 boucles */
for(j=0;j<MAT_H;j++) /* seulement 10 boucles */
somme+=j*matrix[j][i];
Dans cet exemple, on a 2000 itérations pour la boucle externe et 2000x10=20000 itérations
pour la boucle interne soit un total de 22000 itérations. On peut optimiser ainsi :
for(somme=0.0,i=0;i<MAT_H;i++) /* les 10 boucles a` l’exte’rieur */
for(j=0;j<MAT_W;j++) /* les 2000 a` l’inte’rieur */
somme+=i*matrix[i][j];
Ainsi, on a 10 itérations pour la boucle externes et 10x2000=20000 itérations pour la boucle
interne, soit un total de 20010 itérations. On a donc économisé 22000-20010=1990 itérations.
Tester en premier les cas les plus courants
Lors d’une succession de tests, il est souhaitable de commencer à tester les cas les plus
courants afin de minimiser, en moyenne, le nombre de tests effectués. Par exemple :
if (letter==’!’) {
…
} else
if (letter==’?’) {
…
} else
if (isdigit(letter)) {
…
} else
if (isupper(letter)) {
…
} else {
…
}
devient:
if (isupper(letter)) {
…
} else
if (isdigit(letter)) {
…
} else {
if (letter==’!’) {
…
} else
if (letter==’?’) {
…
} else {
…
}
53
*XLGHGH6W\OHGX/DQJDJH&
Tester en premier les cas les moins coûteux à tester
Lorsque dans une expression booléenne l’ordre d’évaluation est a priori indifférent, il est
possible de profiter du fait que l’ordre d’évaluation est connu pour tester en premier les sous
conditions qui sont calculées le plus rapidement. Par exemple :
if (a > 0.0 || exp(b)>log(c*d+a) )
est plus efficace que :
if (exp(b)>log(c*d+a)
|| a > 0.0)
Utiliser un switch à la place d’un if
Un switch est toujours plus rapide qu’une succession de if, même lorsque le nombre de
case est important. Par exemple :
if (isupper(letter)) {
…
} else
if (isdigit(letter)) {
…
} else {
if (letter==’!’) {
…
} else
if (letter==’?’) {
…
} else {
…
}
devient :
54
*XLGHGHVW\OHGX/DQJDJH&
switch (letter) {
case ‘!‘ :
…
break ;
/* les chiffres */
case ‘0‘ : case ‘1‘ : case ‘2‘
case ‘5‘ : case ‘6‘ : case ‘7‘
…
break;
case ‘?‘ :
…
break ;
/* les lettres majuscules */
case ‘A‘ : case ‘B‘ : case ‘C‘
case ‘F‘ : case ‘G‘ : case ‘H‘
case ‘K‘ : case ‘L‘ : case ‘M‘
case ‘P‘ : case ‘Q‘ : case ‘R‘
case ‘U‘ : case ‘V‘ : case ‘W‘
case ‘Z‘ :
…
break;
default:
…
}
: case ‘3‘ : case ‘4‘ :
: case ‘8‘ : case ‘9‘ :
:
:
:
:
:
case
case
case
case
case
‘D‘
‘I‘
‘N‘
‘S‘
‘X‘
:
:
:
:
:
case
case
case
case
case
‘E‘
‘J‘
‘O‘
‘T‘
‘Y‘
:
:
:
:
:
Utilisation des pointeurs pour accéder aux tableaux
Il est parfois plus efficace d’utiliser des pointeurs pour parcourir un tableau. Par exemple :
#define MAT_W 1000
#define MAT_H 1000
double matrix[MAT_H][MAT_W];
for(somme=0.0,i=0;i<MAT_W;i++)
for(j=0;j<MAT_H;j++)
somme+=matrix[j][i];
la boucle devient :
double *p =matrix ;
for(somme=0.0,i=MAT_W*MAT_H;i;i--,p++)
somme+=*p;
C’est en particulier souvent intéressant avec les traitements de chaînes de caractères :
/* Met une chai^ne en majuscules */
void MettreEnMajuscules(char *s)
{
int i;
for(i=0;i<strlen(s);i++)
s[i]=toupper(s[i]);
}
55
*XLGHGH6W\OHGX/DQJDJH&
Dans cet exemple, on calcule la longueur de la chaîne à chaque itération. Evidemment, la
première optimisation qui vient à l’esprit est (cf. Optimisation de la condition de sortie de
boucle, page 51):
/* Met une chai^ne en majuscules */
void MettreEnMajuscules(char *s)
{
int i;
for(i=strlen(s)-1; i>=0; i--)
s[i]=toupper(s[i]);
}
Mais on peut optimiser d’avantage en utilisant les pointeurs. Pour calculer la longueur d’une
chaîne, la fonction strlen utilise une boucle qui parcoure la chaîne jusqu’à trouver le 0
terminal. La fonction MettreEnMajuscule possède aussi une boucle qui parcoure la
chaîne, ici pour mettre les caractères en majuscules. Il est donc facile d’utiliser les pointeurs
pour, avec une boucle unique, mettre les caractères en majuscules jusqu’à rencontrer le 0
terminal :
void MettreEnMajuscules(char *s)
{
for(;*s;s++)
*s=toupper(*s);
}
Précalcul dans des tables
Lorsque qu’une fonction calcule une valeur en utilisant un algorithme coûteux, et que les
paramètres de la fonction ne peuvent prendre qu’un nombre raisonnable de combinaisons de
valeur distinctes, on peut calculer une bonne fois pour toutes tous les résultats possibles de la
fonction et stocker ces valeurs dans une table. A l’exécution, il suffira d’accéder à la table
pour retourner le résultat. Par exemple :
/* inversion des bits dans un octets */
typedef unsigned char BYTE;
BYTE InvertByte(BYTE in)
{
BYTE out=0,maskIn=0x80,maskOut=0x01;
int i;
for(i=0;i<8;i++) {
if (in&maskIn) out|=maskOut;
maskIn >>=1;
maskOut<<=1;
}
return out;
}
Devient:
56
*XLGHGHVW\OHGX/DQJDJH&
/* On a calcule’ toutes les possibilite’s (256 au total) */
static const BYTE _Permutations[256] = {
0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff
};
BYTE InvertByte(BYTE in)
{
return _Permutations[in];
}
Utilisation de memset, memcpy et memmove avec les tableaux
Une initialisation de tableau ou de structure peut être efficacement réalisée avec memset.
Exemple :
int tableau[] ;
int i ;
for(i=0 ;i<nbElements ;i++)
tableau[i]=0 ;
la boucle devient :
memset(tableau,0,nbElements*sizeof(int)) ;
57
*XLGHGH6W\OHGX/DQJDJH&
De même memcpy et memmove peuvent être employés dans les recopies de tableaux à la
place de boucles for.
Enlever les register
Il est possible en C de déclarer une variable en utilisant le mot-clé register qui suggère au
compilateur d’implanter la variable dans un registre du processeur afin d’aller plus vite (les
opérations avec les registres étant plus rapides que celles avec la mémoire).
Les compilateurs modernes utilisent pleinement les registres du processeur sans qu’il soit
nécessaire d’utiliser register. Comme le nombre de registres est limité, le choix
d’implanter telle ou telle variable dans un registre est assez difficile pour le développeur
tandis que le compilateur fait généralement le meilleur choix.
Les compilateurs, face à une déclaration en register peuvent avoir deux types de
comportement :
½ le compilateur sait qu’il est plus malin que le développeur : il ignore totalement le
register et fait lui-même son choix des variables à allouer dans des registres.
½ le compilateur est servile et honore la déclaration en register tout en sachant qu’il
aurait fait un meilleur usage des registres.
Donc le mot-clé register ne sert au mieux à rien, au pire à générer du code moins efficace.
Conclusion : il ne faut pas l’utiliser.
58
*XLGHGHVW\OHGX/DQJDJH&
12
RÉALISATION DE COMPOSANTS LOGICIELS
Qu’est ce qu’un composant logiciel ?
Un composant logiciel regroupe un ensemble de fonctionnalités utilisables par une ou
plusieurs applications. L'accès à ces fonctionnalités se fait au travers d'une interface de
programmation (API).
Les librairies, statiques ou dynamiques, sont des exemples de composant logiciel.
Nommage des fonctions et types exportés par une librairie
Afin d’éviter les problèmes de collision entre les noms de fonctions et de types provenant de
librairies diverses et portant le même nom, il est simple de préfixer tous les symboles exportés
par une librairie avec un préfixe unique et représentatif. Le préfixe conseillé est un trigramme
en lettres majuscules suivi d’un « underscore ». Par exemple, pour une librairie de nom
« Soleil », on pourra avoir les fonctions exportées SOL_OpenMyFile,
SOL_ComputeDistance, SOL_FreeResource et les types SOL_PLANET et
SOL_COMET.
Le principe de la "boîte noire"
Lorsque qu'un composant est utilisé par plusieurs applications, une modification de l'interface
de programmation du composant entraîne généralement une modification de toutes les
applications utilisant ce composant. Ce travail étant donc très lourd, tout doit donc être mis en
œuvre pour qu'une modification d'un composant n'entraîne pas de modification de son
interface de programmation et donc pas de modification des applications.
Pour ceci, l'interface de programmation doit être conçue de manière à être la plus
indépendante possible des structures de données et des algorithmes mis en œuvre pour réaliser
les fonctionnalités du composant. En d'autres termes, les choix d'implémentation doivent
être "cachés" pour les utilisateurs du composant ; il s'agit du "principe de la boîte noire".
Une interface de programmation peut contenir des fonctions, des procédures, des types et
structure de données mis à la disposition des applications qui vont utiliser le composant. Une
des techniques couramment utilisée pour respecter le principe de la "boîte noire" est d’exclure
toute structure de données de l'interface de programmation. Dans ce cas, les données
gérées par le composant sont toujours manipulées au travers d'identifiants ("handlers" ou
pointeurs). L'accès aux propriétés des données se fait alors uniquement à l'aide de fonctions
de l'interface de programmation. L'avantage est immédiat: toute modification des structures de
données n'entraîne que des modifications à l'intérieur du code du composant. De plus, si le
composant est un fichier binaire indépendant comme dans le cas d'une librairie dynamique,
une recompilation des applications n'est pas nécessaire.
59
*XLGHGH6W\OHGX/DQJDJH&
Comment cacher les structures de données
Afin d’éviter que les utilisateurs d’une librairie accèdent directement au champ d’une
structure, les utilisateurs ne doivent pas disposer de la définition de la structure. Mais
comment les utilisateurs peuvent-il manipuler une structure qu’ils ne connaissent pas ?
Pour ceci, les utilisateurs doivent toujours manipuler les données au travers de pointeurs, les
structures étant alors allouées en mémoire dynamique par la librairie.
Par exemple, si on a une librairie manipulant des structures de type « SOL_PLANET », le
fichier include sol.h contiendra :
#if !defined ( _SOL_H )
#define _SOL_H
#if defined( __cplusplus )
extern "C" {
#endif
/* on de’clare le type mais pas le contenu de la structure ! !*/
typedef struct tagSOL_PLANET SOL_PLANET ;
/* Fonction pour cre’er une plane`te */
SOL_PLANET *SOL_CreatePlanet(const char *planetName,int size);
/* Fonction pour libe’rer une plane`te */
void SOL_FreePlanet(SOL_PLANET *aPlanet);
/* Fonction pour afficher une plane`te */
void SOL_DisplayPlanet(SOL_PLANET *aPlanet,int x,int y);
#if defined( __cplusplus )
}
#endif
#endif /* !defined( _SOL_H ) */
Dans le fichier sol.c on implémente la structure et les fonctions :
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "sol.h "
/* De’finition pour les plane`tes */
struct tagSOL_PLANET {
char *m_Name ;
/* Nom de la plane`te */
int
m_Size ;
/* Diame`tre de la plane`te en pixels */
} ;
/* Puis on implémente les fonctions….*/
60
*XLGHGHVW\OHGX/DQJDJH&
/* Fonction pour cre’er une plane`te */
SOL_PLANET *SOL_CreatePlanet(const char *planetName,int size)
{
SOL_PLANET *planet;
assert(planetName!=NULL);
assert(size>0);
/* Allocation de la structure */
planet = (SOL_PLANET *)malloc(sizeof(SOL_PLANET));
/* Test Erreur allocation */
if (planet==NULL) return NULL;
/* Copie de la chai^ne pour le nom et affecte le champ */
planet->m_Name = strdup(planetName);
/* Test Erreur allocation */
if (planet->m_Name ==NULL) {
free(planet);
return NULL;
}
/* affecte la taille */
planet->m_Size=size;
/* retourne l’objet */
return planet;
}
/* Fonction pour libe’rer une plane`te */
void SOL_FreePlanet(SOL_PLANET *aPlanet)
{
assert(aPlanet!=NULL);
assert(aPlanet->m_Name!=NULL);
/* libe`re le nom */
free(aPlanet->m_Name);
/* libe`re la structure */
free(aPlanet);
}
/* Fonction pour afficher une plane`te */
void SOL_DisplayPlanet(SOL_PLANET *aPlanet,int x,int y)
{
/* du code */
}
Les utilisateurs de la librairie ne disposent que de sol.h et de la version compilée de la librairie
(sol.so par exemple…). Ils peuvent donc créer, détruire et manipuler des planètes sans avoir à
en connaître la définition. Le principe de la boîte noire est ici scrupuleusement respecté.
Hétérogénéité des runtimes C entre les modules
Chaque module (librairie dynamique ou exécutable) possède son propre Runtime C qui gère
toutes les fonctions C comme printf, malloc, qsort... Le runtime d'un module n'est
souvent pas le même que le runtime d'un autre module, en particulier lorsque l’on utilise des
compilateurs différents ou même seulement des options de compilation différentes. Ceci peut
poser les problèmes suivants:
61
*XLGHGH6W\OHGX/DQJDJH&
Implémentation différentes des structures
Une structure système comme FILE peut être implémentée différemment d'un runtime à
l'autre. Par exemple, le compilateur Microsoft utilise la définition suivante:
struct _iobuf {
char *_ptr;
int
_cnt;
char *_base;
int
_flag;
int
_file;
int
_charbuf;
int
_bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
Alors que le compilateur WATCOM utilise cette définition:
typedef struct __iobuf
unsigned char
int
unsigned char
unsigned
int
unsigned
unsigned char
unsigned char
} FILE;
{
*_ptr;
_cnt;
*_base;
_flag;
_handle;
_bufsize;
_ungotten;
_tmpfchar;
Par conséquent, un FILE * obtenu dans un module ne peut être utilisé que par ce module.
Par exemple, l'utilisation par un fprintf dans une librairie dynamique d'un FILE *
obtenu par un fopen appelé dans un exécutable ne fonctionnera pas et risque de causer un
plantage.
En ce qui concerne les structures définies par la norme ISO, ce problème peut ce retrouver
lorsque l'on manipule les structures suivantes:
62
*XLGHGHVW\OHGX/DQJDJH&
Structure
FILE
Fichier en-tête
stdio.h
lconv
jmp_buf
fpos_t
tm
locale.h
setjmp.h
stdio.h
time.h
Fonctions concernées
fopen, fclose, fprintf, fscanf, fflush,
freopen, setbuf, setvbuf, tmpfile,
vfprintf, fgetc, fputc, fgets, fputs,
fread, fwrite, fgetpos, fseek, ftell,
rewind, clearerr, feof, ferror
setlocale
setjmp longjmp
fgetpos, fsetpos
asctime, gmtime, localtime, mktime,
strftime
L'allocateur mémoire
Chaque runtime possède son propre allocateur mémoire gérant les appels aux fonctions
malloc, realloc, calloc et free. Par conséquent, un bloc mémoire alloué dans un
module doit toujours être réalloué et/ou libéré par ce même module.
Par exemple, imaginons qu'une fonction dans une librairie dynamique retourne un objet dans
un buffer mémoire alloué par malloc.
/* retourne une copie de la chaîne s en convertissant
les lettres en majuscules. Retourne NULL en cas d'erreur */
char *ToUppercase(const char *s)
{
char *res,*s2;
/* allocation de la nouvelle chaîne */
res=s2=(char *) malloc(strlen(s)+1);
/* test si assez de mémoire */
if (res==NULL) return NULL;
/* copie des caractères avec conversion */
while ((*s2++=toupper(*s++))!='\0');
return res;
}
Si dans un exécutable on fait:
char *s;
s=ToUppercase("Bonjour");
printf(s);
free(s);
Le programme va afficher BONJOUR et se planter dans l'appel à free, puisque le bloc
mémoire appartient à la librairie et pas à l'exécutable.
63
*XLGHGH6W\OHGX/DQJDJH&
Le futur des composants logiciels
Aujourd’hui, l’essentiel des composants logiciels sont des librairies, statiques et dynamiques.
Depuis le début des années 1990, un nouveau type de composant a émergé, il s’agit de l’objet
distribué. Deux standards existent :
• DCOM (Distributed Component Object Model) promu par Microsoft et qui fait parti des
technologies ActiveX et OLE,
• CORBA (Common Object Request Broker Architecture) défini par l’Object Management
Group (OMG), un consortium de plus de 700 sociétés.
Un objet distribué est un composant qui encapsule des données et des traitements. Il est
accessible via une ou plusieurs API qui sont uniquement composées de fonctions (on
retrouve le principe de la boîte noire…). Par rapport à une librairie, les avantages sont
multiples :
• Lorsqu’un objet à été implémenté sur une machine, toutes les applications fonctionnant
sur cette machine ou sur toutes les autres machines connectées au même réseau peuvent
utiliser l’objet.
• Les objets peuvent posséder plusieurs interfaces logicielles (API) correspondant à
plusieurs versions du même objet. Ce mécanisme permet de faire évoluer un objet sans
qu’il soit obligatoire de modifier des applications utilisant une ancienne version de l’objet.
• Les objets peuvent interagir même s’ils sont distribués sur des machines hétérogènes
• Lorsqu’une application utilise un objet, cet objet peut être situé sur une autre machine ou
résider sur la même machine sans qu’il soit nécessaire de modifier l’application. Il n’y
plus de programmation spécifique pour une application devant fonctionner en réseau
• Il est possible de distribuer les objets de manière à utiliser au mieux l’ensemble des
ressources informatiques du réseau. Ainsi un objet nécessitant une grande puissance de
calcul pourra être distribué sur un calculateur tandis qu’un objet manipulant des données
de taille importante sera distribué sur un serveur ayant un grand espace de stockage.
On peut donc parier que les librairies développées aujourd’hui seront rapidement transformées
en composants objets. Respecter dès maintenant le principe de la boîte noire ne pourra que
faciliter cette migration.
64
*XLGHGHVW\OHGX/DQJDJH&
13
CURIOSITÉS DU C
a[b] équivalent à b[a]
En C, les pointeurs ont des affinités avec les tableaux. En effet :
a[b]
est équivalent à :
*(a + b)
Ce qui, grâce à la commutativité de l’addition, est équivalent à :
*(b + a)
lui-même équivalent à :
b[a]
Par conséquent,
1["abc"]
est parfaitement valide en C et vaut ‘b’
Le dispositif de Duff (Duff’s device)
M. Duff, développeur chez Lucas Film, a inventé une technique d’optimisation, qui est une
variante des techniques de déroulage de boucles (cf. page 52). Voici un exemple de routine de
copie de zone mémoire avec cette technique :
/* Copie de “count” octets de “from” vers “to” */
void DuffMemcpy(char *to, const char *from, int count)
{
n = (count + 7) / 8;
switch (count % 8) {
case 0:
do { *to++ = *from++;
case 7:
*to++ = *from++;
case 6:
*to++ = *from++;
case 5:
*to++ = *from++;
case 4:
*to++ = *from++;
case 3:
*to++ = *from++;
case 2:
*to++ = *from++;
case 1:
*to++ = *from++;
} while (--n > 0);
}
65
*XLGHGH6W\OHGX/DQJDJH&
Cette technique résout le problème du traitement des octets restants quand « count » n’est
pas multiple de 8 en intercalant une boucle do à l’intérieur d’un switch/case. Ceci est
possible car en C, le switch est une sorte de goto déguisé avec les cases jouant le rôle de
labels. Inutile de dire que ce type de construction est une négation totale des principes de la
programmation structurée et ne doit être utilisé qu’en cas de gain significatif sur le temps de
calcul.
Les séquences Trigraph
Il s’agit d’une caractéristique peu connu de la norme C, mais toutes les séquences suivantes
dans un source sont substituées à la compilation par un caractère correspondant :
Séquence trigraph
??=
??(
??/
??)
??’
??<
??!
??>
??-
Caractère équivalent
#
[
\
]
^
{
|
}
~
Ceci permet la saisie de code C sur des machines ne possédant pas ces caractères.
Par conséquent :
printf( "Comment??!\n" );
Affiche Comment| car ??! est une séquence trigraph équivalente à |.
Q : Dans ce cas, comment afficher « Comment ? ? ! » ?
R : printf( "Comment?\?!\n" );
66
*XLGHGHVW\OHGX/DQJDJH&
14
BIBLIOGRAPHIE
The C Complete reference, 2nd edition, Hervert Schildt, Ed. MacGraw-Hill, 1990
C Traps and Pitfalls, Andrew Koening, Ed. Addison-Wesley, 1988.
La Qualité en C++, Philippe Prados, Ed. Eyrolles, 1996
C Programming FAQs: Frequently Asked Questions, Steve Summit, Addison-Wesley, 1995
Du code et des hommes, Steve Maguire, Microsoft Press, 1994
Programmation Professionnelle, Steve McConnell, Microsoft Press, 1993
Langages de Programmation – C, norme NF EN 29899, ISO/CEI 9899, 1994
67