2_1.pdf

(615 KB) Pobierz
Od zera do gier kodera
1
PREPROCESOR
Gdy się nie wie, co się robi,
to dzieją się takie rzeczy,
że się nie wie, co się dzieje ;-).
znana prawda programistyczna
Poznawanie bardziej zaawansowanych cech języka C++ zaczniemy od czegoś, co
pochodzi jeszcze z czasów jego poprzednika, czyli C. Podobnie jak wskaźniki, preprocesor
nie pojawił się wraz z dwoma plusami w nazwie języka i programowaniem zorientowanym
obiektowo, lecz był obecny od jego samych początków.
W przypadku wskaźników trzeba jednak powiedzieć, że są one także i teraz niezbędne do
efektywnego i poprawnego konstruowania aplikacji. Natomiast o proceprocesorze
niewielu ma tak pochlebne zdanie: według sporej części programistów, stał się on prawie
zupełnie niepotrzebny wraz z wprowadzeniem do C++ takich elementów jak funkcje
inline oraz szablony. Poza tym uważa się powszechnie, że częste i intensywne używanie
tego narzędzia pogarsza czytelność kodu.
W tym rozdziale będę musiał odpowiedzieć jakoś na te opinie. Nie da się ukryć, że
niektóre z nich są słuszne: rzeczywiście, era świetności preprocesora jest już dawno za
nami. Zgadza się, nadmierne i nieuzasadnione wykorzystywanie tego mechanizmu może
przynieść więcej szkody niż pożytku. Tym bardziej jednak powinieneś wiedzieć jak
najwięcej na temat tego elementu języka, aby móc stosować go poprawnie. Od
korzystania z niego nie można bowiem uciec. Choć może nie zdawałeś sobie z tego
sprawy, lecz korzystałeś z niego w każdym napisanym dotąd programie w C++!
Wspomnij sobie choćby dyrektywę #include
Dotąd jednak zadowalałeś się lakonicznym stwierdzeniem, iż tak po prostu „trzeba”.
Lekturą tego rozdziału masz szansę to zmienić. Teraz bowiem omówimy sobie
zagadnienie preprocesora w całości, od początku do końca i od środka :)
Pomocnik kompilatora
Rozpocząć wypadałoby od przedstawienia głównego bohatera naszej opowieści. Czym
jest więc preprocesor?…
Preprocesor to specjalny mechanizm języka, który przetwarza tekst programu jeszcze
przed jego kompilacją .
To jakby przedsionek właściwego procesu kompilacji programu. Preprocesor
przygotowuje kod tak, aby kompilator mógł go skompilować zgodnie z życzeniem
programisty. Bardzo często uwalnia on też od konieczności powtarzania często
występujących i potrzebnych fragmentów kodu, jak na przykład deklaracji funkcji.
Kiedy wiemy już mniej więcej, czym jest preprocesor, przyjrzymy się wykonywanej przez
niego pracy. Dowiemy się po prostu, co on robi.
95459466.003.png
330
Zaawansowane C++
Gdzie on jest…?
Obecność w procesie budowania aplikacji nie jest taka oczywista. Całkiem duża liczba
języków radzi sobie, nie posiadając w ogóle narzędzia tego typu. Również cel jego
istnienia wydaje się niezbyt klarowny: dlaczego kod naszych programów miałby wymagać
przed kompilacją jakichś przeróbek?…
Tę drugą wątpliwość wyjaśnią kolejne podrozdziały, opisujące możliwości i polecenia
preprocesora. Obecnie zaś określimy sobie jego miejsce w procesie tworzenia
wynikowego programu.
Zwyczajowy przebieg budowania programu
W języku programowania nieposiadającym preprocesora generowanie docelowego pliku z
programem przebiega, jak wiemy, w dwóch etapach.
Pierwszym jest kompilacja , w trakcie której kompilator przetwarza kod źródłowy
aplikacji i produkuje skompilowany kod maszynowy, zapisany w osobnych plikach. Każdy
taki plik - wynik pracy kompilatora - odpowiada jednemu modułowi kodu źródłowego.
W drugiem etapie następuje linkowanie skompilowanych wcześniej modułów oraz
ewentualnych innych kodów, niezbędnych do działania programu. W wyniku tego procesu
powstaje gotowy program.
Schemat 36. Najprostszy proces budowania programu z kodu źródłowego
Przy takim modelu kompilacji zawartość każdego modułu musi wystarczać do jego
samodzielnej kompilacji, niezależnej od innych modułów. W przypadku języków z rodziny
C oznacza to, że każdy moduł musi zawierać deklaracje używanych funkcji oraz definicje
klas, których obiekty tworzy i z których korzysta.
Gdyby zadanie dołączania tych wszystkich deklaracji spoczywało na programiście, to
byłoby to dla niego niezmiernie uciążliwe. Pliki z kodem zostały ponadto rozdęte do
nieprzyzwoitych rozmiarów, a i tak większość zawartych weń informacji przydawałyby się
tylko przez chwilę. Przez tą chwilę, którą zajmuje kompilacja modułu.
95459466.004.png
Preprocesor
331
Nic więc dziwnego, że aby zapobiec podobnym irracjonalnym wymaganiom wprowadzono
mechanizm preprocesora.
Dodajemy preprocesor
Ujawnił się nam pierwszy cel istnienia preprocesora: w języku C(++) służy on do łączenia
w jedną całość modułów kodu wraz z deklaracjami, które są niezbędne do działania tegoż
kodu. A skąd brane są te deklaracje?…
Oczywiście - z plików nagłówkowych. Zawierają one przecież prototypy funkcji i definicje
klas, z jakich można korzystać, jeżeli dołączy się dany nagłówek do swojego modułu.
Jednak kompilator nic nie wie o plikach nagłówkowych. On tylko oczekuje, że zostaną
mu podane pliki z kodem źródłowym, do którego będą się zaliczały także deklaracje
pewnych zewnętrznych elementów - nieobecnych w danym module. Kompilator
potrzebuje tylko ich określenia „z wierzchu”, bez wnikania w implementację, gdyż ta
może znajdować się w innych modułach lub nawet innych bibliotekach i staje się ważna
dopiero przy linkowaniu. Nie jest już ona sprawą kompilatora - on żąda tylko tych
informacji, które są mu potrzebne do kompilacji.
Niezbędne deklaracje powinny się znaleźć na początku każdego modułu. Trudno jednak
oczekiwać, żebyśmy wpisywali je ręcznie w każdym module, który ich wymaga. Byłoby
to niezmiernie uciążliwe, więc wymyślono w tym celu pliki nagłówkowe… i preprocesor.
Jego zadaniem jest tutaj połączenie napisanych przez nas modułów oraz plików
nagłówkowych w pliki z kodem, które mogą mogą być bez przeszkód przetworzone przez
kompilator.
Schemat 37. Budowanie programu C++ z udziałem preprocesora
95459466.005.png
332
Zaawansowane C++
Skąd preprocesor wie, jak ma to zrobić?… Otóż, mówimy o tym wyraźnie, stosując
dyrektywę #include . W miejscu jej pojawienia się zostaje po prostu wstawiona treść
odpowiedniego pliku nagłówkowego.
Włączanie nagłówków nie jest jednak jedynym działaniem podejmowanym przez
preprocesor. Gdyby tak było, to przecież nie poświęcalibyśmy mu całego rozdziału :) Jest
wręcz przeciwnie: dołączanie plików to tylko jedna z czynności, jaką możemy zlecić temu
mechanizmowi - jedna z wielu czynności…
Wszystkie zadania preprocesora są różnorodne, ale mają też kilka cech wspólnych.
Przyjrzyjmy się im w tym momencie.
Działanie preprocesora
Komendy, jakie wydajemy preprocesorowi, różnią się od normalnych instrukcji języka
programowania. Także sposób, w jaki preprocesor traktuje kod źródłowy, jest zupełnie
inny.
Dyrektywy
Polecenie dla preprocesora nazywamy jego dyrektywą (ang. directive ). Jest to specjalna
linijka kodu źródłowego, rozpoczynająca się od znaku # ( hash ), zwanego płotkiem 97 :
#
Na nim też może się zakończyć - wtedy mamy do czynienia z dyrektywą pustą. Jest ona
ignorowana przez preprocesor i nie wykonuje żadnych czynności.
Bardziej praktyczne są inne dyrektywy, których nazwy piszemy zaraz za znakiem # . Nie
oddzielamy ich żadnymi spacjami, więc w praktyce płotek staje się częścią ich nazw.
Mówi się więc o instrukcjach #include , #define , #pragma i innych, gdyż w takiej formie
zapisujemy je w kodzie.
Dalsza część dyrektywy zależy już od jej rodzaju. Różne „parametry” dyrektyw poznamy,
gdy zajmiemy się szczegółowo każdą z nich.
Bez średnika
Jest bardzo ważne, aby zapamiętać, że:
Dyrektywy preprocesora kończą się zawsze przejściem do następnego wiersza.
Innymi słowy, jeżeli preprocesor napotka w swojej dyrektywie na znak końca linijki (nie
widać go w kodzie, ale jest on dodawany po każdym wciśnięciu Enter ), to uznaje go
także za koniec dyrektywy. Nie ma potrzeby wpisywania średnika na zakończenie
instrukcji. Więcej nawet: nie powinno się go wpisywać! Zostanie on bowiem uznany za
część dyrektywy, co w zależności od jej rodzaju może powodować różne niepożądane
efekty. Kończą się one zwykle błędami kompilacji.
Zapamiętaj zatem zalecenie:
Nie kończ dyrektyw preprocesora średnikiem . Nie są to przecież instrukcje języka
programowania, lecz polecenia dla modułu wspomagającego kompilator.
97 Przed hashem mogą znajdować się wyłącznie tzw. białe znaki, czyli spacje lub tabulatory. Zwykle nie
znajduje się nic.
95459466.006.png 95459466.001.png
Preprocesor
333
Można natomiast kończyć dyrektywę komentarzem, opisującym jej działanie. Kiedyś
wiele kompilatorów miało z tym kłopoty, ale obecnie wszystkie liczące się produkty
potrafią radzić sobie z komentarzami na końcu dyrektyw preprocesora.
Ciekawostka: sekwencje trójznakowe
Istnieje jeszcze jedna, bardzo rzadka dzisiaj sytuacja, gdy preprocesor zostaje wezwany
do akcji. Jest to jedyny przypadek, kiedy jego praca jest niezwiązana z dyrektywami
obecnymi w kodzie.
Chodzi o tak zwane sekwencje trójznakowe (ang. trigraphs ). Cóż to takiego?…
W każdym długo i szeroko wykorzystywanym produkcie pewne funkcje mogą być po
pewnym czasie uznane za przestarzałe i przestać być wykorzystywane. Jeżeli mimo to są
one zachowywane w kolejnych wersjach, to zyskują słuszne miano skamieniałości
(ang. fossils ).
Język C++ zawiera kilka takich zmumifikowanych konstrukcji, odziedziczonych po swoim
poprzedniku. Jedną z nich jest na przykład możliwość wpisywania do kodu liczb w
systemie ósemkowym (oktalnym), poprzedzając je zerem (np. 042 to dziesiętnie 34 ).
Obecnie jest to całkowicie niepotrzebne, jako że współczesny programista nie odniesie
żadnej korzyści z wykorzystania tego systemu liczbowego. W architekturze komputerów
został on bowiem całkowicie zastąpiony przez szesnastkowy (heksadecymalny) sposób
liczenia. Ten jest na szczęście także obsługiwany przez C++ 98 , natomiast zachowana
możliwość użycia systemu oktalnego stała się raczej niedogodnością niż plusem języka.
Łatwo przecież omyłkowo wpisać zero przed liczbą dziesiętną i zastanawiać się nad
powstałym błędem…
Inną skamieniałością są właśnie sekwencje trójznakowe. To specjalne złożenia dwóch
znaków zapytania ( ?? ) oraz innego trzeciego znaku, które razem „udają” symbol ważny
dla języka C++. Preprocesor zastępuje te sekwencje docelowym znakiem, postępując
według tej tabelki:
trójznak symbol
??= #
??/ \
??- ~
??’ ^
??! |
??( [
??) ]
??< {
??> }
Tabela 13. Sekwencje trójznakowe w C++
Twórca języka C++, Bjarne Stroustroup, wprowadził do niego sekwencje trójznakowe z
powodu swojej… klawiatury. W wielu duńskich układach klawiszy zamiast przydatnych
symboli z prawej kolumny tabeli widniały bowiem znaki typu å, Æ czy Å. Aby umożliwić
swoim rodakom programowanie w stworzonym języku, Stroustroup zdecydował się na
ten zabieg.
Dzisiaj obecność trójznaków nie jest taka ważna, bo powszechnie występują na całym
świecie klawiatury typu Sholesa, które zawierają potrzebne w C++ znaki. Moglibyśmy
więc o nich zapomnieć, ale…
98 Aby zapisać liczbę w systemie szesnastkowym, należy ją poprzedzić sekwencją 0x lub 0X . Tak więc 0xFF to
dziesiętnie 255 .
95459466.002.png
 
Zgłoś jeśli naruszono regulamin