r21.pdf
(
370 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 dyrektywy
#ifdef
jest spełniony i
w pliku pośrednim znajdzie się wszystko, aż do następnej dyrektywy
#else
lub
#endif
. Jeśli
warunek dyrektywy
#ifdef
nie zostanie spełniony, 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 dyrektywy
#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
Plik z chomika:
Wojteczek
Inne pliki z tego folderu:
rdodc.pdf
(84 KB)
rdodb.pdf
(72 KB)
rdoda.pdf
(184 KB)
r21.pdf
(370 KB)
r20.pdf
(275 KB)
Inne foldery tego chomika:
Cisco
Kurs C++ od zera do hackera v. 1.0
Pascal
ZAHASŁOWANE
Zgłoś jeśli
naruszono regulamin