Hledáme nového kolegu do redakce - 100% home office, 100% flexibilní pracovní doba. Více informací.
Využij akce až 80 % zdarma při nákupu e-learningu - více informací. Zároveň pouze tento týden sleva až 80 % na e-learning týkající se Swift
discount week 80

4. diel - Makrá v programovacom jazyku C

V minulej lekcii, Pokročilé cykly v jazyku C , sme si predstavili ďalšie príkazy, ktorými môžeme ovplyvňovať beh cyklov. V dnešnom C tutoriálu na nás čakajú makrá.

Makra

Makro je fragment kódu, ktorému je priradený identifikačný reťazec. Ak kdekoľvek v kóde narazí preprocesor na tento reťazec, nahradí ho obsahom daného makra - tzv. Expandovanie makrá.

(pozn. autora: preprocesor sa spustí ešte pred samotnou kompiláciou a nahradí všetky makrá v zdrojovom súbore ich obsahom).

Všeobecne možno makrá rozdeliť do dvoch skupín. Prvou skupinou sú makrá bez zátvoriek. Tieto typy makier sa používajú pre definovanie konštánt, znakov alebo reťazcov. Makrá sa zátvorkami sa spravidla používajú v mieste, kde chceme makro nahradiť funkcií alebo blokom jedno alebo viac riadkového kódu, ktorý môže napríklad vykonať výpočet maximálnej hodnoty z dvoch prvkov. Zároveň je možné, rovnako ako u klasickej funkcie, vložiť do zátvoriek jeden alebo viac parametrov.

Pretože sa meno makra v mieste jeho použitia priamo nahradí blokom kódu, nedochádza tak k zaťaženiu procesora. Naopak pri zavolaní funkcie je potrebné prepnúť kontext aktuálne spracovávané funkcie a zároveň vytvoriť na zásobníku dátovú štruktúru zvanú rámec. To sú operácie, ktoré stoja určité množstvo výpočtového času.

(pozn. autora: prepnutie kontextu môže napríklad znamenať zálohovanie registrov dostupných na danom mikrokontroléra alebo procesora).

Pravidlá pre používanie makier

  • Ak je makro rozpísané na viac riadkov, musí byť každý riadok, okrem posledného, zakončený spätným lomítkom
  • Meno makra sa píše spravidla veľkými písmenami
  • Každé makro musí byť umiestnené na novom riadku a nesmie mu predchádzať žiadne znaky okrem bielych (whitespaces)
  • Makrá môžeme definovať v ľubovoľnej časti zdrojového kódu, ale spravidla je dobré ich umiestniť hneď za riadky, kde dochádza ku vkladaniu hlavičkových súborov (pozn. Autora: ak si navyknete rozdeliť zdrojový súbor do niekoľkých segmentov, znateľne tým zvýšite prehľadnosť zdrojového kódu)
  • Rovnako ako je možné jednoducho makro zadefinovať pomocou kľúčového slova #define, možno jeho definíciu kedykoľvek v kóde zrušiť kľúčovým slovom #undef

Makrá bez zátvoriek

Definovanie makra sa vykonáva pomocou direktívy #define name replacement, kde za popisok name dosadíte meno makrá a za popisok replacement možno dosadiť ľubovoľný obsah, napríklad hodnotu, premennú, alebo reťazec.

Príklad definície makrá, ktorého meno bude nahradené hodnotou:

#define SIZE_OF_ARRAY 255

Ak kdekoľvek v texte použijete text SIZE_OF_ARRAY, nahradí ho preprocesor hodnotou 255. Takto definované makro sa hodí napríklad pre prácu s poľami. Hlavná výhoda je, že ak by sme chceli poľa rozšíriť, alebo zmenšiť, stačí danú hodnotu zmeniť len na jednom mieste.

Použitie makra pri práci s poľami:

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

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

Makrá sa zátvorkami

Tento druh makier sa definuje podobne ako predošlý typ, iba na jeho koniec patrí zátvorky. Do zátvoriek možno umiestniť žiadny alebo ľubovoľný počet parametrov. Aby sa to dobre pamätalo, nazveme makrá sa zátvorkami ako funkčné makrá.

Funkčné makra sa spravidla používajú preto, aby sa v mieste jeho nahradenie vložila funkcie alebo blok kódu, ktorý vykoná určitú postupnosť operácií. Parametre sa za makro dosadzujú rovnako ako pri volaní obyčajné funkcie.

Makro bez parametrov:

#define NAME() foo()

Makro s parametrami:

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

Tipy pre používanie makier

Tento výukový obsah pomáhajú rozvíjať nasledujúce firmy, ktoré možno hľadajú práve teba!

Používanie maker sebou ale nesie aj určité chyby, ktoré sa v kóde a aj pri procese ladenia ťažko odhaľujú. Na nasledujúcich riadkoch si ukážeme chybný a správny zápis makrá, ktoré za nás spočíta druhú mocninu vloženého parametra.

Výpočet druhej mocniny:

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

Ak zavoláme chybne zapísané makro s hodnotou 3, nestane sa nič zásadné a výpočet vráti správnu hodnotu. Akonáhle ale makro zavoláte s parametrom 3 + 3, výsledkom bude zlá hodnota.

Expanzia makrá pre výpočet druhej 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

Preto pri používaní makier nezabudnite každý prvok, ktorý bude nahradený, umiestniť do zátvoriek. Ďalšie neočakávaná situácia môže nastať, ak za parameter makra vložíte post-inkrementovanou premennú. To si ukážeme na jednoduchom príklade, v ktorom opäť použijeme makro pre druhú mocninu.

Expanzia makrá pri vložení post inkrementované premenné:

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

Vyššie spomínané prípady sú dôvody, prečo sa od makier v programovaní skôr opúšťa. Je lepšie použiť samostatnú funkciu, ktorá týmito neduhmi netrpí. Pritom sa kompilátor pri preklade programu snaží zdrojový kód optimalizovať, takže je veľmi pravdepodobné, že práve volanie také funkcie nahradí jej telom. Vo výsledku máme rovnako rýchly kód (ako s použitím funkcie, tak s použitím makra), ale bez skrytých chýb. V niektorých prípadoch sa dokonca makra môžu správať rozdielne u rozdielnych kompilátorov (napríklad medzi kompilátorom pre Windows a Linux). Všeobecná rada teda znie: radšej používajte funkcie ako makrá.

Posledný prípad, kedy môže dôjsť k chybe pri používaní makier, je ak vložíte biely znak medzi meno makra a ľavú zátvorku. Takto chybne umiestnený biely znak spôsobí, že sa meno makra pri jeho expanzii nahradí kompletným obsahom, ktorý nasleduje za bielym znakom, vrátane zátvoriek.

Chybné funkčné makro:

#define WRONG_MACRO () foo()

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

Štandardné makrá

Preprocesor ľubovoľného prekladača by mal podporovať aj určitá štandardné makrá. Jedným z najviac používaných štandardných makier sú bezpochyby podmienky (čiže if).

Klasickým prípadom použitia môže byť:

//#define USE_CONST

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

Ak odkomentujete prvý riadok v príklade, potom sa kdekoľvek v kóde, kde preprocesor nájde názov CONST, makro nahradí kľúčovým slovom const. V opačnom prípade sa dané makro nenahradí ničím, pretože je prázdne (pozn. Autora: kľúčové slovo const nemusí byť podporované všetkými kompilátory jazyka C).

Zápisy majú ešte svoje ekvivalentné a skrátené zápisy. Jedná sa o ifdef a ifndef. V prvom prípade sa do zdrojového kódu časť programu pridá, ak je makro nadefinované, v druhom prípade nedefinované. Program výška by išiel prepísať nasledovne:

//#define USE_CONST

#ifdef USE_CONST
  #define CONST const
#endif

Medzi ďalšie užitočná makrá možno zaradiť __DATE__ a __TIME__. Kedykoľvek preprocesor narazí na tieto makrá, nahradí je aktuálnym časom alebo dátumom.

Príklad použitia makier pre dátum a aktuálny čas:

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

Rovnakým spôsobom je možné použiť aj makrá __FILE__ a __LINE__, kedy preprocesor nahradí tieto makrá súborom a riadkom, v ktorom sa objaví.

Odporúčanie

Ako bolo spomenuté v jednej z predošlých kapitol, môže v určitých prípadoch dôjsť k zlej expanziu makra kvôli chybnému zápisu. Zároveň sú viacriadkový makrá neprehľadná a pri ladenie zdrojového kódu sú vykonaná ako jedna inštrukcie. Teda stratíte možnosť prejsť kód riadok po riadku a skontrolovať tak správnosť jednotlivých operácií. Ak by ste chceli zvýšiť bezpečnosť zdrojového kódu, uvediem niekoľko príkladov, ktoré môžu určité typy makier nahradiť.

Použitie kľúč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;

Kľúčové slovo const sa môže vyskytovať pred definíciou ľubovoľné premenné a znemožnia akokoľvek manipulovať s jej obsahom. V prípade, že sa pokúsite hodnotu zmeniť, skončí proces kompilácie chybou.

Použitie vymenované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ý prvok vymenovaného typu COMMAND sa potom správa ako konštanta. Teda s jeho hodnotou nie je možné manipulovať.

Použitie kľúčového slova inline:

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

V prípade kľúčového slova inline je všetok kód obsiahnutý vnútri funkcie vložený do miesta jej volanie. Tým odpadne sekvencie pre volanie a návrat z funkcie. Pamätajte, že inline funkcia musí byť čo najkratšia. Ak by obsahovala viac riadkov, tak v lepšom prípade kompilátor kľúčové slovo ignoruje, ale v horšom prípade spôsobí ešte väčšie zaťaženie systému, než volanie klasické funkcie.

(pozn. autora: kľúčové slovo const je súčasťou štandardu ANSI C od roku 1989, skrátene C89, a kľúčové slovo inline je súčasťou štandardu od roku 1999, skrátene C99. Implementácia týchto štandardov do kompilátorov by mala byť samozrejmosťou, ale existujú aj výnimky)

V programovacom jazyku C ++ možno použiť pre náhradu makier pokročilejšie techniky ako sú šablóny alebo lambda funkcie. To už je ale nad rámec tohto článku.

Include guard

Include guard (vo voľnom preklade "strážca vkladanie") je konštrukcia, s ktorou sa stretnete veľmi často. Uveďme si jednoduchý príklad:

#ifndef COKOLIV
#define COKOLIV

//kód

#endif

S podobnou konštrukciou sa stretnete predovšetkým u hlavičkových súborov a spravidla sa za COKOLVEK napíše meno súboru s koncovkou (napríklad pre súbor FUNKCIE.H by názov makra bol FUNKCE_H_). A čo nám také makro stráži? Pri preklade programu budeme mať istotu, že sa súbor bude includovat práve raz (ak je includován vo viacerých súboroch) a kompilátor nebude mať problém s viacnásobným definovaním rovnaké funkcie. To nám zaistí definovanie makra ihneď za podmienku, že makro nadefinované nie je.

Záver

Aj keď je odporúčané vyhnúť sa používaniu funkčných makier a makier pre definovanie konštánt, s ostatnými typmi makier sa stretnete vždy a rozhodne nie je na ich použití nič zložité. V budúcej lekcii, Pokročilé spracovanie vstupu a výstupu v jazyku C , sa budeme venovať pokročilému spracovanie vstupov a výstupov.


 

Predchádzajúci článok
Pokročilé cykly v jazyku C
Všetky články v sekcii
Pokročilé konštrukcia jazyka C
Článok pre vás napísal SPoon
Avatar
Ako sa ti páči článok?
Ešte nikto nehodnotil, buď prvý!
Aktivity

 

 

Komentáre

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.

 
Odpovedať
11.8.2014 9:44
Avatar
SPoon
Redaktor
Avatar
Odpovedá 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.

 
Odpovedať
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))
 
Odpovedať
29.4.2015 21:27
Avatar
Odpovedá 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

 
Odpovedať
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

 
Odpovedať
4.9.2019 18:25
Tento výukový obsah pomáhajú rozvíjať nasledujúce firmy, ktoré možno hľadajú práve teba!
Avatar
DarkCoder
Člen
Avatar
Odpovedá 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.

Odpovedať
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.1.2020 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? …

Odpovedať
5.1.2020 15:50
j.k.j
Avatar
DarkCoder
Člen
Avatar
Odpovedá na Nositelka Změny
DarkCoder:5.1.2020 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.

Odpovedať
5.1.2020 17:13
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
Nositelka Změny:5.1.2020 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í.

Odpovedať
5.1.2020 18:24
j.k.j
Avatar
DarkCoder
Člen
Avatar
Odpovedá na Nositelka Změny
DarkCoder:5.1.2020 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.

Odpovedať
5.1.2020 19:07
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Robíme čo je v našich silách, aby bola tunajšia diskusia čo najkvalitnejšia. Preto do nej tiež môžu prispievať len registrovaní členovia. Pre zapojenie sa do diskusie sa zaloguj. Ak ešte nemáš účet, zaregistruj sa, je to zadarmo.

Zatiaľ nikto nevložil komentár - buď prvý!