Preprocesor je součást překladače, která zpracovává zdrojový kód před tím, než je dále zpracován. Na zdrojovém kódu provádí textové operace (mazání, vkládání a nahrazování textu). Tyto operace jsou prováděny s následujícími cíly
Překladače umožňují prohlédnout si kód poté, co je zpracován preprocesorem. Pro překladač gcc toho lze dosáhnout pomocí
gcc -E -P source.c
Je důležité si uvědomit, že preprocesor je součástí překladu. Nemůže proto například znát hodnoty proměnných, ty jsou známy až za běhu překladu. Provádí jenom textové substice.
Části kódu určené pro preprocesor, jsou uvozené znakem
#
. Ty, kterým preprocesor rozumí říkáme direktivy
preprocesoru.
Makra definujeme pomocí #define
. Lze také oddefinovat
pomocí #undef macro_name
.
Makro definujeme následovně
#define name replacement
Preprocesor v kódu nahradí text name
textem
replacement
. Nenahrazuje výskyty name
, které
jsou součástí identifikátorů, klíčových slov nebo se vyskytují v
řetězcových literálech.
Například kód
#define PI 3.14
#define ERROR printf("Chyba")
double circle_area(double r)
{
if (r > 0) { return 2 * PI * r; }
;
ERRORreturn -1;
}
předělá preprocesor na následující.
double circle_area(double r)
{
if (r > 0) { return 2 * 3.14 * r; }
("Chyba");
printfreturn -1;
}
Symbol lze definovat i bez replacement
části. Říkáme tím
pouze, že symbol je definován (o použití více později).
V případě, že replacement
chceme napsat na více řádků,
lze řádek zlomit pomocí znaku \
(zpětné lomítko), přitom je
to bráno jako jeden řádek.
#define HELLO printf("%s %s", \
"hello", \
"world")
Podle standardu existují některá předdefinovaná makra, například
__LINE__
, expanduje na číslo řádku, na kterém se makro
nachází (celočíselný literál)__FILE__
, expanduje na jméno souboru (řetězcový
literál)__DATE__
, expanduje na aktuální datum (řetězcový
literál)__TIME__
, expanduje na aktuální čas (řetězcový
literál)Další makra může definovat konkrétní překladač. Například seznam
maker definovaných překladačem gcc
lze vytisknout pomocí
následujího řádku pro shell.
echo "" | gcc -dM -E -
#define name(parameter-list) replacement
parameter-list
je čárkami oddělený seznam jmen
parametrů. Při expanzi jsou jména parametrů vyskytující se v
replacement
(s jednou výjimkou) nahrazena argumenty
makra.
// makro pro druhou mocninu
#define square(x) x*x
// "volani" makra
(value)
square// vede k expanzi na
*value value
Při psaní maker je nutno dávat si pozor na mnohá nebezpečí, například
následující použití makra square
vede k nečekanému
výsledku.
(x+1)
square// expanduje na
+1*x+1
x// a to je něco jiného, než očekáváme: hodnota výrazu je 2x + 1, nikoliv (x+1)*(x+1)
Při psaní maker je tak výhodné využívat uzávorkování v maximální míře: závorky okolo jmen argumentů a závorky okolo celého výrazu.
#define square(x) ((x)*(x))
Problémem může být také situace, kdy je parametr makra použit v jeho těle vícekrát, a příslušný argument je výraz s vedlejším efektem.
#define square(x) ((x)*(x))
int foo(int x)
{
("launching %i misiles\n", x);
printfreturn x;
}
// výraz
int y = square(foo(3));
// expanduje na
int y = ((foo(3))*(foo(3)));
// a vedlejší efekt tak nastane dvakrát
Řešením je psát makra tak, aby se v jejich těle parametry neopakovaly, nebo jim nepředávat jako argumenty výrazy s vedlejším efektem.
int z = foo(3); // vedlejší efekt nastane pouze jednou
int y = square(z);
Výraz #par
v těle makra, kde par
je jméno
parametru, expanduje na řetězcový literál, jehož obsahem je příslušný
argument.
#define print_value(val) printf("value of " #par " is %f", val)
int main()
{
double x = 3;
double y = 4;
(x + y);
print_valuereturn 0;
}
// expanduje na
int main()
{
double x = 3;
double y = 4;
("value of x + y is %f", x + y);
printfreturn 0;
}
Výraz a ## b
expanduje na ab
. Toto lze
využít k vytváření nových identifikátorů spojováním slov.
#define VAR(x) variable_ ## x
(10) // expanduje na variable_10
VAR(f) // expanduje na variable_f VAR
S výhodou to lze použít pro automatické vytváření specializovaných verzí generických funkcí (viz kapitola 3)
// funkce prohodí n bajtů na adresách a,b
void mem_swap(void *a, void *b, size_t n);
//specializacni makro
#define define_swap(type) void swap_ ## type (type *a, type *b) {\
mem_swap(a, b, sizeof(type)); \
}
(double)
define_swap// expanduje na
void swap_double(double *a, double *b) {
(a, b, sizeof(double));
mem_swap}
Krátké shrnutí procesu expanze maker
##
.Makra s parametry by se měla zvenku chovat jako funkce
Makra jsou v zásadě dvojího typu (bloková a nebloková)
Problémy: makro potrebuje sezrat stredník.
#if constant_expression
// code
#endif
constant_expression
je výraz obsahující literály,
logické operátory a operátory porovnání, operátor defined
a
případně již definovaná makra, která se na ně expandují. Pokud je
hodnota constant_expression
nenulová, je část mezi
#if
a #endif
v kódu ponechána, jinak je
vypuštěna. Výraz lze doplnit o #else
a #elif
s
obvyklým významem.
Lze použít se speciálním výrazem defined(symbol)
, který
je nenulový, pokud je symbol
již definované makro. Toto lze
využít k zařazení či vynechání kódu určeného pro specifické situace
(debug/release build atd.) nebo platformy. Definovat jednoduchá makra
lze totiž i přímo pomocí přepínačů překladače, například pro gcc
// definuje makro Symbol
gcc -dSymbol
// definuje makro Symbol na literál 10
gcc -DSymbol=10
Příklad použití
#if defined(WIN)
// kód specifický pro windows
#elif defined(UNIX)
// kód specifický pro unix
#else
// kód pro ostatní platformy, případně vyvolání chyby
// viz níže
#endif
Výše používáme operátor defined
, který je pravdivý,
pokud je jeho argument již definované makro.
// #if defined(SYMB) lze nahradit #ifdef SYMB
// #if !defined(SYMB) lze nahradit #ifndef SYMB
S podmíněným překladem můžeme výhodně použít dalších direktiv preprocesoru
#error message
při překladu se vytiskne
message
a je vyvolána chyba překladu#warning message
při překladu je vypsáno
message
jako warningNapříklad v části #else
předchozího příkladu bychom
mohli dát
#error Won't compile on unsupported platforms. We support only windows and unix.
Direktiva #include
slouží k vložení obsahu jiného
souboru do zdrojového kódu. Soubor, který se má vložit, lze specifikovat
dvojím způsobem
#include <path-to-file>
#include "path-to-file"
V prvním případě je soubor hledán v předdefinovaných adresářích (ty
jsou dány nastavením systému, případně je lze předat jako argumenty
překladači) a path-to-file
je brána jako relativní cesta z
nějakého takového adresáře. Druhém případě se nejdříve prohledává
adresář, ve kterém je uložen aktuální soubor (obsahující
#include
). (Někdy se poté vyhledávají předdefinované
adresáře.
Obvykle je nutno zabránit tomu, aby byl obsah nějakého souboru vložen
vícekrát. (To hrozí například pokud vkládáme dva různé soubory, které
samy obsahují directivu #include
, která vkládá stejný
soubor.) K tomu lze výhodně využít podmíněného překladu (předpokládejme
například, že jde o soubor stdio.h
)
#if !defined(__STDIO_H__)
#define __STDIO_H__
// kód
#endif
U většiny překladačů také funguje vložení následující direktivy.
#pragma once
Mimo výše zmiňované direktivy existují i další. Například direktiva
#pragma
, jejíž syntax i sémantika je závislá na konkrétním
překladači.