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 2020

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio lipiec 2020

Świat Radio

Magazyn użytkowników eteru

APA - Automatyka Podzespoły Aplikacje lipiec 2020

APA - Automatyka Podzespoły Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna lipiec 2020

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Praktyczny Kurs Elektroniki 2018

Praktyczny Kurs Elektroniki

24 pasjonujące projekty elektroniczne

Elektronika dla Wszystkich lipiec 2020

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów