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