Preprocesor
W poprzedniej notatce zobaczyliśmy, że przed analizą kodu kompilator przetwarza dyrektywy takie jak #include. Robi to preprocesor. Jego zadaniem jest przygotowanie tekstu źródłowego: dołącza wskazane pliki, rozwija makra i wybiera fragmenty objęte kompilacją warunkową.
Preprocesor nie wykonuje programu i nie sprawdza typów tak jak kompilator. Działa wcześniej, na tekście. Dlatego dyrektywy z # mogą zmienić to, jaki kod trafi do kompilatora, ale nie potrafią na przykład sprawdzić wartości zmiennej podczas działania programu.
Komentarze również nie są instrukcjami programu. W trakcie wczesnego przetwarzania zostają zastąpione białymi znakami, dzięki czemu nie zmieniają sąsiadujących tokenów. Pisze się je dla osób czytających kod; procesor nie wykonuje ich treści.
Przykłady w tej notatce są zapisane w C++. W C działają te same podstawowe dyrektywy, choć nazwy nagłówków i kod korzystający z bibliotek mogą się różnić.
Dołączanie plików przez #include
Gdy plik źródłowy korzysta z funkcji lub typu opisanego w nagłówku, musi znać jego deklarację. #include włącza treść wskazanego pliku do przetwarzanego pliku źródłowego. Można to sobie wyobrazić jako tekstowe rozwinięcie nagłówka w miejscu dyrektywy.
#include <iostream> // nagłówek biblioteki standardowej C++
#include "powitanie.hpp" // nagłówek z bieżącego projektu
Nawiasy ostre stosuje się zwykle dla nagłówków bibliotek, a cudzysłowy — dla plików projektu. To konwencja dotycząca sposobu wyszukiwania plików: dokładne katalogi przeszukiwane przez kompilator zależą od jego konfiguracji.
Warto rozdzielić dwie rzeczy: dołączenie nagłówka udostępnia deklaracje, ale samo w sobie nie łączy gotowej definicji funkcji z programem. Na przykład nagłówek może powiedzieć kompilatorowi, że istnieje void wypisz_powitanie();, a kod tej funkcji może znajdować się w osobnym pliku .cpp. Pliki obiektowe trzeba później połączyć linkerem.
Dlaczego nagłówki mają strażniki?
Jeden nagłówek może zostać dołączony do pliku kilka razy pośrednio: na przykład main.cpp dołącza a.hpp i b.hpp, a oba te nagłówki dołączają wspolne.hpp. Wielokrotne przetworzenie tej samej deklaracji lub definicji może spowodować błędy. Strażnik nagłówka sprawia, że treść zostanie uwzględniona tylko raz w danej jednostce translacji:
// wspolne.hpp
#ifndef WSPOLNE_HPP
#define WSPOLNE_HPP
int oblicz_sume(int a, int b);
#endif // WSPOLNE_HPP
Przy pierwszym przejściu WSPOLNE_HPP nie jest jeszcze zdefiniowane, więc preprocesor wchodzi do bloku i definiuje makro. Gdy ponownie napotka ten nagłówek, warunek #ifndef jest fałszywy i jego zawartość zostaje pominięta. Nazwa strażnika powinna być charakterystyczna dla pliku, aby nie zderzyła się z inną nazwą.
W wielu popularnych kompilatorach działa też prostsze #pragma once, które zapobiega wielokrotnemu dołączeniu pliku. Jest szeroko obsługiwane, ale nie jest przenośnym zamiennikiem zdefiniowanym w taki sam sposób jak standardowe dyrektywy. Strażniki są bezpiecznym, powszechnym rozwiązaniem.
Zapis #pragma once wygląda tak:
#pragma once
Umieszcza się go na początku nagłówka zamiast strażnika. Wybierz jeden z tych sposobów i stosuj go konsekwentnie w projekcie.
Makra i podstawianie tekstu
#define wprowadza makro. Preprocesor zastępuje jego nazwę wskazanym tekstem, zanim kompilator sprawdzi kod. Makro nie jest zmienną ani funkcją: nie ma własnego typu i kompilator nie analizuje jego argumentów tak jak argumentów zwykłej funkcji.
Przykład makra bez argumentów:
#define PI 3.14159
double pole_kola(double promien) {
return PI * promien * promien;
}
Definicja obowiązuje od miejsca, w którym pojawia się #define, do końca jednostki translacji albo do chwili usunięcia jej dyrektywą #undef. Dlatego makro nazwane PI nie jest zmienną o zasięgu funkcji: jego podmiana może dotyczyć całego dalszego kodu przetwarzanego pliku.
Przed analizą kompilatora użycie PI zostanie zastąpione tekstem 3.14159. W nowym kodzie C++ stałą liczbową zwykle lepiej zapisać jako constexpr, bo ma wtedy typ i podlega regułom języka:
constexpr double pi = 3.14159;
Nawiasy w makrach funkcyjnych
Makro może przyjmować argumenty, ale nadal działa przez podstawienie tekstu. To łatwo przeoczyć, bo zapis makra przypomina wywołanie funkcji.
Rozważmy błędne makro:
#define KWADRAT(x) x * x
int a = 5;
int wynik = KWADRAT(a + 1);
Preprocesor wstawi argument dosłownie. Otrzymamy więc:
int wynik = a + 1 * a + 1;
Mnożenie ma pierwszeństwo przed dodawaniem, więc przy a == 5 wyrażenie oznacza 5 + (1 * 5) + 1, czyli 11. Nie jest to oczekiwany kwadrat liczby 6.
Nawiasy wokół argumentu i całego wyrażenia naprawiają ten konkretny błąd:
#define KWADRAT(x) ((x) * (x))
Teraz KWADRAT(a + 1) rozwija się do ((a + 1) * (a + 1)). Nadal jednak nie jest to bezpieczne zastępstwo funkcji. Argument trafia do makra dwa razy, więc przekazanie wyrażenia, które zmienia stan — na przykład i++ — może spowodować nieoczekiwane zachowanie. W C++ do obliczeń lepiej użyć funkcji; makr używa się głównie tam, gdzie potrzebna jest właśnie praca preprocesora.
Wybieranie kodu przed kompilacją
Dyrektywy #if, #ifdef, #ifndef, #elif, #else i #endif pozwalają włączyć wybrane wiersze do kodu przekazanego kompilatorowi, a inne pominąć. Przydaje się to między innymi do ustawień debugowania albo kodu zależnego od platformy.
Poniższy kompletny przykład wypisuje informację diagnostyczną tylko wtedy, gdy makro NDEBUG nie zostało zdefiniowane:
#include <iostream>
int main() {
#ifndef NDEBUG
std::cout << "Wersja z informacją diagnostyczną\n";
#endif
std::cout << "Program działa\n";
}
Gdy preprocesor napotka #ifndef NDEBUG, sprawdza wyłącznie, czy makro o tej nazwie istnieje. Jeśli istnieje, pomija aż do #endif tekst diagnostyczny. W przeciwnym razie pozostawia ten tekst dla kompilatora. Makro można zdefiniować w poleceniu kompilacji opcją -DNDEBUG w narzędziach GNU. Biblioteka standardowa używa NDEBUG między innymi do wyłączania sprawdzeń assert.
#ifdef NAZWA pyta, czy makro jest zdefiniowane. Nie sprawdza, czy jego wartość jest równa 1. Do sprawdzania wartości służy #if:
#define WERSJA 2
#if WERSJA == 1
// kod dla wersji 1
#elif WERSJA == 2
// kod dla wersji 2
#else
// kod dla pozostałych wersji
#endif
Preprocesor wybiera gałąź na podstawie wyrażenia złożonego ze stałych całkowitych i makr. Nie może w tym miejscu zapytać o wartość zmiennej programu, ponieważ zmienna nie została jeszcze utworzona ani program nie został uruchomiony.
Jeśli w wyrażeniu #if użyjesz nazwy, która nie jest zdefiniowanym makrem, preprocesor potraktuje ją jak 0. Literówka w nazwie wersji może więc po cichu wybrać inną gałąź, zamiast dać błąd. Gdy warunek ma sprawdzać samo istnienie makra, użyj #ifdef NAZWA albo #if defined(NAZWA).
Makra platformowe mogą służyć do wyboru fragmentów przeznaczonych dla danego systemu. Ich nazwy i znaczenie zależą od kompilatora oraz platformy, więc samo użycie #ifdef nie czyni programu przenośnym. W większym projekcie warto ograniczać taki kod do miejsc, które rzeczywiście wymagają funkcji systemowych.
Przykład pokazuje sam mechanizm, a nie gotowy sposób na przenośność całego programu:
#if defined(_WIN32)
// fragment przeznaczony dla środowiska Windows
#elif defined(__linux__)
// fragment przeznaczony dla środowiska Linux
#else
// pozostałe środowiska
#endif
Nazwy _WIN32 i __linux__ są dostarczane przez narzędzia dla odpowiednich platform. Kod wewnątrz każdej gałęzi nadal musi być poprawny dla kompilatora, który ją wybierze.
Pozostałe przydatne dyrektywy
#undef NAZWA usuwa bieżącą definicję makra. Po tej dyrektywie preprocesor nie będzie już rozwijał NAZWA według poprzedniej definicji. Ponowne definiowanie tego samego makra w różnych miejscach zwykle utrudnia zrozumienie kodu, dlatego #undef przydaje się głównie w szczególnych przypadkach.
#define ROZMIAR_BUFORA 1024
// Tutaj ROZMIAR_BUFORA rozwija się do 1024.
#undef ROZMIAR_BUFORA
// Od tego miejsca ta nazwa nie jest już makrem.
#error zatrzymuje przetwarzanie i wyświetla komunikat. Można dzięki niemu jasno zgłosić brak wymaganego ustawienia:
#ifndef WERSJA
#error "Musisz zdefiniować makro WERSJA"
#endif
Jeśli WERSJA nie zostało zdefiniowane, kompilator zatrzyma proces na etapie preprocesowania i poda wskazany komunikat. #warning bywa obsługiwane przez kompilatory, ale jego dostępność i zachowanie zależą od narzędzia.
Przykład #warning:
#warning "Ta część programu wymaga sprawdzenia"
Kompilator może wtedy wypisać ostrzeżenie i kontynuować pracę. Ponieważ obsługa tej dyrektywy różni się między kompilatorami, nie należy jej traktować jako przenośnego mechanizmu wymaganego przez język.
Kompilator udostępnia też predefiniowane nazwy. __FILE__ oznacza nazwę bieżącego pliku, a __LINE__ — numer wiersza. W C++ można użyć ich na przykład w prostym komunikacie diagnostycznym:
#include <iostream>
int main() {
std::cout << "Plik: " << __FILE__
<< ", wiersz: " << __LINE__ << '\n';
}
__DATE__ i __TIME__ dostarczają datę i czas kompilacji. Z kolei __func__ jest w C++ nazwą dostępną wewnątrz funkcji, a nie makrem preprocesora. Takie informacje bywają przydatne przy diagnozowaniu błędów, choć w praktycznych projektach zwykle korzysta się z bibliotek do logowania.
Najczęstsze nieporozumienia
- „
#includewstawia do programu gotową funkcję”. Zwykle nagłówek udostępnia jej deklarację; definicja może być w innym pliku i wymagać linkowania. - „Makro jest funkcją”. Makro zastępuje tekst przed sprawdzaniem typów i składni. Nie zapewnia takich samych reguł jak funkcja.
- „
#ifdef WERSJAsprawdza, czy WERSJA wynosi 1”. Sprawdza tylko, czy nazwa makra została zdefiniowana. Do sprawdzenia liczby służy#if WERSJA == 1. - „Preprocesor widzi wartości zmiennych”. Nie widzi ich podczas działania programu. Kompilacja warunkowa wybiera tekst wcześniej, przed kompilowaniem.
Kiedy preprocesor zakończy pracę, kompilator analizuje przygotowany kod. W następnej notatce przejdziemy od tego tekstu do podstawowych obiektów programu: zmiennych, ich typów i wartości.