IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

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

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
Preskočiť článok
(neodporúčame)
Pokročilé spracovanie vstupu a výstupu v jazyku C
Článok pre vás napísal SPoon
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Aktivity