1 Výpočetní stroj
Tato kapitola jazyk C nepoužívá.
2 Lokální proměnné, řízení toku
Počínaje touto kapitolou budeme většinu programů psát ve
zjednodušené verzi jazyka C. V tomto kurzu budeme psát programy do
jednoho souboru, který bude sestávat z definic typů (uvidíme
později) a podprogramů. Na diskusi o sémantice podprogramů zatím
nejsme připraveni, proto je budeme chápat jako syntaktickou obálku
pro kód, který budeme psát.
Program bude typicky vypadat takto:
int podprogram( int parametr₁, int parametr₂ )
{
…
}
int main()
{
assert( podprogram( 1, 2 ) == 3 );
…
}
Podprogram s názvem main
bude v tomto kurzu vždy obsahovat testy,
které ověřují základní funkcionalitu ostatních podprogramů. Můžete
si do něj vždy přidat svoje vlastní testy. Zápis podprogram( 1,
2 )
je volání (použití) podprogramu – prozatím jej nebudeme mimo
testy potřebovat, protože jediné podprogramy, které budeme moct
v tomto předmětu použít, jsou ty, které si sami napíšeme.
2.0.1 Hodnoty, objekty a proměnné
Proměnné znáte již z kurzu IB111 – proměnné v jazyce C mají s těmi
v Pythonu mnoho společného, ale mají také důležité odlišnosti.
Prvním, v zásadě syntaktickým, rozdílem je, že v jazyce C musíme
každou proměnnou deklarovat – to provedeme zápisem typ jméno;
případně typ jméno = výraz;
. První forma proměnnou pouze
deklaruje, ale její počáteční hodnotu ponechá neurčenu – tuto
hodnotu není dovoleno použít.
Typ proměnné určuje, jakých hodnot může nabývat – k dispozici máme
prozatím tyto zabudované typy:
unsigned
– celé číslo v rozsahu 0 až 65535,
int
– celé číslo v rozsahu -32768 do 32767,
bool
– celé číslo, 0 nebo 1, které typicky reprezentuje
pravdivostní hodnotu – 0 pro false
, 1 pro true
,
signed char
– celé číslo v rozsahu -128 až 127,
unsigned char
– celé číslo v rozsahu 0 až 255,
char
– typ se stejným rozsahem jako jeden z předchozích dvou
(který z nich je určeno implementací), ale přesto z pohledu
kontroly typů od obou odlišný.
Proměnná je v jazyce C pevně svázaná s
objektem. Objekt je
abstrakce paměti – reprezentuje entitu, která je schopna pamatovat
si
hodnotu, již můžeme z objektu
přečíst nebo do objektu
uložit novou (a tím tu předchozí přepsat). Objekt tak můžeme
chápat jako dvojí zobecnění paměťové buňky:
- místo jednoho bajtu si pamatuje hodnotu (která může mít
potenciálně složitou vnitřní strukturu, i když takové zatím
neumíme v jazyce C sestrojit),
- místo adresy má identitu – objekt můžeme „uchopit“ a pracovat
s ním – obvykle tak, že tento objekt svážeme s proměnnou.
Realizace objektů je důležitým prvkem implementace programovacího
jazyka a může se případ od případu lišit. Zejména není pravda, že by
byl objekt pevně svázán s nějakou adresou nebo registrem – překladač
může objekt transparentně přesouvat dle potřeby výpočtu.
2.0.2 Živost a rozsah platnosti
Objekt, který je s proměnnou svázaný, vznikne právě deklarací, a
zanikne opuštěním rozsahu platnosti této proměnné. Čtení objektu je
implicitní – provede se kdykoliv proměnnou použijeme jako hodnotu ve
výrazu, zápis do objektu pak provedeme operátorem přiřazení (viz
také další sekce).
Podobně jméno proměnné je platné počínaje deklarací, a konče pravou
složenou závorkou, která ukončuje nejbližší uzavírající blok
(složený příkaz nebo tělo funkce – podrobněji rozebereme dále).
Například:
{
// zde x ještě není deklarováno
int x;
{
int y;
… // zde můžeme použít jak x tak y
} // zde končí rozsah platnosti y
… // zde již y není lze použít
} // zde končí rozsah platnosti x
U proměnných je tak syntakticky zaručeno, že jsou svázány s živým
objektem – kdykoliv můžeme jméno proměnné použít, objekt, který tato
proměnná pojmenovává, existuje.
2.0.3 Výrazy
Na úrovni jazyka C je základní jednotkou výpočtu výraz – podobně
jako v jazyce Python můžeme výrazy tvořit induktivně. Jsou-li:
e₁
, e₂
… eₙ
výrazy,
var
jméno proměnné,
lit
číselný literál (konstanta),
existují také výrazy tvaru:
lit
(konstanta) je výraz,
var
(jméno proměnné) je výraz,
- použití aritmetického operátoru (binární v infixovém zápisu,
unární v prefixovém):
e₁ + e₂
, e₁ - e₂
,
e₁ * e₂
, e₁ / e₂
, e₁ % e₂
(modulo)
- unární mínus
-e₁
,
- relační operátory:
e₁ == e₂
(rovnost), e₁ != e₂
(nerovnost)
e₁ <= e₂
, e₁ >= e₂
, e₁ < e₂
, e₁ > e₂
- bitové logické operace a posuvy:
- binární
e₁ & e₂
(and), e₁ | e₂
(or), e₁ ^ e₂
(xor),
- unární
~e₁
– bitová negace,
- bitové posuvy zapisujeme
e₁ >> e₂
, e₁ << e₂
,
- operátory přiřazení (pozor na změnu oproti jazyku Python –
v jazyce C je přiřazení výraz, nikoliv příkaz):
- jednoduché
var = e₁
,
- složené
var += e₁
, var -= e₁
,
- dále
var *= e₁
, var /= e₁
, var %= e₁
,
- s bitovým posuvem
var <<= e₁
, var >>= e₂
,
- s bitovou operací
var &= e₁
, var ^= e₁
, var |= e₁
,
- operátory zvýšení a snížení proměnné o jedničku:
- prefixové
++var
, --var
,
- postfixové
var++
, var--
,
- operátor čárka,
e₁, e₂
,
- booleovské logické operace:
- binární
e₁ && e₂
(and), e₁ || e₂
(or),
- unární
!e₁
,
- ternární
e₁ ? e₂ : e₃
,
2.0.4 Vyhodnocení výrazu
Nyní víme, jak výrazy vypadají (jakou mají syntaxi), můžeme tedy
přistoupit k otázce, co takové výrazy znamenají (jakou mají
sémantiku). Všechny zde uvedené výrazy popisují nějakou
hodnotu a výraz samotný je návodem, jak tuto hodnotu získat.
Vyhodnocení výrazu (provedení výpočtu tímto výrazem popsaného)
budeme samozřejmě realizovat pomocí již zavedeného výpočetního
stroje tiny
. Abychom mohli výpočet skutečně provést, musíme určit
registr, do kterého má být výsledek zapsán – budeme mluvit
o vyhodnocení výrazu E do registru R.
- Výraz
lit
se vyhodnotí přímo na číselnou hodnotu zapsanou ve
zdrojovém kódu. Například vyhodnocení výrazu 7
do registru
rv
se realizuje instrukcí put 7 → rv
.
- Výraz
var
se vyhodnotí na hodnotu, která je v momentě
vyhodnocení tohoto výrazu uložena v objektu svázaném s proměnnou
var
. Prozatím uvažujeme pouze situace, kdy je objekt svázaný
s var
uložen přímo v registru. Je-li např. var
uloženo
v l1
, vyhodnocení výrazu var
do registru rv
realizujeme
instrukcí copy l1 → rv
.
- Uvažme nyní výraz tvaru
e₁ + e₂
. Víme, že e₁
a e₂
popisují
nějaké hodnoty. Abychom mohli vyčíslit hodnotu e₁ + e₂
, budeme
nejprve potřebovat tyto hodnoty. Na to použijeme dočasné
registry – vyhodnocení e₁ + e₂
do rv
bude vypadat takto:
- vyhodnoť
e₁
do registru t₁
,
- vyhodnoť
e₂
do registru t₂
,
- proveď
add t₁, t₂ → rv
.
Musíme samozřejmě zabezpečit, že výpočet e₂
nepřepíše registr t₁
– jak přesně se toho dosáhne budeme zkoumat později.
Analogicky se vypočtou ostatní aritmetické, bitové, atd.
operátory (k hodnotám s/bez znaménka a operacím dělení se ještě
vrátíme).
- Výrazy tvaru
var = e₁
mají krom hodnoty také vedlejší efekt
– zápis do objektu svázaného s proměnnou var
. Jejich realizace
vypadá takto – vyhodnocujeme do registru rv
, objekt svázaný
s var
nechť žije v l1
:
- vyhodnoť
e₁
do registru rv
- proveď
copy rv → l1
.
Všimněte si, že hodnota e₁
je zároveň hodnotou celého výrazu,
a zůstává uložená v registru rv
, jak bylo požadováno.
- Složené přiřazení
var += e₁
je analogické, pouze je operaci
copy
předřazena příslušná aritmetická nebo logická operace:
- vyhodnoť
e₁
do registru t₁
- proveď
add t1, l1 → rv
,
- proveď
copy rv → l1
.
Výrazy zvýšení a snížení o jedničku jsou analogické, liší se
pouze ve výsledné hodnotě. Prefixové verze, ++var
, --var
jsou pouze syntaktické zkratky pro var += 1
resp. var -= 1
,
ale postfixové se liší – vyhodnocení var++
do rv
proběhne
takto (var
je svázáno s l1
):
- proveď
copy l1 → rv
,
- proveď
add l1, 1 → l1
.
Hodnota výrazu var++
je tedy původní hodnota var
,
předtím, než bylo provedeno zvýšení proměnné o jedničku.
- Výraz
e₁, e₂
představuje „zapomenutí hodnoty“ výrazu e₁
–
výraz e₁
je proveden pouze pro svoje vedlejší efekty (např.
výše uvedené přiřazení). Vyhodnocení e₁, e₂
do registru rv
lze realizovat např. takto:
- vyhodnoť
e₁
do rv
,
- vyhodnoť
e₂
do rv
.
- Zbývá zatím nejsložitější typ výrazů, a to jsou booleovské
logické operace. XXX
Uvažme nyní několik konkrétních příkladů:
2.0.5 Příkazy
- výraz + středník
- složený příkaz
if
, else
for
while
break
continue