NOVINKA – Víkendový online kurz Software tester, který tě posune dál. Zjisti, jak na to!
NOVINKA - Online rekvalifikační kurz Java programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.

Lekce 4 - Makra v 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.

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.

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ů.


 

Předchozí článek
Pokročilé cykly v jazyce C
Všechny články v sekci
Pokročilé konstrukce jazyka C
Přeskočit článek
(nedoporučujeme)
Pokročilé zpracování vstupu a výstupu v jazyce C
Článek pro vás napsal SPoon
Avatar
Uživatelské hodnocení:
11 hlasů
Aktivity