# Základní prvky jazyka C Tato kapitola se bude zabývat základními stavebními kameny jazyka C – hodnotami, proměnnými, výrazy a příkazy. U každé výpočetní konstrukce si zejména ukážeme, jak se abstraktní zápis na úrovni jazyka C realizuje sekvencemi instrukcí konkrétního výpočetního stroje. ## Hodnoty, objekty, proměnné V této části se budeme zabývat základními «datovými» prvky jazyka C – připomeneme si pojmy jako hodnota, objekt, proměnná nebo typ, které již znáte z jazyka Python, a zasadíme je do nového kontextu. ### Hodnota │ • význam podobný Pythou │ • celé číslo │ • později složitější │ • 12 ~ XII ~ (1100)₂ ~ ‹0xc› Stejně jako v jazyce Python, fundamentálním předmětem výpočtu je v jazyce C «hodnota» – pro tuto chvíli se bude jednat o celé číslo v nějakém pevném rozsahu, nicméně v pozdějších kapitolách se setkáme i se složitějšími hodnotami. Pozor, hodnota je «abstraktní» – hodnota «není» totéž co «reprezentace». Zejména nesmíme hodnotám přisuzovat vlastnosti použité reprezentace. Zápisy ‹0x10›, 16, šestnáct, XVI všechny reprezentují XXX tutéž XXX hodnotu. ¹ Cf. Platón. ### Operace │ • pracuje s hodnotami │ • hodnoty je potřeba si pamatovat │ • realizace výpočetním strojem │ • příklad: součet dvou hodnot Konceptuálně není výpočet ničím jiným, než mechanickou manipulací hodnot použitím kompatibilních «operací». Klasickým příkladem operace je např. sčítání – vstupem jsou dvě celočíselné hodnoty a výstupem je nová hodnota. Aby bylo možné výpočet provést, je nutné si potřebné hodnoty «pamatovat» – při výpočtech ve středoškolské matematice k tomu používáme typicky papír a tužku. Počítač k tomu bude samozřejmě využívat nějaký typ elektronické «paměti». ### Objekt │ • abstrakce (zobecnění) paměťové buňky │ • pamatuje si «hodnotu» │ • má identitu │ ◦ různost při stejné hodnotě │ ◦ stejnost během výpočtu Přímá práce s pamětí a registry je pro větší programy značně nepohodlná – musíme při programování neustále pamatovat, kde máme uloženy které hodnoty (ve kterém registru nebo na jaké adrese), navíc jména registrů nejsou příliš popisná a je jich omezený počet. Z jazyka Python jsme zvyklí hodnoty uchovávat v «proměnných». Vazba mezi proměnnou a hodnotou ale není přímá – ani v Pythonu, ani v C. Hodnoty jsou totiž invariantní, anonymní entity – nemají identitu, ani schopnost se vnitřně měnit. Abychom obdrželi sémantiku, na kterou jsme při programování intuitivně zvyklí, musí do hry vstoupit ještě jeden prvek – «objekt». Hlavními vlastnostmi objektu jsou: • schopnost uchovávat, číst a měnit «hodnotu» (abstraktní entitu programovacího jazyka), • identita: ◦ můžeme od sebe rozlišit různé objekty i v případě, že zrovna obsahují stejnou hodnotu, ◦ jsme schopni určit, že se jedná o tentýž objekt, i když se hodnota v něm uložená během výpočtu změnila. Objekt je tak zobecněním paměťové buňky – má operace „přečti“ a „ulož“ a jeho identita je obdobou adresy. ### Proměnná │ • vazba jména na objekt │ • syntaktický rozsah platnosti │ • vazba je neměnná (pevná) │ • platnost jména ~ živost objektu Objekty mají sice identitu, ale nemají «jména» – jejich identita je více nebo méně abstraktní.¹ Abychom tedy mohli s objektem pracovat, potřebujeme mu přiřadit jméno – a to je přesně úloha «proměnné». Proměnná je (v jazyce C) pojmenovaný objekt, přičemž její jméno (identifikátor) má «syntakticky» omezený «rozsah platnosti»² – přímo ze zdrojového kódu umíme lehce identifikovat, ve kterých příkazech a výrazech je použití tohoto jména přípustné, a případně ke které deklaraci se váže. Vazba mezi jménem (proměnnou) a objektem je «pevná» – vznikne při deklaraci proměnné a až do jejího zániku tuto vazbu není lze měnit.³ ### Typ │ • je vlastnost hodnoty │ • určuje přípustné operace │ • určuje chování operací │ • pouze v době překladu Různé hodnoty mají různé vlastnosti a různé operace. Uvažme 16bitové hodnoty bez znaménka ⟦u₁ = 3, u₂ = 5⟧ a podobné (ale ne tytéž!) 16bitové hodnoty se znaménkem ⟦s₁ = 3, s₂ = 5⟧. Zřejmě: • ⟦s₁ + s₂ = 8⟧, podobně ⟦u₁ + u₂ = 8⟧, • ⟦s₂ - s₁ = -2⟧ ale ⟦u₂ - u₁ = 65534⟧. Je tedy potřeba podobné hodnoty rozlišovat – k tomu slouží «typy». Typy mají v programovacím jazyce dvě funkce: 1. určí, jaké operace jsou pro dané hodnoty přípustné, 2. je-li operace přípustná, typy mohou ovlivnit její «význam» – např. existují dvě různé operace odečítání (viz výše) pro 16bitová čísla: znaménkové a neznaménkové. Typ je «vlastnost hodnoty», ale to neznamená, že je ke každé hodnotě „fyzicky“ připojen její typ – řada programovacích jazyků, a C mezi nimi, typovou informaci uchovává pouze v «době překladu» – ve strojovém kódu bychom informaci o typech hledali marně.⁴ To samozřejmě neznamená, že typy nemají pro výsledný strojový kód důsledky – bude na nich třeba záviset, jestli se pro srovnání použije operace ‹slt› nebo ‹ult›, atp. Typy můžeme přisuzovat krom hodnot také objektům a skrze objekty také proměnným. Je-li objekt nějakého typu, znamená to, že je schopen uchovávat hodnoty pouze tohoto typu. Je-li proměnná nějakého typu, znamená to, že je svázána s objektem tohoto typu.⁵ ### Deklarace │ • proti Pythonu nový prvek │ • ‹typ jméno = výraz;› │ • vytvoří zároveň objekt i vazbu │ • bez inicializace = zapovězená hodnota (jak vznikne proměnná, objekt, …) ¹ V standardní implementaci jazyka Python je každý objekt identifikovatelný adresou, na které je v paměti uložen. V jazyce C to ale neplatí, protože objekt nemusí mít adresu žádnou, nebo se jeho adresa může během výpočtu měnit. ² Známý též jako «lexikální» nebo «statický», v kontrastu s «dynamickým». ³ Zde se objevuje důležitý rozdíl mezi C a Pythonem. Přiřazení v C, jak za chvíli uvidíme, značí «zápis do objektu», kdežto v jazyce Python značí změnu vazby na «jiný objekt». ⁴ Tomuto konceptu se říká „vymazání typů“, angl. „type erasure“ – udržování informací o typech za běhu programu předstauje dodatečnou režii a je-li to možné, překladače se tomu vyhýbají. ⁵ Polymorfismus – schopnost objektu uchovávat různé typy hodnot – lze chápat např. tak, že takovému objektu přisoudíme součtový typ. V jazycích, kde je možné měnit vazbu mezi proměnnou a objektem může polymorfismus existovat jak na úrovni objektu (lze uložit různé typy hodnot) tak na úrovni proměnné (k jednomu jménu lze vázat různé typy objektů). To se ale nacházíme už mimo hranice tohoto předmětu. ## Výrazy Nyní známe základní datové (pasivní) prvky jazyka a můžeme se začít zabývat výpočetními (aktivními) – těmi nejzákladnějšími jsou «výrazy». (XXX denotační + operační sémantika) ### Elementární výrazy │ • název proměnné: ‹x› (typ dle proměnné) │ • číselný literál: │ ◦ typu ‹int›: ‹3›, ‹-1›, ‹0x1f› │ ◦ typu ‹unsigned›: ‹3u›, ‹0x1fu› (jméno proměnné, konstanta) Pozor – číselný literál musí být platnou hodnotou příslušného typu: je-li typ ‹int› 16bitový, literál ‹0xffff› je chybný (tak velké číslo není možné reprezentovat). Naproti tomu, protože ‹0xffffu› je typu ‹unsigned›, je zde vše v pořádku. ### Aritmetické a logické operace │ • popisují hodnotu, žádný vedlejší efekt │ • ‹e₁ + e₂›, …, ‹e₁ % e₂› │ • ‹e₁ << e₂›, ‹e₁ >> e₂› │ • ‹e₁ & e₂›, ‹e₁ | e₂›, ‹e₁ ^ e₂› │ • ‹-e₁›, ‹~e₁›, ‹!e₁› (operátory bez vedlejších efektů) ### Výpočet hodnoty výrazu │ • vyhodnocení do registru R │ • ‹var› ~ ‹copy A → R› │ • ‹e₁ + e₂›, …, ‹e₁ ^ e₂› │ ◦ vyhodnoť ‹e₁› do ‹t₁› │ ◦ vyhodnoť ‹e₂› do ‹t₂› │ ◦ ‹add t₁, t₂ → R› (strojový kód; „vyhodnocení do registru“) ### Kontrola typů │ • ověří spávnost operace × hodnoty │ • vkládá implicitní konverze │ ◦ přesná pravidla jsou složitá │ • špatně utvořený program zamítne (je výraz typově správný?) ### Implicitní konverze │ 1. povýšení – vše menší než ‹int› │ ◦ vejde se do ‹int› → ‹int› │ ◦ jinak ‹unsigned› │ 2. stejná znaménkovost → pouze zvětšení │ 3. různá vede na: │ a. znaménkový je-li striktně větší │ b. jinak na neznaménkový Aritmetické, bitové, atp. operace vyžadují, aby byly operandy stejného typu. Jazyk C zároveň nepodporuje operace na typech menších než ‹int› resp. ‹unsigned› (pro nás to znamená, že „jednobajtová“ aritmetika neexistuje – všimněte si, že podobně neexistuje ani ve výpočetním stroji). Povýšení: typy s rozsahem, který je menší než rozsah typu ‹int›, se nejprve zvětší na ‹int›.¹ Po povýšení se pak najde společný typ – je-li to možné, preferuje se znaménkový typ.² «Pozor»: povýšení se dotkne i unárních operátorů. Máme-li ‹signed char x = 5;›, bude hodnota výrazu ‹-x› typu ‹int›, nikoliv typu ‹signed char›! V naší omezené verzi jazyka to dopadne takto (všechny případy jsou symetrické): │ operand │ operand │ společný typ │ ├─────────────────│─────────────────│──────────────┤ │ ‹signed char› │ ‹signed char› │ ‹int› │ │ │ ‹unsigned char› │ ‹int› │ │ │ ‹int› │ ‹int› │ │ │ ‹unsigned› │ ‹unsigned› ! │ │┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄│ │ ‹unsigned char› │ ‹unsigned char› │ ‹int› │ │ │ ‹int› │ ‹int› │ │ │ ‹unsigned› │ ‹unsigned› │ │┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄│ │ ‹int› │ ‹int› │ ‹int› │ │ │ ‹unsigned› │ ‹unsigned› │ │┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄│ │ ‹unsigned› │ ‹unsigned› │ ‹unsigned› │ «Pozor»: na řádcích označených ! dochází ke konverzi operandu s menším rozsahem ve dvou krocích. Nejprve je rozšířen na typ ‹int› a poté až na společný ‹unsigned›. Zejména to znamená, že jednobajtové hodnoty jsou převedeny «znaménkovým rozšířením». Např.: signed char x = -3; /* 0xfd */ unsigned y = 1; /* 0x0001 */ assert( x + y == 65534u ); /* 0xfffe */ Součet je 65534 (‹0xfffe›), nikoliv 254 (‹0x00fe›) jak by mohl někdo čekat. ¹ Rozsahy všech jednobajtových typů (‹char›, ‹signed char›, ‹unsigned char›) jsou menší než rozsah typu ‹int›. Rozsah typu ‹int› není větší než rozsah typu ‹unsigned›, protože např. 40000 není přípustná hodnota typu ‹int› ale je to přípustná hodnota typu ‹unsigned›. ² V našem jazyce žádné znaménkové typy větší než ‹int› nejsou, proto se toto pravidlo neuplatní – společný typ je ‹unsigned› je-li alespoň jeden operand ‹unsigned›, jinak je to vždy ‹int›. ### Přiřazení │ • ‹var = e₁› (pozor na levou stranu) │ • proběhne typová konverze pravé strany │ • výraz s «vedlejším efektem» │ • provede zápis do objektu │ • «hodnota» je to, co bylo zapsáno Prozatím budeme uvažovat pouze přiřazení tvaru ‹var = e₁› – levá strana může být tedy pouze název proměnné. Pozor, vyhodnocení se bude pro složitější formy lišit! Vyhodnocení tohoto typu přiřazení probíhá takto: 1. vyhodnotí se pravá strana, 2. výsledek se převede (konvertuje) na typ levé strany, ◦ má-li typ levé strany větší nebo stejný rozsah, probíhá stejně jako konverze operandů aritmetických operátorů, ◦ je-li levý operand menší a je neznaménkového typu, vyšší bity se implicitně oříznou, ◦ je-li levý operand menší a je znaménkového typu, výsledek závisí na implementaci – program «může» spadnout, ale «nemá» nedefinované chování, 3. převedená hodnota se zapíše do objektu určeného levou stranou, 4. výsledná hodnota (přiřazení je výraz) je ta, která byla zapsaná (tzn. hodnota po konverzi z druhého bodu). ### Booleovské operace │ • binární ‹e₁ && e₂›, ‹e₁ || e₂› │ • ternární ‹e₁ ? e₂ : e₃› (řízení toku) ### Vyhodnocení booleovských operací │ • vyhodnocení ‹e₁ && e₂› do R: │ ◦ vyhodnoť ‹e₁› do R │ ◦ je-li R true, vyhodnoť ‹e₂› do R │ • vyhodnocení ‹e₁ || e₂› do R: │ ◦ vyhodnoť ‹e₁› do R │ ◦ je-li R false, vyhodnoť ‹e₂› do R (strojový kód) ## Příkazy Výrazy nám poskytují mocný výpočetní aparát, ale díky své deklarativní struktuře nejsou ideální pro popis sledu výpočetních kroků. Je-li potřeba provést nějakou sekvenci operací v daném pořadí, budou se mnohem lépe k zápisu hodit «příkazy». ### Výrazový příkaz │ • ‹e₁;› – jakýkoliv výraz │ • provede vedlejší efekty │ • hodnota je zapomenuta (např. ‹a = b;›) ### Složený příkaz │ • sekvence příkazů │ • uzavřena do složených závorek │ • připouští navíc deklarace │ ◦ rozsah platnosti jmen (ve složených závorkách) ### Podmíněný příkaz │ • ‹if ( expr ) stmt› │ • ‹if ( expr ) stmt₁ else stmt₂› (‹if›, ‹else›) ### Cyklus ‹do … while› │ • ‹do stmt while ( expr );› (jump na konci) ### Řízení iterace │ • ‹break;› – ukončí cyklus │ • ‹continue;› – ukončí aktuální iteraci (‹break›, ‹continue›) ### Cyklus ‹while› │ • ‹while ( expr ) stmt› (while true + break) ### Cyklus ‹for› │ • ‹for ( decl; expr₁; expr₂ ) stmt› (deklarace)