node146






23.1 Moduły programu



























Dalej: 23.2 Przestrzenie nazw
W górę: 23. Moduły i przestrzenie nazw
Wstecz: 23. Moduły i przestrzenie nazw




Podrozdziały


23.1.1 Pliki nagłówkowe i implementacyjne







23.1 Moduły programu



Program w języku C++ może być fizycznie zapisany w wielu plikach.
Jak wiemy
z rozdziału o dyrektywach preprocesora ,
w pliku można umieścić polecenie włączenia innego
pliku z kodem źródłowym. To, co zobaczy kompilator po przetworzeniu
przez preprocesor, to tekst całości: dla kompilatora zatem będzie to
jeden moduł, choć fizycznie zapisany jest w
dwóch lub więcej plikach. Taki moduł zwany jest
jednostką translacji.
Z kolei cały program może składać się z wielu jednostek
translacji, z których każda może być kompilowana osobno.
Przy większych programach jest to bardzo istotne; zmiana wprowadzona
w jednej jednostce powoduje konieczność ponownej kompilacji tej
jednostki, ale nie zawsze całego programu.


Jednostki translacji mogą, i powinny, stanowić jednocześnie
podstawę podziału programu na jednostki logiczne. Tak jak pewne
powtarzalne pojedyncze zadania staramy się zapisać w postaci
oddzielnych funkcji, tak zespół funkcji i klas dotyczących pewnego
wycinka ogólnego zadania realizowanego przez cały program można
zebrać w jednej jednostce translacji. Upraszcza to pisanie,
analizowanie i pielęgnację kodu, szczególnie gdy przybiera on znaczne
rozmiary i jest pisany czy modyfikowany przez wielu programistów.


Każda jednostka translacji kompilowana jest niezależnie, być może
w innym czasie i na innym komputerze. Ponieważ w C++ sprawdzane są
typy zmiennych i poprawność wywołań funkcji, wynika z tego, że
każda funkcja, która jest w danej jednostce translacji używana
(wywoływana), musi być w tej jednostce zadeklarowana. Natomiast
definicja funkcji powinna być tylko jedna, umieszczona w jednej tylko
jednostce translacji (nie dotyczy to funkcji rozwijanych, których
definicja musi być widoczna w każdej jednostce translacji
w której są używane). Oczywiście wszystkie deklaracje i definicja
funkcji muszą być zgodne.


Po połączeniu przez linker (program łączący),
wszystkie funkcje z różnych jednostek translacji „widzą'' się
nawzajem bez dodatkowych zabiegów. Zatem nazwy funkcji globalnych
należą do „uwspólnionego'' zakresu złożonego z zakresów
globalnych wszystkich modułów (mówimy, że są eksportowane).
Wielu programistów umieszcza jednak słowo kluczowe
extern
przed deklaracją funkcji w jednostce translacji, w której nie ma
definicji tej funkcji. Jest to pamiątka po czystym C, w C++
dopuszczalna, ale zbędna.


Wyjątkowo, funkcje globalne zdefiniowane ze specyfikatorem

static nie są eksportowane (włączane do
„uwspólnionego'' zakresu globalnego); są widoczne tylko dla funkcji
z tego samego modułu.


Inaczej rzecz się ma ze zmiennymi zadeklarowanymi w zasięgu globalnym.
Tu uwspólnienia nie ma: zmienna globalna
x z jednej
jednostki translacji jest widoczna tylko wewnątrz tej jednostki; inny
moduł może bezkonfliktowo zdefiniować zmienną globalną o tej
samej nazwie i będą to dwie oddzielne zmienne, każda widoczna tylko
w swoim module. Jeśli taką zmienną chcemy eksportować, to należy
ją zdefiniować w jednym tylko module, a w pozostałych jednostkach
translacji, w których będziemy z niej korzystać, zadeklarować
ją jako zmienną zewnętrzną za pomocą
specyfikatora
extern (patrz
rozdział o zmiennych zewnętrznych ).
Jeśli natomiast, na odwrót, chcemy zdefiniować zmienną globalną
i zagwarantować, że nie będzie ona dostępna w innych
modułach, nawet jeśli, przypadkowo, będzie w nich zadeklarowana
zmienna zewnętrzna (
extern) o tej samej nazwie, to definiujemy
ją z modyfikatorem
static (patrz
rozdział o zmiennych statycznych ).






23.1.1 Pliki nagłówkowe i implementacyjne



Pisząc większy program, musimy godzić ze sobą dwa wymagania.
Z jednej strony, program powinien być łatwy do zrozumienia,
rozwijania i modyfikowania dla autorów programu. Z drugiej strony,
pamiętać trzeba o wygodzie użytkownika - zapewnić trzeba
przejrzysty interfejs pozwalający na efektywne korzystanie z programu
i ewentualne jego rozwijanie bez konieczności wnikania w gąszcz
szczegółów implementacyjnych. Tym celom służy podział programu na
pliki o różnym charakterze.


Wiele jednostek kompilacyjnych może korzystać z tych samych funkcji,
klas, szablonów, przestrzeni nazw, wyliczeń... Ich deklaracje
muszą więc być dokładnie takie same. Moglibyśmy je oczywiście
powtarzać we wszystkich modułach. Byłoby to jednak proszeniem się
o kłopoty. Jakąkolwiek poprawkę czy zmianę trzeba by wtedy
wprowadzać do każdego pliku, gdzie deklaracje te występują.
Zamiast tego można zebrać je do jednego pliku i w tych modułach,
gdzie są potrzebne i powinny być znane, włączać je za pomocą
dyrektywy
#include


(rozdział o preprocesorze ).
W plikach takich nie umieszczamy definicji funkcji czy metod klas,
tylko ich deklaracje (z wyjątkiem funkcji rozwijanych, które powinny
być tam umieszczone wraz z definicją). Pliki te stanowią właśnie
interfejs, z którego odczytać można nazwy, typ, przeznaczenie
i „instrukcje obsługi'' deklarowanych obiektów. Dobrym zwyczajem
jest wprowadzanie do takich plików precyzyjnych komentarzy. Pliki
takie nazywamy plikami nagłówkowymi
i tradycyjnie mają one rozszerzenie

.h. Są zwykle niewielkie, bo nie zawierają
definicji.


Definicje zadeklarowanych w pliku nagłówkowym obiektów
zbieramy z kolei w innym pliku,
pliku implementacyjnym. Tu nie ma
ogólnie przyjętej konwencji co do jego rozszerzenia, ale często
stosuje się rozszerzenie
.cxx
lub
.C, lub po prostu
.cpp.
Do tego pliku również włączamy za pomocą
dyrektywy
#include plik nagłówkowy z deklaracjami
definiowanych funkcji czy metod. W ten sposób mamy gwarancję, że
definicje będą spójne z deklaracjami (a więc z interfejsem), bo
ewentualne niezgodności będą wtedy wychwycone i zgłoszone przez
kompilator.


Przy bardziej skomplikowanej strukturze programu istnieje
niebezpieczeństwo, że wskutek zagnieżdżenia dyrektyw

#include ten sam plik nagłówkowy zostanie do tej samej
jednostki translacji włączony więcej niż raz. Czasem nie ma w tym
niczego złego, czasem może stwarzać problemy. Aby się przed tym
uchronić, można stosować „sztuczkę'' z użyciem dyrektywy

#ifndef opisaną w
rozdziale o dyrektywach preprocesora .


Mając już pliki nagłówkowy i implementacyjny, w których zebraliśmy
zarówno interfejs, jak i implementację pewnej funkcjonalności (na
przykład stosu czy drzewa poszukiwań binarnych), możemy ich użyć
w wielu różnych aplikacjach, które tej funkcjonalności wymagają.
Wystarczy wtedy


w pliku źródłowym aplikacji włączyć plik nagłówkowy;

zadbać o to, by w czasie łączenia (linkowania) programu
był dostępny skompilowany kod plików
implementacyjnych (kod źródłowy nie jest wtedy
potrzebny).


Rozpatrzmy prosty przykład. Przy wielu okazjach przydaje się
możliwość sortowania tablicy czy drukowania zawartości tablicy.
Piszemy zatem moduł złożony z dwóch plików.
W pliku nagłówkowym
sortint.h deklarujemy funkcje do tego
służące. Zamieszczamy komentarze mówiące, jak te funkcje stosować
i do czego służą



Wyszukiwarka