Section courante

A propos

Section administrative du site

Préprocesseur C

Conceptuellement, le préprocesseur est une phase de conversion étant appliquée à votre code source C avant que le compilateur lui-même ne le compile. A l'origine, le préprocesseur était un programme séparé, tout comme le compilateur et l'éditeur de liens peuvent encore être des programmes séparés aujourd'hui. Généralement, le préprocesseur effectue des substitutions textuelles sur votre code source, de trois sortes de manières :

La syntaxe du préprocesseur est différente de la syntaxe du reste de C à plusieurs égards. Tout d'abord, le préprocesseur est basé sur la ligne. Chacune des directives de préprocesseur commencent toutes par le caractère «#» doit être au début d'une ligne et chacune se termine à la fin de la ligne. Le reste de C traite les fins de ligne comme un simple espace supplémentaire et ne se soucie pas de la façon dont le texte de votre programme est organisé en lignes. Deuxièmement, le préprocesseur ne connaît pas la structure de C - à propos des fonctions, instructions ou expressions. Il est possible de jouer d'étranges tours avec le préprocesseur pour transformer quelque chose ne ressemblant pas à C en C (ou vice versa). Il est également possible de rencontrer des problèmes lorsqu'une substitution de préprocesseur ne fait pas ce que vous attendiez, car le préprocesseur ne respecte pas la structure des instructions et expressions C (mais vous vous y attendiez). Pour les utilisations simples du préprocesseur dont nous allons parler, vous ne devriez avoir aucun de ces problèmes, mais vous devrez faire attention avant de faire quoi que ce soit de délicat ou de scandaleux avec le préprocesseur.

Inclusion de fichiers

Une ligne du format :

#include <nomfichier.h>

ou

#include "nomfichier.h"

provoque la lecture, l'analyse et la compilation du contenu du fichier nomfichier.h à ce stade. Après le traitement de nomfichier.h, la compilation se poursuit sur la ligne suivant le #include. Par exemple, supposons que vous soyez fatigué de retaper des prototypes de fonctions externes tels que :

  1. extern int mafonction(char [], int); 

en haut de chaque fichier source. Vous pouvez à la place placer le prototype dans un fichier d'entête, peut-être mafonction.h, puis simplement placer :

  1. #include "mafonction.h"

en haut de chaque fichier source où vous avez appelé mafonction. (Vous ne trouverez peut-être pas utile de créer un fichier d'entête complet pour une seule fonction, mais si vous aviez un paquet de plusieurs fonctions associées, il pourrait être très utile de placer toutes leurs déclarations dans un seul fichier d'entête.) mentionné, c'est exactement ce que sont les fichiers d'entête Standard tels que stdio.h - des collections de déclarations (y compris des déclarations de prototypes de fonctions externes) ayant à voir avec divers ensembles de fonctions de bibliothèque Standard. Lorsque vous utilisez #include pour lire un fichier d'entête, vous obtenez automatiquement les prototypes et autres déclarations qu'il contient, et vous devez utiliser des fichiers d'entête, précisément pour obtenir les prototypes et autres déclarations qu'ils contiennent. La différence entre les formats < > et " " est l'endroit où le préprocesseur recherche nomfichier.h. En règle générale, il recherche les fichiers inclus dans <> dans les répertoires centraux standard, et il recherche les fichiers inclus dans " " dans le répertoire courant ou dans le répertoire contenant le fichier source effectuant l'inclusion. Par conséquent, "" est généralement utilisé pour les fichiers d'entête que vous avez écrits, et <> est généralement utilisé pour les entêtes vous ayant fournis (que quelqu'un d'autre a écrits). L'extension «.h», au fait, signifie simplement entête, et reflète le fait que les directives #include se trouvent généralement en haut (tête) de vos fichiers source et contiennent des déclarations et définitions globales que vous mettriez autrement Là. Cette extension n'est pas obligatoire (vous pouvez théoriquement nommer vos propres fichiers d'entête comme vous le souhaitez) mais .h est traditionnel et recommandé. La raison pour laquelle il faut mettre quelque chose dans un fichier d'entête, puis utiliser #include pour extraire ce fichier d'entête dans plusieurs fichiers sources différents, c'est quand le quelque chose (quel qu'il soit) doit être déclaré ou défini de manière cohérente dans tous les fichiers source. Si, au lieu d'utiliser un fichier d'entête, vous avez tapé quelque chose dans chacun des fichiers source directement, et que quelque chose a jamais changé, vous devrez éditer tous ces fichiers source, et si vous en manquiez un, votre programme pourrait échouer des manières subtiles (ou sérieuses) en raison des déclarations non concordantes (c'est-à-dire en raison de l'incompatibilité entre la nouvelle déclaration dans un fichier source et l'ancienne dans un fichier source que vous avez oublié de modifier). Placer des déclarations et des définitions communes dans des fichiers d'entête signifie que si jamais elles changent, elles ne doivent être modifiées qu'à un seul endroit, ce qui est un système beaucoup plus fonctionnel.

Étant donné que les fichiers d'entête ne contiennent généralement que des déclarations externes et ne doivent pas contenir de corps de fonction, vous devez comprendre ce qui se passe et ce qui ne se passe pas lorsque vous ajouter un #include un fichier d'entête. Le fichier d'entête peut fournir les déclarations pour certaines fonctions, afin que le compilateur puisse générer du code correct lorsque vous les appelez (et pour qu'il puisse s'assurer que vous les appelez correctement), mais le fichier d'entête ne donne pas au compilateur le fonctionne eux-mêmes. Les fonctions réelles seront combinées dans votre programme à la fin de la compilation, par la partie du compilateur appelée l'éditeur de liens. L'éditeur de liens devra peut-être extraire les fonctions des bibliothèques, ou vous devrez peut-être indiquer au compilateur/éditeur de liens où les trouver. En particulier, si vous essayez d'utiliser une bibliothèque tierce contenant des fonctions utiles, la bibliothèque sera souvent livrée avec un fichier d'entête décrivant ces fonctions. L'utilisation de la bibliothèque est donc un processus en deux étapes : vous devez inclure le #include de l'entête dans les fichiers où vous appelez les fonctions de la bibliothèque, et vous devez dire à l'éditeur de liens de lire les fonctions de la bibliothèque elle-même.

Définition et substitution de macros

Une ligne de préprocesseur dans la format de syntaxe :

#define nom texte

définit une macro avec le nom spécifié, ayant comme valeur le texte de remplacement spécifié. Après cela (pour le reste du fichier source actuel), partout où le préprocesseur voit ce nom, il le remplacera par le texte de remplacement. Le nom suit les mêmes règles que les identificateurs ordinaires (il ne peut contenir que des lettres, des chiffres et des traits de soulignement et ne peut pas commencer par un chiffre). Puisque les macros se comportent tout à fait différemment des variables (ou fonctions) normales, il est courant de leur donner des noms étant tous des majuscules (ou du moins commençant par une majuscule). Le texte de remplacement peut être absolument n'importe quoi - il n'est pas limité à des nombres, à de simples chaînes de caractères ou à quoi que ce soit. L'utilisation la plus courante des macros est de propager diverses constantes et de les rendre plus auto-documentées. Prenons pour exemple :

  1. char ligne[100];
  2. /* ... */
  3. demandeligne(ligne,100);

Le code n'est ni lisible ni fiable; ce n'est pas nécessairement évident de savoir ce que sont tous ces 100 dispersés dans le programme, et si jamais nous décidons que 100 est trop petit pour que la taille du tableau contienne des lignes, nous devrons nous rappeler de changer le nombre en deux (ou plus) des endroits. Une bien meilleure solution consiste à utiliser une macro comme ceci :

  1. #define MAXLIGNE 100
  2. char ligne[MAXLIGNE];
  3. /* ... */
  4. demandeligne(ligne, MAXLIGNE);

Maintenant, si jamais nous voulons changer la taille, nous n'avons qu'à le faire à un seul endroit, et ce que signifient les mots MAXLIGNE saupoudrés dans le programme est plus évident que les nombres magiques 100. Puisque le texte de remplacement d'une macro de préprocesseur peut être n'importe quoi, il peut aussi être une expression, même si vous devez vous rendre compte que, comme toujours, le texte est remplacé (et peut-être évalué) plus tard. Aucune évaluation n'est effectuée lorsque la macro est définie. Par exemple, supposons que vous écrivez quelque chose comme&mbsp;:

  1. #define A 2
  2. #define B 3
  3. #define C A + B

C'est un exemple assez banal, mais la situation se présente dans la pratique. Puis, plus tard, supposons que vous écrivez :

  1. int y = C * 2; 

Si A, B et C étaient des variables ordinaires, vous vous attendez à ce que y aboutisse à la valeur 10. Mais voyons ce qui se passe. Le préprocesseur substitue toujours le texte aux macros exactement comme vous l'avez écrit. Donc, il remplace d'abord le texte de remplacement pour la macro C, entraînant :

  1. int y = A + B * 2; 

Ensuite, il remplace les macros A et B, entraînant :

  1. int y = 2 + 3 * 2; 

Ce n'est que lorsque le préprocesseur a terminé de faire tout ce remplacement que le compilateur entre en action. Mais quand il évalue cette expression (en utilisant la priorité normale de la multiplication sur l'addition), il finit par initialiser y avec la valeur 8 ! Pour se prémunir contre ce type de problème, il est toujours judicieux d'inclure des parenthèses explicites dans les définitions des macros contenant des expressions. Si nous devions définir la macro C comme :

  1. #define C (A + B)

alors la déclaration de y s'étendrait finalement à :

  1. int y = (2 + 3) * 2; 

et y serait initialisé à 10, comme nous nous y attendions probablement. Notez qu'il n'est pas nécessaire (et en fait il n'y en a généralement pas) de point-virgule à la fin d'une ligne #define. (Ce n'est qu'une des raisons pour lesquelles la syntaxe du préprocesseur est différente du reste de C.) Si vous tapez accidentellement :

#define MAXLIGNE 100;

puis quand vous déclarez plus tard :

  1. char ligne[MAXLIGNE]; 

le préprocesseur l'étendra de la façon suivante :

char ligne[100;];

étant une erreur de syntaxe. C'est ce que nous voulons dire quand nous disons que le préprocesseur ne sait pas grand-chose sur la syntaxe de C, dans ce dernier exemple, la valeur ou le texte de remplacement de la macro MAXLIGNE était les 4 caractères «100;», et c'est exactement ce que le préprocesseur a remplacé (même si cela n'avait aucun sens). Les macros simples comme MAXLIGNE agissent un peu comme de petites variables, dont les valeurs sont constantes (ou expressions constantes). Il est également possible d'avoir des macros ressemblant à de petites fonctions (c'est-à-dire que vous les appelez avec ce qui ressemble à une syntaxe d'appel de fonction, et elles se développent en texte de remplacement étant une fonction des paramètres réels avec lesquels elles sont appelées) mais nous ne le ferons pas regardez-les encore.

Compilation conditionnelle

La dernière directive de préprocesseur que nous allons examiner est #ifdef. Si vous avez la séquence :

#ifdef nom
textedeprogramme
#else
autretextedeprogramme
#endif

dans votre programme, le code étant compilé dépend du fait qu'une macro de préprocesseur portant ce nom est définie ou non. Si tel est le cas (c'est-à-dire s'il y a eu une ligne #define pour une macro appelée nom), alors «textedeprogramme» est compilé et «autretextedeprogramme» est ignoré. Si la macro n'est pas définie, «autretextedeprogramme» est compilé et «textedeprogramme» est ignoré. Cela ressemble beaucoup à une instruction if, mais elle se comporte complètement différemment : une instruction if contrôle quelles instructions de votre programme sont exécutées au moment de l'exécution, mais #ifdef contrôle quelles parties de votre programme sont réellement compilées. Tout comme pour l'instruction if, le #else dans un #ifdef est facultatif. Il existe une directive compagnon #ifndef, compilant le code si la macro n'est pas définie (bien que la clause «#else» d'une directive #ifndef sera alors compilée si la macro est définie). Il existe également une directive #if compilant le code selon qu'une expression à la compilation est vraie ou fausse. Les expressions autorisées dans une directive #if sont cependant quelque peu restreintes.



Dernière mise à jour : Dimanche, le 8 novembre 2020