Jste zde

Programování v jazyce C - 4. PREPROCESOR

Pokračování seriálu o výuce programování v jazyce C.

4.1. Definice maker

Symbolické konstanty
Makra

4.2. Standardní předdefinovaná makra

Operátory # a ##
Podmíněný překlad
Zbývající direktivy

Název kapitoly napovídá, že se budeme věnovat prostředku, který předchází překladač. Preprocesor zpracovává vstupní text jako text, provádí v něm textové změny a jeho výstupem je opět text. Od preprocesoru tedy nemůžeme čekat kontrolu syntaxe, natož pak typovou kontrolu. Preprocesor zpracovává hlavičkové soubory, rozvíjí makra, nepropouští komentáře a umožňuje provádět podmíněný překlad zdrojového textu.

Při spuštění překladu je nejprve proveden rozvoj maker, teprve výstup zpracovává překladač. Pokud chceme získat výstup, který preprocesor produkuje, můžeme jej zavolat samostatně příkazem cpp (od C Pre Processor). Preprocesor neprovádí rozvoj maker tam, kde nemohou být umístěny ani příkazy jazyka C (například v komentářích a řetězcích).

C preprocesor přijímá tyto direktivy:

#define #elif #else #endif
#error #if #ifdef #ifndef
#include #line #pragma #undef

Jednotlivé direktivy popíšeme v rámci následujících podkapitol.

Direktiva preprocesoru musí být vždy uvozena znakem #. # navíc musí být na řádku prvním jiným znakem, než jsou oddělovače. Od direktivy samotné jej opět mohou oddělovat oddělovače1.

Zdůrazněme ještě jednu důležitou skutečnost. Direktiva preprocesoru není příkaz jazyka C., neukončujme ji proto středníkem.

4.1. Definice maker.

Definice maker ve významu rozsahů polí je snad typickým příkladem použití preprocesoru2. Ve zdrojovém textu se neodvoláváme na magická čísla, ale na vhodně symbolicky pojmenovaná makra. Program to nejen zpřehlední, ale případnou změnu hodnoty makra provedeme na jednom místě.

Pomocí preprocesoru a maker můžeme vytvářet konstrukce, které zvýší čitelnost programu. Můžeme například označit začátek a konec bloku právě identifikátory začátek a konec, které pomocí preprocesoru správně převedeme na znaky { a }. Poznamenejme ovšem, že jsme popsali spíše možnost. Makra mají zvýšit čitelnost programu, nemají za úkol udělat z programu těžko srozumitelný rébus.

Pokud se text makra nevejde na jeden řádek, můžeme jej rozdělit na více následujících řádků. Skutečnost, že makro pokračuje na následujícím řádku, se určí umístěním znaku jako posledního znaku na řádku.

Pro větší přehlednost si makra rozdělme na symbolické konstanty a makra. Klíčem nechť je skutečnost, že makro na rozdíl od symbolické konstanty má argumenty.

Symbolické konstanty

Jejich definování a oddefinování můžeme syntakticky popsat takto:

#define macro_id [token_sequence]
#undef macro_id

kde

macro_id představuje jméno (identifikátor) makra;
token_sequence je nepovinný souvislý řetězec.

 

Při své činnosti prohledává preprocesor vstupní text a při výskytu řetězce macro_id3 provádí jeho nahrazení řetězcem token_sequence. Této činnosti se říká rozvoj (expanze) makra. Z tohoto popisu je jasné, proč se preprocesoru někdy zjednodušeně říká makroprocesor.

#define      START            2
#define      PRIRUSTEK        1
#define      DELKA_RADKU      100
...
int main()
{
int      pocet = 0,
alokovano = START,
prirustek = PRIRUSTEK;
pole_retezcu p_ret = NULL;
...

Makra

Makra již podle našeho dělení mají argumenty. Definujeme je takto:

#define macro_id([arg_list]) [token_sequence]

kde (ostatní položky jsou stejné, jak jsme již uvedli u symbolických konstant):

arg_list představuje seznam argumentů navzájem oddělených jen čárkou.

 

Jako klasický příklad makra si uveďme vrácení maximální hodnoty ze dvou:

#define max(a,b) ((a>b)?a:b)

Výhodou i nevýhodou je, že nepracuje s typy. Výhodou proto, že pokud bychom chtěli definovat podobnou funkci, museli bychom napsat tolik jejích verzí, kolik by bylo navzájem neslučitelných variant datových typů argumentů. Nevýhodou je netypovost makra tehdy, uvedeme-li třebas omylem jako argumenty řetězce (pak by se porovnávaly adresy jejich prvních znaků) nebo dva argumenty neporovnatelných typů (struktura a číslo, ...). Takové chyby pak (někdy) odhalí až překladač.

Při definici makra max nás možná překvapí zdánlivě nadbytečné závorky oddělující token_sequence. Musíme jen připomenout, že makra nejsou příkazy jazyka C. Jejich rozvoj probíhá na textové úrovni. Preprocesor tedy nemůže v závislosti na kontextu jednou nadbytečné závorky vypustit, jindy chybějící přidat. Proto raději sami nadbytečné závorky nevypouštíme.

Z textovosti rozvoje makra mohou plynout i nečekané problémy. Porovnejme makro a funkci, počítající druhou mocninu argumentu:

#define SQR(x)   (x*x)
int sqr(int x)
{
return x * x;
}

a představme si jejich použití:

int x, y, n = 3;
x = sqr(n+1);    /* sqr(4) -> 4*4 = 16           */
y = SQR(n+1);    /* (n+1*n+1) t.j. (4+1*4+1) = 9 */

což nám v případě makra dává zcela jiný (nesprávný) výsledek, než jsme očekávali. Pokud opravíme (x*x) na správnější ((x)*(x)), dostaneme tentokrát sice výsledek správný, ale opět najdeme příklad4, kdy správný nebude.

Jde o skutečnost, že při volání funkce se argument vyhodnotí jen jednou. U makra tomu tak být nemusí. Podívejme (s lepší variantou makra):

int x, y, n = 3;
x = sqr(++n);    /* sqr(4) -> 4*4 = 16                */
y = SQR(++n);    /* ((++n)*(++n)) t.j. ((4)*(5)) = 20 */

Opět dostaneme chybný výsledek u makra SQR().

Právě z důvodů popsaných vedlejších efektů a netypovosti maker se nedoporučuje používat makra pro náhradu funkcí. Doporučuje se použití funkcí s případným modifikátorem inline.

4.2.Standardní předdefinovaná makra.

Podle ANSI standardu musí preprocesor C identifikovat a v uvedeném významu vyhodnocovat následující makra (identifikátory maker jsou obklopeny dvěma podtržítky):

__DATE__ datum překladu, mmm dd yyyy, př. Nov 14 1993
__FILE__ jméno zdrojového souboru
__LINE__ právě zpracovávaný řádek ve zdrojovém souboru
__STDC__ definuje typ (úroveň) překladu (STanDard C)
__TIME__ čas překladu, hh:mm:ss, (hh 00-24), př. 16:02:59

Ovšem i výrobci překladačů vybavují své produkty řadou předdefinovaných maker. Zejména takových, která nám umožňují použít speciální vlastnosti jejich produktu. Z důvodů přenositelnosti se jim raději vyhneme.

Na druhé straně jsou předdefinovaná makra popisující operační systém, případně jeho verzi. Pokud píšeme program pro více OS (obvykle se hovoří o platformách), zřejmě se odvoláme na tyto symbolické předdefinované konstanty v místech, kde jsou volané funkce závislé na OS. Tuto možnost popíšeme dále v podkapitole věnované podmíněnému překladu.

Uveďme si alespoň některé nestandardní makrodefinice:

_DECVAX, IAPX286, MWC, COHERENT, _IEEE, _I386

z produktu Coherent, a některé z BC z prostředí MS-DOS:

__CDECL__, __cplusplus, __MSDOS__, __OVERLAY__, __PASCAL__,

a ještě MS-DOSovská makra pro použitý paměťový model

__TINY__, __SMALL_, __COMPACT__, __MEDIUM__, __LARGE__, __HUGE__.

Operátory # a ##.

ANSI definuje tyto dva operátory a určuje jejich vyhodnocení takto: Operátor # provádí převod argumentu na řetězec (jednoduše řečeno umístí argument mezi pár úvozovek).

Například definujeme-li

#define display(x) show((long)(x), #x)

pak preprocesor rozvine řádek

display(abs(-5));

na řádek

show((long)(abs(-5)), "abs(-5)");

Operátor ## provádí spojování tokenů tak, že argumenty oddělené tímto operátorem po rozvoji makra vytvoří jeden celek (řetězec).

Opět si ukažme činnost popisovaného operátoru.

Definujeme-li

#define printvar(x) printf("%dn", variable ##
x)

pak následující řádek

printvar(3);

přeloží preprocesor na

printf("%dn", variable3);

Jak je z ukázky patrné, mohou být mezi argumenty a operátorem ## mezery.

Podmíněný překlad

Preprocesor může během své činnosti vyhodnocovat, je-li nějaké makro definováno, či nikoliv. Při použití klíčového slova preprocesoru defined pak může spojovat taková vyhodnocení do rozsáhlejších logických výrazů. Argument defined nemusí být uzavřen do závorek. Může se však vyskytnout jen za #if nebo #elif. Například si ukažme složitější podmínku:

#if defined LIMIT && defined OSTRA &&
LIMIT==10

V závislosti na splnění či nesplnění podmínky můžeme určit, bude-li ohraničený úsek programu dále zpracován, nebo bude-li odfiltrován, a tak nebude tedy přeložen. Této možnosti použití preprocesoru říkáme podmíněný překlad.

Vždy musí být jasno, kde podmíněná část zdrojového textu začíná a kde končí. Proto nesmíme zapomínat na #endif či #elif. Podmíněné části musí být ukončeny a omezeny v rámci jednoho zdrojového textu, jinak oznámí preprocesor chybu. Podmínky velmi připomínají konstrukce jazyka C. Navíc je oproti C zavedena i podmínka #elif. Nenechme se však mýlit. Vyhodnocení podmínek provádí již preprocesor.

Ukázka neúplného programu s jednoduchým podmíněným překladem následuje.

#define LADENI
#if defined(LADENI)
#include 
#endif
#if defined(LADENI)
void volno(void)
...
void uvolni(pole_retezcu *p_r, int pocet)
...
#endif
int main(void)
...
#if defined(LADENI)
volno();
#endif
if (alokace(&p_ret, alokovano))
...
#if defined(LADENI)
uvolni(&p_ret, pocet);
volno();
#endif
...

Zbývající direktivy

Dosud jsme nepopsali čtyři direktivy preprocesoru. Tedy postupně.

#include

je direktivou naprosto nepostradatelnou. Používáme ji pro včlenění zdrojového textu jiného souboru. Tento soubor může být určen více způsoby. Proto má direktiva #include tři možné formy (pro snadnější odkazy je očíslujme):

  1. #include
  2. #include "header_name"
  3. #include macro_identifier

které postupně znamenají:

  • soubor header_name je hledán ve standardním adresáři pro include. Takto se zpravidla začleňují standardní hlavičkové soubory. Není-li soubor nalezen, je ohlášena chyba.
  • soubor header_name je hledán v aktivním (pracovním) adresáři. Není-li tam nalezen, postupuje se podle první možnosti. Takto se zpravidla začleňují naše (uživatelské) hlavičkové soubory.
  • macro_identifier je nahrazen. Další činnost podle 1. nebo 2. varianty.

Poznamenejme, že při práci nad velkým projektem se i vlastní hlavičkové soubory umisťují do zvláštního adresáře. Pak se pochopitelně připojují podle 1. varianty. Protože ovšem 2. varianta při neúspěchu přechází do 1., můžeme i v tomto případě popsaným způsobem odlišit vlastní a standardní hlavičkové soubory. Nesmíme ovšem zapomenout definovat více než jeden standardní adresář.

#error

je direktivou, kterou můžeme zajistit výstup námi zadaného chybového hlášení. Nejčastěji se používá v souvislosti s podmíněným překladem. Má formát:

#error chybové hlášení

kde chybové hlášení bude součástí protokolu o překladu.

#line

umožňuje nastavit hodnotu standardního makra __LINE__ a případně i __FILE__. Používá se zejména u strojově generovaných zdrojových textů. Má formát

#line číslo ["jméno"]

kde číslo udává hodnotu uloženou do __LINE__ a platnou pro následující zdrojový řádek.

jméno udává nepovinnou hodnotu uloženou do makra __FILE__.

#pragma

je speciální direktivou, která má uvozovat všechny implementačně závislé direktivy. Pokud jiný překladač speciální direktivu nezná, prostě ji bez chybového stavu ignoruje.


Výsvětlivky:

1 Poznamenejme však, že tato možnost je používána jen zřídka.
2 Moderní styl C v tomto případě dává přednost konstantám.
3 Symbolické konstanty obvykle píšeme velkými písmeny, abychom je v textu snadno odlišili od ostatních identifikátorů jazyka.
4 Jde o příklad bezprostředně následující

RNDr. Petr Šaloun, Ph.D.
petr.saloun@ vsb.cz

DOWNLOAD & Odkazy

Hodnocení článku: