C/Preprocesor
Z Wikibooks, biblioteki wolnych podręczników.
Spis treści |
[edytuj] Wstęp
W języku C wszystkie linijki zaczynające się od symbolu "#" nie podlegają bezpośrednio procesowi kompilacji. Są to natomiast instrukcje preprocesora – elementu kompilatora, który analizuje plik źródłowy 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. 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.
[edytuj] Dyrektywy preprocesora
Dyrektywy preprocesora są to wyrażenia, które występują zaraz za 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.
[edytuj] #include
Najpopularniejsza dyrektywa, wstawiająca w swoje miejsce treść pliku podanego w nawiasach ostrych lub cudzysłowie. Składnia:
Przykład 1
#include <plik_naglowkowy_do_dolaczenia>[1]
Przykład 2
#include "plik_naglowkowy_do_dolaczenia"
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.
[edytuj] #define
Linia pozwalająca zdefiniować stałą, funkcję lub słowo kluczowe, 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, ze każde wystąpienie wywołania "funkcji" SUMA zostanie zastąpione przez sumę argumentów
[edytuj] #undef
Ta instrukcja odwołuje definicję wykonaną instrukcją #define.
#undef STALA
[edytuj] 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ść:
[edytuj] #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\n", &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
[edytuj] #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
[edytuj] #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:
#if BLAD == 1 #error "Poważny błąd kompilacji" #endif
Co jeżeli zdefiniujemy stałą BLAD z wartością 1? 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.
[edytuj] #warning
Wyświetla tekst, zawarty w cudzysłowach, 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.
[edytuj] #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.
[edytuj] Makra
Preprocesor języka C umożliwia też tworzenie makr, czyli automatycznie wykonywanych czynności. Makra deklaruje się za pomocą dyrektywy #define:
#define MAKRO(arg1, arg2, ...) (wyrażenie)
W momencie wystąpienia MAKRA w tekście, preprocesor automatycznie zamieni makro na wyrażenie. 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[2]), ale też mniej bezpieczne i elastyczne niż funkcje.
Przeanalizujmy teraz fragment kodu:
#include <stdio.h>
#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);
Dzieje się tak dlatego, że makra rozwijane są przez preprocesor i kompilator widzi kod:
int x = 1; int y = ((++x)*(++x));
Również poniższe makra są błędne pomimo, że opisany problem w nich nie występuje:
#define SUMA(a, b) a + b #define ILOCZYN(a, b) a * b
Dają one nieoczekiwane wyniki dla wywołań:
SUMA(2, 2) * 2; /* 6 zamiast 8 */ ILOCZYN(2 + 2, 2 + 2); /* 8 zamiast 16 */
Z tego powodu istotne jest użycie nawiasów:
#define SUMA(a, b) ((a) + (b)) #define ILOCZYN(a, b) ((a) * (b))
[edytuj] # oraz ##
Dość ciekawe możliwości ma w makrach znak "#". Zamienia on na napis stojący za nim identyfikator.
#include <stdio.h>
#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 <stdio.h>
#define abc(x) int zmienna ## x
int main()
{
abc(nasza); /* dzięki temu zadeklarujemy zmienną o nazwie zmiennanasza */
zmiennanasza = 2;
return 0;
}
Więcej o dobrych zwyczajach w tworzeniu makr można się dowiedzieć w rozdziale Powszechne praktyki.
[edytuj] Predefiniowane makra
W języku wprowadzono również serię predefiniowanych makr, które mają ułatwić życie programiście. Oto one:
- __DATE__ - data w momencie kompilacji
- __TIME__ - godzina w momencie kompilacji
- __FILE__ - łańcuch, który zawiera nazwę pliku, który aktualnie jest kompilowany przez kompilator
- __LINE__ - definiuje numer linijki
- __STDC__ - w kompilatorach zgodnych ze standardem ANSI lub nowszym makro to przyjmuje wartość 1
- __STDC_VERSION__ - zależnie od poziomu zgodności kompilatora makro przyjmuje różne wartości:
- jeżeli kompilator jest zgodny z ANSI (rok 1989) makro nie jest zdefiniowane,
- jeżeli kompilator jest zgodny ze standardem z 1994 makro ma wartość 199409L,
- jeżeli kompilator jest zgodny ze standardem z 1999 makro ma wartość 199901L.
Warto również wspomnieć o identyfikatorze __func__ zdefiniowanym w standardzie C99, którego wartość to nazwa funkcji.
Spróbujmy użyć tych makr w praktyce:
#include <stdio.h>
#if __STDC_VERSION__ >= 199901L
/* Jezeli mamy do dyspozycji identyfikator __func__ wykorzystajmy go. */
# define BUG(message) fprintf(stderr, "%s:%d: %s (w funkcji %s)\n", \
__FILE__, __LINE__, message, __func__)
#else
/* Jezeli __func__ nie ma, to go nie używamy */
# define BUG(message) fprintf(stderr, "%s:%d: %s\n", \
__FILE__, __LINE__, message)
#endif
int main(void) {
printf("Program ABC, data kompilacji: %s %s\n", __DATE__, __TIME__);
BUG("Przykladowy komunikat bledu");
return 0;
}
Efekt działania programu, gdy kompilowany jest kompilatorem C99:
Program ABC, data kompilacji: Sep 1 2008 19:12:13 test.c:17: Przykladowy komunikat bledu (w funkcji main)
Gdy kompilowany jest kompilatorem ANSI C:
Program ABC, data kompilacji: Sep 1 2008 19:13:16 test.c:17: Przykladowy komunikat bledu