Preprocesor i funkcje ze zmienną liczbą argumentów

Transkrypt

Preprocesor i funkcje ze zmienną liczbą argumentów
Laboratorium 3:
„Preprocesor i funkcje ze zmienną liczbą
argumentów”
mgr inż. Arkadiusz Chrobot
10 listopada 2010
1
Preprocesor
Preprocesor jest programem uruchamianym przed właściwym procesem kompilacji programów napisanych w języku C. Jego działanie przypomina funkcjonowanie
kompilatora, ale jest znacznie prostsze. Preprocesor analizuje kod źródłowy programu w poszukiwaniu przeznaczonych dla niego poleceń. Te polecenia mają postać
dyrektyw lub makr. Celem wykonania preprocesora jest przekształcenie kodu źródłowego programu zgodnie z treścią tych poleceń. To przekształcenie może mieć na
celu:
• włączenie do programu treści pliku nagłówkowego,
• wprowadzenie do kodu źródłowego wartości stałych,
• umieszczenie fragmentów kodu, które mogą być użyte np. do debugowania,
• dostosowanie kodu źródłowego programu do architektury na której będzie
wykonywany kod wynikowy (np. użycie odpowiedniego dla danej platformy
typu danych),
• wykonanie czynności, które powinny być zrobione przed rozpoczęciem kompilacji programu, ale z pewnych przyczyn nie mogą być wykonane „ręcznie”.
Preprocesor w przeciwieństwie do kompilatora nie dokonuje sprawdzenia typów danych ani innych czynności mających na celu kontrolę poprawności programu. Powinniśmy więc pamiętać, aby do stosowania preprocesora podchodzić z umiarem. Jest
on niewątpliwie bardzo pomocnym narzędziem, ale należy umieć korzystać z niego
właściwie, aby uniknąć trudnych do znalezienia błędów. Dzięki preprocesorowi możliwa jest tzw. kompilacja warunkowa, która pozwala na otrzymanie różnych kodów
wynikowych z tego samego kodu źródłowego. Kompilator gcc automatycznie wywołuje preprocesor przed rozpoczęciem właściwego procesu kompilacji, ale możemy
też uruchomić preprocesor samodzielnie, z wiersza poleceń, za pomocą polecenia
cpp podając jako jego argument wywołania nazwę pliku z kodem źródłowym.
2
Dyrektywy preprocesora
Dyrektywy są to krótkie polecenia dla preprocesora. W praktyce już się z takimi poleceniami spotkaliśmy. Dyrektywa #include nakazuje włączenie do treści
programu zawartości pliku nagłówkowego. Dyrektywa #define definiuje stałe. Preprocesor po napotkaniu takiej definicji zapamiętuje identyfikator i wartość za nią
umieszczoną, a następnie analizuje resztę kodu i wszędzie tam, gdzie napotka ten
identyfikator zastępuje go związaną z nim wartością. Wartownicy plików nagłówkowych są niczym innym jak instrukcją warunkową dla preprocesora. Taka instrukcja
może się zaczynać od dyrektywy #ifndef (jeśli nie zdefiniowano) po które występuje nazwa identyfikatora, który powinien być zdefiniowany. Jeśli preprocesor dotychczas nie napotkał w kodzie źródłowym takiego znacznika, to powinien wykonać
resztę instrukcji które występują w kolejnych wierszach po #ifndef. W przypadku wartowników plików nagłówkowych jest to definicja takiego samego znacznika
jak występował w #ifndef, oraz szereg innych „zwykłych” instrukcji języka C lub
preprocesora, które zależne są od przeznaczenia pliku nagłówkowego. Całość jest
zakończona dyrektywą #endif. Zauważmy, że identyfikator definiowany w pliku nagłówkowym nie ma przypisanej żadnej wartości. Służy on jedynie za znacznik. Jeśli
1
preprocesor napotkał już wcześniej taki znacznik to znaczy, że plik nagłówkowy został już włączony do kodu źródłowego i nie należy włączać go ponownie. Jeśli nie
to plik nagłówkowy jest włączany. Instrukcja warunkowa preprocesora ma jeszcze
inną postać, która zaczyna się od dyrektywy #ifdef (jeśli zdefiniowano). Może również zawierać dyrektywę #else, która określa co preprocesor powinien zrobić jeśli
warunek nie jest spełniony. Instrukcje warunkowe preprocesora można zagnieżdżać.
Poniżej zamieszczony jest przykład programu w którym użyto instrukcji preprocesora.
sample.c
#i n c l u d e <s t d i o . h>
i n t main ( v o i d )
{
#i f d e f TEST
p r i n t f ( ” Znacznik TEST z d e f i n i o w a n y . \ n” ) ;
#e l s e
p r i n t f ( ” Znacznik TEST n i e z d e f i n o w n a y . \ n” ) ;
#e n d i f
return 0;
}
Jeśli skompilujemy program tak jak dotychczas to robiliśmy to po jego uruchomieniu
otrzymamy komunikat, że znacznik TEST nie jest zdefiniowany. Jeśli jednak wywołamy kompilator tak: gcc -Wall -o sample sample.c -DTEST , to po uruchomieniu
programu otrzymamy komunikat o tym, że znacznik został zdefiniowany. Podobny
efekt można uzyskać umieszczając w kodzie źródłowym programu przez istniejącymi
instrukcjami preprocesora wiersz #define TEST . Spora część programistów piszących w języku C uważa, że instrukcje preprocesora umieszczane wewnątrz funkcji
utrudniają czytanie ich kodu. Można zapobiec takiemu utrudnieniu definiując osobne funkcje dla każdego przypadku instrukcji warunkowej, tak jak to pokazano na
następnym przykładzie.
sample2.c
#i n c l u d e <s t d i o . h>
#i f d e f TEST
i n t main ( v o i d )
{
p r i n t f ( ” Znacznik TEST z d e f i n i o w a n y . \ n” ) ;
return 0;
}
#e l s e
i n t main ( v o i d )
{
p r i n t f ( ” Znacznik TEST n i e z d e f i n o w n a y . \ n” ) ;
return 0;
}
#e n d i f
2
3
Makra preprocesora
O makrach preprocesora możemy myśleć jako o pewnego rodzaju podprogramach. Od zwykłych funkcji języka C różni je to, że nie są wywoływane w trakcie
wykonywania programu, ale są rozwijane w miejscu wywołania przez preprocesor
przed rozpoczęciem kompilacji. Przypominają pod tym względem funkcje poprzedzone słowem kluczowym inline. Mogą przyjmować dowolną liczbę parametrów, jednakże zgodność typów tych parametrów nie jest sprawdzana. Jest to główna wada
makr preprocesora w porównaniu do funkcji inline. Struktura makr jest stosunkowo prosta. Ich definicję rozpoczyna dyrektywa #define po której występuje nazwa
makra. Po nazwie makra można umieścić listę argumentów. Argumenty makr nie
posiadają typów, a jedynie identyfikatory (nazwy). Po liście argumentów umieszcza
się instrukcję w języku C, która ma być wykonana w ramach makra. Jeśli makro
ma zawierać kilka instrukcji to umieszczamy te instrukcje w nawiasach klamrowych,
przy czym każdy kolejny wiersz makra, łącznie z nagłówkiem, ale oprócz ostatniego,
powinien być zakończony znakiem \ (backslash). Poniżej zamierszczony jest krótki
program ilustrujący użycie makra preprocesora.
sample3.c
#i n c l u d e <s t d i o . h>
#d e f i n e PRINT( format , v a l u e ) {\
p r i n t f ( ( format ) , ( v a l u e ) ) ; \
p r i n t f ( ” \n” ) ; \
}
i n t main ( v o i d ) {
int i = 24;
double j = 5 . 5 ;
PRINT( ”%x” , i ) ;
PRINT( ”%f ” , j ) ;
return 0;
}
Umieszczone w kodzie makro ma za zadanie wypisać podaną wraz z odpowiednim formatowaniem wartość i przenieść kursor do następnego wiersza na ekranie.
Efekt jego użycia możemy obejrzeć wywołując preprocesor w następujący sposób:
cpp sample3.c. Makra nazywane są wymiennie makrodefinicjami. Definiując makro
możemy jego treści nie zamykać w nawiasach klamrowych. Czasem takie postępowanie jest konieczne, ale często powoduje trudne do wykrycia błędy. Zaleca się
aby, tak jak w przykładzie, umieszczać wewnątrz makr odwołania do argumentów
w nawisach okrągłych.
4
Asercje
Ciekawym i pożytecznym zastosowaniem makr są asercje nazywane również niezmiennikami. Zostały one wprowadzone do programowania przez R. Floyda i pierwotnym ich przeznaczeniem było formalne dowodzenie poprawności programów.
Asercja jest po prostu warunkiem, który musi być spełniony przed i po wykonaniu
3
określonego fragmentu programu (np. pętli), aby uznać że ten fragment działa poprawnie. Szybko zauważono, że niezmienniki można wykorzystać w programowaniu
nie tylko do dowodzenia poprawności programu przy użyciu aparatu matematycznego, ale również do testowania jego zachowania podczas wykonania. W języku C
dostępne jest makro o nazwie assert(), które jako parametr przyjmuje warunek, który ma być prawdziwy podczas wykonania programu. Jeśli tak nie będzie to makro
wypisuje na ekran krótki komunikat i przerywa działanie programu. Makra assert()
można używać podczas testowania programu. Po opracowaniu jego wersji finalnej
można je wyłączyć1 definiując znacznik NDEBUG. Aby móc w ogóle skorzystać
z makra assert() należy w kodzie źródłowym programu włączyć nagłówek assert.h
sample4.c
#i n c l u d e <s t d i o . h>
#i n c l u d e <a s s e r t . h>
#d e f i n e SIZE 10
u n s i g n e d c h a r t a b l i c a [ SIZE ] ;
i n t main ( v o i d ) {
int i ;
f o r ( i =0; i <200∗SIZE ; i ++) {
a s s e r t ( i <SIZE ) ;
t a b l i c a [ i ]= i ;
}
return 0;
}
Przedstawiony przykładowy program w rażący sposób narusza ograniczenie rozmiaru zdefiniowanej tablicy. Po jego skompilowaniu możemy się przekonać, że makro
assert() przerywając wykonanie programu nie dopuści aby zmienna indeksująca
miała wartość większą lub równą rozmiarowi tablicy. Jeśli wyłączymy to marko za
pomocą znacznika NDEBUG to program najprawdopodobniej zostanie zakończony
w trybie krytycznym z komunikatem, że została naruszona ochrona pamięci.
5
Makra w debugowaniu
Nie tylko makrodefinicja assert() może być użyta podczas debugowania programu. Możemy tworzyć również własne makra, których będzie można użyć podczas
testowania programu, a następnie wyłączyć za pomocą znacznika NDEBUG lub
innego, który sami zdefiniujemy. Niżej umieszczony został przykład takiego makra. Jego działanie można wyłączyć definiując znacznik NOTEST. Proszę zwrócić
uwagę, na fakt, że po dyrektywie #else zostało zdefiniowane makro o takiej samej
nazwie, ale puste. To oznacza, że jeśli znacznik NOTEST będzie zdefiniowany, to
w miejsce makrodefinicji w kodzie źródłowym preprocesor nic nie wstawi. Na tym
1 Niektórzy informatycy, jak np. C.A.R Hoare uważają, że asercje powinno się zostawiać na
wszelki wypadek również w wersjach finalnych programów. Takie działanie wydaje się być jak
najbardziej uzasadnione.
4
polega właśnie wyłączenie działania makrodefinicji.
sample5.c
#i n c l u d e <s t d i o . h>
#i f n d e f NOTEST
#d e f i n e PRINT( format , v a l u e ) {\
p r i n t f ( ( format ) , ( v a l u e ) ) ; \
p r i n t f ( ” \n” ) ; \
}
#e l s e
#d e f i n e PRINT( format , v a l u e )
#e n d i f
i n t main ( v o i d )
{
int i ;
f o r ( i =0; i <20; i ++)
PRINT( ”%x” , i ) ;
return 0;
}
6
Funkcje o zmiennej liczbie argumentów
Język C pozwala definiować funkcje o zmiennej liczbie argumentów (ang. variadic functions). Oznacza to, że liczba argumentów, które możemy przekazać do takiej
funkcji ograniczona jest jedynie rozmiarem stosu. Na liście argumentów formalnych
funkcji o zmiennej liczbie argumentów powinien znaleźć się choć jeden „zwykły”
argument, który nie jest zmienną rejestrową ani wskaźnikiem. Jeśli takich argumentów będzie na liście więcej, to ta uwaga dotyczy ostatniego z nich. Po zwykłych
argumentach w liście argumentów funkcji o zmiennej liczbie argumentów występuje
przecinek i trzy kropki (...), które oznaczają, że funkcja oprócz wymienionych na
liście argumentów może przyjąć dowolną liczbę innych argumentów. Aby obsłużyć
dodatkowe argumenty wywołania przekazane w ten sposób do funkcji potrzebny jest
nam typ va list i odpowiednie makra zdefiniowane w pliku nagłówkowym stdarg.h.
Makro va start przyjmuje dwa argumenty, pierwszym jest zmienna typu va list, którą to makro inicjalizuje a drugim nazwa ostatniego argumentu spośród „zwykłych”
argumentów umieszczonych na liście argumentów funkcji. To makro musi zostać wywołane przed wszystkimi innymi, które zostaną opisane. Zmienna typu va list jest
listą wszystkich argumentów dodatkowych, przekazanych do funkcji. Makro va arg
umożliwia poruszanie się po tej liście. Przyjmuje ono dwa parametry. Pierwszym
jest lista parametrów dodatkowych, drugim typ wartości, która zostanie zwrócona.
Wywoływanie makra va arg umieszczone jest najczęściej w pętli. Przy każdej iteracji pętli makro to zwraca wartość bieżącego argumentu na liście, przyjmując typ
wartości tego argumentu taki, jaki został określony drugim argumentem wywołania
marka oraz modyfikuje tak listę, aby po jego ponownym wywołaniu zwrócić następny element z tej listy. Po przejrzeniu całej listy należy wywołać makro va end, które
5
jako argument wywołania przyjmuje listę argumentów dodatkowych. Jeśli ponownie chcielibyśmy przejrzeć tę listę, to musimy jeszcze raz użyć wyżej wymienionych
makr. Ostatnim makrem związanym z obsługą listy argumentów dodatkowych jest
va copy. Służy ono, zgodnie ze swoją nazwą do tworzenia kopii listy argumentów.
Nie należy do tego celu używać instrukcji przypisania! W przykładzie znajdującymi
się poniżej przedstawiono program z funkcją, która liczy średnią wartości jej argumentów dodatkowych. Przyjęto w niej, że pierwszy argument przechowuje liczbę
argumentów dodatkowych. W wywołaniu takiej funkcji można przekazać jej argumenty, które są literałami (wartościami), wyrażeniami, stałymi i zmiennymi.
sample6.c
#i n c l u d e <s t d i o . h>
#i n c l u d e <s t d a r g . h>
f l o a t average ( i n t counter , . . . )
{
v a l i s t ap ;
i n t next =0, sum=0, i=c o u n t e r ;
i f ( c o u n t e r ==0)
return 0 . 0 ;
v a s t a r t ( ap , c o u n t e r ) ;
w h i l e ( i −−) {
next = v a a r g ( ap , i n t ) ;
sum += next ;
}
va end ( ap ) ;
r e t u r n ( f l o a t ) sum/ c o u n t e r ;
}
i n t main ( v o i d )
{
f l o a t r e s u l t = average ( 4 , 1 , 2 , 3 , 4 ) ;
p r i n t f ( ”%f \n” , r e s u l t ) ;
return 0;
}
Warto pamiętać, że w języku C prototyp funkcji zapisany np. tak: int funkcja()
również oznacza funkcję, która przyjmuje zmienną liczbę argumentów. Jeśli chcemy stworzyć funkcję, która nie przyjmuje w ogóle argumentów, to powinniśmy jej
prototyp zapisać następująco int funkcja(void).
6