poprzedni temat | w górę | następny temat

Preprocesor

Kod źródłowy tuż przed właściwą kompilacją, jest przetwarzany przez preprocesor.

Do tej pory wykorzystywaliśmy preproceror pisząc dyrektywę np. #include <stdio.h>. Jej działanie polega na dołączeniu zawartości pliku stdio.h do aktualnie kompilowanego pliku.

Aby zobaczyć wynik działania preprocera, użyj opcji -E:

gcc program.c -E -o plik_posredni.c
# lub:
gcc program.c -E -o - | less

Makrodefinicje

Dyrektywa #define służy do definiowania makrodefinicji. Przykład:

#define PI 3.141592653589
#define KWADRAT(x) x*x

Definicja KWADRAT jest wadliwa — dlaczego? Rozważ, jak preprocer rozwinie użycie makrodefinicji w następujący sposób: KWADRAT(x+1).

Rozwiązaniem jest użycie nawiasów, którymi powinny być otoczone parametry oraz całe wyrażenie.

Przykład:

#define MAX(a,b) a>b?a:b
MAX(x?1:0, y?0:1) // wynik: x?1:0>y?0:1?x?1:0:y?0:1
// druga próba:
#define MAX(a,b) (a)>(b)?(a):(b)
// trzecia próba:
#define MAX(a,b) ((a)>(b)?(a):(b))

Wniosek: w makrodefinicjach zawsze warto otoczyć każdy parametr oraz całe wyrażenie nawiasami.

Makrodefinicje instrukcji

Jeśli makrodefinicja definiuje instrukcję, możemy napotkać kłopoty jak w przykładzie poniżej:

#define PRINT_INT_IF_EVEN(n) if(n%2==0) printf("%d", n)
int number = 1;
// pierwsze użycie — w porządku:
PRINT_INT_IF_EVEN(number);
// inny przykład użycia:
if(n>10)
    PRINT_INT_IF_EVEN(n);
else
    printf("Liczba nie jest większa od 10.\n");

Co w powyższym przykładzie powoduje błąd?

Rozwiązaniem, które eliminuje tego typu błędy — które powinniśmy stosować zawsze przy definiowaniu makr rozwijanych do instrukcji jest "sztuczne" użycie pętli do-while:

#define PRINT_INT_IF_EVEN(n) do{ if(n%2==0) printf("%d", n); }while(0)

Konwencje pisania makr

Poniższy akapit pochodzi z wikiksiążki C.

Ponieważ makra preprocesora działają na zasadzie zwykłego zastępowania napisów, są podatne na wiele kłopotliwych błędów, z których części można uniknąć przez stosowanie się do poniższych reguł:

  1. Umieszczaj nawiasy dookoła parametrów makra kiedy to tylko możliwe. Zapewnia to, że gdy są wyrażeniami kolejność działań nie zostanie zmieniona. Na przykład:

    • Źle: #define kwadrat(x) (x*x)
    • Dobrze: #define kwadrat(x) ((x)*(x))
    • Przykład: Załóżmy, że w programie makro kwadrat() zdefiniowane bez nawiasów zostało wywołane następująco: kwadrat(a+b). Wtedy zostanie ono zamienione przez preprocesor na: (a+b*a+b). Z kolejności działań wiemy, że najpierw zostanie wykonane mnożenie, więc wartość wyrażenia kwadrat(a+b) będzie różna od kwadratu wyrażenia a+b.
  2. Umieszczaj nawiasy dookoła całego makra, jeśli jest pojedynczym wyrażeniem. Ponownie, chroni to przed zaburzeniem kolejności działań.

    • Źle: #define kwadrat(x) (x)*(x)
    • Dobrze: #define kwadrat(x) ((x)*(x))
    • Przykład: Definiujemy makro #define suma(a, b) (a)+(b) i wywołujemy je w kodzie wynik = suma(3, 4) * 5. Makro zostanie rozwinięte jako wynik = (3)+(4)*5, co — z powodu kolejności działań — da wynik inny niż pożądany.
  3. Jeśli makro składa się z wielu instrukcji lub deklaruje zmienne, powinno być umieszczone w pętli do { ... } while(0), bez kończącego średnika. Pozwala to na użycie makra jak pojedynczej instrukcji w każdym miejscu, jak ciało innego wyrażenia, pozwalając jednocześnie na umieszczenie średnika po makrze bez tworzenia zerowego wyrażenia. Należy uważać, by zmienne w makrze potencjalnie nie kolidowały z argumentami makra.

    • Źle: #define FREE(p) free(p); p = NULL;
    • Dobrze: #define FREE(p) do { free(p); p = NULL; } while(0)
  4. Unikaj używania parametrów makra więcej niż raz wewnątrz makra. Może to spowodować kłopoty, gdy argument makra ma efekty uboczne (np. zawiera operator inkrementacji).
    • Przykład: #define kwadrat(x) ((x)*(x)) nie powinno być wywoływane z operatorem inkrementacji kwadrat(a++) ponieważ zostanie to rozwinięte jako ((a++) * (a++)), co jest niezgodne ze specyfikacją języka i zachowanie takiego wyrażenia jest niezdefiniowane (dwukrotna inkrementacja w tym samym wyrażeniu).
  5. Jeśli makro może być w przyszłości zastąpione przez funkcję, rozważ użycie w nazwie małych liter, jak w funkcji.

Pliki nagłówkowe

Dyrektywa #include dołącza w miejscu użycia zawartość pliku, którego nazwa po niej występuje. Jeśli plik znajduje się w standardowym dla danego kompilatora katalogu dla plików nagłówkowych, nazwę pliku otacza się nawiasami trójkątnymi, np. #include <stdio.h>. Jeśli jednak ma być dołączony "własny", niestandardowy plik, np. który stanowi część przez nas pisanego kodu źródłowego, nazwę otaczamy podwójnymi cudzysłowami: #include "moj_plik.h".

Aby zawartość pliku nagłówkowego pojawiała się tylko raz, należy zastosować następującą konstrukcję:

#ifndef __KWADRAT__H__
#define __KWADRAT__H__
// tutaj cała zawartość pliku *.h
// np. nagłówki funkcji
#endif // __KWADRAT__H__

#

Znak # postawiony przed parametrem makrodefinicji powoduje, że w momencie rozwijania makra argument jest zamieniany na napis — otaczany cudzysłowami.

#define DEBUG_int(x) do{ fprintf(stderr, #x " == %d\n", (x)); }while(0)

int a = 23;
DEBUG_int(a+1);
// zostanie rozwinięte do:
do{ fprintf(stderr, "a+1" " == %d\n", (a+1)); }while(0);

Przy okazji zauważmy, że kompilator skleja ze sobą literały napisowe występujące bezpośrednio obok siebie w jeden napis.

Zadania

  1. Napisz makrodefinicję, która ma 3 parametry i jest rozwijana do ich sumy.

  2. Napisz makrodefinicję MAX(a,b), która będzie rozwijana do wyrażenia o wartości większego z podanych argumentów.

  3. Napisz makrodefinicję o dwóch parametrach: xn, która będzie rozwijana przez preprocesor do nagłówka pętli for, w której zmienna x przebiega wartości od 0 do n-1 co 1.

  4. Napisz jednoparametrową makrodefinicję, której wartością jest 1, jeżeli argumentem jest liczba parzysta i 0, jeżeli argument jest nieparzysty.

  5. Napisz makrodefinicję READ(t, i), które przy użyciu funkcji scanf wczytuje do zmiennej i wartość zgodnie ze specyfikatorem t.

  6. Napisz makrodefinicję SWAP_INT(a,b), którego wykonanie dla argumentów typu int spowoduje wymianę wartości między ab.

  7. Napisz program, który składa się z dwóch plików źródłowych oraz pliku nagłówkowego. Jeden z plików powinien zawierać definicję funkcji int fib(int n), która oblicza n-ty wyraz ciągu Fibonacciego. Plik nagłówkowy powinien zawierać nagłówek powyższej funkcji. Drugi z plików źródłowych powinien zawierać funkcję main, w której można będzie przetestować funkcję fib.

    Skompiluj program następująco:

    gcc -c fib.c
    gcc -c main.c
    gcc fib.o main.o -o program
    # lub:
    gcc fib.c main.c -o program
    
  8. Napisz makrodefinicję, która będzie przyjmowała jako parametr wyrażenie typu int, a jej użycie spowoduje wypisanie na standardowym wyjściu tego wyrażenia (literalnie) oraz jego wartości.

poprzedni temat | w górę | następny temat