Wstęp Preprocesor jest to program, który analizuje plik źródłowy (programu, biblioteki) w poszukiwaniu wszystkich wyrażeń zaczynających się od "#". Na podstawie tych instrukcji generuje on kod w "czystym" języku C, który następnie jest kompilowany przez kompilator. Ponieważ za pomocą preprocesora można niemal "sterować" kompilatorem, daje on niezwykłe możliwości, które nie były dotąd znane w innych językach programowania.
W języku C wszystkie linijki zaczynające się od symbolu "#" są instrukcjami ( dyrektywami) dla preprocesora. Nie są elementami języka C i nie podlegają bezpośrednio procesowi kompilacji.
Preprocesor może być: standardowy = element kompilatora niestandardowy stackoverflow questions: replacements-for-the-c-preprocessor: CPIP napisany w Pythonie GNU M4 Cog Wave parser (Boost) własny program napisany w: Lua PHP, uruchamiamy z konsoli perl
Przełącznik kompilatora Aby przekonać się, jak wygląda kod przetworzony przez preprocesor, użyj (w kompilatorze gcc) przełącznika "-E":
gcc test.c -E -o test.txt
W pliku test.txt zostanie umieszczony cały kod w postaci, która zdatna jest do przetworzenia przez kompilator.
Dyrektywy preprocesora Dyrektywy preprocesora są to wyrażenia, które zapoczątkowane są symbolem "#" i to właśnie za ich pomocą możemy używać preprocesora. Dyrektywa zaczyna się od znaku # i kończy się wraz z końcem linii. Aby przenieść dalszą część dyrektywy do następnej linii, należy zakończyć linię znakiem "\": #define ADD(a,b) \ a+b
Omówimy teraz kilka ważniejszych dyrektyw.
#include Najpopularniejsza dyrektywa, wstawiająca w swoje miejsce treść pliku podanego w nawiasach ostrych lub cudzysłowie. Składnia:
Przykład 1 #include
Przykład 2 #include "plik_nagłówkowy_do_dołączenia"
Jeżeli nazwa pliku nagłówkowego będzie ujęta w nawiasy ostre (przykład 1), to kompilator poszuka go wśród własnych plików nagłówkowych (które najczęściej się znajdują w podkatalogu "includes" w katalogu kompilatora). Jeśli jednak nazwa ta będzie ujęta w podwójne cudzysłowy(przykład 2), to kompilator poszuka jej w katalogu, w którym znajduje się kompilowany plik (można zmienić to zachowanie w opcjach niektórych kompilatorów). Przy użyciu tej dyrektywy można także wskazać dokładne położenie plików nagłówkowych poprzez wpisanie bezwzględnej lub względnej ścieżki dostępu do tego pliku nagłówkowego.
Przykład 3 - ścieżka bezwzględna do pliku nagłówkowego w Linuksie i w Windowsie Opis: W miejsce jednej i drugiej linijki zostanie wczytany plik umieszczony w danej lokalizacji
#include "/usr/include/plik_nagłówkowy.h" #include "C:\\borland\includes\plik_nagłówkowy.h"
Przykład 4 - ścieżka względna do pliku nagłówkowego Opis: W miejsce linijki zostanie wczytany plik umieszczony w katalogu "katalog1", a ten katalog jest w katalogu z plikiem źródłowym. Inaczej mówiąc, jeśli plik źródłowy jest w katalogu "/home/user/dokumenty/zrodla", to plik nagłówkowy jest umieszczony w katalogu "/home/user/dokumenty/zrodla/katalog1" #include "katalog1/plik_naglowkowy.h"
Przykład 5 - ścieżka względna do pliku nagłówkowego Opis: Jeśli plik źródłowy jest umieszczony w katalogu "/home/user/dokumenty/zrodla", to plik nagłówkowy znajduje się w katalogu "/home/user/dokumenty/katalog1/katalog2/" #include "../katalog1/katalog2/plik_naglowkowy.h"
Więcej informacji możesz uzyskać w rozdziale Biblioteki.
#define Linia pozwalająca zdefiniować: stałą, funkcję słowo kluczowe, makro które będzie potem podmienione w kodzie programu na odpowiednią wartość lub może zostać użyte w instrukcjach warunkowych dla preprocesora.
Składnia:
#define NAZWA_STALEJ WARTOSC
lub #define NAZWA_STALEJ
Przykład: #define LICZBA 8 - spowoduje, że każde wystąpienie słowa LICZBA w kodzie zostanie zastąpione ósemką. #define SUMA(a,b) ((a)+(b)) - spowoduje, że każde wystąpienie wywołania "funkcji" SUMA zostanie zastąpione przez sumę argumentów
Jeśli w miejscu wartości znajduje się wyrażenie, to należy je umieścić w nawiasach. #define A 5 #define B ((2)+(A))
Unikniemy w ten sposób niespodzianek związanych z priorytetem operatorów :
/*
https://github.com/Keith-S-Thompson/42/blob/master/42.c This small C program demonstrates the danger of improper use of the C preprocessor. Keith-S-Thompson
*/ #include
#define SIX 1+5 #define NINE 8+1
int main(void) { printf("%d * %d = %d\n", SIX, NINE, SIX * NINE); return 0;
Po skompilowaniu i uruchomieniu programu otrzymujemy:
6 * 9 = 42
a powinno być:
6 * 9 = 54
Przyczyną błędu jest interpretacja wyrażenia:
1+5*8+1
Ze względu na brak nawiasów i priorytet operatorów (wyższy * niż +) jest to interpretowane jako:
1+(5*8)+1
a nie jak:
(1+5)*(8+1) redefine
C nie obsługuje żadnych dodatkowych dyrektyw służących do przedefiniowania istniejącego makra.codeforwin: c-program-to-define-undefine-redefine-macro by Pankaj Prakash
To samo makro można zdefiniować dowolną ilość razy. Jednakże. spowoduje to zapełnienie stosu ostrzeżeń kompilatora. Dlatego zawsze zaleca się najpierw oddefiniowanie istniejącego makra, a następnie zdefiniowanie go ponownie. // Define a macro #define PI 3.14
// Undefine before redefining #ifdef PI #undef PI #endif
// Redefine PI #define PI 3.14159
#undef Ta instrukcja odwołuje definicję wykonaną instrukcją #define.
#undef STALA
instrukcje warunkowe Preprocesor zawiera również instrukcje warunkowe, pozwalające na wybór tego co ma zostać skompilowane w zależności od tego, czy stała jest zdefiniowana lub jaką ma wartość:
#if #elif #else #endif Te instrukcje uzależniają kompilacje od warunków. Ich działanie jest podobne do instrukcji warunkowych w samym języku C. I tak:
#if wprowadza warunek, który jeśli nie jest prawdziwy powoduje pominięcie kompilowania kodu, aż do napotkania jednej z poniższych instrukcji. #else spowoduje skompilowanie kodu jeżeli warunek za #if jest nieprawdziwy, aż do napotkania któregoś z poniższych instrukcji. #elif wprowadza nowy warunek, który będzie sprawdzony jeżeli poprzedni był nieprawdziwy. Stanowi połączenie instrukcji #if i #else. #endif zamyka blok ostatniej instrukcji warunkowej.
Przykład: #if INSTRUKCJE == 2 printf ("Podaj liczbę z przedziału 10 do 0\n"); /*1*/ #elif INSTRUKCJE == 1 printf ("Podaj liczbę: "); /*2*/ #else printf ("Podaj parametr: "); /*3*/ #endif scanf ("%d", &liczba); /*4*/ wiersz nr 1 zostanie skompilowany jeżeli stała INSTRUKCJE będzie równa 2 wiersz nr 2 zostanie skompilowany, gdy INSTRUKCJE będzie równa 1 wiersz nr 3 zostanie skompilowany w pozostałych wypadkach wiersz nr 4 będzie kompilowany zawsze
#ifdef #ifndef #else #endif Te instrukcje warunkują kompilację od tego, czy odpowiednia stała została zdefiniowana.
#ifdef spowoduje, że kompilator skompiluje poniższy kod tylko gdy została zdefiniowana odpowiednia stała. #ifndef ma odwrotne działanie do #ifdef, a mianowicie brak definicji odpowiedniej stałej umożliwia kompilacje poniższego kodu. #else,#endif mają identyczne zastosowanie jak te z powyższej grupy
Przykład: #define INFO /*definicja stałej INFO*/ #ifdef INFO printf ("Twórcą tego programu jest Jan Kowalski\n");/*1*/ #endif #ifndef INFO printf ("Twórcą tego programu jest znany programista\n");/*2*/ #endif To czy dowiemy się kto jest twórcą tego programu zależy czy instrukcja definiująca stałą INFO będzie istnieć. W powyższym przypadku na ekranie powinno się wyświetlić
Twórcą tego programu jest Jan Kowalski
#error Powoduje przerwanie kompilacji i wyświetlenie tekstu, który znajduje się za tą instrukcją. Przydatne gdy chcemy zabezpieczyć się przed zdefiniowaniem nieodpowiednich stałych.
Przykład: #ifdef BLAD #error Poważny błąd kompilacji #endif Co jeżeli zdefiniujemy stałą BLAD, oczywiście przy pomocy dyrektywy #define? Spowoduje to wyświetlenie w trakcie kompilacji komunikatu podobnego do poniższego: Fatal error program.c 6: Error directive: Poważny błąd kompilacji in function main() *** 1 errors in Compile *** wraz z przerwaniem kompilacji.
#warning Wyświetla tekst, jako ostrzeżenie. Jest często używany do sygnalizacji programiście, że dana część programu jest przestarzała lub może sprawiać problemy.
Przykład:
#warning To jest bardzo prosty program
Spowoduje to takie oto zachowanie kompilatora: test.c:3:2: warning: #warning To jest bardzo prosty program Użycie dyrektywy #warning nie przerywa procesu kompilacji i służy tylko do wyświetlania komunikatów dla programisty w czasie kompilacji programu.
#line Powoduje wyzerowanie licznika linii kompilatora, który jest używany przy wyświetlaniu opisu błędów kompilacji. Pozwala to na szybkie znalezienie możliwej przyczyny błędu w rozbudowanym programie.
Przykład: printf ("Podaj wartość funkcji"); #line printf ("W przedziale od 10 do 0\n); /* tutaj jest błąd - brak cudzysłowu zamykającego */ Jeżeli teraz nastąpi próba skompilowania tego kodu to kompilator poinformuje, że wystąpił błąd składni w linii 1, a nie np. 258.
#pragma
Dyrektywa pragma (od angielskiego: pragmatic information) służy do określania i sterowania funkcjami specyficznymi dla kompilatora i docelowej maszyny wykonującej kod. Listę dostępnych dyrektyw #pragma można znaleźć w dokumentacji kompilatora. Służą one m. in. do wymuszania lokalizacji zmiennych i kodu funkcji w pamięci lub tworzenia dodatkowych wątków z użyciem OpenMP.
# oraz ## Dość ciekawe możliwości ma w makrach znak "#". Zamienia on stojący za nim identyfikator na napis. #include #define wypisz(x) printf("%s=%i\n", #x, x)
int main() { int i=1; char a=5; wypisz(i); wypisz(a); return 0; }
Program wypisze: i=1 a=5
Czyli wypisz(a) jest rozwijane w printf("%s=%i\n", "a", a).
Natomiast znaki "##" łączą dwie nazwy w jedną. Przykład: #include
#define abc(x) int x##_zmienna #define wypisz(y) printf("%s=%i", #y, y) int main() { abc(nasza) = 2; // Robi dwie rzeczy: // 1. Wstawia słowo „nasza” przed słowem „ _zmienna”. // Dzięki temu zadeklarujemy zmienną o nazwie "nasza_zmienna". // 2. Inicjalizuje „nasza_zmienna” wartością "2". wypisz(nasza_zmienna); return 0; }
Program wypisze: nasza_zmienna=2
Więcej o dobrych zwyczajach w tworzeniu makr można się dowiedzieć w rozdziale Powszechne praktyki.
Makra Preprocesor języka C umożliwia też tworzenie makrmike ash : c-macro-tips-and-tricksHow to properly use macros in C by Preslav Mihaylov, czyli automatycznie wykonywanych czynności.
Makra deklaruje się za pomocą dyrektywy #define: #define MAKRO(arg1, arg2, ...) (wyrażenie) /* można również napisać: do {instrukcje} while(0) */ /* lub jeśli jest tylko jedna instrukcja można napisać: instrukcja (bez średnika!) */
W momencie wystąpienia MAKRA w tekście, preprocesor automatycznie zamieni makro na wyrażenie lub instrukcje. Makra mogą być pewnego rodzaju alternatywami dla funkcji, ale powinno się ich używać tylko w specjalnych przypadkach. Ponieważ makro sprowadza się do prostego zastąpienia przez preprocesor wywołania makra przez jego tekst, jest bardzo podatne na trudne do zlokalizowania błędy (kompilator będzie podawał błędy w miejscach, w których nic nie widzimy - bo preprocesor wstawił tam tekst). Makra są szybsze (nie następuje wywołanie funkcji, które zawsze zajmuje trochę czasu), ale też mniej bezpieczne i elastyczne niż funkcje.
Przeanalizujmy teraz fragment kodu: #include #define KWADRAT(x) ((x)*(x))
int main () { printf ("2 do kwadratu wynosi %d\n", KWADRAT(2)); return 0; }
Preprocesor w miejsce wyrażenia KWADRAT(2) wstawił ((2)*(2)). Zastanówmy się, co stałoby się, gdybyśmy napisali KWADRAT("2"). Preprocesor po prostu wstawi napis do kodu, co da wyrażenie (("2")*("2")), które jest nieprawidłowe. Kompilator zgłosi błąd, ale programista widzi tylko w kodzie użycie makra a nie prawdziwą przyczynę błędu. Widać tu, że bezpieczniejsze jest użycie funkcji, które dają możliwość wyspecyfikowania typów argumentów.
Nawet jeżeli program się skompiluje to makro może dawać nieoczekiwany wynik. Jest tak w przypadku poniższego kodu: int x = 1; int y = KWADRAT(++x);