Programowanie w języku C/C++

Preprocesor

Spis treści

     1 Wstęp
     2 Co to jest preprocesor?
     3 Dyrektywy preprocesora
          3.1 Dyrektywa #include
               3.1.1 Do czego może się przydać dyrektywa #include?
          3.2 Dyrektywa #define
               3.2.1 Definiowanie "stałych"
               3.2.2 Definiowanie makrodefinicji
               3.2.3 Makra predefiniowane
          3.3 Dyrektywy #if, #ifdef, #ifndef, #else, #elif, #endif
          3.4 Dyrektywa #pragma
     4 Zakończenie


Wstęp


Wstęp będzie raczej krótki.

Zaznaczam, ze ten artykuł jest tylko wprowadzeniem do tematu preprocesora. Nie ma na celu dogłebnej analizy tego tematu.

Jeśli chcecie wiedzieć więcej, odsyłam do świetnego Megatutoriala Xiona, z którego z resztą korzystałem przy pisaniu tego artykułu.


Co to jest preprocesor?


Preprocesor jest to mechanizm języka C, który przetwarza tekst programu przed jego kompilacją. Jak z niego korzystać? Nic prostszego. Pewnie część z was posługuje się dyrektywą (będę też używał terminu "instrukcja", ew. "polecenie") #include, nie do konca wiedząc co to jest i jak ona działa. Jest to nic innego jak właśnie dyrektywa preprocesora. Właśnie dzięki niej "mówimy" proprocesorowi że ma coś zmienić w tekście programu (a co dokładie to dowiecie się później :) ).

Dyrektywy preprocesora


Dzięki dyrektywom "mówimy" preprocesorowi że ma się włączyć. Każda dyrektywa zaczyna się od znaku # (czyli "hash") i, co ważne, nie kończy się średnikiem. Każda dyrektywa musi też występować w osobnej linii. Nie możemy na przykład napisać:

#include "abc" #include "xyz"


Wtedy to polecenie  zostanie przetworzone na dyrektywę #include i argument "abc" #include "xyz".
To tyle tytułem wstępu, przejdźmy do omówienia poszczególnych dyrektyw..

Dyrektywa #include


Jak można się było spodziewać, zacząłem od dyrektywy #include, ponieważ jest to chyba najczęściej wykorzystywana instrukcja preprocesora. Dołącza ona inny, dowolny, niekoniecznie nagłówkowy plik do pliku, w którym jest ta dyrektywa. Skłądnia tej dyrektywy wygląda nstępująco:

#include plik dołączany


Plik dołączny może być w formacie <plik> lub "plik". W pierwszym wypadku kompilator szuka plików nagłówkowych w zdefiniowanych ścieżkach, w drugim dodatkowo na początku sprawdza, czy w katalogu, w którym jest plik źródłowy nie znajduje sie plik dołączany.

Przypominam jeszcze raz, że preprocesor przetwarza tekst programu, a więc dla niego nie jest ważne, czy w dołączanym plik jest kod programu, czy napis "Ala ma kota". Sprawdzaniem poprawności zajmuje się kompilator. Dam tutaj przykład:

Plik foo.c:

#include <iostream>
#include "foo.h"
 
int main()
{
        std::cout << x << std::endl; // gdzie jest zadeklarowana zmienna x?
        return 0;
}


Plik foo.h:

// tutaj deklarujemy i inicjalizujemy zmienną x
int x = 47;


Preprocesor przetworzy ten kod źródłowy na następujący (oczywiście bez dodatkowych komentarzy):

// tutaj treść pliku iostream
// ...
// ...
 
// tutaj zaczyna się plik foo.h
 
// tutaj deklarujemy i inicjalizujemy zmienną x
int x = 47;
 
//tutaj kontynuujemy ciało pliku foo.c
 
int main()
{
        std::cout << x << std::endl; // gdzie jest zadeklarowana zmienna x?
        return 0;
}


Dopiero taki plik zostaje poddany kompilacji, a więc widzimy, że zmienna x nie bierze się z powietrza.

Do czego może się przydać dyrektywa #include?


Jest to jedna z najbardziej przydatnych dyrektyw. Załóżmy że mamy dwa pliki źródłowe, w których chcemy użyć tej samej funkcji. Możemy zdefiniwać tą funkcję dwa razy, w każdym z plików źródłowych, w takim jednak przypadku kompilator zaprotestuje, argumetując że dana funkcja jest zdefiniowana dwa razy (z resztą nie ma się co dziwić). Tutaj z pomocą przychodzi właśnie #include w pliku nagłówkowym deklarujemy funkcję, dołączamy go do obydwu plików źródłowych, funkcję definujemy tylko w jedym z plików źródłowych i... wszystko działa :)

Dyrektywa #define


Dyrektywa ta słuzy dwóm celom, a mianowicie tworzeniu, nazwijmy to, stałych oraz makrodefinicji. W zasadzie niczym się to nie różni,ale my wrowadzimy taki umowny podział.

Definiowanie "stałych"


Jest to najprostszy sposób użycia dyrektywy #define. Definiowanie "stałych" wygląda tak:
#define nazwa_stałej wartość


Działa to w ten sposób, że jeżeli preprocesor znajdzie w tekście programu nazwę jakiejś zdefiniowanej "stałej", zamieni ją na jej wartość i nie sprawdzi przy tym czy wygenerowany kod jest poprawny. Przykład:

// kod programu
// ...
 
#define WIEK 20
 
// kod programu
// ...
 
cout << WIEK;


Zostanie to przetworzone na:

// kod programu
// ...
 
cout << 20;


"Stała" wcale jednak nie musi posiadać żadnej "wartości". Może być po prostu zdefiniowana i nie mówić o sobie nic więcej.

Tutaj wchodzi do gry dyrektywa #undef, dzięki której cofamy definicję danej "stałej" lub makra(o nich później). Możemy także sprawdzić, czy dane makro istnieje, o tym jednak napiszę w rozdziale o dyrektywach warunkowych.

To tyle, przejdźmy do szerszego tematu, a mianowicie makrodefinicji.

Definiowanie makrodefinicji


Innym sposobem użycia dyrektywy jest definiowaie makrodefinicji. Makrodefinicja, podobnie jak mechanizm "stałych", pozwala na przyspieszenie procesu tworzenia kodu przez tworzenie swoistych szablonów, według których części kodu generowane są przez preprocesor.  definicja makra wygląda następująco:
#define nazwa_makra makro


Widać więc, że "stałe" to szczególny przypadek makra, które nie posiada argumentów. Nazwa makra ma następującą postać:

NAZWA(argument1, argument2, ...)


Argumentem może być dowolny napis. Pamiętajmy, że w przypadku makr cydzysłowia też są częściami napisu! Argumentów tych możemy użyć w ciele makra, po prostu pisząc ich nazwę. A teraz mały przykładzik, dzięki któremu rozwiążemy tajemnicę wszechświata (pozdrowienia dla fanów Adamsa :) ), a także pokaże nam pewną właściwość, na którą trzeba bardzo uważać:

#include <iostream>
 
#define SZESC 1+5
#define DZIEWIEC 8+1
 
#define MNOZENIE_1(a,b) a * b
#define MNOZENIE_2(a,b) (a) * (b)
 
int main()
{
        std::cout << "Sześć razy dziewięć równa się: " << MNOZENIE_1(SZESC, DZIEWIEC) << std::endl;
        std::cout << "Sześć razy dziewięć równa się: " << MNOZENIE_2(SZESC, DZIEWIEC) << std::endl;
        return 0;
}


Wynik będzie co najmniej dziwny:
42
54


Możnaby pomyśleć, że kompilator zgłupiał, ale tak na prawdę on robi dokładnie to co mu każemy. Spróbujmy rozwinąć drugie, poprawne wyrażenie:

MNOZENIE_2(SZESC, DZIEWIEC) = (SZESC)*(DZIEWIEC) = (1+5)*(8+1) = 6*9 = 54


A teraz rozwiniemy pierwsze wyrażenie:

MNOZENIE_1(SZESC, DZIEWIEC) = SZESC * DZIEWIEC = 1+5*8+1 = 1+40+1 = 42


Teraz już widzimy, gdzie jest błąd. Należy pamiętać, że preprocesor operuje na tekście a nie ma kodzie. On nie obliczy nam wyrażenia i nie wyśle dopiero wyniku do makra, tak jakby zrobił to kompilator dla funkcji. Jeszcze tylko wspomnę, że można definiować makra wielolinijkowe. W tym celu na końcu każdej linijki makra piszemy znak \.

Makra predefiniowane


Preprocesor udostępnia nam pewne makra, których nie musimy a naewt nie bylibyśmy w stanie zdefiniować. Są to m. in. TIME, DATE, TIMESTAMP, LINE i FILE.

Makro TIME zostanie zastąpione przez godzinę ostatniej kompilacji pliku. Z kolei DATE wyświetli datę tej kompilacji.

Makro TIMESTAMP zostanie zastąpione przez datę ostatniej modyfikacji pliku.

Najciekawszymi makrami są jednak makra LINE i FILE. Pierwsze z nich zostanie zastąpione numerem linii, w której zostało użyte. Drugie zaś zostanie zastąpione przez nazwę pliku, w którym zostało użyte. Jak widać, mogą one być bardzo przydatne podczas debugowania.

Dyrektywy #if, #ifdef, #ifndef, #else, #elif, #endif


Inaczej nazywane dyrektywami warunkowymi lub dyrektywami kompilacji warunkowej. Pozwalają one, jak sama nazwa wskazuje, na kompilację warunkową, czyli wybranie, czy dany kod ma się skompilować w danych warunkach. Są one bardzo podobne do instrukcji warunkowych, operują jednak na tekście programu, jak cały preprocesor. Składnia takiej dyrektywy:

#if warunek
// instrukcje
// ...
#else
// instrukcje
// ...
#endif


Jak widać, składnia też jest bardzo podobna do tej znanej z instrukcji warunkowych. W warunkach można używać takich samych operatorów logicznych, jak w języku C. Dyrektywa #else jest opcjonalna i ma tą samą funkcję co else. Jej działanie jest bardzo proste. Jeżeli zostanie spełniony warunek to kod pomiędzy #if a #endif jest kompilowany, w przeciwnym wypadku nie. Jeśli dodatkowo między #if a #endif jest dyrektywa #else, to gdy warunek jest spełniony, kompilowany jest tylko kod pomiędzy #if a #else, a gdy warunek nie jest spełniony, skompilowany zostaje kod pomiędzy #else a #endif. Dyrektywy #if, podobnie jak instrukcje if, mogą być zagnieżdżane.

Do tego wszystkiego dochodzi jeszcze #elif. Jest to odpowiednik #else if, tyle tylko, że taki blok nie jest zakończony dyrektywą #endif.

Podczas omawiania dyrektywy #define wspomniałem o możliwości sprawdzenia, czy makro jest zdefiniowane. Służy do tego operator defined (bez hasha), który używa się razem z #if. Składia jest następująca:

defined(makro)


Zwraca on wartość true, jeśli makro jest zdefiniowane, lub false, gdy warunek jest nieprawdziwy.

Zostały jeszcze dyrektywy #ifdef i #ifndef. Powstały one z połązenia dyrektywy #if, operatorów ! i defined. Oto ich składnia:

#ifdef makro
#ifndef makro


Pierwsza dyrektywa jest równoznaczna z:

#if defined(makro)


A druga z:

#if !defined(makro)


To tyle na temat dyrektyw warunkowych.

Dyrektywa #pragma


Dyrektywa pragma jest bardzo specyficzną dyrektywą. Oto jej składnia:

#pragma argument


Nie byłoby w tym nic dziwnego, gdyby nie to, że argumenty dla tej dyrektywy są specyficzne dla kompilatora. Oznacza to, że ta dyrektywa jest jakby furtką w standardzie C/C++. Nie będę tutaj opisywał możliwych argumentów, gdyż te można znaleźć w dokumentacji kompilatora.

Zakończenie


Artykuł ten był poświęcony ciekawemu mechanizmowi języka C - preprocesorowi. Powtarzam jeszcze raz, ze nie jest to dogłębne przedstawienie całego tematu, a jedynie wprowadzenie do niego. Po więcej informacji odsyłam do Megatutoriala Xiona.

3 komentarze

robert_re 2008-03-05 18:34

Bardzo dobry artykuł! :)

Xardas85 2006-06-25 16:30

Podoba sie ;-)