V prosincové lekci programování v jazyce C++ se budeme věnovat problematice priority a asociativity operátorů. Poté zahájíme výk

1. 12. 2008

Sdílet

Priorita a asociativita operátorů Všechny operátory jazyka C++ mají svou prioritu. Priorita říká, v jakém poř...


Priorita a asociativita operátorů

Všechny operátory jazyka C++ mají svou prioritu. Priorita říká, v jakém pořadí budou realizovány programové operace, které jsou předepsané jednotlivými operátory. Kdyby nebyla stanovena přesná prioritní pravidla, tak by kompilátor nedovedl správně vyhodnocovat programové výrazy. Kupříkladu výraz a * b – c bude kompilátor vyhodnocovat jako (a * b) – c, poněvadž ze zapsaných aritmetických operátorů má operátor součinu (*) vyšší prioritu než operátor rozdílu (–). Jistě, zmíněný aritmetický výraz je triviální, neboť nám dovoluje přímo aplikovat pravidla, která známe z matematiky. Ovšem věděli bychom, jak postupovat, kdyby bylo znění výrazu takovéto: a * b % c? Modulo, neboli operátor zbytku po celočíselném dělení (%) vypočte zbytek po celočíselném dělení svých operandů, a ten vrátí v podobě své návratové hodnoty. Otázkou ale zůstává, zda bude překladač vyhodnocovat výraz a *  b % c
jako (a * b) % c, nebo jako a * (b % c). Za předpokladu, že je výraz zapsán korektně, tak kompilátor musí vždy vědět, jak určit jeho hodnotu. A to i za těch okolností, kdy se bude potýkat s opravdu komplikovanými výrazy.
Kompilátor se při své práci nejprve řídí prioritou operátorů. Podle priority smíme operátory jazyka C++ klasifikovat do homogenních prioritních tříd, přičemž v každé prioritní třídě se nacházejí operátory se stejnou prioritou. Například binární aritmetické operátory *, / a % sdílejí stejnou prioritní třídu (označme ji pro lepší orientaci identifikátorem PT1), což znamená, že disponují totožnou prioritou. Další binární aritmetické operátory + a – jsou umístěny v jiné prioritní třídě (PT2) a opět sdílejí stejnou prioritu. Prioritní třídy vytvářejí komplexní hierarchii. Přitom platí, že priorita operátorů patřících do jednotlivých prioritních tříd klesá ve směru shora dolů. Omezíme-li se na prioritní třídy PT1 a PT2, můžeme konstatovat, že prioritní třída PT1 disponuje v celkové hierarchii vyšší prioritou než prioritní třída PT2. Z toho samozřejmě plyne, že binární aritmetické operátory *, / a % mají přednost před operátory + a –. Hierarchii prioritních tříd vybraných operátorů jazyka C++ uvádíme v tabulce.
Někdy se stává, že prioritní pravidla pro jednoznačné vyhodnocení výrazů nestačí. To se děje tehdy, sejdou-li se v jednom výrazu (nebo podvýrazu) operátory se stejnou prioritou (čili operátory z totožné prioritní třídy). Jak vidíme, v tomto kontextu jsou kompilátoru pravidla pro stanovení priority k ničemu, neboť pomocí nich nedovede učinit kvalifikované rozhodnutí. Proto potřebujeme další mechanizmus, jenž bude schopen vzniklou nejednoznačnost odstranit. Řešení jsou asociativní pravidla, která nařizují, v jakém pořadí budou prováděny operace, předepsané operátory se stejnou prioritou. Asociativita je vlastnost operátorů, podle níž je kompilátor schopen určit, zda se operátor váže (čili asociuje) s levým nebo pravým operandem. Pořadí dané asociativitou operátorů bude vždy dáno dvěma směry: buď zleva doprava, anebo zprava doleva. Pro lepší názornost použijeme směrové šipky: ® pro operátory asociativní zleva doprava a ¬ pro operátory asociativní zprava doleva. Třeba všechny aritmetické operátory, které jsme uvedli výše, jsou asociativní zleva doprava (®). Vraťme se nyní k vyhodnocení výrazu a * b % c. Vzhledem k tomu, že operátory * a % mají stejnou prioritu, musí si kompilátor pomoci jejich asociativitou. Ta má orientaci ®, což znamená, že výraz bude vyhodnocen jako (a * b) % c. Pokud předpokládáme následující inicializaci proměnných: a = 50, b = 2 a c = 4, tak hodnotou výrazu bude 0 (100?% 4 = 0).
Ne všechny operátory jsou však asociativní ®, nemálo z nich má přesně opačnou asociativitu. Jedním z takových operátorů je nám velice dobře známý přiřazovací operátor (=). Asociativitu operátoru = jsme objevili již dávno a implicitně jsme s ní pracovali, aniž bychom ji nazvali příslušným odborným termínem. Jednoduše: v příkazu P = V;, kde P je proměnná zvoleného datového typu a V je výraz, bude vždy nejprve vyhodnocen výraz V, až poté bude hodnota výrazu V přiřazena do proměnné P. Podotkněme, že v závislosti na datovém typu proměnné a datovém typu hodnoty výrazu mohou (ovšem nemusí) být zpracovány implicitní typové konverze, jejichž smyslem je odstranění potenciální nekompatibility datových typů entit po obou stranách přiřazovacího operátoru. Rozvineme-li úvahy o asociativitě operátoru =, pak okamžitě vyřešíme i otázku vícenásobného přiřazení. Příkaz m = n = o = p; bude vyhodnocen jako m = (n = (o = p));.
Není žádných pochyb o tom, že priorita a asociativita operátorů stanovují exaktní pravidla pro realizaci programových operací daných aplikovanými operátory. Nicméně musíme dodat, že to nemusí vždy implikovat rovněž pořadí, v jakém jsou vyhodnocovány jednotlivé operandy.

Ve výrazu a * b však může být nejprve vyhodnocen operand a, následován operandem b, ovšem stejně tak to může být i opačně. Přesné pořadí vyhodnocování operandů je v kompetenci kompilátoru. Ten se obvykle řídí optimalizačními kritérií, tedy snahou o co možná nejefektivnější provádění zdrojového kódu. Skutečnost, že pořadí vyhodnocení operandů ve výrazech není pevně dáno, reflektuje následující fragment zdrojového kódu jazyka C++:
Předem vás prosíme, abyste takovýto zdrojový kód nikdy nepsali. Představený výpis je spíše ukázkou praktik, jich bychom se měli vyvarovat. Kritické místo je definiční inicializace proměnné y a přesněji pak styl, jakým kompilátor vyhodnotí inicializační výraz (--x) * (--x). Spustíme-li program na kompilátorech Microsoft Visual C++ 2008 Express a Borland Turbo C++ 2006 Explorer, získáme výsledek 9. To nás přivádí k myšlence, že hodnota proměnné x je dvakrát dekrementována předtím, než je užita v multiplikativní operaci. Na druhou stranu, překladač Intel C++ Compiler 9.0 hlásí výstup 12, čímž dává najevo, že jeho průběh vyhodnocení je jiný.

Rozhodovací příkazy

S rozhodovacími problémy se v běžném životě setkáváme velice často. Přijetí jistého rozhodnutí se odvíjí od charakteru rozhodovacího problému, vstupních dat a potenciálních alternativ, jimiž se může naše rozhodování ubírat. Proces rozhodování lze graficky znázornit pomocí rozhodovacího stromu. Na obrázku je znázorněn vývojový diagram s rozhodovacím stromem, který řeší rozhodování o koupi nového auta.
Programovací jazyk C++ obsahuje několik rozhodovacích příkazů, jejichž pomocí mohou programátoři realizovat větvení toku programu. Jde o následující příkazy:


Příkaz if pro realizaci jednocestného rozhodování.

Příkaz if-else pro realizaci dvojcestného rozhodování.

Příkaz if-else if… pro realizaci vícecestného rozhodování.

Příkaz if-else if-else pro realizaci vícecestného rozhodování.

Příkaz switch pro realizaci vícecestného rozhodování.

Ternární operátor pro realizaci dvojcestného rozhodování.

Všechny zmíněné příkazy blíže charakterizujeme v následujících kapitolách tohoto programátorského kurzu.

Rozhodovací příkaz if
Příkaz if umožňuje větvení toku programu podle jednostranné podmínky. Jeho generický model vypadá takto:
if(PV)
P1;
Nebo:

if(PV)
{
P1; P2; … Pn;
}

kde:

PV je podmínkový výraz, jehož hodnotou bude buď logická pravda (true), nebo logická nepravda (false).

P1, P2, …, Pn jsou příkazy jazyka C++, jež budou zpracovány pokaždé, když bude hodnotou PV logická pravda.

Rozhodování pomocí příkazu if se uskutečňuje takto:
1.
Vyhodnotí se PV.
2.
Jestliže je hodnotou PV logická pravda, tak se zpracují příkazy umístěné v těle větve if. Pokud je v těle větve if situován právě jeden příkaz, není nutno explicitně vyznačovat její tělo pomocí složených závorek. Naopak, nacházejí-li se v těle větve if alespoň dva příkazy, musí být její tělo explicitně vyznačeno složenými závorkami.
3.
Jestliže je hodnotou PV logická nepravda, rozhodování končí. Dochází k opuštění rozhodovacího příkazu if a ke zpracování nejbližšího možného příkazu, jenž následuje za rozhodovacím příkazem if.

Vizualizaci jednocestného rozhodování pomocí příkazu if můžeme vidět na dalším obrázku. Jednocestné rozhodování nám sdělí, zda přirozené číslo, které zadal uživatel na vstupu, je sudé:
Tento program nemá žádná kritická místa, jenom si musíme uvědomit, že sudé přirozené číslo je každé číslo, jehož zbytek po dělení dvojkou je nulový.

Rozhodovací příkaz if-else
Rozhodovací příkaz if-else uskutečňuje řízení toku programu podle oboustranné podmínky. Jeho generický model je následující:

if(PV)
P1;
else
P2;

Nebo:

if(PV)
{
P1; P2; … Pn;
}
else
{
R1; R2; … Rn;
}

kde:

PV je výraz, jehož logickou hodnotu lze určit.

P1, P2, …, Pn jsou příkazy, které budou provedeny tehdy, bude-li hodnotou PV logická pravda.

R1, R2, …, Rn jsou příkazy, jež budou zpracovány v případě, když bude hodnotou PV logická nepravda.

Rozhodování pomocí příkazu if-else probíhá takto:
1. Vyhodnotí se výraz PV.
2.
Je-li hodnotou PV logická pravda, provedou se všechny příkazy v těle větve if. Po zpracování všech příkazů rozhodování končí. To znamená, že exekuce programu pokračuje dalším příkazem, jenž následuje za rozhodovacím příkazem if-else.
3.
Je-li hodnotou PV logická nepravda, vykonají se všechny příkazy ve větvi else. Jakmile se provede i poslední příkaz této větve, rozhodování končí a běh programu pokračuje zpracováním nejbližšího příkazu.

Vývojový diagram demonstrující dvojcestné rozhodování pomocí příkazu if-else najdete na dalším obrázku.
Dvojcestné rozhodování nám poslouží kupříkladu při házení virtuální hrací kostkou.
Komentář ke kódu: Házení virtuální hrací kostkou je ve skutečnosti simulací generování pseudonáhodných celých čísel z intervalu <1, 6>.
Generovaná čísla jsou pseudonáhodná (nikoliv skutečně náhodná), protože jejich distribuci lze z hlediska statistiky považovat za dostatečně stochastickou. Pseudonáhodná čísla nám na požádání poskytne generátor pseudonáhodných čísel, ten však musíme před získáním prvního pseudonáhodného čísla náležitě inicializovat. To se děje voláním funkce srand. Jelikož budeme chtít při každém spuštění programu získat jiné pseudonáhodné číslo, rozhodli jsme se inicializovat generátor pseudonáhodných čísel startovní hodnotou, jež odpovídá aktuálnímu systémovému času. Tento čas nám poskytne funkce time, kterou zavoláme s nulovým argumentem. Je-li generátor pseudonáhodných čísel řádně inicializován, můžeme na něj směrovat požadavky na vygenerování pseudonáhodných čísel. Na syntaktické úrovni je požadavek „přál bych si vygenerovat jedno pseudonáhodné číslo“ vyjádřen voláním funkce rand. Jak si můžeme všimnout, tato funkce je bezparametrická, čili za jejím identifikátorem se nacházejí prázdné kulaté závorky.
Rozhodovací příkaz if-else if…
Příkaz if-else if uskutečňuje vícecestné rozhodování. Jeho generický model vypadá takto:

if(PV1)
{
P1; P2; … Pn;
}
else if(PV2)
{
R1; R2; … Rn;
}
else if(PV3)
{
S1; S2; … Sn;
}

kde:

PV1, PV2, PV3 jsou výrazy s logickými hodnotami.

P1, P2, …, Pn jsou příkazy, jež budou zpracovány, když hodnotou PV1 bude logická pravda.

R1, R2, …, Rn jsou příkazy, které budou zpracovány, když hodnotou PV1 bude logická nepravda a hodnotou PV2 bude logická pravda.

S1, S2, …, Sn jsou příkazy, které budou zpracovány, když hodnotou výrazů PV1 a PV2 bude logická nepravda a hodnotou výrazu PV3 bude logická pravda.

Rozhodování v příkazu if-else if… se provádí takto:

1.
Vyhodnotí se výraz PV1. Je-li jeho hodnotou logická pravda, provedou se všechny příkazy v těle větve if. Poté rozhodování končí a běh programu pokračuje zpracováním dalšího příkazu za rozhodovacím příkazem.
2.
Je-li hodnotou PV1 logická nepravda, řízení se přesouvá na 1. větev else if a testuje se PV2. Pokud je hodnotou PV2 logická pravda, vykonají se příkazy v těle 1. větve else if. Vzápětí dochází k opuštění rozhodovacího příkazu.
3.
Jestliže je hodnotou PV2 logická nepravda, rozhodování se přesouvá na 2. větev else if a dochází k testování PV3. V případě, že je hodnotou PV3 logická pravda, všechny příkazy v těle 2. větve else if budou provedeny. Poté rozhodování končí.
4.
Je-li hodnotou PV3 logická nepravda, rozhodování končí. Dochází k opuštění rozhodovacího příkazu if-else if…, přičemž zpracován bude nejbližší možný programový příkaz. 8 0717/CZ o

Grafickou ilustraci vícecestného rozhodování v příkazu if-else if… přibližuje obrázek.

Když zavoláme funkci rand ve tvaru rand(), získáme pseudonáhodné číslo z intervalu <0, RAND_MAX>, kde RAND_MAX je symbolická konstanta, jejíž hodnota je 215 – 1 (32767). Přestože víme, že obdržíme pseudonáhodné číslo z intervalu <0, 32767>, nedovedeme předpovědět, jaké číslo to doopravdy bude. Možná 3, 57, 116, anebo snad 12001? Jak vidíme, možností je ohromný počet, ovšem pro potřeby naší praktické ukázky budeme chtít pouze pseudonáhodná čísla z intervalu <1, 6>. Otázkou je, jak se k nim dopracujeme. V programování existuje několik postupů řešení nastíněného problému, my se seznámíme s tím, jež používá operátor %. Když na návratovou hodnotu funkce rand aplikujeme operátor % ve výrazu rand() % x, získáme z původně vygenerovaného pseudonáhodného celého čísla číslo z intervalu <0, x – 1>. Konkrétně hodnotou výrazu rand() % 6 bude číslo z intervalu <0, 5>. I když jsme udělali pozoruhodný krok kupředu, stále nejsme u konce, poněvadž naším cílem je číslo z intervalu <1, 6> a ne <0, 5>. Proto k číslu připočítáváme 1, čímž dosahujeme stanoveného cíle. Zbytek zdrojového kódu je již přirozený: v rozhodovacím příkazu if-else testujeme, zda padla šestka či nikoliv. Jestliže ano, blahopřejeme uživateli k úspěšnému pokusu.
Z akademických zkušeností plyne, že studenti najdou v problematice generování a následného zpracování pseudonáhodných celých čísel zalíbení. Proto připojujeme jednu fintu, jak získat pseudonáhodné číslo z předem daného intervalu:

int nahodneCislo =
rand() % (b – a + 1) + a;

V proměnné nahodneCislo bude uloženo číslo z intervalu <a, b>. #include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;

int main()
{
int kostka;
srand((unsigned int)time(0));
kostka = rand() % 6 + 1;
cout << "Na kostce padlo "
<< "cislo " << kostka
<< "." << endl;
if(kostka == 6)
{
cout << "Mate stesti, "
<< "padla sestka.";
}
else
{
cout << "Bohuzel,sestka"
<< " nepadla.";
}
return 0;
}Výjimku tvoří operátory && (operátor logické konjunkce), || (operátor logické disjunkce), ?: (ternární operátor) a , (operátor čárka, resp. operátor postupného vyhodnocování). Z výjimečných operátorů se budeme v tuto chvíli věnovat pouze prvním dvěma: && a ||. Oba logické operátory realizují zkrácené vyhodnocování logických výrazů, takže je nezbytně nutné, aby byl jako první vždy vyhodnocen levý (první) operand.
Pokud si nejsme jisti, jakou prioritu a asociativitu mají operátory v zapsaném výrazu, můžeme si pomoci uzávorkováním těch častí výrazu (čili podvýrazů), které mají být provedeny před ostatními. Vložíme-li podvýraz do závorek, zvyšujeme preferenci jeho zpracování. S prioritou tak ovlivňujeme rovněž asociativitu operátorů. Jenom prosím nezapomínejme na skutečnost, že v kterémkoliv výrazu musí být sudý počet závorek.

Autor článku