Preprocesor języka C

Preprocesor języka C
Pobierz PDF Download icon
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 potrafi preprocesor oraz jak wykorzystać jego potęgę w programowaniu.
78 ELEKTRONIKA PRAKTYCZNA 2/2010 notatnik konstruktora Dodatkowe materiały na CD i FTP Proces kompilacji składa się z  dwóch zasadniczych kroków: kompilacji oraz kon- solidacji. Jednak zanim kod programu trafi 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 #define. #include Instrukcja #include dołącza do kodu źródłowego kopię pliku podanego za tą in- strukcją np. #include . 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? }; 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. #define, #undef Instrukcja #define służy najczęściej do zdefiniowania stałej np. #define PI 3.14 Preprocesor języka C 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 potrafi preprocesor oraz jak wykorzystać jego potęgę w  programowaniu. Nie polecam jednak używania #define w  tego typu sytuacjach. Lepiej jest używać innych wyrażeń języka C, np. float const PI=3.14; Definiując liczby całkowite można rów- nież używać enum np. enum {N=10};. Dyrektywą odwrotną do #define jest #undef. Służy ona do cofnięcia definicji w miejscu użycia np. #define 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 #define są jednak dużo większe niż mogłoby się wydawać. Za pomo- cą tej instrukcji możemy definiować również makra. Przeanalizujmy najpierw makro, któ- re dodaje do siebie dwie liczby: #define ADD(X,Y)((X) + (Y)) int a; float 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; float 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 float. Stosując funkcje potrzeba- by napisać dwie implementacje: jedną dla typu float 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- wych pułapek jest wywołanie makra z in- strukcjami arytmetycznymi np.: #define 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. #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 zdefiniowane #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_ #define _PLIK_H_ // treść właściwa #endif Jeśli przez nieuwagę dołączymy ten plik dwa razy np.: #include ?plik.h? #include #include W takiej sytuacji nic złego się nie stanie. Jest to bardzo dobra praktyka i  większość Kody źródłowe Kody źródłowe prezentowane w artykule są dostępne na stronie: http://toan.pl 79ELEKTRONIKA PRAKTYCZNA 2/2010 Preprocesor języka C istniejących bibliotek używa tego zabezpie- czenia. Makra predefiniowane Preprocesor udostępnia nam kilka bar- dzo użytecznych makr predefiniowanych. 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. 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 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 #define log() printf(?%s:%d\n?,__ FILE__,__LINE__) #define log_int(X) printf(?%s:%d %s=%d\n?,__FILE__,__LINE__,#X,X) int main() {     int zmienna=123;     log();     log_int(zmienna);     return 0; } 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  identyfikatora, 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. OpenOffice Calc lub Microsoft Excel. R E K L A M A 80 ELEKTRONIKA PRAKTYCZNA 2/2010 notatnik konstruktora stoi przy symbolu #. Czyli z identyfikatora 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 zdefiniujemy stałą DEBUG (lub dowolną inną). Kod programu może wyglądać nastę- pująco: #include #define DEBUG #ifdef DEBUG #define log() printf(?%s:%d\n?,__ FILE__,__LINE__) #define log_int(X) printf(?%s:%d %s=%d\n?,__FILE__,__LINE__,#X,X) #else #define log() #define log_int(X) #endif int main() {     int zmienna=123;     log();     log_int(zmienna);     return 0; } 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 #define 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; } 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: 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 #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 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 // blok programu wykonywany na poczatku // startu programu #define BEGIN int main() { #define END   return 0; } // instrukcje do wyswietlenia danych na ekranie #define PRINT(X) printf(?%s\n?,X); #define PRINTI(X) printf(?%d\n?,X); // deklaracja zmiennej calkowitej #define LET(X) int X; // deklaracja procedury #define SUB(X) void X() { #define ENDSUB } // wywolanie procedury #define CALL(X) X(); // deklaracja petli #define LOOP(I,X,Y) for(I=X; I<=Y; I++) { #define ENDLOOP } Jak pamiętamy #define służy do defi- niowania makr. W  tym programie zostało to wykorzystane do zdefiniowania 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 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. 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 firmę Atmel. Znajduje się ona w AVR32 Software Framework (można po- brać ze strony http://atmel.com/avr32). Tomasz Orłowski tomek@toan.pl
Artykuł ukazał się w
Luty 2010
DO POBRANIA
Pobierz PDF Download icon

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik lipiec 2021

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio lipiec - sierpień 2021

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka Podzespoły Aplikacje lipiec 2021

Automatyka Podzespoły Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna lipiec 2021

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich sierpień 2021

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów