Jste zde

Programování v jazyce C - 3. Operátory a výrazy

Pokračování seriálu o výuce programování tentokrát věnované operátorům a výrazům C.

3.1 Operand, operátor, výraz.
3.2.
Rozdělení operátorů.
3.3.
Operátor přiřazení, l-hodnota a p-hodnota.
3.4.
Aritmetické operátory - aditivní a multiplikativní.
3.5.
Logické operátory.
3.6.
Relační operátory.
3.7.
Bitové operátory.
3.8.
Adresový operátor.
3.9.
Podmíněný operátor.
3.10.
Operátor čárka.
3.11.
Přetypování výrazu.

3.1. Operand, operátor, výraz.

Zapíšeme-li v matematice například a + b, hovoříme o výrazu. Ten má dva operandy a a b a jeden operátor + . Jedná se sice o výraz velmi jednoduchý, nicméně nám umožnil zopakování potřebných termínů.

3.2.Rozdělení operátorů.

Operátory rozdělujeme podle počtu operandů (arity) na operátory unární, binární a ternální. Binární operátory jsou aritmetické, relační, logické, bitové a operátory přiřazení a posuvu. Aritmetické operátory jsou aditivní a multiplikativní. Operátory mají svou prioritu a asociativitu. Priorita určuje, že například násobení se vyhodnotí dříve než třeba sčítání. Asociativita říká, vyhodnocuje-li se výraz zleva doprava, nebo naopak.

Operátory rovněž dělíme podle pozice jejich zápisu vzhledem k operandu(-ům). Takto rozlišujeme operátory prefixové, infixové a postfixové. Operátory v jednotlivých případech zapisujeme před operandy, mezi operandy, respektive za operandy. Druhá varianta je nám zřejmě nejbližší. Infixový způsob zápisu jsme používali již na základní škole.

Poznamejme, že v C uplatníme všechny zmíněné varianty operátorů. Základní přehled operátorů jazyka C, rozlišených podle arity, následuje:

Unární operátory:

+, - aritmetické plus a mínus
& reference (získání adresy objektu)
* dereference (získání objektu dle adresy)
! logická negace
~ bitová negace
++, -- inkrementace resp. dekrementace hodnoty, prefixový i postfixový zápis
(typ) přetypování na typ uvedený v závorkách
sizeof operátor pro získání délky objektu nebo typu

Binární operátory:

= přiřazení, možná je i kombinace s jinými operátory, např. +=, -=, *=, /=, <<=, ^=
+ sčítání
- odčítání
* násobení
/ dělení
% zbytek po celočíselném dělení (modulo)
<<, >> bitový posun vlevo resp. vpravo
& bitový součin (and)
| bitový součet (or)
^ bitový vylučovací součet (xor)
&& logický součin (and)
|| logický součet (or)
. tečka, přímý přístup ke členu struktury
-> nepřímý přístup ke členu struktury
, čárka, oddělení výrazů
< menší než
> větší než
<= menší nebo rovno
>= větší nebo rovno
== rovnost
!= nerovnost

Ternální operátor:

? : podmíněný operátor

Při podrobnějším pohledu na přehled operátorů podle arity záhy objevíme některé z operátorů, které jsou uvedeny jako unární i binární současně. Příkladem uveďme -, které může vystupovat jako unární mínus i jako binární operátor odčítání.

Operátory s uvedením priority (v tabulce jsou řazeny sestupně od priority nejvyšší k prioritě nejnižší) a asociativity:

operátortyp operátoruasociativita
[ ] ( ) . -> postfixové ++ postfixové -- výraz zleva doprava
prefixové ++ prefixové -- sizeof & * + - ~ ! unární logické OR
přetypování unární zprava doleva
* / % násobení zleva doprava
+ - sčítání zleva doprava
<< >> bitového posunu zleva doprava
< > <= >= relační zleva doprava
== != rovnosti zleva doprava
& bitové AND zleva doprava
^ bitové vylučovací OR (XOR) zleva doprava
| bitové OR zleva doprava
&& logické AND zleva doprava
|| logické OR zleva doprava
?: podmíněné vyhodnocení zprava doleva
= *= /= %= += -= <<= >>= &= |= ^= jednoduché přiřazení a přiřazení s výpočtem zprava doleva
, postupné vyhodnocení zleva doprava

Unární operátory jsou prefixové s možným postfixovým použítím dekrementace a inkrementace. Binární operátory jsou infixové.

Operátory jsou rovněž [], () ohraničující indexy, resp. argumenty a #, ##, které zpracovává již preprocesor. Preprocesoru v tomto textu věnujeme celou kapitolu. Užitečným operátorem je sizeof, který v průběhu překladu vyhodnotí paměťové nároky svého argumentu. Tento operátor je nezbytný zejména při dynamické alokaci paměti, případně při operacích čtení/zápis z binárních souborů.

3.3. Operátor přiřazení, l-hodnota a p-hodnota

Výrazy, jak již víme, jsou tvořeny posloupností operátorů a operandů. Výraz předepisuje výpočet adresy nebo hodnoty. Upravíme-li například známý vztah

Pytagorova veta

do syntakticky správného zápisu1 v jazyce C

c = sqrt(a*a + b*b);

můžeme zřetelně ukázat některé významné vlastnosti operátoru přiřazení =2. Na pravé straně operátoru přiřazení se nachází výraz, jehož vyhodnocením získáme hodnotu tohoto výrazu. Ovšem na levé straně se nachází výraz (v našem případě je to "jen" proměnná), jehož vyhodnocením získáme adresu. Na tuto adresu, představující začátek paměťového místa pro umístění hodnoty proměnné c, je umístěna hodnota z pravé strany přiřazovacího operátoru.

Ještě, než si zadefinujeme zmíněné pojmy, nesmíme zapomenout na důležitou skutečnost. Výsledkem výrazu3 přiřazení je hodnota. Co to znamená? Například možnost elegantně řešit inicializaci více proměnných stejnou hodnotou, například

int a, b, c;
a = b = c = -1; 

Nezapomínejme, že přiřazovací operátor je asociativní zprava doleva. Nejprve se tedy vyhodnotí c = -1 , výsledkem je hodnota -1, ta tvoří pravou stranu přiřazení b = , jehož výsledkem je opět -1. A jak se uvedená hodnota dostane do proměnné a, není jistě třeba popisovat. Vraťme se však k nastíněným pojmům.

Adresový výraz (lvalue - l-hodnota) je výraz, jehož výpočtem se získá adresa v paměti. Například je-li P nějaký výraz vyhodnocený jako nenulový ukazatel, pak *P je l-hodnota. V souvislosti s modifikátorem const rozlišujeme modifikovatelnou a nemodifikovatelnou l-hodnotu.

Hodnotový výraz (rvalue - p-hodnota) je výraz, jehož výpočtem se získá hodnota jistého typu. Typ je jednoznačně určen typem operandů. Protože se zejména při číselných výpočtech často setkáváme s operandy různých typů, uveďme si pravidla, jež určují typ výsledku. Poznamenejme, že naplnění pravidel testujeme v uvedeném pořadí:

Alespoň jeden z operandů je racionálního typu, pak

  • je-li jeden z operandů typu long double, je rovněž druhý operand konvertován na tento typ;
  • je-li jeden z operandů typu double, je rovněž druhý operand konvertován na tento typ;
  • je-li jeden z operandů typu float, je rovněž druhý operand konvertován na tento typ.

V opačném případě (operandy jsou celočíselných typů4):

  • je-li jeden z operandů typu unsigned long, je rovněž druhý operand konvertován na tento typ;
  • je-li jeden z operandů typu long, a druhý typu unsigned int, je druhý operand konvertován na typ long (16bitový překladač), nebo jsou oba operandy konvertovány na typ unsigned long (32bitový překladač);
  • je-li jeden z operandů typu long, je rovněž druhý operand konvertován na tento typ;
  • je-li jeden z operandů typu unsigned int, je rovněž druhý operand konvertován na tento typ;
  • není-li splněna žádná z předchozích podmínek, jsou oba operandy převedeny na typ int.

Výrazy mohou vyvolávat i vedlejší efekty. ANSI norma nedoporučuje používat výrazy, které během vyhodnocení způsobují vícenásobnou změnu obsahu jednoho paměťového místa. Například:

cc = cc++ + 1.

Některé vedlejší efekty mohou být implementačně závislé, třeba výraz

a[i] = i++.

Operátoru přiřazení jsme se věnovali jako prvnímu v pořadí. Důvod je prostý. Bez tohoto operátoru nemůžeme uchovat výsledky v proměnných. Současně nám tento operátor bude sloužit i v následujícím výkladu látky, včetně popisu dalších operátorů.

3.4. Aritmetické operátory - aditivní a multiplikativní.

Aritmetické operátory + - * / % představují základní matematické operace sčítání, odčítání, násobení, dělení a zbytku po (celočíselném) dělení.

Nejlepší ukázkou bude jistě příklad.

/********************************************************************/
/* program op_int01.c                                               */
/* celociselne nasobeni, deleni, zbytek po deleni                   */
/* navic je ukazano celociselne preteceni (jen pro 16-ti bitove int)*/
/********************************************************************/
#include 
int main()
{
int o1 = 123, o2 = 456, o3 = 295, v1, v2, v3;
int c1 = 20000, c2 = 20001, vc;
v1 = o1 * o2;
v2 = o3 / 2;
v3 = o3 % 2;
printf("%d * %d = %dn", o1, o2, v1);
printf("%d / %d = %dn", o3, 2, v2);
printf("%d %% %d = %dn", o3, 2, v3);
vc = c1 + c2;
printf("nnyni pozor:nt");
printf("%d + %d = %dn", c1, c2, vc);
return 0;
}

/* vystup BC31
            123 * 456 = -9448
            295 / 2 = 147
            295 % 2 = 1
            nyni pozor:
            20000 + 20001 = -25535
            */
/* vystup MWC + COHERENT
            123 * 456 = 56088
            295 / 2 = 147
            295 % 2 = 1
            nyni pozor:
            20000 + 20001 = 40001
            */ 

Příklad ukazuje nejen inicializaci hodnot proměnných "operandů" o1 o2 a o3, ale po prvních očekávaných výsledcích i neočekávané hodnoty5. Ty jsou způsobeny faktem aritmetického přetečení. Dále je vhodné zdůraznit, že vzhledem k typu operandů int je i typ výsledku stejného typu (viz pravidla uvedená dříve). Z toho důvodu je i výsledek dělění celočíselný.

Podívejme se nyní na výsledky aritmetických operací, v nichž argumenty jsou opět celočíselné, ale levá strana je racionálního typu.

/**********************************************************************/
/* program op_int_f.c                                                */
/* zakladni aritmeticke operace a prirazeni vysledky jsou sice i float*/
/* ale vypocty jsou provadeny jako int a teprve pote prevedeny        */
/**********************************************************************/
#include 
int main()
{
int i, j;
float r, x;
j = i = 5;
j *= i;
r = j / 3;
x = j * 3;
printf("i=%dtj=%dtr=%ftx=%fn", i, j, r, x);
return 0;
}
/* vystup BC31
i=5 j=25 r=8.000000 x=75.000000
*/

Rovněž v tomto příkladu vidíme, že výpočet probíhá s hodnotami typů podle zmíněných pravidel, a teprve poté je získaná p-hodnota konvertována do typu odpovídajícího l-hodnotě. Proto podíl 25/3 dává 8.0 a nikoliv 8.333 .

Stejným způsobem probíhají aritmetické operace s racionálními hodnotami.

Chceme-li změnit pořadí vyhodnocení jednotlivých částí výrazu, použijeme k tomuto "pozměnění priority" kulatých závorek6.

3.5. Logické operátory

Logické operátory představují dvě hodnoty, pravda a nepravda. ANSI norma C říká, že hodnota nepravda je představována 0 (nulou), zatímco pravda 1 (jedničkou)7. Ve druhém případě se ovšem jedná o doporučení, neboť užívaným anachronismem je považovat jakoukoliv nenulovou hodnotu za pravdu.

Logické operátory jsou && || !, postupně and or a not. Provádějí výpočet logických výrazů tvořených jejich operandy. Pravidla pro určení výsledku známe z Booleovy algebry. Logické výrazy často obsahují i stanovení (a ověření) podmínek tvořených relačními operátory.

.3.6 Relační operátory

Relační operátory jsou < > <= >= == !=. Pořadě menší, větší, menší nebo rovno, větší nebo rovno, rovno a nerovno. Jsou definovány pro operandy všech základních datových typů8. Jejich výsledkem jsou logické hodnoty pravda a nepravda tak, jak jsou popsány v předchozím odstavci.

3.7. Bitové operátory

Jak sám název napovídá, umožňují provádět operace nad jednotlivými bity. Tuto možnost zdaleka nemají všechny programovací jazyky označované jako vyšší. Jazyk C jí oplývá zejména proto, že byl vytvořen jako nástoj systémového programátora (OS Unix). Použití bitových operátorů vyžaduje znalosti o uložení bitů v paměti, způsobu kódování čísel ... .

Bitové operátory jsou: << >> & | ~ ^ , tedy posun vlevo, posun vpravo, and, or, not a xor. Bitové operace jsou možné pouze s celočíselnými hodnotami. Podívejme se nyní na jednotlivé zástupce bitových operátorů.

Při bitovém posunu vlevo (vpravo) << , ( >> ) se jednotlivé bity posouvají vlevo (vpravo), tedy do pozice s (binárně) vyšším (nižším) řádem. Na nejpravější (nejlevější) posunem vytvořenou pozici je umístěna nula. Posuny ovšem probíhají aritmeticky. To znamená, že uvedené pravidlo neplatí pro posun vpravo hodnoty celočíselného typu se znaménkem. V takovém případě se nejvyšší bit (znaménkový), zachovává. Takto se při posunu doplňuje do bitového řetězce nový bit. Naopak před posunem nejlevější (nejpravější) bit je odeslán do "říše zapomnění".

Bitový posun o jeden (binární) řád vpravo, respektive vlevo, má stejný význam jako celočíselné dělení, respektive násobení, dvěma. Je-li bitový posun o více než jeden řád, jedná se o násobení (dělění) příslušnou mocninou dvou.

Bitové and & , or |, a xor ^ provádí příslušnou binární operaci s každým párem odpovídajících si bitů. Výsledek je umístěn do pozice stejného binárního řádu výsledku. Výsledky operací nad jednotlivými bity jsou stejné jako v Booleově algebře9. Bitové not ~ je operátorem unárním, provádí negaci každého bitu v bitovém řetězci jediného operandu. Tomuto operátoru se často říká bitový doplňek.

/*******************************************************************/
/* program op_bit01.c                                              */
/* ukazuje bitove posuny, a zakladni bitove operace and, or, xor   */
/* a bitovy doplnek                                                */
/*******************************************************************/
#include 
int main()
{
printf("1 << 1 = t%dt%#xn", 1 << 1, 1 << 1);
printf("1 << 7 = t%dt%#xn", 1 << 7, 1 << 7);
printf("-1 >> 1 = t%dt%#xn", -1 >> 1, -1 >> 1);
printf("1024 >> 9 = t%dt%#xn", 1024 >> 9, 1024 >> 9);
printf("13 & 6 = t%dt%#xn", 13 & 6, 13 & 6);
printf("13 | 6 = t%dt%#xn", 13 | 6, 13 | 6);
printf("13 ^ 6 = t%dt%#xn", 13 ^ 6, 13 ^ 6);
printf("2 & 1 = t%dt%#xn", 2 & 1, 2 & 1);
printf("2 | 1 = t%dt%#xn", 2 | 1, 2 | 1);
printf("2 ^ 1 = t%dt%#xn", 2 ^ 1, 2 ^ 1);
return 0;
}
/* BC31 - 16-ti bitovy kod
1 << 1 =        2       0x2
1 << 7 =        128     0x80
-1 >> 1 =       -1      0xffff
1024 >> 9 =     2       0x2
13 & 6 =        4       0x4
13 | 6 =        15      0xf
13 ^ 6 =        11      0xb
2 & 1 =         0       0
2 | 1 =         3       0x3
2 ^ 1 =         3       0x3
*/

Jestliže jsme v úvodu k bitovým operátorům naznačili jejich systémovou orientaci, ukažme ji na příkladu. Často popisujeme rozdíly mezi 16 a 32bitovým kódem (překladačem, který kód generuje). Následující program nám umožní zjistit, s jakým překladačem máme čest.

/************************************************/
/* soubor int_size.c                            */
/* zjisti kolika bitove int pouziva prekladac   */
/************************************************/
#include 
int main(void)
{
unsigned int ui = ~0;
int i = 1;
while (ui >>= 1)
i++;
printf("prekladac pouziva %2d-ti bitovou reprezentaci celeho cislan", i);
return 0;
}

Povšimněme si použití bitového doplňku ui = ~0. Tak snadno (a zejména přenositelně) získáme bitový řetězec tvořený samými jedničkami.

3.8. Adresový operátor

Tento operátor & je unární. Jak již název adresový operátor napovídá, umožňuje získat adresu objektu, na nějž je aplikován. Adresu objektu můžeme použít v nejrůznějších situacích, obvykle je to ale v souvislosti s ukazateli. Bez tohoto operátoru bychom nebyli schopni pracovat se soubory a ani standardní vstup bychom nebyli schopni číst jinak než po znacích. Takto například můžeme přečíst hodnoty dvou proměnných jedinou funkcí pro formátovaný vstup:

int i;
float f;
scanf("%d %f", &i, &f);

3.9. Podmíněný operátor

Podmíněný operátor je poměrně nezvyklý. Proto bude vhodné, objasníme-li si jeho význam. Mějme například výpočet, který potřebujeme provést, v závislosti na nějaké podmínce, jednou ze dvou variant (pochopitelně odlišných). Výsledek výpočtu přiřazujeme vždy stejné proměnné. Pokud navíc je část výrazu popisující výpočet obou variant shodná, jedná se o typický příklad využití podmíněného výrazu.

Buďme však raději konkrétnější. Chceme-li vypočíst absolutní hodnotu nějakého čísla, použijeme velmi pravděpodobně podmíněný operátor. Výpis zdrojového textu takového výpočtu následuje:

/**********************************/
/* soubor op_cond.c               */
/* ukazuje pouziti podmineneho    */
/* operatoru - absolutni hodnota  */
/**********************************/
#include 
int main(void)
{
int i, abs_i;
printf("nZadej cele cislo: ");
scanf("%d", &i);
abs_i = (i < 0) ? -i : i;
printf("abs(%d) = %dn", i, abs_i);
return 0;
}

Další použití (ternálního) podmíněného operátoru ukazuje následující řádek.

return((znak == 0x0) ? getch() + 0x100 : znak);

Tento řádek zajišťuje případné načtení a překódování libovolné stisknuté klávesy na tzv. rozšířené klávesnici IBM PC AT. Základní klávesy této klávesnice produkují kód odpovídající jejich pozici v ASCII tabulce. Rozšířené klávesy produkují dvojici celočíselných kódů, z nichž první je nula. Než si popíšeme jeho činnost, doplňme i předcházející příkaz pro načtení znaku:

znak = getch();
return((znak == 0x0) ? getch() + 0x100 : znak); 

Pomocí podmínky (znak == 0x0) otestujeme, jednalo-li se o rozšířenou klávesu. Jestliže ano, je proveden první příkaz následující za podmíněným operátorem, getch() + 0x100 . Je jím načten tzv. scan kód rozšířené klávesy, který je dále zvětšen o hodnotu 100hex. Jestliže podmínka splněna nebyla, je vykonán příkaz za dvojtečkou. Návratovou hodnotou je pak neupravená hodnota načtená do proměnné znak těsně před ternálním operátorem.

3.10. Operátor čárka

Čárka je operátorem postupného vyhodnocení. Má nejnižšší prioritu ze všech operátorů a vyhodnocuje se zleva doprava10. Čárkou můžeme oddělit jednotlivé výrazy v místě, kde je očekáván jediný výraz, například v cyklu for.

3.11. Přetypování výrazu

Jazyk C nám umožňuje přetypovat výrazy podle naší potřeby. Přetypování má přirozeně svá omezení, ta však často plně odpovídají zdravému rozumu. Těžko se například vyskytne potřeba přetypovat znak na typ ukazatel na double.

Úvodní úvaha postrádá konkrétní ukázku, z níž by vyplýval syntaktický zápis přetypování. Připomeňme si příklad op_int_f.c, v němž se vyskytoval příkaz

r = j / 3;

který byl ovšem vyhodnocen celočíselně a teprve poté konvertován na float. Chceme-li, aby již podíl proběhl v racionálním oboru, musíme pravou stranu upravit. S přetypováním může vypadat pravá strana takto:

r = (float) j / 3;

Přetypování provádíme tak, že před hodnotu, kterou chceme přetypovat, napíšeme typ, který chceme získat, v kulatých závorkách. Syntakticky tedy zapíšeme přetypování takto:

(type) expression

Výsvětlivky:

1 Předpokládejme, že použité proměnné jsou vhodného typu, například double..
2 Nesmíme se nechat svést jinými programovacími jazyky. Jedná se o jeden z nejčastěji se vyskytujících operátorů, proto mu náleží zápis jediným symbolem.
3 Podrobněji se výrazům budeme věnovat v kapitole 6. Řízení chodu programu. Zatím tolik, že příkaz je výraz zakončený středníkem.
4 Celočíselné typy jsou jak všechny varianty typu int, tak za celočíselný typ považujeme typ char. Jak jsme v předchozí kapitole viděli, můžeme i u měj použít modifikátory unsigned a signed. Ve výrazech je konvertován na typ int plus případné znaménko.
5 V případě 16bitového překladače. 32bitový dává správné výsledky.
6 Je-li potřeba více úrovní závorek, používáme vždy jen závorek kulatých. Hranaté a složené závorky představují operátory jazyka C s jiným významem, než je stanovení pořadí vyhodnocení.
7 Jedná se o celočíselné hodnoty.
8 Ovšem například pro ukazatele mají smysl pouze operátory rovnosti a nerovnosti.
9 Připomeňme alespoň, že xor dává výsledek 1 jen v případě různosti hodnot operandů.
10 Tento operátor zajišťuje pořadí vyhodnocení zleva doprava. Výsledkem je hodnota nejpravějšího výrazu ze seznamu výrazů oddělených čárkami.

RNDr. Petr Šaloun, Ph.D.
petr.saloun@ vsb.cz

DOWNLOAD & Odkazy

Hodnocení článku: