R21-06.DOC

(295 KB) Pobierz
Szablon dla tlumaczy

Rozdział 21.
Co dalej

Gratulacje! Przebrnąłeś już prawie przez całe wprowadzenie do C++. W tym momencie powinieneś już dobrze go rozumieć, ale w nowoczesnym programowaniu zawsze jest coś, czego jeszcze można się nauczyć. W tym rozdziale uzupełnimy brakujące szczegóły i wskażemy ci dalsze kierunki rozwoju.

Większość kodu, który zapisuje się w plikach kodu źródłowego, to C++. Ten kod jest interpretowany przez kompilator i zamieniany w program. Jednak przed uruchomieniem kompilatora zostaje uruchomiony preprocesor, który umożliwia kompilację warunkową.

Z tego rozdziału dowiesz się:

·         czym jest kompilacja warunkowa i jak nią zarządzać,

·         jak pisać makra preprocesora,

·         jak używać preprocesora do wyszukiwania błędów,

·         jak manipulować poszczególnymi bitami i używać ich jako znaczników,

·         jakie są następne kroki w efektywnej nauce C++.

 

Preprocesor i kompilator

Za każdym razem, gdy uruchamiasz kompilator, jako pierwszy rusza preprocesor. Preprocesor szuka swoich dyrektyw, z których każda zaczyna się od znaku hash (#). Efektem działania każdej z takich instrukcji jest zmiana tekstu kodu źródłowego. Rezultatem tej zmiany jest nowy plik kodu źródłowego — tymczasowy plik, którego zwykle nie widzisz, choć możesz poinstruować kompilator, aby zapisał go tak, abyś mógł go przeanalizować.

Kompilator nie odczytuje oryginalnego pliku kodu źródłowego; zamiast tego odczytuje i kompiluje plik będący wynikiem pracy preprocesora. Wykorzystywaliśmy ten mechanizm już wcześniej, dołączając pliki nagłówkowe za pomocą dyrektywy #include. Ta dyrektywa powoduje odszukanie pliku o wskazanej w instrukcji nazwie i dołączenie go w bieżącym miejscu do pliku pośredniego. Odpowiada to wpisaniu całego pliku nagłówkowego do kodu źródłowego; w momencie, gdy plik trafia do kompilatora, plik nagłówkowy już znajduje się w kodzie.

 

Przeglądanie formy pośredniej

Prawie każdy kompilator posiada przełącznik powodujący zapisanie pliku pośredniego na dysku; przełącznik ten można ustawiać albo w zintegrowanym środowisku programistycznym (IDE) albo w linii poleceń kompilatora. Jeśli chcesz przejrzeć plik pośredni, poszukaj odpowiedniego przełącznika w podręczniku dla swojego kompilatora.

 

Użycie dyrektywy #define

Dyrektywa #define definiuje podstawienie symbolu. Jeśli napiszemy:

 

#define BIG 512

 

to poinstruujemy preprocesor, by podstawił łańcuch 512 w każde miejsce, w którym napotka symbol BIG. Nie jest to jednak łańcuch w rozumieniu C++. Znaki 512 są wstawiane do kodu źródłowego w każdym miejscu, w którym zostanie napotkany symbol BIG. Symbol jest łańcuchem znaków, który może być użyty tam, gdzie może być użyty łańcuch, stała lub inny spójny zestaw znaków. Tak więc, jeśli napiszemy:

 

#define BIG 512

int myArray[BIG];

 

wtedy stworzony przez preprocesor plik pośredni będzie wyglądał następująco:

 

int myArray[512];

 

Zwróć uwagę na brak instrukcji #define. Instrukcje preprocesora są usuwane z pliku pośredniego i w ogóle nie występują w ostatecznym kodzie źródłowym.

 

Użycie #define dla stałych

Jednym z zadań dyrektywy #define jest podstawianie stałych. Jednak nie należy jej w tym celu wykorzystywać, gdyż dyrektyw ta jedynie podstawia łańcuch i nie dokonuje sprawdzenia typu. Jak wyjaśniono w podrozdziale dotyczącym stałych, użycie słowa kluczowego const ma o wiele więcej zalet niż użycie dyrektywy #define.

 

Użycie #define do definiowania symboli

Drugim zastosowaniem #define jest po prostu definiowanie określonych symboli. W związku z tym możemy napisać:

 

#define BIG

 

Później możemy sprawdzić, czy symbol BIG został zdefiniowany i jeśli tak, podjąć odpowiednie działania. Dyrektywami preprocesora, które sprawdzają, czy symbol został zdefiniowany, są dyrektywy #ifdef (if defined, jeśli zdefiniowany) oraz #ifndef (if not defined, jeśli nie zdefiniowany). Po obu z nich musi wystąpić dyrektywa #endif, kończąca blok kompilowany warunkowo.

Dyrektywa #ifdef jest prawdziwa, jeśli sprawdzany w niej symbol jest już zdefiniowany. Możemy więc napisać:

 

#ifdef DEBUG

cout << "Debug defined";

#endif

 

Gdy kompilator odczyta dyrektywę #ifdef, sprawdzi we wbudowanej wewnątrz siebie tablicy, czy zdefiniowany został symbol DEBUG. Jeśli tak, to warunek, dyrektywya #ifdef zostaniejest spełnionya i w pliku pośrednim znajdzie się wszystko, aż do następnej dyrektywy #else lub #endif. Jeśli warunek dyrektywya #ifdef nie zostanie spełnionya, to w pliku źródłowym nie znajdzie się żadna linia zawarta pomiędzy tymi dyrektywami; efektem będzie zupełne pominięcie kodu znajdującego się w tym miejscu.

Zwróć uwagę, że #ifndef stanowi logiczną odwrotność dyrektywy #ifdef. Warunek Ddyrektywya #ifndef jest spełniony, gdy w danym miejscu pliku nie został jeszcze zdefiniowany symbol.

 

Dyrektywa #else preprocesora

Jak można się domyślać, dyrektywa #else może być wstawiona pomiędzy dyrektywę #ifdef (lub #ifndef) a dyrektywę #endif. Sposób użycia tych dyrektyw ilustruje listing 21.1.

 

Listing 21.1. Użycie #define

  0:  #define DemoVersion

  1:  #define NT_VERSION 5

  2:  #include <iostream>

  3: 

  4: 

  5:  int main()

  6:  {

  7:     std::cout << "Sprawdzanie definicji DemoVersion,";

  8:     std::cout << "NT_VERSION oraz WINDOWS_VERSION...\n";

  9: 

10:     #ifdef DemoVersion

11:        std::cout << "Symbol DemoVersion zdefiniowany.\n";

12:     #else

13:        std::cout << "Symbol DemoVersion nie zdefiniowany.\n";

14:     #endif

15: 

16:     #ifndef NT_VERSION

17:        std::cout << "Symbol NT_VERSION nie zdefiniowany!\n";

18:     #else

19:        std::cout<<"Symbol NT_VERSION zdefiniowany jako: "<<NT_VERSION<<std::endl;

20:     #endif

21: 

22:     #ifdef WINDOWS_VERSION

23:        std::cout << "Symbol WINDOWS_VERSION zdefiniowany!\n";

24:     #else

25:        std::cout << "Symbol WINDOWS_VERSION nie zostal zdefiniowany.\n";

26:     #endif

27: 

28:     std::cout << "Gotowe.\n";

29:     return 0;

30:  }

 

Wynik

Sprawdzanie definicji DemoVersion,NT_VERSION oraz WINDOWS_VERSION...

Symbol DemoVersion zdefiniowany.

Symbol NT_VERSION zdefiniowany jako: 5

Symbol WINDOWS_VERSION nie zostal zdefiniowany.

Gotowe.

 

Analiza

W liniach 0. i 1. zostały zdefiniowane symbole DemoVersion oraz NT_VERSION, przy czym symbol NT_VERSION został zdefiniowany jako łańcuch 5. W linii 10. sprawdzana jest definicja DemoVersion, a ponieważ została zdefiniowana (mimo, iż nie ma wartości), warunek został spełniony, dlatego wypisany zostaje łańcuch z linii 11.

W linii 16. dyrektywa #ifndef sprawdza, czy symbol NT_VERSION nie został zdefiniowany. Ponieważ został zdefiniowany, warunek nie jest spełniony i wykonanie programu przeskakuje do linii 19. W tej linii, w miejscu symbolu NT_VERSION, jest podstawiany łańcuch 5, więc dla kompilatora cała linia ma postać:

 

std::cout<<"Symbol NT_VERSION zdefiniowany jako: "<<5<<std::endl;

 

Zwróć uwagę, że w miejscu pierwszego słowa NT_VERSION nic nie zostało podstawione, gdyż znajduje się ono w łańcuchu ujętym w cudzysłowy. Drugie NT_VERSION zostało jednak podstawione, więc kompilator widzi wartość 5, tak jakbyśmy ją sami wpisali.

Na koniec, w linii 22., program sprawdza symbol WINDOWS_VERSION. Ponieważ nie zdefiniowaliśmy tego symbolu, warunek nie jest spełniony i wypisany zostaje komunikat z linii 25.

 

Dołączanie i wartowniki dołączania

W przyszłości będziesz tworzył projekty zawierające wiele różnych plików. Prawdopodobnie zorganizujesz swoje kartoteki tak, aby każda klasa posiadała swój własny plik nagłówkowy (na przykład .hpp), zawierający deklarację klasy oraz własny plik implementacji (na przykład .cpp), zawierający kod źródłowy dla metod tej klasy.

Funkcja main() znajdzie się we własnym pliku .cpp, a wszystkie pliki .cpp będą kompilowane do plików .obj, które z kolei zostaną połączone przez linker w pojedynczy program.

Ponieważ twoje programy będą używać metod z wielu klas, więc do każdego pliku będzie dołączanych wiele plików nagłówkowych. Poza tym, pliki nagłówkowe często muszą dołączać następne pliki. Na przykład, plik nagłówkowy dla deklaracji klasy pochodnej musi dołączyć plik nagłówkowy dla jej klasy bazowej.

Wyobraźmy sobie, że klasa Animal jest zadeklarowana w pliku ANIMAL.hpp. Klasa Dog (pochodząca od klasy Animal) musi w pliku DOG.hpp dołączać plik ANIMAL.hpp, gdyż w przeciwnym razie klasa Dog nie będzie mogła zostać wyprowadzona z klasy Animal. Plik nagłówkowy klasy Cat z tego samego powodu także dołącza plik ANIMAL.hpp.

Gdy stworzysz metodę używającą zarówno klas Cat, jak i Dog, oznacza to niebezpieczeństwo dwukrotnego dołączenia pliku ANIMAL.hpp. To spowoduje błąd kompilacji, gdyż dwukrotne zadeklarowanie klasy (Animal) nie jest dozwolone, nawet jeśli obie deklaracje są identyczne. Możesz rozwiązać ten problem, stosując wartowniki dołączania. Na początku pliku nagłówkowego ANIMAL.hpp dopisz poniższe linie:

 

#ifndef ANIMAL_HPP

#define ANIMAL_HPP

...                      // w tym miejscu cała zawartość pliku

#endif // ANIMAL_HPP

 

W ten sposób informujemy preprocesor, że jeśli symbol ANIMAL_HPP nie jest zdefiniowany, ma go zdefiniować i dołączyć całą zawartość pliku występującą pomiędzy dyrektywami #define a #endif.

Za pierwszym razem, gdy program dołącza ten plik, odczytuje pierwszą linię i sprawdza, czy symbol ANIMAL_HPP nie jest zdefiniowany. Ponieważ nie jest on jeszcze zdefiniowany, zostaje zdefiniowany w następnej linii, a do pliku pośredniego zostaje dołączona cała zawartość pliku nagłówkowego.

Za drugim razem, gdy preprocesor dołącza plik ANIMAL.hpp, także testuje symbol ANIMAL_HPP, lecz tym razem jest on już zdefiniowany, więc preprocesor pomija cały blok kodu, aż do dyrektywy #else (która w tym przypadku nie występuje) lub #endif (na końcu naszego pliku). Tak więc pomija całą zawartość pliku nagłówkowego, a klasa nie zostaje zadeklarowana ponownie.

Sama nazwa definiowanego symbolu (ANIMAL_HPP) nie ma znaczenia, ale przyjęło się stosowanie nazwy pliku zapisanej dużymi literami, w której znak kropki (.) zostaje zamieniony na znak podkreślenia (_). Jest to jednak jedynie konwencja.

 

UWAGA              Stosowanie wartowników dołączania zawsze jest przydatne. Mogą one oszczędzić ci one wielu godzin debuggowania.

 

Funkcje makro

Dyrektywa #define może być używana także w celu tworzenia funkcji makro (tzw. makr). Funkcja makro jest symbolem stworzonym za pomocą dyrektywy #define; przyjmuje ona argument, podobnie jak zwykłe funkcje. Preprocesor podstawi przekazany łańcuch w każdym miejscu definicji, w którym występuje argument makra. Na przykład, makro TWICE (dwakroć) możemy zdefiniować jako:

 

#define TWICE(x) ( (x) * 2 )

 

 

a następnie napisać w kodzie:

 

TWICE(4)

 

Cały łańcuch TWICE(4) zostanie usunięty, a w jego miejscu zostanie podstawiona wartość 8! Gdy preprocesor natrafi na to makro z argumentem 4, w jego miejscu podstawi ( (4) * 2), co z kolei zostanie obliczone jako 4*2, czyli 8.

Makro może mieć więcej niż jeden parametr; każdy parametr może występować w tekście definicji wielokrotnie. Dwa powszechnie używane makra to MAX oraz MIN:

 

#define MAX(x,y) ( (x) > (y) ? (x) :  (y) )

#define MIN(x,y) ( (x) < (y) ? (x) :  (y) )

 

Zwróć uwagę, że w definicji makra nawiasy otwierające listę parametrów muszą występować bezpośrednio po nazwie makra bez występującej pomiędzy nimi spacji. Preprocesor nie pozwala na tak liberalne używanie spacji, jak robi to kompilator.

Jeśli napiszemy:

 

#define MAX (x,y) ( (x) > (y) ? (x) :  (y) )

 

a następnie spróbujemy użyć tak zdefiniowanego makra MAX:

 

int x = 5, y = 7, z;

z = MAX(x,y);

 

wtedy kod pośredni przyjmie postać:

 

int x = 5, y = 7, z;

z = (x,y) ( (x) > (y) ? (x) :  (y) )(x,y);

 

Zostałby jedynie podstawiony prosty tekst; nie nastąpiłoby wywołanie funkcji makro. Symbol MAX zostałby zastąpiony przez (x,y) ( (x) > (y) ? (x) :  (y) ), po którym występuje łańcuch (x,y), który miał pełnić rolę parametrów makra.

Po usunięciu spacji pomiędzy MAX a (x,y) kod pośredni przyjąłby postać:

 

int x =5, y = 7, z;

z =7;

 

Po co te wszystkie nawiasy?

Być może zastanawiasz się, po co w przedstawianych dotąd makrach stosowaliśmy tak wiele nawiasów. Preprocesor nie wymaga, by wokół argumentów w łańcuchu podstawiania były umieszczane nawiasy, ale nawiasy te pomagają unikać niepożądanych efektów ubocznych w przypadkach, gdy do makra przekazujemy skomplikowane wyrażenia. Na przykład, jeśli zdefiniujemy makro MAX jako:

 

#define MAX(x,y) x > y ? x : y

 

i przekażemy mu wartości ...

Zgłoś jeśli naruszono regulamin