Preprocesor języka C.pdf

(590 KB) Pobierz
686084320 UNPDF
notatnik konstruktora
Preprocesor języka C
Dodatkowe materiały
na CD i FTP
Preprocesor jest jednym z  najbardziej niedocenianych narzędzi
przez programistów. Co ciekawe, każdy programista korzysta z  co
najmniej kilku jego podstawowych funkcji. Opisujemy, co potrai
preprocesor oraz jak wykorzystać jego potęgę w  programowaniu.
kody źródłowe
Kody źródłowe prezentowane w artykule są
dostępne na stronie: http://toan.pl
wych pułapek jest wywołanie makra z in-
strukcjami arytmetycznymi np.:
#deine KWADRAT(x)((x)*(x))
int a;
int x=1;
a = KWADRAT(x++);
Tego typu makro zostanie rozwinięte jako:
a = (x++) * (x++);
Wynik będzie inny niż gdybyśmy do
realizacji tego zadania użyli funkcji. Kolej-
ną wadą jest bardzo trudne wyszukiwanie
błędów w  programach. Makra powodu-
ją, że komunikaty kompilatora o błędach
stają się dziwne i  niezrozumiałe. Sam
stosuję zasadę, że używam makr tylko
w sytuacjach, gdzie czas wykonania jest
krytyczny. W typowych zastosowaniach,
zamiast używania makr można również
zastosować funkcje typu inline. Będzie to
bardziej eleganckie rozwiązanie. Są jed-
nak przypadki, gdy makra są niezbędne
i nie da się ich zastąpić instrukcjami kom-
pilatora. Będziemy o tym mówić w dalszej
części artykułu.
Proces kompilacji składa się z dwóch
zasadniczych kroków: kompilacji oraz kon-
solidacji. Jednak zanim kod programu trai
do kompilatora, jest przetwarzany przez pre-
procesor. Preprocesor jest więc narzędziem,
które przetwarza kod źródłowy przed właści-
wym procesem kompilacji. Wszystkie dyrek-
tywy preprocesora zaczynają się znakiem #.
Najbardziej popularnymi są #include oraz
#deine.
Nie polecam jednak używania #deine
w tego typu sytuacjach. Lepiej jest używać
innych wyrażeń języka C, np.
loat const PI=3.14;
Deiniując liczby całkowite można rów-
nież używać enum np.
enum {N=10}; .
Dyrektywą odwrotną do #deine jest
#undef. Służy ona do cofnięcia deinicji
w miejscu użycia np.
#deine STALA 1
printf(„stala=%2”, STALA);
#undef STALA
printf(„stala=%2”, STALA);
W linii numer 4 wystąpi błąd kompilacji,
ponieważ po użyciu dyrektywy #undef, sta-
ła o nazwie STALA nie będzie istniała w pro-
gramie.
Możliwości #deine są jednak dużo
większe niż mogłoby się wydawać. Za pomo-
cą tej instrukcji możemy deiniować również
makra. Przeanalizujmy najpierw makro, któ-
re dodaje do siebie dwie liczby:
#deine ADD(X,Y)((X) + (Y))
int a;
loat b;
a = ADD(4, 5);
b = ADD(3.2, 1.1);
Z powyższego kodu zostanie utworzony
przez preprocesor następujący kod dla kom-
pilatora:
int a;
loat b;
a = 4 + 5;
b = 3.2 + 1.1;
Można zauważyć, że używanie makr
nie powoduje zbędnego narzutu jaki po-
wodują funkcje. Nie ma instrukcji skoku
do funkcji oraz nie trzeba pamiętać na sto-
sie argumentów funkcji. Kolejną ciekawą
właściwością jest niezależność operacji na
danych. W powyższym przykładzie użyli-
śmy tego samego makra dla danych typu
int oraz loat. Stosując funkcje potrzeba-
by napisać dwie implementacje: jedną dla
typu loat i drugą dla int . Pewnie zastana-
wiacie się, skoro te makra są takie dobre,
to dlaczego nie są powszechnie stosowane
w  programach? Odpowiedź jest bardzo
prosta: nie nadają się do pisania skompli-
kowanych i długich funkcji. Należy także
pamiętać, że kod jest wklejany w miejscu
użycia, a  to może powodować znaczne
rozrośnięcie się pliku wynikowego. Makra
trzeba używać z rozwagą, ponieważ mogą
być niebezpieczne w użyciu. Jedną z typo-
#include
Instrukcja #include dołącza do kodu
źródłowego kopię pliku podanego za tą in-
strukcją np. #include <stdio.h> . Dosłownie
wstawi zawartość pliku stdio.h w miejscu
wywołania.
Przyjęło się używanie plików z  roz-
szerzeniem „.h”, jednak może być to
dowolny plik tekstowy o  dowolnym
rozszerzeniu. Aby sprawdzić w  swo-
im projekcie, jak wygląda kod programu
przetworzony przez preprocesor, nale-
ży użyć opcji -E kompilatora np. gcc -E
program.c .
Możemy użyć instrukcji #include rów-
nież do innych ciekawych zastosowań. Za-
łóżmy, że mamy plik typu CSV (patrz ram-
ka) i chcemy wkleić zawartość tego pliku
jako tabelę do programu. Pierwszy sposób,
który wybierze większość programistów, to
oczywiście skopiowanie zawartości pliku
i wklejenie go do kodu. Jest to jednak strata
czasu. W projekcie pojawia się kolejna rzecz
o której musimy pamiętać, ponieważ za każ-
dym razem gdy plik CSV zmieni się, musi-
my także zmienić dane w kodzie programu.
Zamiast mozolnie wklejać dane, lepiej wy-
korzystać instrukcję #include i utworzyć
tabelę bezpośrednio z pliku CSV. Można to
zrobić w następujący sposób
int tabela[4][4] = {
#include «dane.csv»
};
#if, #ifdef, #ifndef, #elif, #else,
#endif
Preprocesor dostarcza również instruk-
cje warunkowe. Składnia jest bardzo prosta
np.:
#ifdef ALFA
// wykonaj instrukcje jeśli ALFA jest
zdeiniowane
#endif
Dyrektywa #if może sprawdzać wartość
stałej np.:
#if ALFA == 1
// wykonaj instrukcje jeśli ALFA jest
równe 1
#elif ALFA == 2
// wykonaj instrukcje jeśli ALFA jest
równe 2
#else
// wykonaj instrukcje jeśli ALFA jest
różne od 1 i 2
#endif
Instrukcje warunkowe są często stosowa-
ne do zapobiegania dołączaniu kilku kopii
plików nagłówkowych do tego samego pro-
jektu. Przykładowy plik nagłówkowy mógłby
wyglądać następująco:
#ifndef _PLIK_H_
#deine _PLIK_H_
// treść właściwa
#endif
Jeśli przez nieuwagę dołączymy ten plik
dwa razy np.:
#include „plik.h”
#include <stdio.h>
#include <plik.h>
W takiej sytuacji nic złego się nie stanie.
Jest to bardzo dobra praktyka i większość
Należy pamiętać, aby sprawdzić czy plik
CSV zawiera końcowe przecinki, ponieważ
dużo programów nie dodaje przecinka na
końcach linii, a to spowoduje błędną inter-
pretację pliku i błąd kompilacji.
#deine, #undef
Instrukcja #deine służy najczęściej do
zdeiniowania stałej np. #deine PI 3.14
78
ELEKTRONIKA PRAKTYCZNA 2/2010
686084320.006.png 686084320.007.png
Preprocesor języka C
istniejących bibliotek używa tego zabezpie-
czenia.
błędu w programie. Jest to dobre zabezpie-
czenie, gdy ktoś próbuje obarczyć winą
za awarię programistę. Jak wynika z mojej
praktyki, najczęstsze tłumaczenie obsługi
to „program zwariował”. Zwykle okazuje się
później, że pracownik z nudów „poklikał”
i narobił zamieszania.
Zastanówmy się, co warto zapisać do lo-
gów? Możemy użyć systemu komunikatów
typu „wskaźnik ma wartość NULL”. Jest to
jednak dość niewygodne i przy większych
programach można się pogubić w  komu-
nikatach. Lepiej jest zastosować notację,
która będzie bliska dla programisty np. za-
pisać w logu nazwę pliku wraz z numerem
wiersza. Przykładowy log mógłby wyglądać
następująco:
main.c:12
main.c:24
i2c.c:11
i2c.c:15
main.c:28
Za numerem linii można umieścić do-
datkowe informacje np. wartość zmiennej na
której operujemy. Tego typu informacje po-
zwolą ustalić faktyczny przebieg programu.
Tworzenie „na piechotę” tego typu logów nie
ma sensu i lepiej jest użyć w tym celu pre-
procesora. Oto przykładowy program:
#include <stdio.h>
#deine log() printf(«%s:%d\n»,__
FILE__,__LINE__)
#deine log_int(X) printf(«%s:%d
%s=%d\n»,__FILE__,__LINE__,#X,X)
int main()
{
int zmienna=123;
log();
log_int(zmienna);
return 0;
}
Makra predeiniowane
Preprocesor udostępnia nam kilka bar-
dzo użytecznych makr predeiniowanych.
Poniżej umieściłem listę podstawowych, do-
stępnych standardowo:
– __TIME__ – zwraca godzinę w  chwili
kompilacji
__DATE__ – zwraca datę w chwili kompi-
lacji
__LINE__ – numer linii, w której zostało
użyte
__FILE__ – nazwa pliku, w którym zosta-
ło użyte
Makra te są bardzo przydatne przy uru-
chamianiu programu. Mogą się także przy-
dać, gdy chcemy w programie umieścić datę
kompilacji.
Po uruchomieniu otrzymamy:
example.c:9
example.c:10 zmienna=123
W programie są dwa makra: log oraz
log_int . Makro log dodaje do logów tylko na-
zwę pliku i numer linii. Natomiast log_int
dodatkowo umożliwia dodanie do logów
wartości zmiennej typu int oraz wartość tej
zmiennej.
Wyjaśnienia wymaga konstrukcja #X .
Tworzy ona napis z identyikatora, który
Czym są pliki CsV?
Pliki CSV (Comma Separated Values) są
plikami tekstowymi, w których dane są
oddzielone przecinkami. Przykładowa
zawartość pliku CSV znajduje się poniżej:
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16,
Nie ma ograniczeń, co do liczby danych
w wierszu lub liczby wierszy w pliku.
Należy pamiętać o zachowaniu w każdym
wierszu takiej samej liczby danych oraz
unikać pustych wierszy. Pliki CSV można
wygenerować w popularnych arkuszach
kalkulacyjnych np. OpenOfice Calc lub
Microsoft Excel.
Praktyczne wykorzystanie makr
W praktyce programowania często za-
chodzi potrzeba utworzenia zbiorów tzw.
logów działania programu. Osobiście czę-
sto stosuję logi zapisywane w pamięci typu
Flash lub wysyłam je przez RS232. Zapisa-
nie logów w pamięci typu Flash jest moim
zdaniem dobrą praktyką, ponieważ w chwili
awarii mogę sprawdzić, co działo się w pro-
gramie i czy była to wina użytkownika, czy
R
E
K
L
A
M
A
ELEKTRONIKA PRAKTYCZNA 2/2010
79
686084320.008.png 686084320.009.png 686084320.001.png 686084320.002.png 686084320.003.png
notatnik konstruktora
stoi przy symbolu # . Czyli z identyikatora
zmienna utworzy „zmienna”. Dzięki temu
oszczędzamy swoje palce i nie musimy uży-
wać dłuższej formy wywołania np. log_in-
t(„zmienna”,zmienna);.
Załóżmy, że chcemy stosować logi tyl-
ko na etapie testów urządzenia. W takim
przypadku możemy wykorzystać preproce-
sor aby dołączał funkcje logujące tylko gdy
zdeiniujemy stałą DEBUG (lub dowolną
inną). Kod programu może wyglądać nastę-
pująco:
#include <stdio.h>
#deine DEBUG
#ifdef DEBUG
#deine log() printf(«%s:%d\n»,__
FILE__,__LINE__)
#deine log_int(X) printf(«%s:%d
%s=%d\n»,__FILE__,__LINE__,#X,X)
#else
#deine log()
#deine log_int(X)
#endif
int main()
{
int zmienna=123;
log();
log_int(zmienna);
return 0;
}
Tab. 1.
int a[3];
void printt() {
int indeks;
for(indeks=0; indeks<=2;
indeks++){
printf(„%d\n”,a[indeks]);
}
}
int main() {
a[0] = 1;
a[1] = 2;
a[2] = 3;
printf(„%s\n”,”Witaj”);
printt();
return 0;
}
LET(a[3])
SUB(printt)
LET(indeks)
LOOP(indeks,0,2)
PRINTI(a[indeks])
ENDLOOP
ENDSUB
BEGIN
a[0] = 1;
a[1] = 2;
a[2] = 3;
PRINT(„Witaj”)
CALL(printt)
END
Jest to użyteczna praktyka, ponieważ
usuwając deklaracje stałej DEBUG automa-
tycznie usuwamy wszystkie funkcje logujące
z kodu programu. Proszę sobie wyobrazić,
że w dużym programie możemy mieć nawet
kilkaset miejsc, w których używamy logo-
wania. Zmieniając tylko jedną linię kodu
możemy włączyć lub wyłączyć logowanie
w całym programie. Jest to bardzo wygodne
dla programisty. Zamiast deklarować stałą
w programie możemy również deklarować
makra jako opcje w wywołaniu kompilato-
ra gcc np. gcc –DDEBUG program.c . Jest to
równoważne używaniu dyrektywy #deine
w programie.
Preprocesor bardzo często jest używany
w bibliotekach do tworzenia w jednym pliku
różnych wersji programu np. dla kompilato-
rów różnych producentów, które nie są ze
sobą kompatybilne. Spójrzmy na poniższy
program źródłowy:
#if __GNUC__
__attribute__((__always_inline__))
#endif
static __inline__ int usart_mode_
is_multidrop(volatile avr32_usart_t
*usart)
{
return ((usart->mr >> AVR32_USART_
MR_PAR_OFFSET) & AVR32_USART_MR_PAR_
MULTI) == AVR32_USART_MR_PAR_MULTI;
}
#include «basic.h»
// deklaracja tablicy
LET(a[3])
// procedura printt wyswietla
wszystkie elementy tablicy «a»
SUB(printt)
// deklaracja zmiennej o nazwie
indeks
LET(indeks)
// petla od 0..2
LOOP(indeks,0,2)
PRINTI(a[indeks]);
ENDLOOP
ENDSUB
// start programu
BEGIN
// ustawienie elementow tablicy
a[0] = 1;
a[1] = 2;
a[2] = 3;
// wyswietlenie napisu powitalnego
PRINT(«Witaj»)
// wywolanie procedury printt
CALL(printt)
END
przykładu jeśli wywołamy LET(zmienna)
to zostanie utworzony kod int zmienna;
a nie int X; .
Dla lepszego zrozumienia porównajmy
program przetworzony przez preprocesor
z wersją oryginalną (polecenie gcc –E li-
sting1.c ) – tab. 1 .
Zapewne wielu z was zastanawia się
do czego taka funkcjonalność przydaje
się w praktyce? Jak się okazuje, istnieją
sytuacje, w których się przydaje. W stycz-
niowym numerze EP pojawił się artykuł
na temat maszyny stanów skończonych.
W artykule tym zaprezentowałem biblio-
tekę, która w całości była napisana w pre-
procesorze języka C. Interfejs tej biblioteki
to nic innego jak specyficzny język pro-
gramowania przeznaczony do tworzenia
maszyny stanów. Tego typu rozwiązania
są spotykane szczególnie przy programo-
waniu małych mikroprocesorów, które po-
winny posiadać implementacje statyczną
pewnych funkcji. Dzięki temu wydajność
oraz zużycie pamięci są optymalne przy
zachowaniu odpowiedniego stopnia abs-
trakcji naszego programu.
Zastanówmy się czy ten program będzie
poprawnie zinterpretowany przez kompila-
tor języka C. Od razu nasuwa się odpowiedź,
że to nie jest poprawny kod. Jednak wszyst-
ko zależy od tego, co znajduje się w pliku
basic.h . Poniżej znajduje się zawartość tego
pliku:
#include <stdio.h>
// blok programu wykonywany na
poczatku
// startu programu
#deine BEGIN int main() {
#deine END return 0; }
// instrukcje do wyswietlenia danych
na ekranie
#deine PRINT(X) printf(«%s\n»,X);
#deine PRINTI(X) printf(«%d\n»,X);
// deklaracja zmiennej calkowitej
#deine LET(X) int X;
// deklaracja procedury
#deine SUB(X) void X() {
#deine ENDSUB }
// wywolanie procedury
#deine CALL(X) X();
// deklaracja petli
#deine LOOP(I,X,Y) for(I=X; I<=Y; I++)
{
#deine ENDLOOP }
Jak pamiętamy #deine służy do dei-
niowania makr. W tym programie zostało
to wykorzystane do zdeiniowania kawał-
ków kodu. Przeanalizujmy dla przykładu
pierwszą deklarację. BEGIN będzie odpo-
wiednikiem kodu int main() { . Oznacza
to, że gdy napiszemy w swoim programie
słowo BEGIN, to tak jak byśmy napisali ten
kawałek kodu. Pisząc BEGIN informujemy
preprocesor, żeby wziął fragment kodu
i wstawił go w miejscu użycia.
Ponieważ makra mają możliwość prze-
kazywania parametrów, więc możemy
przekazać np. nazwę zmiennej i zmody-
fikować w ten sposób kod programu. Dla
Podsumowanie
Preprocesor jest doskonałym narzę-
dziem, jednak trzeba go używać z rozwagą
i  ostrożnością. Nieumiejętne stosowanie
może doprowadzić do błędów oraz zaciem-
nienia kodu i bardzo trudnej interpretacji
przez innych programistów. Preprocesor
służy do wykonania pewnych zadań, któ-
re nie są możliwe w języku C i właśnie do
tego należy go używać. Pomimo różnych
„pułapek”, które mogą nas spotkać, pole-
cam wszystkim używanie preprocesora,
ponieważ przynosi to wymierne korzyści
oraz skraca czas potrzebny na stworzenie
programu.
Na koniec zachęcam do zapoznanie się
z małą biblioteką dla preprocesora napi-
saną przez irmę Atmel. Znajduje się ona
w AVR32 Software Framework (można po-
brać ze strony http://atmel.com/avr32 ).
tomasz orłowski
tomek@toan.pl
Przykład zaczerpnięto z  biblioteki dla
procesora AVR32. Jak widać preprocesor zo-
stał użyty do sprawdzenia czy mamy do czy-
nienia z kompilatorem GNU GCC. Jeśli tak
jest faktycznie, to zostaną dodane atrybuty
wymagane przez ten kompilator dla funkcji
typu inline.
BASIC w języku C
Preprocesor umożliwia stworzenie mini
języka programowania. Oznacza to, że w pre-
procesorze można stworzyć język z zupełnie
inną składnią niż język C. Spójrzmy na po-
niższy listing:
80
ELEKTRONIKA PRAKTYCZNA 2/2010
686084320.004.png 686084320.005.png
Zgłoś jeśli naruszono regulamin