Jak napisałem we wstępie, programy w C/C++ przed wykonaniem muszą być przetłumaczone i zlinkowane. Najważniejszy jest w tym zadaniu kompilator, ale ma on jeszcze dwóch pomocników, bez których programista miałby bardzo trudną pracę.
Preprocesor "rusza do pracy" przed właściwym skompilowaniem programu. Reaguje on tylko na te linijki (wiersze) kodu źródłowego, które zaczynają się od znaku # (krzyżyk); w takich linijkach zawarte są dyrektywy dla niego, mówiące mu co ma robić, co ma dać kompilatorowi "na pożarcie", a czego nie.
#include <nazwa_pliku>
#include "nazwa_pliku"
Dyrektywy te mówią preprocesorowi, aby w miejscu ich wystąpienia dołączył do kompilacji treść zawartą w pliku o podanej nazwie, jakby była częścią pliku aktualnie kompilowanego. Jeśli nazwę pliku ujmie się w nawiasy kątowe, to plik ten poszukiwany jest w katalogu przeznaczonym na tzw. pliki nagłówkowe (zwykle wśród podkatalogów z kompilatorem jest podkatalog inc lub include, a w nim mnóstwo plików z rozszerzeniem .h), zawierające deklaracje zmiennych, funkcji i innych obiektów zawartych w bibliotekach. Jeśli nazwa pliku zostanie ujęta w cudzysłowy, to plik ten jest poszukiwany w tym samym katalogu, w którym znajduje się aktualnie kompilowany plik. W dołączanym pliku może również wystąpić dyrektywa #include, która także będzie respektowana przez preprocesor, który dołączy treść kolejnego pliku do kompilacji.
Przykład:
#include <stdio.h>
dołącza w tym miejscu treść pliku stdio.h.
#if warunek
...
#else
...
#endif
Są to dyrektywy tzw. kompilacji warunkowej. W dyrektywie tej można pominąć część #else wraz z następującym przed dyrektywą #endif kodem. Fragment kodu źródłowego pomiędzy dyrektywą #if a #else lub #endif jest poddawana kompilacji tylko wtedy, gdy w momencie interpretacji tej dyrektywy spełniony jest podany warunek. W przeciwnym razie, jeśli występuje dyrektywa #else, to zamiast tamtego fragmentu, kompilacji poddawany jest fragment spomiędzy #else a #endif; jeżeli brakuje dyrektywy #else, to w przypadku niespełnienia warunku nic spomiędzy #if a #endif nie jest poddawane kompilacji. Warunek może być dowolnym wyrażeniem, jednak musi ono dać się obliczyć już na tym etapie kompilacji - zwykle używa się w nim zdefiniowanych wcześniej stałych kompilatora.
Przykład:
#if POZIOM == 2
printf("Kontrola\n");
#else
//normalne wykonanie
#endif
spowoduje sprawdzenie czy wartość stałej POZIOM jest równa 2. Jeśli tak, kompilacji poddany zostanie fragment
#define stała
#define stała wartość
Dyrektywy służące do definiowania tzw. stałych kompilatora. Pierwsza postać powoduje tylko zdefiniowanie takiej stałej, bez nadawania jej wartości. Aby dalej w takim programie stwierdzić fakt zdefiniowania lub niezdefiniowania takiej stałej, można posłużyć się dyrektywą #if, #ifdef lub #ifndef (dwie ostatnie wyjaśnione dalej). W wyrażeniu warunku w dyrektywie #if trzeba posłużyć się konstrukcją defined(stała), dającą w wyniku 1, gdy stała jest zdefiniowana i 0 w przeciwnym wypadku. Druga postać tej dyrektywy definiuje stałą i nadaje jej wartość. W dalszej części można sprawdzić tą wartość w wyrażeniu warunkowym dyrektywy #if; można jej także używać w innych częściach kodu (nie będących dyretywami preprocesora) - w każdym z takich miejsc stała ta przez preprocesor zostanie zastąpiona jej aktualną wartością. Tak naprawdę wartość ta nie musi być liczbą.
Przykład:
#define CPU Pentium
Powoduje zdefiniowanie stałej kompilatora o nazwie CPU. Każde dalsze wystąpienie tej stałej w kodzie źródłowym zostanie zastąpione jej wartością Pentium. Nie zostaną jedynie zastąpione wystąpienia ciągu CPU na ciąg Pentium w tzw. stałych łańcuchowych (napisach umieszczonych w cudzysłowach), np. stała "CPU 200 MHz" nie zostanie zastąpiona przez "Pentium 200 MHz".
#define makro(parametry) rozwinięcie
Od poprzednich ta dyrektywa różni się tym, że bezpośrednio po nazwie (bez choćby jednej spacji) umieszczone zostały nawiasy okrągłe, a w nich parametry makra. W dalszej części kodu źródłowego odwołanie się do tego makra wraz z towarzyszącymi mu parametrami powoduje zastąpienie go przez preprocesor jego rozwinięciem, w którym parametry także zostają zastąpione ich wartościami aktualnymi. Jednak nie zostają zastąpione wystąpienia makra w stałych łańcuchowych.
Przykład:
#define DODAJ(a, b) a+b
Powoduje zdefiniowanie makra DODAJ z dwoma parametrami. Późniejsze wystąpienie w kodzie odwołania
#undef stała_lub_makro
Odwołanie (unieważnienie, skasowanie) definicji stałej kompilatora lub makra, sprawiające, że wystąpienie go w dalszej części kodu źródłowego nie będzie powodowało jego zastępowania wartością lub rozwinięciem.
Przykłady:
#undef CPU
#undef DODAJ
Unieważnienie wcześniejszych definicji. Jeśli nie było wcześniej żadnej definicji, to dyrektywa jest ignorowana.
#ifdef stała_lub_makro
...
#else
...
#endif
Konstrukcja tych dyrektych jest podobna do #if, ale nie ma tu wyrażenia warunkowego, zaś sam warunek uważa się za spełniony, jeśli w momencie interpretowania tej dyrektywy JEST zdefiniowana podana stała kompilatora lub makro, bez względu na to, czy z wartością, czy bez.
#ifndef stała_lub_makro
...
#else
...
#endif
Jak wyżej. Warunek uważa się za spełniony, jeśli w momencie interpretowania tej dyrektywy NIE JEST zdefiniowana podana stała kompilatora lub makro (lub jego definicja została unieważniona za pomocą #undef).
#error treść_komunikatu_błędu
Czasami, wskutek błędnych wartości zdefiniowanych stałych, trzeba przerwać kompilację programu. Dyrektywa #error umożliwia to w dowolnym momencie. Gdy kompilacja zostanie zatrzymana, wyświetlany jest podany komunikat błędu.
Przykład:
#if POZIOM == 0
#error Poziom nie może byc zerowy!
#endif
powoduje przerwanie dalszej kompilacji, gdy wartość stałej kompilatora POZIOM jest równa 0.
#pragma opcje
O tej dyrektywie trzeba przeczytać w opisie konkretnego kompilatora. Przytoczyłem ją tutaj dlatego, żeby pokazać, że taka jest. Służy ona do włączania i wyłączania różnego rodzaju opcji kompilatora.
Przykład:
#pragma argsused
int fun(char ch)
{
...
}
użyta przed definicją funkcji w pewnych kompilatorach powoduje niewyświetlanie ostrzeżenia w przypadku, gdy w funkcji tej ani razu nie został użyty jej argument. Kompilatory, które nie rozpoznają danej opcji w dyrektywie #pragma ignorują ją.
Preprocesor wykonuje jeszcze inną pracę. Mianowicie skleja każdy wiersz zakończony znakiem \ (backslash) z wierszem następnym. Dzięki temu jest możliwa dyrektywa:
#define TROJMIAN(a, b, c) \
a*x*x+\
b*x+\
c
zajmująca cztery wiersze, która jest równoważna poniższej mieszczącej się w jednej linijce:
#define TROJMIAN(a, b, c) a*x*x+b*x+c
W rozwinięciach makr można posłużyć się operatorem sklejania. Tu od razu posłużę się przykładem:
#define CPU(model, speed) model##speed
spowoduje zdefiniowanie makra CPU, do którego przykładowe odwołanie:
Omówionych mechanizmów preprocesora należy używać z rozwagą, bo bardzo łatwo można popełnić długo niemożliwy do wykrycia błąd.
Przykład:
#define MNOZENIE(x, y) x*y
...
...MNOZENIE(2+4, 3-1)...
będzie trudnym do wykrycia błędem. Kiedy już dowiesz się czegoś o wyrażeniach, może dostrzeżesz ten błąd. Przecież po rozwinięciu makra otrzymamy:
#define MNOZENIE(x, y) (x)*(y)
To jeszcze nie koniec pułapek preprocesora. Zaraz zacznę omawiać linkera, więc słów parę o plikach nagłówkowych. Gdy napisze się własną bibliotekę, trzeba też napisać do niej plik nagłówkowy, który będziemy dołączać dyrektywą #include. Czasem jednak zdarza się, że pisany przez nas program składa się z wielu modułów, czasem jest też tak, że są one z sobą skomplikowanie powiązane i występuje sytuacja, gdy ten sam plik nagłówkowy jest kilka razy włączany do kompilacji. Ponieważ w plikach nagłówkowych znajdują się deklaracje zwykle mogące wystąpić tylko raz podczas kompilacji, to powstaje pytanie: jak zagwarantować tylko jednokrotne dołączenie danego pliku nagłówkowego? Oto odpowiedź:
//poczatek pliku header.h
#ifndef _HEADER_H
#define _HEADER_H
...
#endif
//koniec pliku header.h
Przed pierwszym dołączeniem (
Linker, to drugi pomocnik kompilatora. Odpowiada on za takie połączenie osobno skompilowanych modułów oraz bibliotek, by w wyniku otrzymać cały i poprawnie działający program. Czasem linker umieszczony jest integralnie z kompilatorem, a czasem jest to zewnętrzny program link.exe, tlink.exe lub tlink32.exe (dla programów działających w środowisku 32-bitowym). Na razie nie za dużo pewnie wiesz o funkcjach, ale już teraz musisz wiedzieć, że poprawnie działający program musi być "wyposażony" w dokładnie jedną definicję funkcji main. Jeśli linker łącząc z sobą moduły i biblioteki napotka na więcej niż jedną taką definicję lub nie napotka na nią wcale, wówczas zgłasza błąd i nie może powstać program wynikowy. W obrębie jednego modułu podwójnych definicji zabrania już sam kompilator, ale gdy w różnych modułach są dwie definicje (choćby identyczne) tego samego obiektu (zmiennej, funkcji) - kompilator tego nie wykryje. Dopiero linker napotykając na taką sytuację "nie wie", której definicji użyć i wtedy przerywa proces łączenia wyświetlając komunikat o błędzie. Podobnie, gdy w jakimś module obiekt został zadeklarowany i, co gorsza, użyty, a nigdzie indziej nie został zdefiniowany.
A po co są pliki nagłówkowe? Linker przecież połączy z sobą moduły i biblioteki, więc wydawałoby się, że są niepotrzebne. A jednak. Przed procesem łączenia jest proces kompilacji. A kompilator musi znać deklaracje wszystkich obiektów (nawet tych w bibliotekach). Załóżmy, że w bibliotece mouse.lib jest funkcja getmouse(). Używamy jej w swoim programie. Ale skąd kompilator ma "wiedzieć", co oznacza nazwa getmouse: nazwę zmiennej, nazwę funkcji czy nazwę innego obiektu? Trzeba więc zadeklarować, że getmouse, to nazwa funkcji. Zbiór wszystkich takich deklaracji obiektów bibliotecznych wpisuje się do jednego pliku i dołącza się potem na żądanie do swojego programu dyrektywą #include za każdym razem, gdy zachodzi potrzeba użycia danej biblioteki. Na dalszych podstronach przeczytasz o różnicy pomiędzy deklaracją a definicją.
Linker zatem umożliwia osobne i niezależne kompilowanie modułów, co sprawia, że jakiś bardzo duży program może być pisany przez wiele osób naraz, gdy każda z osób zajmuje się innym aspektem (np. jedna interfejsem użytkownika, inna obliczeniami, a jeszcze inna obsługą plików). Potem osoby te się spotykają i łączą (linkują) z sobą swoje moduły, tworząc w ten sposób cały program.