Lekce 4 - Makra v programovacím jazyce C

V minulé lekci, Pokročilé cykly v jazyce C, jsme si představili další příkazy, kterými můžeme ovlivňovat běh cyklů. V dnešním C tutoriálu na nás čekají makra.

Makra

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

Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!

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. V příští lekci, Pokročilé zpracování vstupu a výstupu v jazyce C, se budeme věnovat pokročilému zpracování vstupů a výstupů.


 

 

Aktivity (5)

 

 

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

Avatar
coells
Redaktor
Avatar
coells:11.8.2014 9:44

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
11.8.2014 9:44
Avatar
SPoon
Redaktor
Avatar
Odpovídá na coells
SPoon:11.8.2014 10:01

Š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
11.8.2014 10:01
Avatar
pracansky
Člen
Avatar
pracansky:29.4.2015 21:27

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
29.4.2015 21:27
Avatar
Odpovídá na SPoon
Patrik Pastor:4.9.2019 18:23

a proc vadi ze se zapise constanta do pameti (v pripade ze se na ni &promenna referencujes)? To zabira tolik mista? Nebo je to nebezpecne ze strany kompilatory, jak si s tim poradi? To ze je constanta, by melo znamenat, za by se nemela dat zmenit hodnota teto promenne. Ale stale prece muzes mit na ni ukazatel, kdyz je to stale promenna, jako kazda jina (jestli to spravne rozumim). Nebo co presne vadi kompilatoru na tom, ze se pokousis referencovat uzakatelem na adresu constanty

 
Odpovědět
4.9.2019 18:23
Avatar
Patrik Pastor:4.9.2019 18:25

Proc se vlastne u guardu definuje jmeno (nazev)?

#ifndef NAZEV
#define NAZEV

kdyz stejne nento nazev nebudu pri includovani tohoto souboru pouzivat, ale budu pouzivate nazev vlastniho souboru. Proc ma ale samotny guard svuj nazev? to ho muzu jeste nekde jinde pouzit

 
Odpovědět
4.9.2019 18:25
Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Patrik Pastor
DarkCoder:4.9.2019 19:50

V článku to je popsané, odstavec "Include guard". Celá tato konstrukce:

#ifndef JMENO_MAKRA
#define JMENO_MAKRA
...

#endif

má za úkol nedovolit vložit obsah souboru do programu více než jednou. Např. definice typů, které bývají obsahem hlavičkových souborů, nelze do programu vložit více než jednou. Program se nepovede přeložit. Celé to funguje tak, že při prvním pokusu o přeložení se otestuje existence makra. Jelikož makro neexistuje, vloží se celý obsah souboru do programu a zároveň se definuje makro. Při druhém pokusu nedojde k opětovnému vložení, protože makro už existuje a test na jeho neexistenci se vyhodnotí jako nepravdivá a nedojde tak k vložení obsahu souboru do programu. Pokud makro nedefinuješ, test podmínky projde a dojde k vícero vložení obsahu souboru což způsobí při překladu chybu.

Odpovědět
4.9.2019 19:50
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
Nositelka Změny:5. ledna 15:50

Je pravda, že dnes již není potřeba psát inline a dnešní kompilátory si ho tam dopíší samy, pokud to bude vhodné? Podobně jako existovaly modifikátory auto a register, kterými programátor určil, zda se proměnná má uložit do registru nebo na zásobník. Dnes to již není potřeba a kompilátor si to zařídí sám. I když tato klíčová slova použijeme, kompilátor je může klidně ignorovat. Může ignorovat i inline, když se nehodí. Ale nejsem si jistý, zda může krátkou funkci použít jako inline, i když ji tak neoznačíme. Vím jen, že to je možné - v Javě to funguje, ale jestli i C? …

Odpovědět
5. ledna 15:50
j.k.j
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Nositelka Změny
DarkCoder:5. ledna 17:13

Ne. Klíčové slovo inline, přidané v C99, není automaticky k funkci doplňováno překladačem. Je to pouze doporučení pro překladač. Začlenění modifikované funkce pomocí klíčového slova inline ještě neznamená, že funkce bude napřímo vložena. Vložení funkce má jistá pravidla, která překladač vyhodnocuje a na jejich základě se rozhodne, zda-li modifikovanou funkci do programu začlení.

Klíčové slovo auto, které existuje dodnes, se vesměs nepoužívá. Každá lokální proměnná je obecně deklarována jako auto. Toto klíčové slovo do jazyka C bylo přidáno kvůli kompatibilitě s jazykem B.

U klíčového slova register nedochází k automatickému uložení proměnné do registru. Je to opět instrukce pro překladač, že má k proměnné přistupovat pokud možno co nejrychleji. Register je stále nejlepší volba. Dále automatické uložení proměnné modifikované klíčovým slovem register do registru nemusí proběhnout z důvodu omezeného počtu registrových proměnných.

Tedy vkládání těchto klíčových slov před funkce respektive před proměnné má svůj význam, neprobíhá to ani automaticky ani zaručeně. Záleží také na druhu optimalizace a na tom zda je optimalizace vůbec povolena.

Odpovědět
5. ledna 17:13
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
Nositelka Změny:5. ledna 18:24

Tak trochu jsem se toho obávala - takže psát inline má smysl i dnes. Ale podle linuxsoftu je použití register při dnešní optimalizaci zbytečné (předpokládám, že tehdejší dnešní na tom není jinak než dnešní dnešní, proč by se zrovna tohle mělo měnit, když optimalizace lze vypnout). O auto vím jenom díky jednomu seznamu klíčových slov C, najít jeho význam bylo složitější. Na žádném českém webu se o tom nemluví, ale v angličtině je toho dost. Díky za vysvětlení.

Odpovědět
5. ledna 18:24
j.k.j
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Nositelka Změny
DarkCoder:5. ledna 19:07

Není třeba se toho obávat, na funkčnost kódu to vliv nemá. Použití inline má velký smysl. Dokáže zrychlit provádění programu. To proč není automaticky použito pro každou funkci je to, že nemá jen výhody. Začlenění funkce přímo do programu ovlivňuje velikost programu. Takže je dobré si promyslet, kde to smysl má a kde ne. Než přišla norma C99, používala se především makra s parametry. Tato makra si vyžadovala a stále vyžadují správné použití závorek. Jejich absence může mít vliv (a dost často má) na funkci makra. Od C99 bylo vesměs použití parametrizovaných maker nahrazeno mnohem jednodušším způsobem za pomocí klíčového slova inline.

Kdyby měla optimalizace jedno kritérium, pak by tomu tak bylo. Ale jelikož jich je více, je použití klíčového slova registr stále aktuální. Počet proměnných může být hodně velké a počet registrovaných proměnných je omezen. Když budu chtít optimalizovat svůj program, těžko překladač bude vědět jak to chci já. Bude optimalizovat ty proměnné které se v programu vyskytují nejčastěji nebo mi optimalizuje ty, kde část kódu trvá nejdéle? Nebo bude pro mě kritická jiná oblast, kterou překladač nerozpozná.

Jak už jsem psal, modifikátor auto se už dnes nepoužívá, ale lze ho spatřit v mnoha starších programech. Je tedy dobré vědět, že něco takového existuje a nebýt tak překvapen.

Odpovědět
5. ledna 19:07
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
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 20. Zobrazit vše