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ł:
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żeniakwadrat(a+b)
będzie różna od kwadratu wyrażeniaa+b
.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 kodziewynik = suma(3, 4) * 5
. Makro zostanie rozwinięte jakowynik = (3)+(4)*5
, co — z powodu kolejności działań — da wynik inny niż pożądany.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)
- 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 inkrementacjikwadrat(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).- 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
-
Napisz makrodefinicję, która ma 3 parametry i jest rozwijana do ich sumy.
-
Napisz makrodefinicję
MAX(a,b)
, która będzie rozwijana do wyrażenia o wartości większego z podanych argumentów. -
Napisz makrodefinicję o dwóch parametrach:
x
in
, która będzie rozwijana przez preprocesor do nagłówka pętlifor
, w której zmiennax
przebiega wartości od 0 do n-1 co 1. -
Napisz jednoparametrową makrodefinicję, której wartością jest 1, jeżeli argumentem jest liczba parzysta i 0, jeżeli argument jest nieparzysty.
-
Napisz makrodefinicję
READ(t, i)
, które przy użyciu funkcjiscanf
wczytuje do zmienneji
wartość zgodnie ze specyfikatoremt
. -
Napisz makrodefinicję
SWAP_INT(a,b)
, którego wykonanie dla argumentów typuint
spowoduje wymianę wartości międzya
ib
. -
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
-
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.