1. Úvod .........................................................
Transkript
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ČŮ
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...
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...
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, podsvětlený displej,...
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...
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...
CˇESKE´VYSOKE´UCˇENÍTECHNICKE´ DIPLOMOVA´PRA´CE
7.14 Kód umožňujı́cı́ přetečenı́ zásobnı́ku III . . . . . . . . . . . . . . . . . . . . . . . .