Article de reference

Préprocesseur C

( Découvrez comment et quand supprimer ce message ) Le préprocesseur C est le préprocesseur de macros pour plusieurs langages de programmation informatique , tels que C , Object...

( Découvrez comment et quand supprimer ce message )

Le préprocesseur C est le préprocesseur de macros pour plusieurs langages de programmation informatique , tels que C , Objective-C , C++ et une variété de langages Fortran . Le préprocesseur permet l'inclusion de fichiers d'en-tête , d'extensions de macros , de compilation conditionnelle et de contrôle de ligne.

Le langage des directives du préprocesseur n'est que faiblement lié à la grammaire du C, et est donc parfois utilisé pour traiter d'autres types de fichiers texte .

Histoire

Le préprocesseur a été introduit dans C vers 1973 à la demande d'Alan Snyder et également en reconnaissance de l'utilité des mécanismes d'inclusion de fichiers disponibles dans BCPL et PL/I . Sa version originale n'offrait que l'inclusion de fichiers et le remplacement simple de chaînes en utilisant #includeet #definepour les macros sans paramètres, respectivement. Il a été étendu peu de temps après, d'abord par Mike Lesk puis par John Reiser, pour incorporer des macros avec des arguments et une compilation conditionnelle.

Le préprocesseur C faisait partie d'une longue tradition de macro-langage aux Bell Labs, lancée par Douglas Eastwood et Douglas McIlroy en 1959.

Phases

Le prétraitement est défini par les quatre premières (des huit) phases de traduction spécifiées dans la norme C.

  1. Remplacement de trigraphes : Le préprocesseur remplace les séquences de trigraphes par les caractères qu'elles représentent. Cette phase sera supprimée dans C23 en suivant les étapes de C++17 .
  2. Épissure de ligne : les lignes sources physiques qui se poursuivent avec des séquences de nouvelle ligne échappées sont épissées pour former des lignes logiques.
  3. Tokenisation : le préprocesseur décompose le résultat en jetons de prétraitement et en espaces . Il remplace les commentaires par des espaces.
  4. Extension des macros et gestion des directives : les lignes de directives de prétraitement, y compris l'inclusion de fichiers et la compilation conditionnelle, sont exécutées. Le préprocesseur étend simultanément les macros et, depuis la version 1999 de la norme C, gère _Pragmales opérateurs.

Fichiers inclus

L’une des utilisations les plus courantes du préprocesseur est d’inclure un autre fichier source :

#include <stdio.h>
int main ( void ) { printf ( "Bonjour, monde ! " ); return 0 ; }

Le préprocesseur remplace la ligne #include <stdio.h>par le contenu textuel du fichier 'stdio.h', qui déclare la printf() fonction entre autres choses.

Cela peut également être écrit en utilisant des guillemets doubles, par exemple #include "stdio.h". Si le nom de fichier est entouré de crochets angulaires, le fichier est recherché dans les chemins d'inclusion standard du compilateur. Si le nom de fichier est entouré de guillemets doubles, le chemin de recherche est étendu pour inclure le répertoire du fichier source actuel. Les compilateurs C et les environnements de programmation ont tous une fonction qui permet au programmeur de définir où les fichiers d'inclusion peuvent être trouvés. Cela peut être introduit via un indicateur de ligne de commande, qui peut être paramétré à l'aide d'un makefile , de sorte qu'un ensemble différent de fichiers d'inclusion puisse être échangé pour différents systèmes d'exploitation, par exemple.

Par convention, les fichiers d'inclusion sont nommés avec une extension .hou .hpp. Cependant, il n'y a aucune obligation de respecter cette règle. Les fichiers avec une .defextension peuvent désigner des fichiers conçus pour être inclus plusieurs fois, développant à chaque fois le même contenu répétitif ; #include "icon.xbm"il s'agit probablement d'un fichier image XBM (qui est en même temps un fichier source C).

#includeoblige souvent à utiliser des #includeprotections ou #pragma onceà empêcher une double inclusion.

Compilation conditionnelle

Les directives if–else#if , #ifdef, #ifndef, #else, #elifet #endifpeuvent être utilisées pour la compilation conditionnelle . #ifdefet #ifndefsont de simples raccourcis pour #if defined(...)et #if !defined(...).

#si VERBOSE >= 2 
printf ( "message de trace" ); #endif

La plupart des compilateurs ciblant Microsoft Windows définissent implicitement _WIN32. Cela permet au code, y compris aux commandes du préprocesseur, de compiler uniquement lorsqu'il cible les systèmes Windows. Quelques compilateurs définissent WIN32à la place. Pour les compilateurs qui ne définissent pas implicitement la _WIN32macro, elle peut être spécifiée sur la ligne de commande du compilateur, à l'aide de -D_WIN32.

#ifdef __unix__ /* __unix__ est généralement défini par les compilateurs ciblant les systèmes Unix */ 
# include <unistd.h> #elif defined _WIN32 /* _WIN32 est généralement défini par les compilateurs ciblant les systèmes Windows 32 ou 64 bits */ # include <windows.h> #endif

L'exemple de code teste si une macro __unix__est définie. Si c'est le cas, le fichier <unistd.h>est alors inclus. Sinon, il teste si une macro _WIN32est définie à la place. Si c'est le cas, le fichier <windows.h>est alors inclus.

Un exemple plus complexe #ifpeut utiliser des opérateurs ; par exemple :

#if !(defined __LP64__ || defined __LLP64__) || defined _WIN32 && !defined _WIN64 
// nous compilons pour un système 32 bits #else // nous compilons pour un système 64 bits #endif

La traduction peut également échouer en utilisant la #errordirective :

#si RUBY_VERSION == 190 
#erreur 1.9.0 non pris en charge 
#endif

Définition et développement de la macro

Il existe deux types de macros : celles de type objet et celles de type fonction . Les macros de type objet ne prennent pas de paramètres, contrairement aux macros de type fonction (bien que la liste des paramètres puisse être vide). La syntaxe générique pour déclarer un identifiant comme macro de chaque type est respectivement :

#define <identifiant> <liste de jetons de remplacement> // macro de type objet 
#define <identifiant>(<liste de paramètres>) <liste de jetons de remplacement> // macro de type fonction, notez les paramètres

La déclaration de macro de type fonction ne doit pas comporter d'espace entre l'identifiant et la première parenthèse ouvrante. Si un espace est présent, la macro sera interprétée comme étant de type objet, tout ce qui commence par la première parenthèse étant ajouté à la liste des jetons.

Une définition de macro peut être supprimée avec #undef:

#undef <identifiant> // supprimer la macro

Chaque fois que l'identifiant apparaît dans le code source, il est remplacé par la liste de jetons de remplacement, qui peut être vide. Pour un identifiant déclaré comme étant une macro de type fonction, il n'est remplacé que lorsque le jeton suivant est également une parenthèse gauche qui commence la liste d'arguments de l'invocation de la macro. La procédure exacte suivie pour l'expansion des macros de type fonction avec des arguments est subtile.

Les macros de type objet étaient traditionnellement utilisées dans le cadre de bonnes pratiques de programmation pour créer des noms symboliques pour les constantes ; par exemple :

#define PI 3.14159

au lieu de coder en dur les nombres dans tout le code. Une alternative en C et C++, en particulier dans les situations où un pointeur vers le nombre est requis, consiste à appliquer le constqualificateur à une variable globale. Cela entraîne le stockage de la valeur en mémoire, au lieu d'être substituée par le préprocesseur. Cependant, dans le code C++ moderne, le constexprmot-clé, introduit dans C++11 , est utilisé à la place :

constexpr double PI = 3,14159 ;

Les utilisations de variables déclarées comme constexpr, en tant que macros de type objet, peuvent être remplacées par leur valeur au moment de la compilation.

Un exemple de macro de type fonction est :

#define RADTODEG(x) ((x) * 57.29578)

Ceci définit une conversion de radians en degrés qui peut être insérée dans le code si nécessaire ; par exemple, RADTODEG(34). Ceci est développé sur place, de sorte que la multiplication répétée par la constante n'est pas affichée dans tout le code. La macro ici est écrite en majuscules pour souligner qu'il s'agit d'une macro et non d'une fonction compilée.

La seconde xest entourée de sa propre paire de parenthèses pour éviter la possibilité d' un ordre d'opérations incorrect lorsqu'il s'agit d'une expression au lieu d'une valeur unique. Par exemple, l'expression se développe correctement sous la forme ; sans parenthèses, donne la priorité à la multiplication. RADTODEG(r+1)((r+1)*57.29578)(r+1*57.29578)

De même, la paire de parenthèses extérieure maintient l'ordre correct des opérations. Par exemple, se développe en ; sans parenthèses, donne la priorité à la division. 1/RADTODEG(r)1/((r)*57.29578)1/(r)*57.29578

Ordre d'expansion

L'expansion macro de type fonction se déroule selon les étapes suivantes :

  1. Les opérations de stringification sont remplacées par la représentation textuelle de la liste de remplacement de leur argument (sans effectuer d'expansion).
  2. Les paramètres sont remplacés par leur liste de remplacement (sans effectuer d'expansion).
  3. Les opérations de concaténation sont remplacées par le résultat concaténé des deux opérandes (sans développer le jeton résultant).
  4. Les jetons provenant de paramètres sont développés.
  5. Les jetons résultants sont développés normalement.

Cela peut produire des résultats surprenants :

#define HE HI 
#define LLO _THERE 
#define HELLO "HI THERE" 
#define CAT(a,b) a##b 
#define XCAT(a,b) CAT(a,b) 
#define CALL(fn) fn(HE,LLO) 
CAT ( HE , LLO ) // "HI THERE", car la concaténation se produit avant le développement normal XCAT ( HE , LLO ) // HI_THERE, car les jetons provenant des paramètres ("HE" et "LLO") sont développés en premier CALL ( CAT ) // "HI THERE", car cela équivaut à CAT(a,b)

Macros et directives spéciales

Certains symboles doivent être définis par une implémentation lors du prétraitement. Il s'agit notamment de __FILE__et __LINE__, prédéfinis par le préprocesseur lui-même, qui s'étendent au fichier et au numéro de ligne actuels. Par exemple, les éléments suivants :

// macros de débogage afin que nous puissions déterminer l'origine du message en un coup d'œil 
// est mauvais 
#define WHERESTR "[fichier %s, ligne %d]: " 
#define WHEREARG __FILE__, __LINE__ 
#define DEBUGPRINT2(...) fprintf(stderr, __VA_ARGS__) 
#define DEBUGPRINT(_fmt, ...) DEBUGPRINT2(WHERESTR _fmt, WHEREARG, __VA_ARGS__) 
// OU 
// est bon 
#define DEBUGPRINT(_fmt, ...) fprintf(stderr, "[fichier %s, ligne %d]: " _fmt, __FILE__, __LINE__, __VA_ARGS__)
DEBUGPRINT ( "hé, x=%d " , x );

imprime la valeur de x, précédée du fichier et du numéro de ligne dans le flux d'erreur, permettant un accès rapide à la ligne sur laquelle le message a été produit. Notez que l' WHERESTRargument est concaténé avec la chaîne qui le suit. Les valeurs de __FILE__et __LINE__peuvent être manipulées avec la #linedirective. La #linedirective détermine le numéro de ligne et le nom de fichier de la ligne ci-dessous. Par exemple :

#ligne 314 "pi.c" 
printf ( "ligne=%d fichier=%s " , __LINE__ , __FILE__ );

génère la printffonction :

printf ( "ligne=%d fichier=%s " , 314 , "pi.c" );

Les débogueurs de code source font également référence à la position source définie avec __FILE__et __LINE__. Cela permet le débogage du code source lorsque C est utilisé comme langage cible d'un compilateur, pour un langage totalement différent. La première norme C spécifiait que la macro __STDC__devait être définie à 1 si l'implémentation est conforme à la norme ISO et à 0 sinon, et que la macro __STDC_VERSION__devait être définie comme un littéral numérique spécifiant la version de la norme prise en charge par l'implémentation. Les compilateurs C++ standard prennent en charge la __cplusplusmacro. Les compilateurs exécutés en mode non standard ne doivent pas définir ces macros ou doivent en définir d'autres pour signaler les différences.

D'autres macros standard incluent __DATE__, la date actuelle et __TIME__, l'heure actuelle.

La deuxième édition de la norme C, C99 , a ajouté la prise en charge de __func__, qui contient le nom de la définition de fonction dans laquelle il est contenu, mais comme le préprocesseur est indépendant de la grammaire de C, cela doit être fait dans le compilateur lui-même à l'aide d'une variable locale à la fonction.

Les macros qui peuvent accepter un nombre variable d'arguments ( macros variadiques ) ne sont pas autorisées dans C89, mais ont été introduites par un certain nombre de compilateurs et normalisées dans C99 . Les macros variadiques sont particulièrement utiles lors de l'écriture de wrappers pour des fonctions prenant un nombre variable de paramètres, comme printfpar exemple lors de la journalisation des avertissements et des erreurs.

Un modèle d'utilisation peu connu du préprocesseur C est connu sous le nom de X-Macros . Une X-Macro est un fichier d'en-tête . Généralement, ceux-ci utilisent l'extension .defau lieu du traditionnel .h. Ce fichier contient une liste d'appels de macro similaires, qui peuvent être appelés « macros de composants ». Le fichier d'inclusion est ensuite référencé à plusieurs reprises.

De nombreux compilateurs définissent des macros supplémentaires non standard, bien que celles-ci soient souvent mal documentées. Une référence courante pour ces macros est le projet Pre-defined C/C++ Compiler Macros, qui répertorie « diverses macros de compilateur prédéfinies qui peuvent être utilisées pour identifier les normes, les compilateurs, les systèmes d'exploitation, les architectures matérielles et même les bibliothèques d'exécution de base au moment de la compilation ».

Stringification des jetons

L' #opérateur (connu sous le nom d' opérateur de stringification ou d'opérateur de stringisation ) convertit un jeton en un littéral de chaîne C , en échappant de manière appropriée aux guillemets ou aux barres obliques inverses.

Exemple:

#définir chaîne(s) #s
str ( p = "foo " ;) // affiche "p = \"foo\ \";" str ( \ n ) // affiche " "

Si la stringification de l'expansion d'un argument de macro est souhaitée, deux niveaux de macros doivent être utilisés :

#définir xstr(s) str(s) 
#définir str(s) #s 
#définir foo 4
str ( foo ) // affiche "foo" xstr ( foo ) // affiche "4"

Un argument de macro ne peut pas être combiné avec du texte supplémentaire puis transformé en chaîne. Cependant, une série de constantes de chaîne adjacentes et d'arguments transformés en chaîne peut être écrite : le compilateur C combinera alors toutes les constantes de chaîne adjacentes en une seule longue chaîne.

Concaténation de jetons

L' ##opérateur (connu sous le nom d'« opérateur de collage de jetons ») concatène deux jetons en un seul jeton.

Exemple:

#define DECLARE_STRUCT_TYPE(nom) typedef struct nom##_s nom##_t
DECLARE_STRUCT_TYPE ( g_object ); // Sorties : typedef struct g_object_s g_object_t ;

Erreurs de compilation définies par l'utilisateur

La #errordirective génère un message via le flux d'erreur.

#erreur "message d'erreur"

Inclusion de ressources binaires

C23 introduira la #embeddirective pour l'inclusion de ressources binaires. Cela permet aux fichiers binaires (comme les images) d'être inclus dans le programme sans qu'ils soient des fichiers source C valides (comme XBM), sans nécessiter de traitement par des outils externes comme xxd -iet sans l'utilisation de littéraux de chaîne qui ont une limite de longueur sur MSVC . De la même manière, xxd -ila directive est remplacée par une liste séparée par des virgules d'entiers correspondant aux données de la ressource spécifiée. Plus précisément, si un tableau de type unsigned charest initialisé à l'aide d'une #embeddirective, le résultat est le même que si la ressource était écrite dans le tableau à l'aide de fread(à moins qu'un paramètre ne modifie la largeur de l'élément d'intégration en quelque chose d'autre que CHAR_BIT). Outre la commodité, #embedelle est également plus facile à gérer pour les compilateurs, car ils sont autorisés à ignorer l'extension de la directive à sa forme complète en raison de la règle as-if .

Le fichier à intégrer peut être spécifié de manière identique à #include, c'est-à-dire soit entre chevrons , soit entre guillemets. La directive permet également de lui transmettre certains paramètres pour personnaliser son comportement, qui suivent le nom du fichier. La norme C définit les paramètres suivants et les implémentations peuvent définir les leurs. Le limitparamètre est utilisé pour limiter la largeur des données incluses. Il est principalement destiné à être utilisé avec des fichiers « infinis » comme urandom . Les paramètres prefixet suffixpermettent au programmeur de spécifier un préfixe et un suffixe aux données intégrées, qui sont utilisés si et seulement si la ressource intégrée n'est pas vide. Enfin, le if_emptyparamètre remplace toute la directive si la ressource est vide (ce qui se produit si le fichier est vide ou si une limite de 0 est spécifiée). Tous les paramètres standard peuvent également être entourés de doubles traits de soulignement, tout comme les attributs standard sur C23, par exemple __prefix__est interchangeable avec prefix. Les paramètres définis par l'implémentation utilisent une forme similaire à la syntaxe d'attribut (par exemple, vendor::attr) mais sans les crochets. Bien que tous les paramètres standard nécessitent qu'un argument leur soit transmis (par exemple, limit nécessite une largeur), cela est généralement facultatif et même l'ensemble de parenthèses peut être omis si un argument n'est pas requis, ce qui peut être le cas pour certains paramètres définis par l'implémentation.

Implémentations

Toutes les implémentations C, C++ et Objective-C fournissent un préprocesseur, car le prétraitement est une étape requise pour ces langages, et son comportement est décrit par les normes officielles de ces langages, telles que la norme ISO C.

Les implémentations peuvent fournir leurs propres extensions et déviations, et varier dans leur degré de conformité aux normes écrites. Leur comportement exact peut dépendre des indicateurs de ligne de commande fournis lors de l'invocation. Par exemple, le préprocesseur GNU C peut être rendu plus conforme aux normes en fournissant certains indicateurs.

Fonctionnalités de préprocesseur spécifiques au compilateur

La #pragmadirective est une directive spécifique au compilateur , que les fournisseurs de compilateurs peuvent utiliser à leurs propres fins. Par exemple, a #pragmaest souvent utilisé pour permettre la suppression de messages d'erreur spécifiques, gérer le débogage du tas et de la pile, etc. Un compilateur prenant en charge la bibliothèque de parallélisation OpenMP peut automatiquement paralléliser une forboucle avec #pragma omp parallel for.

Le C99 a introduit quelques #pragmadirectives standard, sous la forme #pragma STDC ..., qui sont utilisées pour contrôler l'implémentation en virgule flottante. La forme alternative, de type macro, _Pragma(...)a également été ajoutée.

  • De nombreuses implémentations ne prennent pas en charge les trigrammes ou ne les remplacent pas par défaut.
  • De nombreuses implémentations (comme les compilateurs C de GNU, Intel, Microsoft et IBM) fournissent une directive non standard pour afficher un message d'avertissement dans la sortie, mais sans arrêter le processus de compilation ( C23 et C++23 viendront compléter #warningla norme à cet effet). Une utilisation typique consiste à avertir de l'utilisation d'un ancien code, qui est désormais obsolète et inclus uniquement pour des raisons de compatibilité ; par exemple :
    // GNU, Intel et IBM 
    #avertissement « N'utilisez pas ABC, qui est obsolète. Utilisez plutôt XYZ. »
    
    // Microsoft 
    #pragma message("N'utilisez pas ABC, qui est obsolète. Utilisez plutôt XYZ.")
    
  • Certains préprocesseurs Unix fournissent traditionnellement des « assertions », qui ont peu de similitudes avec les assertions utilisées en programmation.
  • GCC permet #include_nextde chaîner les en-têtes du même nom.

Fonctionnalités de préprocesseur spécifiques au langage

Il existe certaines directives de préprocesseur qui ont été ajoutées au préprocesseur C par les spécifications de certains langages et qui sont spécifiques auxdits langages.

  • Les préprocesseurs Objective-C ont #import, qui est similaire #includeà mais n'inclut le fichier qu'une seule fois. Un pragma de fournisseur courant avec une fonctionnalité similaire en C est #pragma once.
  • C++ à partir de C++20 dispose des directives import et module pour les modules . Ces directives sont les seules qui ne commencent pas par un #caractère ; à la place, elles commencent respectivement par importet module, éventuellement précédées de export.

Autres utilisations

Comme le préprocesseur C peut être invoqué séparément du compilateur avec lequel il est fourni, il peut être utilisé séparément, sur des langages différents. Parmi les exemples notables, on peut citer son utilisation dans le système imake, aujourd'hui obsolète , et pour le prétraitement de Fortran . Cependant, une telle utilisation en tant que préprocesseur à usage général est limitée : le langage d'entrée doit être suffisamment semblable au C. Le compilateur GNU Fortran appelle automatiquement le « mode traditionnel » (voir ci-dessous) cpp avant de compiler le code Fortran si certaines extensions de fichier sont utilisées. Intel propose un préprocesseur Fortran, fpp, à utiliser avec le compilateur ifort , qui a des capacités similaires.

CPP fonctionne également de manière acceptable avec la plupart des langages d'assemblage et des langages de type Algol. Cela nécessite que la syntaxe du langage ne soit pas en conflit avec la syntaxe CPP, ce qui signifie qu'aucune ligne ne commence par #et que les guillemets doubles, que cpp interprète comme des littéraux de chaîne et ignore donc, n'aient pas d'autre signification syntaxique que celle-ci. Le « mode traditionnel » (agissant comme un préprocesseur C pré-ISO) est généralement plus permissif et mieux adapté à une telle utilisation.

Le préprocesseur C n'est pas Turing-complet , mais il s'en rapproche beaucoup : des calculs récursifs peuvent être spécifiés, mais avec une limite supérieure fixe sur la quantité de récursivité effectuée. Cependant, le préprocesseur C n'est pas conçu pour être un langage de programmation à usage général, et il ne fonctionne pas bien comme tel. Comme le préprocesseur C ne possède pas les fonctionnalités de certains autres préprocesseurs, comme les macros récursives, l'expansion sélective selon les guillemets et l'évaluation de chaîne dans les conditions, il est très limité par rapport à un processeur de macros plus général comme m4 .

Plus d articles de Worldlex Wiki

Revenez a l index pour explorer davantage de pages sur l histoire, la science, la culture, la geographie et la societe en francais.

Explorer l index