4. díl - Makra v programovacím jazyce C

C++ Pokročilé konstrukce Makra v programovacím jazyce C

Makro je fragment kódu, kterému je přiřazen identifikační řetězec. Pokud kdekoli v kódu narazí preprocesor na tento řetězec, nahradí jej obsahem daného makra - tzv. expandování makra.

(pozn. autora: preprocesor se spustí ještě před samotnou kompilací a nahradí všechna makra ve zdrojovém souboru jejich obsahem).

Všeobecně lze makra rozdělit do dvou skupin. První skupinou jsou makra bez závorek. Tyto typy maker se používají pro definování konstant, znaků nebo řetězců. Makra se závorkami se zpravidla používají v místě, kde chceme makro nahradit funkcí nebo blokem jedno či více řádkového kódu, který může například provést výpočet maximální hodnoty ze dvou prvků. Zároveň je možné, stejně jako u klasické funkce, vložit do závorek jeden nebo více parametrů.

Protože se jméno makra v místě jeho použití přímo nahradí blokem kódu, nedochází tak k zatížení procesoru. Naopak při zavolání funkce je potřeba přepnout kontext aktuálně zpracovávané funkce a zároveň vytvořit na zásobníku datovou strukturu zvanou rámec. To jsou operace, které stojí určité množství výpočetního času.

(pozn. autora: přepnutí kontextu může například znamenat zálohování registrů dostupných na daném mikrokontroléru nebo procesoru).

Pravidla pro používání maker

  • Pokud je makro rozepsáno na více řádků, musí být každý řádek, kromě posledního, zakončen zpětným lomítkem
  • Jméno makra se píše zpravidla velkými písmeny
  • Každé makro musí být umístěno na novém řádku a nesmí mu předcházet žádné znaky kromě bílých (whitespaces)
  • Makra můžeme definovat v libovolné části zdrojového kódu, ale zpravidla je dobré je umístit hned za řádky, kde dochází ke vkládání hlavičkových souborů (pozn. autora: pokud si navyknete rozdělit zdrojový soubor do několika segmentů, znatelně tím zvýšíte přehlednost zdrojového kódu)
  • Stejně jako lze jednoduše makro zadefinovat pomocí klíčového slova #define, lze jeho definici kdykoli v kódu zrušit klíčovým slovem #undef

Makra bez závorek

Definování makra se provádí pomocí direktivy #define name replacement, kde za popisek name dosadíte jméno makra a za popisek replacement lze dosadit libovolný obsah, například hodnotu, proměnnou, nebo řetězec.

Příklad definice makra, jehož jméno bude nahrazeno hodnotou:

#define SIZE_OF_ARRAY 255

Pokud kdekoliv v textu použijete text SIZE_OF_ARRAY, nahradí jej preprocesor hodnotou 255. Takto definované makro se hodí například pro práci s poli. Hlavní výhoda je, že pokud bychom chtěli pole rozšířit, nebo zmenšit, stačí danou hodnotu změnit pouze na jednom místě.

Použití makra při práci s poli:

int array[SIZE_OF_ARRAY];
int main (void)
{
        int i;

        for (i = 0; i < SIZE_OF_ARRAY; i++)
        {
                //práce s polem
        }
}

Makra se závorkami

Tento druh maker se definuje podobně jako předešlý typ, pouze na jeho konec patří závorky. Do závorek lze umístit žádný nebo libovolný počet parametrů. Aby se to dobře pamatovalo, nazveme makra se závorkami jako funkční makra.

Funkční makra se zpravidla používají proto, aby se v místě jeho nahrazení vložila funkce nebo blok kódu, který provede určitou posloupnost operací. Parametry se za makro dosazují stejně jako při volání obyčejné funkce.

Makro bez parametrů:

#define NAME() foo()

Makro s parametry:

#define NAME(a, b) foo(a, b)
#define MAX(a, b) (((a) < (b))? (a) : (b))

Tipy pro používání maker

Používání maker sebou ale nese i určité chyby, které se v kódu a i při procesu ladění těžko odhalují. Na následujících řádcích si ukážeme chybný a správný zápis makra, které za nás spočte druhou mocninu vloženého parametru.

Výpočet druhé mocniny:

// chybný zápis
#define SQR(a) a*a
// správný zápis
#define SQR(a) ((a)*(a))

Pokud zavoláme chybně zapsané makro s hodnotou 3, nestane se nic zásadního a výpočet vrátí správnou hodnotu. Jakmile ale makro zavoláte s parametrem 3+3, výsledkem bude špatná hodnota.

Expanze makra pro výpočet druhé mocniny:

// expanze špatně zapsaného makra
int result = SQR(3+3);
int result = 3+3*3+3;     // výsledek je 3+9+3=15
// expanze správně zapsaného makra
int result = SQR(3+3);
int result = (3+3)*(3+3); // výsledek je 6*6=36

Proto při používání maker nezapomeňte každý prvek, který bude nahrazen, umístit do závorek. Další neočekávaná situace může nastat, pokud za parametr makra vložíte post-inkrementovanou proměnnou. To si ukážeme na jednoduchém příkladu, ve kterém opět použijeme makro pro druhou mocninu.

Expanze makra při vložení post inkrementované proměnné:

int i = 3;

// makro před expanzí
int result = SQR(i++);
// makro po expanzi
int result = (i++)*(i++); // očekáváný výsledek je 9
                          // reálný výsledek je (3)*(4), tedy 12

Výše zmíněné případy jsou důvody, proč se od maker v programování spíše opouští. Je lepší použít samostatnou funkci, která těmito neduhy netrpí. Přitom se kompilátor při překladu programu snaží zdrojový kód optimalizovat, takže je velmi pravděpodobné, že právě volání takové funkce nahradí jejím tělem. Ve výsledku máme stejně rychlý kód (jak s použitím funkce, tak s použitím makra), ale bez skrytých chyb. V některých případech se dokonce makra mohou chovat rozdílně u rozdílných kompilátorů (například mezi kompilátorem pro Windows a Linux). Obecná rada tedy zní: radši používejte funkce než makra.

Poslední případ, kdy může dojít k chybě při používání maker, je pokud vložíte bílý znak mezi jméno makra a levou závorku. Takto chybně umístěný bílý znak způsobí, že se jméno makra při jeho expanzi nahradí veškerým obsahem, který následuje za bílým znakem, včetně závorek.

Chybné funkční makro:

#define WRONG_MACRO () foo()

// makro před expanzí
int a = WRONG_MACRO();
//makro po expanzi
int a = () foo();

Standardní makra

Preprocesor libovolného překladače by měl podporovat i určitá standardní makra. Jedním z nejvíce používaných standardních maker jsou bezpochyby podmínky (neboli if).

Klasickým případem použití může být:

//#define USE_CONST

#if defined(USE_CONST)
  #define CONST const
#else
  #define CONST /* */
#end

Pokud odkomentujete první řádek v příkladu, pak se kdekoli v kódu, kde preprocesor nalezne název CONST, makro nahradí klíčovým slovem const. V opačném případě se dané makro nenahradí ničím, protože je prázdné (pozn. autora: klíčové slovo const nemusí být podporováno všemi kompilátory jazyka C).

Zápisy mají ještě své ekvivalentní a zkrácené zápisy. Jedná se o ifdef a ifndef. V prvním případě se do zdrojového kódu část programu přidá, pokud je makro nadefinované, ve druhém případě nedefinované. Program výše by šel přepsat následovně:

//#define USE_CONST

#ifdef USE_CONST
  #define CONST const
#endif

Mezi další užitečná makra lze zařadit __DATE__ a __TIME__. Kdykoli preprocesor narazí na tato makra, nahradí je aktuálním časem nebo datem.

Příklad použití maker pro datum a aktuální čas:

// makro před expanzí
string date = __DATE__;
string time = __TIME__;
// makro po expanzi
string date = "Mar 27 2014";
string time = "21:06:19";

Stejným způsobem lze použít i makra __FILE__ a __LINE__, kdy preprocesor nahradí tato makra souborem a řádkem, ve kterém se objeví.

Doporučení

Jak bylo zmíněno v jedné z předešlých kapitol, může v určitých případech dojít ke špatné expanzi makra kvůli chybnému zápisu. Zároveň jsou víceřádková makra nepřehledná a při ladění zdrojového kódu jsou vykonána jako jedna instrukce. Tudíž ztratíte možnost projít kód řádek po řádku a zkontrolovat tak správnost jednotlivých operací. Pokud byste chtěli zvýšit bezpečnost zdrojového kódu, uvedu několik příkladů, které mohou určité typy maker nahradit.

Použití klíčového slova const:

// Makro pro definování konstanty
#define SIZE 10
// Alternativní příklad s použitím klíčového slova const
const int SIZE = 10;

Klíčové slovo const se může vyskytovat před definicí libovolné proměnné a znemožní jakkoli manipulovat s jejím obsahem. V případě, že se pokusíte hodnotu změnit, skončí proces kompilace chybou.

Použití výčtového typu enum:

// Definice konstant pro skupinu příkazů
#define COMMAND_GO   1
#define COMMAND_STOP 2
#define COMMAND_NEXT 3
#define COMMAND_PREV 4
// Nahrazení výčtovým typem
enum COMMAND
{
        COMMAND_GO = 1,
        COMMAND_STOP,
        COMMAND_NEXT,
        COMMAND_PREV,
}

Každý prvek výčtového typu COMMAND se pak chová jako konstanta. Tudíž s jeho hodnotou není možné manipulovat.

Použití klíčového slova inline:

inline void sqr(a)
{
        return a*a;
}

V případě klíčového slova inline je veškerý kód obsažený uvnitř funkce vložen do místa jejího volání. Tím odpadne sekvence pro volání a návrat z funkce. Pamatujte, že inline funkce musí být co nejkratší. Pokud by obsahovala více řádků, tak v lepším případě kompilátor klíčové slovo ignoruje, ale v horším případě způsobí ještě větší zatížení systému, než volání klasické funkce.

(pozn. autora: klíčové slovo const je součástí standardu ANSI C od roku 1989, zkráceně C89, a klíčové slovo inline je součástí standardu od roku 1999, zkráceně C99. Implementace těchto standardů do kompilátorů by měla být samozřejmostí, ale existují i výjimky)

V programovacím jazyce C++ lze použít pro náhradu maker pokročilejší techniky jako jsou šablony nebo lambda funkce. To už je ale nad rámec tohoto článku.

Include guard

Include guard (ve volném překladu "hlídač vkládání") je konstrukce, se kterou se setkáte velmi často. Uveďme si jednoduchý příklad:

#ifndef COKOLIV
#define COKOLIV

//kód

#endif

S podobnou konstrukcí se setkáte především u hlavičkových souborů a zpravidla se za COKOLIV napíše jméno souboru s koncovkou (například pro soubor funkce.h by název makra byl FUNKCE_H_). A co nám takové makro hlídá? Při překladu programu budeme mít jistotu, že se soubor bude includovat právě jednou (je-li includován ve více souborech) a kompilátor nebude mít problém s vícenásobným definováním stejné funkce. To nám zajistí definování makra ihned za podmínku, že makro nadefinováno není.

Závěr

I když je doporučeno vyhnout se používání funkčních maker a maker pro definování konstant, s ostatními typy maker se setkáte vždy a rozhodně není na jejich použití nic složitého.


 

  Aktivity (4)

Článek pro vás napsal SPoon
Avatar

Jak se ti líbí článek?
Celkem (3 hlasů) :
55555


 



 

 

Komentáře
Zobrazit starší komentáře (3)

Avatar
paja
Člen
Avatar
paja:

Moc pěkný článek. Pěkné shrnutí problematiky. Osobně se snažím makrům spíš vyhnout.Přimlouval bych za pokračování C čka.;)

 
Odpovědět 30.7.2014 6:05
Avatar
Bertram
Člen
Avatar
Bertram:

citace z článku:
Klíčové slovo const se může vyskytovat před definicí libovolné proměnné a znemožní jakkoli manipulovat s jejím obsahem. V případě, že se pokusíte hodnotu změnit, skončí proces kompilace chybou.

Takto definovaná konstanta je uložena na rozdíl od "symbolické" v paměti, takže jde její hodnota změnit pomocí ukazatele.

const int SIZE = 10;
int* p_SIZE = &SIZE;
*p_SIZE = 100;
printf("SIZE = %d\n", SIZE);
 
Odpovědět 10.8.2014 6:58
Avatar
SPoon
Redaktor
Avatar
Odpovídá na Bertram
SPoon:

Ahoj,

díky za připomínku. Správně by to ale mělo být:

const int SIZE = 10;
int* p_SIZE = (int *)&SIZE;
*p_SIZE = 100;
printf("SIZE = %d\n", SIZE);

Pokud se reference nepřetypuje, měl by kompilátor zahlásit chybu.

 
Odpovědět  +1 10.8.2014 18:28
Avatar
coells
Redaktor
Avatar
Odpovídá na Bertram
coells:

Tohle je velice nebezpečný kód a není pravda, že konstanta musí být v paměti. Je několik variant, co se stane:

  1. program vypíše hodnotu 100
  2. program vypíše hodnotu 10
  3. program skončí na SEGFAULT

Konstantní hodnoty vůbec nemusí být alokované v paměti nebo dokonce v zapisovatelné paměti. Všechno záleží jenom na kompilátoru, typu zapnuté optimalizace a způsobu nalinkování knihovny.

 
Odpovědět  +4 10.8.2014 20:10
Avatar
SPoon
Redaktor
Avatar
Odpovídá na coells
SPoon:

Prováděli jsme testy na jednom typu kompilátoru a pokud byla konstantní proměnná menší jak 16-bitů, tak se chovala stejně jako konstanta zadefinovaná pomocí makra #define. To znamená, že nebyla uložena v paměti a kdekoli byla konstanta použita, byla přímo nahrazena číselnou hodnotou. Pokud byla konstanta větší, kompilátor pro ni vyhradil místo v paměti.

V tomto případě, ale souhlasím s coellsem, protože výše uvedený příklad zcela narušuje principy bezpečného programování.

 
Odpovědět 10.8.2014 22:05
Avatar
coells
Redaktor
Avatar
Odpovídá na SPoon
coells:

Používám LLVM 5.1 a povedlo se mi snadno nasimulovat všechny tři situace, které jsem zmínil. Hodně vtipná je varianta 1+2, kdy mi kód vypíše současně 100 i 10.

To, co uvádíš, je dost podivné chování a moc mi nedává smysl. Spíš bych řekl, že tam byly jiné faktory, které jste přehlídli.

 
Odpovědět 10.8.2014 22:28
Avatar
SPoon
Redaktor
Avatar
Odpovídá na coells
SPoon:

V případě, že je k dispozici malé množství paměti RAM (například 128 KB), což je typické pro embedded systémy, tak je velmi důležité, aby konstanty zbytečně nezabíraly potřebné místo. V takovém případě provede kompilátor optimalizaci - konstanta se do paměti neumístí a každý její výskyt přímo nahradí danou hodnotou.

Daná hodnota musí ale splnit dvě základní pravidla. Prvním je, že se na danou konstantu nikdy neodkážeme pomocí pointeru. Druhým pravidlem je, že číselná hodnota musí být velká tak, aby se vešla do jedné instrukce (to pro 16-bitovou instrukční sadu a 32-bitovou architekturu znamená, že číslo musí být velké maximálně 16-bitů).

Samozřejmě výše uvedené vlastnosti jsou "compiler specific", tedy závislé na typu použitého kompilátoru.

PS: Podle mého názoru se musí kompilátor pokaždé chovat deterministicky. Pokud tedy zareaguje ve stejné situaci pokaždé jinak (jako např. v tvém případě), není to správné chování.

Editováno 11.8.2014 9:22
 
Odpovědět 11.8.2014 9:21
Avatar
coells
Redaktor
Avatar
Odpovídá na SPoon
coells:

Požadavek "neodkazovat se pomocí pointeru" nelze brát v úvahu, jsme v C. Na proměnnou j se nikdy nemusím odkázat a stejně vím, kde je a mám na ni pointer (v tom lepším případě, v tom horším mám pouze pointer).

const int i = 1;
const int j = 2;

int *pj = &i + 1;

Klíčová věc je, jak je konstanta definovaná a kde bude uložená. Díky tomu, jsem clang donutil chovat se pokaždé jinak, stále je to ale deterministické chování.

Naprosto klíčová věc jsou ale optimalizace. A tady je kámen úrazu a důvod, proč je takový kód nekorektní. Vygenerovaný kód se bude chovat odlišně v závislosti na typu optimalizace a dokonce i zařízení, na kterém poběží. Jedny z nejtěžších chyb, které občas musím hledat, jsou pády aplikace, která skvěle běží v simulátoru na core i7, ale na armv7 "bezdůvodně" padá. Na tom není nic nedeterministic­kého, stačí vědět, co se děje na úrovni kompilátoru.

 
Odpovědět  +1 11.8.2014 9:44
Avatar
SPoon
Redaktor
Avatar
Odpovídá na coells
SPoon:

Špatně jsem se vyjádřil. Myslel jsem tím, že pokud se kdekoli v kódu objeví tento řádek z tvého příkladu, tak i při zapnutých optimalizacích bude konstanta umístěna v paměti, protože se už pracuje s její adresou.

int *pj = &i + 1;

Pokud se konstanta použije například jenom v operaci přiřazení nebo porovnání, tak se při zapnutých optimalizacích nemusí v paměti objevit vůbec a je v kódu přímo nahrazena její hodnotou.

 
Odpovědět  +1 11.8.2014 10:01
Avatar
pracansky
Člen
Avatar
pracansky:

Doporučený zápis na začátku článku ve skutečnosti není úplně bezpečný.

// správný zápis
#define SQR(a) (a)*(a)

Uvedu to na jednoduchém příkladu

#define Secti(a,b)  (a)+(b)
int x = 2 * Secti(2,2);

Na první pohled by se zdálo že Secti vrátí hodnotu 4 a po vynásobení dvěma bude výsledek 8.
Makro se ale rozbalí na:

int x = 2 * (2) + (2);

takže výsledek bude 6.
Makra toho typu by se **vždy **měla závorkovat takto

#define Secti(a,b) ((a)+(b))
 
Odpovědět  +1 29.4.2015 21:27
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 13. Zobrazit vše