Preprocesor C

Preprocesor C/C++ ( eng.  preprocesor , preprocessor) - un program care pregătește codul programului în C / C++ pentru compilare .

Funcțiile de bază ale preprocesorului

Preprocesorul face următoarele:

Compilarea condiționată vă permite să alegeți ce cod să compilați pe baza:

Etapele preprocesorului:

Limbajul preprocesorului C/C++ nu este Turing complet, fie doar pentru că este imposibil să blochezi preprocesorul folosind directive. Vezi funcția recursivă (teoria calculabilității) .

Sintaxa directivelor

O directivă de preprocesor (linie de comandă) este o linie din codul sursă care are următorul format #ключевое_слово параметры:

Lista de cuvinte cheie:

Descrierea directivelor

Inserarea fișierelor (#include)

Când directivele #include "..."și sunt găsite #include <...>, unde „...” este un nume de fișier, preprocesorul citește conținutul fișierului specificat, execută directive și substituții (substituții), înlocuiește directiva #includecu o directivă #lineși conținutul fișierului procesat.

Pentru a #include "..."căuta un fișier, se efectuează în folderul curent și folderele specificate pe linia de comandă a compilatorului. Pentru a #include <...>căuta un fișier, se efectuează în foldere care conțin fișiere de bibliotecă standard (căile către aceste foldere depind de implementarea compilatorului).

Dacă se găsește o directivă care #include последовательность-лексем nu se potrivește cu niciuna dintre formele anterioare, aceasta consideră secvența de jetoane drept text, care, ca rezultat al tuturor substituțiilor macro, ar trebui să dea #include <...>sau #include "...". Directiva generată în acest fel va fi interpretată în continuare în conformitate cu formularul primit.

Fișierele incluse conțin de obicei:

Directiva #includeeste de obicei specificată la începutul fișierului (în antet), așa că fișierele incluse sunt numite fișiere antet .

Un exemplu de includere a fișierelor din biblioteca standard C.

#include <math.h> // include declarații de funcție matematică #include <stdio.h> // include declarațiile funcției I/O

Utilizarea unui preprocesor este considerată ineficientă din următoarele motive:

  • de fiecare dată când sunt incluse fișiere, se execută directive și substituții (substituții); compilatorul ar putea stoca rezultatele preprocesării pentru utilizare ulterioară;
  • includerile multiple ale aceluiași fișier trebuie împiedicate manual folosind directive de compilare condiționată; compilatorul ar putea face singur această sarcină.

Începând cu anii 1970, au început să apară metode care au înlocuit includerea fișierelor. Limbile Java și Common Lisp folosesc pachete (cuvânt cheie package) (vezi pachetul în Java ),  Pascal folosește limba engleză.  unități (cuvinte cheie unitși uses), în module Modula , OCaml , Haskell și Python  . Conceput pentru a înlocui limbajele C și C++ , D folosește cuvintele cheie și . moduleimport

Constante și macrocomenzi #define

Constantele preprocesorului și macrocomenzile sunt folosite pentru a defini bucăți mici de cod.

// constantă #define BUFFER_SIZE ( 1024 ) // macro #define NUMBER_OF_ARRAY_ITEMS( array ) ( sizeof( array ) / sizeof( *(array) ) )

Fiecare constantă și fiecare macro este înlocuită cu definiția corespunzătoare. Macro-urile au parametri asemănători funcției și sunt folosite pentru a reduce supraîncărcarea apelurilor de funcții în cazurile în care cantitatea mică de cod pe care o apelează funcția este suficientă pentru a provoca o performanță vizibilă.

Exemplu. Definiția macro-ului max , care ia două argumente: a și b .

#define max( a, b ) ( (a) > (b) ? (a) : (b) )

O macrocomandă este numită la fel ca orice funcție.

z = max ( x , y );

După înlocuirea macrocomenzii, codul va arăta astfel:

z = ( ( ( x ) > ( y ) ? ( x ) : ( y ) );

Cu toate acestea, alături de avantajele utilizării macrocomenzilor în limbajul C, de exemplu, pentru a defini tipuri de date generice sau instrumente de depanare, acestea reduc oarecum eficiența utilizării lor și pot duce chiar la erori.

De exemplu, dacă f și g  sunt două funcții, apelul

z = max ( f (), g () );

nu va evalua f() o dată și g() o dată și va pune cea mai mare valoare în z , așa cum v-ați aștepta. În schimb, una dintre funcții va fi evaluată de două ori. Dacă o funcție are efecte secundare, este probabil ca comportamentul ei să fie diferit de cel așteptat.

Macro-urile C pot fi asemenea funcțiilor, creând într-o oarecare măsură sintaxă nouă și pot fi, de asemenea, mărite cu text arbitrar (deși compilatorul C cere ca textul să fie în cod C fără erori sau formatat ca comentariu), dar au unele limitări precum structurile software. Macro-urile asemănătoare unei funcții, de exemplu, pot fi numite ca funcții „reale”, dar o macrocomandă nu poate fi transmisă unei alte funcții folosind un pointer, deoarece macro-ul în sine nu are nicio adresă.

Unele limbi moderne nu folosesc de obicei acest tip de metaprogramare folosind macrocomenzi ca completări ale șirurilor de caractere, bazându-se pe cablarea automată sau manuală a funcțiilor și metodelor, ci în schimb pe alte modalități de abstractizare, cum ar fi șabloanele , funcțiile generice sau polimorfismul parametric . În special, funcțiile inline unul dintre deficiențele majore ale macrocomenzilor în versiunile moderne de C și C++, deoarece o funcție inline oferă avantajul macrocomenzilor în reducerea supraîncărcării unui apel de funcție, dar adresa sa poate fi transmisă într-un pointer pentru indirect. apeluri sau utilizate ca parametru. La fel, problema evaluărilor multiple menționată mai sus în macro-ul maxim este irelevantă pentru funcțiile încorporate.

Puteți înlocui constantele #define cu enumerari și macrocomenzi cu funcții inline.

Operatorii # și ##

Acești operatori sunt utilizați la crearea macrocomenzilor. Operatorul # înaintea unui parametru macro îl încadrează între ghilimele duble, de exemplu:

#define make_str( bar ) # bar printf ( make_str ( 42 ) );

preprocesorul se convertește în:

printf ( "42" );

Operatorul ## din macrocomandă concatenează două jetoane, de exemplu:

#define MakePosition( x ) x##X, x##Y, x##Width, x##Height int MakePosition ( Object );

preprocesorul se convertește în:

int ObjectX , ObjectY , ObjectWidth , ObjectHeight ; Descrierea oficială a macrosubstituțiilor

1) Linia de control din următoarea formă forțează preprocesorul să înlocuiască identificatorul cu o secvență de jetoane în restul textului programului:

#define identificator token_sequence

În acest caz, caracterele cu spații albe de la începutul și de la sfârșitul secvenței de jetoane sunt eliminate. O linie #define repetată cu același identificator este considerată o eroare dacă secvențele de jetoane nu sunt identice (nepotrivirile în caracterele de spațiu alb nu contează).

2) Un șir de forma următoare, în care nu trebuie să existe caractere de spațiu alb între primul identificator și paranteza de deschidere, este o definiție macro cu parametri specificați de identificator-list.

#define identifier(list_of_identifiers) sequence_of_tokens

Ca și în prima formă, caracterele de spații albe de la începutul și sfârșitul secvenței de jetoane sunt eliminate, iar macro-ul poate fi redefinită numai cu aceeași listă de parametri de număr și nume și aceeași secvență de jetoane.

O linie de control ca aceasta îi spune preprocesorului să „uite” definiția dată identificatorului:

#undef identificator

Aplicarea directivei #undef la un identificator nedefinit anterior nu este considerată o eroare.

{

  • Dacă definiția macro-ului a fost specificată în a doua formă, atunci orice șir suplimentar de caractere din textul programului, constând dintr-un identificator de macrocomandă (eventual urmat de caractere cu spațiu alb), o paranteză de deschidere, o listă de simboluri separate prin virgulă și o paranteză de închidere, constituie o macro invocare.
  • Argumentele apelului macro sunt simboluri separate prin virgulă, iar virgulele cuprinse între ghilimele sau parantezele imbricate nu participă la separarea argumentelor.
  • (!)La gruparea argumentelor, extinderea macro nu este efectuată în ele.
  • Numărul de argumente din apelul macro trebuie să se potrivească cu numărul de parametri de definiție macro.
  • După extragerea argumentelor din text, caracterele cu spații albe din jurul lor sunt eliminate.
  • Apoi, în secvența de înlocuire a jetoanelor macro, fiecare parametru-identificator necotat este înlocuit cu argumentul real corespunzător din text.
  • (!)Dacă parametrul nu este precedat de semnul # în secvența de înlocuire și nici înainte, nici după acesta nu este semnul ##, atunci tokenurile de argument sunt verificate pentru prezența apelurilor macro în ele; dacă există, atunci extinderea macrocomenzilor corespunzătoare este efectuată în ea înainte ca argumentul să fie înlocuit.

Procesul de înlocuire este afectat de două semne speciale de operator.

  • În primul rând, dacă un parametru dintr-un șir de jetoane de înlocuire este precedat de un semn #, atunci ghilimele (") sunt plasate în jurul argumentului corespunzător, iar apoi identificatorul parametrului, împreună cu semnul #, este înlocuit cu literalul șir rezultat. .
    • O bară oblică inversă este inserată automat înaintea fiecărui caracter " sau \ care apare în jurul sau în interiorul unui șir sau constantă de caractere.
  • În al doilea rând, dacă o secvență de jetoane dintr-o definiție macro de orice fel conține caracterul ##, atunci imediat după înlocuirea parametrilor, acesta, împreună cu caracterele de spațiu alb care o înconjoară, este aruncat, din cauza căreia jetoanele adiacente sunt concatenate, formând astfel un nou simbol.
    • Rezultatul este nedefinit atunci când jetoanele de limbă invalide sunt generate în acest mod sau când textul rezultat depinde de ordinea în care este aplicată operația ##.
    • În plus, caracterul ## nu poate apărea nici la începutul, nici la sfârșitul unei secvențe de înlocuire de jetoane.

}

  • (!) În macro-urile de ambele tipuri, secvența de înlocuire a jetoanelor este rescanată în căutarea de noi definitori-identificatori.
  • (!)Cu toate acestea, dacă vreun identificator a fost deja înlocuit în procesul de extindere curent, reapariția unui astfel de identificator nu va determina înlocuirea acestuia; va rămâne neatins.
  • (!)Chiar dacă linia extinsă de apel macro începe cu semnul #, nu va fi luată ca o directivă de preprocesor.

Un semn de exclamare (!) marchează regulile responsabile pentru invocarea recursivă și definițiile.

Exemplu de extindere a macrocomenzii #define cat( x, y ) x ## y

Apelul macro „cat(var, 123)” va fi înlocuit cu „var123”. Cu toate acestea, apelarea „cat(cat(1, 2), 3)” nu va produce rezultatul dorit. Luați în considerare pașii preprocesorului:

0: pisica( pisica( 1, 2 ), 3 ) 1: pisica ( 1, 2 ) ## 3 2: pisica( 1, 2 )3

Operația „##” a împiedicat extinderea corectă a argumentelor celui de-al doilea apel „pisica”. Rezultatul este următorul șir de jetoane:

pisica ( 1 , 2 ) 3

unde „)3” este rezultatul concatenării ultimului simbol al primului argument cu primul simbol al celui de-al doilea argument, nu este un simbol valid.

Puteți specifica al doilea nivel macro după cum urmează:

#define xcat( x, y ) cat( x, y )

Apelul „xcat(xcat(1, 2), 3)” va fi înlocuit cu „123”. Luați în considerare pașii preprocesorului:

0: xcat( xcat( 1, 2 ), 3 ) 1: pisică( xcat( 1, 2 ), 3 ) 2: pisica( pisica( 1, 2 ), 3 ) 3: pisica ( 1 ## 2, 3 ) 4: pisica (12, 3) 5:12##3 6:123

Totul a mers bine, deoarece operatorul „##” nu a participat la extinderea macrocomenzii „xcat”.

Mulți analizoare statice nu sunt capabili să proceseze macro-urile corect, astfel încât calitatea analizei statice este redusă .

Constante predefinite #define

Constante generate automat de preprocesor:

  • __LINE__este înlocuit cu numărul curent al liniei; numărul de linie curent poate fi înlocuit de directivă #line; folosit pentru depanare ;
  • __FILE__este înlocuit cu numele fișierului; numele fișierului poate fi, de asemenea, suprascris cu #line;
  • __FUNCTION__este înlocuit cu numele funcției curente;
  • __DATE__este înlocuit cu data curentă (la momentul în care codul este procesat de preprocesor);
  • __TIME__este înlocuit cu ora curentă (la momentul în care codul a fost procesat de preprocesor);
  • __TIMESTAMP__este înlocuit cu data și ora curente (la momentul în care codul a fost procesat de preprocesor);
  • __COUNTER__este înlocuit cu un număr unic începând de la 0; după fiecare înlocuire, numărul crește cu unul;
  • __STDC__se înlocuiește cu 1 dacă compilarea este în conformitate cu standardul limbajului C;
  • __STDC_HOSTED__definite în C99 și mai sus; este înlocuit cu 1 dacă execuția este sub control OS ;
  • __STDC_VERSION__definite în C99 și mai sus; pentru C99 se înlocuiește cu numărul 199901, iar pentru C11 se înlocuiește cu numărul 201112;
  • __STDC_IEC_559__definite în C99 și mai sus; constanta există dacă compilatorul acceptă operații în virgulă mobilă IEC 60559;
  • __STDC_IEC_559_COMPLEX__definite în C99 și mai sus; constanta există dacă compilatorul acceptă operații cu numere complexe IEC 60559; standardul C99 obligă să susțină operațiuni cu numere complexe;
  • __STDC_NO_COMPLEX__definit în C11; se înlocuiește cu 1 dacă operațiile cu numere complexe nu sunt acceptate;
  • __STDC_NO_VLA__definit în C11; înlocuit cu 1 dacă matricele de lungime variabilă nu sunt acceptate; tablourile cu lungime variabilă trebuie să fie acceptate în C99;
  • __VA_ARGS__definit în C99 și vă permite să creați macrocomenzi cu un număr variabil de argumente.

Compilare condiționată

Preprocesorul C oferă capacitatea de a compila cu condiții. Acest lucru permite posibilitatea unor versiuni diferite ale aceluiași cod. De obicei, această abordare este utilizată pentru a personaliza programul pentru platforma compilatorului, stare (codul depanat poate fi evidențiat în codul rezultat) sau capacitatea de a verifica conexiunea fișierului exact o dată.

În general, programatorul trebuie să folosească un construct precum:

# ifndef FOO_H # definește FOO_H ...( codul fișierului antet )... # endif

Această „protecție macro” împiedică un fișier antet să fie dublu inclus prin verificarea existenței acelei macrocomenzi, care are același nume ca fișierul antet. Definiția macrocomenzii FOO_H apare atunci când fișierul antet este procesat pentru prima dată de preprocesor. Apoi, dacă acest fișier antet este re-inclus, FOO_H este deja definit, determinând preprocesorul să omite întregul text al acestui fișier antet.

Același lucru se poate face prin includerea următoarei directive în fișierul antet:

# pragma o dată

Condițiile preprocesorului pot fi specificate în mai multe moduri, de exemplu:

# ifdef x ... #altfel ... # endif

sau

# ifx ... #altfel ... # endif

Această metodă este adesea folosită în fișierele antet de sistem pentru a testa diferite capabilități, a căror definiție poate varia în funcție de platformă; de exemplu, biblioteca Glibc folosește macrocomenzi de verificare a caracteristicilor pentru a verifica dacă sistemul de operare și hardware-ul le suportă corect (macromorile) menținând în același timp aceeași interfață de programare.

Majoritatea limbajelor de programare moderne nu profită de aceste caracteristici, bazându-se mai mult pe instrucțiunile condiționate tradiționale if...then...else..., lăsând compilatorului sarcina de a extrage cod inutil din programul compilat.

Digrafe și trigrafe

Vedeți digrafele și trigrafele în limbaje C/C++.

Preprocesorul prelucrează digrafele „ %:” (“ #”), “ %:%:” (“ ##”) și trigrafele “ ??=” (“ #”), “ ??/” (“ \”).

Preprocesorul consideră secvența „ %:%: ” ca fiind două jetoane atunci când procesează codul C și un simbol atunci când procesează codul C++.

Vezi și

Note

Link -uri