Wstęp

edytuj

Preprocesor jest to program, który analizuje plik źródłowy (programu, biblioteki) w poszukiwaniu wszystkich wyrażeń zaczynających się od "#". Na podstawie tych instrukcji generuje on kod w "czystym" języku C, który następnie jest kompilowany przez kompilator. Ponieważ za pomocą preprocesora można niemal "sterować" kompilatorem, daje on niezwykłe możliwości, które nie były dotąd znane w innych językach programowania.

W języku C wszystkie linijki zaczynające się od symbolu "#" są instrukcjami ( dyrektywami) dla preprocesora. Nie są elementami języka C i nie podlegają bezpośrednio procesowi kompilacji.


Preprocesor może być:

  • standardowy = element kompilatora
  • niestandardowy [1]:
    • CPIP napisany w Pythonie
    • GNU M4
    • Cog
    • Wave parser (Boost)
    • własny program napisany w:

Przełącznik kompilatora

edytuj

Aby przekonać się, jak wygląda kod przetworzony przez preprocesor, użyj (w kompilatorze gcc) przełącznika "-E":

gcc test.c -E -o test.txt

W pliku test.txt zostanie umieszczony cały kod w postaci, która zdatna jest do przetworzenia przez kompilator.

Dyrektywy preprocesora

edytuj

Dyrektywy preprocesora są to wyrażenia, które zapoczątkowane są symbolem "#" i to właśnie za ich pomocą możemy używać preprocesora. Dyrektywa zaczyna się od znaku # i kończy się wraz z końcem linii. Aby przenieść dalszą część dyrektywy do następnej linii, należy zakończyć linię znakiem "\":

#define ADD(a,b) \
  a+b

Omówimy teraz kilka ważniejszych dyrektyw.

#include

edytuj

Najpopularniejsza dyrektywa, wstawiająca w swoje miejsce treść pliku podanego w nawiasach ostrych lub cudzysłowie. Składnia:

Przykład 1 [2]

#include <plik_nagłówkowy_do_dołączenia>

Przykład 2

#include "plik_nagłówkowy_do_dołączenia"

Jeżeli nazwa pliku nagłówkowego będzie ujęta w nawiasy ostre (przykład 1), to kompilator poszuka go wśród własnych plików nagłówkowych (które najczęściej się znajdują w podkatalogu "includes" w katalogu kompilatora). Jeśli jednak nazwa ta będzie ujęta w podwójne cudzysłowy(przykład 2), to kompilator poszuka jej w katalogu, w którym znajduje się kompilowany plik (można zmienić to zachowanie w opcjach niektórych kompilatorów). Przy użyciu tej dyrektywy można także wskazać dokładne położenie plików nagłówkowych poprzez wpisanie bezwzględnej lub względnej ścieżki dostępu do tego pliku nagłówkowego.

Przykład 3 - ścieżka bezwzględna do pliku nagłówkowego w Linuksie i w Windowsie
Opis: W miejsce jednej i drugiej linijki zostanie wczytany plik umieszczony w danej lokalizacji

#include "/usr/include/plik_nagłówkowy.h"
#include "C:\\borland\includes\plik_nagłówkowy.h"


Przykład 4 - ścieżka względna do pliku nagłówkowego
Opis: W miejsce linijki zostanie wczytany plik umieszczony w katalogu "katalog1", a ten katalog jest w katalogu z plikiem źródłowym. Inaczej mówiąc, jeśli plik źródłowy jest w katalogu "/home/user/dokumenty/zrodla", to plik nagłówkowy jest umieszczony w katalogu "/home/user/dokumenty/zrodla/katalog1"

#include "katalog1/plik_naglowkowy.h"


Przykład 5 - ścieżka względna do pliku nagłówkowego
Opis: Jeśli plik źródłowy jest umieszczony w katalogu "/home/user/dokumenty/zrodla", to plik nagłówkowy znajduje się w katalogu "/home/user/dokumenty/katalog1/katalog2/"

#include "../katalog1/katalog2/plik_naglowkowy.h"

Więcej informacji możesz uzyskać w rozdziale Biblioteki.

#define

edytuj

Linia pozwalająca zdefiniować:

  • stałą,
  • funkcję
  • słowo kluczowe,
  • makro

które będzie potem podmienione w kodzie programu na odpowiednią wartość lub może zostać użyte w instrukcjach warunkowych dla preprocesora.

Składnia:

#define NAZWA_STALEJ WARTOSC

lub

#define NAZWA_STALEJ

Przykład:
#define LICZBA 8 - spowoduje, że każde wystąpienie słowa LICZBA w kodzie zostanie zastąpione ósemką.
#define SUMA(a,b) ((a)+(b)) - spowoduje, że każde wystąpienie wywołania "funkcji" SUMA zostanie zastąpione przez sumę argumentów

Jeśli w miejscu wartości znajduje się wyrażenie, to należy je umieścić w nawiasach.

#define A  5
#define B  ((2)+(A))

Unikniemy w ten sposób niespodzianek związanych z priorytetem operatorów :

/* 

https://github.com/Keith-S-Thompson/42/blob/master/42.c
This small C program demonstrates the danger of improper use of the C preprocessor.
Keith-S-Thompson


*/ 
#include <stdio.h>

#define SIX 1+5
#define NINE 8+1

int main(void)
{
    printf("%d * %d = %d\n", SIX, NINE, SIX * NINE);
    return 0;

}

Po skompilowaniu i uruchomieniu programu otrzymujemy:

6 * 9 = 42

a powinno być:

6 * 9 = 54

Przyczyną błędu jest interpretacja wyrażenia:

1+5*8+1

Ze względu na brak nawiasów i priorytet operatorów (wyższy * niż +) jest to interpretowane jako:

1+(5*8)+1 

a nie jak:

(1+5)*(8+1)

redefine

edytuj

C nie obsługuje żadnych dodatkowych dyrektyw służących do przedefiniowania istniejącego makra.[3]

To samo makro można zdefiniować dowolną ilość razy. Jednakże. spowoduje to zapełnienie stosu ostrzeżeń kompilatora. Dlatego zawsze zaleca się najpierw oddefiniowanie istniejącego makra, a następnie zdefiniowanie go ponownie.

// Define a macro
#define PI 3.14

// Undefine before redefining
#ifdef PI
#undef PI
#endif

// Redefine PI
#define PI 3.14159

#undef

edytuj

Ta instrukcja odwołuje definicję wykonaną instrukcją #define.

#undef STALA

instrukcje warunkowe

edytuj

Preprocesor zawiera również instrukcje warunkowe, pozwalające na wybór tego co ma zostać skompilowane w zależności od tego, czy stała jest zdefiniowana lub jaką ma wartość:

#if #elif #else #endif

edytuj

Te instrukcje uzależniają kompilacje od warunków. Ich działanie jest podobne do instrukcji warunkowych w samym języku C. I tak:

#if
wprowadza warunek, który jeśli nie jest prawdziwy powoduje pominięcie kompilowania kodu, aż do napotkania jednej z poniższych instrukcji.
#else
spowoduje skompilowanie kodu jeżeli warunek za #if jest nieprawdziwy, aż do napotkania któregoś z poniższych instrukcji.
#elif
wprowadza nowy warunek, który będzie sprawdzony jeżeli poprzedni był nieprawdziwy. Stanowi połączenie instrukcji #if i #else.
#endif
zamyka blok ostatniej instrukcji warunkowej.

Przykład:

#if INSTRUKCJE == 2
  printf ("Podaj liczbę z przedziału 10 do 0\n"); /*1*/
#elif INSTRUKCJE == 1
  printf ("Podaj liczbę: "); /*2*/
#else
  printf ("Podaj parametr: "); /*3*/
#endif
scanf ("%d", &liczba); /*4*/
  • wiersz nr 1 zostanie skompilowany jeżeli stała INSTRUKCJE będzie równa 2
  • wiersz nr 2 zostanie skompilowany, gdy INSTRUKCJE będzie równa 1
  • wiersz nr 3 zostanie skompilowany w pozostałych wypadkach
  • wiersz nr 4 będzie kompilowany zawsze

#ifdef #ifndef #else #endif

edytuj

Te instrukcje warunkują kompilację od tego, czy odpowiednia stała została zdefiniowana.

#ifdef
spowoduje, że kompilator skompiluje poniższy kod tylko gdy została zdefiniowana odpowiednia stała.
#ifndef
ma odwrotne działanie do #ifdef, a mianowicie brak definicji odpowiedniej stałej umożliwia kompilacje poniższego kodu.
#else,#endif
mają identyczne zastosowanie jak te z powyższej grupy

Przykład:

 #define INFO /*definicja stałej INFO*/
 #ifdef INFO
   printf ("Twórcą tego programu jest Jan Kowalski\n");/*1*/
 #endif
 #ifndef INFO
   printf ("Twórcą tego programu jest znany programista\n");/*2*/
 #endif

To czy dowiemy się kto jest twórcą tego programu zależy czy instrukcja definiująca stałą INFO będzie istnieć. W powyższym przypadku na ekranie powinno się wyświetlić

Twórcą tego programu jest Jan Kowalski

#error

edytuj

Powoduje przerwanie kompilacji i wyświetlenie tekstu, który znajduje się za tą instrukcją. Przydatne gdy chcemy zabezpieczyć się przed zdefiniowaniem nieodpowiednich stałych.

Przykład:

#ifdef BLAD
#error Poważny błąd kompilacji
#endif

Co jeżeli zdefiniujemy stałą BLAD, oczywiście przy pomocy dyrektywy #define? Spowoduje to wyświetlenie w trakcie kompilacji komunikatu podobnego do poniższego:

Fatal error program.c 6: Error directive: Poważny błąd kompilacji in function main()
*** 1 errors in Compile ***

wraz z przerwaniem kompilacji.

#warning

edytuj

Wyświetla tekst, jako ostrzeżenie. Jest często używany do sygnalizacji programiście, że dana część programu jest przestarzała lub może sprawiać problemy.

Przykład:

#warning To jest bardzo prosty program

Spowoduje to takie oto zachowanie kompilatora:

test.c:3:2: warning: #warning To jest bardzo prosty program

Użycie dyrektywy #warning nie przerywa procesu kompilacji i służy tylko do wyświetlania komunikatów dla programisty w czasie kompilacji programu.

Powoduje wyzerowanie licznika linii kompilatora, który jest używany przy wyświetlaniu opisu błędów kompilacji. Pozwala to na szybkie znalezienie możliwej przyczyny błędu w rozbudowanym programie.

Przykład:

printf ("Podaj wartość funkcji");
#line
printf ("W przedziale od 10 do 0\n); /* tutaj jest błąd - brak cudzysłowu zamykającego */

Jeżeli teraz nastąpi próba skompilowania tego kodu to kompilator poinformuje, że wystąpił błąd składni w linii 1, a nie np. 258.

#pragma

edytuj

Dyrektywa pragma (od angielskiego: pragmatic information) służy do określania i sterowania funkcjami specyficznymi dla kompilatora i docelowej maszyny wykonującej kod. Listę dostępnych dyrektyw #pragma można znaleźć w dokumentacji kompilatora. Służą one m. in. do wymuszania lokalizacji zmiennych i kodu funkcji w pamięci lub tworzenia dodatkowych wątków z użyciem OpenMP.

# oraz ##

edytuj

Dość ciekawe możliwości ma w makrach znak "#". Zamienia on stojący za nim identyfikator na napis.

#include <stdio.h>
#define wypisz(x) printf("%s=%i\n", #x, x)
  
int main()
{
  int i=1;
  char a=5;
  wypisz(i);
  wypisz(a);
  return 0;
}

Program wypisze:

i=1
a=5

Czyli wypisz(a) jest rozwijane w printf("%s=%i\n", "a", a).

Natomiast znaki "##" łączą dwie nazwy w jedną. Przykład:

#include <stdio.h>

#define abc(x) int x##_zmienna
#define wypisz(y) printf("%s=%i", #y, y)
int main()
{
  abc(nasza) = 2; // Robi dwie rzeczy:
                  // 1.	Wstawia słowo „nasza” przed słowem „ _zmienna”.
                  // Dzięki temu zadeklarujemy zmienną o nazwie "nasza_zmienna".
                  // 2.	Inicjalizuje „nasza_zmienna” wartością "2".
  wypisz(nasza_zmienna);
  return 0;
}

Program wypisze:

nasza_zmienna=2

Więcej o dobrych zwyczajach w tworzeniu makr można się dowiedzieć w rozdziale Powszechne praktyki.

Preprocesor języka C umożliwia też tworzenie makr[4][5], czyli automatycznie wykonywanych czynności.


Makra deklaruje się za pomocą dyrektywy #define:

#define MAKRO(arg1, arg2, ...) (wyrażenie) 
/* można również napisać: do {instrukcje} while(0) */
/* lub jeśli jest tylko jedna instrukcja można napisać: instrukcja (bez średnika!) */

W momencie wystąpienia MAKRA w tekście, preprocesor automatycznie zamieni makro na wyrażenie lub instrukcje. Makra mogą być pewnego rodzaju alternatywami dla funkcji, ale powinno się ich używać tylko w specjalnych przypadkach. Ponieważ makro sprowadza się do prostego zastąpienia przez preprocesor wywołania makra przez jego tekst, jest bardzo podatne na trudne do zlokalizowania błędy (kompilator będzie podawał błędy w miejscach, w których nic nie widzimy - bo preprocesor wstawił tam tekst). Makra są szybsze (nie następuje wywołanie funkcji, które zawsze zajmuje trochę czasu[6]), ale też mniej bezpieczne i elastyczne niż funkcje.

Przeanalizujmy teraz fragment kodu:

#include <stdio.h>
#define KWADRAT(x) ((x)*(x))

int main ()
{
  printf ("2 do kwadratu wynosi %d\n", KWADRAT(2));
  return 0;
}

Preprocesor w miejsce wyrażenia KWADRAT(2) wstawił ((2)*(2)). Zastanówmy się, co stałoby się, gdybyśmy napisali KWADRAT("2"). Preprocesor po prostu wstawi napis do kodu, co da wyrażenie (("2")*("2")), które jest nieprawidłowe. Kompilator zgłosi błąd, ale programista widzi tylko w kodzie użycie makra a nie prawdziwą przyczynę błędu. Widać tu, że bezpieczniejsze jest użycie funkcji, które dają możliwość wyspecyfikowania typów argumentów.

Nawet jeżeli program się skompiluje to makro może dawać nieoczekiwany wynik. Jest tak w przypadku poniższego kodu:

int x = 1;
int y = KWADRAT(++x);

Dzieje się tak dlatego, że makra rozwijane są przez preprocesor i kompilator widzi kod:

int x = 1;
int y = ((++x)*(++x));

Również poniższe makra są błędne[7] pomimo, że opisany problem w nich nie występuje:

#define SUMA(a, b) a + b
#define ILOCZYN(a, b) a * b

Dają one nieoczekiwane wyniki dla wywołań:

SUMA(2, 2) * 2; /* 6 zamiast 8 */
ILOCZYN(2 + 2, 2 + 2); /* 8 zamiast 16 */

Z tego powodu istotne jest użycie nawiasów:

#define SUMA(a, b) ((a) + (b))
#define ILOCZYN(a, b) ((a) * (b))



Predefiniowane makra

edytuj

W języku wprowadzono również serię predefiniowanych makr, które mają ułatwić życie programiście. Oto one:

  • __DATE__ - data w momencie kompilacji
  • __TIME__ - godzina w momencie kompilacji
  • __FILE__ - łańcuch, który zawiera nazwę pliku, który aktualnie jest kompilowany przez kompilator
  • __LINE__ - definiuje numer linijki
  • __STDC__ - w kompilatorach zgodnych ze standardem ANSI lub nowszym makro to przyjmuje wartość 1
  • __STDC_VERSION__ - zależnie od poziomu zgodności kompilatora makro przyjmuje różne wartości:
    • jeżeli kompilator jest zgodny z ANSI (rok 1989) makro nie jest zdefiniowane,
    • jeżeli kompilator jest zgodny ze standardem z 1994 makro ma wartość 199409L,
    • jeżeli kompilator jest zgodny ze standardem z 1999 makro ma wartość 199901L.

Warto również wspomnieć o identyfikatorze __func__ zdefiniowanym w standardzie C99, którego wartość to nazwa funkcji.

Spróbujmy użyć tych makr w praktyce:

#include <stdio.h>

#if __STDC_VERSION__ >= 199901L
/*Jezeli mamy do dyspozycji identyfikator __func__ wykorzystajmy go.*/
#define BUG(message) fprintf(stderr, "%s:%d: %s (w funkcji %s)\n", \
                                __FILE__, __LINE__, message, __func__)
#else
/*Jezeli __func__ nie ma, to go nie używamy*/
#define BUG(message) fprintf(stderr, "%s:%d: %s\n", \
                                __FILE__, __LINE__, message)
#endif
 
int main(void) {
  printf("Program ABC,  data kompilacji: %s %s\n", __DATE__, __TIME__);

  BUG("Przykladowy komunikat bledu");
  return 0;
}

Efekt działania programu, gdy kompilowany jest kompilatorem C99:

Program ABC,  data kompilacji: Sep  1 2008 19:12:13
test.c:17: Przykladowy komunikat bledu (w funkcji main)

Gdy kompilowany jest kompilatorem ANSI C:

Program ABC,  data kompilacji: Sep  1 2008 19:13:16
test.c:17: Przykladowy komunikat bledu



pozostałe makra

edytuj

Problemy

edytuj

Problemy[8]

  • "Połykanie" średnika
  • Błędne zagnieżdżanie
  • Problemy z pierwszeństwem operatorów
  • Duplikacja skutków ubocznych
  • Makra rekurencyjne
  • Pre-skanowanie argumentów
  • Znaki nowej linii w argumentach

Uwagi:

  • NIE umieszczaj znaku średnika na końcu instrukcji #define. To częsty błąd.

Cechy, czyli zalety i wady

edytuj

Kiedy używać makr:[9]

  • tylko wtedy, gdy nie masz wyboru i nie możesz użyć funkcji
  • kiedy musisz użyć ich wyniku jako stałej ( uwaga: w c++ lepiej uzyć constexpr )
  • kiedy nie chcesz sprawdzać typu
    • Gdy chcesz zdefiniować funkcję „ogólną” pracującą z kilkoma typami

Kiedy nie używać makr:

  • żeby przyspieszyć kod

Przykłady

edytuj
  • CLM_LIBS a collection of code-generating macros for the C preprocessor.



Przypisy