1. Úvod .........................................................

Transkript

1. Úvod .........................................................
Obsah
1. Úvod . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1 Historie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Výhody (a nevýhody) jazyka C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2. Jak vypadá program v jazyce C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.1 Obecně . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.2 Program v Pascalu a v jazyce C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.3 Procedury VS funkce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.4 Definice proměnných . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.5 Definice funkcí . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3. Základní operátory a řídící struktury . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.1 Neexistuje typ boolean . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.2 Priority operátorů . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.3 Operátor přetypování . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.4 Základní řídící struktury . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
4. Oddělený překlad, hlavičkové soubory (funkční prototypy) . . . . . . . . . . . . . . . . . . . 16
4.1 Vzhled překladače . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.2 Preprocesor, makra, konstanty, velké projekty . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.2.1 #include . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.2.2 #define . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.2.3 Ostatní direktivy překladače . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.3 Rozdělený překlad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.4 Správa velkých projektů – make . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
5. Ukazatele vulgo pointery . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.1 Znaky . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.2 Pole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.3 Řetězce, funkce atoi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.3.1 Funkce malloc a free . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
5.4 Pointery obecné . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
5.5 Spojové seznamy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
6. Některé funkce obvykle přítomné v jazyce C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
6.1 Funkce printf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
6.2 Práce se soubory pomocí stdio.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
6.3 Převzetí argumentů programem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
7. Struktury, unie, enumy, typedef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
7.1 enum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
7.2 Unie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
7.3 Struktury . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
7.4 Typedef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
8. Proměnlivý počet argumentů funkce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
9. Překladače a jejich použití . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
9.1 Warningy a errory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
9.2 GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
9.3 Visual C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
10. Komentáře k příkladům . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
–1–
10.1
10.2
10.3
10.4
10.5
10.6
10.7
Popis příkladů . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Práce s řetězci . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Pole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Třídění . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Rekurze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Divide et impera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Vsuvka z trochu jiného světa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
–2–
43
43
44
44
44
45
45
1. Úvod
Rozhodli jste se naučit programovat v jazyce C? Udělali jste pěkné leč ne úplně snadné
rozhodnutí. Tento materiál je koncipován tak, aby v něm bylo možno nalézt odpovědi na
co nejvíce otázek, které vás při studiu napadnou, není koncipován jako kniha, která by
měla být přečtena od začátku do konce. Chcete-li návod ke čtení této knihy, můžeme určit
pořadí čtení pasáží této knihy asi takto: Kapitola 1, kapitola 2, orientační prohlédnutí
kapitoly 3 obsahující zejména pochopení operátorů ++ a
, kapitoly 5, 6, orientační
prohlédnutí kapitoly 9 s praktickým zkoušením překladače, zbytek materiálu v pořadí od
začátku. Samozřejmě každému může vyhovovat jiný průchod, každopádně k nějakému
netriviálnímu programování je potřeba pochopit vpodstatě vše mimo pokročilejší partie
podkapitoly 4.2.2, není-li potřeba zásadní portabilita, lze ignorovat podkapitolu 4.2.3,
podkapitola 4.3 je určena pro ty, kteří se chystají dělat větší projekt (nebo si chtějí
práci zjednodušit a zpříjemnit), rovněž se většinou lze obejít bez obsahu kapitoly 8,
nicméně aby byl výklad aspoň trochu úplný, nebylo možno vynechat žádnou z těchto
položek. V materiálu budeme (vzhledem k teoretickému založení alespoň části autorů)
místy odbíhat od výkladu jazyka C k teoretičtějším pasážím informatiky, u kterých nemusí
být pravdivost zřejmá (či lépe nemusí být jasné, jak souvisí s programováním v jazyku
C). Tyto pasáže by však měly být na pohled zjevné miminálně proto, že by měly být
vysázeny kurzívou.
Myslete zejména na to, že jako u jakéhokoliv přirozeného jazyka, ani programovacímu
jazyku C se nelze naučit bez praktické přípravy, je tudíž potřeba pokud možno co nejdříve
začít experimentovat s překladačem. Myslete na to, že jak si program napíšete, takový ho
budete mít. Když kód prošpikujete všemi postranními efekty, které vás napadnou, budete
je tam mít na sebe přichystané do fáze ladění. A pamatujte, že při ladění programu proti
sobě máte nepřítele tak důmyslného, jak důmyslné pasti jste vyrobili při psaní kódu.
Uvědomte si, že překladač není vaším nepřítelem, on dělá, co může (přesněji co musí),
ale naopak autor programu si ne vždy přesně uvědomuje, co po překladači ve skutečnosti
chce.
1.1 Historie
Jazyk C je spjat s vývojem UNIXu, konkrétně obojí vznikalo v Bellových laboratořích
v týmech značně personálně propojených.
1.2 Výhody (a nevýhody) jazyka C
Programovací jazyk C je vyšší strukturovaný procedurální programovací jazyk s řadou nízkoúrovňových nástrojů. Spojuje tudíž výhody programování ve vyšších jazycích s
výhodami programování v jazycích nízké úrovně. Tedy lze v něm psát aspoň tak dobře,
jako třeba v Pascalu a tak dobře, jako v assembleru. Navíc, jelikož se jedná o vyšší programovací jazyk, má základní předpoklad pro to být portabilní, což také je (programují se v
něm nejen počítače, ale třeba i mobilní telefony, pračky a vůbec všechno, u čeho vás ani
nenapadne, že by bylo potřeba programovat). Dále to, že umožňuje poměrně nízkoúrovňový přístup do paměti, je dobrým předpokladem pro možnost psát v něm efektivní kód
schopný běžet rychlostí srovnatelnou s programem napsaným třeba přímo v assembleru
a ještě k tomu být přenositelný. Ve skutečnosti program napsaný v jakémkoliv výpočetně
úplném jazyce je co do rychlosti srovnatelný s kódem napsaným v assembleru, tedy běží
nejvýše k-krát pomaleji pro nějakou konstantu k, protože každou assemblerskou instrukci
lze nějak přeložit do dotyčného programovacího jazyku na konečně mnoho kroků a následně simulovat (třeba jinak pojmenované) assemblerské instrukce. To ovšem není to, co
by nás při praktickém programování příliš potěšilo.
Nevýhody jazyka C opět vyplývají z jeho povahy. Jelikož jazyk C umožňuje provádět
operace celkem nízké úrovně, může se stát, že jich člověk použije, aniž o tom ví (přesněji
–3–
aniž ví, co dělá). Vlivem toho překladač jazyka C nemůže příliš pečlivě kontrolovat, zda
pisatel opravdu ví, co dělá. Zkrátka jazyk C je určený k tomu, aby se v něm programovalo.
Překladač nikoho nebude vodit za ručičku a říkat mu: Tady střílíš za konec pole (protože
překladač v mnoha případech nemá ani tušení, kde pole končí nebo dokonce že to vůbec
pole je). Navíc svou poměrně obecnou gramatikou dává poměrně široké možnosti psát
nekultivovaný kód, čehož někteří s oblibou využívají (mnohdy k vlastní následné nemalé
nelibosti).
2. Jak vypadá program v jazyce C
2.1 Obecně
Předpokládáme, že čtenář již má základní znalosti programování v Pascalu, konkrétně
že zná základní řídící struktury, je schopen psát ne zcela triviální programy (v Pascalu)
a v lepším případě i používat dynamicky alokovaných proměnných. Výklad bude veden
v mnoha případech vysvětlením rozdílů oproti Pascalu, případně bude předvedeno, jak
přeložíme konstrukci z Pascalu do jazyka C (zpět to bývá poněkud obtížnější). V textu se
budou občas vyskytovat poznámky z teorie složitosti a vyčíslitelnosti, které lze rozpoznat
pouhým pohledem, jsouť sázeny jiným fontem (nebo aspoň měly být).
Kromě jazyka C existuje jazyk C++, který má s jazykem C neprázdný průnik. V
následujícím textu bude probrán právě tento průnik (tedy to, co mají oba jazyky společné),
na odlišnosti mezi nimi bude případně upozorněno.
2.2 Program v Pascalu a v jazyce C
Program v Pascalu píšeme tak, že napřed definujeme funkce a procedury, které budeme používat, za definicemi funkcí následuje vstupní bod programu (tedy kód, který
program vykoná) odkud se volají výše definované funkce a procedury. Jednotlivé statementy ukončujeme středníkem (což vypadá od pohledu podobně jako v Pascalu, kde je
ovšem středníkem jen oddělujeme, proto dochází k určitým drobným rozdílům).
Příklad 1: (program v Pascalu)
program faktorial;
var a,x:integer;
function fakt(a:integer):integer;
var b,c:integer;
begin
b:=1;
for c:=1 to a do
b:=b*c;
fakt:=b;
end;
begin
write(’Zadej cislo: ’);
readln(a);
x:=fakt(a);
writeln(a,’! := ’,x);
end.
–4–
2.3 Procedury VS funkce
Než si ukážeme nějaký program v jazyku C, připomeňme si, že procedura se od funkce
liší jen tím, že nevrací hodnotu, což činí procedury svým způsobem nadbytečnými. Místo
procedury můžeme vytvořit funkci, která vrátí vždy nulu, jedničku či jakoukoliv (třeba
i náhodnou) hodnotu, kterou pak ignorujeme. Jelikož jazyk C je v některých směrech
rozmáchlý, kdežto v jiných skromný, neexistují v něm procedury. Existují jen funkce.
Současně v něm existuje datový typ void, který nenese hodnotu. Čili chceme-li vyrobit
ekvivalent pascalské procedury, vyrobíme funkci, která vrací void. Další odlišností oproti
Pascalu je, že hlavní program (v Pascalu kód mezi begin a end.) je tvořen též jen funkcí,
konkrétně funkcí jménem main. Krom toho v jazyce C nepoužíváme k otevírání resp.
uzavírání bloků klíčová slova begin a end (protože jsou zbytečně dlouhá), ale složené
závorky.
Dalším podstatným faktem je, že místo, kde definujeme proměnné, lze snadno rozpoznat, zvláště prohodíme-li jméno proměnné s jejím typem (tedy napíšeme-li napřed
jméno typu a pak jméno proměnné, tedy místo a:integer napíšeme int a a to i v případě, že definujeme návratový typ funkce). Pro nedostatek schopností pro práci s řetězci
předvedeme v jazyce C zatím jako příklad program vypisující řetězec (počítání faktoriálu
si necháme na později):
Příklad 2:
#include <stdio.h>
int vypis()
{
printf("Zkousime jazyk C\n");
return 0;
}
int main()
{
vypis();
}
V příkladu definujeme dvě funkce (vypis a main), z nichž žádná nepřijímá žádné
argumenty* a obě vrací typ integer (v jazyce C int). Program spustí kód funkce main, v
němž je jen volání funkce vypis. Povšimněte si, že v jazyce C, abychom zavolali funkci
musíme za její jméno přidat kulaté závorky s případnými argumenty a to i
prázdné, což v Pascalu nebylo (až zjistíme, co je pointer na funkci a že lze jakoukoliv
hodnotu během výpočtu zignorovat, bude jasné proč, zatím to přijměte jako fakt!). Dále
ve funkci vypis voláme funkci printf, která (v tomto případě) vypíše text zadaný jako
argument (obecně je situace trochu složitější, viz mnohem dále). Klíčové slovo return
určuje, že chceme současnou funkci (v našem případě funkci vypis) ukončit a vrátit
hodnotu určenou výrazem za slovem return, v našem případě nulu.
Ještě zbývá okomentovat první řádek programu. K náležitému pochopení je však
potřeba vědět, co je funkční prototyp, jak probíhá kompilace překladačem jazyka C a jak
vypadá tzv. oddělený překlad, kdy máme zdrojové texty rozmístěny v několika souborech.
Prozatím vemte na vědomí, že #include <stdio.h> nám umožňuje volat funkce jako
například printf (čili že určitým způsobem odpovídá klíčovému slovu uses v Pascalu,
jenže v Pascalu šlo slovo uses použít řádově na jednotky identifikátorů, v jazyce C mohou
být includů řádově desítky, stovky. . . a v průměrném případě jich také kolem deseti bývá).
* Nepřípadné poznámky o tom, kolik warningů při kompilaci napadá, si nechte, ještě
řekněte, že byste během deseti minut člověku, který vidí jazyk C poprvé, vysvětlili, co
znamená int main(int argc, char*argv[]). . .
–5–
2.4 Definice proměnných
Jak bylo řečeno dříve, při definici proměnných a funkcí provádíme bez klíčového
slova var místo kterého klademe jméno nosného typu. Napíšeme-li jméno typu, překladač za ním očekává identifikátor, který definujeme. Zda následně definujeme proměnnou
či funkci, se pozná podle toho, zda za identifikátorem je kulatá otevírací zavorka (pak
definujeme funkci) nebo ne (pak definujeme proměnnou):
Příklad 3:
int a,b,c;
char d=0;
vyrobí celočíselné (integerové) proměnné a, b, c a znakovou (charovou) proměnnou
d, u proměnné d si povšimněte rovnítka a nuly. Tím, že za název proměnné přidáme
rovnítko a hodnotu říkáme, jak se má příslušná proměnná zinicializovat, tedy v našem
příkladě, že d má být na začátku rovno nule. V Pascalu bychom toto zapsali asi následovně
s příslušnou nemotornou inicializací:
Příklad 4:
var
a,b,c:integer;
d:char;
...
begin
d:=0;
...
end.
Zatímco v Pascalu byly typy jako integer, char, byte, short, v jazyce C jsou
typy char, short (či short int – jedná se o stejný typ), int, long (analogicky long
int) – tyto typy jsou celočíselné v pořadí rostoucí velikosti. Velikost je dána pouze pro
typ char a to tak, že je to jeden byte (u ostatních je definováno jen, že se nesmějí
zmenšovat, v různých překladačích mohou mít velikosti různé, a také se tak děje!). Dále
existují neceločíselné typy float a double (double je v tzv. dvojnásobné přesnosti určené
normou IEEE 764), pak lze v různých překladačích nacházet různé nestandardní typy jako
třeba long long, každopádně ale vždy lze nalézt jako typ ukazatel (anglicky pointer),
který ukazuje na nějaké místo do paměti a typ void.
Číselné typy (různé od pointerů a voidu) mohou být znaménkové nebo neznaménkové.
Zcela očekávaně znaménkový typ podporuje záporná čísla, neznaménkový je pouze nezáporný. Znaménkové typy označíme klíčovým slovem signed, neznaménkové unsigned.
Chceme-li například znaménkový typ char, napíšeme unsigned char zz;. Obdobně
chceme-li neznaménkový integer, řekneme unsigned int nz;. Povšimněte si, že v předchozím příkladu se klíčová slova signed ani unsigned nevyskytují. Toto je možné proto,
že každý typ má jednu variantu jako implicitní. U charu se jedná o variantu neznaménkovou, u intu o znaménkovou. . ., chcete-li mít jistotu, napište variantu explicitně.
Jelikož se znaménková i neznaménková varianta jednoho typu musí vejít v paměti do
stejného prostoru, lze do znaménkových proměnných ukládat jen čísla poloviční oproti
neznaménkovým.
2.5 Definice funkcí
Funkce definujeme následovně. Napřed určíme návratový typ funkce, následuje jméno
funkce, za ním složené závorky, v nich popíšeme argumenty funkci předávané (podobně
jako když definujeme proměnné, jen tentokrát jednotlivé položky oddělujeme čárkami).
Pak do složených závorek zapisujeme jednotlivé příkazy. Chceme-li vyrobit proceduru
–6–
(přesněji ekvivalent procedury v Pascalu), vyrobíme funkci vracející typ void. Tento datový typ nenese hodnotu, nesmí být přiřazen, nesmí se na něj provádět aritmetické, logické
ani jiné operace. Je tudíž nesmysl definovat proměnnou typu void (protože bychom do
ní neměli co přiřadit) a překladačem by konstrukce void a; ani neměla projít. V následujícím příkladu definujeme funkce, které pokaždé mají různé argumenty či návratové
typy:
Příklad 5:
int secti(int a, int b)
{
return a+b;
}
void vypis(char*a)
{
printf(a);
return;
}
int deset(void)
{
return 10;
}
První (funkce secti) přijímá dva celočíselné argumenty a vrací jejich součet. Funkce
vypis dostává ukazatel na char (zatím vezměte na vědomí jen, že pointery se definují
pomocí operátorů hvězdičky a ampersandu), nevrací nic a argument vypíše pomocí funkce
printf. Ve třetím případě funkce deset nepřijímá žádné argumenty a vrací číslo 10. V
jazyce C je funkce určena jménem (identifikátorem), čili není možné vyrobit dvě funkce
téhož jména lišící se jen počtem nebo typem argumentů! V jazyce C++ to možné je, proto
se nedivte, pokud by vám překladačem prošly dvě funkce téhož jména lišící se jen v počtu
nebo typu argumentů (pozor nestačí, aby se lišily jen v návratovém typu), neznamená to
totiž nic jiného, než že máte zapnutý překladač C++ (aniž o tom třeba víte).
3. Základní operátory a řídící struktury
Nyní se naučíme manipulovat s číselnými datovými typy. Manipulace s řetězci je podstatně složitější a bude předmětem výkladu až po alespoň mírném úvodu do problematiky
pointerů.
3.1 Neexistuje typ boolean
Jistě jste si všimli, že mezi typy chyběl uvedený z Pascalu dobře známý logický typ
boolean. To není samo sebou, v jazyce C opravdu není. On je ve skutečnosti nadbytečný
s ohledem na to, že logické operace jsou jen zvlášním případem základních algebraických
operací, tedy konjunkci a disjunkci vyjádříme pomocí sčítání a násobení v tělese Z2 , ve
kterém existuje jen 0 a 1. Ne nadarmo se tomu říká speciální případ Boolovy algebry
(Boolovy algebry v plné obecnosti ale příliš vybočují z probíraného tématu). Zkusme tedy
reprezentovat logické hodnoty čísly. Lež (eufemicky nepravda) budiž reprezentována nulou, pravda čímkoliv jiným. V jazyce C tedy logické hodnoty získáme z celočíselných
následující konverzí: Nule přiřadíme nulu, čemukoliv jinému jedničku. Výhod této konverze je několik. Předně se typ boolean stane nadbytečným, protože každé číslo umíme
zkonvertovat (nestane se tudíž za běhu, že by program byl konfrontován s nezkonvertovatelným číslem) a krom toho lze provádět logické operace naprosto na jakákoliv čísla,
což člověk občas ocení.
Operátory (alespoň ty, které znáte), se chovají přibližně tak, jak by člověk očekával, tedy operátor + sečte dvě čísla, - odečte, hvězdička vynásobí, lomítko vydělí, ale
–7–
pozor. Zadáte-li operátoru dělení dvě celá čísla, dostanete jako výsledek opět celé číslo.
K dosažení neceločíselného dělení je potřeba předložit operátoru dělení neceločíselný typ
(čehož, jsou-li dělená čísla celá, dosáhneme třeba operátorem typové konverze, o kterém
si povíme až později).
Operátory bitových posunů pohlédnou na vyjádření čísla ve dvojkové soustavě a v
tomto zápisu přidají příslušný počet nul na začátek (resp. na konec) a z druhé strany
stejný počet číslic utrhnou. Operátor levého bitového posunu («) tudíž odpovídá násobení příslušnou mocninou dvojky, operátor pravého bitového posunu dělení příslušnou
mocninou dvojky beze zbytku. Logické operátory jsou ekvivalenty příslušných operátorů
v Pascalu, je tudíž snad zbytečné je komentovat.
3.2 Priority operátorů
Stejně jako v matematice mají operátory ve výrazech priority – je dáno, které operátory se vyhodnotí dříve a které později. Pro klasické matematické operátory platí pravidla,
na která jsme zvyklí ze základní školy: nejdříve se násobí a dělí, poté se sčítá a odčítá. V
jazyku C máme k dispozici mnoho dalších operátorů, takže je nutné uvést tabulku, která
nám určí, v jakém pořadí se bude daný výraz vyhodnocovat.
Také je nutné si uvědomit, že ačkoli většina operátorů se vyhodnocuje zleva doprava,
tedy tak jak jsme zvyklí, existují i operátory s vyhodnocením opačným, zprava doleva.
To se týká především operátoru přiřazení a unárních operátorů.
Například ve výrazu x = y = 4 se nejdříve přiřadí do proměnné y číslo 4. Výsledek
přiřazení je přiřazovaná hodnota (tedy 4), která se pak přiřadí do proměnné x.
Pokud si nejsme jisti, jak se vyhodnotí nějaký výraz, můžeme nahlédnout do následující tabulky nebo změnit prioritu použitím závorek (stejně jako v matematice).
Priorita
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
Operátory
Asociativita
() [] -> .
!
++ -- + - (typ) * & sizeof
zprava doleva
* / %
+ << >>
< <= > =>
== !=
&
^
|
&&
||
? :
zprava doleva
= += -= *= /= %= >>= <<= &= |= ^= zprava doleva
,
1. primární
()
[]
->
.
závorky
indexování pole
přístup k privku struktury
přístup k prvku struktury
Závorky se chovají podobně jako v Pascalu, tj. uzavírají části výrazu, které se mají
vyhodnotit prioritně, operátor pole a další dva operátory přístupu do struktury
budou probrány později v kapitole o pointerech.
–8–
2. unární operátory (vyhodnocují se zprava doleva)
!
~
++
-+
(typ)
*
&
sizeof
negace
if (!x) x = 4;
bitová negace (jedničkový doplněk) ~16
inkrementace
a++
dekrementace
--b
plus (unární)
i = +3
mínus (unární)
i = -4
přetypování
y = (long)x
dereference
*a = 4
reference
*z = &a
velikost typu v bytech
sizeof(long)
Operátor vikřičníku (logické negace) odpovídá z Pascalu operátoru not, operátor vlnky zpřeklápí jedničky a nuly v binárním zápisu čísla, unární plus a minus
určuje znamení hodnoty za sebou (např. x = -5; je typické použití unárního minus). Operátoru přetypování je věnována následující podkapitola, operátory unární
hvězdička, unární ampersand a sizeof budou pečlivě probrány v kapitole o pointerech.
Jelikož při programování v jednom kuse potřebujeme hodnoty proměnných zvyšovat či snižovat o jedničku, existují unární operátory ++ a --. Oba operátory
můžeme napsat buďto před identifikátor, nebo za identifikátor. Dva plusy zvýší
o jedna hodnotu proměnné u které stojí, dva minusy sníží. Na tom, je-li operátor před (prefixový) nebo za jménem (postfixový), dosti záleží. Rozdíl je takový,
že hodnotou postfixové operace je původní hodnota inkrementované (resp. dekrementované) proměnné, použijeme-li variantu prefixovou, je hodnotou výrazu již
inkrementovaná (resp. dekrementovaná) hodnota proměnné.
Příklad 6:
a=1;
b=1;
x=a++;
y=++b;
Po proběhnutí tohoto příkladu bude v proměnné x hodnota jedna, ve všech ostatních dvě. Hodnoty proměnných a a b zvyšujeme unárními operátory ++ (z jedné na
dvě), do proměnné y přiřazujeme hodnotu b po provedení prefixového operátoru
++, o kterém jsme si řekli, že vrací již novou hodnotu, tedy dvě. Do x přiřazujeme
výsledek postfixové varianty, která napřed zjistí hodnotu (tu vrátí) a jen tak
mezi řečí hodnotu proměnné a zvýší o jedna. Tyto operátory jsou velmi užitečné,
ač se bez nich lze teoreticky obejít, prakticky inkrementování a dekrementování
neobyčejně brzdí tempo programování.
Platí, že na jednu proměnnou lze použít v jednom výrazu jen jediný operátor
inkrementace nebo dekrementace. Můžeme napsat b = ++a++;. Není však
jasné, jaký bude obsah proměnné a. Podobně bychom mohli napsat: b= a++ +
++a;, pak ovšem musíme počítat s tím, že obsah obou proměnných bude nejistý.
Není totiž normou definováno, v jakém pořadí bude inkrementace probíhat.
3. dělení a násobení
*
/
%
násobení
8 * 4
dělení (celočíselné nebo reálné) 8 / 4
zbytek po dělení (modulo)
9 % 4
4. sčítání a odčítání
+
-
součet 8 + 4
rozdíl 3 - 2
–9–
5. opreátory posunů
<<
>>
posun bitů doleva
x << 1
posun bitů doprava x >> 1
6. relace
<
<=
>
>=
menší než
menší nebo rovno než
větší než
větší nebo rovno než
x
x
x
x
< 4
<= 4
> 5
>= 5
Relační operátory shodné s Pascalem.
7. operátory rovnosti
==
!=
je rovno
x == 4
není rovno x != 5
Povšimněte si, že porovnání na rovnost jsou vždy dvě rovnítka, porovnání na
nerovnost je jiné než v Pascalu (!).
8. bitový součin
&
bitový součin (AND) x & 7
Bitové operátory provádějí téměř totéž, co jejich logické ekvivalenty s tím rozdílem,
že logický operátor provedou na každý bit (binárního zápisu) čísla zvlášť. Tedy
například 2 & 2 je dvě, 5 & 3 vyjde jedna (protože 5 je (101)2 a 3 zase (011)2
– tato konvence zápisu čísel v jiných než desítkových soustavách byla definována
v učebnici matematiky pro základní školy, proto ji teď používáme, rozhodně se
nejedná z žádnou syntaktickou konstrukci jazyka C). Bitové and tudíž přežijí právě
ty bity, které jsou v obou operandech rovny jedné.
9. bitový exkluzivní součet
^ exkluzivní bitový součet (XOR) x ^ 4
Exkluzívní OR je operátorem logické nerovnosti, tedy platí pouze pokud je jeden
bit jedna a jeden nula. Toto je bitová varianta, tedy prováděna na každý bit čísla
zvlášť.
10. bitový součet
|
bitový součet (OR) x | 4
11. logický součin
&&
logický součin (AND) x == 3 && y == 4
12. logický součet
||
logický součet (OR) x == 3 || y == 4
13. ternární operátor podmínky (vyhodnocuje se zprava doleva!)
?:
podmínkový operátor x = x > 10 ? x = 0 : x + 1
Tento operátor používáme k podmínečnému přiřazení. Z výrazu před dvojtečkou je
vzata logická hodnota (v případě příkladu x¿10). Je-li tento výraz logická pravda,
je hodnotou celého výrazu výraz mezi otazníkem a dvojtečkou (tedy v našem
případě x = 0). Je-li výraz před otazníkem logická lež, je hodnotou výrazu výraz
– 10 –
za dvojtečkou. Tedy náš příklad je-li x větší, než deset, přiřadíme do x nulu,
pokud ne, zvýšíme x o jedna.
14. přiřazení (vyhodnocuje se zprava doleva!)
=
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
přiřazení
součet a přiřazení
rozdíl a přiřazení
součin a přiřazení
dělení a přiřazení
modulo a přiřazení
bitový posun doprava a přiřazení
bitový posun doleva a přiřazení
bitový součin a přiřazení
bitový součet a přiřazení
exklusivní bitový součet a přiřazení
x
x
x
x
x
x
x
x
x
x
x
= 4
+= 4
-= 3
*= 2
/= 4
%= 4
>>= 2
<<= 2
&= 1
|= 255
^= 16
Povšimněte si (a dávejte pozor), že operátor přiřazení je v jazyku C jedno rovnítko.
V Pascalu bylo jedno rovnítko operátorem porovnání na rovnost. Překladač jazyka
C vám povolí použít oba operátory přibližně na týchž místech, pokaždé se ale
jedná o něco jiného! Tedy pokud napíšete do podmínky x=1, překladač má za to,
že chcete do proměnné x přiřadit jedničku a tuto hodnotu vrátit jako hodnotu
celého výrazu, tedy se jedná o podmínku vždy splněnou!
15. operátor čárky
,
operátor čárky (zapomínací operátor) x = 3, y = 2
Operátor čárky je určitým oslabením středníku. Odděluje dva výrazy. Najede-li se
na operátor čárky, zahodí se výsledek před ní a výsledkem výrazu je hodnota za
čárkou. Tedy například x=(2,3); přiřadí do proměnné x hodnotu 3, což pokud výraz v kulatých závorkách poněkud zesložitíme, může člověk k nemalému vlastnímu
překvapení vyrobit. Proto buďte opatrní a s čárkou zacházejte opatrně.
Příklad použití tabulky:
3.3 Operátor přetypování
V jazyku C musíme u každé proměnné určit nosný typ (int, long, char*...). Občas se ale stává, že máme proměnnou jistého typu, ale potřebujeme na ni pohlížet jako
na proměnnou jiného typu. Nejjednodušším příkladem může být operátor dělení, který
aplikován na celočíselné typy vrací celé číslo, aplikován na neceločíselné typy vrací float
resp. double (podle zadaného typu). Co ale dělat, máme-li zadané celočíselné proměnné,
které chceme vydělit přesně (v mezích možností)? Proto máme v jazyku C jednoduchou
konstrukci v podobě operátoru přetypování. Operátor přetypování vyrobíme tak, že před
příslušnou proměnnou napíšeme jméno nového typu do kulatých závorek. Tedy například máme-li intovou proměnnou a, dáváme konstrukcí (double) a překladači najevo,
že chceme, aby v tomto případě na proměnnou a pohlížel jako na double.
Příklad 7:
#include <stdio.h>
int main()
{
int a=3,b=2;
double c;
c=a/b;
printf("Vysledek deleni celych cisel je: %e\n",c);
c=(double)a/(double)b;
printf("Vysledek deleni doublu je: %e\n",c);
– 11 –
printf("A to je snad trochu jiny kafe, ne?\n");
return 0;
}
Právě předvedený příklad počítá podíl proměnných a a b. Jednou je nechá jako celá
čísla (a tedy vydělí se zbytkem), jednou si vynutí přetypování na double a výsledek je
hnedle přesnější. . .
Přetypovávání celých čísel na necelá je jednou z aplikací operátoru přetypování, jiná
forma použití se naskytuje při volání funkcí, které očekávají/vracejí jiné datové typy,
než jaké jim podsouváme my, když víme, že tím nic nepokazíme. Typicky se přetypování
hojně vyskytuje při práci s pointery, kde je problém v tom, na jaký typ pointer ukazuje.
Obecné funkce rády manipulují s generickým pointerem (tedy ukazatelem na typ void),
který nelze dereferencovat, ale který je též ukazatelem, kdežto my máme obvykle ambice
pod pointer koukat (tedy jej dereferencovat). Tudíž naše proměnné nebývají tak často
typu void*, pročež musíme (hlavně v C++) přetypovávat. Následující příklad si přečtěte
po absolvování kapitoly o pointerech. Říká, že chceme naalokovat 50 bytů, funkce malloc
vrací void-pointer, kdežto my máme char-pointer, což v jazyce C není zásadní problém,
v C++ už to ovšem hraje roli:
Příklad 8:
#include <stdio.h>
#include <malloc.h>
int main()
{
char*a=(char*)malloc(50);
strcpy(a,"ahoj");
printf("Retezec %s je na adrese %p\n",a,(void*)a);
free((void*)a);
return 0;
}
Ač má operátor přetypování, jakožto prefixní unární operátor, poměrně vysokou prioritu, je občas vhodné se ujistit, že nebude přetlučen operátorem jiným. Chceme-li překladači natvrdo vnutit pořadí vyhodnocování, výraz uzávorkujeme tak, aby nepřipouštěl
jiné vyhodnocení.
3.4 Základní řídící struktury
Základní řídící struktury jsou velmi podobné těm v Pascalu, mají pouze jinou syntax,
abychom ušetřili psaní zbytečných klíčových slov, jako then nebo do.
1. if(podm) vyraz;
Je asi tak nejjednodušší řídící strukturou. Používá se naprosto stejně jako v Pascalu, tedy za slovo if napíšeme do závorky podmínku, která se má vyhodnotit,
je-li podmínka splněna, provede se výraz, který následuje za ní. Oproti Pascalu
vynecháváme klíčové slovo then. Aby překladač poznal, kde podmínka končí, musíme ji uzavřít do závorek. Chceme-li v případě, že podmínka platí, provést více
příkazů, zavřeme je do složených závorek:
Příklad 9:
if(a>b)
{
printf("a je vetsi nez b\n");
a=b;
printf("a uz neni...\n");
}
– 12 –
2. if() vyraz else vyraz
Chceme-li něco provést i v případě, že podmínka neplatí, můžeme opět jako v
Pascalu použít klíčového slova else. Opět případných více výrazů obalíme složenými závorkami. Oproti Pascalu zde ovšem drobný rozdíl je. Není-li před else
blok uzavřený ve složených závorkách, nýbrž jen jeden výraz, před else píšeme
středník!
3. nejasné else
Dejte pozor, pokud vnořujete několik if konstrukcí. Pokud na vnořené máte else,
nemusí být jasné, k čemu patří:
Příklad 10:
if(a>b)
if(b>c)
printf("a>b>c");
else
printf("Ale ted je to nejasne\n");
4. while(podm) vyraz
Opět se jedná o analogii Pascalské konstrukce. Dokud je podmínka podm splněna,
cyklicky vyhodnocujeme výraz. Pro výraz vyraz platí stále stejná pravidla jako
u konstrukce if a platit budou i v dalších případech této podkapitoly. Tedy podmínku uzavíráme do závorek, výraz je buďto jednoduchý statement, nebo blok
uzavřený do složených závorek.
5. do... while();
Analogie Pascalského repeat ... until, rozdíl je v tom, že se opakuje, dokud
podmínka platí (repeat ... until opakovalo naopak, dokud podmínka neplatila.
6. for(start;podm;inkrem)telo
Tato konstrukce je na pohled poněkud odlišná od for-cyklu v Pascalu a ve skutečnosti je také výpočetně mnohem silnější. Jedná se opět o konstrukci, která
cyklicky vyhodnocuje tělo dokud platí podmínka podm. Po každém vyhodnocení
cyklu spustí kód inkrem a ještě dříve, než začne vyhodnocovat podmínku a cyklit,
provede kód start. Oproti Pascalu tudíž můžeme cyklit přes obecnou podmínku,
nejen přes podmínku být menší, než hodnota,. Překladač se ovšem tudíž nestará
o inkrementaci cyklící proměnné (protože vlastně žádná neexistuje). Zda chceme
inkrementovat nebo dekrementovat a jakým způsobem, si řídíme sami. Situace
bude snad jasnější předvedeme-li si, jakým způsobem přepsat pascalský for-cyklus
do notace jazyka C.
Příklad 11: (Pascal)
program forcyklus;
var i,j:integer;
begin
j:=0;
for i:=1 to 10 do
j:=j+i;
writeln(’Soucet 1... 10 je: ’,j);
end.
Příklad 12: (přepsaný z Pascalu)
#include <stdio.h>
int main()
{
int i,j=0;
for(i=1; i<=10; i++)
j=j+i;
– 13 –
printf("Soucet 1... 10 je: %d\n",j);
return 0;
}
Jazyk C ovšem umožňuje daleko elegantnější zapis:
Příklad 13:
#include <stdio.h>
int main()
{
int i,j;
for(i=j=0;i<=10;j+=i++);
printf("Soucet 1... 10 je: %d\n",j);
return 0;
}
Rozeberme si trochu poslední příklad. Inicializaci provádíme ve startovacím kódu
konstrukce for. Podmínka je stejná jako dříve, v inkrementačním kódu ovšem
nejen zvyšujeme hodnotu proměnné i, ale ještě ji mezi řečí přičítáme k současné
hodnotě proměnné j. Středník za konstrukcí for je legitimní a značí prázdný
statement, tedy jsme vyrobili for-cyklus s prázdným tělem, což se v jazyce C
občas stává, v Pascalu je tato situace těžko představitelná. Když jsme u středníku
za závorkami určujícími parametry řídící struktury, středník syntakticky můžeme
umístit za naprosto jakoukoliv řídící strukturu. Pak se ovšem stane to, že vyrobíme
strukturu s prázdným tělem a ve fázi ladění budeme jen koukat, co se to děje.
7. konstrukce switch
switch(vyraz)
{
case ’a’: prikazy
...
}
Tentokrát se jedná o analogii case vyraz of... z Pascalu. Konstrukci používáme tak, že napíšeme klíčové slovo switch, za něj do kulatých závorek napíšeme
výraz, který se má vyhodnotit, následují case-bloky. Za slovem case následuje
hodnota, která, je-li nabyta vyhodnoceným výrazem, má způsobit vykonání příslušného kódu. Za slovem case smí následovat jedině konstanta (nikoliv proměnná)
a je krajně nevhodné zkoušet zadat řetězec, jelikož pak bychom porovnávali charpointer. Další odlišnost od Pascalu spočívá v tom, že jak jednou najdeme správnou
case-klauzuli, interpretujeme od ní dále bez ohledu na to, zda jsme nenajeli na
další case. Toto nám umožňuje sloučit obsluhu více jevů je-li jeden jev podproblémem druhého (například děláme-li switch podle toho, zda zkoumaný je muž, žena
nebo dítě, u ženy vypíšeme tři rozměry, výšku a věk, u muže výšku a věk, kdežto
u dítěte jen věk, protože výška se mění poměrně rychle, odpovídající konstrukce
bude vypadat asi takto:
Příklad 14:
#define MUZ 0
#define ZENA 1
#define DITE 2
switch(osoba->pohlavi)
{
case ZENA:
case MUZ:
case DITE:
printf("tri miry: %d, %d, %d\n",
osoba->rozm1, osoba->rozm2, osoba->rozm3);
printf("Vyska: %d\n", osoba->vyska);
printf("Vek: %d\n",osoba->vek);
}
Chceme-li v jistém okamžiku z řídící struktury vyskočit (tedy třeba napsat pro
každou hodnotu ovladač zvlášť), vložíme na patřičné místo klíčové slovo break.
– 14 –
Příklad
#define
#define
#define
15:
MUZ 0
ZENA 1
DITE 2
switch(osoba->pohlavi)
{
case ZENA:
break;
case MUZ:
break;
case DITE:
printf("tri miry: %d, %d, %d\n",
osoba->rozm1, osoba->rozm2, osoba->rozm3);
printf("Vyska: %d\n", osoba->vyska);
printf("Vek: %d\n",osoba->vek);
}
Tento kód vypíše dítěti jen věk, muži jen výšku u ženy jen 3 čísla.
Chceme-li provést určitý kód, je-li hodnota jiná, než ostatní, použijeme klíčové
slovo default:
Příklad 16:
switch(osoba-> pohlavi)
{
case ZENA:
printf("Je to zena.\n");
break;
case MUZ:
printf("Koukame na chlapa.\n");
break;
case DITE:
printf("Deti jsou deti!\n");
break;
default:
printf("Bezpohlavni kreatura!\n");
break;
}
8. klíčová slova break, return, continue a funkce exit. Jedná se o klíčová slova,
která zasahují do běhu, obvykle ukončí nějakou řídící strukturu.
break – Ukončí vyhodnocování nejbližší řídící struktury (switche, while
cyklu, for-cyklu (ne vyhodnocování ifu). Podmínka se už znovu nevyhodnocuje a končí tudíž celý cyklus.
return – Ukončí provádění současné funkce, program pokračuje za místem,
ze kterého byla funkce zavolána. Následuje-li za slovem return výraz, má
být hodnota onoho výrazu návratovou hodnotou funkce.
continue – Ukončí současnou iteraci cyklu (while, for...) a nechá znovu
vyhodnotit podmínku (je-li podmínka splněna, vyhodnocuje se tělo cyklu
znovu, pokud ne, cyklus zcela očekávaně končí).
exit – je funkce a je potřeba jí předat jeden celočíselný argument. Tato
funkce patří mezi nejúčinnější v jazyce C, protože neprodleně ukončí běh
celého programu a jí předaný argument se stává chybovým kódem. Používá
se jako záchranná brzda ve chvíli, kdy programátor neví, kudy kam, dostal
program do stavu, se kterým naprosto nepočítal a naprosto neví, jak vzniklý
požár uhasit.
– 15 –
4. Oddělený překlad, hlavičkové soubory (funkční
prototypy)
Asi by se lépe vyjímalo rozdělený překlad, protože zdrojové texty rozdělíme do několika souborů. Alespoň by to lépe reflektovalo fakt, že nakonec rozdělené soubory linker
slepuje dohromady. Ale taková je česká terminologie. . .
4.1 Vzhled překladače
Překlad programu v jazyce C na pohled probíhá jako jednoduchý proces, kdy na vstup
programu nasypeme zdrojový text a z překladače vypadne strojový kód. S takto omezeným pohledem ovšem při programování naprosto nevystačíme. Skutečnost je taková,
že překlad probíhá v několika fázích. První fázi provádí tzv. preprocesor, který stručně
řečeno vyhodnocuje direktivy začínající křížkem, dosazuje hodnoty za konstanty, expanduje makra (o kterých si povíme v první podkapitole). Výstup této fáze je ještě okem
čitelný. Následuje samotný překlad do mezikódu překladače, pak převod do tzv. objectfilu (budeme mu též říkat objektový soubor, s objektovým programováním ale nemá nic
společného). Nakonec linker popadne několik object-filů a několik knihoven (v knihovnách jsou poskládané funkce, které z programu voláme a které jsme si sami nenapsali, v
Pascalu je tato fáze před námi ukrytá, ale ve skutečnosti kdykoliv voláme jakoukoliv cizí
funkci, program se slinkuje s knihovnou, v níž je kód příslušné funkce (překladač žádné
vestavěné funkce – aspoň v jazyce C – nemá).
4.2 Preprocesor, makra, konstanty, velké projekty
V tomto materiálu kromě samotného překladače probereme jen něco málo o preprocesoru, čili linker, objektové soubory a knihovny ponecháme neprobrané. Jak bylo stručně
naznačeno v předchozím odstavci, preprocesor interpretuje direktivy začínající znakem
#. Povšimněte si v příkladech, že řádky obsahující direktivy preprocesoru nekončí středníkem! Které direktivy tedy preprocesor rozpozná a interpretuje?
4.2.1 #include
Předně je to již v první kapitole použitá direktiva #include následovaná jménem
souboru ve špičatých závorkách nebo uvozovkách. Na počátku materiálu jsme si řekli, že
#include <stdio.h> nám umožní kupříkladu zavolat funkci printf. Realita je poněkud
méně romantická. Preprocesor najde soubor, jehož jméno je v uvozovkách či špičatých
závorkách (podle toho, čím je jméno obloženo, se určují cesty, ve kterých se má hledat,
soubor, jehož jméno je zauvozovkováno, se hledá zpravidla v současném adresáři, kdežto
ten, jehož jméno je mezi menšítkem a většítkem, je souborem dodávaným s překladačem
a vyskytuje se kdesi v systémových adresářích.
Co tak zajímavého tedy překladač najde v souboru stdio.h, že nám po jeho načtení
umožní používat řadu užitečných funkcí (a nejen funkcí, ale i proměnných či datových
typů)? Najde tam zejména funkční prototypy. Funkční prototyp není definice funkce, ale
pouze její popis, tedy údaj o jménu a počtu a typu argumentů. Oproti definici funkce je
zpravidla jednořádkový a ukončuje se středníkem.
Příklad 17:
int faktorial(int a);
int atoi(char* a);
int deset(void);
int soucet(int, int);
void printf(char*,...);
– 16 –
Takto mohou vypadat funkční prototypy. První říká, že někde existuje funkce jménem
faktorial, která vrací integer, přijímá také integer, druhý řádek slibuje funkci atoi,
která přijme ukazatel na char (tedy řetězec) a vrátí integer Tato funkce konvertuje řetězec
na číslo, což v Pascalu nebylo třeba dělat, tam funkce read sama inteligentně řetězec
přeštípala. Ono jí to na druhou stranu dalo dost práce, což není to, co chceme, jde-li o
to napsat výkonný program, který má být prakticky použitelný. Třetí a čtvrtá deklarace
by měla být vnímavému čtenáři jasná. Tři tečky v posledním řádku nejsou důsledkem
naší lenosti, ty opravdu v prototypu být mohou a říkají, že funkce má nepevný počet
argumentů (viz kapitolu Nepevný počet argumentů), což je třeba případ funkce printf.
V souborech s koncovkou .h není žádný rafinovaný kód, ale obvykle zejména sliby, že
někde (přesněji v knihovně libc, se kterou se linkuje implicitně, aniž bychom se o to
snažili) existují nějaké funkce. Pokud se stane, že v knihovnách dotyčné funkce neexistují,
kompilátor (tedy jeho jádro) na to nepřijde, zjistí to právě až linker tak, že není schopen
vyřešit odkaz na onen neexistující symbol (proto upozornění na volání neexistujících
funkcí chodí poměrně později, než upozornění na syntaktické chyby). Mimochodem údaj o
tom, že #include <stdio.h> nám umožní zavolat funkci printf byl poněkud zavádějící.
Překladač jazyka C by měl pouze vygenerovat warning, protože neexistující prototypy
by pro něj neměly být překážkou (stará norma Kernighana a Ritchieho s nimi vůbec
nepočítala a vlastně popisovala i jiné definice funkcí), problémy už ale určitě nastanou,
použijeme-li překladač C++, kde jsou funkční prototypy povinné.
4.2.2 #define
Chceme-li v Pascalu definovat konstanty použijeme asi takovéto konstrukce:
Příklad 18:
const a=2004;
V jazyce C použijeme direktivu define, která říká, že místo výrazu následující výraz
se má ve zbytku programu ponahrazovat tím, co je dále (za definovaným výrazem). Tedy
například:
Příklad
#define
#define
#define
#define
#define
#define
#define
19:
a 2004
NULA 0
POZDRAV "AHOJ"
write printf
jestli if
begin {
end }
Jak vidíte, direktiva define v jazyce C má daleko širší použití, než jen pro definování konstant. Můžeme její pomocí předefinovat (přesněji přidefinovat) plno dalších věcí,
včetně třeba klíčových slov jazyka. Doteď jsme si ale jen hráli, to pravé přijde teď. Dáme-li
za definované slovo kulaté závorky, definujeme makro. V závorkách jsou potom popsány
argumenty. Potom kdykoliv se objeví jméno takto nadefinované jako makro, preprocesor
je rozexpanduje. Toto je užitečné, má-li se dělat několik (stále stejných) úkonů na různých
místech programu. Nepoužije-li se k tomu makro, člověk zestárne na opravování všech
chyb na všech místech.
Příklad 20:
#define check(a) if(a<0) printf("Error")
Povšimněte si, že ani tentokrát na konci definovaného makra není středník. Důvod je
jednoduchý. Preprocesor je věc tupá, tudíž jen tupě nahradí text. Řekneme-li pak někde
v programu check(xxx);, preprocesor popadne řetězec check(xxx) a nahradí řetězcem
– 17 –
if(xxx<0) printf(„Errorÿ). Středník za voláním makra zůstane, preprocesor s ním nic
neudělá a pošle dále k překladu.
Není však dobré podléhat euforii nad možností předefinovávání všeho možného (ač
tímto například lze napsat poměrně solidní překladač Pascalu do Céčka), pokud to člověk
přežene, snadno program může dopadnout velice zajímavě.
Na tomto místě jsem původně měl v úmyslu předvést kus kódu, který vlivem nemírného používání služeb preprocesoru ani nevypadal jako program v jazyku C, ač opak byl
pravdou, leč bylo mi to rozmluveno jako neinstruktivní a zbytečně komplikované, což je
pravda.
4.2.3 Ostatní direktivy překladače
Preprocesor dále umí zejména podmínečně přikompilovávat vybrané kusy kódu. Jak a
k čemu to je? Jde o to, že ne všechna prostředí jsou stejná, občas chceme používat kupříkladu konstanty, které nemusí být definované. K tomu můžeme užít konstrukci #ifdef
... #endif. Za direktivou #ifdef následuje identifikátor, na který se ptáme, zda je
dobře definován. Například chceme-li něco odvozovat z maximální hodnoty, která se vejde do floatu, ujistíme se napřed, zda je definována konstanta MAX FLOAT, pak například
nadefinujeme konstantu MY MAX FLOAT, který bude desetina MAX FLOATu. Blok otevřený
direktivou #ifdef ukončíme direktivou #endif, podobně jako u konstrukce if máme
možnost vyrobit blok pro případ nesplnění podmínky. Direktiva preprocesoru, která toto
zařizuje, je #else.
Příklad 21:
#ifdef MAX_FLOAT
#define MY_MAX_FLOAT MAX_FLOAT/10
#else
#define MY_MAX_FLOAT 1000000
#endif
V příkladu jsme provedli toto: Je-li nadefinováno MAX FLOAT, nadefinujeme konstantu
MY MAX FLOAT v desetinové velikosti. Není-li MAX FLOAT definován, dosaď MY MAX FLOAT
konstantou milion. Naopak chceme-li zaručit, aby konstanta definována byla, užijeme
#ifndef (která je negací #ifdef), není-li, dodefinujeme ji nějakou důvěryhodnou hodnotou a blok opět ukončíme direktivou #endif:
Příklad 22:
#ifndef MAX_FLOAT
#define MAX_FLOAT 1000000
#endif
Zde, pokud MAX FLOAT není definován, dodefinujeme jej milionem.
Potřebujeme-li poněkud obecnější konstrukci, použijeme direktivu #if, za níž následuje obecně aritmetický výraz, který pokud je pravdivý (tedy nenulový), vjede se dovnitř
#if bloku.
Příklad 23:
#if MAX_FLOAT > 1000000
muzeme_se_rozsoupnout();
#else
jak_u_suchanku();
#endif
Aby byl příklad správný, musíme ještě tento blok obložit testem, zda je MAX FLOAT
vůbec definován. Navíc, jelikož se jedná o agendu vyřizovanou preprocesorem, není možné
– 18 –
testovat stav proměnných, ale jen a jen konstant!!
4.3 Rozdělený překlad
Máme-li větší program, občas se vyplatí tento rozdělit do několika menších zdrojových
souborů. Výhody jsou zjevné. Předně v menších souborech se snáze vyznáme, překlad
menších souborů je rychlejší a jelikož jádro překladače vyrobí pouze objektové soubory,
které pak linker lepí dohromady. Není tudíž potřeba nutit kompilátor za všech okolností
koukat do všech zdrojových textů, když se většina z nich neměnila). Pak ovšem potřebujeme z jednoho souboru volat funkce definované v jiném souboru. I když jsme v jazyce C,
je vhodné předložit překladači funkční prototypy (i když je překladač podle normy nesmí
vyžadovat). Pokud před překladačem prototypy utajíme, vymyslí si překladač své vlastní,
které mohou naší chybou vyústit v chybný kód. Je tudíž vhodné (a v C++ potřeba) kdykoliv, než zavoláme funkci, tuto buďto definovat, nebo zadat její prototyp. Prototyp se s
definicí nevylučuje, proto je ideální vyrobit ke každému zdrojového souboru s koncovkou
.c soubor s koncovkou .h a do něj nastrkat prototypy funkcí, které budeme volat odjinud.
Příslušný .h soubor (dále jen header-file) naincludujeme (#include „myfile.hÿ) do stejnojmenného souboru s koncovkou .c (tedy do toho, kde je funkce definována) a dále pak
do všech souborů, odkud se volá některá z inkriminovaných (tam popsaných) funkcí. Čeho
tím dosáhneme? Předně překladač bude mít k dispozici prototypy volaných funkcí a včas
nás upozorní, kdybychom někdy omylem zadali třeba špatný počet argumentů, navíc se
může stát, že změníme argumenty v definici funkce. V takovém případě musíme opravit
všechny výskyty funkčních volání a též její prototyp. Kdybychom zapomněli opravit prototyp (a překladač to nezpozoroval), bude nás dokonce nutit, abychom ve zbytku kódu
použili funkci špatně (se špatnými argumenty). Je-li je ale příslušný hlavičkový soubor
naincludován před definicí, překladač případný rozdíl zpozoruje a nás upozorní.
Příklad 24:
file.c:
int volany(void)
{
return 10;
}
-------------------volajici.c:
int b(void)
{
return volany();
}
Nyní je potřeba (aby překladač mohl zkontrolovat, že volání funkce volany() ze souboru volajici.c je v pořádku (tedy že funkce volany opravdu nebere žádné argumenty,
vyrobit soubor file.h, do něj přidat funkční prototyp a tento soubor naincludovat do
obou zdrojových souborů:
Příklad 25:
file.h:
int volany(void);
-------------------file.c:
#include "file.h"
int volany(void)
{
return 10;
– 19 –
}
-------------------volajici.c:
#include "file.h"
int volajici(void)
{
return volany();
}
4.4 Správa velkých projektů – make
V minulé podkapitole jsme si řekli, jak udržovat velké projekty konzistentní, nyní si
zkusíme povědět, jaké nástroje lze použít k automatizované kompilaci větších výtvorů,
abychom nemuseli každý program kompilovat tak, že vyhrabeme seznam všech souborů
s koncovkou .c a předáme je jako argumenty překladači na argumentové řádce.
Různá integrovaná prostředí umožňují vytvářet tzv. projekty, kdy člověk klikacím
způsobem popíše, jaké soubory má zájem do projektu přidat a klikomet zajistí, aby při
každém pokusu o kompilaci byly tyto soubory obslouženy (tedy byly-li modifikovány, aby
byly překompilovány). Probádání těchto hraček ponecháme libovůli čtenářově. Řádková
rozhraní podporují ještě mnohem obecnější nástroj na správu všech možných projektů
(tedy nejen těch napsaných v jazyce C), správnou výrobu zaručuje program make.
Programu make lze zadat argument – tzv. cíl, co má dělat. Není-li tento zadán, make
začne prohledávat svůj konfigurační soubor (nazvaný Makefile uložený v současném
adresáři), v něm najde první pravidlo a provede. Zdůrazněme, že make hledá svůj konfigurační soubor v současném adresáři, tedy rozbalíte-li cizí zdrojáky, ve vrchním adresáři
najdete zpravidla soubor Makefile, což je právě onen konfigurační soubor pro make.
Jak jsme již řekli, Makefile obsahuje sadu pravidel (nejen) pro překlad souborů.
Pravidlo vypadá tak, že napřed je určeno jméno souboru, který po něm má zbýt, za ním je
dvojtečka a za dvojtečkou mezerami oddělená jména souborů, na kterých výsledný soubor
závisí (tzv. dependence, hezky česky závislosti). Na dalším řádku za tabelátorem může
následovat popis, co má make udělat (jakým způsobem ze souborů napravo od dvojtečky
vyrobit soubor nalevo od dvojtečky). Je možné tento popis akce nechat prázdný a doufat,
že make rozpozná z koncovek souborů, co se od něj očekává. To ale určitě předpokládá
používání standardních koncovek a poměrně omezeného počtu nástrojů (na druhou stranu
používání příliš nestandardních nástrojů ústí v chatrnou portabilitu kódu).
Příklad 26:
program: volajici.o volany.o
volajici.o: volajici.c file.h
volany.o: volany.c file.h
clean:
rm -f program *.o
Tento Makefile obsahuje čtyři pravidla. První tři využívají implicitní reakce programu make (v prvním případě vyrábíme program nazvaný program ze dvou objektových
souborů, na což make dobře ví, že má zavolat gcc -o program volajici.o volany.o,
– 20 –
v ostatních případech snadno z koncovky zjistíme, že z Céčkového zdrojáku chceme vyrobit objektový soubor, tedy make vyrozumí, že má spustit překladač jazyka C (konkrétně takto: gcc -o volajici.o volajici.c, resp. gcc -o volany.o volany.c. Odkaz k file.h pouze oznamuje programu make závislosti, tedy že se má překompilovat
kdykoliv je změněn některý ze souborů za dvojtečkou.
Čtvrté pravidlo spustíme příkazem make clean. Toto slouží ke smazání všeho, co
bylo pomocí tohoto Makefilu vyrobeno, tedy všechny binární soubory.
5. Ukazatele vulgo pointery
Nejdůležitější partií jazyka C jsou pointery. Pointer (česky ukazatel) ukazuje na nějaké
místo v paměti. Pomocí pointerů se realizuje mimo jiné téměř veškerá práce s řetězci,
jelikož, jak jste si jistě povšimli, jsme o práci s řetězci dosud nemluvili. Že jsme se o
řetězcích dosud zmiňovali jen neochotně a z donucení, není samoúčelné, protože práce s
nimi v jazyce C není úplná legrace, on totiž datový typ string neexistuje.
V Pascalu existoval typ string, který umožňoval reprezentovat řetězce, porovnávat, který řetězec je (lexikograficky) větší či menší, řetězce kopírovat pomocí operátoru
přiřazení, předávat je hodnotou funkci. Typ string byl v paměti překladačem Pascalu
implementován tak, že na začátku řetězce byl údaj o jeho délce a pak teprve následoval samotný řetězec. Nic z toho v jazyce C není, jediné, co se snad shoduje s Pascalem,
je přístup k řetězci jako k poli znaků, kdy pomocí operátoru hranatých závorek ([ ])
přistupujeme k jednotlivým znakům řetězce.
5.1 Znaky
Datový typ char se používá k ukládání malých celých čísel a současně též k ukládání
znaků (uvědomte si, že každý znak je reprezentován nějakou hodnotou – např. ASCIIkódem – pro jednoduchost nebudeme uvažovat jiná kódování a budeme mluvit o ASCIIhodnotě znaku, ač existuje plno jiných kódování, např. UNICODE, EBCDIC. . .). Tato
dvojakost typu char je velmi důležitá a budeme jí příležitostně využívat.
Chceme-li tedy do charové proměnné přiřadit znak, máme možnost buďto přiřadit
jeho ASCII-hodnotu (například pro znak A hodnotu 65, pro B 66. . . (čili char a=65;,
druhou možností je přiřadit dotyčný znak obložený apostrofy (zdůrazňuji apostrofy!!),
tedy například char a=’A’;. Jelikož typ char nese hodnotu, má smysl jeho obsah porovnávat na rovnost, nerovnosti, sčítat, odčítat. . ., vždy si ovšem rozmyslete, co chcete udělat,
protože veškerá případná aritmetika se odehrává na příslušných ASCII-hodnotách, tedy
například if(a>’0’), je podmínka splněná nejen pro čísla, ale kupříkladu i pro všechna
písmena. Podobně výraz ’0’+’1’ je malé a (protože ASCII-hodnota nuly je 48, ASCII
hodnota jedničky 49 a ASCII kód 97 odpovídá právě malému ’a’).
5.2 Pole
Pole v jazyku C se chovají velmi podobně polím v Pascalu. Použití je prakticky stejné,
jiné je jen definování. Předně jazyk C neumožňuje definovat vícerozměrná pole! Chcete-li
tudíž vícerozměrné pole vytvořit, musíte si nějak pomoci poli jednorozměrnými. Možnosti jsou dvě. Buďto vytvořit pole o velikosti součinu obou rozměrů a prvek s indexem
(i,j) budeme hledat na indexu i+j*m, kde m je šířka žádaného 2-rozměrného pole (toto
odpovídá situaci, kdy vezmeme jednotlivé řádky 2-rozměrného pole a poskládáme je za
sebe). Druhou možnost jen naznačíme (obvykle se nepoužívá a je na pochopení poněkud náročnější), a to je pole pointerů vpodstatě shodné s tím, jehož pomocí se předávají
argumenty programu (viz podkapitolu Předávání argumentů programu), kde zkusíte napodobit obsah proměnné argv. . .
– 21 –
Jak již bylo řečeno, od Pascalu se liší pouze definování polí. V Pascalu jste řekli, že
chcete desetiprvkové pole asi takto:
var a:array [1..10] of integer;
v jazyku C řekneme totéž následovně:
int a[10];
Jak vidíte, konstrukce je jednodušší, odbourává zbytečné klíčové slovo array a neurčuje minimální index, což není nic proti ničemu, v jazyce C se pole indexují zásadně
od nuly!! Uvědomte si, jaký je to rozdíl oproti Pascalu, kde jste byli zvyklí indexovat od
jedničky. Současně, jelikož číslo v hranatých závorkách udává, kolik prvků má mít pole,
pamatujte, že poslední prvek pole má index o jedna menší, než je hodnota v hranaticích!!
Tedy výše uvedená konstrukce int a[10]; vyrobí pole deseti integerů. První prvek pole
má index 0, poslední 9. Konstrukce funkční v jazyku C tudíž odpovídá přibližně tomuto
zápisu v Pascalu:
var a:array [0..9] of integer;
Cítí-li někdo potřebu počítat od jiného indexu, musí příslušnou transformaci provést
při každém přístupu do pole, tedy vždy přičíst nebo odečíst příslušnou konstantu.
Problematika polí v jazyce C je natolik stejná, že nám přijde zbytečné výklad dále
piglovat, na závěr pro úplnost uveďme příklad, jak v Pascalu a jak v jazyku C vyrobit
pole deseti integerů, do první položky přiřadit 10 a následně vypsat obsah první položky.
Příklad 27:
program nic;
var a:array [1..10] of integer;
begin
a[1]:=10;
writeln(a[1]);
end.
Příklad 28:
#include <stdio.h>
int a[10];
int main()
{
a[0]=10;
printf("%d\n",a[0]);
return 0;
}
Nyní si ještě povšimněme, že řetězce v Pascalu se v mnoha ohledech chovají jako
pole znaků, kde první znak určuje délku řetězce, tudíž je jen přirozené, že dotažením této
myšlenky do konce vznikla v jazyku C začátečníkovi poněkud nepřehledná, leč neobyčejně
snadná a elegantní implementace řetězců.
5.3 Řetězce, funkce atoi
Představme si nyní řetězec jako pole znaků. Jakým způsobem určíme konec řetězce
prozatím nechme stranou. Chceme-li řetězce porovnávat na rovnost, prostě porovnáváme
postupně jednotlivé znaky pole. Chceme-li porovnávat na nerovnost, porovnáváme jednotlivé znaky pole na nerovnost. Chceme-li řetězce konkatenovat (slepit dva za sebe),
– 22 –
vyrobíme pole délky součtu délek jednotlivých řetězců a do něj naládujeme napřed obsah prvního a pak i druhého řetězce. Chceme-li řetězec uříznout (truncnout), změníme
příslušným způsobem počet prvků pole. A nyní už zbývá jen drobnost, a to určit konec
řetězce. Říkali jsme si, že v Pascalu byl na začátku řetězce jeden byte vyhrazený na údaj
o délce. Tento přístup má problém, že shora omezuje délku řetězce. My proto použijeme
jinou variantu, a to ukončení řetězce speciálním znakem, konkrétně znakem číslo nula.
Tento přístup sice znesnadňuje určení délky řetězce, ale zbavíme se jím nepříjemného
omezení maximální délky.
Konstrukci uvedené v předchozím odstavci chybí k dokonalosti jediná věc. Dosud
nevíme, jak vytvořit pole, jehož délku zjistíme až v době běhu programu (run-timu).
Umíme dosud vytvořit pouze pole délky známe v době kompilace (compile-timu). Proto
zavedeme další naprosto přirozenou věc. Uvědomíme si, jak jsou pole reprezentována
v paměti. První věc, která nás napadne, je, že pole v paměti reprezentujeme tak, že
„urafnemeÿ kus paměti a do tohoto „urafnutéhoÿ místa skládáme postupně jeden prvek
za druhý. Jak poznáme, kde máme naše pole hledat? Jednoduše. Ukážeme si na jeho
začátek. Toto je motivace asi tak nejsložitější (přesněji jediné ne zcela zřejmé) pasáže
jazyka C, tedy pointerů.
Jak bylo řečeno na začátku kapitoly, pointer není nic jiného, než ukazatel do paměti,
tedy ukazatel na nějakou konkrétní adresu. Jelikož nás v současné chvíli zajímá zejména
jak jsou implementovány řetězce, omezíme se prozatím na pointer na chary (ukazatel,
který ukazuje na začátek posloupnosti znaků, což je snad každému jasné, že není nic
jiného, než kýžený řetězec). Chceme-li vyrobit proměnnou typu ukazatel na char (dále
char-pointer), napíšeme: char* a;
Nyní umíme vyrobit proměnnou typu char-pointer. Co ale dál? První věc, kterou
patrně budeme chtít udělat, je zinicializovat ji. Jedna z možností, jak toho dosáhnout,
je přiřadit do ní řetězec. Řetězce se v jazyku C uzavírají zásadně do uvozovek!!! Následuje jednoduchý příklad, který vyrobí řetězcovou (tedy char-pointerovou) proměnnou a,
přiřadí do ní řetězec ahoj a tento řetězec vypíše:
Příklad 29:
#include <stdio.h>
char*a="ahoj";
int main(){
printf(a);
return 0;
}
Nechceme se sice ňoumat ve vnitřnostech počítače, ale musíme toho učinit, chcemeli porozumět fungování řetězců v jazyku C (jelikož tento nebyl navrhován jako hračka
pro děti, ale napřed jako recese a pak jako prostředek vážného programování). Jak jsme
si říkali, pointer (a tedy i char-pointer) není nic jiného než ukazatel. V proměnné a z
minulého příkladu tudíž najdeme pouze odkaz na jistou adresu v paměti, přestože do
a přiřazujeme příčetně vyhlížející řetězec. Ve skutečnosti se však stalo toto: Překladač
zjistil, že má vyrobit ukazatel na char a zinicializovat. Inicializace pak vypadala pouze
tak, že do proměnné a přiřadil adresu začátku přiřazovaného řetězce.
Příklad 30: Vidíme obrázek paměti, zleva doprava adresy narůstají, v paměti je naznačeno umístění řetězce a pointer na něj.
– 23 –
...
adresa 1
...
a h o j \0
...
0xffffffff – konec paměti
řetězec
pointer na text
Pointery implementují stejné funkce, jako pole (tj. lze v nich vyhledávat podle indexu
operátorem hranatých závorek podle pravidel uvedených v minulé podkapitole). Chcemeli tudíž řetězce porovnávat, musíme porovnávat znak po znaku, chceme-li změnit řetězec
pod příslušným pointerem, stačí jej nasměrovat na řetězec jiný.
Všimněte si, že nejjednodušší cesta, jak se pohybovat v řetězci reprezentovaném pointerem jako v poli, je prosté sčítání pointerů. Toto snad není bezpodmínečně nutné pochopit, ale programování to poněkud ulehčí. Proto má smysl pointery nejen sčítat či odečítat,
ale zejména inkrementovat a dekrementovat, což reprezentuje posun pointeru na další (v
našem případě) znak. Děláme-li pointer na jiný typ, než char, což jsme dosud neřekli, že
je možné, budeme inkrementací posunovat o celou délku příslušného typu.
Příklad 31:
#include <stdio.h>
char*a,*b;
avetsinezb(char*a,char*b)
{
int i=-1;
while(a[i]) /* Dokud a nekonci */
{
if(a[i]>b[i]) /* a je vetsi */
return 1;
if(a[i]<b[i]) /* b je vetsi */
return 0;
i++;
}
return 0; /* Retezec a nikdy nebyl vetsi nez b
a uz jsme na konci aspon jednoho */
}
int main()
{
a="ahoj";
b="bye";
if(avetsinezb(a,b))
printf(a);
else
printf(b);
return 0;
}
Cvičení 1: Zkoušejte psát programy manipulující s řetězci jako s poli znaků.
Máme-li řetězec obsahující numerickou hodnotu (což se stává například při načítání
znaků z klávesnice, kdy načteme řetězec, který chceme interpretovat), může nás spíše
než obsah řetězce zajímat příslušná numerická hodnota. Ač napsat konverter řetězce do
integeru by pro vás mělo být téměř cvičením, můžeme k tomuto účelu použít funkce atoi.
Prototyp: int atoi (char* retezec);
– 24 –
Funkce (zcela očekávaně) přijímá jeden řetězcový argument a vrací celé číslo vyextrahované z argumentu. Funkce má prototyp uložený v souboru stdlib.h, je tudíž při
jejím použití potřeba na začátek zdrojového textu přidat #include <stdlib.h>.
Příklad 32:
#include <stdio.h> /* kvuli printf */
#include <stdlib.h> /* kvuli atoi */
char*a="10";
int i;
int main()
{
i=atoi(a);
if(i==10)
printf("Cislo je deset\n");
else
printf("Cislo neni deset\n");
}
Cvičení 2: Implementujte funkci atoi při znalosti toho, že číslo nula má ASCII-hodnotu
48, za nulou následují další čísla až do ASCII-hodnoty 57 pro číslici devět.
A nyní hurá na konkatenaci řetězců. Napřed menší přípravička:
5.3.1 Funkce malloc a free
Jak jsme říkali, při manipulaci s řetězci někdy potřebujeme vyrobit něco jako pole
předem (v době kompilace) neznámé velikosti. Problém vyřešíme pomocí pointerů tak, že
uhryzneme (naalokujeme) kus paměti. Alokaci provedeme pomocí funkce malloc, která
je popsána (pro překladač) v souboru malloc.h. Funkci malloc zadáme jako argument
počet bytů (charů), které chceme naalokovat. Nazpět dostaneme pointer, o němž víme,
že od něj dále najdeme příslušné množství volné paměti, se kterou můžeme manipulovat.
Přestaneme-li dotyčné místo potřebovat, zavoláme funkci free, která jako argument dostává pointer (kdysi vrácený funkcí malloc). Ta místo pod určené zadaným pointerem
odalokuje.
Potřebujeme-li změřit délku řetězce, můžeme použít funkci strlen, které jako argument předáme pointer na začátek řetězce a funkce vrátí údaj o jeho délce (jedná
se o počet nenulových znaků od začátku řetězce, čili řetězec retez má znaky indexované 0...(strlen(retez)-1), na pozici strlen(retez)) je nula, která řetězec ukončila.
Ekvivalentně si řekněme, že strlen vrací index prvního znaku nula, na který najede. Opět
si jako snadné cvičení můžete napsat funkci mystrlen, která poleze řetězcem a uvidí-li
znak nula, vrátí offset, na který se dostala. Rozmyslete si tudíž, že chcete-li naalokovat
místo, do kterého řetězec okopírujete, musíte alokovat prostor v délce strlen(retez)+1.
K čemu může být kopírování řetězce dobré objasní následující cvičení. Než je ovšem
zadáme, předvedeme si jako příklad onu kýženou konkatenaci dvou řetězců:
Příklad 33:
#include <stdio.h>
#include <malloc.h>
char* concatenate(char*a, char*b)
{
int i=0,j=0;
char*c=malloc(strlen(a)+strlen(b)+1);
while(a[i])
c[i++]=a[i++];/* kopiruj nenulove znaky a do c */
while(c[i++]=b[j++]);
return c;
– 25 –
}
int main()
{
char*a="ahoj ";
char*b="nicemo!";
char*c;
printf("Prvni retezec je: %s\n",a);
printf("Druhy retezec je: %s\n",b);
c=concatenate(a,b);
printf("Konkatenovane je to: %s\n",c);
return 0;
}
Připouštíme, že právě předvedená funkce concatenate je poněkud ďábelská, proto si ji
mírně rozeberme. Přijímáme dva argumenty, char-pointery, tedy řetězce. Poté, co naalokujeme proměnnou c v délce rovné součtu délek konkatenovaných řetězců + 1 (protože na
konci potřebujeme místo na znak 0), nastupuje první while-cyklus (ten přehlednější). Tam
kopírujeme obsah prvního řetězce. Podmínka zajišťuje, aby se okopíroval začátek řetězce
bez koncové nuly řetězce a. Nyní chceme za konec okopírovaného řetězce a do proměnné
c okopírovat jednotlivé znaky z řetězce b, tentokrát v mezích možností včetně závěrečné
nuly. No a přesně to dělá druhý while cyklus. Ten má přiřazení přímo v podmínce, tedy
ve chvíli, kdy se zjistí, že jsme v řetězci b najeli na nulu, je tato již přiřazena.
Cvičení 3: Napište program, který se nějakým způsobem dostane k řetězci (třeba v něm
zakompilovanému nebo lépe načtenému z klávesnice), tento řetězec si zapamatuje, otočí a
otočený vypíše (tedy vypíše jednotlivé znaky od konce k začátku) a pak (na další řádek)
ještě vypíše původní řetězec. Nesmíte dělat žádné závěry o délce zadaného řetězce (tj.
použijte funkce malloc).
Ve chvíli, kdy již obsah naalokované proměnné nepotřebujeme, je vhodné pod pointerem uklidit pomocí funkce free. Následující příklad snad za vše hovoří:
Příklad 34:
#include <stdio.h>
#include <malloc.h>
char* concatenate(char*a, char*b)
{
int i=0,j=0;
char*c=malloc(strlen(a)+strlen(b)+1);
while(a[i])
c[i++]=a[i++];/* kopiruj nenulove znaky a do c */
while(c[i++]=b[j++]);
return c;
}
int main()
{
char*a="ahoj ";
char*b="nicemo!";
char*c;
printf("Prvni retezec je: %s\n",a);
printf("Druhy retezec je: %s\n",b);
c=concatenate(a,b);
printf("Konkatenovane je to: %s\n",c);
free(c); /* Zde jiz obsah c nepotrebujeme */
– 26 –
return 0;
}
Pozor, freeovat nelze staticky naalokované řetězce, tedy následující příklad je špatně!!
Příklad 35: (varovný)
int main()
{
char*a="ahoj";
free(a); /* Ted jsme to zmastili! */
}
V jazyce C lze udělat pointer prakticky na cokoliv, není ovšem pointer jako pointer.
Kupříkladu na SPARCu je zakázaný nezarovnaný přístup do paměti, tedy datový typ netriviální velikosti (větší než 1 byte) musí začínat od adresy tvaru k x, kde k je velikost
dotyčného typu a x celé číslo, nedodržení tohoto pravidla vyústí v Bus-error. Aby bylo
možné vyrábět funkce, kterým je jedno, na co ukazuje jimi zpracovávaný pointer, existuje
tzv. generický pointer, tedy ukazatel na typ void. Ten pouze ukazuje do paměti a není
možné jej dereferencovat (koukat pod něj). K čemu je tedy dobrý? K tomu, abychom
jej pomocí operátoru přetypování (typové konverze) zkonvertovali do jakéhokoliv netriviálního datového typu. Ukazatel na void je zkrátka normální pointer, který neukazuje
na konkrétní data, ale který lze přetypovat na jakýkoliv jiný (negenerický) poiner. Proto
prototypy výše uvedených funkcí vypadají takto:
Prototyp: void* malloc(int delka);
Prototyp: void free(void*ukazatel);
Prototyp: int strlen(char* retezec);
Čili první dvě funkce manipulují s jakýmkoliv ukazatelem, nejen s řetězci, třetí funkce,
jelikož řetězce prohledává, si říká o argument přímo ve tvaru ukazatele na znak.
5.4 Pointery obecné
V jazyce C lze udělat pointer vpodstatě na cokoliv. Nejen na typ char, ale klidně
na integery, floaty, funkce (o tom si možná povíme později), ale hlavně na struktury.
Pointery ovládáme zejména pomocí znaků hvězdička (*) a ampersand (&). Definujeme-li
proměnnou typu pointer na něco, napíšeme napřed jméno typu, hvězdičku a jméno proměnné. Tedy jak jsme viděli v předchozí podkapitole kupříkladu: char * a;. Analogicky
píšeme třeba long * b; nebo struct clovek*osoba;
K čemu mohou být pointery dobré? Užijeme je zejména, máme-li pracovat s předem
neznámým počtem prvků. Například máme-li reprezentovat telefonní seznam, nevíme
předem, kolik lidí do něj chceme vložit. Vyrobíme proto pro každého člověka zvláštní
instanci struktury clovek tak, že pomocí funkce malloc urafneme kus paměti o velikosti oné struktury. Jak určíme, kolik paměti potřebujeme na různé datové typy (zvláště
struktury)? Snadno. Pomocí makra sizeof. Makru sizeof předáme jako argument jméno
datového typu a makro spočítá kolik bytů (přesněji obecných paměťových buněk) potřebujeme. Pak strukturu vyplníme. Jen musíme dát pozor, abychom pointer na naalokovaný
kus paměti neztratili (v každém okamžiku si na onen kus musíme odněkud ukazovat, jinak se k němu už nikdy nedostaneme a vznikne garbage, kterou po nás uklidí až systém
v okamžiku konce programu, ziterujeme-li to mockrát, paměť dojde a náš proces patrně
špatně dopadne).
– 27 –
Máme-li pod pointerem naalokované místo, přistupujeme do něj unárním operátorem
hvězdičky (*):
Příklad 36:
#include <stdio.h>
int main()
{
int*a=malloc(sizeof(int));
*a=10;
printf("Proměnná a ukazuje na adresu %d",a);
printf("Na teto adrese je hodnota %d",*a);
}
Chceme-li naopak pointer nasměrovat na nějaké místo, použijeme operátor ampersandu. Uvědomte si, že pointer jen ukazuje po paměti, tudíž pokud si jím ukážeme na
nějakou proměnnou a pak pod něj zapíšeme, změníme oné proměnné obsah. Potřebujeme
ještě vědět, že operátor vzetí reference je unární ampersand (&) a používá se asi takto:
Příklad 37:
#include <stdio.h>
int main()
{
int i=10,j=100,*a;
printf("V promenne i je %d.\n",i);
printf("V promenne j je %d.\n",j);
printf("Coz snad nikoho neprekvapi\n");
a=&i; /*
*a=50;/*
a=&j; /*
*a=60;/*
Ukaz si na promennou i
Zapis pod a hodnotu 50
Ukaz si na promennou j
Zapis pod a hodnotu 60
*/
*/
*/
*/
printf("V promenne i je ted %d.\n",i);
printf("V promenne j je ted %d.\n",j);
}
Jak vidíte, pomocí jednoho pointeru (a) jsme si ukázali postupně na dvě proměnné
a zapsali do nich jinou hodnotu. Tato technika se používá k tomu, k čemu se v Pascalu
používalo předávání argumentů referencí. V jazyku C je předání argumentu referencí vyloučeno, parametry se zásadně předávají hodnotou. Chceme-li umožnit funkci zasahovat
pod cizí proměnné, předáme jí na ně v Pascalu referenci, v jazyce C pointer. Předveďme
si ekvivalentní programy v Pascalu a v jazyce C.
Příklad 38: (Pascal)
program reference;
var
x:integer;
y:integer;
procedure zapis(var a:integer;var b:integer);
begin
a:=50;
b:=60;
end;
– 28 –
begin
x:=10;
y:=100;
writeln(’X je ’,x,’ Y je ’,y);
zapis(x,y);
writeln(’Ted je X ’,x,’ a y ’,y);
end.
Příklad 39: (Jazyk C)
#include <stdio.h>
int x=10,y=100;
void zapis(int * a, int*b)
{
*a=50;
*b=60;
}
int main()
{
printf("x je %d, y je %d\n",x,y);
zapis(&x,&y);
printf("Ted je x %d, a y %d\n",x,y);
}
Nyní si ukažme, jak se manipuluje v jazyce C s dynamicky alokovanými strukturami.
Je to analogické všemu, co jsme již viděli. Pointer je třeba nasměrovat na kus paměti,
ve kterém máme allokátorem zaručeno, že smíme dovádět v potřebném rozsahu, pak
pointer dereferencujeme operátorem hvězdičky (čímž získáme strukturu) a do té následně
přistupujeme operátorem tečky podobně jako v Pascalu.
Příklad 40: (částečně varovný)
struct clovek*osoba;
osoba=malloc(sizeof(struct clovek));
(*osoba).jmeno="Karel";
(*osoba).vek=50;
osoba=malloc(sizeof(struct clovek));
/* Zde jsme ztratili obsah minuleho pointeru */
(*osoba).jmeno="Jizicek";
(*osoba).vek=1;
free(osoba);
Okomentujme si předchozí varovný příklad. Vyrobíme proměnnou typu struct clovek *. Na druhém řádku tento pointer nasměrujeme někam do paměti, kde máme allokátorem zaručeno, že můžeme tuto strukturu vyplnit. Následují dva řádky vyplňování.
Operátorem hvězdičky (dereference) koukneme pod pointer a máme strukturu. Závorky
nejsou samoúčelné, protože postfixní operátory obecně mají vyšší prioritu než prefixní,
což je i případ interakce hvězdičky a tečky v našem příkladu. Abychom nemuseli kvůli
každému zápisu pod pointer na strukturu psát kulaté závorky (protože se to stává celkem
často), existuje operátor šipičky (->), který dělá přesně totéž, tedy (*osoba).vek=50; je
totéž jako osoba->vek=50; Nadále tedy budeme používat druhou možnost pro celkovou
větší eleganci.
– 29 –
Dále (na pátém řádku příkladu) ukazatel osoba přesměrujeme na jiné místo, kde
máme opět zaručeno, že se do něj struktura clovek vejde. V této chvíli si na původní
obsah odnikud neukazujeme, tudíž se ztratil. Jedná se o typickou začátečnickou chybu,
ale příklad ukazuje poměrně správnou manipulaci s dynamicky alokovanou strukturou
včetně správného odallokování na konci. Opravdu funkci malloc zadáváme jako argument
jen struct clovek, nikoliv struct clovek *, protože chceme místo o velikosti struktury (na které pointer nasměrujeme), nechceme allokovat prostor velikosti pointeru na
strukturu (ten by byl v tomto případě patrně podstatně menší a snadno bychom špatně
dopadli).
Nyní si ukážeme jeden ze způsobů organizování dynamických dat v paměti, tedy
předpoklady jsou takové, že máme do paměti naskládat předem neznámý počet záznamů.
Samozřejmě je možné skladovat data v paměti mnoha způsoby, následující konstrukce je
ale rychlá na implementaci (o to horší teoretické vlastnosti ale má za běhu).
5.5 Spojové seznamy
Spojový seznam je datová struktura sestávající z „vagonkůÿ, které si na sebe postupně
ukazují. Existuje mnoho variant jak spojový seznam implementovat podle toho, k čemu
jej potřebujeme. Jedna z možností je zapamatovat si začátek seznamu, z počátečního
vagonku ukazuje pointer na další a tak dále, až na posledním si nějakým způsobem
označíme, že už jsme na konci. Takhle nějak vypadá kupříkladu fronta v obchodě. Každý
dobře ví, kdo je poslední (a běda, jestli si nově příchozí posledního splete). Každý, kdo
stojí ve frontě, velice dobře ví, kdo je před ním. A první si už ukazuje na prodavače, který
se pozná podle bílé zástěry, či visačky.
Jiná možnost, jak implementovat spojové seznamy, je odkazy mezi jednotlivými vagonky zacyklit. Toto si můžeme představit jednou z typických formací v country-tancích,
kdy jednotlivé páry stojí dokola jeden za druhým. Tanečnice při takových rituálech obvykle proudí směrem dozadu, tanečníci kupředu, to ale není podstatné. Podstatné je, že
tady každý musí vědět, kdo je před ním. Tato varianta spojového seznamu nemá intuitivní začátek, každopádně si jej můžeme nějak dodefinovat (třeba jednomu páru dáme
klobouk a to, že jsme celý seznam prohlédli poznáme podle toho, že koukáme na osobu s
kloboukem).
Další možností implementace spojového seznamu je obousměrný spojový seznam.
Každý vagonek si tentokrát pamatuje, kdo je před ním a i za ním. BFU-motivaci si udělejme rovnou pro cyklickou variantu a může jí být třeba když děti v mateřské školce
tancují kolo mlejnský. Každý má jednoho souseda vpravo, jednoho souseda vlevo. Situace, kdy by se z tohoto seznamu ztratila data, odpovídá tomu, že by nějaké dítě z kola
mlejnskýho vypadlo. Mimochodem daleko častějším problémem je, že kolo rozpojíme a
přejdeme na neexistujícího souseda.
Doteď jsme si hráli, nyní začněme to hraní poněkud formalizovat. Začátek spojového
seznamu poznáme snadno, budeť naň ukazovat nějaký pointer. Jak ale poznat konec?
Máme-li necyklickou variantu, je obvyklá implementace taková, že poslednímu prvku
nastavíme jako následníka nulu. Proč zrovna nulu? Protože ve většině civilizovaných operačních systémů je začátek paměti vyhrazen a alokátor nám tudíž nikdy nevrátí nízké
adresy. Například v reálném režimu na PC byla na začátku paměti tabulka vektorů přerušení, která popisovala, kde se nachází k přerušením příslušné ovladače (handlery) a
do této tabulky neměl nikdo co zapisovat (ovšem ochrana jaksi chyběla). Dnes v době
virtuálních pamětí nebývá problém na (virtuální) začátek paměti namapovat naprosto
cokoliv, včetně prázdného místa. Pamatujte si tudíž obecnou poučku: Nulový pointer
nedereferencujeme!
Teď, když už víme, jak poznat v necyklické variantě začátek a konec si povězme, jak
určit totéž ve variantě cyklické. Tam je situace ještě jednodušší. Přecházíme postupně po
kruhu a zastavíme, až znovu dojdeme na počáteční vagonek.
– 30 –
Nyní si ilustrujme, jak mají jednotlivé implementace vypadat (příklad vždy popisuje
seznam o čtyřech prvcích):
Jednoduchý spojový seznam
a
c
b
d
Každý prvek ukazuje na následníka, na prvního ukazuje pointer zvenku.
Cyklický spojový seznam
a
b
c
d
Opět na prvního ukazuje pointer zvenku, ale poslední neukazuje na uzemnění, ale na
prvního.
Obousměrný spojový seznam
a
b
c
d
Jako první příklad, jen si každý vagonek ukazuje i na předcházejícího.
Obousměrný cyklický spojový seznam
a
b
c
d
A nyní k samotné technické realizaci. Každý vagonek bude reprezentován strukturou,
která bude obsahovat pointer na předchůdce (či předchůdce a následníka).
Jednoduchý příklad manipulace se spojovými seznamy můžete najít v souboru spojaky.c umožňujícím implementovat oba druhy obousměrných spojových seznamů. Chceteli vidět použití těchto funkcí, je v programku spojak majícím funkci main v souboru aplspoj.c, jak vidíte v Makefilu, linkují se do něj oba dva céčkové zdrojáky (gcc -o spojak
spojaky.c aplspoj.c). Dále je kvůli prototypové kontrole přibalen i soubor spojaky.h
(viz kapitola Správa velkých projektů).
V souboru spojaky.c jsou implementovány tyto funkce:
create – zřídí necyklický obousměrný spojový seznam,
crecir – zřídí cyklický obousměrný spojový seznam,
add – přidá prvek na začátek seznamu, pozor, tato funkce je předepsána pro jed
nosměrné seznamy!
addboth – přidá prvek na začátek obousměrného spojového seznamu (cyklického
stejně jako acyklického),
sortadd – zatřídí prvek do seznamu sestupně podle velikosti. Aby tato funkce
– 31 –
udržovala seznam setříděný, nesmí se přidávat pomocí funkce add, ale jen a jen
pomocí sortadd.
removeboth – vyjme prvek z obousměrného (cyklického i necyklického) spojového
seznamu. Najde prvek s příslušným klíčem a dotyčný záznam vymaže. Je-li v
seznamu více prvků stejného klíče, vymaže jen první výskyt.
sort – setřídí spojový seznam bubblesortem, který je sice zoufale neefektivní, ale
snadný na implementaci.
Celá tato implementace je pro jednoduchost omezena pro klíče kladné celé (klíč nesmí
být nula, protože nula je zarážka a záporná nesmí být kvůli korektnosti třídění).
Rovněž pro implementační jednoduchost je ovládání poněkud zvláštní. Má-li se při
příkazu zadat číslo, je třeba toto zadat ihned bez mezery za číslem příkazu, čili kupř.
pro zadání čísla 5 do spojáku zadáme číslo 15 (jedna jako přidej, zbytek, tedy pět, jako
přidávané číslo).
6. Některé funkce obvykle přítomné v jazyce C
V této kapitole si popíšeme několik funkcí běžně používaných při programování v
jazyce C ovládajících vstup a výstup. Všechny se nacházejí v knihovně jménem libc.
6.1 Funkce printf
Funkce printf je reprezentována tímto prototypem:
int printf(const char*format,...);
Tedy printf je jedna (dokonce velmi typická) funkce s proměnlivým počtem argumentů. Definování funkcí s předem neurčeným počtem argumentů věnujeme celou kapitolu, teď se pouze několik takových funkcí naučíme používat. V prvním argumentu
(povinném) mimo jiné popíšeme počet a formát následujících argumentů. Prvním argumentem je povinně řetězec. Téměř všechny znaky v něm obsažené budou vytištěny, jsou
ovšem speciální sekvence. Tyto začínají zejména backslash (zpětné lomítko, zvrhlítko
alias atd.), za zpětným lomítkem může následovat znak, který má ve formátovacím řetězci zvláštní význam, ale my nechceme využít jeho schopností, nýbrž jej vytisknout. Dále
mají zvláštní význam dvojice nn, nt a nr, první reprezentuje znak LINE-FEED, tedy
posune kurzor na další řádek, druhý znak tabelátoru a třetí znak CARRIAGE-RETURN,
který vrátí kurzor na začátek řádku. Někdy LINE-FEED kurzor na začátek řádku posune,
někdy ne (rozdíl mezi UNIXem a DOSem).
Další významný znak je znak procenta (%). Za znakem procenta může následovat
poměrně komplikovaná sekvence. Sám znak procenta je zinterpretován jako příslib dalšího
argumentu. Sekvence pak popisuje, jak má být s oním argumentem naloženo. Končí
znakem, který určuje typ argumentu. Znaky určující typ (ukončující sekvenci) mohou
být tyto:
d, i – desítkové číslo (celé znaménkové),
o – osmičkové číslo,
u – celé desítkové neznaménkové,
x, X – celé šestnáctkové (velikosti si odpovídají),
e, E – double tvaru d.dddeexponent - velikosti e si odpovídají,
f, F – float stejného tvaru,
– 32 –
c – znak (char),
s – řetězec,
p – pointer (konkrétně void*) v šestnáctkovém zápisu.
příklad:
Mezi znakem procenta a znakem určujícím typ mohou být:
znaky určující způsob vycpávání
délka výstupního řetězce – přesnost (tedy počet výstupních znaků),
...
0 – vycpat nulami,
(mezera) – vycpat mezerami,
...
V jazyce C se argumenty ukládají na zásobník od konce (v Pascalu od začátku), což
v současném okamžiku zní jako technický detail, ve skutečnosti je to klíčová finta, která
umožňuje tuto techniku při tvorbě funkcí s předem neurčeným počtem argumentů. Dále
z toho plyne, že dáme-li argumentů moc (a z nějakého důvodu to kompilátorem projde),
použijí se argumenty počáteční.
Příklad 41:
#include <stdio.h>
int main()
{
int a=5;
double b=10;
float c=10;
printf("Tisknu retezec: %s\n","ahoj");
printf("Retezec na 10 mist: %10s\n","ahoj");
printf("Desitkove cislo: %d \n",a);
printf("Cele cislo na 5 mist vycpane nulami: %05d\n",a);
printf("Necele cislo: %f\n",c);
printf("Double %e\n",b);
printf("Tisknu char: %c\n",’x’);
return 0;
}
Chceme-li zkonvertovat řetězec na číslo, použijeme výše popsanou funkci atoi. Chcemeli postup otočit, tedy vyrobit z čísla řetězec (aniž bychom ho tiskli funkcí printf), použijeme funkci sprintf s podobnou syntaxí jako printf. Jen jako první argument určíme, do
kterého řetězce se má výsledek uložit.
Příklad 42:
#include <stdio.h>
int main()
{
int a=5;
char vysl[80];
double b=10;
float c=10;
sprintf(vysl,"Tisknu retezec: %s\n","ahoj");
– 33 –
printf("%s",vysl);
sprintf(vysl,"Retezec na 10 mist: %10s\n","ahoj");
printf("%s",vysl);
sprintf(vysl,"Desitkove cislo: %d \n",a);
printf("%s",vysl);
sprintf(vysl,"Cele na 5 mist vycpane nulami: %05d\n",a);
printf("%s",vysl);
sprintf(vysl,"Necele cislo: %f\n",c);
printf("%s",vysl);
sprintf(vysl,"Double %e\n",b);
printf("%s",vysl);
sprintf(vysl,"Tisknu char: %c\n",’x’);
printf("%s",vysl);
return 0;
}
Zde se příliš nezabýváme bezpečností a doufáme, že délka výsledného řetězce nepřesáhne 79 bytů. V praxi bychom místo funkce sprintf měli použít funkci snprintf, která
nám umožňuje omezit délku vraceného řetězce (o níž může být těžké dělat závěry).
Z bezpečnostních záležitostí si jen uvědomme, že není dobré používat funkci printf
přímo k tisku předem neznámého řetězce, tedy zadat neurčitý řetězec jako první argument
funkci printf, protože budou-li v něm znaky procenta (%), budou zinterpretovány jako
znaky řídící a funkce printf snadno může začít prohrabávat zásobník na místech, do
kterých jí naprosto nic není a způsobit třeba krach programu (na zanedbání tohoto faktu
existuje několik DOS-attacků).
6.2 Práce se soubory pomocí stdio.h
V jazyce C je několik možností, jak pracovat se soubory. Jedna z těch přenositelnějších
je jednotka stdio. Funkce z ní jsou obvykle uloženy v libc (tudíž se o jejich slinkování
nemusíme nijak zvlášť snažit). Pro používání této jednotky je (zejména v C++) zapotřebí
naincludovat hlavičkový soubor stdio.h.
Funkce implementované pro práci se soubory ve stdio používají strukturu FILE. S
touto strukturou pracují samy již implementované funkce, nemusíme se jí tudíž zabývat.
Popíšeme jen následující funkce.
Funkce fopen
Prototyp: FILE * fopen (const char*path,const char*mode);
Význam: otevře soubor path v režimu mode. Možné režimy jsou:
r – pro čtení,
r+ – pro čtení a zápis, ukazatel začíná na začátku souboru,
w – pro zápis; případný obsah se vymaže,
w+ – pro čtení i zápis; neexistující soubor se vytvoří, existující zkrátí na nulu,
a, a+ – připisování na konec, druhé umožní i číst.
Výsledkem volání je ukazatel na strukturu FILE.
Funkce fprintf
Tato funkce má syntax podobnou funkci printf, jako první argument vyžaduje údaj
o souboru, do něhož má zapsat (další argumenty, jsou stejné, jako u funkce printf.
– 34 –
Prototyp: fprintf(FILE*,const char* format,...);
Funkce fgets
Prototyp: char * fgets(char* str, int size, FILE* stream);
Načítá řetězec do konce řádku nebo souboru, konec řádku je součástí řetězce. Zda už
jsme na konci souboru zjistíme funcí int feof (FILE* soubor);.
Další funkce
Chceme-li načíst znak z klávesnice, použijeme funkce int getchar(void). Tato funkce
vrací integer, což je typ větší než char. Necharových hodnot funkce nabývá při mimořádných událostech, např. při konci souboru. Funkce int fgetc(FILE* stream) dělá v
podstatě totéž, co getchar, ale vstupem nemusí nutně být standardní vystup, nýbrž jím
může být cokoliv.
6.3 Převzetí argumentů programem
Argumenty programu říkáme případnému textu zadávanému na příkazové řádce za
názvem programu. Jedná se obecně o velice snadný způsob nastrkání dat programu (a
to jak ze strany programátora tak ze strany uživatele). V jazyku C je toto implementováno velice jednoduchým způsobem. Prototyp funkce main totiž vypadá takto: int
main(int argc, char*argv[]). Z proměnné argc zjistíme počet argumentů (prvním
argumentem je jméno programu), druhý argument funkce main je typu pole pointerů na
char, tedy zinterpretováno česky pole ukazatelů na řetězce. Jednotlivé řetězce jsou pak
tvořeny jednotlivými argumenty předanými programu. Je třeba mít na mysli, že první
argument odkazuje k názvu programu (proto je argc vždycky alespoň jedna), navíc v
jazyce C indexujeme pole od nuly, čili zadáme-li dva argumenty, v argc bude hodnota 3,
v argv[0] ukazuje na název programu, argv[1] odkazuje k prvnímu a argv[2] ke druhému
argumentu.
Příklad 43:
main(int argc,char*argv[])
{
int i=0;
printf("Argumentu je: %d\n a jsou to:\n",argc);
while(i<argc)
printf("%s\n",argv[i++]);
}
Zcela očekávaně není nutné argumenty funkce main pojmenovávat argc a argv (můžeme je pojmenovat třeba a a b), ale první varianta patří k programátorskému folklóru.
7. Struktury, unie, enumy, typedef
Struktury odpovídají pascalským recordům (tj. umožňují uchovávat více údajů na
jednom místě), unie umožňují uchovávat na jednom místě jen jeden údaj, ale můžeme se
rozhodnout, jakého typu, enum určuje výčtové prostředí.
7.1 enum
Definuje výčtové prostředí, tj. identifikátorům přiřazuje hodnoty. Různé identifikátory
dostávají různé hodnoty (v rámci jednoho prostředí).
– 35 –
Příklad 44:
enum pohlavi {muz,zena};
Chceme-li udělat proměnnou tohoto typu, postupujeme takto:
enum pohlavi a;
Následně můžeme zkoumat například:
if(a==muz)...
Místo enumů lze se stejným ohlasem použít typu int a konstant. Výhoda je, že při
použití konstant musíme jednotlivé celočíselné hodnoty přiřazovat ručně, prostředí enum
je přidělí za nás, navíc (v případě enumu) nevzniká nebezpečí, že pojmenujeme shodně
dvě různé konstanty. Naopak rozhodneme-li se používat konstant místo enumů, máme
možnost útočit na konkrétní hodnoty, v takovém případě ale musíme dost přesně vědět,
co děláme, jinak se dočkáme překvapení.
7.2 Unie
Máme-li hromadu hodnot různých typů, unie umožňuje ukládat každou jednotlivou
hodnotu. Tedy kupříkladu máme-li hromadu čísel, některá z nich celá, některá ne, můžeme
unií vyrobit entitu, která nám umožní uložit čísla všech druhů. Syntakticky postupujeme
takto:
Příklad 45:
union cislicka {
int integer;
long dlouhe;
float necele;
double ieee;
};
void aplikace()
{
union cislicka cislo;
cislo.integer=10;
cislo.ieee=7.56;
printf("Double je: %e\n",cislo.ieee);
}
Podstatné je, že do unie smíme uložit vždy jen jednu z možností. Současně si musíme
pamatovat, co jsme tam uložili (protože překladač nastrká jednotlivá políčka do paměti
přes sebe), čili pokud přiřadíme do cislo.ieee, je syntakticky správně následně číst třeba
cislo.dlouhe, ale sotva se tím dobereme toho, co známe z konstrukce record v Pascalu.
Analogií pascalských recordů jsou struktury, unie jsou něco zcela jiného, v Pascalu snad
ekvivalent nemají. Ostatně je otázka, k čemu by tam byly. V Pascalu jsou přesně dány
velikosti jednotlivých typů a tudíž je zřejmé, který typ na který můžeme konvertovat. V
jazyce C máme bezeztrátovou konverzi zaručenu jen v pár případech. K posílení jistoty
při konvertování můžeme tudíž použít unii.
– 36 –
7.3 Struktury
Struktury umožňují ukládat najednou několik údajů. Definice struktury se zahajuje
klíčovým slovem struct. Do struktury lze uložit najednou několik údajů libovolných
typů. Jak jsme již uvedli, jedná se o jistou analogii konstrukcí record v Pascalu.
Příklad 46:
struct clovek { char jmeno [20];
char prijmeni [50];
int vek;
enum pohlavi pohl;};
Instanci této proměnné vyrobíme takto: struct clovek osoba;
K přístupu do struktury používáme stejně jako v Pascalu operátor tečky:
Příklad 47:
osoba.jmeno="Karel";
osoba.vek=100;
osoba.pohl=zena;
...
7.4 Typedef
Při definování proměnné typu enum, unie i struktury je nutné psát klíčová slova
je rozlišující (tedy struct, union, resp. enum). Pomocí klíčového slova typedef můžeme
vyrábět nové datové typy, tedy typedef struct clovek clovicek; umožňuje používat
nadále clovicek jako jméno typu struct clovek (tedy definovat například clovicek
osoba;), typedef union cislicka numera;
Umožňuje vyrobit typ i na „ jeden zátahÿ, tedy jako postranní efekt při definici struktury. Zatím jsme se učili toto:
Příklad 48:
typedef struct clovek {
char jmeno [20];
char prijmeni [40];
int vek;} clovicek;
Strukturu ovšem není nutné pojmenovávat dvakrát, můžeme postupovat tak, jako
na následujícím příkladu, tedy definovat strukturu beze jména a pojmenovat ji až za
typedefem:
Příklad 49:
typedef struct {char jmeno[20];
char prijmeni[40];
int vek} clovicek;
Při použití této konstrukce ovšem musíme myslet na to, že dokud není typ nadefinován, nemůžeme jej použít, tedy pokud bychom mínili do struktury například nastrčit
– 37 –
pointer na sebe samu, není možno tuto konstrukci použít a je potřeba (alespoň při její
vlastní definici) použít jméno struktury uvedené za klíčovým slovem struct. Více viz
kapitolu 40 o spojových seznamech, které jsou jednou z typických aplikací struktur v
jazyce C.
8. Proměnlivý počet argumentů funkce
Zatímco v Pascalu existovaly čtyři funkce, které se vypořádaly s nepevným počtem
argumentů (write, writeln, read, readln) a programátor neměl možnost tvořit vlastní takové, v jazyce C je možné psát funkce s předem neznámým počtem argumentů. Chceme-li
takové funkce definovat, musíme naincludovat soubor stdarg.h (#include <stdarg.h>).
Z něj používáme zejména makra va start, va arg a va end.
Hlavička pak vypadá přibližně takto: navratovy typ jmeno(typ povinne, typ argumenty,...). Tedy lišit se začne v momentě, kdy za povinné argumenty přidáme tři
tečky. Dále potřebujeme proměnnou typu va list, což je speciální typ (specifikovaný
právě v stdarg.h) odkazující k seznamu oněch neurčitých argumentů.
Abychom mohli začít pracovat s předem neurčenými argumenty, musíme zavolat
va start a jako argumenty předat proměnnou typu va list a jméno posledního povinného argumentu.
Prototyp: void va start(va list sezn, posl);
Nyní máme seznam sezn zinicializován a můžeme číst hodnoty jednotlivých argumentů voláním makra va arg.
datovy typ va arg(va list sezn, datovy typ);
Zcela zjevně va arg nemůže být funkce, ale makro, protože funkci nemůžeme jako
jeden z argumentů předat datový typ! Výsledkem použití makra va arg je hodnota v datovém typu (zadaném jako druhý argument). Makro va arg nikterak netestuje, jestli načítáme hodnotu ve správném typu, ani zda jsme už nepřejeli konec argumentů! Zavoláme-li
makro va arg vícekrát, než jsme měli (než kolik bylo argumentů) výsledek není definován (což znamená, že může být naprosto jakýkoliv (každopádně se jedná o bezpečnostní
pochybení).
Máme-li argumenty přečteny a chceme práci s proměnlivými argumenty (makrem
va arg) ukončit, zavoláme va end a jako argument mu dáme jméno proměnné typu
va list, přes kterou jsme argumenty načítali:
void va end (va list sezn);
Příklad 50:
#include <stdio.h>
#include <stdarg.h>
void foo(char *fmt, ...)
{
va_list ap;
int d;
char c, *s;
va_start(ap, fmt);
while (*fmt)
switch(*fmt++) {
case ’s’:
/* string */
s = va_arg(ap, char *);
printf("string %s\n", s);
– 38 –
break;
case ’d’:
/* int */
d = va_arg(ap, int);
printf("int %d\n", d);
break;
case ’c’:
/* char */
/* need a cast here since va_arg only
takes fully promoted types */
c = (char) va_arg(ap, int);
printf("char %c\n", c);
break;
}
va_end(ap);
}
int main()
{
foo("sdc","ahoj",10,’a’);
printf("To bylo prvni volani\n\nTed to druhe:\n");
foo("ssss","string1","string2","string3","string4");
return 0;
}
Cvičení 4:
1. Napište funkci, která jako první argument dostane počet čísel, které má posčítat,
které má konkatenovat (a která to provede ;-) ).
2. napište program, který posčítá celá čísla zadaná mu jako argumenty,
3. napište program, který počítá kombinační čísla,
4. napište funkci, která dostane jako argument délku a pointer na pole oné délky, a
která posčítá čísla (třeba inty) v onom poli.
9. Překladače a jejich použití
9.1 Warningy a errory
Není-li překladač Pascalu spokojen se vstupem, ohlásí chybu (error), čímž dá najevo,
že s vaším zdrojákem skončil a že už nic dalšího od něj čekat nemáte. V Jazyce C je
to podobné, tedy je-li ve zdrojáku chyba (se kterou si překladač neví rady), také ohlásí
chybu. Oproti Pascalu se ale pokusí ještě zachytit a zkompilovat zbytek, kde, narazí-li
na další chybu, ohlásí další error. Toto má jasnou výhodu, že můžete po jednom pokusu
o kompilaci opravit všechny chyby, aniž byste kompilátor zbytečně tůrovali. Nevýhoda
se projeví zvláště u začátečníků ještě dříve, než tato výhoda. Problém je, že překladač
se pokouší zachytit a kompilovat dál. Pokud je ovšem zdrojový text na jednom místě
dostatečně zvrtaný, překladač ohlásí další chybu záhy. Tak se stane, že v důsledku jedné
chyby na člověka vypadne 50 errorů a ten prvotní, který celou tu kaskádu spustil a ze
kterého se člověk dozví nejvíc, zmizí z obrazovky a je potřeba jej ve výpisu od překladače
pracně hledat.
Kromě chybových hlášení (která jsou normou přesně specifikována) může překladač
vydat warning, který říká, že vstup sice nějaký smysl dává, ale je podezřelý. Co to tak
může znamenat? Představme si následující příklad:
– 39 –
Příklad 51:
int faktorial(int a)
{
int b=1;
while(!(a=0))
b*=a--;
return b;
}
Jako příklad jazyka C je to pěkné, leč nefunkční (funkce vždy vrátí jedničku). Kdo
zjistil, že proto, že v podmínce while-cyklu místo porovnání přiřazujeme? Ano, program
je syntakticky správně, nějaká semantika se pro něj také najde, tak proč by ho překladač
neměl přeložit? Také ho přeloží. Jen (v průměrném případě) vysype warning, že na řádku
3 máte přiřazení místo porovnání v podmínce, navíc v tomto případě bych od překladače
ještě očekával warning, že jsme vyrobili konstantní podmínku, a to vždy nesplněnou (což
si přiznejme, sotva může dávat smysl).
Překladač jazyka C (narozdíl od překladačů Pascalu) má právo výstupní kód optimalizovat. Co to znamená? Například napíšete-li podmínku, která je vždy splněna, překladač
ji z výsledného kódu vyhodí. Podobně může prohazovat pořadí, provádění instrukcí, v
žádném případě mu to ale neumožňuje měnit význam programu. Například pokud dvakrát po sobě přiřadíte konstanty do proměnných, nezáleží na pořadí, v jakém se tak stane.
Podstatné je, že mezi tím neděláte nic dalšího. Optimalizátor také může zjistit, že jste
napsali dvakrát tentýž kód, vygenerovat jej jen jednou a udělat do něj odkaz, zjistit,
že jste udělali smyčku s konstantním počtem opakování a rozbalit vnitřek jejího kódu
příslušněkrát za sebe. . . Program se optimalizováním má urychlit a v mezích možností i
zkrátit (délka programu ovšem nebývá tak podstatná, jako jeho rychlost, v té jednotlivé
překladače závodí).
Je-li program úspěšně přeložen, ještě to neznamená konec starostí, protože se krásně
mohlo stát, že jsme v dobré víře napsali program, který dělá něco úplně jiného (třeba že
místo faktoriálu vypisuje jedničku). Určit, zda program dělá to, co má, je algoritmicky
neřešitelný problém (tedy neexistuje algoritmus, který by to dělal), naopak určit, zda programy dělají každý něco jiného je algoritmicky řešitelné (nikoliv rozhodnutelné). Proto,
ukáže-li se, že náš program dělá něco jiného, než jsme chtěli, je potřeba tento opravit. Ke
zjednání nápravy je ovšem potřeba zjistit, kde se co nepovedlo. Kde se začínají objevovat
hodnoty, které jsme si nepředstavovali, kterémužto rituálu říkáme ladění. K tomu můžeme použít buďto ladících výpisů, které jsou velmi užitečné a v některých programech
vpodstatě nepostradatelné, nebo výkřiku moderní techniky v podobě debuggerů. Debuggery umožňují pouštět program postupně a po částech, tedy napřed jednu část, pak
další. . . Každý debugger má jiné schopnosti, nicméně obvykle umožňuje alespoň vypisovat hodnoty proměnných, provést řádek zdrojáku a přeskočit volání funkce. Některým
překladačům je potřeba říci, že chcete program s ladicími informacemi (jinak je překladač
může z taktických důvodů nepřibalit – aby program byl menší, rychlejší apod., současně
je vhodné ve verzi pro ladění vypnout optimalizace, jinak se budete divit, jak strašně
jste zdrojáky napsali (co všechno napadlo překladač po vás vylepšit). Nevypnete-li optimalizace, můžete zjistit, že program není prováděn postupně po řádcích tak, jak jste
napsali, že se odskakuje do kódu, který je na druhém konci programu (ale náhodou úplně
stejný). . .
9.2 GCC
Gnu C Compiler je překladač volně šířený včetně vlastních zdrojových textů (mimochodem překládaných sebou samým). Jedná se o řádkové rozhraní, ve kterém člověk
může jednotlivé akce provádět ručně a zjišťovat, co z překladače leze. Sestává zejména z
– 40 –
programů cpp (preprocesor jazyka C, který zinterpretuje například direktivy #include,
#define, #ifdef, #if, #endif... Padá z něj stále ještě poměrně čitelný text (stále
zdrojáky v jazyku C). Následuje samotný překladač nazvaný cc1, který zdrojový text
přežvýká do binárního tvaru tzv. object-filu, což už je (jak bylo právě řečeno) binární
soubor, tedy prostým okem nečitelný. V něm je již binární verze (tedy skoro spustitelná)
našeho programu. Problém je v tom, že v object-filech je jen a jen to, co jsme napsali,
tedy přeložená volně sypaná sada funkcí. Pokud voláme nějaké funkce, je na ně pouze
vygenerován odkaz, tyto funkce ještě nemusí být nutně zakompilované (zakompilované
jsou jen ty, které jsme sami napsali, ne ty, které za nás napsal někdo jiný). Proto, aby
bylo konečně možno program spustit, následuje linker (ld), který popadne objektové soubory, a označené knihovny, slepí dohromady, zjistí, odkud se co volá, odkazy na jména
funkcí z objektového souboru nahradí skutečnými odkazy do kódu (samozřejmě to není
tak snadné, protože ld je linker dynamický, umožňuje programy linkovat dynamicky a
také se tak typicky děje, tedy program cizí funkce neobsahuje, ale obsahuje stále odkazy
ven, tentokrát ovšem do tzv. dynamických knihoven, ze kterých příslušné funkce nahraje
zavaděč programu).
Abychom nemuseli tyto programy volat každý zvlášť, existuje program gcc, který je
podle našich pokynů volá za nás. Řekneme-li například gcc soubor.c, překladač spustí
preprocesor, překladač a linker a pokusí se vyrobit spustitelný soubor a.out. Jak lze
očekávat, gcc disponuje mnoha přepínači (featurami a optiony). Například gcc -E soubor.c spustí jen preprocesor, gcc -S zase vynutí výrobu assemblerového zdrojáku (ve
kterém se člověk může pokoušet zjistit, co se nepovedlo, snáz, než v binárním souboru).
Chceme-li program debugovat, musíme přidat option -g (dříve -ggdb, -gdwarf apod.
podle toho, čím jsme mínili běh zkoumat). Konečně, aby gcc nevyrobilo natvrdo soubor
a.out, můžeme ovlivnit optionem -o následovaným jménem výstupního souboru. Optimalizace zapneme -O (následuje úroveň, ve které chceme optimalizovat, což je číslo mezi
nulou a šestkou, nula znamená bez optimalizací, šestka se všemi optimalizacemi) jak vidíte, jazyk C je extrémně case-sensitive, toto je další případ, kdy na velikosti záleží. Další
důležitá dvojice optionů je -l a -L. První říká, s jakými knihovnami chceme program
slinkovat, druhý zase, kde je máme hledat. Například -lfl slinkuje program s knihovnou nazvanou libfl (kterou je potřeba používáme-li nástroj nazvaný flex určený pro
generování konečných automatů), -lm říká, že chceme přilinkovat knihovnu libm, která
implementuje matematické funkce, -L /usr/lib/moje/ zase nařídí, že knihovny se mají
hledat mimo jiné v adresáři /usr/lib/moje.
Příklad 52:
gcc -o program1 soubor.c -g
gcc -O3 -o program soubor.c -lfl
První řádka příkladu přeloží soubor.c, přidá ladicí informace a výsledný program
pojmenuje program1. Ve druhém případě bude výsledkem masívně zoptimalizovaný spustitelný soubor program, do něhož bude přeložen soubor.c a ještě k tomu překladači
říkáme, že chceme volat funkce z knihovny libfl.
Spadne-li program, systém umožní uložit (na disk) soubor popisující, jaký byl obsah
paměti v okamžiku, kdy program špatně dopadl (tzv. core). K čemu takový soubor může
být? Předně k tomu, abychom zjistili, co se v programu stalo, než spadl. Samozřejmě to
nebudeme dělat ručně, když to umí některé debuggery. Dále jím lze třeba utloukat místo
na zbytečně prázdném disku, protože core umí být klidně mnohasetmegový soubor.
Dojde-li na ladění, je k dispozici vysoce kvalitní debugger nazvaný gdb. Tento disponuje řádkovým rozhraním, se kterým člověk nějakou dobu srůstá, pak se od něj ale
těžko odděluje. Debužení zahájíme spuštěním programu gdb, jako argumenty mu obvykle
zadáváme (v tomto pořadí) jméno binárního souboru, který chceme ladit (spouštět) a
– 41 –
jméno core-souboru (chceme-li zkoumat konkrétní spadlý případ). K dispozici jsou mimo
hromadu jiných příkazy:
run (spustí program),
step (provede jednu instrukci,
jedná-li se o funkční volání, zastaví na začátku
volané funkce),
next (provede jednu instrukci, ale případnou volanou funkci provede celou),
list (vypíše kus programu – implicitně 10 řádků okolo místa, na němž se právě
vyskytujeme, zadáme-li jiné číslo, jedná se o číslo řádku, který chceme vypsat,
kolem něj je z každé strany dalších pět řádků),
print jmeno promenne (vypíše obsah určené proměnné),
set args prvni argument druhy argument ... (umožňuje předat laděnému programu argumenty),
break (následuje číslo řádku, na který chceme dát breakpoint, tedy vyrobit místo,
při jehož přecházení se provádění programu zastaví a my můžeme inspektovat stav
programu).
K dispozici je i plno dalších užitečných věcí, my ale nechceme psát další manuál ke
gdb, chceme pouze poněkud vysvětlit některé jeho základní funkce.
9.3 Visual C++
Tentokrát se jedná o komerční produkt společnosti Microsoft. Jedná se o monolitické
prostředí zaměřené na vývoj softwaru v jazyce C++. Chceme-li začít programovat, musíme se proklikat skrz menu, tedy říci, že chceme nový projekt (New/Project), následně
je třeba upřesnit, že chceme „Win32 Console applicationÿ, u některých verzí upřesníme,
že chceme „Hello world applicationÿ. Ještě vymyslíme jméno projektu (programu) a určíme adresář, kam jej chceme uložit. Prostředí nám vyrobí projekt s jedním souborem
obsahujícím funkci main. V tomto souboru pak můžeme dovádět a zkoušet své programátorské umění. Samozřejmě můžeme do projektu přidávat další soubory (případně ubírat).
Chceme-li kompilovat, je nejlépe vybrat v menu Build/Build all. Pokud chceme program
ladit, má Visual C++ vestavěný debugger, který se ovládá buďto v menu Debug, nebo
se otevře přes Build/Start Debug (pak se položka menu Build změní na Debug). Při ladění můžeme vkládat breakpointy (nejlépe myší klepnutím na lištu nalevo od příslušného
řádku), klávesou F5 program spustíme, je-li proram spuštěn, znamená F5 pokračování
v programu do chvíle, kdy se narazí na breakpoint, chceme-li program krokovat, máme
možnost použít klávesy F10 a F11, jejich funkčnost se liší v tom, že máme-li zavolat
funkci, klávesa F10 zastaví program až po jejím vykonání, klávesa F11 zastaví na jejím
začátku.
10. Komentáře k příkladům
Přiložené programy mají demonstrovat základní techniky programování v jazyce C.
Pojmenovávací konvence je taková, že jméno programu popisuje stručně jeho funkčnost.
Jelikož jazyk C umožňuje jednu konstrukci zapsat mnoha způsoby, jsou z instruktivních
důvodů některé problémy řešeny až třemi různými (ekvivalentními) způsoby. Například
generování kombinačních čísel. Jeden zdrojový text (kombin.c) ukazuje (alespoň dle mého
názoru) typické řešení v jazyce C, které ovšem třeba lidem, kteří právě přecházejí z Pascalu
nemusí být zcela přístupné. Aby bylo vidět, že jazyk C umožňuje alespoň to, co jazyk
Pascal, je přibalen i soubor kombin.pascal.c, kde jsou jednotlivé úkony realizovány
poněkud těžkopádněji konstrukcemi používanými v Pascalu (neříkám, že je dobré tak
psát kód v C). Aby bylo učiněno zadost těm, kteří chtějí jazyk C poznat důkladněji, je
u některých problémů ještě třetí možnost (např. kombin.prase.c), která demonstruje, co
– 42 –
všechno si může autor v jazyce C dovolit (opět neříkám, že je dobré takto programovat,
minimálně jako ukázka pro případ čtení cizích zdrojových textů to ale posloužit může).
Pro jednoduchost (aby polovinu programu nezabralo zadávání vstupu) přijímáme
argumenty zadané programu z příkazové řádky. Jejich přítomnost testujeme velice jednoduše tak, že se spokojíme se správným počtem argumentů, tedy případné nadbytečné
argumenty se ignorují, což je jednoduché, leč ne vždy nejšťastnější řešení (pro naše účely
ale bohatě postačuje).
Příklady jsou rozděleny do těchto kapitol:
Základy: Faktoriál (počítání faktoriálu), generátor permutace (vygenerujeme n-tou
permutaci v lexikografickém uspořádání, tedy srovnaných jako ve slovníku), kombinační
čísla (vygeneruje kombinační číslo popsané dvěma argumenty).
Práce s řetězci: Konkatenace (čili nalepení dvou řetězců za sebe), otáčení řetězce.
Pole: Počítání permanentu, kůň na šachovnici.
Třídění: Generátor anagramů.
Rekurze: Faktoriál rekurzí, kůň na šachovnici (figurkou koně máme proskákat celou
šachovnici).
Rozděl a panuj: Evaluace výrazu v prefixní notaci.
Ke zkompilování všeho by mělo stačit (tam, kde je program make) napsat make.
Povšimněte si toho, jak vypadá Makefile, který se používá pro údržbu rozsáhlejších děl.
Obvykle se používá trochu, ale jen trochu, odlišným způsobem.
10.1 Popis příkladů
Základy mají demonstrovat základní řídící struktury jako přiřazování do proměnných, rozhodování, cyklení, numerické výpočty. Předváděné programy řeší běžně známé
problémy, ke kterým by nemělo být třeba delšího vysvětlování. (Samozřejmě někdo by
mohl považovat za užitečnější příklad napsání třeba textového editoru, to je však problém
poněkud komplikovanější a svým způsobem by byl kontraproduktivní. Měl-li by někdo
takové tendence, nechť si poslouží na www.sf.net hromadou zdrojových kódů prakticky
použitelnějších programů, ve kterých se však rozhodně není snadné vyznat.) Faktoriál počítáme tak, že postupně násobíme čísla 1, 2, . . . n. Permutace generujeme lexikograficky
uspořádané. Generujeme-li k-tý prvek l-té permutace, víme, že zbylými čísly můžeme
index permutace posunout maximálně o (n k)! 1, musíme tudíž zaručit, abychom
ve zbytku nemuseli generovat permutaci indexu většího, než můžeme dosáhnout. Je to
vlastně obdoba vyjadřování čísla v číselných soustavách (desítkové, šestnáctkové, dvojkové), ale řád (základ) oné soustavy mezi čísly vždy klesne o jedna a čísla se „stále
přejmenovávajíÿ.
10.2 Práce s řetězci
Řetězec je v jazyce C realizován ukazatelem na typ char, tudíž můžeme v jednom
řetězci ukazovat na různá místa současně (jen si musíme uvědomit, že manipulujeme s jediným exemplářem řetězce!). Konkatenace je jasná – zkopírujeme do výsledného řetězce
napřed řetězec první a pak druhý. Proč nemůžeme hned okopírovat druhý řetězec za
první? Teoreticky bychom mohli, ale první řetězec by musel být naalokován v dostatečné
délce. Otáčení řetězce je rovněž jednoduché. Ukazujeme do řetězce dvěma ukazateli. Jedním postupujeme od začátku ke konci, druhým od konce k počátku a prohazujeme znaky
pod nimi. Když se ukazatelé potkají, je řetězec otočen.
– 43 –
10.3 Pole
Jazyk C zná jen jednorozměrná pole. Vícerozměrná pole lze realizovat buďto přes
násobné pointery (v programu pak narůstá kvalita hotelu až třeba k pěti hvězdičkám),
nebo přes pole velikosti součinu jednotlivých rozměrů. My používáme druhou možnost
a demonstrujeme ji na problému počítání permanentu. Permanent je definován téměř
stejně jako determinant, tedy
P Qa
n
i,π(i)
, kde Sn značí prvky permutační grupy (tedy
π∈Sn i=1
jednotlivé permutace) na n prvcích, π(i) značí i-té číslo v permutaci. U determinantu
musíme ještě onen součin vynásobit znamením permutace. Trochu překvapivé je, že ač
determinant má definici na pohled složitější, tak zatímco determinant (jak známo) lze
počítat v polynomiálním čase (modifikací Gaussovy eliminační metody), pro permanent
není znám žádný pronikavě lepší algoritmus, než samotná definice (a problém je znám
jako #P-úplný). Program vpodstatě jen překládá definici permanentu do jazyka C.
10.4 Třídění
Často se stává, že potřebujeme setřídit nějakou množinu údajů (např. telefonní seznam). Existuje několik způsobů třídění (většinu těch známějších, předpokládám, znáte).
Mezi třídicími algoritmy vynikají algoritmy, které třídí porovnáním, tedy tak, že porovnávají dvojice údajů. Mezi tyto algoritmy nepatří např. Bucketsort, kdy třídíme data
z omezené množiny (např. dny v týdnu, dny v měsíci. . .). Bucketsort využívá toho, že
údaje různých hodnot strkáme do různých pytlíků. Těžko bychom ovšem takto třídili
třeba racionální čísla. Tam nastupují algoritmy třídění porovnáním (mergesort, heapsort,
quicksort, bubblesort, shakesort. . .). Mimochodem jaká je složitost problému třídění porovnáním a proč? Operace porovnání většinou říká, zda a > b. Jakou složitost ale získáme,
vrátí-li porovnání jednu ze tří hodnot – třeba jedničku pro a > b; minus jedna pro a < b
a nulu pro a == b. Změní se asymptotická složitost problému třídění tímto porovnáním?
Jakou časovou složitost má který z vyjmenovaných algoritmů třídících porovnáním?
Náš příklad demonstruje generování anagramů. Anagramy byly oblíbeny středověkými matematiky a fyziky, kteří si určitým krátkým popisem svého výsledku zajišťovali
prvenství. V tomto krátkém popisu setřídili písmena podle abecedy a poslali některému
svému kolegovi, který v mnoha případech pořídil stejný výsledek (což není nikterak překvapivé s ohledem na to, že si tito matematici dopisovali), načež po rozluštění některého
z anagramů jeden z vědců sklidil obdiv, kdežto druhý výsměch, což vedlo ke konci nejednoho přátelství. Program dělá přesně to, co středověcí matematici s oním krátkým
popisem, tedy lexikograficky setřídí znaky a vysype na výstup. Aby nebylo potřeba složitějších datových struktur, je použito bublinkového třídění, kdy nižší hodnoty „bublají
nahoru„, zatímco vyšší hodnoty zvolna propadají dolů. Mimochodem narozdíl od středověkých matematiků, kteří údajně pořádali matematické souboje, kde si vzájemně zadávali
úlohy a kdo jich vyřešil víc, vyhrál, matematici starověcí pronesli tvrzení a prohlásili je
za jasné. Ostatní se zamysleli a po chvíli je též prohlásili za jasné. Z novodobých matematiků údajně stejně (jako starověcí matematici) postupoval indický matematik Ramanujan,
kterému se jeho slavné identity prý zdály ve snu a který se údajně nikdy nedozvěděl, že
v matematice se tvrzení většinou dokazují.
10.5 Rekurze
Používáme k vyhodnocení funkce v bodě n takovým způsobem, že funkční hodnotu
pro n == 1 známe, jinak zjistíme hodnotu v bodě n 1 a z její hodnoty spočítáme
hodnotu pro n. Nejjasnějším příkladem je počítání faktoriálu f (n), protože jak známo
f (n) = f (n 1) n. Podobně vhodnou funkcí pro počítání rekurzí je třeba exponenciála
e(n) = an . (Neříkám, že je šťastné ji tak počítat), kde e(n) = e(n 1) e. Pro počítání
rekurzí pochopitelně nejsou vhodné všechny funkce (např. ne sin, log.
– 44 –
Zformulujte nějaká tvrzení, popisující, které funkce lze rekurzí počítat s výhodou.
U koně na šachovnici používáme zvláštní modifikaci rekurze zvanou prohledávání
do hloubky (DFS – depth first search), kdy podle určitých pravidel (pohyb koně po
šachovnici) prohledáváme celou šachovnici a klademe si otázku, zda už jsme prohledali
bez návratu šachovnici celou.
Diskutujte, které problémy lze s výhodou řešit rekurzí (konkrétně co třeba hledání
konvexního obalu bodů v rovině, třídění, hledání maximálního vybalancovaného úseku
posloupnosti nebo generování všech permutací).
10.6 Divide et impera
Zvláštním způsobem použití rekurze získáváme techniku zvanou rozděl a panuj. Vychází z toho, že poštveme-li své nepřátele vzájemně proti sobě, nenadřeme se s nimi tolik.
Pro aplikaci v informatice nepřátele neštveme, ale pouze rozdělujeme. Napřed vyřídíme
jednu část, pak druhou, atd. Evaluace výrazu v prefixní notaci je typickou aplikací, kdy
víme, že buďto máme napřed ve výrazu binární operátor a za ním popis dvou argumentů
(operandů), nebo číslo. My postupujeme tak, že vidíme-li číslo, vrátíme jeho hodnotu.
Koukáme-li na operátor, tento utrhneme a řešíme dva stejné podproblémy (evaluace částí
výrazu – konec podvýrazu se pozná tak, že za operátorem přečteme správný počet operandů). Až jsou oba podvýrazy vyhodnoceny, provedeme operátor na získané vysledky.
Nálezem každého operátoru spouštíme vyhodnocení dvou nových podproblémů.
10.7 Vsuvka z trochu jiného světa
Mimochodem na kouzle metody divide et impera je založena mohutná (velmi zajímavá a v poslední době zkoumaná) třída on-line algoritmů, tedy algoritmů, které dostávají zadání po částech a o nichž se dokazuje, že udělá-li algoritmus o libovolné části
(začátku) vstupu určitý závěr, příliš se nesplete. Dosti se zkoumá např. problém rozvrhování. Uvažme variantu identických počítačů, kdy máme sadu úloh (s určenou dobou
počítání), sadu (identických, tedy stejně výkonných) počítačů a chceme rozvrhnout úlohy
na počítače tak, aby bylo vše spočítáno co nejdříve (čili aby námi sestavený rozvrh skončil co nejdříve). Motivace „on-lineovostiÿ této úlohy je jasná, uvědomíme-li si, že úlohy
můžeme dostávat v čase postupně. On-line algoritmus, který je jednoduchý a poměrně
dobrý, rozvrhuje hladově, tedy na počítač, který má v současném kroku rozvrh nejkratší
(jinými slovy na stroj, který by zatím skončil ze všech nejdříve). Tvrzení je, že v nejhorším případě získáme nejvýše dvakrát delší rozvrh, než optimum. Argument pro toto
tvrzení je jednoduchý. Vezměme počítač s nejdelším rozvrhem. Z jedné strany na nějakém
stroji musel proběhnout jeho poslední proces, z druhé strany optimální rozvrh musí být
aspoň tak dlouhý, jako rozvrh nejméně vytíženého počítače. Rozvrh nejdelšího počítače
byl bez posledního procesu v nějakém okamžiku nejkratší vyrobený (proto jsme přidali
ten poslední proces) a tudíž nejvýše tolik, co optimum, čímž získáváme nejvýše dvakrát
optimální hodnotu.
– 45 –
Doporučená literatura
BS – Bjarne Stroustrup: The C++ Programming Language,
BE – Bruce Eckel: Myslíme v jazyku C++,
JC – James O. Coplien: Advanced C++ Programming Styles and Idioms,
HS – Herb Sutter: Exceptional C++,
MV1 – Miroslav Virius: Pasti a propasti jazyka C++,
MV2 – Miroslav Virius: Programování v C++,
MV3 – http://kmdec.fjfi.cvut.cz/ virius/liter/litCpp.htm
– 46 –

Podobné dokumenty

ŘEŠENÍ KOLIZÍ FREKVENCE SÍTĚ VYSÍLAČŮ

ŘEŠENÍ KOLIZÍ FREKVENCE SÍTĚ VYSÍLAČŮ Frekvence je potřeba uložit tak, aby k nim byl snadný přístup, protože právě je budeme přiřazovat vysílačům. Ideální způsob je pole. Bohužel nevíme předem kolik frekvencí budeme mít k dispozici, ta...

Více

126 NOVINKY ZAHRANIČNÍ LITERATURY

126 NOVINKY ZAHRANIČNÍ LITERATURY in der Stadtbücherei Stuttgart [Ponor do informační na bídky : školení učitelů v Městské knihovně Stuttgart]. BuB-Jounal : Forum Bibliothek und Information. 2008, vol. 60, no. 2, s. 16-17. ISSN 034...

Více

5 textil

5 textil měření barometrického tlaku-historie 24 h, animovaná předpověď Měření teploty IN 0–50C, OUT –35+65°C, vlhkost IN 20–98%, paměť prmin/max, teplotní alarm, hodiny DCF s budíkem, pod­světlený displej,...

Více

Sbírka úloh z jazyka C

Sbírka úloh z jazyka C Procvičované učivo: ukazatele, práce s textovými řetězci, funkce, cykly Napište v jazyku C funkci int porovnej(char *t1, char *t2), která porovná předané textové řetězce a vrátı́ -1...

Více

1. test z PJC Jméno studenta: Pocet bodu

1. test z PJC Jméno studenta: Pocet bodu (a) Odstraní z řetězce písmena. (b) Z písmen v řetězci udělá čísla. (c) Z malých písmen udělá velká. (d) Prohodí velká a malá písmena. (e) V programu je chyba, a proto zřejmě skončí v nekonečné smy...

Více

CˇESKE´VYSOKE´UCˇENÍTECHNICKE´ DIPLOMOVA´PRA´CE

CˇESKE´VYSOKE´UCˇENÍTECHNICKE´ DIPLOMOVA´PRA´CE 7.14 Kód umožňujı́cı́ přetečenı́ zásobnı́ku III . . . . . . . . . . . . . . . . . . . . . . . .

Více