C/C++ (12) - Preprocesor

Před vlastním překladem se dostane ke slovu preprocesor. S jeho pomocí můžeme například definovat konstantu, vložit soubor nebo překládat jen část zdrojového souboru.

18.1.2005 15:00 | Jan Němec | přečteno 35089×

Tři fáze překladu

Celý proces překladu od zdrojového kódu v C až po spustitelný soubor má 3 fáze. Nejprve se dostane ke slovu preprocesor, následuje vlastní překlad a závěrečnou fází je linkování. Kód programu tvoří jeden nebo více *.c souborů, preprocesor zpracovává každý zvlášť. Provádí úpravy na textové úrovni, například vypouští komentáře, vkládá soubory, nahrazuje jeden symbol za jiný atd. Překlad také probíhá na jednotlivých souborech odděleně. Výsledkem je již téměř hotový spustitelný binární kód, obsahuje ovšem odkazy na funkce a proměnné z ostatních přeložených souborů a také knihoven. Závěrečnou fází je linkování, kdy se z jednoho nebo několika takto "téměř přeložených" souborů vytvoří jeden spustitelný program.

Preprocesor

Celá řada jazyků (například Pascal, Java, ...) preprocesor vůbec nemá. Já osobně to považuji za chybu v návrhu těchto jazyků, která mohla vzniknout nejen autorovou neznalostí nebo ideologickou zaslepeností, ale někdy i prostým zděšením, k čemu všemu se v Céčku preprocesor používá. Pokud pracujete s překladačem gcc (na Linuxu obvykle ano), zkuste některý z příkladů předchozího dílu prohnat pouze preprocesorem bez dalších fází překladu.

gcc -E priklad.c

Díky příkazu preprocesoru #include <stdio.h>, který vkládá do našeho kódu standardní soubor stdio.h a tím zpřístupňuje některé funkce standardní knihovny, se na standardní výstup vyhrne přes 30 stránek kódu a až úplně nakonec následuje několik řádek našeho příkladu. Ještě mnohem horší je to u C++ a jeho standardních i nestandardních (napadá mě třeba Qt) knihoven. Díky množství #include kódu, který jen zpřístupňuje volání knihovních funkcí, trvá překlad i středně velkého projektu neúnosně dlouho, ačkoliv kód napsaný programátorem projektu není příliš rozsáhlý.

Dalším vážným problémem Céčka, který preprocesor jen zhoršuje, je ochrana identifikátorů. Snad každému zkušenějšímu C programátorovi se stalo, že si definoval makro preprocesoru, jehož název kolidoval s identifikátorem z nějaké knihovny.

Na druhou stranu preprocesor řadu věcí velmi usnadňuje. Vyzdvihl bych zejména podmíněný překlad. Díky němu lze poměrně jednoduše psát multiplatformní programy nebo psát ladící kód, který nebude v distribuční verzi přeloženého programu.

Vložení souboru

Soubor vložíme pomocí příkazu #include, který má dvě varianty.

#include <soubor.h>
#include "soubor.h"

V prvním případě se vezme soubor z adresáře se standardními hlavičkovými soubory (na Linuxu obvykle /usr/include), ve druhém pak z adresáře se zdrojovým kódem programu. V obou případech mohou jména souborů obsahovat relativní cestu, adresáře se oddělují obyčejným lomítkem a to i v DOSu a odvozených systémech, kde se jinak používá lomítko zpětné.

#include <sys/socket.h>
#include "../moje_knihovna/knihovna.h"

Vkládané soubory mají obvykle příponu .h, ale jde pouze o programátorskou konvenci, nikoli o vlastnost jazyka C. Obsah vkládaného souboru také není nijak omezen. Lze tedy například rozčlenit velký projekt do více zdrojových souborů a v jediném *.c souboru, který se bude překládat, mít jen několik #include příkazů. Takto se ovšem v C v praxi neprogramuje, vkládání souborů se obvykle používá pouze na zpřístupnění maker, typů, funkcí a proměnných. Podrobnosti si povíme v některém z dalších dílů, zatím jen stačí vědět, že známý příkaz preprocesoru #include <stdio.h>, kterým si zpřístupňujeme funkce puts a printf, je ve skutečnosti vložení souboru /usr/include/stdio.h.

Generování chyby

Překlad můžeme explicitně přerušit pomocí příkazu #error. Význam má zejména v souvislosti s podmíněným překladem, ale pro přepracované programátory má jistě význam i konstrukce

#error tady jsem skončil před dovolenou

Makro bez hodnoty

Makro bez parametrů definujeme příkazem #define

#define MAKRO

Definice platí do konce souboru, přesněji řečeno do konce souboru ve smyslu překladu nikoli preprocesoru. Pokud například makro definujeme v hlavičkovém souboru hlavicka.h a ten pak pomocí

#include "hlavicka.h"

vložíme do zdroj.c, bude definice platit i pro kód ze souboru zdroj.c a to od příkazu #include "hlavicka.h" dále, platná bude rovněž pro veškerý kód z případných následujících #include příkazů.

Na makro se můžeme dotázat pomocí příkazů #ifdef, #ifndef, #else a #endif, říká se tomu podmíněný překlad.

#define LADENI

 /*
  Tady je nějaký kód
*/

#ifdef LADENI
  printf("Proměnná i má hodnotu %i\n", i);
#endif

Pomocí jediné definice makra LADENI, kterou umístíme nejlépe do nějakého hlavičkového souboru, tak lze snadno zapnout ladící výpisy. V distribuční verzi programu pak definici makra zrušíme (tj. provedeme jedinou změnu na jednom místě) a program přestane ladící hlášky vypisovat.

Překladače navíc umožňují definovat makra externě mimo zdrojáky. V grafických vývojových prostředích lze obvykle vše naklikat, "správný" linuxový programátor použije parametr -D překladače gcc.

gcc program.c -DLADENI -o program

Pro přeložení distribuční verze zavolá standardně.

gcc program.c -o program

Tento způsob ovlivňování překladu je lepší než dotaz na ručně definované makro v *.h souboru, ušetříme si jím i tu jedinou změnu v jednom souboru.

Překladače definují (nebo nedefinují) celou řadu maker v závislosti na prostředí překladu. Z těch nejužitečnějších bych jmenoval unix, _WIN32 nebo __cplusplus, podrobnosti získáte v dokumentaci ke konkrétnímu překladači. Následující kód demonstruje užitečnost podmíněného překladu při multiplatformním programování. Na Windows pomocí API funkce z windows.h vyhodí dialog a všude jinde vypíše řetězec na standardní výstup.

#ifdef _WIN32
  MessageBox(NULL, "Ahoj světe!", "Zpráva", MB_OK);
#else
  puts("Ahoj světe");
#endif

Bez použití podmíněného překladu by na Linuxu překlad skončil s chybou kvůli neznámé funkci MessageBox a konstantě MB_OK.

Zda je program překládán jako C nebo jako C++ se zase můžeme zeptat pomocí makra __cplusplus. Díky částečné jednosměrné kompatibilitě lze obvykle C program přeložit i pomocí C++ překladače, pokud v něm nepoužíváme (např. jako identifikátory proměnných) klíčová slova C++ ani některé problematické konstrukce, které jsou v C++ zakázané.

#ifndef __cplusplus
  puts("Přeloženo jako čisté C");
#else
  puts("Přeloženo jako C++");
#endif

Makro s hodnotou

Makro může mít nějakou hodnotu, běžně se jako makra definují konstanty používané na více místech nebo pokud chceme mít jakési nastavení programu na jednom místě například v hlavičkovém souboru. Z hlediska vlastního překladu je makro konstanta a nikoli proměnná, takže lze makra použít i jako meze polí.

#define N 10

int a[N];

int main(void) {
  int i;

  for(i = 0; i < N; i++)
    a[i] = i;
  return 0;
}

Hodnotou makra může být prakticky cokoli, takže lze napsat i následující hrůzu:

#include <stdio.h>

#define KONEC return 0;
#define CYKLUS_PRES_I for(i = 0; i < N; i++)
#define RETEZEC "Ahoj světe"
#define NOVY_RADEK puts("");
#define N 10

int main(void) {
  int i;
  
  CYKLUS_PRES_I {
    printf(RETEZEC);
    NOVY_RADEK
  }
  KONEC
}

Hodnotu makra můžeme pomocí zpětného lomítka napsat i na více řádků.

#define ERR_STRING "Chyba 134\n" \
                   "syntaxe: prhlizec soubor.cfg url\n"\
                   "příklad: prohlizec /etc/prohlizec.cfg http://linuxsoft.cz"

Řídit překlad můžeme také pomocí #if a konstantního výrazu. Běžné komentáře v C mají tu nevýhodu, že je nelze zanořovat. Pokud tedy chceme dočasně zakomentovat kus kódu, který již obsahuje běžné komentáře, nelze použít

 /*
  int moje_funkce() {
    /* Vnořený komentář - chyba !!! */
    return 0;
  }
*/

Nic nám však nebrání použít #if.

#if 0
  /* Tak trochu jiný komentář... */
  int moje_funkce() {
    /* Vnořený komentář - OK */
    return 0;
  }
#endif

V konstantním výrazu v #if lze navíc použít i makra a také běžné operátory. Ve spolupráci s direktivou #elif a operátorem defined lze již napsat opravdu zajímavé věci. Ukážeme si to na příkladu.

Příklad pro dnešní díl

Při psaní větších projektů je třeba předem myslet na ladění. Náš příklad je velmi jednoduchý, a ladící kód proto působí dost nepřirozeně. Ve skutečných a složitých případech však může podobně opatrný přístup ušetřit spoustu práce při hledání chyb.

#include <stdio.h>

#define N 10

 /*

Tyto definice raději zadáme zvnějšku

#define LADIM
#define MALA 0
#define VELKA 1
#define UROVEN VELKA

*/

int a[N];

int main(void) {
  int i;
  
  for (i = 0; i <= N; i++) {
#ifdef LADIM
    if (i >= sizeof(a)/sizeof(int)) {
      printf("Pokus o přístup mimo pole na index %i\n", i);
      return 1;
    }
#if UROVEN==VELKA
    printf("Do pole a na index %i píšu %i\n", i, i);
#elif UROVEN==MALA
    putchar('.'); /* Aby bylo vidět, že se program nekousl */
#endif
#endif /* od #ifdef LADIM */
    a[i] = 1;
  }
#if defined LADIM && UROVEN==VELKA
  puts("Konec programu");
#endif
  return 0;
}

Zkuste si program přeložit třeba takhle:

gcc program.c -o program -DLADIM -DMALA=0 -DVELKA=1 -DUROVEN=VELKA

Pokud používáte jiný překladač a neumíte zadat makro zvnějšku, odkomentujte definici maker ve zdrojovém kódu.

Pokračování příště

I v dalším dílu se zaměříme na preprocesor. Ukážeme si, jak se píší makra s parametry.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=611