Podstawy programowania 9

Preprocesor

Jedynie bardzo proste programy w C++ nie używają funkcji bibliotecznych. Dlatego właściwie wszystkie programy zaczynają się od dołączenia nagłówków w których zawarte są deklaracje funkcji bibliotecznych. Dołączanie nagłówków to właśnie jedno z zadań preprocesora.

Preprocesor to program wywoływany przed wywołaniem kompilatora. Jest on stosunkowo prosty i wykonuje po prostu pewne informacje na tekście. Preprocesor nie rozumie typów ani składni C/C++, więc w języku C był stosowany do pisania kodu mającego działać z różnymi typami. W języku C++ role te przejęły bezpieczniejsze funkcje inline i szablony. W innych zastosowaniach jednak preprocesor bywa przydatny.

Dyrektywa #include

Na razie spotkaliśmy tylko jedną dyrektywę preprocesora – #include. Dyrektywa ta każe preprocesorowi znaleźć odpowiedni plik i wstawić jego zawartość w tym miejscu zamiast linijki z dyrektywą. Następnie przetwarzane są dyrektywy preprocesora wewnątrz wstawionego pliku.

#include <...> a #include "..."

Jeżeli nazwę pliku podamy w nawiasach kątowych, to w celu znalezienia żądanego pliku przeszukiwane są Tylko katalogi systemowe (można wpływać na listę tych katalogów przez odpowiednie opcje kompilatora). Jeśli nazwę pliku podamy w cudzysłowach, to najpierw przeszukiwany jest bieżący katalog, dopiero gdy nie ma żądanego pliku przeszukiwane są katalogi systemowe.

Z reguły wstawiany plik jest plikiem nagłówkowym, jednak tak naprawdę nie ma to znaczenia. Preprocesor wstawi tutaj dowolny wskazany przez nas plik (o ile będzie w stanie go znaleźć).

Dyrektywa #define

Z użyciem tej dyrektywy możemy definiować makra preprocesora. Są dwa rodzaje makr:

Makra tylko z nazwą

Dyrektywa w rodzaju:

#define DEBUG

powoduje, że preprocesor dodaje od tej pory nazwę (słowo) DEBUG do swojej listy zdefiniowanych makr. Ewentualne wywołanie makra powoduje, że preprocesor zamieni makro na pusty łańcuch symbol. Samo w sobie niespecjalnie użyteczne, ale przydatne wraz z dyrektywą #if o której za chwilę.

Proste makra bez parametrów

Dyrektywa:

#define DEBUG true

powoduje, że preprocesor dodaje od tej pory nazwę (słowo) DEBUG do swojej listy zdefiniowanych makr i zastępuje wystąpienia DEBUG w tekście programu na true (to jest tylko operacja na tekście, preprocesor nie ma pojęcia o znaczeniu tych słów). Czyli po napisaniu

#define DEBUG true
if (DEBUG) {
  ...
}

Kompilator zobaczy tylko

if (true) {
  ...
}

Makra z parametrami

Makra te wyglądają jak powyższe, ale mają też argumenty, co pozwala na pisanie czegoś co przypomina funkcje, ale nie podlega kontroli typów. W języku C czasem bywało przydatne, w języku C++ raczej nie należy z tego korzystać. Dla przykładu:

#define MAX(a, b) (((a) > (b)) ? (a) : (b))

tworzy makro o dwóch parametrach mające obliczyć większą z dwóch liczb. Zachowuje się ono prawie jak funkcja, ale nie do końca. Dopóki a i b są zwykłymi zmiennymi nie będzie z reguły problemów, ale w innym przypadku może działać nie tak jak byśmy tego chcieli. Wywołanie:

MAX(1, 2);
MAX(--i, ++j);

Spowoduje, że kompilator otrzyma:

(((1) > (2)) ? (1) : (2));
(((++i) > (--j)) ? (++i) : (--j));

Z pierwszą linijką nie ma problemu – zwróci 2 tak jakbyśmy się tego spodziewali, jednak druga linijka zmienia jedną ze zmiennych i, j dwa razy a drugą tylko raz.

Nazwy makr pisaliśmy wielkimi literami. Nie jest to wymagane – preprocesor przełknie także małe litery. Jednak jest to bardzo dobra konwencja, która powoduje, że wywołując makro będziemy wiedzieć, że jest to makro i trzeba nieco uważać.

Dyrektywa #undef

Dyrektywa #undef usuwa makro z listy symboli preprocesora (działa więc odwrotnie do #define). Podane makro nie musi na liście symboli istnieć – w takim przypadku nic się nie dzieje:

#undef MAX

Dyrektywa #if

Dyrektywa #if służy do przeprowadzania tzw. kompilacji warunkowej. Odpowiednie linijki kodu mogą być przekazywane do kompilatora lub nie w zależności od poleceń programisty. Pozwala to np. pisać kod testujący obok kodu właściwego. Jednocześnie w wersji produkcyjnej nie ma już śladu po kodzie testującym.

W najprostszym przypadku dyrektywa ta wygląda następująco:

#if warunek
//
// linijki pomijane jeśli warunek nie jest spełniony
//
#endif

Dyrektywa przyjmuje warunek, w którym najpierw rozwijane są wszystkie makra preprocesora, a następnie powstałe wyrażenie jest wyliczane podobnie do reguł w języku C/C++ jako wyrażenie liczbowe stałoprzecinkowe. Można używać większości operatorów logicznych i arytmetycznych z C++. Jeżeli obliczona wartość jest niezerowa, to preprocesor przyjmuje, że warunek zaszedł. Istnieje specjalny operator do stosowania wewnątrz warunków – operator defined który pozwala testować, czy podane makro zostało uprzednio zdefiniowane czy nie.

Każde #if musi być zakończone przez #endif, ponadto może być także część z #else. Podobnie jak w C++, często zachodzi potrzeba zagnieżdżania warunków. Aby nieco uprościć składnię dodano dyrektywę #elif, która zastępuje zagnieżdżone wywołanie #else oraz #if i nie potrzebuje końcowego #endif.

Bardzo często spotykane w praktyce wywołania #if defined MACRO oraz #if not defined MACRO można skrócić do odpowiednio #ifdef MACRO i #ifndef MACRO.

#ifdef DEBUG
//
// kod testowy
//
#endif /* DEBUG */

Komentarz za #endif nie ma znaczenia dla preprocesora, jedynie pomaga programiście rozumieć lepiej i orientować się co się dzieje – jest to pomocne, gdy kod testowy jest bardzo długi.

Użycie w nagłówkach

Z reguły chcemy, by każdy z plików nagłówkowych był dołączany najwyżej raz, w przeciwnym przypadku może to prowadzić do bardzo dziwnych zachować kompilatora. Dlatego w plikach nagłówkowych można spotkać bardzo często coś takiego:

#ifndef _PLIK_H_
#define _PLIK_H_

// cała zawartość
// pliku nagłówkowego "plik.h"

#endif // _PLIK_H_

Powoduje to, że za pierwszym razem preprocesor dołączy plik nagłówkowy, ale za drugim razem już nie, bo teraz makro _PLIK_H_ będzie już zdefiniowane. Sama nazwa makra jest dowolna, jednak połączenie jej z nazwą pliku w powyższy sposób powoduje, że jest bardzo mało prawdopodobne, aby preprocesor natknął się na powyższe makro gdzie indziej.