Preprocesor C

Preprocesor C jest preprocesorem makr dla komputerowych języków programowania C , Objective-C i C++ . Preprocesor zapewnia możliwość dołączania plików nagłówkowych , rozszerzeń makr , kompilacji warunkowej i kontroli linii.

W wielu implementacjach C jest to osobny program wywoływany przez kompilator jako pierwsza część tłumaczenia .

Język dyrektyw preprocesora jest tylko słabo powiązany z gramatyką C, dlatego czasami jest używany do przetwarzania innego rodzaju plików tekstowych .

Historia

Preprocesor został wprowadzony do C około 1973 roku za namową Alana Snydera, a także w uznaniu użyteczności mechanizmów włączania plików dostępnych w BCPL i PL/I . Jego oryginalna wersja oferowała jedynie włączanie plików i proste zastępowanie ciągów znaków przy użyciu #include i #define dla makr bez parametrów. Wkrótce potem został rozszerzony, najpierw przez Mike'a Leska , a następnie przez Johna Reisera, w celu włączenia makr z argumentami i kompilacją warunkową.

Preprocesor C był częścią długiej tradycji makrojęzyków w Bell Labs, zapoczątkowanej przez Douglasa Eastwooda i Douglasa McIlroya w 1959 roku.

Fazy

Przetwarzanie wstępne jest zdefiniowane przez pierwsze cztery (z ośmiu) faz tłumaczenia określonych w standardzie C.

  1. Zastąpienie trygrafu: preprocesor zastępuje sekwencje trygrafów znakami, które reprezentują.
  2. Łączenie linii: Fizyczne linie źródłowe, które są kontynuowane sekwencjami znaku nowej linii , są łączone w celu utworzenia linii logicznych.
  3. Tokenizacja: preprocesor dzieli wynik na tokeny przetwarzania wstępnego i spacje . Zastępuje komentarze białymi znakami.
  4. Ekspansja makr i obsługa dyrektyw: Wykonywane są wiersze dyrektyw przetwarzania wstępnego, w tym włączanie plików i kompilacja warunkowa. Preprocesor jednocześnie rozwija makra i od wersji standardu C z 1999 roku obsługuje operatory _Pragma .

Łącznie z plikami

Jednym z najczęstszych zastosowań preprocesora jest dołączenie innego pliku:

 

 

    
     0
 #include  <stdio.h>  int  main  (  void  )  {  printf  (  "Witaj, świecie!  \n  "  );  powrót  ;  } 

Preprocesor zastępuje wiersz #include <stdio.h> tekstową zawartością pliku „stdio.h”, który między innymi deklaruje funkcję printf() .

Można to również zapisać za pomocą podwójnych cudzysłowów, np. #include "stdio.h" . Jeśli nazwa pliku jest ujęta w nawiasy ostre, plik jest wyszukiwany w standardowym kompilatorze z uwzględnieniem ścieżek. Jeśli nazwa pliku jest ujęta w podwójne cudzysłowy, ścieżka wyszukiwania jest rozszerzana w celu uwzględnienia bieżącego katalogu pliku źródłowego. Kompilatory C i środowiska programistyczne mają funkcję, która pozwala programiście określić, gdzie można znaleźć pliki dołączane. Można to wprowadzić za pomocą flagi wiersza poleceń, którą można sparametryzować za pomocą makefile , tak aby na przykład inny zestaw plików dołączanych mógł zostać zamieniony dla różnych systemów operacyjnych.

Zgodnie z konwencją nazwy plików dołączanych mają rozszerzenie .h lub .hpp . Nie ma jednak wymogu, aby to było przestrzegane. Pliki z .def mogą oznaczać pliki przeznaczone do wielokrotnego dołączania, za każdym razem rozszerzające tę samą powtarzalną treść; #include „icon.xbm” prawdopodobnie odnosi się do pliku obrazu XBM (który jest jednocześnie plikiem źródłowym C).

#include często wymusza użycie strażników #include lub #pragma raz , aby zapobiec podwójnemu włączeniu.

Kompilacja warunkowa

Dyrektywy if - else #if , #ifdef , #ifndef , #else , #elif i #endif mogą być używane do kompilacji warunkowej . #ifdef i #ifndef to proste skróty dla #jeśli zdefiniowano(...) i #jeśli !zdefiniowano(...) .


  
 #if VERBOSE >= 2  printf  (  "śledzenie wiadomości"  );  #endif 

Większość kompilatorów przeznaczonych dla systemu Microsoft Windows niejawnie definiuje _WIN32 . Dzięki temu kod, w tym polecenia preprocesora, może się kompilować tylko w przypadku systemów Windows. kilka kompilatorów definiuje WIN32 . W przypadku takich kompilatorów, które niejawnie nie definiują _WIN32 , można je określić w wierszu polecenia kompilatora przy użyciu -D_WIN32 .


  

  
 #ifdef __unix__  /* __unix__ jest zwykle definiowany przez kompilatory przeznaczone dla systemów Unix */  #  include  <unistd.h>  #elif zdefiniowany _WIN32  /* _WIN32 jest zwykle definiowany przez kompilatory przeznaczone dla 32- lub 64-bitowych systemów Windows */ #  include  <  windows.h >  #endif 

Przykładowy kod sprawdza, czy zdefiniowane jest makro __unix__ . Jeśli tak, dołączany jest plik <unistd.h> . W przeciwnym razie sprawdza, czy zamiast tego zdefiniowane jest makro _WIN32 . Jeśli tak, dołączany jest plik <windows.h> .

Bardziej złożony przykład #if może wykorzystywać operatory, na przykład coś takiego:


	

	
 #if!(zdefiniowano __LP64__ || zdefiniowano __LLP64__) || zdefiniowano _WIN32 && !zdefiniowano _WIN64   // kompilujemy dla systemu 32-bitowego  #else  // kompilujemy dla systemu 64-bitowego  #endif 

Tłumaczenie może również spowodować niepowodzenie przy użyciu dyrektywy #error :



 #if RUBY_VERSION == 190  #błąd 1.9.0 nieobsługiwany  #endif 

Definicja i rozszerzenie makr

Istnieją dwa rodzaje makr, obiektowe i funkcyjne . Makra obiektowe nie przyjmują parametrów; robią to makra funkcyjne (chociaż lista parametrów może być pusta). Ogólna składnia deklarowania identyfikatora jako makro każdego typu to odpowiednio:


 #define <identyfikator> <lista tokenów zastępczych>  // makro obiektowe  #define <identyfikator>(<lista parametrów>) <lista tokenów zastępczych>  // makro funkcyjne, zanotuj parametry 

Deklaracja makropodobna do funkcji nie może zawierać spacji między identyfikatorem a pierwszym nawiasem otwierającym. Jeśli obecne są białe znaki, makro zostanie zinterpretowane jako obiektowe, a wszystko, począwszy od pierwszego nawiasu, zostanie dodane do listy tokenów.

Definicja makra może zostać usunięta za pomocą #undef :

 #undef <identyfikator>  // usuń makro 

Ilekroć identyfikator pojawia się w kodzie źródłowym, jest zastępowany listą tokenów zastępczych, która może być pusta. W przypadku identyfikatora zadeklarowanego jako makro podobne do funkcji jest on zastępowany tylko wtedy, gdy następujący token jest również lewym nawiasem rozpoczynającym listę argumentów wywołania makra. Dokładna procedura stosowana do rozszerzania makr funkcyjnych za pomocą argumentów jest subtelna.

Makra obiektowe były tradycyjnie używane jako część dobrej praktyki programistycznej do tworzenia symbolicznych nazw stałych, np.

# zdefiniuj PI 3.14159

zamiast twardych numerów w całym kodzie. Alternatywą zarówno w C, jak i C++, szczególnie w sytuacjach, w których wymagany jest wskaźnik do liczby, jest zastosowanie kwalifikatora const do zmiennej globalnej. Powoduje to, że wartość jest przechowywana w pamięci, zamiast być zastępowana przez preprocesor.

Przykładem makra podobnego do funkcji jest:

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

Definiuje to konwersję radianów na stopnie, którą można wstawić do kodu tam, gdzie jest to wymagane, tj. RADTODEG(34) . Jest to rozwijane w miejscu, dzięki czemu wielokrotne mnożenie przez stałą nie jest pokazywane w całym kodzie. Makro tutaj jest napisane wielkimi literami, aby podkreślić, że jest to makro, a nie skompilowana funkcja.

Drugi x jest ujęty we własną parę nawiasów, aby uniknąć możliwości nieprawidłowej kolejności operacji , gdy jest to wyrażenie zamiast pojedynczej wartości. Na przykład wyrażenie RADTODEG ( r + 1 ) rozwija się poprawnie jako (( r + 1 ) * 57,29578 ) ; bez nawiasów ( r + 1 * 57,29578 ) daje pierwszeństwo mnożeniu.

Podobnie zewnętrzna para nawiasów zachowuje poprawną kolejność działań. Na przykład 1 / RADTODEG ( r ) rozwija się do 1 / ( ( r ) * 57,29578 ) ; bez nawiasów, 1 / ( r ) * 57,29578 daje pierwszeństwo dzieleniu.

Kolejność ekspansji

makropodobna do funkcji przebiega w następujących etapach:

  1. Operacje ciągnienia są zastępowane tekstową reprezentacją listy zastępczej ich argumentów (bez wykonywania interpretacji).
  2. Parametry są zastępowane ich listą zastępczą (bez wykonywania interpretacji).
  3. Operacje konkatenacji są zastępowane połączonym wynikiem dwóch operandów (bez rozwijania wynikowego tokena).
  4. Tokeny pochodzące z parametrów są rozwijane.
  5. Powstałe żetony są rozszerzane w normalny sposób.

Może to dać zaskakujące rezultaty:







  
  
  #define HE HI  #define LLO _THERE  #define HELLO "CZEŚĆ TAM"  #define CAT(a,b) a##b  #define XCAT(a,b) CAT(a,b)  #define CALL(fn) fn(HE, LLO)  CAT  (  HE  ,  LLO  )  // „HI THERE”, ponieważ konkatenacja następuje przed normalną interpretacją  XCAT  (  HE  ,  LLO  )  // HI_THERE, ponieważ tokeny pochodzące z parametrów („HE” i „LLO”) są interpretowane jako pierwsze  CALL  (  CAT  )  // „Cześć, tam”, ponieważ najpierw interpretowane są parametry 

Specjalne makra i dyrektywy

Niektóre symbole muszą zostać zdefiniowane przez implementację podczas przetwarzania wstępnego. Należą do nich __FILE__ i __LINE__ , predefiniowane przez sam preprocesor, które rozwijają się do bieżącego pliku i numeru wiersza. Na przykład następujące:











    // debugowanie makr, abyśmy mogli szybko określić pochodzenie wiadomości  // jest złe  #define WHERESTR "[file %s, line %d]: "  #define WHEREARG __FILE__, __LINE__  #define DEBUGPRINT2(...) fprintf(stderr , __VA_ARGS__)  #define DEBUGPRINT(_fmt, ...) DEBUGPRINT2(WHERESTR _fmt, WHEREARG, __VA_ARGS__)  // LUB  // jest dobry  #define DEBUGPRINT(_fmt, ...) fprintf(stderr, "[plik %s, linia %d]: " _fmt, __FILE__, __LINE__, __VA_ARGS__)  DEBUGPRINT  (  "hej, x=%d  \n  "  ,  x  ); 

wypisuje wartość x , poprzedzoną numerem pliku i linii do strumienia błędów, umożliwiając szybki dostęp do linii, w której wiadomość została wyprodukowana. Zauważ, że WHERESTR jest połączony z ciągiem znaków następującym po nim. Wartościami __FILE__ i __LINE__ można manipulować za pomocą dyrektywy #line . Dyrektywa #line określa numer linii i nazwę pliku linii poniżej. Np:


   #line 314 "pi.c"  printf  (  "line=%d plik=%s  \n  "  ,  __LINE__  ,  __FILE__  ); 

generuje funkcję printf:

   printf  (  "linia=%d plik=%s  \n  "  ,  314  ,  "pi.c"  ); 

Debugery kodu źródłowego odwołują się również do pozycji źródłowej zdefiniowanej za pomocą __FILE__ i __LINE__ . Pozwala to na debugowanie kodu źródłowego, gdy C jest używany jako język docelowy kompilatora, dla zupełnie innego języka. Pierwszy standard C określał, że makro __STDC__ jest zdefiniowane na 1, jeśli implementacja jest zgodna ze standardem ISO, a 0 w przeciwnym razie, a makro __STDC_VERSION__ jest zdefiniowane jako literał liczbowy określający wersję standardu obsługiwaną przez implementację. Standardowe kompilatory języka C++ obsługują __cplusplus . Kompilatory działające w trybie niestandardowym nie mogą ustawiać tych makr lub muszą definiować inne, aby zasygnalizować różnice.

Inne standardowe makra to __DATE__ , bieżąca data i __TIME__ , bieżąca godzina.

Druga edycja standardu C, C99 , dodała obsługę __func__ , która zawiera nazwę definicji funkcji, w której jest zawarta, ale ponieważ preprocesor jest niezależny od gramatyki C, należy to zrobić w samym kompilatorze, używając zmienna lokalna dla funkcji.

Makra, które mogą przyjmować różną liczbę argumentów ( makra zmienne ) nie są dozwolone w C89, ale zostały wprowadzone przez wiele kompilatorów i ustandaryzowane w C99 . Makra Variadic są szczególnie przydatne podczas pisania opakowań do funkcji przyjmujących zmienną liczbę parametrów, takich jak printf , na przykład podczas rejestrowania ostrzeżeń i błędów.

Jeden z mało znanych wzorców użycia preprocesora C jest znany jako X-Macros . X-Macro to plik nagłówkowy . Zwykle używają one rozszerzenia „.def” zamiast tradycyjnego „.h”. Ten plik zawiera listę podobnych wywołań makr, które można nazwać „makrami składowymi”. Plik dołączany jest następnie wielokrotnie przywoływany.

Wiele kompilatorów definiuje dodatkowe, niestandardowe makra, chociaż często są one słabo udokumentowane. Wspólnym odniesieniem do tych makr jest projekt Predefiniowane makra kompilatora C/C++ , który zawiera listę „różnych predefiniowanych makr kompilatora, których można użyć do identyfikacji standardów, kompilatorów, systemów operacyjnych, architektur sprzętowych, a nawet podstawowych bibliotek wykonawczych w czasie kompilacji".

Stringizacja tokena

Operator # (znany jako „Operator ciągu znaków”) konwertuje token na literał łańcuchowy języka C , odpowiednio unikając cudzysłowów lub odwrotnych ukośników.

Przykład:



   
            #define str(s) #s  str  (  p  =  "foo  \n  "  ;)  // wyniki "p = \"foo\\n\";"  str  (  \  n  )  // wyświetla "\n" 

Jeśli chcesz uporządkować rozwinięcie argumentu makra, musisz użyć dwóch poziomów makr:





   
   #define xstr(s) str(s)  #define str(s) #s  #define foo 4  str  (  foo  )  // wyprowadza „foo”  xstr  (  foo  )  // wyprowadza „4” 

Nie można połączyć argumentu makro z dodatkowym tekstem i połączyć go w ciąg. Możesz jednak napisać serię sąsiadujących stałych łańcuchowych i argumentów: kompilator C połączy następnie wszystkie sąsiednie stałe łańcuchowe w jeden długi łańcuch.

Łączenie tokenów

Operator ## (znany jako „Operator wklejania tokenów”) łączy dwa tokeny w jeden token.

Przykład:



  #define DECLARE_STRUCT_TYPE(nazwa) typedef nazwa struktury ##_s nazwa##_t  DECLARE_STRUCT_TYPE  (  g_object  );  // Dane wyjściowe: typedef struct g_object_s g_object_t; 

Zdefiniowane przez użytkownika błędy kompilacji

Dyrektywa #error wysyła komunikat przez strumień błędów.

#error "komunikat o błędzie"

Implementacje

Wszystkie implementacje C, C++ i Objective-C zapewniają preprocesor, ponieważ wstępne przetwarzanie jest wymaganym krokiem dla tych języków, a jego zachowanie jest opisane przez oficjalne standardy dla tych języków, takie jak standard ISO C.

Wdrożenia mogą zapewniać własne rozszerzenia i odstępstwa oraz różnić się stopniem zgodności z pisemnymi standardami. Ich dokładne zachowanie może zależeć od flag wiersza poleceń dostarczanych podczas wywołania. Na przykład preprocesor GNU C można uczynić bardziej zgodnym ze standardami, dostarczając określone flagi.

Funkcje preprocesora specyficzne dla kompilatora

Dyrektywa #pragma jest dyrektywą specyficzną dla kompilatora , której dostawcy kompilatorów mogą używać do własnych celów. Na przykład #pragma jest często używany do pomijania określonych komunikatów o błędach, zarządzania debugowaniem sterty i stosu i tak dalej. Kompilator obsługujący OpenMP może automatycznie zrównoleglać pętlę for z #pragma omp parallel for .

C99 wprowadził kilka standardowych dyrektyw #pragma , przyjmując postać #pragma STDC ... , które służą do sterowania implementacją zmiennoprzecinkową. Dodano również alternatywną, przypominającą makro formę _Pragma(...) .

  • Wiele implementacji nie obsługuje trygrafów lub domyślnie ich nie zastępuje.
  • Wiele implementacji (w tym np. kompilatory C GNU, Intela, Microsoftu i IBM) dostarcza niestandardową dyrektywę, która wyświetla komunikat ostrzegawczy na wyjściu, ale nie zatrzymuje procesu kompilacji. Typowym zastosowaniem jest ostrzeżenie o użyciu starego kodu, który jest obecnie przestarzały i dołączony tylko ze względu na kompatybilność, np.:
    
     // GNU, Intel i IBM  #warning "Nie używaj ABC, które jest przestarzałe. Zamiast tego użyj XYZ." 
    
    
     // Microsoft  #pragma message("Nie używaj ABC, które jest przestarzałe. Zamiast tego użyj XYZ.") 
    
  • Niektóre preprocesory Uniksa tradycyjnie dostarczały „asercji”, które mają niewielkie podobieństwo do asercji używanych w programowaniu.
  • GCC zapewnia #include_next do łączenia nagłówków o tej samej nazwie.
  • Objective-C mają #import , który jest podobny do #include , ale zawiera plik tylko raz. Częstym pragmatem dostawcy o podobnej funkcjonalności w C jest #pragma Once .

Inne zastosowania

Ponieważ preprocesor C może być wywoływany niezależnie od kompilatora, z którym jest dostarczany, może być używany oddzielnie w różnych językach. Godne uwagi przykłady obejmują jego użycie w przestarzałym imake i do wstępnego przetwarzania Fortran . Jednak takie użycie jako preprocesora ogólnego przeznaczenia jest ograniczone: język wejściowy musi być wystarczająco podobny do C. Kompilator GNU Fortran automatycznie wywołuje „tryb tradycyjny” (patrz poniżej) cpp przed kompilacją kodu Fortran, jeśli używane są określone rozszerzenia plików. Intel oferuje preprocesor Fortran, fpp, do użytku z ifort , który ma podobne możliwości.

CPP działa również zadowalająco z większością języków asemblera i języków podobnych do Algola. Wymaga to, aby składnia języka nie kolidowała ze składnią CPP, co oznacza, że ​​żadne linie nie zaczynają się od # , a podwójne cudzysłowy, które cpp interpretuje jako literały łańcuchowe , a zatem ignoruje, nie mają innego znaczenia składniowego. „Tryb tradycyjny” (działający jak preprocesor pre-ISO C) jest generalnie bardziej liberalny i lepiej nadaje się do takiego zastosowania.

Preprocesor C nie jest Turing-complete , ale jest bardzo zbliżony: można określić obliczenia rekurencyjne, ale ze stałą górną granicą ilości wykonywanej rekurencji. Jednak preprocesor C nie został zaprojektowany jako język programowania ogólnego przeznaczenia ani nie działa dobrze. Ponieważ preprocesor C nie ma cech niektórych innych preprocesorów, takich jak makra rekurencyjne, selektywne interpretowanie według cudzysłowów i obliczanie łańcuchów w trybie warunkowym, jest bardzo ograniczony w porównaniu z bardziej ogólnymi makroprocesorami, takimi jak m4 .

Zobacz też

Źródła

Linki zewnętrzne