Poté, co jsme v předcházející lekci sestavili první funkční program v jazyce C++, můžeme se tentokrát blíže podívat na téma zpracování a běhu nativních programů. Seznámíme se také s datovými typy a proměnnými. Ján Hanák
Proces zpracování programu v jazyce C++
V minulé lekci jsme společnými silami vytvořili první program v jazyce C++ a rovněž jsme si podrobně popsali jeho syntaktickou strukturu. Ukázali jsme si, jak program přeložit (neboli zkompilovat) a následně spustit. Jaksi stranou jsme prozatím ponechali ryze technický pohled na zpracování vytvořeného programu napsaného v jazyce C++. To záhy napravíme, poněvadž chceme, abyste věděli, co všechno musí Visual C++ 2008 Express provést v zájmu vygenerování spustitelného (.exe) souboru našeho programu.
Proces zpracování programu v jazyce C++ můžeme rozdělit do následujících tří etap:
1. Předběžná úprava zdrojového kódu programu preprocesorem.
Zdrojový kód reprezentuje myšlenky programátora a slouží k implementaci vybraných algoritmů coby postupů řešících množinu problémových úkolů. Jako programátoři začínáme vždy analýzou řešeného problému a návrhem příslušného algoritmu, který bude splňovat předložené zadání. Algoritmy vyjadřujeme prostřednictvím zdrojového kódu, který zapisujeme do editoru zdrojového kódu Visual C++ 2008 Express. Editor zdrojového kódu je součástí integrovaného vývojového prostředí a jeho hlavním úkolem je pomoci nám s psaním programových příkazů jazyka C++. Zdrojový kód musí být vždy uložen v souboru, který nazýváme zdrojový nebo také implementační soubor. Zdrojový soubor jazyka C++ má implicitní příponu .cpp. V úvodních lekcích budeme pracovat pouze s jedním zdrojovým souborem, poněvadž veškerý kód, s nímž budeme nakládat, do něj umístíme bez jakýchkoliv potíží. Je však nutno podotknout, že jeden program (a tedy jeden projekt) může být složen z většího počtu zdrojových souborů. Zdrojové soubory nejsou jediným typem souborů, ve kterých se může nacházet zdrojový kód jazyka C++. Visual C++ 2008 Express a ve skutečnosti každý softwarový produkt, jenž obsahuje ISO-kompatibilní kompilátor jazyka C++, si rozumí s hlavičkovými soubory (to jsou soubory s extenzí .h). Podobně jako implementační soubory, i hlavičkové soubory smí sdružovat zdrojový kód jazyka C++. Kód uložený v hlavičkových souborech je však poněkud speciálního ražení a v tuto chvíli jej nebudeme blíže zkoumat.
Jakmile jsme zapsali zdrojový kód, můžeme vydat pokyn k jeho přeložení (kompilaci). Ve chvíli, kdy Visual C++ 2008 Express obdrží náš požadavek na překlad zdrojového kódu, povolá do práce preprocesor. Preprocesor je softwarový stroj (textový procesor), jehož úkolem je zpracovat zdrojový kód ještě předtím, než bude poskytnut překladači. Když si vzpomenete na náš první ukázkový program, jenž zobrazoval uvítací zprávu, zjistíte, že jsme mluvili o direktivě preprocesoru. Tato direktiva se jmenovala #include a stála na samém začátku programu. Direktiva preprocesoru působí jako směrnice, která ovlivňuje chování preprocesoru. Direktiv preprocesoru existuje více, my jsme se zatím seznámili pouze s #include. A jakýpak že má tato direktiva vliv na práci preprocesoru? Když preprocesor při skenování zdrojového kódu identifikuje direktivu #include, podívá se, jaký hlavičkový soubor tato direktiva uvádí. V našem případě se jedná o hlavičkový soubor iostream.h. Preprocesor načte uvedený hlavičkový soubor a jeho obsah nakopíruje na místo, které je označeno direktivou #include. Tím pádem se ve zdrojovém souboru budou nacházet deklarace důležitých entit, které zdrojový kód ke své plné funkčnosti vyžaduje (jde zejména o objekty cin a cout, jež jsou odpovědné za realizaci vstupně-výstupních datových operací). Preprocesor provádí také další činnosti, kupříkladu provádí textovou substituci symbolických konstant a rozvíjí makra, ovšem tyto prostředky jsme ještě nepoužili, takže od nich můžeme v tuto chvíli abstrahovat. Hlavní úloha preprocesoru spočívá v prvotním zpracování zdrojového textu programu. Preprocesor nahlíží na zdrojový kód programu doopravdy jako na pouhopouhý text, poněvadž se nijak nezabývá jeho syntaktickou ani sémantickou formou.
2. Kompilace předzpracovaného zdrojového kódu překladačem. Je-li preprocesor hotov, je aktivován překladač. Úkolem překladače je převést již předzpracovaný zdrojový kód programu jazyka C++ do tzv. objektového neboli relativního kódu. Překladač pracuje vždy s překladovými jednotkami kódu. Překladová jednotka je tvořena zdrojovým souborem a množinou začleněných hlavičkových souborů, které dotyčný zdrojový soubor používá. V případě ukázkového programu je překladovou jednotkou zdrojový soubor a hlavičkový soubor iostream.h. Překladač rozloží zdrojový kód na symboly a provede jejich lexikální, syntaktickou a sémantickou analýzu. Pomocí těchto typů analýz dovede překladač určit, zda je zdrojový kód zapsaný syntakticky správně a zda přítomné symboly mají korektní sémantiku. Mimochodem, zmíněné tři typy analýz jsme zmínili nejenom v pořadí, v němž jsou uskutečňovány, nýbrž také v posloupnosti „náročnosti“ jejich realizace z pohledu překladače. Pokud kompilátor neobjeví žádné lexikální nedostatky, syntaktické chyby či sémantické nejednoznačnosti, přistoupí ke generování mezikódu, na který ještě aplikuje optimalizační algoritmy. Cílem překladače je vygenerovat tak rychlý objektový kód, jak to jenom jde. Implicitně překladač upřednostňuje rychlost sestaveného kódu před jeho kapacitní náročností. Po optimalizaci je objektový kód odeslán do objektového (nebo též relativního) souboru s příponou .obj.
3. Sestavení přenositelného a spustitelného (PE) souboru programu pomocí spojovacího programu. Poslední stádium v procesu zpracování zdrojového kódu programu v jazyce C++ je vyhrazeno pro aktivaci spojovacího programu (linkeru), jehož úkolem je transformovat relativní kód z objektového souboru na absolutní kód, který vystupuje jako ryzí nativní, tedy strojový kód. Strojový kód je nakonec uložen do standardního přenositelného a spustitelného souboru s koncovkou .exe. Jde o tzv. PE soubor, což je zkratka z anglického slovního spojení Portable Executable.Máme-li PE soubor, můžeme jej spustit poklepáním na jeho ikonu (pokud se nacházíme v grafickém prostředí operačního systému), nebo zapsáním spouštěcího příkazu (jsme-li v příkazovém řádku). V další kapitole si vysvětlíme, jak probíhá exekuce (běh) programu připraveného v jazyce C++.
Proces běhu programu
jazyka C++
Ještě než přejdeme k podrobnému výkladu běhu programu jazyka C++, rádi bychom předeslali, že zde uvedené informace jsou platné pro jakýkoliv nativní program. Nemusí tedy jít vyloženě o program napsaný v C++, stejně jsou provedeny také programy zhotovené v jazycích C a Visual Basic 6.0 (s vyloučením tzv.
P-kódu).
Když operační systém (OS) obdrží požadavek na spuštění programu, alokuje pro něj jistý paměťový prostor, do něhož budou načteny všechny součásti spouštěného programu. Přidělenému paměťovému prostoru se říká fyzický proces. Fyzický proces je izolační primitivum operačního systému, poněvadž pomocí fyzických procesů může v OS běžet více aplikací paralelně. Když kupříkladu autor tohoto kurzu píše tyto řádky textu, kromě textového procesoru běží v OS také Visual C++ 2008 Express a další množství softwarových služeb na pozadí. Situování programu do fyzického procesu má zabránit tomu, aby se souběžně běžící programy vyrušovaly, nebo nedej bože nějakým jiným způsobem nepříznivě ovlivňovaly.
Do fyzického procesu je vloženo primární programové vlákno, které vystupuje jako hlavní exekuční kanál, na němž budou zpracovávány příkazy programu. Programové vlákno (někdy též podproces) je výkonnou jednotkou exekuce nativního programu. Na něm je totiž prováděn strojový kód aplikace. Dobře, ovšem odkud se tento strojový kód bere? Z PE souboru, ve kterém je uložen. Operační systém zabezpečí načtení příslušných fragmentů strojového kódu a poskytne je procesoru počítače ke přímému zpracování. Procesor (CPU) strojový kód rozložený na jednotlivé mikroinstrukce vykoná, čímž uskuteční to, co od něj požadujeme. Samozřejmě, z technického hlediska je celý proces zpracování strojového kódu procesorem daleko komplikovanější.
Procesor totiž sám určuje, jak bude strojový kód prováděn a kdy se tak stane. Procesor je zpravidla schopen zajistit optimální zpracování kódu, ovšem připustíme-li optimalizaci, pak musíme jedním dechem dodat, že CPU smí „zamíchat“ přicházejícími instrukcemi tak, aby byl výsledný běh programu co nejefektivnější. Pořadí příkazů zdrojového kódu a posloupnost zpracování příslušných strojových instrukcí procesorem se mohou lišit, ačkoliv tyto „magické hrátky“ nebudou mít na funkcionalitu programu z hlediska uživatele nikdy žádný vliv (jednodušeji řečeno, uživatel nic nepozná, protože program pracuje tak, jak má).
Každá aplikace, kterou v jazyce C++ vytvoříme, bude disponovat pouze jedním programovým vláknem, bude tudíž jednovláknová. Přestože pro naše potřeby bude jednovláknová aplikace bohatě postačovat, považujeme za vhodné zmínit, že v praktickém nasazení se s čistě jednovláknovými aplikacemi setkáme jenom ojediněle. Je to proto, že v daném okamžiku mohou být realizovány příkazy pouze na jednom vlákně. To se jeví jako omezující podmínka zejména tehdy, kdy je aplikace zatížena jistou výpočetně náročnou úlohou. Kdyby měla takováto aplikace jenom jedno programové vlákno, na němž by byla výpočetně náročná úloha zpracovávána, nezbyl by jí žádný prostor pro provádění dalších činností (jako je třeba reagování na pokyny uživatele). Proto se veliká zátěž zpravidla proporcionálně rozloží mezi více programových vláken s tím, že výpočetně náročný úkol převezme na svá bedra pracovní vlákno, zatímco primární vlákno nebude zatíženo a zůstane i nadále citlivé vůči uživatelským vstupům.
Datové typy a proměnné v jazyce C++
Počítače jsou deterministické stroje, které pracují s daty. Ačkoliv výsledný strojový kód ztělesňovaný soustavou nul a jedniček je snadno srozumitelný pro procesor počítače, my lidé potřebujeme pracovat na vyšší úrovni abstrakce. Data, s nimiž programy pracují, mohou být různého charakteru. Smí jít o celá čísla, desetinná čísla, logické hodnoty, textové znaky anebo textové řetězce. Abychom mohli na úrovni programovacího jazyka data vhodně klasifikovat, došlo k zavedení tzv. datových typů. Datový typ představuje abstraktní reprezentaci dat, proto se mu v informatice často říká abstraktní datový typ. V našich kurzech budeme častěji používat zkrácené spojení datový typ, ačkoliv původní definici budeme pochopitelně respektovat. Jazyková specifikace ISO standardu jazyka C++ rozděluje datové typy tohoto programovacího jazyka na dvě základní skupiny:
1. Primitivní datové typy. Primitivní datové typy jsou typy, jež jsou přímo vestavěny do jazyka C++. Řečeno jinými slovy, překladač tyto typy zná a když je ve zdrojovém kódu použijeme, dovede je okamžitě správně identifikovat. Primitivní datové typy se dále člení na celočíselné datové typy, reálné datové typy, znakové datové typy, logické datové typy a ukazatelové datové typy.
2. Uživatelsky deklarované datové typy (UDDT). Programátoři v jazyce C++ mohou kromě primitivních datových typů pracovat rovněž s dalšími datovými typy, které si sami vytvoří. Typy, jež jsou produktem algoritmického uvažování vývojářů, se nazývají uživatelsky deklarované datové typy. UDDT jsou zpravidla agregované datové typy, při jejichž stavbě se uplatňují primitivní datové typy.
Abychom dodrželi optimální linii výkladu, zaměříme se nejprve na primitivní datové typy jazyka C++. Přehled vybraných (čili pro nás nejdůležitějších) primitivních datových typů uvádíme v tabulce 1.
Každý z primitivních datových typů je pojmenován, přičemž název typu je klíčovým slovem jazyka C++. Klíčové slovo je rezervované slovo, které nemůžeme použít při pojmenování nějaké entity (kupříkladu proměnné, o které se zmíníme za chvíli). Podívejme se nyní na jednotlivé primitivní datové typy blíže.
Logický datový typ bool (jehož název je odvozen ze jména britského matematika Georgea Boolea) slouží k interpretaci logických hodnot v standardní dvoustavové Boolově logice. Těmito hodnotami jsou logická pravda reprezentována klíčovým slovem true a logická nepravda, jíž přisluší klíčové slovo false.
Další čtyři datové typy, jmenovitě char, short int, int a long int, patří do kategorie celočíselných, resp. integrálních datových typů. Všechny tyto typy reprezentují celočíselné hodnoty, ať už kladné nebo záporné, a to ve vymezeném rozsahu. Na 32bitových platformách, které jsou tvořeny 32bitovými operačními systémy a procesory, jsou operace nejefektivněji realizovány s hodnotami datového typu int.
Typ int je nejpoužívanějším primitivním celočíselným datovým typem s dostatečným rozsahem. Ve skutečnosti může datový typ int vyjadřovat až 232–1 možných celočíselných hodnot, což představuje takřka 4,3 miliardy různých čísel. V situacích, kdy bude pro nás rozsah typu int příliš objemný, můžeme sáhnout po typu short int, nebo dokonce po typu char. Typ short int slouží pro reprezentaci krátkých celých čísel, zatímco typ char se zpravidla užívá při práci s textovými znaky. Nejméně tak rozsáhlý, jako je typ int, je rovněž typ long int.
Jazyk C++ obsahuje dva primitivní reálné datové typy: float a double. Oba typy se uplatňují při interpretaci reálných (desetinných) hodnot, které kromě integrální části disponují také reálným rozvojem. Mezi typy float a double existují dva zásadní rozdíly. Prvním rozdílem je přesnost: typ float pracuje pouze se sedmi signifikantními ciframi za desetinným oddělovačem, což odpovídá jednoduché přesnosti. Na druhou stranu, typ double se může pochlubit dvojitou přesností, jež je dána významností 15 číslic za desetinným oddělovačem, které jsou brány v potaz. Druhý rozdíl spočívá v rozsahu obou typů: double je předurčen pro vyjádření neporovnatelně objemnějších dat.
Datové typy ovšem nedeterminují pouze typ a rozsah dat, s nimiž mohou programátoři pracovat. Stanovují také množinu operací, jež mohou být s kýženými daty prováděny. Kupříkladu celá čísla mohou být sčítána, odčítána, násobena nebo dělena – všechny tyto matematické operace musí implementovat také datové typy.
Pomocí datových typů umíme charakterizovat data, s nimiž budeme v našich programech manipulovat. Nicméně potřebujeme také něco, do čeho budeme data ukládat. Když budeme chtít vypočítat hmotnostní index BMI jisté osoby, budeme potřebovat data charakterizující její výšku a hmotnost. Tyto údaje musíme někam uložit, poněvadž je budeme potřebovat při dalších výpočtech. Datové kontejnery, jejichž primárním smyslem je uskladnění dat reprezentovaných jistými datovými typy, se v programování nazývají proměnné.
Proměnná je datový objekt, který po svém vytvoření existuje v operační paměti počítače. Proměnná má v informatice jiný význam než v matematice. V matematice jste se s proměnnými setkali zejména při řešení rovnic a jejich soustav. Budeme-li chtít vypočítat kvadratickou rovnici, jejíž standardní tvar je ax2 + bx + c = 0, pak bude naším zájmem zjistit všechny její kořeny. Jinými slovy, potřebujeme najít všechny reálné hodnoty, jejichž dosazením do proměnné x získáme platnou rovnici. V informatice je proměnná vždy chápana jako datový objekt, jenž je schopen pojmout hodnoty určitého datového typu. Přitom platí, že v daný okamžik se může v proměnné nacházet právě jedna hodnota.
Každá proměnná má svůj identifikátor (název) a datový typ. Proměnná se v jazyce C++ vytváří definičním příkazem. Všeobecná podoba definičního příkazu zní:
kde T je datový typ proměnné a I je identifikátor proměnné.
Pokud budeme chtít definovat celočíselnou proměnnou X typu int, pak do těla hlavní funkce main zapíšeme příkaz:
Zkráceně se definiční příkaz nazývá definice proměnné. Když je zpracován definiční příkaz proměnné, dochází k jejímu vytvoření: proměnné je v operační paměti přidělen paměťový blok o jisté alokační kapacitě. Alokační kapacita proměnné říká, kolik bajtů (B) v paměti proměnná zabírá. Alokační kapacita proměnné je závislá na datovém typu, jenž byl specifikován při definici proměnné. V případě proměnné X je aplikován typ int, z čehož plyne, že alokační kapacita této celočíselné proměnné je 4 bajty (32 bitů : 8 bitů = 4 bajty). Proměnná může mít libovolný z již charakterizovaných primitivních datových typů. Datový typ proměnné volí programátor podle rozsahu hodnot, které hodlá do proměnné ukládat. Kromě typu určuje vývojář také identifikátor proměnné, jenž musí vyhovovat následujícím požadavkům:
q Nesmí začínat číslicí.
q Nesmí obsahovat mezeru.
q
Nesmí jím být klíčové slovo jazyka C++.
q
Nesmí se shodovat s identifikátorem jiné dříve definované proměnné (za těchto okolností by šlo o redefinici proměnné, kterou by kompilátor označil jako chybnou).
Název proměnné může obsahovat podtržítko (_). To se užívá zejména při definici proměnných, u nichž má být identifikátor tvořen více slovy. Kupříkladu hmotnost_osoby je platný název proměnné. Mnozí programátoři si zvykli na tzv. velbloudí notaci, kterou dodržují při pojmenovávání proměnných, jejichž názvy jsou víceslovné. Předcházející proměnná by se pak jmenovala hmotnostOsoby. Velbloudí notace říká, že začáteční písmeno prvního slova víceslovného názvu je psáno malým písmenem, zatímco začátečná písmena všech ostatních slov víceslovného názvu jsou již psána velkými písmeny (třeba hmotnostTestovaneOsoby).
Na závěr ještě zmíníme skutečnost, že v jazyce C++ se bere v potaz velikost znaků, jež tvoří identifikátory. To znamená, že na proměnné s názvy indexBMI a IndexBMI nahlíží C++ jako na rozdílné proměnné (byť se jejich identifikátory liší pouze v prvním písmenu). Bude dobré, když si toto pravidlo dobře zapamatujete, protože se nevztahuje pouze na jména proměnných, ale na identifikátory všech entit, s nimiž při programování přijdete do styku.